Browse Source

初始提交

ruanyuxuan 8 months ago
parent
commit
c56d4cdbdc
100 changed files with 20769 additions and 0 deletions
  1. 10 0
      .bowerrc
  2. 11 0
      .env
  3. 191 0
      LICENSE
  4. 1 0
      addons/alioss/.addonrc
  5. 124 0
      addons/alioss/Alioss.php
  6. 0 0
      addons/alioss/assets/js/spark.js
  7. 245 0
      addons/alioss/bootstrap.js
  8. 282 0
      addons/alioss/config.php
  9. 272 0
      addons/alioss/controller/Index.php
  10. 10 0
      addons/alioss/info.ini
  11. 78 0
      addons/alioss/library/Auth.php
  12. 262 0
      addons/alioss/library/OSS/Core/MimeTypes.php
  13. 54 0
      addons/alioss/library/OSS/Core/OssException.php
  14. 461 0
      addons/alioss/library/OSS/Core/OssUtil.php
  15. 25 0
      addons/alioss/library/OSS/Http/LICENSE
  16. 896 0
      addons/alioss/library/OSS/Http/RequestCore.php
  17. 8 0
      addons/alioss/library/OSS/Http/RequestCore_Exception.php
  18. 56 0
      addons/alioss/library/OSS/Http/ResponseCore.php
  19. 78 0
      addons/alioss/library/OSS/Model/BucketInfo.php
  20. 39 0
      addons/alioss/library/OSS/Model/BucketListInfo.php
  21. 99 0
      addons/alioss/library/OSS/Model/CnameConfig.php
  22. 113 0
      addons/alioss/library/OSS/Model/CorsConfig.php
  23. 150 0
      addons/alioss/library/OSS/Model/CorsRule.php
  24. 34 0
      addons/alioss/library/OSS/Model/GetLiveChannelHistory.php
  25. 68 0
      addons/alioss/library/OSS/Model/GetLiveChannelInfo.php
  26. 107 0
      addons/alioss/library/OSS/Model/GetLiveChannelStatus.php
  27. 88 0
      addons/alioss/library/OSS/Model/LifecycleAction.php
  28. 107 0
      addons/alioss/library/OSS/Model/LifecycleConfig.php
  29. 126 0
      addons/alioss/library/OSS/Model/LifecycleRule.php
  30. 134 0
      addons/alioss/library/OSS/Model/ListMultipartUploadInfo.php
  31. 97 0
      addons/alioss/library/OSS/Model/ListPartsInfo.php
  32. 121 0
      addons/alioss/library/OSS/Model/LiveChannelConfig.php
  33. 59 0
      addons/alioss/library/OSS/Model/LiveChannelHistory.php
  34. 107 0
      addons/alioss/library/OSS/Model/LiveChannelInfo.php
  35. 107 0
      addons/alioss/library/OSS/Model/LiveChannelListInfo.php
  36. 86 0
      addons/alioss/library/OSS/Model/LoggingConfig.php
  37. 93 0
      addons/alioss/library/OSS/Model/ObjectInfo.php
  38. 126 0
      addons/alioss/library/OSS/Model/ObjectListInfo.php
  39. 63 0
      addons/alioss/library/OSS/Model/PartInfo.php
  40. 36 0
      addons/alioss/library/OSS/Model/PrefixInfo.php
  41. 93 0
      addons/alioss/library/OSS/Model/RefererConfig.php
  42. 74 0
      addons/alioss/library/OSS/Model/StorageCapacityConfig.php
  43. 55 0
      addons/alioss/library/OSS/Model/UploadInfo.php
  44. 76 0
      addons/alioss/library/OSS/Model/WebsiteConfig.php
  45. 27 0
      addons/alioss/library/OSS/Model/XmlConfig.php
  46. 2739 0
      addons/alioss/library/OSS/OssClient.php
  47. 32 0
      addons/alioss/library/OSS/Result/AclResult.php
  48. 27 0
      addons/alioss/library/OSS/Result/AppendResult.php
  49. 19 0
      addons/alioss/library/OSS/Result/BodyResult.php
  50. 21 0
      addons/alioss/library/OSS/Result/CallbackResult.php
  51. 30 0
      addons/alioss/library/OSS/Result/CopyObjectResult.php
  52. 27 0
      addons/alioss/library/OSS/Result/DeleteObjectsResult.php
  53. 35 0
      addons/alioss/library/OSS/Result/ExistResult.php
  54. 19 0
      addons/alioss/library/OSS/Result/GetCnameResult.php
  55. 35 0
      addons/alioss/library/OSS/Result/GetCorsResult.php
  56. 41 0
      addons/alioss/library/OSS/Result/GetLifecycleResult.php
  57. 19 0
      addons/alioss/library/OSS/Result/GetLiveChannelHistoryResult.php
  58. 19 0
      addons/alioss/library/OSS/Result/GetLiveChannelInfoResult.php
  59. 19 0
      addons/alioss/library/OSS/Result/GetLiveChannelStatusResult.php
  60. 30 0
      addons/alioss/library/OSS/Result/GetLocationResult.php
  61. 41 0
      addons/alioss/library/OSS/Result/GetLoggingResult.php
  62. 41 0
      addons/alioss/library/OSS/Result/GetRefererResult.php
  63. 34 0
      addons/alioss/library/OSS/Result/GetStorageCapacityResult.php
  64. 40 0
      addons/alioss/library/OSS/Result/GetWebsiteResult.php
  65. 23 0
      addons/alioss/library/OSS/Result/HeaderResult.php
  66. 29 0
      addons/alioss/library/OSS/Result/InitiateMultipartUploadResult.php
  67. 33 0
      addons/alioss/library/OSS/Result/ListBucketsResult.php
  68. 16 0
      addons/alioss/library/OSS/Result/ListLiveChannelResult.php
  69. 55 0
      addons/alioss/library/OSS/Result/ListMultipartUploadResult.php
  70. 71 0
      addons/alioss/library/OSS/Result/ListObjectsResult.php
  71. 42 0
      addons/alioss/library/OSS/Result/ListPartsResult.php
  72. 16 0
      addons/alioss/library/OSS/Result/PutLiveChannelResult.php
  73. 20 0
      addons/alioss/library/OSS/Result/PutSetDeleteResult.php
  74. 175 0
      addons/alioss/library/OSS/Result/Result.php
  75. 24 0
      addons/alioss/library/OSS/Result/SymlinkResult.php
  76. 28 0
      addons/alioss/library/OSS/Result/UploadPartResult.php
  77. 1 0
      addons/simditor/.addonrc
  78. 31 0
      addons/simditor/Simditor.php
  79. 0 0
      addons/simditor/assets/css/simditor.min.css
  80. BIN
      addons/simditor/assets/images/image.png
  81. 0 0
      addons/simditor/assets/js/simditor.min.js
  82. 48 0
      addons/simditor/bootstrap.js
  83. 4 0
      addons/simditor/build/build.sh
  84. 4 0
      addons/simditor/build/css.js
  85. 10 0
      addons/simditor/build/js.js
  86. 4699 0
      addons/simditor/build/r.js
  87. 4 0
      addons/simditor/config.php
  88. 10 0
      addons/simditor/info.ini
  89. 460 0
      addons/simditor/src/css/mobile.css
  90. 2 0
      addons/simditor/src/css/simditor.css
  91. BIN
      addons/simditor/src/images/image.png
  92. 241 0
      addons/simditor/src/js/hotkeys.js
  93. 172 0
      addons/simditor/src/js/module.js
  94. 5641 0
      addons/simditor/src/js/simditor.js
  95. 261 0
      addons/simditor/src/js/uploader.js
  96. 1 0
      addons/summernote/.addonrc
  97. 31 0
      addons/summernote/Summernote.php
  98. 0 0
      addons/summernote/assets/css/summernote.css
  99. BIN
      addons/summernote/assets/font/summernote.eot
  100. BIN
      addons/summernote/assets/font/summernote.ttf

+ 10 - 0
.bowerrc

@@ -0,0 +1,10 @@
+{
+  "directory": "public/assets/libs",
+  "ignoredDependencies": [
+    "es6-promise",
+    "file-saver",
+    "html2canvas",
+    "jspdf",
+    "jspdf-autotable"
+  ]
+}

+ 11 - 0
.env

@@ -0,0 +1,11 @@
+[app]
+debug = false
+trace = false
+
+[database]
+hostname = 127.0.0.1
+database = ts_shanghairongx
+username = ts_shanghairongx
+password = eMmGtCB4M2tSXWBY
+hostport = 3306
+prefix = fa_

+ 191 - 0
LICENSE

@@ -0,0 +1,191 @@
+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:
+
+You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+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
+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 2017 Karson
+
+   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.

+ 1 - 0
addons/alioss/.addonrc

@@ -0,0 +1 @@
+{"files":[],"license":"extended","licenseto":"65832","licensekey":"s7jZuIXYRFvaU4AC KARsBK3StwQPQq+4tn4ebA==","domains":[],"licensecodes":[],"validations":[]}

+ 124 - 0
addons/alioss/Alioss.php

@@ -0,0 +1,124 @@
+<?php
+
+namespace addons\alioss;
+
+use OSS\Core\OssException;
+use OSS\OssClient;
+use think\Addons;
+use think\App;
+use think\Config;
+use think\Loader;
+
+/**
+ * 阿里云OSS上传插件
+ */
+class Alioss extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        return true;
+    }
+
+    /**
+     * 添加命名空间
+     */
+    public function appInit()
+    {
+        if (!class_exists("\OSS\OssClient")) {
+            //添加支付包的命名空间
+            Loader::addNamespace('OSS', ADDON_PATH . 'alioss' . DS . 'library' . DS . 'OSS' . DS);
+        }
+    }
+
+    /**
+     * 判断是否来源于API上传
+     */
+    public function moduleInit($request)
+    {
+        $config = $this->getConfig();
+        $module = strtolower($request->module());
+        if ($module == 'api' && ($config['apiupload'] ?? 0) &&
+            strtolower($request->controller()) == 'common' &&
+            strtolower($request->action()) == 'upload') {
+            request()->param('isApi', true);
+            App::invokeMethod(["\\addons\\alioss\\controller\\Index", "upload"], ['isApi' => true]);
+        }
+    }
+
+    /**
+     * 加载配置
+     */
+    public function uploadConfigInit(&$upload)
+    {
+        $config = $this->getConfig();
+        $module = request()->module();
+        $module = $module ? strtolower($module) : 'index';
+
+        $data = ['deadline' => time() + $config['expire']];
+        $signature = hash_hmac('sha1', json_encode($data), $config['accessKeySecret'], true);
+
+        $token = '';
+
+        $noNeedLogin = array_filter(explode(',', $config['noneedlogin'] ?? ''));
+        if (in_array($module, $noNeedLogin) || ($module == 'admin' && \app\admin\library\Auth::instance()->id) || ($module != 'admin' && \app\common\library\Auth::instance()->id)) {
+            $token = $config['accessKeyId'] . ':' . base64_encode($signature) . ':' . base64_encode(json_encode($data));
+        }
+        $multipart = [
+            'aliosstoken' => $token
+        ];
+        $config['uploadurl'] = 'https://' . $config['bucket'] . '.' . $config['endpoint'];
+        $upload = array_merge($upload, [
+            'cdnurl'     => $config['cdnurl'],
+            'uploadurl'  => $config['uploadmode'] == 'client' ? $config['uploadurl'] : addon_url('alioss/index/upload', [], false, true),
+            'uploadmode' => $config['uploadmode'],
+            'bucket'     => $config['bucket'],
+            'maxsize'    => $config['maxsize'],
+            'mimetype'   => $config['mimetype'],
+            'savekey'    => $config['savekey'],
+            'chunking'   => (bool)($config['chunking'] ?? $upload['chunking']),
+            'chunksize'  => (int)($config['chunksize'] ?? $upload['chunksize']),
+            'multipart'  => $multipart,
+            'storage'    => $this->getName(),
+            'multiple'   => $config['multiple'] ? true : false,
+        ]);
+    }
+
+    /**
+     * 附件删除后
+     */
+    public function uploadDelete($attachment)
+    {
+        $config = $this->getConfig();
+        if ($attachment['storage'] == 'alioss' && isset($config['syncdelete']) && $config['syncdelete']) {
+            try {
+                $ossClient = new OssClient($config['accessKeyId'], $config['accessKeySecret'], $config['endpoint']);
+                $ossClient->deleteObject($config['bucket'], ltrim($attachment->url, '/'));
+            } catch (OssException $e) {
+                return false;
+            }
+            //如果是服务端中转,还需要删除本地文件
+            //if ($config['uploadmode'] == 'server') {
+            //    $filePath = ROOT_PATH . 'public' . str_replace('/', DS, $attachment->url);
+            //    if ($filePath) {
+            //        @unlink($filePath);
+            //    }
+            //}
+        }
+        return true;
+    }
+
+}

File diff suppressed because it is too large
+ 0 - 0
addons/alioss/assets/js/spark.js


+ 245 - 0
addons/alioss/bootstrap.js

@@ -0,0 +1,245 @@
+if (typeof Config.upload.storage !== 'undefined' && Config.upload.storage === 'alioss') {
+    require(['upload'], function (Upload) {
+        //获取文件MD5值
+        var getFileMd5 = function (file, cb) {
+            //如果savekey中未检测到md5,则无需获取文件md5,直接返回upload的uuid
+            if (!Config.upload.savekey.match(/\{(file)?md5\}/)) {
+                cb && cb(file.upload.uuid);
+                return;
+            }
+            require(['../addons/alioss/js/spark'], function (SparkMD5) {
+                var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
+                    chunkSize = 10 * 1024 * 1024,
+                    chunks = Math.ceil(file.size / chunkSize),
+                    currentChunk = 0,
+                    spark = new SparkMD5.ArrayBuffer(),
+                    fileReader = new FileReader();
+
+                fileReader.onload = function (e) {
+                    spark.append(e.target.result);
+                    currentChunk++;
+                    if (currentChunk < chunks) {
+                        loadNext();
+                    } else {
+                        cb && cb(spark.end());
+                    }
+                };
+
+                fileReader.onerror = function () {
+                    console.warn('文件读取错误');
+                };
+
+                function loadNext() {
+                    var start = currentChunk * chunkSize,
+                        end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
+
+                    fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
+                }
+
+                loadNext();
+
+            });
+        };
+
+        var _onInit = Upload.events.onInit;
+        //初始化中完成判断
+        Upload.events.onInit = function () {
+            _onInit.apply(this, Array.prototype.slice.apply(arguments));
+            //如果上传接口不是阿里OSS,则不处理
+            if (this.options.url !== Config.upload.uploadurl) {
+                return;
+            }
+            $.extend(this.options, {
+                //关闭自动处理队列功能
+                autoQueue: false,
+                params: function (files, xhr, chunk) {
+                    var params = Config.upload.multipart;
+                    if (chunk) {
+                        return $.extend({}, params, {
+                            filesize: chunk.file.size,
+                            filename: chunk.file.name,
+                            chunkid: chunk.file.upload.uuid,
+                            chunkindex: chunk.index,
+                            chunkcount: chunk.file.upload.totalChunkCount,
+                            chunksize: this.options.chunkSize,
+                            chunkfilesize: chunk.dataBlock.data.size,
+                            width: chunk.file.width || 0,
+                            height: chunk.file.height || 0,
+                            type: chunk.file.type,
+                            uploadId: chunk.file.uploadId,
+                            key: chunk.file.key,
+                        });
+                    } else {
+                        params = $.extend({}, params, files[0].params);
+                        params.category = files[0].category || '';
+                    }
+                    return params;
+                },
+                chunkSuccess: function (chunk, file, response) {
+                    var etag = chunk.xhr.getResponseHeader("ETag").replace(/(^")|("$)/g, '');
+                    file.etags = file.etags ? file.etags : [];
+                    file.etags[chunk.index] = etag;
+                },
+                chunksUploaded: function (file, done) {
+                    var that = this;
+                    Fast.api.ajax({
+                        url: "/addons/alioss/index/upload",
+                        data: {
+                            action: 'merge',
+                            filesize: file.size,
+                            filename: file.name,
+                            chunkid: file.upload.uuid,
+                            chunkcount: file.upload.totalChunkCount,
+                            md5: file.md5,
+                            key: file.key,
+                            uploadId: file.uploadId,
+                            etags: file.etags,
+                            category: file.category || '',
+                            aliosstoken: Config.upload.multipart.aliosstoken,
+                        },
+                    }, function (data, ret) {
+                        done(JSON.stringify(ret));
+                        return false;
+                    }, function (data, ret) {
+                        file.accepted = false;
+                        that._errorProcessing([file], ret.msg);
+                        return false;
+                    });
+
+                },
+            });
+
+            var _success = this.options.success;
+            //先移除已有的事件
+            this.off("success", _success).on("success", function (file, response) {
+                var ret = {code: 0, msg: response};
+                try {
+                    if (response) {
+                        ret = typeof response === 'string' ? JSON.parse(response) : response;
+                    }
+                    if (file.xhr.status === 200) {
+                        if (Config.upload.uploadmode === 'client') {
+                            ret = {code: 1, data: {url: '/' + file.key}};
+                        }
+                        if (ret.code == 1) {
+                            var url = ret.data.url || '';
+
+                            Fast.api.ajax({
+                                url: "/addons/alioss/index/notify",
+                                data: {name: file.name, url: url, md5: file.md5, size: file.size, width: file.width || 0, height: file.height || 0, type: file.type, category: file.category || '', aliosstoken: Config.upload.multipart.aliosstoken}
+                            }, function () {
+                                return false;
+                            }, function () {
+                                return false;
+                            });
+                        } else {
+                            console.error(ret);
+                        }
+                    } else {
+                        console.error(file.xhr);
+                    }
+                } catch (e) {
+                    console.error(e);
+                }
+                _success.call(this, file, ret);
+            });
+
+            this.on("addedfile", function (file) {
+                var that = this;
+                setTimeout(function () {
+                    if (file.status === 'error') {
+                        return;
+                    }
+                    getFileMd5(file, function (md5) {
+                        var chunk = that.options.chunking && file.size > that.options.chunkSize ? 1 : 0;
+                        var params = $(that.element).data("params") || {};
+                        var category = typeof params.category !== 'undefined' ? params.category : ($(that.element).data("category") || '');
+                        category = typeof category === 'function' ? category.call(that, file) : category;
+                        Fast.api.ajax({
+                            url: "/addons/alioss/index/params",
+                            data: {method: 'POST', category: category, md5: md5, name: file.name, type: file.type, size: file.size, chunk: chunk, chunksize: that.options.chunkSize, aliosstoken: Config.upload.multipart.aliosstoken},
+                        }, function (data) {
+                            file.md5 = md5;
+                            file.id = data.id;
+                            file.key = data.key;
+                            file.date = data.date;
+                            file.uploadId = data.uploadId;
+                            file.policy = data.policy;
+                            file.signature = data.signature;
+                            file.partsAuthorization = data.partsAuthorization;
+                            file.params = data;
+                            file.category = category;
+
+                            if (file.status != 'error') {
+                                //开始上传
+                                that.enqueueFile(file);
+                            } else {
+                                that.removeFile(file);
+                            }
+                            return false;
+                        }, function () {
+                            that.removeFile(file);
+                        });
+                    });
+                }, 0);
+            });
+
+            if (Config.upload.uploadmode === 'client') {
+                var _method = this.options.method;
+                var _url = this.options.url;
+                this.options.method = function (files) {
+                    if (files[0].upload.chunked) {
+                        var chunk = null;
+                        files[0].upload.chunks.forEach(function (item) {
+                            if (item.status === 'uploading') {
+                                chunk = item;
+                            }
+                        });
+                        if (!chunk) {
+                            return "POST";
+                        } else {
+                            return "PUT";
+                        }
+                    }
+                    return _method;
+                };
+                this.options.url = function (files) {
+                    if (files[0].upload.chunked) {
+                        var chunk = null;
+                        files[0].upload.chunks.forEach(function (item) {
+                            if (item.status === 'uploading') {
+                                chunk = item;
+                            }
+                        });
+                        var index = chunk.dataBlock.chunkIndex;
+                        // debugger;
+                        this.options.headers = {"Authorization": "OSS " + files[0]['id'] + ":" + files[0]['partsAuthorization'][index], "x-oss-date": files[0]['date']};
+                        if (!chunk) {
+                            return Config.upload.uploadurl + "/" + files[0].key + "?uploadId=" + files[0].uploadId;
+                        } else {
+                            return Config.upload.uploadurl + "/" + files[0].key + "?partNumber=" + (index + 1) + "&uploadId=" + files[0].uploadId;
+                        }
+                    }
+                    return _url;
+                };
+                this.on("sending", function (file, xhr, formData) {
+                    var that = this;
+                    if (file.upload.chunked) {
+                        var _send = xhr.send;
+                        xhr.send = function () {
+                            var chunk = null;
+                            file.upload.chunks.forEach(function (item) {
+                                if (item.status == 'uploading') {
+                                    chunk = item;
+                                }
+                            });
+                            if (chunk) {
+                                _send.call(xhr, chunk.dataBlock.data);
+                            }
+                        };
+                    }
+                });
+            }
+        };
+    });
+}

+ 282 - 0
addons/alioss/config.php

@@ -0,0 +1,282 @@
+<?php
+
+return array (
+  0 => 
+  array (
+    'name' => 'accessKeyId',
+    'title' => 'AccessKey ID',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => 'LTAIH2H5ow3XxjX6',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  1 => 
+  array (
+    'name' => 'accessKeySecret',
+    'title' => 'AccessKey Secret',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => '47OgCZgItbW9SrkhyUeIz9Quene4ww',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  2 => 
+  array (
+    'name' => 'bucket',
+    'title' => 'Bucket名称',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => '6dbcn',
+    'rule' => 'required;bucket',
+    'msg' => '',
+    'tip' => '阿里云OSS的空间名',
+    'ok' => '',
+    'extend' => 'data-rule-bucket="[/^[0-9a-z_\\-]{3,63}$/, \'请输入正确的Bucket名称\']"',
+  ),
+  3 => 
+  array (
+    'name' => 'endpoint',
+    'title' => 'Endpoint',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => 'oss-cn-beijing.aliyuncs.com',
+    'rule' => 'required;endpoint',
+    'msg' => '',
+    'tip' => '请填写从阿里云存储获取的Endpoint',
+    'ok' => '',
+    'extend' => 'data-rule-endpoint="[/^(?!http(s)?:\\/\\/).*$/, \'不能以http(s)://开头\']"',
+  ),
+  4 => 
+  array (
+    'name' => 'cdnurl',
+    'title' => 'CDN地址',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => 'https://6dbcn.oss-cn-beijing.aliyuncs.com',
+    'rule' => 'required;cdnurl',
+    'msg' => '',
+    'tip' => '请填写CDN地址,必须以http(s)://开头',
+    'ok' => '',
+    'extend' => 'data-rule-cdnurl="[/^http(s)?:\\/\\/.*$/, \'必需以http(s)://开头\']"',
+  ),
+  5 => 
+  array (
+    'name' => 'uploadmode',
+    'title' => '上传模式',
+    'type' => 'select',
+    'content' => 
+    array (
+      'server' => '服务器中转(占用服务器带宽,可备份)',
+    ),
+    'value' => 'server',
+    'rule' => '',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  6 => 
+  array (
+    'name' => 'serverbackup',
+    'title' => '服务器中转模式备份',
+    'type' => 'radio',
+    'content' => 
+    array (
+      1 => '备份(附件管理将产生2条记录)',
+      0 => '不备份',
+    ),
+    'value' => '0',
+    'rule' => '',
+    'msg' => '',
+    'tip' => '服务器中转模式下是否备份文件',
+    'ok' => '',
+    'extend' => '',
+  ),
+  7 => 
+  array (
+    'name' => 'savekey',
+    'title' => '保存文件名',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => '/uploads/{year}{mon}{day}/{filemd5}{.suffix}',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  8 => 
+  array (
+    'name' => 'expire',
+    'title' => '上传有效时长',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => '600',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '用户停留页面上传有效时长,单位秒',
+    'ok' => '',
+    'extend' => '',
+  ),
+  9 => 
+  array (
+    'name' => 'maxsize',
+    'title' => '最大可上传',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => '10M',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  10 => 
+  array (
+    'name' => 'mimetype',
+    'title' => '可上传后缀格式',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => 'jpg,png,bmp,jpeg,gif,zip,rar,xls,xlsx,mp4',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  11 => 
+  array (
+    'name' => 'multiple',
+    'title' => '多文件上传',
+    'type' => 'bool',
+    'content' => 
+    array (
+    ),
+    'value' => '0',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  12 => 
+  array (
+    'name' => 'thumbstyle',
+    'title' => '缩略图样式',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => '',
+    'rule' => '',
+    'msg' => '',
+    'tip' => '用于后台列表缩略图样式,可使用:?x-oss-process=image/resize,m_lfit,w_120,h_90',
+    'ok' => '',
+    'extend' => '',
+  ),
+  13 => 
+  array (
+    'name' => 'chunking',
+    'title' => '分片上传',
+    'type' => 'radio',
+    'content' => 
+    array (
+      1 => '开启',
+      0 => '关闭',
+    ),
+    'value' => '0',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  14 => 
+  array (
+    'name' => 'chunksize',
+    'title' => '分片大小',
+    'type' => 'number',
+    'content' => 
+    array (
+    ),
+    'value' => '4194304',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  15 => 
+  array (
+    'name' => 'syncdelete',
+    'title' => '附件删除时是否同步删除文件',
+    'type' => 'bool',
+    'content' => 
+    array (
+    ),
+    'value' => '1',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  16 => 
+  array (
+    'name' => 'apiupload',
+    'title' => 'API接口使用云存储',
+    'type' => 'bool',
+    'content' => 
+    array (
+    ),
+    'value' => '0',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  17 => 
+  array (
+    'name' => 'noneedlogin',
+    'title' => '免登录上传',
+    'type' => 'checkbox',
+    'content' => 
+    array (
+      'api' => 'API',
+      'index' => '前台',
+      'admin' => '后台',
+    ),
+    'value' => '',
+    'rule' => '',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+);

+ 272 - 0
addons/alioss/controller/Index.php

@@ -0,0 +1,272 @@
+<?php
+
+namespace addons\alioss\controller;
+
+use app\common\exception\UploadException;
+use app\common\library\Upload;
+use app\common\model\Attachment;
+use OSS\Core\OssException;
+use OSS\OssClient;
+use think\addons\Controller;
+use think\Config;
+
+/**
+ * 阿里OSS云储存
+ *
+ */
+class Index extends Controller
+{
+
+    public function _initialize()
+    {
+        //跨域检测
+        //check_cors_request();
+
+        parent::_initialize();
+        Config::set('default_return_type', 'json');
+    }
+
+    public function index()
+    {
+        Config::set('default_return_type', 'html');
+        $this->error("当前插件暂无前台页面");
+    }
+
+    /**
+     * 获取签名
+     */
+    public function params()
+    {
+        $this->check();
+        $name = $this->request->post('name');
+        $md5 = $this->request->post('md5');
+        $chunk = $this->request->post('chunk');
+
+        $auth = new \addons\alioss\library\Auth();
+        $params = $auth->params($name, $md5);
+        $params['OSSAccessKeyId'] = $params['id'];
+        $params['success_action_status'] = 200;
+        $config = get_addon_config('alioss');
+
+        if ($chunk) {
+            $oss = new OssClient($config['accessKeyId'], $config['accessKeySecret'], $config['endpoint']);
+            // 初始化
+            $fileSize = $this->request->post('size');
+            $chunkSize = $this->request->post('chunksize');
+            $uploadId = $oss->initiateMultipartUpload($config['bucket'], $params['key']);
+            $params['uploadId'] = $uploadId;
+            $params['parts'] = $oss->generateMultiuploadParts($fileSize, $chunkSize);
+            $params['partsAuthorization'] = [];
+            $date = gmdate('D, d M Y H:i:s \G\M\T');
+            foreach ($params['parts'] as $index => $part) {
+                $partNumber = $index + 1;
+                $signstr = "PUT\n\n\n{$date}\nx-oss-date:{$date}\n/{$config['bucket']}/{$params['key']}?partNumber={$partNumber}&uploadId={$uploadId}";
+                $authorization = base64_encode(hash_hmac('sha1', $signstr, $config['accessKeySecret'], true));
+                $params['partsAuthorization'][$index] = $authorization;
+            }
+            $params['date'] = $date;
+        }
+
+        $this->success('', null, $params);
+        return;
+    }
+
+    /**
+     * 服务器中转上传文件
+     * 上传分片
+     * 合并分片
+     * @param bool $isApi
+     */
+    public function upload($isApi = false)
+    {
+        if ($isApi === true) {
+            if (!$this->auth->isLogin()) {
+                $this->error("请登录后再进行操作");
+            }
+        } else {
+            $this->check();
+        }
+        $config = get_addon_config('alioss');
+        $oss = new OssClient($config['accessKeyId'], $config['accessKeySecret'], $config['endpoint']);
+
+        //检测删除文件或附件
+        $checkDeleteFile = function ($attachment, $upload, $force = false) use ($config) {
+            //如果设定为不备份则删除文件和记录 或 强制删除
+            if ((isset($config['serverbackup']) && !$config['serverbackup']) || $force) {
+                if ($attachment && !empty($attachment['id'])) {
+                    $attachment->delete();
+                }
+                if ($upload) {
+                    //文件绝对路径
+                    $filePath = $upload->getFile()->getRealPath() ?: $upload->getFile()->getPathname();
+                    @unlink($filePath);
+                }
+            }
+        };
+
+        $chunkid = $this->request->post("chunkid");
+        if ($chunkid) {
+            $action = $this->request->post("action");
+            $chunkindex = $this->request->post("chunkindex/d");
+            $chunkcount = $this->request->post("chunkcount/d");
+            $filesize = $this->request->post("filesize");
+            $filename = $this->request->post("filename");
+            $method = $this->request->method(true);
+            $key = $this->request->post("key");
+            $uploadId = $this->request->post("uploadId");
+
+            if ($action == 'merge') {
+                $attachment = null;
+                $upload = null;
+                //合并分片
+                if ($config['uploadmode'] == 'server') {
+                    //合并分片文件
+                    try {
+                        $upload = new Upload();
+                        $attachment = $upload->merge($chunkid, $chunkcount, $filename);
+                    } catch (UploadException $e) {
+                        $this->error($e->getMessage());
+                    }
+                }
+
+                $etags = $this->request->post("etags/a", []);
+                if (count($etags) != $chunkcount) {
+                    $checkDeleteFile($attachment, $upload, true);
+                    $this->error("分片数据错误");
+                }
+                $listParts = [];
+                for ($i = 0; $i < $chunkcount; $i++) {
+                    $listParts[] = array("PartNumber" => $i + 1, "ETag" => $etags[$i]);
+                }
+                try {
+                    $ret = $oss->completeMultipartUpload($config['bucket'], $key, $uploadId, $listParts);
+                } catch (\Exception $e) {
+                    $checkDeleteFile($attachment, $upload, true);
+                    $this->error($e->getMessage());
+                }
+
+                $result = json_decode(json_encode(simplexml_load_string($ret['body'], "SimpleXMLElement", LIBXML_NOCDATA)), true);
+                if (!isset($result['Key'])) {
+                    $checkDeleteFile($attachment, $upload, true);
+                    $this->error("上传失败");
+                } else {
+                    $checkDeleteFile($attachment, $upload);
+                    $this->success("上传成功", '', ['url' => "/" . $key, 'fullurl' => cdnurl("/" . $key, true)]);
+                }
+            } else {
+                //默认普通上传文件
+                $file = $this->request->file('file');
+                try {
+                    $upload = new Upload($file);
+                    $file = $upload->chunk($chunkid, $chunkindex, $chunkcount);
+                } catch (UploadException $e) {
+                    $this->error($e->getMessage());
+                }
+                try {
+                    //上传分片到OSS
+                    $ret = $oss->uploadPart($config['bucket'], $key, $uploadId, ['fileUpload' => $file->getRealPath(), 'partNumber' => $chunkindex + 1]);
+                } catch (\Exception $e) {
+                    $this->error($e->getMessage());
+                }
+
+                $this->success("上传成功", "", [], 3, ['ETag' => $ret]);
+            }
+        } else {
+            $attachment = null;
+            //默认普通上传文件
+            $file = $this->request->file('file');
+            try {
+                $upload = new Upload($file);
+                $attachment = $upload->upload();
+            } catch (UploadException $e) {
+                $this->error($e->getMessage());
+            }
+
+            //文件绝对路径
+            $filePath = $upload->getFile()->getRealPath() ?: $upload->getFile()->getPathname();
+
+            $url = $attachment->url;
+
+            try {
+                $ret = $oss->uploadFile($config['bucket'], ltrim($attachment->url, "/"), $filePath);
+                //成功不做任何操作
+            } catch (\Exception $e) {
+                $checkDeleteFile($attachment, $upload, true);
+                $this->error("上传失败");
+            }
+            $checkDeleteFile($attachment, $upload);
+
+            $this->success("上传成功", '', ['url' => $url, 'fullurl' => cdnurl($url, true)]);
+        }
+        return;
+    }
+
+    /**
+     * 回调
+     */
+    public function notify()
+    {
+        $this->check();
+        $this->request->filter('trim,strip_tags,htmlspecialchars,xss_clean');
+        $size = $this->request->post('size/d');
+        $name = $this->request->post('name', '');
+        $md5 = $this->request->post('md5', '');
+        $type = $this->request->post('type', '');
+        $url = $this->request->post('url', '');
+        $width = $this->request->post('width/d');
+        $height = $this->request->post('height/d');
+        $category = $this->request->post('category', '');
+        $category = array_key_exists($category, config('site.attachmentcategory') ?? []) ? $category : '';
+        $suffix = strtolower(pathinfo($name, PATHINFO_EXTENSION));
+        $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
+        $attachment = Attachment::where('url', $url)->where('storage', 'alioss')->find();
+        if (!$attachment) {
+            $params = array(
+                'category'    => $category,
+                'admin_id'    => (int)session('admin.id'),
+                'user_id'     => (int)cookie('uid'),
+                'filesize'    => $size,
+                'filename'    => $name,
+                'imagewidth'  => $width,
+                'imageheight' => $height,
+                'imagetype'   => $suffix,
+                'imageframes' => 0,
+                'mimetype'    => $type,
+                'url'         => $url,
+                'uploadtime'  => time(),
+                'storage'     => 'alioss',
+                'sha1'        => $md5,
+            );
+            Attachment::create($params, true);
+        }
+        $this->success();
+        return;
+    }
+
+    /**
+     * 检查签名是否正确或过期
+     */
+    protected function check()
+    {
+        $aliosstoken = $this->request->post('aliosstoken', '', 'trim');
+        if (!$aliosstoken) {
+            $this->error("参数不正确");
+        }
+        $config = get_addon_config('alioss');
+        list($accessKeyId, $sign, $data) = explode(':', $aliosstoken);
+        if (!$accessKeyId || !$sign || !$data) {
+            $this->error("参数不能为空");
+        }
+        if ($accessKeyId !== $config['accessKeyId']) {
+            $this->error("参数不正确");
+        }
+        if ($sign !== base64_encode(hash_hmac('sha1', base64_decode($data), $config['accessKeySecret'], true))) {
+            $this->error("签名不正确");
+        }
+        $json = json_decode(base64_decode($data), true);
+        if ($json['deadline'] < time()) {
+            $this->error("请求已经超时");
+        }
+    }
+
+}

+ 10 - 0
addons/alioss/info.ini

@@ -0,0 +1,10 @@
+name = alioss
+title = 阿里云OSS云储存
+intro = 使用阿里云OSS云储存,支持直传、服务器中转、分片上传
+author = FastAdmin
+website = https://www.fastadmin.net
+version = 1.2.1
+state = 0
+url = /addons/alioss
+license = extended
+licenseto = 65832

+ 78 - 0
addons/alioss/library/Auth.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace addons\alioss\library;
+
+use app\common\library\Upload;
+
+class Auth
+{
+
+    public function __construct()
+    {
+
+    }
+
+    public function params($name, $md5, $callback = true)
+    {
+        $config = get_addon_config('alioss');
+        $callback_param = array(
+            'callbackUrl'      => isset($config['notifyurl']) ? $config['notifyurl'] : '',
+            'callbackBody'     => 'filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}',
+            'callbackBodyType' => "application/x-www-form-urlencoded"
+        );
+
+        $base64_callback_body = base64_encode(json_encode($callback_param));
+
+        $now = time();
+        $end = $now + $config['expire']; //设置该policy超时时间是10s. 即这个policy过了这个有效时间,将不能访问
+        $expiration = $this->gmt_iso8601($end);
+
+        preg_match('/(\d+)(\w+)/', $config['maxsize'], $matches);
+        $type = strtolower($matches[2]);
+        $typeDict = ['b' => 0, 'k' => 1, 'kb' => 1, 'm' => 2, 'mb' => 2, 'gb' => 3, 'g' => 3];
+        $size = (int)$config['maxsize'] * pow(1024, isset($typeDict[$type]) ? $typeDict[$type] : 0);
+
+        //最大文件大小.用户可以自己设置
+        $condition = array(0 => 'content-length-range', 1 => 0, 2 => $size);
+        $conditions[] = $condition;
+
+        //表示用户上传的数据,必须是以$dir开始, 不然上传会失败,这一步不是必须项,只是为了安全起见,防止用户通过policy上传到别人的目录
+        //$start = array(0 => 'starts-with', 1 => '$key', 2 => $dir);
+        //$conditions[] = $start;
+
+        $arr = array('expiration' => $expiration, 'conditions' => $conditions);
+
+        $policy = base64_encode(json_encode($arr));
+        $signature = base64_encode(hash_hmac('sha1', $policy, $config['accessKeySecret'], true));
+
+        $key = (new Upload())->getSavekey($config['savekey'], $name, $md5);
+        $key = ltrim($key, "/");
+
+        $response = array();
+        $response['id'] = $config['accessKeyId'];
+        $response['key'] = $key;
+        $response['policy'] = $policy;
+        $response['signature'] = $signature;
+        $response['expire'] = $end;
+        $response['callback'] = '';
+        return $response;
+    }
+
+    public function check($signature, $policy)
+    {
+        $config = get_addon_config('alioss');
+        $sign = base64_encode(hash_hmac('sha1', $policy, $config['accessKeySecret'], true));
+        return $signature == $sign;
+    }
+
+    private function gmt_iso8601($time)
+    {
+        $dtStr = date("c", $time);
+        $mydatetime = new \DateTime($dtStr);
+        $expiration = $mydatetime->format(\DateTime::ISO8601);
+        $pos = strpos($expiration, '+');
+        $expiration = substr($expiration, 0, $pos);
+        return $expiration . "Z";
+    }
+
+}

+ 262 - 0
addons/alioss/library/OSS/Core/MimeTypes.php

@@ -0,0 +1,262 @@
+<?php
+
+namespace OSS\Core;
+
+/**
+ * Class MimeTypes
+ *
+ * 在上传文件的时候,根据文件的缺省名,得到其对应的Content-type
+ *
+ * @package OSS\Core
+ */
+class MimeTypes
+{
+    /**
+     * 根据文件名,获取http协议header中的content-type应该填写的数据
+     *
+     * @param string $name 缺省名
+     * @return string content-type
+     */
+    public static function getMimetype($name)
+    {
+        $parts = explode('.', $name);
+        if (count($parts) > 1) {
+            $ext = strtolower(end($parts));
+            if (isset(self::$mime_types[$ext])) {
+                return self::$mime_types[$ext];
+            }
+        }
+
+        return null;
+    }
+
+    private static $mime_types = array(
+        'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+        'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
+        'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
+        'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
+        'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+        'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
+        'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+        'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
+        'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
+        'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
+        'apk' => 'application/vnd.android.package-archive',
+        'hqx' => 'application/mac-binhex40',
+        'cpt' => 'application/mac-compactpro',
+        'doc' => 'application/msword',
+        'ogg' => 'audio/ogg',
+        'pdf' => 'application/pdf',
+        'rtf' => 'text/rtf',
+        'mif' => 'application/vnd.mif',
+        'xls' => 'application/vnd.ms-excel',
+        'ppt' => 'application/vnd.ms-powerpoint',
+        'odc' => 'application/vnd.oasis.opendocument.chart',
+        'odb' => 'application/vnd.oasis.opendocument.database',
+        'odf' => 'application/vnd.oasis.opendocument.formula',
+        'odg' => 'application/vnd.oasis.opendocument.graphics',
+        'otg' => 'application/vnd.oasis.opendocument.graphics-template',
+        'odi' => 'application/vnd.oasis.opendocument.image',
+        'odp' => 'application/vnd.oasis.opendocument.presentation',
+        'otp' => 'application/vnd.oasis.opendocument.presentation-template',
+        'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
+        'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template',
+        'odt' => 'application/vnd.oasis.opendocument.text',
+        'odm' => 'application/vnd.oasis.opendocument.text-master',
+        'ott' => 'application/vnd.oasis.opendocument.text-template',
+        'oth' => 'application/vnd.oasis.opendocument.text-web',
+        'sxw' => 'application/vnd.sun.xml.writer',
+        'stw' => 'application/vnd.sun.xml.writer.template',
+        'sxc' => 'application/vnd.sun.xml.calc',
+        'stc' => 'application/vnd.sun.xml.calc.template',
+        'sxd' => 'application/vnd.sun.xml.draw',
+        'std' => 'application/vnd.sun.xml.draw.template',
+        'sxi' => 'application/vnd.sun.xml.impress',
+        'sti' => 'application/vnd.sun.xml.impress.template',
+        'sxg' => 'application/vnd.sun.xml.writer.global',
+        'sxm' => 'application/vnd.sun.xml.math',
+        'sis' => 'application/vnd.symbian.install',
+        'wbxml' => 'application/vnd.wap.wbxml',
+        'wmlc' => 'application/vnd.wap.wmlc',
+        'wmlsc' => 'application/vnd.wap.wmlscriptc',
+        'bcpio' => 'application/x-bcpio',
+        'torrent' => 'application/x-bittorrent',
+        'bz2' => 'application/x-bzip2',
+        'vcd' => 'application/x-cdlink',
+        'pgn' => 'application/x-chess-pgn',
+        'cpio' => 'application/x-cpio',
+        'csh' => 'application/x-csh',
+        'dvi' => 'application/x-dvi',
+        'spl' => 'application/x-futuresplash',
+        'gtar' => 'application/x-gtar',
+        'hdf' => 'application/x-hdf',
+        'jar' => 'application/java-archive',
+        'jnlp' => 'application/x-java-jnlp-file',
+        'js' => 'application/javascript',
+        'json' => 'application/json',
+        'ksp' => 'application/x-kspread',
+        'chrt' => 'application/x-kchart',
+        'kil' => 'application/x-killustrator',
+        'latex' => 'application/x-latex',
+        'rpm' => 'application/x-rpm',
+        'sh' => 'application/x-sh',
+        'shar' => 'application/x-shar',
+        'swf' => 'application/x-shockwave-flash',
+        'sit' => 'application/x-stuffit',
+        'sv4cpio' => 'application/x-sv4cpio',
+        'sv4crc' => 'application/x-sv4crc',
+        'tar' => 'application/x-tar',
+        'tcl' => 'application/x-tcl',
+        'tex' => 'application/x-tex',
+        'man' => 'application/x-troff-man',
+        'me' => 'application/x-troff-me',
+        'ms' => 'application/x-troff-ms',
+        'ustar' => 'application/x-ustar',
+        'src' => 'application/x-wais-source',
+        'zip' => 'application/zip',
+        'm3u' => 'audio/x-mpegurl',
+        'ra' => 'audio/x-pn-realaudio',
+        'wav' => 'audio/x-wav',
+        'wma' => 'audio/x-ms-wma',
+        'wax' => 'audio/x-ms-wax',
+        'pdb' => 'chemical/x-pdb',
+        'xyz' => 'chemical/x-xyz',
+        'bmp' => 'image/bmp',
+        'gif' => 'image/gif',
+        'ief' => 'image/ief',
+        'png' => 'image/png',
+        'wbmp' => 'image/vnd.wap.wbmp',
+        'ras' => 'image/x-cmu-raster',
+        'pnm' => 'image/x-portable-anymap',
+        'pbm' => 'image/x-portable-bitmap',
+        'pgm' => 'image/x-portable-graymap',
+        'ppm' => 'image/x-portable-pixmap',
+        'rgb' => 'image/x-rgb',
+        'xbm' => 'image/x-xbitmap',
+        'xpm' => 'image/x-xpixmap',
+        'xwd' => 'image/x-xwindowdump',
+        'css' => 'text/css',
+        'rtx' => 'text/richtext',
+        'tsv' => 'text/tab-separated-values',
+        'jad' => 'text/vnd.sun.j2me.app-descriptor',
+        'wml' => 'text/vnd.wap.wml',
+        'wmls' => 'text/vnd.wap.wmlscript',
+        'etx' => 'text/x-setext',
+        'mxu' => 'video/vnd.mpegurl',
+        'flv' => 'video/x-flv',
+        'wm' => 'video/x-ms-wm',
+        'wmv' => 'video/x-ms-wmv',
+        'wmx' => 'video/x-ms-wmx',
+        'wvx' => 'video/x-ms-wvx',
+        'avi' => 'video/x-msvideo',
+        'movie' => 'video/x-sgi-movie',
+        'ice' => 'x-conference/x-cooltalk',
+        '3gp' => 'video/3gpp',
+        'ai' => 'application/postscript',
+        'aif' => 'audio/x-aiff',
+        'aifc' => 'audio/x-aiff',
+        'aiff' => 'audio/x-aiff',
+        'asc' => 'text/plain',
+        'atom' => 'application/atom+xml',
+        'au' => 'audio/basic',
+        'bin' => 'application/octet-stream',
+        'cdf' => 'application/x-netcdf',
+        'cgm' => 'image/cgm',
+        'class' => 'application/octet-stream',
+        'dcr' => 'application/x-director',
+        'dif' => 'video/x-dv',
+        'dir' => 'application/x-director',
+        'djv' => 'image/vnd.djvu',
+        'djvu' => 'image/vnd.djvu',
+        'dll' => 'application/octet-stream',
+        'dmg' => 'application/octet-stream',
+        'dms' => 'application/octet-stream',
+        'dtd' => 'application/xml-dtd',
+        'dv' => 'video/x-dv',
+        'dxr' => 'application/x-director',
+        'eps' => 'application/postscript',
+        'exe' => 'application/octet-stream',
+        'ez' => 'application/andrew-inset',
+        'gram' => 'application/srgs',
+        'grxml' => 'application/srgs+xml',
+        'gz' => 'application/x-gzip',
+        'htm' => 'text/html',
+        'html' => 'text/html',
+        'ico' => 'image/x-icon',
+        'ics' => 'text/calendar',
+        'ifb' => 'text/calendar',
+        'iges' => 'model/iges',
+        'igs' => 'model/iges',
+        'jp2' => 'image/jp2',
+        'jpe' => 'image/jpeg',
+        'jpeg' => 'image/jpeg',
+        'jpg' => 'image/jpeg',
+        'kar' => 'audio/midi',
+        'lha' => 'application/octet-stream',
+        'lzh' => 'application/octet-stream',
+        'm4a' => 'audio/mp4a-latm',
+        'm4p' => 'audio/mp4a-latm',
+        'm4u' => 'video/vnd.mpegurl',
+        'm4v' => 'video/x-m4v',
+        'mac' => 'image/x-macpaint',
+        'mathml' => 'application/mathml+xml',
+        'mesh' => 'model/mesh',
+        'mid' => 'audio/midi',
+        'midi' => 'audio/midi',
+        'mov' => 'video/quicktime',
+        'mp2' => 'audio/mpeg',
+        'mp3' => 'audio/mpeg',
+        'mp4' => 'video/mp4',
+        'mpe' => 'video/mpeg',
+        'mpeg' => 'video/mpeg',
+        'mpg' => 'video/mpeg',
+        'mpga' => 'audio/mpeg',
+        'msh' => 'model/mesh',
+        'nc' => 'application/x-netcdf',
+        'oda' => 'application/oda',
+        'ogv' => 'video/ogv',
+        'pct' => 'image/pict',
+        'pic' => 'image/pict',
+        'pict' => 'image/pict',
+        'pnt' => 'image/x-macpaint',
+        'pntg' => 'image/x-macpaint',
+        'ps' => 'application/postscript',
+        'qt' => 'video/quicktime',
+        'qti' => 'image/x-quicktime',
+        'qtif' => 'image/x-quicktime',
+        'ram' => 'audio/x-pn-realaudio',
+        'rdf' => 'application/rdf+xml',
+        'rm' => 'application/vnd.rn-realmedia',
+        'roff' => 'application/x-troff',
+        'sgm' => 'text/sgml',
+        'sgml' => 'text/sgml',
+        'silo' => 'model/mesh',
+        'skd' => 'application/x-koan',
+        'skm' => 'application/x-koan',
+        'skp' => 'application/x-koan',
+        'skt' => 'application/x-koan',
+        'smi' => 'application/smil',
+        'smil' => 'application/smil',
+        'snd' => 'audio/basic',
+        'so' => 'application/octet-stream',
+        'svg' => 'image/svg+xml',
+        't' => 'application/x-troff',
+        'texi' => 'application/x-texinfo',
+        'texinfo' => 'application/x-texinfo',
+        'tif' => 'image/tiff',
+        'tiff' => 'image/tiff',
+        'tr' => 'application/x-troff',
+        'txt' => 'text/plain',
+        'vrml' => 'model/vrml',
+        'vxml' => 'application/voicexml+xml',
+        'webm' => 'video/webm',
+        'webp' => 'image/webp',
+        'wrl' => 'model/vrml',
+        'xht' => 'application/xhtml+xml',
+        'xhtml' => 'application/xhtml+xml',
+        'xml' => 'application/xml',
+        'xsl' => 'application/xml',
+        'xslt' => 'application/xslt+xml',
+        'xul' => 'application/vnd.mozilla.xul+xml',
+    );
+}

+ 54 - 0
addons/alioss/library/OSS/Core/OssException.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace OSS\Core;
+
+/**
+ * Class OssException
+ *
+ * OssClient在使用的时候,所抛出的异常,用户在使用OssClient的时候,要Try住相关代码,
+ * try的Exception应该是OssException,其中会得到相关异常原因
+ *
+ * @package OSS\Core
+ */
+class OssException extends \Exception
+{
+    private $details = array();
+
+    function __construct($details)
+    {
+        if (is_array($details)) {
+            $message = $details['code'] . ': ' . $details['message']
+                     . ' RequestId: ' . $details['request-id'];
+            parent::__construct($message);
+            $this->details = $details;
+        } else {
+            $message = $details;
+            parent::__construct($message);
+        }
+    }
+
+    public function getHTTPStatus()
+    {
+        return isset($this->details['status']) ? $this->details['status'] : '';
+    }
+
+    public function getRequestId()
+    {
+        return isset($this->details['request-id']) ? $this->details['request-id'] : '';
+    }
+
+    public function getErrorCode()
+    {
+        return isset($this->details['code']) ? $this->details['code'] : '';
+    }
+
+    public function getErrorMessage()
+    {
+        return isset($this->details['message']) ? $this->details['message'] : '';
+    }
+
+    public function getDetails()
+    {
+        return isset($this->details['body']) ? $this->details['body'] : '';
+    }
+}

+ 461 - 0
addons/alioss/library/OSS/Core/OssUtil.php

@@ -0,0 +1,461 @@
+<?php
+
+namespace OSS\Core;
+
+/**
+ * Class OssUtil
+ *
+ * Oss工具类,主要供OssClient使用,用户也可以使用本类进行返回结果的格式化
+ *
+ * @package OSS
+ */
+class OssUtil
+{
+    const OSS_CONTENT = 'content';
+    const OSS_LENGTH = 'length';
+    const OSS_HEADERS = 'headers';
+    const OSS_MAX_OBJECT_GROUP_VALUE = 1000;
+    const OSS_MAX_PART_SIZE = 5368709120; // 5GB
+    const OSS_MID_PART_SIZE = 10485760; // 10MB
+    const OSS_MIN_PART_SIZE = 102400; // 100KB
+
+    /**
+     * 生成query params
+     *
+     * @param array $options 关联数组
+     * @return string 返回诸如 key1=value1&key2=value2
+     */
+    public static function toQueryString($options = array())
+    {
+        $temp = array();
+        uksort($options, 'strnatcasecmp');
+        foreach ($options as $key => $value) {
+            if (is_string($key) && !is_array($value)) {
+                $temp[] = rawurlencode($key) . '=' . rawurlencode($value);
+            }
+        }
+        return implode('&', $temp);
+    }
+
+    /**
+     * 转义字符替换
+     *
+     * @param string $subject
+     * @return string
+     */
+    public static function sReplace($subject)
+    {
+        $search = array('<', '>', '&', '\'', '"');
+        $replace = array('&lt;', '&gt;', '&amp;', '&apos;', '&quot;');
+        return str_replace($search, $replace, $subject);
+    }
+
+    /**
+     * 检查是否是中文编码
+     *
+     * @param $str
+     * @return int
+     */
+    public static function chkChinese($str)
+    {
+        return preg_match('/[\x80-\xff]./', $str);
+    }
+
+    /**
+     * 检测是否GB2312编码
+     *
+     * @param string $str
+     * @return boolean false UTF-8编码  TRUE GB2312编码
+     */
+    public static function isGb2312($str)
+    {
+        for ($i = 0; $i < strlen($str); $i++) {
+            $v = ord($str[$i]);
+            if ($v > 127) {
+                if (($v >= 228) && ($v <= 233)) {
+                    if (($i + 2) >= (strlen($str) - 1)) return true;  // not enough characters
+                    $v1 = ord($str[$i + 1]);
+                    $v2 = ord($str[$i + 2]);
+                    if (($v1 >= 128) && ($v1 <= 191) && ($v2 >= 128) && ($v2 <= 191))
+                        return false;
+                    else
+                        return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 检测是否GBK编码
+     *
+     * @param string $str
+     * @param boolean $gbk
+     * @return boolean
+     */
+    public static function checkChar($str, $gbk = true)
+    {
+        for ($i = 0; $i < strlen($str); $i++) {
+            $v = ord($str[$i]);
+            if ($v > 127) {
+                if (($v >= 228) && ($v <= 233)) {
+                    if (($i + 2) >= (strlen($str) - 1)) return $gbk ? true : FALSE;  // not enough characters
+                    $v1 = ord($str[$i + 1]);
+                    $v2 = ord($str[$i + 2]);
+                    if ($gbk) {
+                        return (($v1 >= 128) && ($v1 <= 191) && ($v2 >= 128) && ($v2 <= 191)) ? FALSE : TRUE;//GBK
+                    } else {
+                        return (($v1 >= 128) && ($v1 <= 191) && ($v2 >= 128) && ($v2 <= 191)) ? TRUE : FALSE;
+                    }
+                }
+            }
+        }
+        return $gbk ? TRUE : FALSE;
+    }
+
+    /**
+     * 检验bucket名称是否合法
+     * bucket的命名规范:
+     * 1. 只能包括小写字母,数字
+     * 2. 必须以小写字母或者数字开头
+     * 3. 长度必须在3-63字节之间
+     *
+     * @param string $bucket Bucket名称
+     * @return boolean
+     */
+    public static function validateBucket($bucket)
+    {
+        $pattern = '/^[a-z0-9][a-z0-9-]{2,62}$/';
+        if (!preg_match($pattern, $bucket)) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 检验object名称是否合法
+     * object命名规范:
+     * 1. 规则长度必须在1-1023字节之间
+     * 2. 使用UTF-8编码
+     * 3. 不能以 "/" "\\"开头
+     *
+     * @param string $object Object名称
+     * @return boolean
+     */
+    public static function validateObject($object)
+    {
+        $pattern = '/^.{1,1023}$/';
+        if (empty($object) || !preg_match($pattern, $object) ||
+            self::startsWith($object, '/') || self::startsWith($object, '\\')
+        ) {
+            return false;
+        }
+        return true;
+    }
+
+
+    /**
+     * 判断字符串$str是不是以$findMe开始
+     *
+     * @param string $str
+     * @param string $findMe
+     * @return bool
+     */
+    public static function startsWith($str, $findMe)
+    {
+        if (strpos($str, $findMe) === 0) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 生成createBucketXmlBody接口的xml消息
+     *
+     * @param string $storageClass
+     * @return string
+     */
+    public static function createBucketXmlBody($storageClass)
+    {
+        $xml = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><CreateBucketConfiguration></CreateBucketConfiguration>');
+        $xml->addChild('StorageClass',  $storageClass);
+        return $xml->asXML();
+    }
+
+    /**
+     * 检验$options
+     *
+     * @param array $options
+     * @throws OssException
+     * @return boolean
+     */
+    public static function validateOptions($options)
+    {
+        //$options
+        if ($options != NULL && !is_array($options)) {
+            throw new OssException ($options . ':' . 'option must be array');
+        }
+    }
+
+    /**
+     * 检查上传文件的内容是否合法
+     *
+     * @param $content string
+     * @throws OssException
+     */
+    public static function validateContent($content)
+    {
+        if (empty($content)) {
+            throw new OssException("http body content is invalid");
+        }
+    }
+
+    /**
+     * 校验BUCKET/OBJECT/OBJECT GROUP是否为空
+     *
+     * @param  string $name
+     * @param  string $errMsg
+     * @throws OssException
+     * @return void
+     */
+    public static function throwOssExceptionWithMessageIfEmpty($name, $errMsg)
+    {
+        if (empty($name)) {
+            throw new OssException($errMsg);
+        }
+    }
+
+    /**
+     * 仅供测试使用的接口,请勿使用
+     *
+     * @param $filename
+     * @param $size
+     */
+    public static function generateFile($filename, $size)
+    {
+        if (file_exists($filename) && $size == filesize($filename)) {
+            echo $filename . " already exists, no need to create again. ";
+            return;
+        }
+        $part_size = 1 * 1024 * 1024;
+        $fp = fopen($filename, "w");
+        $characters = <<<BBB
+0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+BBB;
+
+        $charactersLength = strlen($characters);
+        if ($fp) {
+            while ($size > 0) {
+                if ($size < $part_size) {
+                    $write_size = $size;
+                } else {
+                    $write_size = $part_size;
+                }
+                $size -= $write_size;
+                $a = $characters[rand(0, $charactersLength - 1)];
+                $content = str_repeat($a, $write_size);
+                $flag = fwrite($fp, $content);
+                if (!$flag) {
+                    echo "write to " . $filename . " failed. <br>";
+                    break;
+                }
+            }
+        } else {
+            echo "open " . $filename . " failed. <br>";
+        }
+        fclose($fp);
+    }
+
+    /**
+     * 得到文件的md5编码
+     *
+     * @param $filename
+     * @param $from_pos
+     * @param $to_pos
+     * @return string
+     */
+    public static function getMd5SumForFile($filename, $from_pos, $to_pos)
+    {
+        $content_md5 = "";
+        if (($to_pos - $from_pos) > self::OSS_MAX_PART_SIZE) {
+            return $content_md5;
+        }
+        $filesize = filesize($filename);
+        if ($from_pos >= $filesize || $to_pos >= $filesize || $from_pos < 0 || $to_pos < 0) {
+            return $content_md5;
+        }
+
+        $total_length = $to_pos - $from_pos + 1;
+        $buffer = 8192;
+        $left_length = $total_length;
+        if (!file_exists($filename)) {
+            return $content_md5;
+        }
+
+        if (false === $fh = fopen($filename, 'rb')) {
+            return $content_md5;
+        }
+
+        fseek($fh, $from_pos);
+        $data = '';
+        while (!feof($fh)) {
+            if ($left_length >= $buffer) {
+                $read_length = $buffer;
+            } else {
+                $read_length = $left_length;
+            }
+            if ($read_length <= 0) {
+                break;
+            } else {
+                $data .= fread($fh, $read_length);
+                $left_length = $left_length - $read_length;
+            }
+        }
+        fclose($fh);
+        $content_md5 = base64_encode(md5($data, true));
+        return $content_md5;
+    }
+
+    /**
+     * 检测是否windows系统,因为windows系统默认编码为GBK
+     *
+     * @return bool
+     */
+    public static function isWin()
+    {
+        return strtoupper(substr(PHP_OS, 0, 3)) == "WIN";
+    }
+
+    /**
+     * 主要是由于windows系统编码是gbk,遇到中文时候,如果不进行转换处理会出现找不到文件的问题
+     *
+     * @param $file_path
+     * @return string
+     */
+    public static function encodePath($file_path)
+    {
+        if (self::chkChinese($file_path) && self::isWin()) {
+            $file_path = iconv('utf-8', 'gbk', $file_path);
+        }
+        return $file_path;
+    }
+
+    /**
+     * 判断用户输入的endpoint是否是 xxx.xxx.xxx.xxx:port 或者 xxx.xxx.xxx.xxx的ip格式
+     *
+     * @param string $endpoint 需要做判断的endpoint
+     * @return boolean
+     */
+    public static function isIPFormat($endpoint)
+    {
+        $ip_array = explode(":", $endpoint);
+        $hostname = $ip_array[0];
+        $ret = filter_var($hostname, FILTER_VALIDATE_IP);
+        if (!$ret) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * 生成DeleteMultiObjects接口的xml消息
+     *
+     * @param string[] $objects
+     * @param bool $quiet
+     * @return string
+     */
+    public static function createDeleteObjectsXmlBody($objects, $quiet)
+    {
+        $xml = new \SimpleXMLElement('<?xml version="1.0" encoding="utf-8"?><Delete></Delete>');
+        $xml->addChild('Quiet', $quiet);
+        foreach ($objects as $object) {
+            $sub_object = $xml->addChild('Object');
+            $object = OssUtil::sReplace($object);
+            $sub_object->addChild('Key', $object);
+        }
+        return $xml->asXML();
+    }
+
+    /**
+     * 生成CompleteMultipartUpload接口的xml消息
+     *
+     * @param array[] $listParts
+     * @return string
+     */
+    public static function createCompleteMultipartUploadXmlBody($listParts)
+    {
+        $xml = new \SimpleXMLElement('<?xml version="1.0" encoding="utf-8"?><CompleteMultipartUpload></CompleteMultipartUpload>');
+        foreach ($listParts as $node) {
+            $part = $xml->addChild('Part');
+            $part->addChild('PartNumber', $node['PartNumber']);
+            $part->addChild('ETag', $node['ETag']);
+        }
+        return $xml->asXML();
+    }
+
+    /**
+     * 读取目录
+     *
+     * @param string $dir
+     * @param string $exclude
+     * @param bool $recursive
+     * @return string[]
+     */
+    public static function readDir($dir, $exclude = ".|..|.svn|.git", $recursive = false)
+    {
+        $file_list_array = array();
+        $base_path = $dir;
+        $exclude_array = explode("|", $exclude);
+        $exclude_array = array_unique(array_merge($exclude_array, array('.', '..')));
+
+        if ($recursive) {
+            foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir)) as $new_file) {
+                if ($new_file->isDir()) continue;
+                $object = str_replace($base_path, '', $new_file);
+                if (!in_array(strtolower($object), $exclude_array)) {
+                    $object = ltrim($object, '/');
+                    if (is_file($new_file)) {
+                        $key = md5($new_file . $object, false);
+                        $file_list_array[$key] = array('path' => $new_file, 'file' => $object,);
+                    }
+                }
+            }
+        } else if ($handle = opendir($dir)) {
+            while (false !== ($file = readdir($handle))) {
+                if (!in_array(strtolower($file), $exclude_array)) {
+                    $new_file = $dir . '/' . $file;
+                    $object = $file;
+                    $object = ltrim($object, '/');
+                    if (is_file($new_file)) {
+                        $key = md5($new_file . $object, false);
+                        $file_list_array[$key] = array('path' => $new_file, 'file' => $object,);
+                    }
+                }
+            }
+            closedir($handle);
+        }
+        return $file_list_array;
+    }
+
+    /**
+     * Decode key based on the encoding type
+     *
+     * @param string $key
+     * @param string $encoding
+     * @return string
+     */
+    public static function decodeKey($key, $encoding)
+    {
+        if ($encoding == "") {
+            return $key;
+        }
+
+        if ($encoding == "url") {
+            return rawurldecode($key);
+        } else {
+            throw new OssException("Unrecognized encoding type: " . $encoding);
+        }
+    }
+}

+ 25 - 0
addons/alioss/library/OSS/Http/LICENSE

@@ -0,0 +1,25 @@
+Copyright (c) 2006-2010 Ryan Parman, Foleeo Inc., and contributors. 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 Ryan Parman, Foleeo Inc. 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 HOLDERS
+AND 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.

+ 896 - 0
addons/alioss/library/OSS/Http/RequestCore.php

@@ -0,0 +1,896 @@
+<?php
+namespace OSS\Http;
+
+
+/**
+ * Handles all HTTP requests using cURL and manages the responses.
+ *
+ * @version 2011.06.07
+ * @copyright 2006-2011 Ryan Parman
+ * @copyright 2006-2010 Foleeo Inc.
+ * @copyright 2010-2011 Amazon.com, Inc. or its affiliates.
+ * @copyright 2008-2011 Contributors
+ * @license http://opensource.org/licenses/bsd-license.php Simplified BSD License
+ */
+class RequestCore
+{
+    /**
+     * The URL being requested.
+     */
+    public $request_url;
+
+    /**
+     * The headers being sent in the request.
+     */
+    public $request_headers;
+   
+    /**
+     * The raw response callback headers
+     */
+    public $response_raw_headers;
+
+    /**
+     * Response body when error occurs
+     */
+    public $response_error_body;
+
+    /**
+     *The hander of write file
+     */
+    public $write_file_handle; 
+
+    /**
+     * The body being sent in the request.
+     */
+    public $request_body;
+
+    /**
+     * The response returned by the request.
+     */
+    public $response;
+
+    /**
+     * The headers returned by the request.
+     */
+    public $response_headers;
+
+    /**
+     * The body returned by the request.
+     */
+    public $response_body;
+
+    /**
+     * The HTTP status code returned by the request.
+     */
+    public $response_code;
+
+    /**
+     * Additional response data.
+     */
+    public $response_info;
+
+    /**
+     * The method by which the request is being made.
+     */
+    public $method;
+
+    /**
+     * Stores the proxy settings to use for the request.
+     */
+    public $proxy = null;
+
+    /**
+     * The username to use for the request.
+     */
+    public $username = null;
+
+    /**
+     * The password to use for the request.
+     */
+    public $password = null;
+
+    /**
+     * Custom CURLOPT settings.
+     */
+    public $curlopts = null;
+
+    /**
+     * The state of debug mode.
+     */
+    public $debug_mode = false;
+
+    /**
+     * The default class to use for HTTP Requests (defaults to <RequestCore>).
+     */
+    public $request_class = 'OSS\Http\RequestCore';
+
+    /**
+     * The default class to use for HTTP Responses (defaults to <ResponseCore>).
+     */
+    public $response_class = 'OSS\Http\ResponseCore';
+
+    /**
+     * Default useragent string to use.
+     */
+    public $useragent = 'RequestCore/1.4.3';
+
+    /**
+     * File to read from while streaming up.
+     */
+    public $read_file = null;
+
+    /**
+     * The resource to read from while streaming up.
+     */
+    public $read_stream = null;
+
+    /**
+     * The size of the stream to read from.
+     */
+    public $read_stream_size = null;
+
+    /**
+     * The length already read from the stream.
+     */
+    public $read_stream_read = 0;
+
+    /**
+     * File to write to while streaming down.
+     */
+    public $write_file = null;
+
+    /**
+     * The resource to write to while streaming down.
+     */
+    public $write_stream = null;
+
+    /**
+     * Stores the intended starting seek position.
+     */
+    public $seek_position = null;
+
+    /**
+     * The location of the cacert.pem file to use.
+     */
+    public $cacert_location = false;
+
+    /**
+     * The state of SSL certificate verification.
+     */
+    public $ssl_verification = true;
+
+    /**
+     * The user-defined callback function to call when a stream is read from.
+     */
+    public $registered_streaming_read_callback = null;
+
+    /**
+     * The user-defined callback function to call when a stream is written to.
+     */
+    public $registered_streaming_write_callback = null;
+
+    /**
+     * 请求超时时间, 默认是5184000秒,6天
+     *
+     * @var int
+     */
+    public $timeout = 5184000;
+
+    /**
+     * 连接超时时间,默认是10秒
+     *
+     * @var int
+     */
+    public $connect_timeout = 10;
+
+    /*%******************************************************************************************%*/
+    // CONSTANTS
+
+    /**
+     * GET HTTP Method
+     */
+    const HTTP_GET = 'GET';
+
+    /**
+     * POST HTTP Method
+     */
+    const HTTP_POST = 'POST';
+
+    /**
+     * PUT HTTP Method
+     */
+    const HTTP_PUT = 'PUT';
+
+    /**
+     * DELETE HTTP Method
+     */
+    const HTTP_DELETE = 'DELETE';
+
+    /**
+     * HEAD HTTP Method
+     */
+    const HTTP_HEAD = 'HEAD';
+
+
+    /*%******************************************************************************************%*/
+    // CONSTRUCTOR/DESTRUCTOR
+
+    /**
+     * Constructs a new instance of this class.
+     *
+     * @param string $url (Optional) The URL to request or service endpoint to query.
+     * @param string $proxy (Optional) The faux-url to use for proxy settings. Takes the following format: `proxy://user:pass@hostname:port`
+     * @param array $helpers (Optional) An associative array of classnames to use for request, and response functionality. Gets passed in automatically by the calling class.
+     * @return $this A reference to the current instance.
+     */
+    public function __construct($url = null, $proxy = null, $helpers = null)
+    {
+        // Set some default values.
+        $this->request_url = $url;
+        $this->method = self::HTTP_GET;
+        $this->request_headers = array();
+        $this->request_body = '';
+
+        // Set a new Request class if one was set.
+        if (isset($helpers['request']) && !empty($helpers['request'])) {
+            $this->request_class = $helpers['request'];
+        }
+
+        // Set a new Request class if one was set.
+        if (isset($helpers['response']) && !empty($helpers['response'])) {
+            $this->response_class = $helpers['response'];
+        }
+
+        if ($proxy) {
+            $this->set_proxy($proxy);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Destructs the instance. Closes opened file handles.
+     *
+     * @return $this A reference to the current instance.
+     */
+    public function __destruct()
+    {
+        if (isset($this->read_file) && isset($this->read_stream)) {
+            fclose($this->read_stream);
+        }
+
+        if (isset($this->write_file) && isset($this->write_stream)) {
+            fclose($this->write_stream);
+        }
+
+        return $this;
+    }
+
+
+    /*%******************************************************************************************%*/
+    // REQUEST METHODS
+
+    /**
+     * Sets the credentials to use for authentication.
+     *
+     * @param string $user (Required) The username to authenticate with.
+     * @param string $pass (Required) The password to authenticate with.
+     * @return $this A reference to the current instance.
+     */
+    public function set_credentials($user, $pass)
+    {
+        $this->username = $user;
+        $this->password = $pass;
+        return $this;
+    }
+
+    /**
+     * Adds a custom HTTP header to the cURL request.
+     *
+     * @param string $key (Required) The custom HTTP header to set.
+     * @param mixed $value (Required) The value to assign to the custom HTTP header.
+     * @return $this A reference to the current instance.
+     */
+    public function add_header($key, $value)
+    {
+        $this->request_headers[$key] = $value;
+        return $this;
+    }
+
+    /**
+     * Removes an HTTP header from the cURL request.
+     *
+     * @param string $key (Required) The custom HTTP header to set.
+     * @return $this A reference to the current instance.
+     */
+    public function remove_header($key)
+    {
+        if (isset($this->request_headers[$key])) {
+            unset($this->request_headers[$key]);
+        }
+        return $this;
+    }
+
+    /**
+     * Set the method type for the request.
+     *
+     * @param string $method (Required) One of the following constants: <HTTP_GET>, <HTTP_POST>, <HTTP_PUT>, <HTTP_HEAD>, <HTTP_DELETE>.
+     * @return $this A reference to the current instance.
+     */
+    public function set_method($method)
+    {
+        $this->method = strtoupper($method);
+        return $this;
+    }
+
+    /**
+     * Sets a custom useragent string for the class.
+     *
+     * @param string $ua (Required) The useragent string to use.
+     * @return $this A reference to the current instance.
+     */
+    public function set_useragent($ua)
+    {
+        $this->useragent = $ua;
+        return $this;
+    }
+
+    /**
+     * Set the body to send in the request.
+     *
+     * @param string $body (Required) The textual content to send along in the body of the request.
+     * @return $this A reference to the current instance.
+     */
+    public function set_body($body)
+    {
+        $this->request_body = $body;
+        return $this;
+    }
+
+    /**
+     * Set the URL to make the request to.
+     *
+     * @param string $url (Required) The URL to make the request to.
+     * @return $this A reference to the current instance.
+     */
+    public function set_request_url($url)
+    {
+        $this->request_url = $url;
+        return $this;
+    }
+
+    /**
+     * Set additional CURLOPT settings. These will merge with the default settings, and override if
+     * there is a duplicate.
+     *
+     * @param array $curlopts (Optional) A set of key-value pairs that set `CURLOPT` options. These will merge with the existing CURLOPTs, and ones passed here will override the defaults. Keys should be the `CURLOPT_*` constants, not strings.
+     * @return $this A reference to the current instance.
+     */
+    public function set_curlopts($curlopts)
+    {
+        $this->curlopts = $curlopts;
+        return $this;
+    }
+
+    /**
+     * Sets the length in bytes to read from the stream while streaming up.
+     *
+     * @param integer $size (Required) The length in bytes to read from the stream.
+     * @return $this A reference to the current instance.
+     */
+    public function set_read_stream_size($size)
+    {
+        $this->read_stream_size = $size;
+
+        return $this;
+    }
+
+    /**
+     * Sets the resource to read from while streaming up. Reads the stream from its current position until
+     * EOF or `$size` bytes have been read. If `$size` is not given it will be determined by <php:fstat()> and
+     * <php:ftell()>.
+     *
+     * @param resource $resource (Required) The readable resource to read from.
+     * @param integer $size (Optional) The size of the stream to read.
+     * @return $this A reference to the current instance.
+     */
+    public function set_read_stream($resource, $size = null)
+    {
+        if (!isset($size) || $size < 0) {
+            $stats = fstat($resource);
+
+            if ($stats && $stats['size'] >= 0) {
+                $position = ftell($resource);
+
+                if ($position !== false && $position >= 0) {
+                    $size = $stats['size'] - $position;
+                }
+            }
+        }
+
+        $this->read_stream = $resource;
+
+        return $this->set_read_stream_size($size);
+    }
+
+    /**
+     * Sets the file to read from while streaming up.
+     *
+     * @param string $location (Required) The readable location to read from.
+     * @return $this A reference to the current instance.
+     */
+    public function set_read_file($location)
+    {
+        $this->read_file = $location;
+        $read_file_handle = fopen($location, 'r');
+
+        return $this->set_read_stream($read_file_handle);
+    }
+
+    /**
+     * Sets the resource to write to while streaming down.
+     *
+     * @param resource $resource (Required) The writeable resource to write to.
+     * @return $this A reference to the current instance.
+     */
+    public function set_write_stream($resource)
+    {
+        $this->write_stream = $resource;
+
+        return $this;
+    }
+
+    /**
+     * Sets the file to write to while streaming down.
+     *
+     * @param string $location (Required) The writeable location to write to.
+     * @return $this A reference to the current instance.
+     */
+    public function set_write_file($location)
+    {
+        $this->write_file = $location;
+    }
+
+    /**
+     * Set the proxy to use for making requests.
+     *
+     * @param string $proxy (Required) The faux-url to use for proxy settings. Takes the following format: `proxy://user:pass@hostname:port`
+     * @return $this A reference to the current instance.
+     */
+    public function set_proxy($proxy)
+    {
+        $proxy = parse_url($proxy);
+        $proxy['user'] = isset($proxy['user']) ? $proxy['user'] : null;
+        $proxy['pass'] = isset($proxy['pass']) ? $proxy['pass'] : null;
+        $proxy['port'] = isset($proxy['port']) ? $proxy['port'] : null;
+        $this->proxy = $proxy;
+        return $this;
+    }
+
+    /**
+     * Set the intended starting seek position.
+     *
+     * @param integer $position (Required) The byte-position of the stream to begin reading from.
+     * @return $this A reference to the current instance.
+     */
+    public function set_seek_position($position)
+    {
+        $this->seek_position = isset($position) ? (integer)$position : null;
+
+        return $this;
+    }
+
+    /**
+     * A callback function that is invoked by cURL for streaming up.
+     *
+     * @param resource $curl_handle (Required) The cURL handle for the request.
+     * @param resource $header_content (Required) The header callback result.
+     * @return headers from a stream.
+     */
+   public function streaming_header_callback($curl_handle, $header_content)
+   {
+        $code = curl_getinfo($curl_handle, CURLINFO_HTTP_CODE);
+
+        if (isset($this->write_file) && intval($code) / 100 == 2 && !isset($this->write_file_handle))
+        {
+            $this->write_file_handle = fopen($this->write_file, 'w');
+            $this->set_write_stream($this->write_file_handle);
+        }
+
+        $this->response_raw_headers .= $header_content;
+        return strlen($header_content); 
+    }
+        
+
+    /**
+     * Register a callback function to execute whenever a data stream is read from using
+     * <CFRequest::streaming_read_callback()>.
+     *
+     * The user-defined callback function should accept three arguments:
+     *
+     * <ul>
+     *    <li><code>$curl_handle</code> - <code>resource</code> - Required - The cURL handle resource that represents the in-progress transfer.</li>
+     *    <li><code>$file_handle</code> - <code>resource</code> - Required - The file handle resource that represents the file on the local file system.</li>
+     *    <li><code>$length</code> - <code>integer</code> - Required - The length in kilobytes of the data chunk that was transferred.</li>
+     * </ul>
+     *
+     * @param string|array|function $callback (Required) The callback function is called by <php:call_user_func()>, so you can pass the following values: <ul>
+     *    <li>The name of a global function to execute, passed as a string.</li>
+     *    <li>A method to execute, passed as <code>array('ClassName', 'MethodName')</code>.</li>
+     *    <li>An anonymous function (PHP 5.3+).</li></ul>
+     * @return $this A reference to the current instance.
+     */
+    public function register_streaming_read_callback($callback)
+    {
+        $this->registered_streaming_read_callback = $callback;
+
+        return $this;
+    }
+
+    /**
+     * Register a callback function to execute whenever a data stream is written to using
+     * <CFRequest::streaming_write_callback()>.
+     *
+     * The user-defined callback function should accept two arguments:
+     *
+     * <ul>
+     *    <li><code>$curl_handle</code> - <code>resource</code> - Required - The cURL handle resource that represents the in-progress transfer.</li>
+     *    <li><code>$length</code> - <code>integer</code> - Required - The length in kilobytes of the data chunk that was transferred.</li>
+     * </ul>
+     *
+     * @param string|array|function $callback (Required) The callback function is called by <php:call_user_func()>, so you can pass the following values: <ul>
+     *    <li>The name of a global function to execute, passed as a string.</li>
+     *    <li>A method to execute, passed as <code>array('ClassName', 'MethodName')</code>.</li>
+     *    <li>An anonymous function (PHP 5.3+).</li></ul>
+     * @return $this A reference to the current instance.
+     */
+    public function register_streaming_write_callback($callback)
+    {
+        $this->registered_streaming_write_callback = $callback;
+
+        return $this;
+    }
+
+
+    /*%******************************************************************************************%*/
+    // PREPARE, SEND, AND PROCESS REQUEST
+
+    /**
+     * A callback function that is invoked by cURL for streaming up.
+     *
+     * @param resource $curl_handle (Required) The cURL handle for the request.
+     * @param resource $file_handle (Required) The open file handle resource.
+     * @param integer $length (Required) The maximum number of bytes to read.
+     * @return binary Binary data from a stream.
+     */
+    public function streaming_read_callback($curl_handle, $file_handle, $length)
+    {
+        // Once we've sent as much as we're supposed to send...
+        if ($this->read_stream_read >= $this->read_stream_size) {
+            // Send EOF
+            return '';
+        }
+
+        // If we're at the beginning of an upload and need to seek...
+        if ($this->read_stream_read == 0 && isset($this->seek_position) && $this->seek_position !== ftell($this->read_stream)) {
+            if (fseek($this->read_stream, $this->seek_position) !== 0) {
+                throw new RequestCore_Exception('The stream does not support seeking and is either not at the requested position or the position is unknown.');
+            }
+        }
+
+        $read = fread($this->read_stream, min($this->read_stream_size - $this->read_stream_read, $length)); // Remaining upload data or cURL's requested chunk size
+        $this->read_stream_read += strlen($read);
+
+        $out = $read === false ? '' : $read;
+
+        // Execute callback function
+        if ($this->registered_streaming_read_callback) {
+            call_user_func($this->registered_streaming_read_callback, $curl_handle, $file_handle, $out);
+        }
+
+        return $out;
+    }
+
+    /**
+     * A callback function that is invoked by cURL for streaming down.
+     *
+     * @param resource $curl_handle (Required) The cURL handle for the request.
+     * @param binary $data (Required) The data to write.
+     * @return integer The number of bytes written.
+     */
+    public function streaming_write_callback($curl_handle, $data)
+    {
+        $code = curl_getinfo($curl_handle, CURLINFO_HTTP_CODE);
+        
+        if (intval($code) / 100 != 2)
+        {
+            $this->response_error_body .= $data;
+            return strlen($data);
+        }
+
+        $length = strlen($data);
+        $written_total = 0;
+        $written_last = 0;
+        
+        while ($written_total < $length) {
+            $written_last = fwrite($this->write_stream, substr($data, $written_total));
+
+            if ($written_last === false) {
+                return $written_total;
+            }
+
+            $written_total += $written_last;
+        }
+
+        // Execute callback function
+        if ($this->registered_streaming_write_callback) {
+            call_user_func($this->registered_streaming_write_callback, $curl_handle, $written_total);
+        }
+
+        return $written_total;
+    }
+
+    /**
+     * Prepares and adds the details of the cURL request. This can be passed along to a <php:curl_multi_exec()>
+     * function.
+     *
+     * @return resource The handle for the cURL object.
+     *
+     */
+    public function prep_request()
+    {
+        $curl_handle = curl_init();
+
+        // Set default options.
+        curl_setopt($curl_handle, CURLOPT_URL, $this->request_url);
+        curl_setopt($curl_handle, CURLOPT_FILETIME, true);
+        curl_setopt($curl_handle, CURLOPT_FRESH_CONNECT, false);
+//		curl_setopt($curl_handle, CURLOPT_CLOSEPOLICY, CURLCLOSEPOLICY_LEAST_RECENTLY_USED);
+        curl_setopt($curl_handle, CURLOPT_MAXREDIRS, 5);
+        curl_setopt($curl_handle, CURLOPT_HEADER, true);
+        curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($curl_handle, CURLOPT_TIMEOUT, $this->timeout);
+        curl_setopt($curl_handle, CURLOPT_CONNECTTIMEOUT, $this->connect_timeout);
+        curl_setopt($curl_handle, CURLOPT_NOSIGNAL, true);
+        curl_setopt($curl_handle, CURLOPT_REFERER, $this->request_url);
+        curl_setopt($curl_handle, CURLOPT_USERAGENT, $this->useragent);
+        curl_setopt($curl_handle, CURLOPT_HEADERFUNCTION, array($this, 'streaming_header_callback'));
+        curl_setopt($curl_handle, CURLOPT_READFUNCTION, array($this, 'streaming_read_callback'));
+
+        // Verification of the SSL cert
+        if ($this->ssl_verification) {
+            curl_setopt($curl_handle, CURLOPT_SSL_VERIFYPEER, true);
+            curl_setopt($curl_handle, CURLOPT_SSL_VERIFYHOST, 2);
+        } else {
+            curl_setopt($curl_handle, CURLOPT_SSL_VERIFYPEER, false);
+            curl_setopt($curl_handle, CURLOPT_SSL_VERIFYHOST, false);
+        }
+
+        // chmod the file as 0755
+        if ($this->cacert_location === true) {
+            curl_setopt($curl_handle, CURLOPT_CAINFO, dirname(__FILE__) . '/cacert.pem');
+        } elseif (is_string($this->cacert_location)) {
+            curl_setopt($curl_handle, CURLOPT_CAINFO, $this->cacert_location);
+        }
+
+        // Debug mode
+        if ($this->debug_mode) {
+            curl_setopt($curl_handle, CURLOPT_VERBOSE, true);
+        }
+
+        // Handle open_basedir & safe mode
+        if (!ini_get('safe_mode') && !ini_get('open_basedir')) {
+            curl_setopt($curl_handle, CURLOPT_FOLLOWLOCATION, true);
+        }
+
+        // Enable a proxy connection if requested.
+        if ($this->proxy) {
+
+            $host = $this->proxy['host'];
+            $host .= ($this->proxy['port']) ? ':' . $this->proxy['port'] : '';
+            curl_setopt($curl_handle, CURLOPT_PROXY, $host);
+
+            if (isset($this->proxy['user']) && isset($this->proxy['pass'])) {
+                curl_setopt($curl_handle, CURLOPT_PROXYUSERPWD, $this->proxy['user'] . ':' . $this->proxy['pass']);
+            }
+        }
+
+        // Set credentials for HTTP Basic/Digest Authentication.
+        if ($this->username && $this->password) {
+            curl_setopt($curl_handle, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
+            curl_setopt($curl_handle, CURLOPT_USERPWD, $this->username . ':' . $this->password);
+        }
+
+        // Handle the encoding if we can.
+        if (extension_loaded('zlib')) {
+            curl_setopt($curl_handle, CURLOPT_ENCODING, '');
+        }
+
+        // Process custom headers
+        if (isset($this->request_headers) && count($this->request_headers)) {
+            $temp_headers = array();
+
+            foreach ($this->request_headers as $k => $v) {
+                $temp_headers[] = $k . ': ' . $v;
+            }
+
+            curl_setopt($curl_handle, CURLOPT_HTTPHEADER, $temp_headers);
+        }
+
+        switch ($this->method) {
+            case self::HTTP_PUT:
+                //unset($this->read_stream);
+                curl_setopt($curl_handle, CURLOPT_CUSTOMREQUEST, 'PUT');
+                if (isset($this->read_stream)) {
+                    if (!isset($this->read_stream_size) || $this->read_stream_size < 0) {
+                        throw new RequestCore_Exception('The stream size for the streaming upload cannot be determined.');
+                    }
+                    curl_setopt($curl_handle, CURLOPT_INFILESIZE, $this->read_stream_size);
+                    curl_setopt($curl_handle, CURLOPT_UPLOAD, true);
+                } else {
+                    curl_setopt($curl_handle, CURLOPT_POSTFIELDS, $this->request_body);
+                }
+                break;
+
+            case self::HTTP_POST:
+                curl_setopt($curl_handle, CURLOPT_CUSTOMREQUEST, 'POST');
+                if (isset($this->read_stream)) {
+                    if (!isset($this->read_stream_size) || $this->read_stream_size < 0) {
+                        throw new RequestCore_Exception('The stream size for the streaming upload cannot be determined.');
+                    }
+                    curl_setopt($curl_handle, CURLOPT_INFILESIZE, $this->read_stream_size);
+                    curl_setopt($curl_handle, CURLOPT_UPLOAD, true);
+                } else {
+                    curl_setopt($curl_handle, CURLOPT_POSTFIELDS, $this->request_body);
+                }
+                break;
+
+            case self::HTTP_HEAD:
+                curl_setopt($curl_handle, CURLOPT_CUSTOMREQUEST, self::HTTP_HEAD);
+                curl_setopt($curl_handle, CURLOPT_NOBODY, 1);
+                break;
+
+            default: // Assumed GET
+                curl_setopt($curl_handle, CURLOPT_CUSTOMREQUEST, $this->method);
+                if (isset($this->write_stream) || isset($this->write_file)) {
+                    curl_setopt($curl_handle, CURLOPT_WRITEFUNCTION, array($this, 'streaming_write_callback'));
+                    curl_setopt($curl_handle, CURLOPT_HEADER, false);
+                } else {
+                    curl_setopt($curl_handle, CURLOPT_POSTFIELDS, $this->request_body);
+                }
+                break;
+        }
+
+        // Merge in the CURLOPTs
+        if (isset($this->curlopts) && sizeof($this->curlopts) > 0) {
+            foreach ($this->curlopts as $k => $v) {
+                curl_setopt($curl_handle, $k, $v);
+            }
+        }
+
+        return $curl_handle;
+    }
+
+    /**
+     * Take the post-processed cURL data and break it down into useful header/body/info chunks. Uses the
+     * data stored in the `curl_handle` and `response` properties unless replacement data is passed in via
+     * parameters.
+     *
+     * @param resource $curl_handle (Optional) The reference to the already executed cURL request.
+     * @param string $response (Optional) The actual response content itself that needs to be parsed.
+     * @return ResponseCore A <ResponseCore> object containing a parsed HTTP response.
+     */
+    public function process_response($curl_handle = null, $response = null)
+    {
+        // Accept a custom one if it's passed.
+        if ($curl_handle && $response) {
+            $this->response = $response;
+        }
+
+        // As long as this came back as a valid resource...
+        if (is_resource($curl_handle)) {
+            // Determine what's what.
+            $header_size = curl_getinfo($curl_handle, CURLINFO_HEADER_SIZE);
+            $this->response_headers = substr($this->response, 0, $header_size);
+            $this->response_body = substr($this->response, $header_size);
+            $this->response_code = curl_getinfo($curl_handle, CURLINFO_HTTP_CODE);
+            $this->response_info = curl_getinfo($curl_handle);
+            
+            if (intval($this->response_code) / 100 != 2 && isset($this->write_file))
+            {
+                $this->response_headers = $this->response_raw_headers;
+                $this->response_body = $this->response_error_body;
+            }
+
+            // Parse out the headers
+            $this->response_headers = explode("\r\n\r\n", trim($this->response_headers));
+            $this->response_headers = array_pop($this->response_headers);
+            $this->response_headers = explode("\r\n", $this->response_headers);
+            array_shift($this->response_headers);
+
+            // Loop through and split up the headers.
+            $header_assoc = array();
+            foreach ($this->response_headers as $header) {
+                $kv = explode(': ', $header);
+                $header_assoc[strtolower($kv[0])] = isset($kv[1]) ? $kv[1] : '';
+            }
+
+            // Reset the headers to the appropriate property.
+            $this->response_headers = $header_assoc;
+            $this->response_headers['info'] = $this->response_info;
+            $this->response_headers['info']['method'] = $this->method;
+            
+            if ($curl_handle && $response) {
+                return new ResponseCore($this->response_headers, $this->response_body, $this->response_code);
+            }
+        }
+
+        // Return false
+        return false;
+    }
+
+    /**
+     * Sends the request, calling necessary utility functions to update built-in properties.
+     *
+     * @param boolean $parse (Optional) Whether to parse the response with ResponseCore or not.
+     * @return string The resulting unparsed data from the request.
+     */
+    public function send_request($parse = false)
+    {
+        set_time_limit(0);
+
+        $curl_handle = $this->prep_request();
+        $this->response = curl_exec($curl_handle);
+
+        if ($this->response === false) {
+            throw new RequestCore_Exception('cURL resource: ' . (string)$curl_handle . '; cURL error: ' . curl_error($curl_handle) . ' (' . curl_errno($curl_handle) . ')');
+        }
+
+        $parsed_response = $this->process_response($curl_handle, $this->response);
+
+        curl_close($curl_handle);
+
+        if ($parse) {
+            return $parsed_response;
+        }
+
+        return $this->response;
+    }
+
+    /*%******************************************************************************************%*/
+    // RESPONSE METHODS
+
+    /**
+     * Get the HTTP response headers from the request.
+     *
+     * @param string $header (Optional) A specific header value to return. Defaults to all headers.
+     * @return string|array All or selected header values.
+     */
+    public function get_response_header($header = null)
+    {
+        if ($header) {
+            return $this->response_headers[strtolower($header)];
+        }
+        return $this->response_headers;
+    }
+
+    /**
+     * Get the HTTP response body from the request.
+     *
+     * @return string The response body.
+     */
+    public function get_response_body()
+    {
+        return $this->response_body;
+    }
+
+    /**
+     * Get the HTTP response code from the request.
+     *
+     * @return string The HTTP response code.
+     */
+    public function get_response_code()
+    {
+        return $this->response_code;
+    }
+}

+ 8 - 0
addons/alioss/library/OSS/Http/RequestCore_Exception.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace OSS\Http;
+
+class RequestCore_Exception extends \Exception
+{
+
+}

+ 56 - 0
addons/alioss/library/OSS/Http/ResponseCore.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace OSS\Http;
+
+/**
+ * Container for all response-related methods.
+ */
+class ResponseCore
+{
+    /**
+     * Stores the HTTP header information.
+     */
+    public $header;
+
+    /**
+     * Stores the SimpleXML response.
+     */
+    public $body;
+
+    /**
+     * Stores the HTTP response code.
+     */
+    public $status;
+
+    /**
+     * Constructs a new instance of this class.
+     *
+     * @param array $header (Required) Associative array of HTTP headers (typically returned by <RequestCore::get_response_header()>).
+     * @param string $body (Required) XML-formatted response from AWS.
+     * @param integer $status (Optional) HTTP response status code from the request.
+     * @return Mixed Contains an <php:array> `header` property (HTTP headers as an associative array), a <php:SimpleXMLElement> or <php:string> `body` property, and an <php:integer> `status` code.
+     */
+    public function __construct($header, $body, $status = null)
+    {
+        $this->header = $header;
+        $this->body = $body;
+        $this->status = $status;
+
+        return $this;
+    }
+
+    /**
+     * Did we receive the status code we expected?
+     *
+     * @param integer|array $codes (Optional) The status code(s) to expect. Pass an <php:integer> for a single acceptable value, or an <php:array> of integers for multiple acceptable values.
+     * @return boolean Whether we received the expected status code or not.
+     */
+    public function isOK($codes = array(200, 201, 204, 206))
+    {
+        if (is_array($codes)) {
+            return in_array($this->status, $codes);
+        }
+
+        return $this->status === $codes;
+    }
+}

+ 78 - 0
addons/alioss/library/OSS/Model/BucketInfo.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace OSS\Model;
+
+
+/**
+ * Bucket信息,ListBuckets接口返回数据
+ *
+ * Class BucketInfo
+ * @package OSS\Model
+ */
+class BucketInfo
+{
+    /**
+     * BucketInfo constructor.
+     *
+     * @param string $location
+     * @param string $name
+     * @param string $createDate
+     */
+    public function __construct($location, $name, $createDate)
+    {
+        $this->location = $location;
+        $this->name = $name;
+        $this->createDate = $createDate;
+    }
+
+    /**
+     * 得到bucket所在的region
+     *
+     * @return string
+     */
+    public function getLocation()
+    {
+        return $this->location;
+    }
+
+    /**
+     * 得到bucket的名称
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * 得到bucket的创建时间
+     *
+     * @return string
+     */
+    public function getCreateDate()
+    {
+        return $this->createDate;
+    }
+
+    /**
+     * bucket所在的region
+     *
+     * @var string
+     */
+    private $location;
+    /**
+     * bucket的名称
+     *
+     * @var string
+     */
+    private $name;
+
+    /**
+     * bucket的创建事件
+     *
+     * @var string
+     */
+    private $createDate;
+
+}

+ 39 - 0
addons/alioss/library/OSS/Model/BucketListInfo.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace OSS\Model;
+
+/**
+ * Class BucketListInfo
+ *
+ * ListBuckets接口返回的数据类型
+ *
+ * @package OSS\Model
+ */
+class BucketListInfo
+{
+    /**
+     * BucketListInfo constructor.
+     * @param array $bucketList
+     */
+    public function __construct(array $bucketList)
+    {
+        $this->bucketList = $bucketList;
+    }
+
+    /**
+     * 得到BucketInfo列表
+     *
+     * @return BucketInfo[]
+     */
+    public function getBucketList()
+    {
+        return $this->bucketList;
+    }
+
+    /**
+     * BucketInfo信息列表
+     *
+     * @var array
+     */
+    private $bucketList = array();
+}

+ 99 - 0
addons/alioss/library/OSS/Model/CnameConfig.php

@@ -0,0 +1,99 @@
+<?php
+
+namespace OSS\Model;
+
+
+use OSS\Core\OssException;
+
+/**
+ * Class CnameConfig
+ * @package OSS\Model
+ *
+ * TODO: fix link
+ * @link http://help.aliyun.com/document_detail/oss/api-reference/cors/PutBucketcors.html
+ */
+class CnameConfig implements XmlConfig
+{
+    public function __construct()
+    {
+        $this->cnameList = array();
+    }
+
+    /**
+     * @return array
+     * @example
+     *  array(2) {
+     *    [0]=>
+     *    array(3) {
+     *      ["Domain"]=>
+     *      string(11) "www.foo.com"
+     *      ["Status"]=>
+     *      string(7) "enabled"
+     *      ["LastModified"]=>
+     *      string(8) "20150101"
+     *    }
+     *    [1]=>
+     *    array(3) {
+     *      ["Domain"]=>
+     *      string(7) "bar.com"
+     *      ["Status"]=>
+     *      string(8) "disabled"
+     *      ["LastModified"]=>
+     *      string(8) "20160101"
+     *    }
+     *  }
+     */
+    public function getCnames()
+    {
+        return $this->cnameList;
+    }
+
+
+    public function addCname($cname)
+    {
+        if (count($this->cnameList) >= self::OSS_MAX_RULES) {
+            throw new OssException(
+                "num of cname in the config exceeds self::OSS_MAX_RULES: " . strval(self::OSS_MAX_RULES));
+        }
+        $this->cnameList[] = array('Domain' => $cname);
+    }
+
+    public function parseFromXml($strXml)
+    {
+        $xml = simplexml_load_string($strXml);
+        if (!isset($xml->Cname)) return;
+        foreach ($xml->Cname as $entry) {
+            $cname = array();
+            foreach ($entry as $key => $value) {
+                $cname[strval($key)] = strval($value);
+            }
+            $this->cnameList[] = $cname;
+        }
+    }
+
+    public function serializeToXml()
+    {
+        $strXml = <<<EOF
+<?xml version="1.0" encoding="utf-8"?>
+<BucketCnameConfiguration>
+</BucketCnameConfiguration>
+EOF;
+        $xml = new \SimpleXMLElement($strXml);
+        foreach ($this->cnameList as $cname) {
+            $node = $xml->addChild('Cname');
+            foreach ($cname as $key => $value) {
+                $node->addChild($key, $value);
+            }
+        }
+        return $xml->asXML();
+    }
+
+    public function __toString()
+    {
+        return $this->serializeToXml();
+    }
+
+    const OSS_MAX_RULES = 10;
+
+    private $cnameList = array();
+}

+ 113 - 0
addons/alioss/library/OSS/Model/CorsConfig.php

@@ -0,0 +1,113 @@
+<?php
+
+namespace OSS\Model;
+
+
+use OSS\Core\OssException;
+
+/**
+ * Class CorsConfig
+ * @package OSS\Model
+ *
+ * @link http://help.aliyun.com/document_detail/oss/api-reference/cors/PutBucketcors.html
+ */
+class CorsConfig implements XmlConfig
+{
+    /**
+     * CorsConfig constructor.
+     */
+    public function __construct()
+    {
+        $this->rules = array();
+    }
+
+    /**
+     * 得到CorsRule列表
+     *
+     * @return CorsRule[]
+     */
+    public function getRules()
+    {
+        return $this->rules;
+    }
+
+
+    /**
+     * 添加一条CorsRule
+     *
+     * @param CorsRule $rule
+     * @throws OssException
+     */
+    public function addRule($rule)
+    {
+        if (count($this->rules) >= self::OSS_MAX_RULES) {
+            throw new OssException("num of rules in the config exceeds self::OSS_MAX_RULES: " . strval(self::OSS_MAX_RULES));
+        }
+        $this->rules[] = $rule;
+    }
+
+    /**
+     * 从xml数据中解析出CorsConfig
+     *
+     * @param string $strXml
+     * @throws OssException
+     * @return null
+     */
+    public function parseFromXml($strXml)
+    {
+        $xml = simplexml_load_string($strXml);
+        if (!isset($xml->CORSRule)) return;
+        foreach ($xml->CORSRule as $rule) {
+            $corsRule = new CorsRule();
+            foreach ($rule as $key => $value) {
+                if ($key === self::OSS_CORS_ALLOWED_HEADER) {
+                    $corsRule->addAllowedHeader(strval($value));
+                } elseif ($key === self::OSS_CORS_ALLOWED_METHOD) {
+                    $corsRule->addAllowedMethod(strval($value));
+                } elseif ($key === self::OSS_CORS_ALLOWED_ORIGIN) {
+                    $corsRule->addAllowedOrigin(strval($value));
+                } elseif ($key === self::OSS_CORS_EXPOSE_HEADER) {
+                    $corsRule->addExposeHeader(strval($value));
+                } elseif ($key === self::OSS_CORS_MAX_AGE_SECONDS) {
+                    $corsRule->setMaxAgeSeconds(strval($value));
+                }
+            }
+            $this->addRule($corsRule);
+        }
+        return;
+    }
+
+    /**
+     * 生成xml字符串
+     *
+     * @return string
+     */
+    public function serializeToXml()
+    {
+        $xml = new \SimpleXMLElement('<?xml version="1.0" encoding="utf-8"?><CORSConfiguration></CORSConfiguration>');
+        foreach ($this->rules as $rule) {
+            $xmlRule = $xml->addChild('CORSRule');
+            $rule->appendToXml($xmlRule);
+        }
+        return $xml->asXML();
+    }
+
+    public function __toString()
+    {
+        return $this->serializeToXml();
+    }
+
+    const OSS_CORS_ALLOWED_ORIGIN = 'AllowedOrigin';
+    const OSS_CORS_ALLOWED_METHOD = 'AllowedMethod';
+    const OSS_CORS_ALLOWED_HEADER = 'AllowedHeader';
+    const OSS_CORS_EXPOSE_HEADER = 'ExposeHeader';
+    const OSS_CORS_MAX_AGE_SECONDS = 'MaxAgeSeconds';
+    const OSS_MAX_RULES = 10;
+
+    /**
+     * orsRule列表
+     *
+     * @var CorsRule[]
+     */
+    private $rules = array();
+}

+ 150 - 0
addons/alioss/library/OSS/Model/CorsRule.php

@@ -0,0 +1,150 @@
+<?php
+
+namespace OSS\Model;
+
+use OSS\Core\OssException;
+
+
+/**
+ * Class CorsRule
+ * @package OSS\Model
+ * @link http://help.aliyun.com/document_detail/oss/api-reference/cors/PutBucketcors.html
+ */
+class CorsRule
+{
+    /**
+     * Rule中增加一条allowedOrigin
+     *
+     * @param string $allowedOrigin
+     */
+    public function addAllowedOrigin($allowedOrigin)
+    {
+        if (!empty($allowedOrigin)) {
+            $this->allowedOrigins[] = $allowedOrigin;
+        }
+    }
+
+    /**
+     * Rule中增加一条allowedMethod
+     *
+     * @param string $allowedMethod
+     */
+    public function addAllowedMethod($allowedMethod)
+    {
+        if (!empty($allowedMethod)) {
+            $this->allowedMethods[] = $allowedMethod;
+        }
+    }
+
+    /**
+     * Rule中增加一条allowedHeader
+     *
+     * @param string $allowedHeader
+     */
+    public function addAllowedHeader($allowedHeader)
+    {
+        if (!empty($allowedHeader)) {
+            $this->allowedHeaders[] = $allowedHeader;
+        }
+    }
+
+    /**
+     * Rule中增加一条exposeHeader
+     *
+     * @param string $exposeHeader
+     */
+    public function addExposeHeader($exposeHeader)
+    {
+        if (!empty($exposeHeader)) {
+            $this->exposeHeaders[] = $exposeHeader;
+        }
+    }
+
+    /**
+     * @return int
+     */
+    public function getMaxAgeSeconds()
+    {
+        return $this->maxAgeSeconds;
+    }
+
+    /**
+     * @param int $maxAgeSeconds
+     */
+    public function setMaxAgeSeconds($maxAgeSeconds)
+    {
+        $this->maxAgeSeconds = $maxAgeSeconds;
+    }
+
+    /**
+     * 得到AllowedHeaders列表
+     *
+     * @return string[]
+     */
+    public function getAllowedHeaders()
+    {
+        return $this->allowedHeaders;
+    }
+
+    /**
+     * 得到AllowedOrigins列表
+     *
+     * @return string[]
+     */
+    public function getAllowedOrigins()
+    {
+        return $this->allowedOrigins;
+    }
+
+    /**
+     * 得到AllowedMethods列表
+     *
+     * @return string[]
+     */
+    public function getAllowedMethods()
+    {
+        return $this->allowedMethods;
+    }
+
+    /**
+     * 得到ExposeHeaders列表
+     *
+     * @return string[]
+     */
+    public function getExposeHeaders()
+    {
+        return $this->exposeHeaders;
+    }
+
+    /**
+     * 根据提供的xmlRule, 把this按照一定的规则插入到$xmlRule中
+     *
+     * @param \SimpleXMLElement $xmlRule
+     * @throws OssException
+     */
+    public function appendToXml(&$xmlRule)
+    {
+        if (!isset($this->maxAgeSeconds)) {
+            throw new OssException("maxAgeSeconds is not set in the Rule");
+        }
+        foreach ($this->allowedOrigins as $allowedOrigin) {
+            $xmlRule->addChild(CorsConfig::OSS_CORS_ALLOWED_ORIGIN, $allowedOrigin);
+        }
+        foreach ($this->allowedMethods as $allowedMethod) {
+            $xmlRule->addChild(CorsConfig::OSS_CORS_ALLOWED_METHOD, $allowedMethod);
+        }
+        foreach ($this->allowedHeaders as $allowedHeader) {
+            $xmlRule->addChild(CorsConfig::OSS_CORS_ALLOWED_HEADER, $allowedHeader);
+        }
+        foreach ($this->exposeHeaders as $exposeHeader) {
+            $xmlRule->addChild(CorsConfig::OSS_CORS_EXPOSE_HEADER, $exposeHeader);
+        }
+        $xmlRule->addChild(CorsConfig::OSS_CORS_MAX_AGE_SECONDS, strval($this->maxAgeSeconds));
+    }
+
+    private $allowedHeaders = array();
+    private $allowedOrigins = array();
+    private $allowedMethods = array();
+    private $exposeHeaders = array();
+    private $maxAgeSeconds = null;
+}

+ 34 - 0
addons/alioss/library/OSS/Model/GetLiveChannelHistory.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace OSS\Model;
+/**
+ * Class GetLiveChannelHistory
+ * @package OSS\Model
+ */
+class GetLiveChannelHistory implements XmlConfig
+{
+     public function getLiveRecordList()
+    {
+        return $this->liveRecordList;
+    }
+
+    public function parseFromXml($strXml)
+    {
+        $xml = simplexml_load_string($strXml);
+
+        if (isset($xml->LiveRecord)) {
+            foreach ($xml->LiveRecord as $record) {
+            $liveRecord = new LiveChannelHistory();
+            $liveRecord->parseFromXmlNode($record);
+            $this->liveRecordList[] = $liveRecord;
+           }
+        }
+    }
+
+    public function serializeToXml()
+    {
+        throw new OssException("Not implemented.");
+    }
+    
+    private $liveRecordList = array();
+}

+ 68 - 0
addons/alioss/library/OSS/Model/GetLiveChannelInfo.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace OSS\Model;
+/**
+ * Class GetLiveChannelInfo
+ * @package OSS\Model
+ */
+class GetLiveChannelInfo implements XmlConfig
+{
+    public function getDescription()
+    {
+        return $this->description;
+    }
+
+    public function getStatus()
+    {
+        return $this->status;
+    }
+
+    public function getType()
+    {
+        return $this->type;
+    }
+  
+    public function getFragDuration()
+    {
+        return $this->fragDuration;
+    }
+   
+    public function getFragCount()
+    {
+        return $this->fragCount;
+    }
+   
+    public function getPlayListName()
+    {
+        return $this->playlistName;
+    }
+
+    public function parseFromXml($strXml)
+    {
+        $xml = simplexml_load_string($strXml);
+
+        $this->description = strval($xml->Description);
+        $this->status = strval($xml->Status);
+
+        if (isset($xml->Target)) {
+            foreach ($xml->Target as $target) {
+            $this->type = strval($target->Type);
+            $this->fragDuration = strval($target->FragDuration);
+            $this->fragCount = strval($target->FragCount);
+            $this->playlistName = strval($target->PlaylistName);
+           }
+        }
+    }
+
+    public function serializeToXml()
+    {
+        throw new OssException("Not implemented.");
+    }
+    
+    private $description;
+    private $status;
+    private $type;
+    private $fragDuration;
+    private $fragCount;
+    private $playlistName;
+}

+ 107 - 0
addons/alioss/library/OSS/Model/GetLiveChannelStatus.php

@@ -0,0 +1,107 @@
+<?php
+
+namespace OSS\Model;
+/**
+ * Class GetLiveChannelStatus
+ * @package OSS\Model
+ */
+class GetLiveChannelStatus implements XmlConfig
+{
+    public function getStatus()
+    {
+        return $this->status;
+    }
+
+    public function getConnectedTime()
+    {
+        return $this->connectedTime;
+    }
+
+    public function getRemoteAddr()
+    {
+        return $this->remoteAddr;
+    }
+
+    public function getVideoWidth()
+    {
+        return $this->videoWidth;
+    }
+    public function getVideoHeight()
+    {
+        return $this->videoHeight;
+    }
+    public function getVideoFrameRate()
+    {
+        return $this->videoFrameRate;
+    }
+    public function getVideoBandwidth()
+    {
+        return $this->videoBandwidth;
+    }
+    public function getVideoCodec()
+    {
+        return $this->videoCodec;
+    }
+
+    public function getAudioBandwidth()
+    {
+        return $this->audioBandwidth;
+    }
+    public function getAudioSampleRate()
+    {
+        return $this->audioSampleRate;
+    }
+    public function getAudioCodec()
+    {
+        return $this->audioCodec;
+    }
+
+
+    public function parseFromXml($strXml)
+    {
+        $xml = simplexml_load_string($strXml);
+        $this->status = strval($xml->Status);
+        $this->connectedTime = strval($xml->ConnectedTime);
+        $this->remoteAddr = strval($xml->RemoteAddr);
+
+        if (isset($xml->Video)) {
+            foreach ($xml->Video as $video) {
+            $this->videoWidth = intval($video->Width);
+            $this->videoHeight = intval($video->Height);
+            $this->videoFrameRate = intval($video->FrameRate);
+            $this->videoBandwidth = intval($video->Bandwidth);
+            $this->videoCodec = strval($video->Codec);
+           }
+        }
+        
+        if (isset($xml->Video)) {
+            foreach ($xml->Audio as $audio) {
+            $this->audioBandwidth = intval($audio->Bandwidth);
+            $this->audioSampleRate = intval($audio->SampleRate);
+            $this->audioCodec = strval($audio->Codec);
+           }
+        }
+
+    }
+
+    public function serializeToXml()
+    {
+        throw new OssException("Not implemented.");
+    }
+    
+    private $status;
+    private $connectedTime;
+    private $remoteAddr;
+
+    private $videoWidth;
+    private $videoHeight;
+    private $videoFrameRate;
+    private $videoBandwidth;
+    private $videoCodec;
+
+    private $audioBandwidth;
+    private $audioSampleRate;
+    private $audioCodec;
+    
+
+}

+ 88 - 0
addons/alioss/library/OSS/Model/LifecycleAction.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace OSS\Model;
+
+/**
+ * Class LifecycleAction
+ * @package OSS\Model
+ * @link http://help.aliyun.com/document_detail/oss/api-reference/bucket/PutBucketLifecycle.html
+ */
+class LifecycleAction
+{
+    /**
+     * LifecycleAction constructor.
+     * @param string $action
+     * @param string $timeSpec
+     * @param string $timeValue
+     */
+    public function __construct($action, $timeSpec, $timeValue)
+    {
+        $this->action = $action;
+        $this->timeSpec = $timeSpec;
+        $this->timeValue = $timeValue;
+    }
+
+    /**
+     * @return LifecycleAction
+     */
+    public function getAction()
+    {
+        return $this->action;
+    }
+
+    /**
+     * @param string $action
+     */
+    public function setAction($action)
+    {
+        $this->action = $action;
+    }
+
+    /**
+     * @return string
+     */
+    public function getTimeSpec()
+    {
+        return $this->timeSpec;
+    }
+
+    /**
+     * @param string $timeSpec
+     */
+    public function setTimeSpec($timeSpec)
+    {
+        $this->timeSpec = $timeSpec;
+    }
+
+    /**
+     * @return string
+     */
+    public function getTimeValue()
+    {
+        return $this->timeValue;
+    }
+
+    /**
+     * @param string $timeValue
+     */
+    public function setTimeValue($timeValue)
+    {
+        $this->timeValue = $timeValue;
+    }
+
+    /**
+     * appendToXml 把actions插入到xml中
+     *
+     * @param \SimpleXMLElement $xmlRule
+     */
+    public function appendToXml(&$xmlRule)
+    {
+        $xmlAction = $xmlRule->addChild($this->action);
+        $xmlAction->addChild($this->timeSpec, $this->timeValue);
+    }
+
+    private $action;
+    private $timeSpec;
+    private $timeValue;
+
+}

+ 107 - 0
addons/alioss/library/OSS/Model/LifecycleConfig.php

@@ -0,0 +1,107 @@
+<?php
+
+namespace OSS\Model;
+
+use OSS\Core\OssException;
+
+
+/**
+ * Class BucketLifecycleConfig
+ * @package OSS\Model
+ * @link http://help.aliyun.com/document_detail/oss/api-reference/bucket/PutBucketLifecycle.html
+ */
+class LifecycleConfig implements XmlConfig
+{
+    /**
+     * 从xml数据中解析出LifecycleConfig
+     *
+     * @param string $strXml
+     * @throws OssException
+     * @return null
+     */
+    public function parseFromXml($strXml)
+    {
+        $this->rules = array();
+        $xml = simplexml_load_string($strXml);
+        if (!isset($xml->Rule)) return;
+        $this->rules = array();
+        foreach ($xml->Rule as $rule) {
+            $id = strval($rule->ID);
+            $prefix = strval($rule->Prefix);
+            $status = strval($rule->Status);
+            $actions = array();
+            foreach ($rule as $key => $value) {
+                if ($key === 'ID' || $key === 'Prefix' || $key === 'Status') continue;
+                $action = $key;
+                $timeSpec = null;
+                $timeValue = null;
+                foreach ($value as $timeSpecKey => $timeValueValue) {
+                    $timeSpec = $timeSpecKey;
+                    $timeValue = strval($timeValueValue);
+                }
+                $actions[] = new LifecycleAction($action, $timeSpec, $timeValue);
+            }
+            $this->rules[] = new LifecycleRule($id, $prefix, $status, $actions);
+        }
+        return;
+    }
+
+
+    /**
+     * 生成xml字符串
+     *
+     * @return string
+     */
+    public function serializeToXml()
+    {
+
+        $xml = new \SimpleXMLElement('<?xml version="1.0" encoding="utf-8"?><LifecycleConfiguration></LifecycleConfiguration>');
+        foreach ($this->rules as $rule) {
+            $xmlRule = $xml->addChild('Rule');
+            $rule->appendToXml($xmlRule);
+        }
+        return $xml->asXML();
+    }
+
+    /**
+     *
+     * 添加LifecycleRule
+     *
+     * @param LifecycleRule $lifecycleRule
+     * @throws OssException
+     */
+    public function addRule($lifecycleRule)
+    {
+        if (!isset($lifecycleRule)) {
+            throw new OssException("lifecycleRule is null");
+        }
+        $this->rules[] = $lifecycleRule;
+    }
+
+    /**
+     *  将配置转换成字符串,便于用户查看
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->serializeToXml();
+    }
+
+    /**
+     * 得到所有的生命周期规则
+     *
+     * @return LifecycleRule[]
+     */
+    public function getRules()
+    {
+        return $this->rules;
+    }
+
+    /**
+     * @var LifecycleRule[]
+     */
+    private $rules;
+}
+
+

+ 126 - 0
addons/alioss/library/OSS/Model/LifecycleRule.php

@@ -0,0 +1,126 @@
+<?php
+
+namespace OSS\Model;
+
+
+/**
+ * Class LifecycleRule
+ * @package OSS\Model
+ *
+ * @link http://help.aliyun.com/document_detail/oss/api-reference/bucket/PutBucketLifecycle.html
+ */
+class LifecycleRule
+{
+    /**
+     * 得到规则ID
+     *
+     * @return string
+     */
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    /**
+     * @param string $id 规则ID
+     */
+    public function setId($id)
+    {
+        $this->id = $id;
+    }
+
+    /**
+     * 得到文件前缀
+     *
+     * @return string
+     */
+    public function getPrefix()
+    {
+        return $this->prefix;
+    }
+
+    /**
+     * 设置文件前缀
+     *
+     * @param string $prefix 文件前缀
+     */
+    public function setPrefix($prefix)
+    {
+        $this->prefix = $prefix;
+    }
+
+    /**
+     * Lifecycle规则的状态
+     *
+     * @return string
+     */
+    public function getStatus()
+    {
+        return $this->status;
+    }
+
+    /**
+     * 设置Lifecycle规则状态
+     *
+     * @param string $status
+     */
+    public function setStatus($status)
+    {
+        $this->status = $status;
+    }
+
+    /**
+     *
+     * @return LifecycleAction[]
+     */
+    public function getActions()
+    {
+        return $this->actions;
+    }
+
+    /**
+     * @param LifecycleAction[] $actions
+     */
+    public function setActions($actions)
+    {
+        $this->actions = $actions;
+    }
+
+
+    /**
+     * LifecycleRule constructor.
+     *
+     * @param string $id 规则ID
+     * @param string $prefix 文件前缀
+     * @param string $status 规则状态,可选[self::LIFECYCLE_STATUS_ENABLED, self::LIFECYCLE_STATUS_DISABLED]
+     * @param LifecycleAction[] $actions
+     */
+    public function __construct($id, $prefix, $status, $actions)
+    {
+        $this->id = $id;
+        $this->prefix = $prefix;
+        $this->status = $status;
+        $this->actions = $actions;
+    }
+
+    /**
+     * @param \SimpleXMLElement $xmlRule
+     */
+    public function appendToXml(&$xmlRule)
+    {
+        $xmlRule->addChild('ID', $this->id);
+        $xmlRule->addChild('Prefix', $this->prefix);
+        $xmlRule->addChild('Status', $this->status);
+        foreach ($this->actions as $action) {
+            $action->appendToXml($xmlRule);
+        }
+    }
+
+    private $id;
+    private $prefix;
+    private $status;
+    private $actions = array();
+
+    const LIFECYCLE_STATUS_ENABLED = 'Enabled';
+    const LIFECYCLE_STATUS_DISABLED = 'Disabled';
+}

+ 134 - 0
addons/alioss/library/OSS/Model/ListMultipartUploadInfo.php

@@ -0,0 +1,134 @@
+<?php
+
+namespace OSS\Model;
+
+/**
+ * Class ListMultipartUploadInfo
+ * @package OSS\Model
+ *
+ * @link http://help.aliyun.com/document_detail/oss/api-reference/multipart-upload/ListMultipartUploads.html
+ */
+class ListMultipartUploadInfo
+{
+    /**
+     * ListMultipartUploadInfo constructor.
+     *
+     * @param string $bucket
+     * @param string $keyMarker
+     * @param string $uploadIdMarker
+     * @param string $nextKeyMarker
+     * @param string $nextUploadIdMarker
+     * @param string $delimiter
+     * @param string $prefix
+     * @param int $maxUploads
+     * @param string $isTruncated
+     * @param array $uploads
+     */
+    public function __construct($bucket, $keyMarker, $uploadIdMarker, $nextKeyMarker, $nextUploadIdMarker, $delimiter, $prefix, $maxUploads, $isTruncated, array $uploads)
+    {
+        $this->bucket = $bucket;
+        $this->keyMarker = $keyMarker;
+        $this->uploadIdMarker = $uploadIdMarker;
+        $this->nextKeyMarker = $nextKeyMarker;
+        $this->nextUploadIdMarker = $nextUploadIdMarker;
+        $this->delimiter = $delimiter;
+        $this->prefix = $prefix;
+        $this->maxUploads = $maxUploads;
+        $this->isTruncated = $isTruncated;
+        $this->uploads = $uploads;
+    }
+
+    /**
+     * 得到bucket名称
+     *
+     * @return string
+     */
+    public function getBucket()
+    {
+        return $this->bucket;
+    }
+
+    /**
+     * @return string
+     */
+    public function getKeyMarker()
+    {
+        return $this->keyMarker;
+    }
+
+    /**
+     *
+     * @return string
+     */
+    public function getUploadIdMarker()
+    {
+        return $this->uploadIdMarker;
+    }
+
+    /**
+     * @return string
+     */
+    public function getNextKeyMarker()
+    {
+        return $this->nextKeyMarker;
+    }
+
+    /**
+     * @return string
+     */
+    public function getNextUploadIdMarker()
+    {
+        return $this->nextUploadIdMarker;
+    }
+
+    /**
+     * @return string
+     */
+    public function getDelimiter()
+    {
+        return $this->delimiter;
+    }
+
+    /**
+     * @return string
+     */
+    public function getPrefix()
+    {
+        return $this->prefix;
+    }
+
+    /**
+     * @return int
+     */
+    public function getMaxUploads()
+    {
+        return $this->maxUploads;
+    }
+
+    /**
+     * @return string
+     */
+    public function getIsTruncated()
+    {
+        return $this->isTruncated;
+    }
+
+    /**
+     * @return UploadInfo[]
+     */
+    public function getUploads()
+    {
+        return $this->uploads;
+    }
+
+    private $bucket = "";
+    private $keyMarker = "";
+    private $uploadIdMarker = "";
+    private $nextKeyMarker = "";
+    private $nextUploadIdMarker = "";
+    private $delimiter = "";
+    private $prefix = "";
+    private $maxUploads = 0;
+    private $isTruncated = "false";
+    private $uploads = array();
+}

+ 97 - 0
addons/alioss/library/OSS/Model/ListPartsInfo.php

@@ -0,0 +1,97 @@
+<?php
+
+namespace OSS\Model;
+
+/**
+ * Class ListPartsInfo
+ * @package OSS\Model
+ * @link http://help.aliyun.com/document_detail/oss/api-reference/multipart-upload/ListParts.html
+ */
+class ListPartsInfo
+{
+
+    /**
+     * ListPartsInfo constructor.
+     * @param string $bucket
+     * @param string $key
+     * @param string $uploadId
+     * @param int $nextPartNumberMarker
+     * @param int $maxParts
+     * @param string $isTruncated
+     * @param array $listPart
+     */
+    public function __construct($bucket, $key, $uploadId, $nextPartNumberMarker, $maxParts, $isTruncated, array $listPart)
+    {
+        $this->bucket = $bucket;
+        $this->key = $key;
+        $this->uploadId = $uploadId;
+        $this->nextPartNumberMarker = $nextPartNumberMarker;
+        $this->maxParts = $maxParts;
+        $this->isTruncated = $isTruncated;
+        $this->listPart = $listPart;
+    }
+
+    /**
+     * @return string
+     */
+    public function getBucket()
+    {
+        return $this->bucket;
+    }
+
+    /**
+     * @return string
+     */
+    public function getKey()
+    {
+        return $this->key;
+    }
+
+    /**
+     * @return string
+     */
+    public function getUploadId()
+    {
+        return $this->uploadId;
+    }
+
+    /**
+     * @return int
+     */
+    public function getNextPartNumberMarker()
+    {
+        return $this->nextPartNumberMarker;
+    }
+
+    /**
+     * @return int
+     */
+    public function getMaxParts()
+    {
+        return $this->maxParts;
+    }
+
+    /**
+     * @return string
+     */
+    public function getIsTruncated()
+    {
+        return $this->isTruncated;
+    }
+
+    /**
+     * @return array
+     */
+    public function getListPart()
+    {
+        return $this->listPart;
+    }
+
+    private $bucket = "";
+    private $key = "";
+    private $uploadId = "";
+    private $nextPartNumberMarker = 0;
+    private $maxParts = 0;
+    private $isTruncated = "";
+    private $listPart = array();
+}

+ 121 - 0
addons/alioss/library/OSS/Model/LiveChannelConfig.php

@@ -0,0 +1,121 @@
+<?php
+
+namespace OSS\Model;
+
+
+/**
+ * Class LiveChannelConfig
+ * @package OSS\Model
+ */
+class LiveChannelConfig implements XmlConfig
+{
+    public function __construct($option = array())
+    {
+        if (isset($option['description'])) {
+            $this->description = $option['description'];
+        }
+        if (isset($option['status'])) {
+            $this->status = $option['status'];
+        }
+        if (isset($option['type'])) {
+            $this->type = $option['type'];
+        }
+        if (isset($option['fragDuration'])) {
+            $this->fragDuration = $option['fragDuration'];
+        }
+        if (isset($option['fragCount'])) {
+            $this->fragCount = $option['fragCount'];
+        }
+        if (isset($option['playListName'])) {
+            $this->playListName = $option['playListName'];
+        }
+    }
+
+    public function getDescription()
+    {
+        return $this->description;
+    }
+
+    public function getStatus()
+    {
+        return $this->status;
+    }
+
+    public function getType()
+    {
+        return $this->type;
+    }
+
+    public function getFragDuration()
+    {
+        return $this->fragDuration;
+    }
+
+    public function getFragCount()
+    {
+        return $this->fragCount;
+    }
+
+    public function getPlayListName()
+    {
+        return $this->playListName;
+    }
+
+    public function parseFromXml($strXml)
+    {
+        $xml = simplexml_load_string($strXml);
+        $this->description = strval($xml->Description);
+        $this->status = strval($xml->Status);
+        $target = $xml->Target;
+        $this->type = strval($target->Type);
+        $this->fragDuration = intval($target->FragDuration);
+        $this->fragCount = intval($target->FragCount);
+        $this->playListName = strval($target->PlayListName);
+    }
+
+    public function serializeToXml()
+    {
+        $strXml = <<<EOF
+<?xml version="1.0" encoding="utf-8"?>
+<LiveChannelConfiguration>
+</LiveChannelConfiguration>
+EOF;
+        $xml = new \SimpleXMLElement($strXml);
+        if (isset($this->description)) {
+            $xml->addChild('Description', $this->description);
+        }
+
+        if (isset($this->status)) {
+            $xml->addChild('Status', $this->status);
+        }
+
+        $node = $xml->addChild('Target');
+        $node->addChild('Type', $this->type);
+
+        if (isset($this->fragDuration)) {
+            $node->addChild('FragDuration', $this->fragDuration);
+        }
+
+        if (isset($this->fragCount)) {
+            $node->addChild('FragCount', $this->fragCount);
+        }
+
+        if (isset($this->playListName)) {
+            $node->addChild('PlayListName', $this->playListName);
+        }
+
+        return $xml->asXML();
+    }
+
+    public function __toString()
+    {
+        return $this->serializeToXml();
+    }
+    
+    private $description;
+    private $status = "enabled";
+    private $type;
+    private $fragDuration = 5;
+    private $fragCount = 3;
+    private $playListName = "playlist.m3u8";
+}

+ 59 - 0
addons/alioss/library/OSS/Model/LiveChannelHistory.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace OSS\Model;
+/**
+ * Class LiveChannelHistory
+ * @package OSS\Model
+ *
+ */
+class LiveChannelHistory implements XmlConfig
+{
+    public function __construct()
+    {
+    }
+
+    public function getStartTime()
+    {
+        return $this->startTime;
+    }
+
+    public function getEndTime()
+    {
+        return $this->endTime;
+    }
+
+    public function getRemoteAddr()
+    {
+        return $this->remoteAddr;
+    }
+
+    public function parseFromXmlNode($xml)
+    {
+        if (isset($xml->StartTime)) {
+            $this->startTime = strval($xml->StartTime);
+        }
+
+        if (isset($xml->EndTime)) {
+            $this->endTime = strval($xml->EndTime);
+        }
+
+        if (isset($xml->RemoteAddr)) {
+            $this->remoteAddr = strval($xml->RemoteAddr);
+        }
+    }
+
+    public function parseFromXml($strXml)
+    {
+        $xml = simplexml_load_string($strXml);
+        $this->parseFromXmlNode($xml);
+    }
+
+    public function serializeToXml()
+    {
+        throw new OssException("Not implemented.");
+    }
+    
+    private $startTime;
+    private $endTime;
+    private $remoteAddr;
+}

+ 107 - 0
addons/alioss/library/OSS/Model/LiveChannelInfo.php

@@ -0,0 +1,107 @@
+<?php
+
+namespace OSS\Model;
+/**
+ * Class LiveChannelInfo
+ * @package OSS\Model
+ *
+ */
+class LiveChannelInfo implements XmlConfig
+{
+    public function __construct($name = null, $description = null)
+    {
+        $this->name = $name;
+        $this->description = $description;
+        $this->publishUrls = array();
+        $this->playUrls = array();
+    }
+
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    public function setName($name)
+    {
+        $this->name = $name;
+    }
+
+    public function getPublishUrls()
+    {
+        return $this->publishUrls;
+    }
+
+    public function getPlayUrls()
+    {
+        return $this->playUrls;
+    }
+
+    public function getStatus()
+    {
+        return $this->status;
+    }
+
+    public function getLastModified()
+    {
+        return $this->lastModified;
+    }
+
+    public function getDescription()
+    {
+        return $this->description;
+    }
+
+    public function setDescription($description)
+    {
+        $this->description = $description;
+    }
+
+    public function parseFromXmlNode($xml)
+    {
+        if (isset($xml->Name)) {
+            $this->name = strval($xml->Name);
+        }
+
+        if (isset($xml->Description)) {
+            $this->description = strval($xml->Description);
+        }
+
+        if (isset($xml->Status)) {
+            $this->status = strval($xml->Status);
+        }
+
+        if (isset($xml->LastModified)) {
+            $this->lastModified = strval($xml->LastModified);
+        }
+
+        if (isset($xml->PublishUrls)) {
+            foreach ($xml->PublishUrls as $url) {
+                $this->publishUrls[] = strval($url->Url);
+            }
+        }
+
+        if (isset($xml->PlayUrls)) {
+            foreach ($xml->PlayUrls as $url) {
+                $this->playUrls[] = strval($url->Url);
+            }
+        }
+    }
+
+    public function parseFromXml($strXml)
+    {
+        $xml = simplexml_load_string($strXml);
+        $this->parseFromXmlNode($xml);
+    }
+
+    public function serializeToXml()
+    {
+        throw new OssException("Not implemented.");
+    }
+    
+    private $name;
+    private $description;
+    private $publishUrls;
+    private $playUrls;
+    private $status;
+    private $lastModified;
+}

+ 107 - 0
addons/alioss/library/OSS/Model/LiveChannelListInfo.php

@@ -0,0 +1,107 @@
+<?php
+
+namespace OSS\Model;
+
+/**
+ * Class LiveChannelListInfo
+ *
+ * ListBucketLiveChannels接口返回数据
+ *
+ * @package OSS\Model
+ * @link http://help.aliyun.com/document_detail/oss/api-reference/bucket/GetBucket.html
+ */
+class LiveChannelListInfo implements XmlConfig
+{
+    /**
+     * @return string
+     */
+    public function getBucketName()
+    {
+        return $this->bucket;
+    }
+
+    public function setBucketName($name)
+    {
+        $this->bucket = $name;
+    }
+
+    /**
+     * @return string
+     */
+    public function getPrefix()
+    {
+        return $this->prefix;
+    }
+
+    /**
+     * @return string
+     */
+    public function getMarker()
+    {
+        return $this->marker;
+    }
+
+    /**
+     * @return int
+     */
+    public function getMaxKeys()
+    {
+        return $this->maxKeys;
+    }
+
+    /**
+     * @return mixed
+     */
+    public function getIsTruncated()
+    {
+        return $this->isTruncated;
+    }
+
+    /**
+     * @return LiveChannelInfo[]
+     */
+    public function getChannelList()
+    {
+        return $this->channelList;
+    }
+
+    /**
+     * @return string
+     */
+    public function getNextMarker()
+    {
+        return $this->nextMarker;
+    }
+
+    public function parseFromXml($strXml)
+    {
+        $xml = simplexml_load_string($strXml);
+
+        $this->prefix = strval($xml->Prefix);
+        $this->marker = strval($xml->Marker);
+        $this->maxKeys = intval($xml->MaxKeys);
+        $this->isTruncated = (strval($xml->IsTruncated) == 'true');
+        $this->nextMarker = strval($xml->NextMarker);
+
+        if (isset($xml->LiveChannel)) {
+            foreach ($xml->LiveChannel as $chan) {
+                $channel = new LiveChannelInfo();
+                $channel->parseFromXmlNode($chan);
+                $this->channelList[] = $channel;
+            }
+        }
+    }
+
+    public function serializeToXml()
+    {
+        throw new OssException("Not implemented.");
+    }
+    
+    private $bucket = '';
+    private $prefix = '';
+    private $marker = '';
+    private $nextMarker = '';
+    private $maxKeys = 100;
+    private $isTruncated = 'false';
+    private $channelList = array();
+}

+ 86 - 0
addons/alioss/library/OSS/Model/LoggingConfig.php

@@ -0,0 +1,86 @@
+<?php
+
+namespace OSS\Model;
+
+
+/**
+ * Class LoggingConfig
+ * @package OSS\Model
+ * @link http://help.aliyun.com/document_detail/oss/api-reference/bucket/PutBucketLogging.html
+ */
+class LoggingConfig implements XmlConfig
+{
+    /**
+     * LoggingConfig constructor.
+     * @param null $targetBucket
+     * @param null $targetPrefix
+     */
+    public function __construct($targetBucket = null, $targetPrefix = null)
+    {
+        $this->targetBucket = $targetBucket;
+        $this->targetPrefix = $targetPrefix;
+    }
+
+    /**
+     * @param $strXml
+     * @return null
+     */
+    public function parseFromXml($strXml)
+    {
+        $xml = simplexml_load_string($strXml);
+        if (!isset($xml->LoggingEnabled)) return;
+        foreach ($xml->LoggingEnabled as $status) {
+            foreach ($status as $key => $value) {
+                if ($key === 'TargetBucket') {
+                    $this->targetBucket = strval($value);
+                } elseif ($key === 'TargetPrefix') {
+                    $this->targetPrefix = strval($value);
+                }
+            }
+            break;
+        }
+    }
+
+    /**
+     *  序列化成xml字符串
+     *
+     */
+    public function serializeToXml()
+    {
+        $xml = new \SimpleXMLElement('<?xml version="1.0" encoding="utf-8"?><BucketLoggingStatus></BucketLoggingStatus>');
+        if (isset($this->targetBucket) && isset($this->targetPrefix)) {
+            $loggingEnabled = $xml->addChild('LoggingEnabled');
+            $loggingEnabled->addChild('TargetBucket', $this->targetBucket);
+            $loggingEnabled->addChild('TargetPrefix', $this->targetPrefix);
+        }
+        return $xml->asXML();
+    }
+
+    /**
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->serializeToXml();
+    }
+
+    /**
+     * @return string
+     */
+    public function getTargetBucket()
+    {
+        return $this->targetBucket;
+    }
+
+    /**
+     * @return string
+     */
+    public function getTargetPrefix()
+    {
+        return $this->targetPrefix;
+    }
+
+    private $targetBucket = "";
+    private $targetPrefix = "";
+
+}

+ 93 - 0
addons/alioss/library/OSS/Model/ObjectInfo.php

@@ -0,0 +1,93 @@
+<?php
+
+namespace OSS\Model;
+
+/**
+ *
+ * Class ObjectInfo
+ *
+ * listObjects接口中返回的Object列表中的类
+ *
+ * listObjects接口返回数据中包含两个Array
+ * 一个是拿到的Object列表【可以理解成对应文件系统中的文件列表】
+ * 一个是拿到的Prefix列表【可以理解成对应文件系统中的目录列表】
+ *
+ * @package OSS\Model
+ */
+class ObjectInfo
+{
+    /**
+     * ObjectInfo constructor.
+     *
+     * @param string $key
+     * @param string $lastModified
+     * @param string $eTag
+     * @param string $type
+     * @param int $size
+     * @param string $storageClass
+     */
+    public function __construct($key, $lastModified, $eTag, $type, $size, $storageClass)
+    {
+        $this->key = $key;
+        $this->lastModified = $lastModified;
+        $this->eTag = $eTag;
+        $this->type = $type;
+        $this->size = $size;
+        $this->storageClass = $storageClass;
+    }
+
+    /**
+     * @return string
+     */
+    public function getKey()
+    {
+        return $this->key;
+    }
+
+    /**
+     * @return string
+     */
+    public function getLastModified()
+    {
+        return $this->lastModified;
+    }
+
+    /**
+     * @return string
+     */
+    public function getETag()
+    {
+        return $this->eTag;
+    }
+
+    /**
+     * @return string
+     */
+    public function getType()
+    {
+        return $this->type;
+    }
+
+    /**
+     * @return int
+     */
+    public function getSize()
+    {
+        return $this->size;
+    }
+
+    /**
+     * @return string
+     */
+    public function getStorageClass()
+    {
+        return $this->storageClass;
+    }
+
+    private $key = "";
+    private $lastModified = "";
+    private $eTag = "";
+    private $type = "";
+    private $size = 0;
+    private $storageClass = "";
+}

+ 126 - 0
addons/alioss/library/OSS/Model/ObjectListInfo.php

@@ -0,0 +1,126 @@
+<?php
+
+namespace OSS\Model;
+
+/**
+ * Class ObjectListInfo
+ *
+ * ListObjects接口返回数据
+ *
+ * @package OSS\Model
+ * @link http://help.aliyun.com/document_detail/oss/api-reference/bucket/GetBucket.html
+ */
+class ObjectListInfo
+{
+    /**
+     * ObjectListInfo constructor.
+     *
+     * @param string $bucketName
+     * @param string $prefix
+     * @param string $marker
+     * @param string $nextMarker
+     * @param string $maxKeys
+     * @param string $delimiter
+     * @param null $isTruncated
+     * @param array $objectList
+     * @param array $prefixList
+     */
+    public function __construct($bucketName, $prefix, $marker, $nextMarker, $maxKeys, $delimiter, $isTruncated, array $objectList, array $prefixList)
+    {
+        $this->bucketName = $bucketName;
+        $this->prefix = $prefix;
+        $this->marker = $marker;
+        $this->nextMarker = $nextMarker;
+        $this->maxKeys = $maxKeys;
+        $this->delimiter = $delimiter;
+        $this->isTruncated = $isTruncated;
+        $this->objectList = $objectList;
+        $this->prefixList = $prefixList;
+    }
+
+    /**
+     * @return string
+     */
+    public function getBucketName()
+    {
+        return $this->bucketName;
+    }
+
+    /**
+     * @return string
+     */
+    public function getPrefix()
+    {
+        return $this->prefix;
+    }
+
+    /**
+     * @return string
+     */
+    public function getMarker()
+    {
+        return $this->marker;
+    }
+
+    /**
+     * @return int
+     */
+    public function getMaxKeys()
+    {
+        return $this->maxKeys;
+    }
+
+    /**
+     * @return string
+     */
+    public function getDelimiter()
+    {
+        return $this->delimiter;
+    }
+
+    /**
+     * @return mixed
+     */
+    public function getIsTruncated()
+    {
+        return $this->isTruncated;
+    }
+
+    /**
+     * 返回ListObjects接口返回数据中的ObjectInfo列表
+     *
+     * @return ObjectInfo[]
+     */
+    public function getObjectList()
+    {
+        return $this->objectList;
+    }
+
+    /**
+     * 返回ListObjects接口返回数据中的PrefixInfo列表
+     *
+     * @return PrefixInfo[]
+     */
+    public function getPrefixList()
+    {
+        return $this->prefixList;
+    }
+
+    /**
+     * @return string
+     */
+    public function getNextMarker()
+    {
+        return $this->nextMarker;
+    }
+
+    private $bucketName = "";
+    private $prefix = "";
+    private $marker = "";
+    private $nextMarker = "";
+    private $maxKeys = 0;
+    private $delimiter = "";
+    private $isTruncated = null;
+    private $objectList = array();
+    private $prefixList = array();
+}

+ 63 - 0
addons/alioss/library/OSS/Model/PartInfo.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace OSS\Model;
+
+/**
+ * Class PartInfo
+ * @package OSS\Model
+ */
+class PartInfo
+{
+    /**
+     * PartInfo constructor.
+     *
+     * @param int $partNumber
+     * @param string $lastModified
+     * @param string $eTag
+     * @param int $size
+     */
+    public function __construct($partNumber, $lastModified, $eTag, $size)
+    {
+        $this->partNumber = $partNumber;
+        $this->lastModified = $lastModified;
+        $this->eTag = $eTag;
+        $this->size = $size;
+    }
+
+    /**
+     * @return int
+     */
+    public function getPartNumber()
+    {
+        return $this->partNumber;
+    }
+
+    /**
+     * @return string
+     */
+    public function getLastModified()
+    {
+        return $this->lastModified;
+    }
+
+    /**
+     * @return string
+     */
+    public function getETag()
+    {
+        return $this->eTag;
+    }
+
+    /**
+     * @return int
+     */
+    public function getSize()
+    {
+        return $this->size;
+    }
+
+    private $partNumber = 0;
+    private $lastModified = "";
+    private $eTag = "";
+    private $size = 0;
+}

+ 36 - 0
addons/alioss/library/OSS/Model/PrefixInfo.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace OSS\Model;
+
+/**
+ * Class PrefixInfo
+ *
+ * listObjects接口中返回的Prefix列表中的类
+ * listObjects接口返回数据中包含两个Array:
+ * 一个是拿到的Object列表【可以理解成对应文件系统中的文件列表】
+ * 一个是拿到的Prefix列表【可以理解成对应文件系统中的目录列表】
+ *
+ * @package OSS\Model
+ * @link http://help.aliyun.com/document_detail/oss/api-reference/bucket/GetBucket.html
+ */
+class PrefixInfo
+{
+    /**
+     * PrefixInfo constructor.
+     * @param string $prefix
+     */
+    public function __construct($prefix)
+    {
+        $this->prefix = $prefix;
+    }
+
+    /**
+     * @return string
+     */
+    public function getPrefix()
+    {
+        return $this->prefix;
+    }
+
+    private $prefix;
+}

+ 93 - 0
addons/alioss/library/OSS/Model/RefererConfig.php

@@ -0,0 +1,93 @@
+<?php
+
+namespace OSS\Model;
+
+/**
+ * Class RefererConfig
+ *
+ * @package OSS\Model
+ * @link http://help.aliyun.com/document_detail/oss/api-reference/bucket/PutBucketReferer.html
+ */
+class RefererConfig implements XmlConfig
+{
+    /**
+     * @param string $strXml
+     * @return null
+     */
+    public function parseFromXml($strXml)
+    {
+        $xml = simplexml_load_string($strXml);
+        if (!isset($xml->AllowEmptyReferer)) return;
+        if (!isset($xml->RefererList)) return;
+        $this->allowEmptyReferer =
+            (strval($xml->AllowEmptyReferer) === 'TRUE' || strval($xml->AllowEmptyReferer) === 'true') ? true : false;
+
+        foreach ($xml->RefererList->Referer as $key => $refer) {
+            $this->refererList[] = strval($refer);
+        }
+    }
+
+
+    /**
+     * 把RefererConfig序列化成xml
+     *
+     * @return string
+     */
+    public function serializeToXml()
+    {
+        $xml = new \SimpleXMLElement('<?xml version="1.0" encoding="utf-8"?><RefererConfiguration></RefererConfiguration>');
+        if ($this->allowEmptyReferer) {
+            $xml->addChild('AllowEmptyReferer', 'true');
+        } else {
+            $xml->addChild('AllowEmptyReferer', 'false');
+        }
+        $refererList = $xml->addChild('RefererList');
+        foreach ($this->refererList as $referer) {
+            $refererList->addChild('Referer', $referer);
+        }
+        return $xml->asXML();
+    }
+
+    /**
+     * @return string
+     */
+    function __toString()
+    {
+        return $this->serializeToXml();
+    }
+
+    /**
+     * @param boolean $allowEmptyReferer
+     */
+    public function setAllowEmptyReferer($allowEmptyReferer)
+    {
+        $this->allowEmptyReferer = $allowEmptyReferer;
+    }
+
+    /**
+     * @param string $referer
+     */
+    public function addReferer($referer)
+    {
+        $this->refererList[] = $referer;
+    }
+
+    /**
+     * @return boolean
+     */
+    public function isAllowEmptyReferer()
+    {
+        return $this->allowEmptyReferer;
+    }
+
+    /**
+     * @return array
+     */
+    public function getRefererList()
+    {
+        return $this->refererList;
+    }
+
+    private $allowEmptyReferer = true;
+    private $refererList = array();
+}

+ 74 - 0
addons/alioss/library/OSS/Model/StorageCapacityConfig.php

@@ -0,0 +1,74 @@
+<?php
+
+namespace OSS\Model;
+
+/**
+ * Class StorageCapacityConfig
+ *
+ * @package OSS\Model
+ * @link http://docs.alibaba-inc.com/pages/viewpage.action?pageId=271614763
+ */
+class StorageCapacityConfig implements XmlConfig
+{
+    /**
+     * StorageCapacityConfig constructor.
+     *
+     * @param int $storageCapacity            
+     */
+    public function __construct($storageCapacity)
+    {
+        $this->storageCapacity = $storageCapacity;
+    }
+
+    /**
+     * Not implemented
+     */
+    public function parseFromXml($strXml)
+    {
+        throw new OssException("Not implemented.");
+    }
+
+    /**
+     * 把StorageCapacityConfig序列化成xml
+     *
+     * @return string
+     */
+    public function serializeToXml()
+    {
+        $xml = new \SimpleXMLElement('<?xml version="1.0" encoding="utf-8"?><BucketUserQos></BucketUserQos>');
+        $xml->addChild('StorageCapacity', strval($this->storageCapacity));
+        return $xml->asXML();
+    }
+
+    /**
+     * To string
+     *
+     * @return string
+     */
+    function __toString()
+    {
+        return $this->serializeToXml();
+    }
+
+    /**
+     * Set storage capacity
+     *
+     * @param int $storageCapacity            
+     */
+    public function setStorageCapacity($storageCapacity)
+    {
+        $this->storageCapacity = $storageCapacity;
+    }
+
+    /**
+     * Get storage capacity
+     * 
+     * @return int
+     */
+    public function getStorageCapacity()
+    {
+        return $this->storageCapacity;
+    }
+
+    private $storageCapacity = 0;
+}

+ 55 - 0
addons/alioss/library/OSS/Model/UploadInfo.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace OSS\Model;
+
+/**
+ * Class UploadInfo
+ *
+ * ListMultipartUpload接口得到的UploadInfo
+ *
+ * @package OSS\Model
+ */
+class UploadInfo
+{
+    /**
+     * UploadInfo constructor.
+     *
+     * @param string $key
+     * @param string $uploadId
+     * @param string $initiated
+     */
+    public function __construct($key, $uploadId, $initiated)
+    {
+        $this->key = $key;
+        $this->uploadId = $uploadId;
+        $this->initiated = $initiated;
+    }
+
+    /**
+     * @return string
+     */
+    public function getKey()
+    {
+        return $this->key;
+    }
+
+    /**
+     * @return string
+     */
+    public function getUploadId()
+    {
+        return $this->uploadId;
+    }
+
+    /**
+     * @return string
+     */
+    public function getInitiated()
+    {
+        return $this->initiated;
+    }
+
+    private $key = "";
+    private $uploadId = "";
+    private $initiated = "";
+}

+ 76 - 0
addons/alioss/library/OSS/Model/WebsiteConfig.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace OSS\Model;
+
+
+use OSS\Core\OssException;
+
+
+/**
+ * Class WebsiteConfig
+ * @package OSS\Model
+ * @link http://help.aliyun.com/document_detail/oss/api-reference/bucket/PutBucketWebsite.html
+ */
+class WebsiteConfig implements XmlConfig
+{
+    /**
+     * WebsiteConfig constructor.
+     * @param  string $indexDocument
+     * @param  string $errorDocument
+     */
+    public function __construct($indexDocument = "", $errorDocument = "")
+    {
+        $this->indexDocument = $indexDocument;
+        $this->errorDocument = $errorDocument;
+    }
+
+    /**
+     * @param string $strXml
+     * @return null
+     */
+    public function parseFromXml($strXml)
+    {
+        $xml = simplexml_load_string($strXml);
+        if (isset($xml->IndexDocument) && isset($xml->IndexDocument->Suffix)) {
+            $this->indexDocument = strval($xml->IndexDocument->Suffix);
+        }
+        if (isset($xml->ErrorDocument) && isset($xml->ErrorDocument->Key)) {
+            $this->errorDocument = strval($xml->ErrorDocument->Key);
+        }
+    }
+
+    /**
+     * 把WebsiteConfig序列化成xml
+     *
+     * @return string
+     * @throws OssException
+     */
+    public function serializeToXml()
+    {
+        $xml = new \SimpleXMLElement('<?xml version="1.0" encoding="utf-8"?><WebsiteConfiguration></WebsiteConfiguration>');
+        $index_document_part = $xml->addChild('IndexDocument');
+        $error_document_part = $xml->addChild('ErrorDocument');
+        $index_document_part->addChild('Suffix', $this->indexDocument);
+        $error_document_part->addChild('Key', $this->errorDocument);
+        return $xml->asXML();
+    }
+
+    /**
+     * @return string
+     */
+    public function getIndexDocument()
+    {
+        return $this->indexDocument;
+    }
+
+    /**
+     * @return string
+     */
+    public function getErrorDocument()
+    {
+        return $this->errorDocument;
+    }
+
+    private $indexDocument = "";
+    private $errorDocument = "";
+}

+ 27 - 0
addons/alioss/library/OSS/Model/XmlConfig.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace OSS\Model;
+
+/**
+ * Interface XmlConfig
+ * @package OSS\Model
+ */
+interface XmlConfig
+{
+
+    /**
+     * 接口定义,实现此接口的类都需要实现从xml数据解析的函数
+     *
+     * @param string $strXml
+     * @return null
+     */
+    public function parseFromXml($strXml);
+
+    /**
+     * 接口定义,实现此接口的类,都需要实现把子类序列化成xml字符串的接口
+     *
+     * @return string
+     */
+    public function serializeToXml();
+
+}

+ 2739 - 0
addons/alioss/library/OSS/OssClient.php

@@ -0,0 +1,2739 @@
+<?php
+
+namespace OSS;
+
+use OSS\Core\MimeTypes;
+use OSS\Core\OssException;
+use OSS\Http\RequestCore;
+use OSS\Http\RequestCore_Exception;
+use OSS\Http\ResponseCore;
+use OSS\Model\CorsConfig;
+use OSS\Model\CnameConfig;
+use OSS\Model\LoggingConfig;
+use OSS\Model\LiveChannelConfig;
+use OSS\Model\LiveChannelInfo;
+use OSS\Model\LiveChannelListInfo;
+use OSS\Model\StorageCapacityConfig;
+use OSS\Result\AclResult;
+use OSS\Result\BodyResult;
+use OSS\Result\GetCorsResult;
+use OSS\Result\GetLifecycleResult;
+use OSS\Result\GetLoggingResult;
+use OSS\Result\GetRefererResult;
+use OSS\Result\GetWebsiteResult;
+use OSS\Result\GetCnameResult;
+use OSS\Result\GetLocationResult;
+use OSS\Result\HeaderResult;
+use OSS\Result\InitiateMultipartUploadResult;
+use OSS\Result\ListBucketsResult;
+use OSS\Result\ListMultipartUploadResult;
+use OSS\Model\ListMultipartUploadInfo;
+use OSS\Result\ListObjectsResult;
+use OSS\Result\ListPartsResult;
+use OSS\Result\PutSetDeleteResult;
+use OSS\Result\DeleteObjectsResult;
+use OSS\Result\CopyObjectResult;
+use OSS\Result\CallbackResult;
+use OSS\Result\ExistResult;
+use OSS\Result\PutLiveChannelResult;
+use OSS\Result\GetLiveChannelHistoryResult;
+use OSS\Result\GetLiveChannelInfoResult;
+use OSS\Result\GetLiveChannelStatusResult;
+use OSS\Result\ListLiveChannelResult;
+use OSS\Result\GetStorageCapacityResult;
+use OSS\Result\AppendResult;
+use OSS\Model\ObjectListInfo;
+use OSS\Result\UploadPartResult;
+use OSS\Model\BucketListInfo;
+use OSS\Model\LifecycleConfig;
+use OSS\Model\RefererConfig;
+use OSS\Model\WebsiteConfig;
+use OSS\Core\OssUtil;
+use OSS\Model\ListPartsInfo;
+use OSS\Result\SymlinkResult;
+
+/**
+ * Class OssClient
+ *
+ * Object Storage Service(OSS) 的客户端类,封装了用户通过OSS API对OSS服务的各种操作,
+ * 用户通过OssClient实例可以进行Bucket,Object,MultipartUpload, ACL等操作,具体
+ * 的接口规则可以参考官方OSS API文档
+ */
+class OssClient
+{
+    /**
+     * 构造函数
+     *
+     * 构造函数有几种情况:
+     * 1. 一般的时候初始化使用 $ossClient = new OssClient($id, $key, $endpoint)
+     * 2. 如果使用CNAME的,比如使用的是www.testoss.com,在控制台上做了CNAME的绑定,
+     * 初始化使用 $ossClient = new OssClient($id, $key, $endpoint, true)
+     * 3. 如果使用了阿里云SecurityTokenService(STS),获得了AccessKeyID, AccessKeySecret, Token
+     * 初始化使用  $ossClient = new OssClient($id, $key, $endpoint, false, $token)
+     * 4. 如果用户使用的endpoint是ip
+     * 初始化使用 $ossClient = new OssClient($id, $key, “1.2.3.4:8900”)
+     *
+     * @param string  $accessKeyId     从OSS获得的AccessKeyId
+     * @param string  $accessKeySecret 从OSS获得的AccessKeySecret
+     * @param string  $endpoint        您选定的OSS数据中心访问域名,例如oss-cn-hangzhou.aliyuncs.com
+     * @param boolean $isCName         是否对Bucket做了域名绑定,并且Endpoint参数填写的是自己的域名
+     * @param string  $securityToken
+     * @param string  $requestProxy    添加代理支持
+     * @throws OssException
+     */
+    public function __construct($accessKeyId, $accessKeySecret, $endpoint, $isCName = false, $securityToken = null, $requestProxy = null)
+    {
+        $accessKeyId = trim($accessKeyId);
+        $accessKeySecret = trim($accessKeySecret);
+        $endpoint = trim(trim($endpoint), "/");
+
+        if (empty($accessKeyId)) {
+            throw new OssException("access key id is empty");
+        }
+        if (empty($accessKeySecret)) {
+            throw new OssException("access key secret is empty");
+        }
+        if (empty($endpoint)) {
+            throw new OssException("endpoint is empty");
+        }
+        $this->hostname = $this->checkEndpoint($endpoint, $isCName);
+        $this->accessKeyId = $accessKeyId;
+        $this->accessKeySecret = $accessKeySecret;
+        $this->securityToken = $securityToken;
+        $this->requestProxy = $requestProxy;
+
+        self::checkEnv();
+    }
+
+    /**
+     * 列举用户所有的Bucket[GetService], Endpoint类型为cname不能进行此操作
+     *
+     * @param array $options
+     * @return BucketListInfo
+     * @throws OssException
+     */
+    public function listBuckets($options = null)
+    {
+        if ($this->hostType === self::OSS_HOST_TYPE_CNAME) {
+            throw new OssException("operation is not permitted with CName host");
+        }
+        $this->precheckOptions($options);
+        $options[self::OSS_BUCKET] = '';
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_OBJECT] = '/';
+        $response = $this->auth($options);
+        $result = new ListBucketsResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 创建bucket,默认创建的bucket的ACL是OssClient::OSS_ACL_TYPE_PRIVATE
+     *
+     * @param string $bucket
+     * @param string $acl
+     * @param array  $options
+     * @param string $storageType
+     * @return null
+     */
+    public function createBucket($bucket, $acl = self::OSS_ACL_TYPE_PRIVATE, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_PUT;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_HEADERS] = array(self::OSS_ACL => $acl);
+        if (isset($options[self::OSS_STORAGE])) {
+            $this->precheckStorage($options[self::OSS_STORAGE]);
+            $options[self::OSS_CONTENT] = OssUtil::createBucketXmlBody($options[self::OSS_STORAGE]);
+            unset($options[self::OSS_STORAGE]);
+        }
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 删除bucket
+     * 如果Bucket不为空(Bucket中有Object,或者有分块上传的碎片),则Bucket无法删除,
+     * 必须删除Bucket中的所有Object以及碎片后,Bucket才能成功删除。
+     *
+     * @param string $bucket
+     * @param array  $options
+     * @return null
+     */
+    public function deleteBucket($bucket, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_DELETE;
+        $options[self::OSS_OBJECT] = '/';
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 判断bucket是否存在
+     *
+     * @param string $bucket
+     * @return bool
+     * @throws OssException
+     */
+    public function doesBucketExist($bucket)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'acl';
+        $response = $this->auth($options);
+        $result = new ExistResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获取bucket所属的数据中心位置信息
+     *
+     * @param string $bucket
+     * @param array  $options
+     * @return string
+     * @throws OssException
+     */
+    public function getBucketLocation($bucket, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'location';
+        $response = $this->auth($options);
+        $result = new GetLocationResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获取Bucket的Meta信息
+     *
+     * @param string $bucket
+     * @param array  $options 具体参考SDK文档
+     * @return array
+     */
+    public function getBucketMeta($bucket, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_HEAD;
+        $options[self::OSS_OBJECT] = '/';
+        $response = $this->auth($options);
+        $result = new HeaderResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获取bucket的ACL配置情况
+     *
+     * @param string $bucket
+     * @param array  $options
+     * @return string
+     * @throws OssException
+     */
+    public function getBucketAcl($bucket, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'acl';
+        $response = $this->auth($options);
+        $result = new AclResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 设置bucket的ACL配置情况
+     *
+     * @param string $bucket  bucket名称
+     * @param string $acl     读写权限,可选值 ['private', 'public-read', 'public-read-write']
+     * @param array  $options 可以为空
+     * @return null
+     * @throws OssException
+     */
+    public function putBucketAcl($bucket, $acl, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_PUT;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_HEADERS] = array(self::OSS_ACL => $acl);
+        $options[self::OSS_SUB_RESOURCE] = 'acl';
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获取object的ACL属性
+     *
+     * @param string $bucket
+     * @param string $object
+     * @return string
+     * @throws OssException
+     */
+    public function getObjectAcl($bucket, $object)
+    {
+        $options = array();
+        $this->precheckCommon($bucket, $object, $options, true);
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_OBJECT] = $object;
+        $options[self::OSS_SUB_RESOURCE] = 'acl';
+        $response = $this->auth($options);
+        $result = new AclResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 设置object的ACL属性
+     *
+     * @param string $bucket bucket名称
+     * @param string $object object名称
+     * @param string $acl    读写权限,可选值 ['default', 'private', 'public-read', 'public-read-write']
+     * @return null
+     * @throws OssException
+     */
+    public function putObjectAcl($bucket, $object, $acl)
+    {
+        $this->precheckCommon($bucket, $object, $options, true);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_PUT;
+        $options[self::OSS_OBJECT] = $object;
+        $options[self::OSS_HEADERS] = array(self::OSS_OBJECT_ACL => $acl);
+        $options[self::OSS_SUB_RESOURCE] = 'acl';
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获取Bucket的访问日志配置情况
+     *
+     * @param string $bucket  bucket名称
+     * @param array  $options 可以为空
+     * @return LoggingConfig
+     * @throws OssException
+     */
+    public function getBucketLogging($bucket, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'logging';
+        $response = $this->auth($options);
+        $result = new GetLoggingResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 开启Bucket访问日志记录功能,只有Bucket的所有者才能更改
+     *
+     * @param string $bucket       bucket名称
+     * @param string $targetBucket 日志文件存放的bucket
+     * @param string $targetPrefix 日志的文件前缀
+     * @param array  $options      可以为空
+     * @return null
+     * @throws OssException
+     */
+    public function putBucketLogging($bucket, $targetBucket, $targetPrefix, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $this->precheckBucket($targetBucket, 'targetbucket is not allowed empty');
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_PUT;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'logging';
+        $options[self::OSS_CONTENT_TYPE] = 'application/xml';
+
+        $loggingConfig = new LoggingConfig($targetBucket, $targetPrefix);
+        $options[self::OSS_CONTENT] = $loggingConfig->serializeToXml();
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 关闭bucket访问日志记录功能
+     *
+     * @param string $bucket  bucket名称
+     * @param array  $options 可以为空
+     * @return null
+     * @throws OssException
+     */
+    public function deleteBucketLogging($bucket, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_DELETE;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'logging';
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 将bucket设置成静态网站托管模式
+     *
+     * @param string        $bucket  bucket名称
+     * @param WebsiteConfig $websiteConfig
+     * @param array         $options 可以为空
+     * @return null
+     * @throws OssException
+     */
+    public function putBucketWebsite($bucket, $websiteConfig, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_PUT;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'website';
+        $options[self::OSS_CONTENT_TYPE] = 'application/xml';
+        $options[self::OSS_CONTENT] = $websiteConfig->serializeToXml();
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获取bucket的静态网站托管状态
+     *
+     * @param string $bucket bucket名称
+     * @param array  $options
+     * @return WebsiteConfig
+     * @throws OssException
+     */
+    public function getBucketWebsite($bucket, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'website';
+        $response = $this->auth($options);
+        $result = new GetWebsiteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 关闭bucket的静态网站托管模式
+     *
+     * @param string $bucket bucket名称
+     * @param array  $options
+     * @return null
+     * @throws OssException
+     */
+    public function deleteBucketWebsite($bucket, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_DELETE;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'website';
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 在指定的bucket上设定一个跨域资源共享(CORS)的规则,如果原规则存在则覆盖原规则
+     *
+     * @param string     $bucket     bucket名称
+     * @param CorsConfig $corsConfig 跨域资源共享配置,具体规则参见SDK文档
+     * @param array      $options    array
+     * @return null
+     * @throws OssException
+     */
+    public function putBucketCors($bucket, $corsConfig, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_PUT;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'cors';
+        $options[self::OSS_CONTENT_TYPE] = 'application/xml';
+        $options[self::OSS_CONTENT] = $corsConfig->serializeToXml();
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获取Bucket的CORS配置情况
+     *
+     * @param string $bucket  bucket名称
+     * @param array  $options 可以为空
+     * @return CorsConfig
+     * @throws OssException
+     */
+    public function getBucketCors($bucket, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'cors';
+        $response = $this->auth($options);
+        $result = new GetCorsResult($response, __FUNCTION__);
+        return $result->getData();
+    }
+
+    /**
+     * 关闭指定Bucket对应的CORS功能并清空所有规则
+     *
+     * @param string $bucket bucket名称
+     * @param array  $options
+     * @return null
+     * @throws OssException
+     */
+    public function deleteBucketCors($bucket, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_DELETE;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'cors';
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 为指定Bucket增加CNAME绑定
+     *
+     * @param string $bucket bucket名称
+     * @param string $cname
+     * @param array  $options
+     * @return null
+     * @throws OssException
+     */
+    public function addBucketCname($bucket, $cname, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_POST;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'cname';
+        $options[self::OSS_CONTENT_TYPE] = 'application/xml';
+        $cnameConfig = new CnameConfig();
+        $cnameConfig->addCname($cname);
+        $options[self::OSS_CONTENT] = $cnameConfig->serializeToXml();
+        $options[self::OSS_COMP] = 'add';
+
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获取指定Bucket已绑定的CNAME列表
+     *
+     * @param string $bucket bucket名称
+     * @param array  $options
+     * @return CnameConfig
+     * @throws OssException
+     */
+    public function getBucketCname($bucket, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'cname';
+        $response = $this->auth($options);
+        $result = new GetCnameResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 解除指定Bucket的CNAME绑定
+     *
+     * @param string      $bucket bucket名称
+     * @param CnameConfig $cnameConfig
+     * @param array       $options
+     * @return null
+     * @throws OssException
+     */
+    public function deleteBucketCname($bucket, $cname, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_POST;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'cname';
+        $options[self::OSS_CONTENT_TYPE] = 'application/xml';
+        $cnameConfig = new CnameConfig();
+        $cnameConfig->addCname($cname);
+        $options[self::OSS_CONTENT] = $cnameConfig->serializeToXml();
+        $options[self::OSS_COMP] = 'delete';
+
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 为指定Bucket创建LiveChannel
+     *
+     * @param string            $bucket bucket名称
+     * @param string channelName  $channelName
+     * @param LiveChannelConfig $channelConfig
+     * @param array             $options
+     * @return LiveChannelInfo
+     * @throws OssException
+     */
+    public function putBucketLiveChannel($bucket, $channelName, $channelConfig, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_PUT;
+        $options[self::OSS_OBJECT] = $channelName;
+        $options[self::OSS_SUB_RESOURCE] = 'live';
+        $options[self::OSS_CONTENT_TYPE] = 'application/xml';
+        $options[self::OSS_CONTENT] = $channelConfig->serializeToXml();
+
+        $response = $this->auth($options);
+        $result = new PutLiveChannelResult($response);
+        $info = $result->getData();
+        $info->setName($channelName);
+        $info->setDescription($channelConfig->getDescription());
+
+        return $info;
+    }
+
+    /**
+     * 设置LiveChannel的status
+     *
+     * @param string $bucket bucket名称
+     * @param string channelName $channelName
+     * @param string channelStatus $channelStatus 为enabled或disabled
+     * @param array  $options
+     * @return null
+     * @throws OssException
+     */
+    public function putLiveChannelStatus($bucket, $channelName, $channelStatus, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_PUT;
+        $options[self::OSS_OBJECT] = $channelName;
+        $options[self::OSS_SUB_RESOURCE] = 'live';
+        $options[self::OSS_LIVE_CHANNEL_STATUS] = $channelStatus;
+
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获取LiveChannel信息
+     *
+     * @param string $bucket bucket名称
+     * @param string channelName $channelName
+     * @param array  $options
+     * @return GetLiveChannelInfo
+     * @throws OssException
+     */
+    public function getLiveChannelInfo($bucket, $channelName, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_OBJECT] = $channelName;
+        $options[self::OSS_SUB_RESOURCE] = 'live';
+
+        $response = $this->auth($options);
+        $result = new GetLiveChannelInfoResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获取LiveChannel状态信息
+     *
+     * @param string $bucket bucket名称
+     * @param string channelName $channelName
+     * @param array  $options
+     * @return GetLiveChannelStatus
+     * @throws OssException
+     */
+    public function getLiveChannelStatus($bucket, $channelName, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_OBJECT] = $channelName;
+        $options[self::OSS_SUB_RESOURCE] = 'live';
+        $options[self::OSS_COMP] = 'stat';
+
+        $response = $this->auth($options);
+        $result = new GetLiveChannelStatusResult($response);
+        return $result->getData();
+    }
+
+    /**
+     *获取LiveChannel推流记录
+     *
+     * @param string $bucket bucket名称
+     * @param string channelName $channelName
+     * @param array  $options
+     * @return GetLiveChannelHistory
+     * @throws OssException
+     */
+    public function getLiveChannelHistory($bucket, $channelName, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_OBJECT] = $channelName;
+        $options[self::OSS_SUB_RESOURCE] = 'live';
+        $options[self::OSS_COMP] = 'history';
+
+        $response = $this->auth($options);
+        $result = new GetLiveChannelHistoryResult($response);
+        return $result->getData();
+    }
+
+    /**
+     *获取指定Bucket下的live channel列表
+     *
+     * @param string $bucket bucket名称
+     * @param array  $options
+     * @return LiveChannelListInfo
+     * @throws OssException
+     */
+    public function listBucketLiveChannels($bucket, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'live';
+        $options[self::OSS_QUERY_STRING] = array(
+            'prefix'   => isset($options['prefix']) ? $options['prefix'] : '',
+            'marker'   => isset($options['marker']) ? $options['marker'] : '',
+            'max-keys' => isset($options['max-keys']) ? $options['max-keys'] : '',
+        );
+        $response = $this->auth($options);
+        $result = new ListLiveChannelResult($response);
+        $list = $result->getData();
+        $list->setBucketName($bucket);
+
+        return $list;
+    }
+
+    /**
+     * 为指定LiveChannel生成播放列表
+     *
+     * @param string $bucket       bucket名称
+     * @param string channelName $channelName
+     * @param string $playlistName 指定生成的点播播放列表的名称,必须以“.m3u8”结尾
+     * @param array  $setTime      startTime和EndTime以unix时间戳格式给定,跨度不能超过一天
+     * @return null
+     * @throws OssException
+     */
+    public function postVodPlaylist($bucket, $channelName, $playlistName, $setTime)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_POST;
+        $options[self::OSS_OBJECT] = $channelName . '/' . $playlistName;
+        $options[self::OSS_SUB_RESOURCE] = 'vod';
+        $options[self::OSS_LIVE_CHANNEL_END_TIME] = $setTime['EndTime'];
+        $options[self::OSS_LIVE_CHANNEL_START_TIME] = $setTime['StartTime'];
+
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 删除指定Bucket的LiveChannel
+     *
+     * @param string $bucket bucket名称
+     * @param string channelName $channelName
+     * @param array  $options
+     * @return null
+     * @throws OssException
+     */
+    public function deleteBucketLiveChannel($bucket, $channelName, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_DELETE;
+        $options[self::OSS_OBJECT] = $channelName;
+        $options[self::OSS_SUB_RESOURCE] = 'live';
+
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 生成带签名的推流地址
+     *
+     * @param string $bucket bucket名称
+     * @param string channelName $channelName
+     * @param int timeout 设置超时时间,单位为秒
+     * @param array  $options
+     * @return 推流地址
+     * @throws OssException
+     */
+    public function signRtmpUrl($bucket, $channelName, $timeout = 60, $options = null)
+    {
+        $this->precheckCommon($bucket, $channelName, $options, false);
+        $expires = time() + $timeout;
+        $proto = 'rtmp://';
+        $hostname = $this->generateHostname($bucket);
+        $cano_params = '';
+        $query_items = array();
+        $params = isset($options['params']) ? $options['params'] : array();
+        uksort($params, 'strnatcasecmp');
+        foreach ($params as $key => $value) {
+            $cano_params = $cano_params . $key . ':' . $value . "\n";
+            $query_items[] = rawurlencode($key) . '=' . rawurlencode($value);
+        }
+        $resource = '/' . $bucket . '/' . $channelName;
+
+        $string_to_sign = $expires . "\n" . $cano_params . $resource;
+        $signature = base64_encode(hash_hmac('sha1', $string_to_sign, $this->accessKeySecret, true));
+
+        $query_items[] = 'OSSAccessKeyId=' . rawurlencode($this->accessKeyId);
+        $query_items[] = 'Expires=' . rawurlencode($expires);
+        $query_items[] = 'Signature=' . rawurlencode($signature);
+
+        return $proto . $hostname . '/live/' . $channelName . '?' . implode('&', $query_items);
+    }
+
+    /**
+     * 检验跨域资源请求, 发送跨域请求之前会发送一个preflight请求(OPTIONS)并带上特定的来源域,
+     * HTTP方法和header信息等给OSS以决定是否发送真正的请求。 OSS可以通过putBucketCors接口
+     * 来开启Bucket的CORS支持,开启CORS功能之后,OSS在收到浏览器preflight请求时会根据设定的
+     * 规则评估是否允许本次请求
+     *
+     * @param string $bucket          bucket名称
+     * @param string $object          object名称
+     * @param string $origin          请求来源域
+     * @param string $request_method  表明实际请求中会使用的HTTP方法
+     * @param string $request_headers 表明实际请求中会使用的除了简单头部之外的headers
+     * @param array  $options
+     * @return array
+     * @throws OssException
+     * @link http://help.aliyun.com/document_detail/oss/api-reference/cors/OptionObject.html
+     */
+    public function optionsObject($bucket, $object, $origin, $request_method, $request_headers, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_OPTIONS;
+        $options[self::OSS_OBJECT] = $object;
+        $options[self::OSS_HEADERS] = array(
+            self::OSS_OPTIONS_ORIGIN          => $origin,
+            self::OSS_OPTIONS_REQUEST_HEADERS => $request_headers,
+            self::OSS_OPTIONS_REQUEST_METHOD  => $request_method
+        );
+        $response = $this->auth($options);
+        $result = new HeaderResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 设置Bucket的Lifecycle配置
+     *
+     * @param string          $bucket          bucket名称
+     * @param LifecycleConfig $lifecycleConfig Lifecycle配置类
+     * @param array           $options
+     * @return null
+     * @throws OssException
+     */
+    public function putBucketLifecycle($bucket, $lifecycleConfig, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_PUT;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'lifecycle';
+        $options[self::OSS_CONTENT_TYPE] = 'application/xml';
+        $options[self::OSS_CONTENT] = $lifecycleConfig->serializeToXml();
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获取Bucket的Lifecycle配置情况
+     *
+     * @param string $bucket bucket名称
+     * @param array  $options
+     * @return LifecycleConfig
+     * @throws OssException
+     */
+    public function getBucketLifecycle($bucket, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'lifecycle';
+        $response = $this->auth($options);
+        $result = new GetLifecycleResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 删除指定Bucket的生命周期配置
+     *
+     * @param string $bucket bucket名称
+     * @param array  $options
+     * @return null
+     * @throws OssException
+     */
+    public function deleteBucketLifecycle($bucket, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_DELETE;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'lifecycle';
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 设置一个bucket的referer访问白名单和是否允许referer字段为空的请求访问
+     * Bucket Referer防盗链具体见OSS防盗链
+     *
+     * @param string        $bucket bucket名称
+     * @param RefererConfig $refererConfig
+     * @param array         $options
+     * @return ResponseCore
+     * @throws null
+     */
+    public function putBucketReferer($bucket, $refererConfig, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_PUT;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'referer';
+        $options[self::OSS_CONTENT_TYPE] = 'application/xml';
+        $options[self::OSS_CONTENT] = $refererConfig->serializeToXml();
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获取Bucket的Referer配置情况
+     * Bucket Referer防盗链具体见OSS防盗链
+     *
+     * @param string $bucket bucket名称
+     * @param array  $options
+     * @return RefererConfig
+     * @throws OssException
+     */
+    public function getBucketReferer($bucket, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'referer';
+        $response = $this->auth($options);
+        $result = new GetRefererResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 设置bucket的容量大小,单位GB
+     * 当bucket的容量大于设置的容量时,禁止继续写入
+     *
+     * @param string $bucket bucket名称
+     * @param int    $storageCapacity
+     * @param array  $options
+     * @return ResponseCore
+     * @throws null
+     */
+    public function putBucketStorageCapacity($bucket, $storageCapacity, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_PUT;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'qos';
+        $options[self::OSS_CONTENT_TYPE] = 'application/xml';
+        $storageCapacityConfig = new StorageCapacityConfig($storageCapacity);
+        $options[self::OSS_CONTENT] = $storageCapacityConfig->serializeToXml();
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获取bucket的容量大小,单位GB
+     *
+     * @param string $bucket bucket名称
+     * @param array  $options
+     * @return int
+     * @throws OssException
+     */
+    public function getBucketStorageCapacity($bucket, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'qos';
+        $response = $this->auth($options);
+        $result = new GetStorageCapacityResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获取bucket下的object列表
+     *
+     * @param string $bucket
+     * @param array  $options
+     *      其中options中的参数如下
+     *      $options = array(
+     *      'max-keys'  => max-keys用于限定此次返回object的最大数,如果不设定,默认为100,max-keys取值不能大于1000。
+     *      'prefix'    => 限定返回的object key必须以prefix作为前缀。注意使用prefix查询时,返回的key中仍会包含prefix。
+     *      'delimiter' => 是一个用于对Object名字进行分组的字符。所有名字包含指定的前缀且第一次出现delimiter字符之间的object作为一组元素
+     *      'marker'    => 用户设定结果从marker之后按字母排序的第一个开始返回。
+     *      )
+     *      其中 prefix,marker用来实现分页显示效果,参数的长度必须小于256字节。
+     * @return ObjectListInfo
+     * @throws OssException
+     */
+    public function listObjects($bucket, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_HEADERS] = array(
+            self::OSS_DELIMITER => isset($options[self::OSS_DELIMITER]) ? $options[self::OSS_DELIMITER] : '/',
+            self::OSS_PREFIX    => isset($options[self::OSS_PREFIX]) ? $options[self::OSS_PREFIX] : '',
+            self::OSS_MAX_KEYS  => isset($options[self::OSS_MAX_KEYS]) ? $options[self::OSS_MAX_KEYS] : self::OSS_MAX_KEYS_VALUE,
+            self::OSS_MARKER    => isset($options[self::OSS_MARKER]) ? $options[self::OSS_MARKER] : '',
+        );
+        $query = isset($options[self::OSS_QUERY_STRING]) ? $options[self::OSS_QUERY_STRING] : array();
+        $options[self::OSS_QUERY_STRING] = array_merge(
+            $query,
+            array(self::OSS_ENCODING_TYPE => self::OSS_ENCODING_TYPE_URL)
+        );
+
+        $response = $this->auth($options);
+        $result = new ListObjectsResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 创建虚拟目录 (本函数会在object名称后增加'/', 所以创建目录的object名称不需要'/'结尾,否则,目录名称会变成'//')
+     *
+     * 暂不开放此接口
+     *
+     * @param string $bucket bucket名称
+     * @param string $object object名称
+     * @param array  $options
+     * @return null
+     */
+    public function createObjectDir($bucket, $object, $options = null)
+    {
+        $this->precheckCommon($bucket, $object, $options);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_PUT;
+        $options[self::OSS_OBJECT] = $object . '/';
+        $options[self::OSS_CONTENT_LENGTH] = array(self::OSS_CONTENT_LENGTH => 0);
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 上传内存中的内容
+     *
+     * @param string $bucket  bucket名称
+     * @param string $object  objcet名称
+     * @param string $content 上传的内容
+     * @param array  $options
+     * @return null
+     */
+    public function putObject($bucket, $object, $content, $options = null)
+    {
+        $this->precheckCommon($bucket, $object, $options);
+
+        $options[self::OSS_CONTENT] = $content;
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_PUT;
+        $options[self::OSS_OBJECT] = $object;
+
+        if (!isset($options[self::OSS_LENGTH])) {
+            $options[self::OSS_CONTENT_LENGTH] = strlen($options[self::OSS_CONTENT]);
+        } else {
+            $options[self::OSS_CONTENT_LENGTH] = $options[self::OSS_LENGTH];
+        }
+
+        $is_check_md5 = $this->isCheckMD5($options);
+        if ($is_check_md5) {
+            $content_md5 = base64_encode(md5($content, true));
+            $options[self::OSS_CONTENT_MD5] = $content_md5;
+        }
+
+        if (!isset($options[self::OSS_CONTENT_TYPE])) {
+            $options[self::OSS_CONTENT_TYPE] = $this->getMimeType($object);
+        }
+        $response = $this->auth($options);
+
+        if (isset($options[self::OSS_CALLBACK]) && !empty($options[self::OSS_CALLBACK])) {
+            $result = new CallbackResult($response);
+        } else {
+            $result = new PutSetDeleteResult($response);
+        }
+
+        return $result->getData();
+    }
+
+    /**
+     * 创建symlink
+     * @param string $bucket       bucket名称
+     * @param string $symlink      symlink名称
+     * @param string $targetObject 目标object名称
+     * @param array  $options
+     * @return null
+     */
+    public function putSymlink($bucket, $symlink, $targetObject, $options = null)
+    {
+        $this->precheckCommon($bucket, $symlink, $options);
+
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_PUT;
+        $options[self::OSS_OBJECT] = $symlink;
+        $options[self::OSS_SUB_RESOURCE] = self::OSS_SYMLINK;
+        $options[self::OSS_HEADERS][self::OSS_SYMLINK_TARGET] = rawurlencode($targetObject);
+
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获取symlink
+     * @param string $bucket  bucket名称
+     * @param string $symlink symlink名称
+     * @return null
+     */
+    public function getSymlink($bucket, $symlink)
+    {
+        $this->precheckCommon($bucket, $symlink, $options);
+
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_OBJECT] = $symlink;
+        $options[self::OSS_SUB_RESOURCE] = self::OSS_SYMLINK;
+
+        $response = $this->auth($options);
+        $result = new SymlinkResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 上传本地文件
+     *
+     * @param string $bucket bucket名称
+     * @param string $object object名称
+     * @param string $file   本地文件路径
+     * @param array  $options
+     * @return null
+     * @throws OssException
+     */
+    public function uploadFile($bucket, $object, $file, $options = null)
+    {
+        $this->precheckCommon($bucket, $object, $options);
+        OssUtil::throwOssExceptionWithMessageIfEmpty($file, "file path is invalid");
+        $file = OssUtil::encodePath($file);
+        if (!file_exists($file)) {
+            throw new OssException($file . " file does not exist");
+        }
+        $options[self::OSS_FILE_UPLOAD] = $file;
+        $file_size = filesize($options[self::OSS_FILE_UPLOAD]);
+        $is_check_md5 = $this->isCheckMD5($options);
+        if ($is_check_md5) {
+            $content_md5 = base64_encode(md5_file($options[self::OSS_FILE_UPLOAD], true));
+            $options[self::OSS_CONTENT_MD5] = $content_md5;
+        }
+        if (!isset($options[self::OSS_CONTENT_TYPE])) {
+            $options[self::OSS_CONTENT_TYPE] = $this->getMimeType($object, $file);
+        }
+        $options[self::OSS_METHOD] = self::OSS_HTTP_PUT;
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_OBJECT] = $object;
+        $options[self::OSS_CONTENT_LENGTH] = $file_size;
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 追加上传内存中的内容
+     *
+     * @param string $bucket  bucket名称
+     * @param string $object  objcet名称
+     * @param string $content 本次追加上传的内容
+     * @param array  $options
+     * @return int next append position
+     * @throws OssException
+     */
+    public function appendObject($bucket, $object, $content, $position, $options = null)
+    {
+        $this->precheckCommon($bucket, $object, $options);
+
+        $options[self::OSS_CONTENT] = $content;
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_POST;
+        $options[self::OSS_OBJECT] = $object;
+        $options[self::OSS_SUB_RESOURCE] = 'append';
+        $options[self::OSS_POSITION] = strval($position);
+
+        if (!isset($options[self::OSS_LENGTH])) {
+            $options[self::OSS_CONTENT_LENGTH] = strlen($options[self::OSS_CONTENT]);
+        } else {
+            $options[self::OSS_CONTENT_LENGTH] = $options[self::OSS_LENGTH];
+        }
+
+        $is_check_md5 = $this->isCheckMD5($options);
+        if ($is_check_md5) {
+            $content_md5 = base64_encode(md5($content, true));
+            $options[self::OSS_CONTENT_MD5] = $content_md5;
+        }
+
+        if (!isset($options[self::OSS_CONTENT_TYPE])) {
+            $options[self::OSS_CONTENT_TYPE] = $this->getMimeType($object);
+        }
+        $response = $this->auth($options);
+        $result = new AppendResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 追加上传本地文件
+     *
+     * @param string $bucket bucket名称
+     * @param string $object object名称
+     * @param string $file   追加上传的本地文件路径
+     * @param array  $options
+     * @return int next append position
+     * @throws OssException
+     */
+    public function appendFile($bucket, $object, $file, $position, $options = null)
+    {
+        $this->precheckCommon($bucket, $object, $options);
+
+        OssUtil::throwOssExceptionWithMessageIfEmpty($file, "file path is invalid");
+        $file = OssUtil::encodePath($file);
+        if (!file_exists($file)) {
+            throw new OssException($file . " file does not exist");
+        }
+        $options[self::OSS_FILE_UPLOAD] = $file;
+        $file_size = filesize($options[self::OSS_FILE_UPLOAD]);
+        $is_check_md5 = $this->isCheckMD5($options);
+        if ($is_check_md5) {
+            $content_md5 = base64_encode(md5_file($options[self::OSS_FILE_UPLOAD], true));
+            $options[self::OSS_CONTENT_MD5] = $content_md5;
+        }
+        if (!isset($options[self::OSS_CONTENT_TYPE])) {
+            $options[self::OSS_CONTENT_TYPE] = $this->getMimeType($object, $file);
+        }
+
+        $options[self::OSS_METHOD] = self::OSS_HTTP_POST;
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_OBJECT] = $object;
+        $options[self::OSS_CONTENT_LENGTH] = $file_size;
+        $options[self::OSS_SUB_RESOURCE] = 'append';
+        $options[self::OSS_POSITION] = strval($position);
+
+        $response = $this->auth($options);
+        $result = new AppendResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 拷贝一个在OSS上已经存在的object成另外一个object
+     *
+     * @param string $fromBucket 源bucket名称
+     * @param string $fromObject 源object名称
+     * @param string $toBucket   目标bucket名称
+     * @param string $toObject   目标object名称
+     * @param array  $options
+     * @return null
+     * @throws OssException
+     */
+    public function copyObject($fromBucket, $fromObject, $toBucket, $toObject, $options = null)
+    {
+        $this->precheckCommon($fromBucket, $fromObject, $options);
+        $this->precheckCommon($toBucket, $toObject, $options);
+        $options[self::OSS_BUCKET] = $toBucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_PUT;
+        $options[self::OSS_OBJECT] = $toObject;
+        if (isset($options[self::OSS_HEADERS])) {
+            $options[self::OSS_HEADERS][self::OSS_OBJECT_COPY_SOURCE] = '/' . $fromBucket . '/' . $fromObject;
+        } else {
+            $options[self::OSS_HEADERS] = array(self::OSS_OBJECT_COPY_SOURCE => '/' . $fromBucket . '/' . $fromObject);
+        }
+        $response = $this->auth($options);
+        $result = new CopyObjectResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获取Object的Meta信息
+     *
+     * @param string $bucket  bucket名称
+     * @param string $object  object名称
+     * @param string $options 具体参考SDK文档
+     * @return array
+     */
+    public function getObjectMeta($bucket, $object, $options = null)
+    {
+        $this->precheckCommon($bucket, $object, $options);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_HEAD;
+        $options[self::OSS_OBJECT] = $object;
+        $response = $this->auth($options);
+        $result = new HeaderResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 删除某个Object
+     *
+     * @param string $bucket bucket名称
+     * @param string $object object名称
+     * @param array  $options
+     * @return null
+     */
+    public function deleteObject($bucket, $object, $options = null)
+    {
+        $this->precheckCommon($bucket, $object, $options);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_DELETE;
+        $options[self::OSS_OBJECT] = $object;
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 删除同一个Bucket中的多个Object
+     *
+     * @param string $bucket  bucket名称
+     * @param array  $objects object列表
+     * @param array  $options
+     * @return ResponseCore
+     * @throws null
+     */
+    public function deleteObjects($bucket, $objects, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        if (!is_array($objects) || !$objects) {
+            throw new OssException('objects must be array');
+        }
+        $options[self::OSS_METHOD] = self::OSS_HTTP_POST;
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'delete';
+        $options[self::OSS_CONTENT_TYPE] = 'application/xml';
+        $quiet = 'false';
+        if (isset($options['quiet'])) {
+            if (is_bool($options['quiet'])) { //Boolean
+                $quiet = $options['quiet'] ? 'true' : 'false';
+            } elseif (is_string($options['quiet'])) { // string
+                $quiet = ($options['quiet'] === 'true') ? 'true' : 'false';
+            }
+        }
+        $xmlBody = OssUtil::createDeleteObjectsXmlBody($objects, $quiet);
+        $options[self::OSS_CONTENT] = $xmlBody;
+        $response = $this->auth($options);
+        $result = new DeleteObjectsResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获得Object内容
+     *
+     * @param string $bucket  bucket名称
+     * @param string $object  object名称
+     * @param array  $options 该参数中必须设置ALIOSS::OSS_FILE_DOWNLOAD,ALIOSS::OSS_RANGE可选,可以根据实际情况设置;如果不设置,默认会下载全部内容
+     * @return string
+     */
+    public function getObject($bucket, $object, $options = null)
+    {
+        $this->precheckCommon($bucket, $object, $options);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_OBJECT] = $object;
+        if (isset($options[self::OSS_LAST_MODIFIED])) {
+            $options[self::OSS_HEADERS][self::OSS_IF_MODIFIED_SINCE] = $options[self::OSS_LAST_MODIFIED];
+            unset($options[self::OSS_LAST_MODIFIED]);
+        }
+        if (isset($options[self::OSS_ETAG])) {
+            $options[self::OSS_HEADERS][self::OSS_IF_NONE_MATCH] = $options[self::OSS_ETAG];
+            unset($options[self::OSS_ETAG]);
+        }
+        if (isset($options[self::OSS_RANGE])) {
+            $range = $options[self::OSS_RANGE];
+            $options[self::OSS_HEADERS][self::OSS_RANGE] = "bytes=$range";
+            unset($options[self::OSS_RANGE]);
+        }
+        $response = $this->auth($options);
+        $result = new BodyResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 检测Object是否存在
+     * 通过获取Object的Meta信息来判断Object是否存在, 用户需要自行解析ResponseCore判断object是否存在
+     *
+     * @param string $bucket bucket名称
+     * @param string $object object名称
+     * @param array  $options
+     * @return bool
+     */
+    public function doesObjectExist($bucket, $object, $options = null)
+    {
+        $this->precheckCommon($bucket, $object, $options);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_HEAD;
+        $options[self::OSS_OBJECT] = $object;
+        $response = $this->auth($options);
+        $result = new ExistResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 针对Archive类型的Object读取
+     * 需要使用Restore操作让服务端执行解冻任务
+     *
+     * @param string $bucket bucket名称
+     * @param string $object object名称
+     * @return null
+     * @throws OssException
+     */
+    public function restoreObject($bucket, $object, $options = null)
+    {
+        $this->precheckCommon($bucket, $object, $options);
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_METHOD] = self::OSS_HTTP_POST;
+        $options[self::OSS_OBJECT] = $object;
+        $options[self::OSS_SUB_RESOURCE] = self::OSS_RESTORE;
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获取分片大小,根据用户提供的part_size,重新计算一个更合理的partsize
+     *
+     * @param int $partSize
+     * @return int
+     */
+    private function computePartSize($partSize)
+    {
+        $partSize = (integer)$partSize;
+        if ($partSize <= self::OSS_MIN_PART_SIZE) {
+            $partSize = self::OSS_MIN_PART_SIZE;
+        } elseif ($partSize > self::OSS_MAX_PART_SIZE) {
+            $partSize = self::OSS_MAX_PART_SIZE;
+        }
+        return $partSize;
+    }
+
+    /**
+     * 计算文件可以分成多少个part,以及每个part的长度以及起始位置
+     * 方法必须在 <upload_part()>中调用
+     *
+     * @param integer $file_size 文件大小
+     * @param integer $partSize  part大小,默认5M
+     * @return array An array 包含 key-value 键值对. Key 为 `seekTo` 和 `length`.
+     */
+    public function generateMultiuploadParts($file_size, $partSize = 5242880)
+    {
+        $i = 0;
+        $size_count = $file_size;
+        $values = array();
+        $partSize = $this->computePartSize($partSize);
+        while ($size_count > 0) {
+            $size_count -= $partSize;
+            $values[] = array(
+                self::OSS_SEEK_TO => ($partSize * $i),
+                self::OSS_LENGTH  => (($size_count > 0) ? $partSize : ($size_count + $partSize)),
+            );
+            $i++;
+        }
+        return $values;
+    }
+
+    /**
+     * 初始化multi-part upload
+     *
+     * @param string $bucket  Bucket名称
+     * @param string $object  Object名称
+     * @param array  $options Key-Value数组
+     * @return string 返回uploadid
+     * @throws OssException
+     */
+    public function initiateMultipartUpload($bucket, $object, $options = null)
+    {
+        $this->precheckCommon($bucket, $object, $options);
+        $options[self::OSS_METHOD] = self::OSS_HTTP_POST;
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_OBJECT] = $object;
+        $options[self::OSS_SUB_RESOURCE] = 'uploads';
+        $options[self::OSS_CONTENT] = '';
+
+        if (!isset($options[self::OSS_CONTENT_TYPE])) {
+            $options[self::OSS_CONTENT_TYPE] = $this->getMimeType($object);
+        }
+        if (!isset($options[self::OSS_HEADERS])) {
+            $options[self::OSS_HEADERS] = array();
+        }
+        $response = $this->auth($options);
+        $result = new InitiateMultipartUploadResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 分片上传的块上传接口
+     *
+     * @param string $bucket  Bucket名称
+     * @param string $object  Object名称
+     * @param string $uploadId
+     * @param array  $options Key-Value数组
+     * @return string eTag
+     * @throws OssException
+     */
+    public function uploadPart($bucket, $object, $uploadId, $options = null)
+    {
+        $this->precheckCommon($bucket, $object, $options);
+        $this->precheckParam($options, self::OSS_FILE_UPLOAD, __FUNCTION__);
+        $this->precheckParam($options, self::OSS_PART_NUM, __FUNCTION__);
+
+        $options[self::OSS_METHOD] = self::OSS_HTTP_PUT;
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_OBJECT] = $object;
+        $options[self::OSS_UPLOAD_ID] = $uploadId;
+
+        if (isset($options[self::OSS_LENGTH])) {
+            $options[self::OSS_CONTENT_LENGTH] = $options[self::OSS_LENGTH];
+        }
+        $response = $this->auth($options);
+        $result = new UploadPartResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 获取已成功上传的part
+     *
+     * @param string $bucket   Bucket名称
+     * @param string $object   Object名称
+     * @param string $uploadId uploadId
+     * @param array  $options  Key-Value数组
+     * @return ListPartsInfo
+     * @throws OssException
+     */
+    public function listParts($bucket, $object, $uploadId, $options = null)
+    {
+        $this->precheckCommon($bucket, $object, $options);
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_OBJECT] = $object;
+        $options[self::OSS_UPLOAD_ID] = $uploadId;
+        $options[self::OSS_QUERY_STRING] = array();
+        foreach (array('max-parts', 'part-number-marker') as $param) {
+            if (isset($options[$param])) {
+                $options[self::OSS_QUERY_STRING][$param] = $options[$param];
+                unset($options[$param]);
+            }
+        }
+        $response = $this->auth($options);
+        $result = new ListPartsResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 中止进行一半的分片上传操作
+     *
+     * @param string $bucket   Bucket名称
+     * @param string $object   Object名称
+     * @param string $uploadId uploadId
+     * @param array  $options  Key-Value数组
+     * @return null
+     * @throws OssException
+     */
+    public function abortMultipartUpload($bucket, $object, $uploadId, $options = null)
+    {
+        $this->precheckCommon($bucket, $object, $options);
+        $options[self::OSS_METHOD] = self::OSS_HTTP_DELETE;
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_OBJECT] = $object;
+        $options[self::OSS_UPLOAD_ID] = $uploadId;
+        $response = $this->auth($options);
+        $result = new PutSetDeleteResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 在将所有数据Part都上传完成后,调用此接口完成本次分块上传
+     *
+     * @param string $bucket    Bucket名称
+     * @param string $object    Object名称
+     * @param string $uploadId  uploadId
+     * @param array  $listParts array( array("PartNumber"=> int, "ETag"=>string))
+     * @param array  $options   Key-Value数组
+     * @return PutSetDeleteResult
+     * @throws OssException
+     */
+    public function completeMultipartUpload($bucket, $object, $uploadId, $listParts, $options = null)
+    {
+        $this->precheckCommon($bucket, $object, $options);
+        $options[self::OSS_METHOD] = self::OSS_HTTP_POST;
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_OBJECT] = $object;
+        $options[self::OSS_UPLOAD_ID] = $uploadId;
+        $options[self::OSS_CONTENT_TYPE] = 'application/xml';
+        if (!is_array($listParts)) {
+            throw new OssException("listParts must be array type");
+        }
+        $options[self::OSS_CONTENT] = OssUtil::createCompleteMultipartUploadXmlBody($listParts);
+        $response = $this->auth($options);
+        if (isset($options[self::OSS_CALLBACK]) && !empty($options[self::OSS_CALLBACK])) {
+            $result = new CallbackResult($response);
+        } else {
+            $result = new PutSetDeleteResult($response);
+        }
+        return $result->getData();
+    }
+
+    /**
+     * 罗列出所有执行中的Multipart Upload事件,即已经被初始化的Multipart Upload但是未被
+     * Complete或者Abort的Multipart Upload事件
+     *
+     * @param string $bucket  bucket
+     * @param array  $options 关联数组
+     * @return ListMultipartUploadInfo
+     * @throws OssException
+     */
+    public function listMultipartUploads($bucket, $options = null)
+    {
+        $this->precheckCommon($bucket, null, $options, false);
+        $options[self::OSS_METHOD] = self::OSS_HTTP_GET;
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_OBJECT] = '/';
+        $options[self::OSS_SUB_RESOURCE] = 'uploads';
+
+        foreach (array('delimiter', 'key-marker', 'max-uploads', 'prefix', 'upload-id-marker') as $param) {
+            if (isset($options[$param])) {
+                $options[self::OSS_QUERY_STRING][$param] = $options[$param];
+                unset($options[$param]);
+            }
+        }
+        $query = isset($options[self::OSS_QUERY_STRING]) ? $options[self::OSS_QUERY_STRING] : array();
+        $options[self::OSS_QUERY_STRING] = array_merge(
+            $query,
+            array(self::OSS_ENCODING_TYPE => self::OSS_ENCODING_TYPE_URL)
+        );
+
+        $response = $this->auth($options);
+        $result = new ListMultipartUploadResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * 从一个已存在的Object中拷贝数据来上传一个Part
+     *
+     * @param string $fromBucket 源bucket名称
+     * @param string $fromObject 源object名称
+     * @param string $toBucket   目标bucket名称
+     * @param string $toObject   目标object名称
+     * @param int    $partNumber 分块上传的块id
+     * @param string $uploadId   初始化multipart upload返回的uploadid
+     * @param array  $options    Key-Value数组
+     * @return null
+     * @throws OssException
+     */
+    public function uploadPartCopy($fromBucket, $fromObject, $toBucket, $toObject, $partNumber, $uploadId, $options = null)
+    {
+        $this->precheckCommon($fromBucket, $fromObject, $options);
+        $this->precheckCommon($toBucket, $toObject, $options);
+
+        //如果没有设置$options['isFullCopy'],则需要强制判断copy的起止位置
+        $start_range = "0";
+        if (isset($options['start'])) {
+            $start_range = $options['start'];
+        }
+        $end_range = "";
+        if (isset($options['end'])) {
+            $end_range = $options['end'];
+        }
+        $options[self::OSS_METHOD] = self::OSS_HTTP_PUT;
+        $options[self::OSS_BUCKET] = $toBucket;
+        $options[self::OSS_OBJECT] = $toObject;
+        $options[self::OSS_PART_NUM] = $partNumber;
+        $options[self::OSS_UPLOAD_ID] = $uploadId;
+
+        if (!isset($options[self::OSS_HEADERS])) {
+            $options[self::OSS_HEADERS] = array();
+        }
+
+        $options[self::OSS_HEADERS][self::OSS_OBJECT_COPY_SOURCE] = '/' . $fromBucket . '/' . $fromObject;
+        $options[self::OSS_HEADERS][self::OSS_OBJECT_COPY_SOURCE_RANGE] = "bytes=" . $start_range . "-" . $end_range;
+        $response = $this->auth($options);
+        $result = new UploadPartResult($response);
+        return $result->getData();
+    }
+
+    /**
+     * multipart上传统一封装,从初始化到完成multipart,以及出错后中止动作
+     *
+     * @param string $bucket  bucket名称
+     * @param string $object  object名称
+     * @param string $file    需要上传的本地文件的路径
+     * @param array  $options Key-Value数组
+     * @return null
+     * @throws OssException
+     */
+    public function multiuploadFile($bucket, $object, $file, $options = null)
+    {
+        $this->precheckCommon($bucket, $object, $options);
+        if (isset($options[self::OSS_LENGTH])) {
+            $options[self::OSS_CONTENT_LENGTH] = $options[self::OSS_LENGTH];
+            unset($options[self::OSS_LENGTH]);
+        }
+        if (empty($file)) {
+            throw new OssException("parameter invalid, file is empty");
+        }
+        $uploadFile = OssUtil::encodePath($file);
+        if (!isset($options[self::OSS_CONTENT_TYPE])) {
+            $options[self::OSS_CONTENT_TYPE] = $this->getMimeType($object, $uploadFile);
+        }
+
+        $upload_position = isset($options[self::OSS_SEEK_TO]) ? (integer)$options[self::OSS_SEEK_TO] : 0;
+
+        if (isset($options[self::OSS_CONTENT_LENGTH])) {
+            $upload_file_size = (integer)$options[self::OSS_CONTENT_LENGTH];
+        } else {
+            $upload_file_size = filesize($uploadFile);
+            if ($upload_file_size !== false) {
+                $upload_file_size -= $upload_position;
+            }
+        }
+
+        if ($upload_position === false || !isset($upload_file_size) || $upload_file_size === false || $upload_file_size < 0) {
+            throw new OssException('The size of `fileUpload` cannot be determined in ' . __FUNCTION__ . '().');
+        }
+        // 处理partSize
+        if (isset($options[self::OSS_PART_SIZE])) {
+            $options[self::OSS_PART_SIZE] = $this->computePartSize($options[self::OSS_PART_SIZE]);
+        } else {
+            $options[self::OSS_PART_SIZE] = self::OSS_MID_PART_SIZE;
+        }
+
+        $is_check_md5 = $this->isCheckMD5($options);
+        // 如果上传的文件小于partSize,则直接使用普通方式上传
+        if ($upload_file_size < $options[self::OSS_PART_SIZE] && !isset($options[self::OSS_UPLOAD_ID])) {
+            return $this->uploadFile($bucket, $object, $uploadFile, $options);
+        }
+
+        // 初始化multipart
+        if (isset($options[self::OSS_UPLOAD_ID])) {
+            $uploadId = $options[self::OSS_UPLOAD_ID];
+        } else {
+            // 初始化
+            $uploadId = $this->initiateMultipartUpload($bucket, $object, $options);
+        }
+
+        // 获取的分片
+        $pieces = $this->generateMultiuploadParts($upload_file_size, (integer)$options[self::OSS_PART_SIZE]);
+        $response_upload_part = array();
+        foreach ($pieces as $i => $piece) {
+            $from_pos = $upload_position + (integer)$piece[self::OSS_SEEK_TO];
+            $to_pos = (integer)$piece[self::OSS_LENGTH] + $from_pos - 1;
+            $up_options = array(
+                self::OSS_FILE_UPLOAD => $uploadFile,
+                self::OSS_PART_NUM    => ($i + 1),
+                self::OSS_SEEK_TO     => $from_pos,
+                self::OSS_LENGTH      => $to_pos - $from_pos + 1,
+                self::OSS_CHECK_MD5   => $is_check_md5,
+            );
+            if ($is_check_md5) {
+                $content_md5 = OssUtil::getMd5SumForFile($uploadFile, $from_pos, $to_pos);
+                $up_options[self::OSS_CONTENT_MD5] = $content_md5;
+            }
+            $response_upload_part[] = $this->uploadPart($bucket, $object, $uploadId, $up_options);
+        }
+
+        $uploadParts = array();
+        foreach ($response_upload_part as $i => $etag) {
+            $uploadParts[] = array(
+                'PartNumber' => ($i + 1),
+                'ETag'       => $etag,
+            );
+        }
+        return $this->completeMultipartUpload($bucket, $object, $uploadId, $uploadParts);
+    }
+
+    /**
+     * 上传本地目录内的文件或者目录到指定bucket的指定prefix的object中
+     *
+     * @param string $bucket         bucket名称
+     * @param string $prefix         需要上传到的object的key前缀,可以理解成bucket中的子目录,结尾不能是'/',接口中会补充'/'
+     * @param string $localDirectory 需要上传的本地目录
+     * @param string $exclude        需要排除的目录
+     * @param bool   $recursive      是否递归的上传localDirectory下的子目录内容
+     * @param bool   $checkMd5
+     * @return array 返回两个列表 array("succeededList" => array("object"), "failedList" => array("object"=>"errorMessage"))
+     * @throws OssException
+     */
+    public function uploadDir($bucket, $prefix, $localDirectory, $exclude = '.|..|.svn|.git', $recursive = false, $checkMd5 = true)
+    {
+        $retArray = array("succeededList" => array(), "failedList" => array());
+        if (empty($bucket)) throw new OssException("parameter error, bucket is empty");
+        if (!is_string($prefix)) throw new OssException("parameter error, prefix is not string");
+        if (empty($localDirectory)) throw new OssException("parameter error, localDirectory is empty");
+        $directory = $localDirectory;
+        $directory = OssUtil::encodePath($directory);
+        //判断是否目录
+        if (!is_dir($directory)) {
+            throw new OssException('parameter error: ' . $directory . ' is not a directory, please check it');
+        }
+        //read directory
+        $file_list_array = OssUtil::readDir($directory, $exclude, $recursive);
+        if (!$file_list_array) {
+            throw new OssException($directory . ' is empty...');
+        }
+        foreach ($file_list_array as $k => $item) {
+            if (is_dir($item['path'])) {
+                continue;
+            }
+            $options = array(
+                self::OSS_PART_SIZE => self::OSS_MIN_PART_SIZE,
+                self::OSS_CHECK_MD5 => $checkMd5,
+            );
+            $realObject = (!empty($prefix) ? $prefix . '/' : '') . $item['file'];
+
+            try {
+                $this->multiuploadFile($bucket, $realObject, $item['path'], $options);
+                $retArray["succeededList"][] = $realObject;
+            } catch (OssException $e) {
+                $retArray["failedList"][$realObject] = $e->getMessage();
+            }
+        }
+        return $retArray;
+    }
+
+    /**
+     * 支持生成get和put签名, 用户可以生成一个具有一定有效期的
+     * 签名过的url
+     *
+     * @param string $bucket
+     * @param string $object
+     * @param int    $timeout
+     * @param string $method
+     * @param array  $options Key-Value数组
+     * @return string
+     * @throws OssException
+     */
+    public function signUrl($bucket, $object, $timeout = 60, $method = self::OSS_HTTP_GET, $options = null)
+    {
+        $this->precheckCommon($bucket, $object, $options);
+        //method
+        if (self::OSS_HTTP_GET !== $method && self::OSS_HTTP_PUT !== $method) {
+            throw new OssException("method is invalid");
+        }
+        $options[self::OSS_BUCKET] = $bucket;
+        $options[self::OSS_OBJECT] = $object;
+        $options[self::OSS_METHOD] = $method;
+        if (!isset($options[self::OSS_CONTENT_TYPE])) {
+            $options[self::OSS_CONTENT_TYPE] = '';
+        }
+        $timeout = time() + $timeout;
+        $options[self::OSS_PREAUTH] = $timeout;
+        $options[self::OSS_DATE] = $timeout;
+        $this->setSignStsInUrl(true);
+        return $this->auth($options);
+    }
+
+    /**
+     * 检测options参数
+     *
+     * @param array $options
+     * @throws OssException
+     */
+    private function precheckOptions(&$options)
+    {
+        OssUtil::validateOptions($options);
+        if (!$options) {
+            $options = array();
+        }
+    }
+
+    /**
+     * 校验bucket参数
+     *
+     * @param string $bucket
+     * @param string $errMsg
+     * @throws OssException
+     */
+    private function precheckBucket($bucket, $errMsg = 'bucket is not allowed empty')
+    {
+        OssUtil::throwOssExceptionWithMessageIfEmpty($bucket, $errMsg);
+    }
+
+    /**
+     * 校验object参数
+     *
+     * @param string $object
+     * @throws OssException
+     */
+    private function precheckObject($object)
+    {
+        OssUtil::throwOssExceptionWithMessageIfEmpty($object, "object name is empty");
+    }
+
+    /**
+     * 校验option restore
+     *
+     * @param string $restore
+     * @throws OssException
+     */
+    private function precheckStorage($storage)
+    {
+        if (is_string($storage)) {
+            switch ($storage) {
+                case self::OSS_STORAGE_ARCHIVE:
+                    return;
+                case self::OSS_STORAGE_IA:
+                    return;
+                case self::OSS_STORAGE_STANDARD:
+                    return;
+                default:
+                    break;
+            }
+        }
+        throw new OssException('storage name is invalid');
+    }
+
+    /**
+     * 校验bucket,options参数
+     *
+     * @param string $bucket
+     * @param string $object
+     * @param array  $options
+     * @param bool   $isCheckObject
+     */
+    private function precheckCommon($bucket, $object, &$options, $isCheckObject = true)
+    {
+        if ($isCheckObject) {
+            $this->precheckObject($object);
+        }
+        $this->precheckOptions($options);
+        $this->precheckBucket($bucket);
+    }
+
+    /**
+     * 参数校验
+     *
+     * @param array  $options
+     * @param string $param
+     * @param string $funcName
+     * @throws OssException
+     */
+    private function precheckParam($options, $param, $funcName)
+    {
+        if (!isset($options[$param])) {
+            throw new OssException('The `' . $param . '` options is required in ' . $funcName . '().');
+        }
+    }
+
+    /**
+     * 检测md5
+     *
+     * @param array $options
+     * @return bool|null
+     */
+    private function isCheckMD5($options)
+    {
+        return $this->getValue($options, self::OSS_CHECK_MD5, false, true, true);
+    }
+
+    /**
+     * 获取value
+     *
+     * @param array  $options
+     * @param string $key
+     * @param string $default
+     * @param bool   $isCheckEmpty
+     * @param bool   $isCheckBool
+     * @return bool|null
+     */
+    private function getValue($options, $key, $default = null, $isCheckEmpty = false, $isCheckBool = false)
+    {
+        $value = $default;
+        if (isset($options[$key])) {
+            if ($isCheckEmpty) {
+                if (!empty($options[$key])) {
+                    $value = $options[$key];
+                }
+            } else {
+                $value = $options[$key];
+            }
+            unset($options[$key]);
+        }
+        if ($isCheckBool) {
+            if ($value !== true && $value !== false) {
+                $value = false;
+            }
+        }
+        return $value;
+    }
+
+    /**
+     * 获取mimetype类型
+     *
+     * @param string $object
+     * @return string
+     */
+    private function getMimeType($object, $file = null)
+    {
+        if (!is_null($file)) {
+            $type = MimeTypes::getMimetype($file);
+            if (!is_null($type)) {
+                return $type;
+            }
+        }
+
+        $type = MimeTypes::getMimetype($object);
+        if (!is_null($type)) {
+            return $type;
+        }
+
+        return self::DEFAULT_CONTENT_TYPE;
+    }
+
+    /**
+     * 验证并且执行请求,按照OSS Api协议,执行操作
+     *
+     * @param array $options
+     * @return ResponseCore
+     * @throws OssException
+     * @throws RequestCore_Exception
+     */
+    public function auth($options)
+    {
+        OssUtil::validateOptions($options);
+        //验证bucket,list_bucket时不需要验证
+        $this->authPrecheckBucket($options);
+        //验证object
+        $this->authPrecheckObject($options);
+        //Object名称的编码必须是utf8
+        $this->authPrecheckObjectEncoding($options);
+        //验证ACL
+        $this->authPrecheckAcl($options);
+        // 获得当次请求使用的协议头,是https还是http
+        $scheme = $this->useSSL ? 'https://' : 'http://';
+        // 获得当次请求使用的hostname,如果是公共域名或者专有域名,bucket拼在前面构成三级域名
+        $hostname = $this->generateHostname($options[self::OSS_BUCKET]);
+        $string_to_sign = '';
+        $headers = $this->generateHeaders($options, $hostname);
+        $signable_query_string_params = $this->generateSignableQueryStringParam($options);
+        $signable_query_string = OssUtil::toQueryString($signable_query_string_params);
+        $resource_uri = $this->generateResourceUri($options);
+        //生成请求URL
+        $conjunction = '?';
+        $non_signable_resource = '';
+        if (isset($options[self::OSS_SUB_RESOURCE])) {
+            $conjunction = '&';
+        }
+        if ($signable_query_string !== '') {
+            $signable_query_string = $conjunction . $signable_query_string;
+            $conjunction = '&';
+        }
+        $query_string = $this->generateQueryString($options);
+        if ($query_string !== '') {
+            $non_signable_resource .= $conjunction . $query_string;
+            $conjunction = '&';
+        }
+        $this->requestUrl = $scheme . $hostname . $resource_uri . $signable_query_string . $non_signable_resource;
+
+        //创建请求
+        $request = new RequestCore($this->requestUrl, $this->requestProxy);
+        $request->set_useragent($this->generateUserAgent());
+        // Streaming uploads
+        if (isset($options[self::OSS_FILE_UPLOAD])) {
+            if (is_resource($options[self::OSS_FILE_UPLOAD])) {
+                $length = null;
+
+                if (isset($options[self::OSS_CONTENT_LENGTH])) {
+                    $length = $options[self::OSS_CONTENT_LENGTH];
+                } elseif (isset($options[self::OSS_SEEK_TO])) {
+                    $stats = fstat($options[self::OSS_FILE_UPLOAD]);
+                    if ($stats && $stats[self::OSS_SIZE] >= 0) {
+                        $length = $stats[self::OSS_SIZE] - (integer)$options[self::OSS_SEEK_TO];
+                    }
+                }
+                $request->set_read_stream($options[self::OSS_FILE_UPLOAD], $length);
+            } else {
+                $request->set_read_file($options[self::OSS_FILE_UPLOAD]);
+                $length = $request->read_stream_size;
+                if (isset($options[self::OSS_CONTENT_LENGTH])) {
+                    $length = $options[self::OSS_CONTENT_LENGTH];
+                } elseif (isset($options[self::OSS_SEEK_TO]) && isset($length)) {
+                    $length -= (integer)$options[self::OSS_SEEK_TO];
+                }
+                $request->set_read_stream_size($length);
+            }
+        }
+        if (isset($options[self::OSS_SEEK_TO])) {
+            $request->set_seek_position((integer)$options[self::OSS_SEEK_TO]);
+        }
+        if (isset($options[self::OSS_FILE_DOWNLOAD])) {
+            if (is_resource($options[self::OSS_FILE_DOWNLOAD])) {
+                $request->set_write_stream($options[self::OSS_FILE_DOWNLOAD]);
+            } else {
+                $request->set_write_file($options[self::OSS_FILE_DOWNLOAD]);
+            }
+        }
+
+        if (isset($options[self::OSS_METHOD])) {
+            $request->set_method($options[self::OSS_METHOD]);
+            $string_to_sign .= $options[self::OSS_METHOD] . "\n";
+        }
+
+        if (isset($options[self::OSS_CONTENT])) {
+            $request->set_body($options[self::OSS_CONTENT]);
+            if ($headers[self::OSS_CONTENT_TYPE] === 'application/x-www-form-urlencoded') {
+                $headers[self::OSS_CONTENT_TYPE] = 'application/octet-stream';
+            }
+
+            $headers[self::OSS_CONTENT_LENGTH] = strlen($options[self::OSS_CONTENT]);
+            $headers[self::OSS_CONTENT_MD5] = base64_encode(md5($options[self::OSS_CONTENT], true));
+        }
+
+        if (isset($options[self::OSS_CALLBACK])) {
+            $headers[self::OSS_CALLBACK] = base64_encode($options[self::OSS_CALLBACK]);
+        }
+        if (isset($options[self::OSS_CALLBACK_VAR])) {
+            $headers[self::OSS_CALLBACK_VAR] = base64_encode($options[self::OSS_CALLBACK_VAR]);
+        }
+
+        if (!isset($headers[self::OSS_ACCEPT_ENCODING])) {
+            $headers[self::OSS_ACCEPT_ENCODING] = '';
+        }
+
+        uksort($headers, 'strnatcasecmp');
+
+        foreach ($headers as $header_key => $header_value) {
+            $header_value = str_replace(array("\r", "\n"), '', $header_value);
+            if ($header_value !== '' || $header_key === self::OSS_ACCEPT_ENCODING) {
+                $request->add_header($header_key, $header_value);
+            }
+
+            if (
+                strtolower($header_key) === 'content-md5' ||
+                strtolower($header_key) === 'content-type' ||
+                strtolower($header_key) === 'date' ||
+                (isset($options['self::OSS_PREAUTH']) && (integer)$options['self::OSS_PREAUTH'] > 0)
+            ) {
+                $string_to_sign .= $header_value . "\n";
+            } elseif (substr(strtolower($header_key), 0, 6) === self::OSS_DEFAULT_PREFIX) {
+                $string_to_sign .= strtolower($header_key) . ':' . $header_value . "\n";
+            }
+        }
+        // 生成 signable_resource
+        $signable_resource = $this->generateSignableResource($options);
+        $string_to_sign .= rawurldecode($signable_resource) . urldecode($signable_query_string);
+
+        //对?后面的要签名的string字母序排序
+        $string_to_sign_ordered = $this->stringToSignSorted($string_to_sign);
+
+        $signature = base64_encode(hash_hmac('sha1', $string_to_sign_ordered, $this->accessKeySecret, true));
+        $request->add_header('Authorization', 'OSS ' . $this->accessKeyId . ':' . $signature);
+
+        if (isset($options[self::OSS_PREAUTH]) && (integer)$options[self::OSS_PREAUTH] > 0) {
+            $signed_url = $this->requestUrl . $conjunction . self::OSS_URL_ACCESS_KEY_ID . '=' . rawurlencode($this->accessKeyId) . '&' . self::OSS_URL_EXPIRES . '=' . $options[self::OSS_PREAUTH] . '&' . self::OSS_URL_SIGNATURE . '=' . rawurlencode($signature);
+            return $signed_url;
+        } elseif (isset($options[self::OSS_PREAUTH])) {
+            return $this->requestUrl;
+        }
+
+        if ($this->timeout !== 0) {
+            $request->timeout = $this->timeout;
+        }
+        if ($this->connectTimeout !== 0) {
+            $request->connect_timeout = $this->connectTimeout;
+        }
+
+        try {
+            $request->send_request();
+        } catch (RequestCore_Exception $e) {
+            throw(new OssException('RequestCoreException: ' . $e->getMessage()));
+        }
+        $response_header = $request->get_response_header();
+        $response_header['oss-request-url'] = $this->requestUrl;
+        $response_header['oss-redirects'] = $this->redirects;
+        $response_header['oss-stringtosign'] = $string_to_sign;
+        $response_header['oss-requestheaders'] = $request->request_headers;
+
+        $data = new ResponseCore($response_header, $request->get_response_body(), $request->get_response_code());
+        //retry if OSS Internal Error
+        if ((integer)$request->get_response_code() === 500) {
+            if ($this->redirects <= $this->maxRetries) {
+                //设置休眠
+                $delay = (integer)(pow(4, $this->redirects) * 100000);
+                usleep($delay);
+                $this->redirects++;
+                $data = $this->auth($options);
+            }
+        }
+
+        $this->redirects = 0;
+        return $data;
+    }
+
+    /**
+     * 设置最大尝试次数
+     *
+     * @param int $maxRetries
+     * @return void
+     */
+    public function setMaxTries($maxRetries = 3)
+    {
+        $this->maxRetries = $maxRetries;
+    }
+
+    /**
+     * 获取最大尝试次数
+     *
+     * @return int
+     */
+    public function getMaxRetries()
+    {
+        return $this->maxRetries;
+    }
+
+    /**
+     * 打开sts enable标志,使用户构造函数中传入的$sts生效
+     *
+     * @param boolean $enable
+     */
+    public function setSignStsInUrl($enable)
+    {
+        $this->enableStsInUrl = $enable;
+    }
+
+    /**
+     * @return boolean
+     */
+    public function isUseSSL()
+    {
+        return $this->useSSL;
+    }
+
+    /**
+     * @param boolean $useSSL
+     */
+    public function setUseSSL($useSSL)
+    {
+        $this->useSSL = $useSSL;
+    }
+
+    /**
+     * 检查bucket名称格式是否正确,如果非法抛出异常
+     *
+     * @param $options
+     * @throws OssException
+     */
+    private function authPrecheckBucket($options)
+    {
+        if (!(('/' == $options[self::OSS_OBJECT]) && ('' == $options[self::OSS_BUCKET]) && ('GET' == $options[self::OSS_METHOD])) && !OssUtil::validateBucket($options[self::OSS_BUCKET])) {
+            throw new OssException('"' . $options[self::OSS_BUCKET] . '"' . 'bucket name is invalid');
+        }
+    }
+
+    /**
+     *
+     * 检查object名称格式是否正确,如果非法抛出异常
+     *
+     * @param $options
+     * @throws OssException
+     */
+    private function authPrecheckObject($options)
+    {
+        if (isset($options[self::OSS_OBJECT]) && $options[self::OSS_OBJECT] === '/') {
+            return;
+        }
+
+        if (isset($options[self::OSS_OBJECT]) && !OssUtil::validateObject($options[self::OSS_OBJECT])) {
+            throw new OssException('"' . $options[self::OSS_OBJECT] . '"' . ' object name is invalid');
+        }
+    }
+
+    /**
+     * 检查object的编码,如果是gbk或者gb2312则尝试将其转化为utf8编码
+     *
+     * @param mixed $options 参数
+     */
+    private function authPrecheckObjectEncoding(&$options)
+    {
+        $tmp_object = $options[self::OSS_OBJECT];
+        try {
+            if (OssUtil::isGb2312($options[self::OSS_OBJECT])) {
+                $options[self::OSS_OBJECT] = iconv('GB2312', "UTF-8//IGNORE", $options[self::OSS_OBJECT]);
+            } elseif (OssUtil::checkChar($options[self::OSS_OBJECT], true)) {
+                $options[self::OSS_OBJECT] = iconv('GBK', "UTF-8//IGNORE", $options[self::OSS_OBJECT]);
+            }
+        } catch (\Exception $e) {
+            try {
+                $tmp_object = iconv(mb_detect_encoding($tmp_object), "UTF-8", $tmp_object);
+            } catch (\Exception $e) {
+            }
+        }
+        $options[self::OSS_OBJECT] = $tmp_object;
+    }
+
+    /**
+     * 检查ACL是否是预定义中三种之一,如果不是抛出异常
+     *
+     * @param $options
+     * @throws OssException
+     */
+    private function authPrecheckAcl($options)
+    {
+        if (isset($options[self::OSS_HEADERS][self::OSS_ACL]) && !empty($options[self::OSS_HEADERS][self::OSS_ACL])) {
+            if (!in_array(strtolower($options[self::OSS_HEADERS][self::OSS_ACL]), self::$OSS_ACL_TYPES)) {
+                throw new OssException($options[self::OSS_HEADERS][self::OSS_ACL] . ':' . 'acl is invalid(private,public-read,public-read-write)');
+            }
+        }
+    }
+
+    /**
+     * 获得档次请求使用的域名
+     * bucket在前的三级域名,或者二级域名,如果是cname或者ip的话,则是二级域名
+     *
+     * @param $bucket
+     * @return string 剥掉协议头的域名
+     */
+    private function generateHostname($bucket)
+    {
+        if ($this->hostType === self::OSS_HOST_TYPE_IP) {
+            $hostname = $this->hostname;
+        } elseif ($this->hostType === self::OSS_HOST_TYPE_CNAME) {
+            $hostname = $this->hostname;
+        } else {
+            // 专有域或者官网endpoint
+            $hostname = ($bucket == '') ? $this->hostname : ($bucket . '.') . $this->hostname;
+        }
+        return $hostname;
+    }
+
+    /**
+     * 获得当次请求的资源定位字段
+     *
+     * @param $options
+     * @return string 资源定位字段
+     */
+    private function generateResourceUri($options)
+    {
+        $resource_uri = "";
+
+        // resource_uri + bucket
+        if (isset($options[self::OSS_BUCKET]) && '' !== $options[self::OSS_BUCKET]) {
+            if ($this->hostType === self::OSS_HOST_TYPE_IP) {
+                $resource_uri = '/' . $options[self::OSS_BUCKET];
+            }
+        }
+
+        // resource_uri + object
+        if (isset($options[self::OSS_OBJECT]) && '/' !== $options[self::OSS_OBJECT]) {
+            $resource_uri .= '/' . str_replace(array('%2F', '%25'), array('/', '%'), rawurlencode($options[self::OSS_OBJECT]));
+        }
+
+        // resource_uri + sub_resource
+        $conjunction = '?';
+        if (isset($options[self::OSS_SUB_RESOURCE])) {
+            $resource_uri .= $conjunction . $options[self::OSS_SUB_RESOURCE];
+        }
+        return $resource_uri;
+    }
+
+    /**
+     * 生成signalbe_query_string_param, array类型
+     *
+     * @param array $options
+     * @return array
+     */
+    private function generateSignableQueryStringParam($options)
+    {
+        $signableQueryStringParams = array();
+        $signableList = array(
+            self::OSS_PART_NUM,
+            'response-content-type',
+            'response-content-language',
+            'response-cache-control',
+            'response-content-encoding',
+            'response-expires',
+            'response-content-disposition',
+            self::OSS_UPLOAD_ID,
+            self::OSS_COMP,
+            self::OSS_LIVE_CHANNEL_STATUS,
+            self::OSS_LIVE_CHANNEL_START_TIME,
+            self::OSS_LIVE_CHANNEL_END_TIME,
+            self::OSS_PROCESS,
+            self::OSS_POSITION,
+            self::OSS_SYMLINK,
+            self::OSS_RESTORE,
+        );
+
+        foreach ($signableList as $item) {
+            if (isset($options[$item])) {
+                $signableQueryStringParams[$item] = $options[$item];
+            }
+        }
+
+        if ($this->enableStsInUrl && (!is_null($this->securityToken))) {
+            $signableQueryStringParams["security-token"] = $this->securityToken;
+        }
+
+        return $signableQueryStringParams;
+    }
+
+    /**
+     *  生成用于签名resource段
+     *
+     * @param mixed $options
+     * @return string
+     */
+    private function generateSignableResource($options)
+    {
+        $signableResource = "";
+        $signableResource .= '/';
+        if (isset($options[self::OSS_BUCKET]) && '' !== $options[self::OSS_BUCKET]) {
+            $signableResource .= $options[self::OSS_BUCKET];
+            // 如果操作没有Object操作的话,这里最后是否有斜线有个trick,ip的域名下,不需要加'/', 否则需要加'/'
+            if ($options[self::OSS_OBJECT] == '/') {
+                if ($this->hostType !== self::OSS_HOST_TYPE_IP) {
+                    $signableResource .= "/";
+                }
+            }
+        }
+        //signable_resource + object
+        if (isset($options[self::OSS_OBJECT]) && '/' !== $options[self::OSS_OBJECT]) {
+            $signableResource .= '/' . str_replace(array('%2F', '%25'), array('/', '%'), rawurlencode($options[self::OSS_OBJECT]));
+        }
+        if (isset($options[self::OSS_SUB_RESOURCE])) {
+            $signableResource .= '?' . $options[self::OSS_SUB_RESOURCE];
+        }
+        return $signableResource;
+    }
+
+    /**
+     * 生成query_string
+     *
+     * @param mixed $options
+     * @return string
+     */
+    private function generateQueryString($options)
+    {
+        //请求参数
+        $queryStringParams = array();
+        if (isset($options[self::OSS_QUERY_STRING])) {
+            $queryStringParams = array_merge($queryStringParams, $options[self::OSS_QUERY_STRING]);
+        }
+        return OssUtil::toQueryString($queryStringParams);
+    }
+
+    private function stringToSignSorted($string_to_sign)
+    {
+        $queryStringSorted = '';
+        $explodeResult = explode('?', $string_to_sign);
+        $index = count($explodeResult);
+        if ($index === 1)
+            return $string_to_sign;
+
+        $queryStringParams = explode('&', $explodeResult[$index - 1]);
+        sort($queryStringParams);
+
+        foreach ($queryStringParams as $params) {
+            $queryStringSorted .= $params . '&';
+        }
+
+        $queryStringSorted = substr($queryStringSorted, 0, -1);
+
+        return $explodeResult[0] . '?' . $queryStringSorted;
+    }
+
+    /**
+     * 初始化headers
+     *
+     * @param mixed  $options
+     * @param string $hostname hostname
+     * @return array
+     */
+    private function generateHeaders($options, $hostname)
+    {
+        $headers = array(
+            self::OSS_CONTENT_MD5  => '',
+            self::OSS_CONTENT_TYPE => isset($options[self::OSS_CONTENT_TYPE]) ? $options[self::OSS_CONTENT_TYPE] : self::DEFAULT_CONTENT_TYPE,
+            self::OSS_DATE         => isset($options[self::OSS_DATE]) ? $options[self::OSS_DATE] : gmdate('D, d M Y H:i:s \G\M\T'),
+            self::OSS_HOST         => $hostname,
+        );
+        if (isset($options[self::OSS_CONTENT_MD5])) {
+            $headers[self::OSS_CONTENT_MD5] = $options[self::OSS_CONTENT_MD5];
+        }
+
+        //添加stsSecurityToken
+        if ((!is_null($this->securityToken)) && (!$this->enableStsInUrl)) {
+            $headers[self::OSS_SECURITY_TOKEN] = $this->securityToken;
+        }
+        //合并HTTP headers
+        if (isset($options[self::OSS_HEADERS])) {
+            $headers = array_merge($headers, $options[self::OSS_HEADERS]);
+        }
+        return $headers;
+    }
+
+    /**
+     * 生成请求用的UserAgent
+     *
+     * @return string
+     */
+    private function generateUserAgent()
+    {
+        return self::OSS_NAME . "/" . self::OSS_VERSION . " (" . php_uname('s') . "/" . php_uname('r') . "/" . php_uname('m') . ";" . PHP_VERSION . ")";
+    }
+
+    /**
+     * 检查endpoint的种类
+     * 如有有协议头,剥去协议头
+     * 并且根据参数 is_cname 和endpoint本身,判定域名类型,是ip,cname,还是专有域或者官网域名
+     *
+     * @param string  $endpoint
+     * @param boolean $isCName
+     * @return string 剥掉协议头的域名
+     */
+    private function checkEndpoint($endpoint, $isCName)
+    {
+        $ret_endpoint = null;
+        if (strpos($endpoint, 'http://') === 0) {
+            $ret_endpoint = substr($endpoint, strlen('http://'));
+        } elseif (strpos($endpoint, 'https://') === 0) {
+            $ret_endpoint = substr($endpoint, strlen('https://'));
+            $this->useSSL = true;
+        } else {
+            $ret_endpoint = $endpoint;
+        }
+
+        if ($isCName) {
+            $this->hostType = self::OSS_HOST_TYPE_CNAME;
+        } elseif (OssUtil::isIPFormat($ret_endpoint)) {
+            $this->hostType = self::OSS_HOST_TYPE_IP;
+        } else {
+            $this->hostType = self::OSS_HOST_TYPE_NORMAL;
+        }
+        return $ret_endpoint;
+    }
+
+    /**
+     * 用来检查sdk所以来的扩展是否打开
+     *
+     * @throws OssException
+     */
+    public static function checkEnv()
+    {
+        if (function_exists('get_loaded_extensions')) {
+            //检测curl扩展
+            $enabled_extension = array("curl");
+            $extensions = get_loaded_extensions();
+            if ($extensions) {
+                foreach ($enabled_extension as $item) {
+                    if (!in_array($item, $extensions)) {
+                        throw new OssException("Extension {" . $item . "} is not installed or not enabled, please check your php env.");
+                    }
+                }
+            } else {
+                throw new OssException("function get_loaded_extensions not found.");
+            }
+        } else {
+            throw new OssException('Function get_loaded_extensions has been disabled, please check php config.');
+        }
+    }
+
+    /**
+     * //* 设置http库的请求超时时间,单位秒
+     *
+     * @param int $timeout
+     */
+    public function setTimeout($timeout)
+    {
+        $this->timeout = $timeout;
+    }
+
+    /**
+     * 设置http库的连接超时时间,单位秒
+     *
+     * @param int $connectTimeout
+     */
+    public function setConnectTimeout($connectTimeout)
+    {
+        $this->connectTimeout = $connectTimeout;
+    }
+
+    // 生命周期相关常量
+    const OSS_LIFECYCLE_EXPIRATION = "Expiration";
+    const OSS_LIFECYCLE_TIMING_DAYS = "Days";
+    const OSS_LIFECYCLE_TIMING_DATE = "Date";
+    //OSS 内部常量
+    const OSS_BUCKET = 'bucket';
+    const OSS_OBJECT = 'object';
+    const OSS_HEADERS = OssUtil::OSS_HEADERS;
+    const OSS_METHOD = 'method';
+    const OSS_QUERY = 'query';
+    const OSS_BASENAME = 'basename';
+    const OSS_MAX_KEYS = 'max-keys';
+    const OSS_UPLOAD_ID = 'uploadId';
+    const OSS_PART_NUM = 'partNumber';
+    const OSS_COMP = 'comp';
+    const OSS_LIVE_CHANNEL_STATUS = 'status';
+    const OSS_LIVE_CHANNEL_START_TIME = 'startTime';
+    const OSS_LIVE_CHANNEL_END_TIME = 'endTime';
+    const OSS_POSITION = 'position';
+    const OSS_MAX_KEYS_VALUE = 100;
+    const OSS_MAX_OBJECT_GROUP_VALUE = OssUtil::OSS_MAX_OBJECT_GROUP_VALUE;
+    const OSS_MAX_PART_SIZE = OssUtil::OSS_MAX_PART_SIZE;
+    const OSS_MID_PART_SIZE = OssUtil::OSS_MID_PART_SIZE;
+    const OSS_MIN_PART_SIZE = OssUtil::OSS_MIN_PART_SIZE;
+    const OSS_FILE_SLICE_SIZE = 8192;
+    const OSS_PREFIX = 'prefix';
+    const OSS_DELIMITER = 'delimiter';
+    const OSS_MARKER = 'marker';
+    const OSS_ACCEPT_ENCODING = 'Accept-Encoding';
+    const OSS_CONTENT_MD5 = 'Content-Md5';
+    const OSS_SELF_CONTENT_MD5 = 'x-oss-meta-md5';
+    const OSS_CONTENT_TYPE = 'Content-Type';
+    const OSS_CONTENT_LENGTH = 'Content-Length';
+    const OSS_IF_MODIFIED_SINCE = 'If-Modified-Since';
+    const OSS_IF_UNMODIFIED_SINCE = 'If-Unmodified-Since';
+    const OSS_IF_MATCH = 'If-Match';
+    const OSS_IF_NONE_MATCH = 'If-None-Match';
+    const OSS_CACHE_CONTROL = 'Cache-Control';
+    const OSS_EXPIRES = 'Expires';
+    const OSS_PREAUTH = 'preauth';
+    const OSS_CONTENT_COING = 'Content-Coding';
+    const OSS_CONTENT_DISPOSTION = 'Content-Disposition';
+    const OSS_RANGE = 'range';
+    const OSS_ETAG = 'etag';
+    const OSS_LAST_MODIFIED = 'lastmodified';
+    const OS_CONTENT_RANGE = 'Content-Range';
+    const OSS_CONTENT = OssUtil::OSS_CONTENT;
+    const OSS_BODY = 'body';
+    const OSS_LENGTH = OssUtil::OSS_LENGTH;
+    const OSS_HOST = 'Host';
+    const OSS_DATE = 'Date';
+    const OSS_AUTHORIZATION = 'Authorization';
+    const OSS_FILE_DOWNLOAD = 'fileDownload';
+    const OSS_FILE_UPLOAD = 'fileUpload';
+    const OSS_PART_SIZE = 'partSize';
+    const OSS_SEEK_TO = 'seekTo';
+    const OSS_SIZE = 'size';
+    const OSS_QUERY_STRING = 'query_string';
+    const OSS_SUB_RESOURCE = 'sub_resource';
+    const OSS_DEFAULT_PREFIX = 'x-oss-';
+    const OSS_CHECK_MD5 = 'checkmd5';
+    const DEFAULT_CONTENT_TYPE = 'application/octet-stream';
+    const OSS_SYMLINK_TARGET = 'x-oss-symlink-target';
+    const OSS_SYMLINK = 'symlink';
+    const OSS_HTTP_CODE = 'http_code';
+    const OSS_REQUEST_ID = 'x-oss-request-id';
+    const OSS_INFO = 'info';
+    const OSS_STORAGE = 'storage';
+    const OSS_RESTORE = 'restore';
+    const OSS_STORAGE_STANDARD = 'Standard';
+    const OSS_STORAGE_IA = 'IA';
+    const OSS_STORAGE_ARCHIVE = 'Archive';
+
+    //私有URL变量
+    const OSS_URL_ACCESS_KEY_ID = 'OSSAccessKeyId';
+    const OSS_URL_EXPIRES = 'Expires';
+    const OSS_URL_SIGNATURE = 'Signature';
+    //HTTP方法
+    const OSS_HTTP_GET = 'GET';
+    const OSS_HTTP_PUT = 'PUT';
+    const OSS_HTTP_HEAD = 'HEAD';
+    const OSS_HTTP_POST = 'POST';
+    const OSS_HTTP_DELETE = 'DELETE';
+    const OSS_HTTP_OPTIONS = 'OPTIONS';
+    //其他常量
+    const OSS_ACL = 'x-oss-acl';
+    const OSS_OBJECT_ACL = 'x-oss-object-acl';
+    const OSS_OBJECT_GROUP = 'x-oss-file-group';
+    const OSS_MULTI_PART = 'uploads';
+    const OSS_MULTI_DELETE = 'delete';
+    const OSS_OBJECT_COPY_SOURCE = 'x-oss-copy-source';
+    const OSS_OBJECT_COPY_SOURCE_RANGE = "x-oss-copy-source-range";
+    const OSS_PROCESS = "x-oss-process";
+    const OSS_CALLBACK = "x-oss-callback";
+    const OSS_CALLBACK_VAR = "x-oss-callback-var";
+    //支持STS SecurityToken
+    const OSS_SECURITY_TOKEN = "x-oss-security-token";
+    const OSS_ACL_TYPE_PRIVATE = 'private';
+    const OSS_ACL_TYPE_PUBLIC_READ = 'public-read';
+    const OSS_ACL_TYPE_PUBLIC_READ_WRITE = 'public-read-write';
+    const OSS_ENCODING_TYPE = "encoding-type";
+    const OSS_ENCODING_TYPE_URL = "url";
+
+    // 域名类型
+    const OSS_HOST_TYPE_NORMAL = "normal";//http://bucket.oss-cn-hangzhou.aliyuncs.com/object
+    const OSS_HOST_TYPE_IP = "ip";  //http://1.1.1.1/bucket/object
+    const OSS_HOST_TYPE_SPECIAL = 'special'; //http://bucket.guizhou.gov/object
+    const OSS_HOST_TYPE_CNAME = "cname";  //http://mydomain.com/object
+    //OSS ACL数组
+    static $OSS_ACL_TYPES = array(
+        self::OSS_ACL_TYPE_PRIVATE,
+        self::OSS_ACL_TYPE_PUBLIC_READ,
+        self::OSS_ACL_TYPE_PUBLIC_READ_WRITE
+    );
+    // OssClient版本信息
+    const OSS_NAME = "aliyun-sdk-php";
+    const OSS_VERSION = "2.3.0";
+    const OSS_BUILD = "20180105";
+    const OSS_AUTHOR = "";
+    const OSS_OPTIONS_ORIGIN = 'Origin';
+    const OSS_OPTIONS_REQUEST_METHOD = 'Access-Control-Request-Method';
+    const OSS_OPTIONS_REQUEST_HEADERS = 'Access-Control-Request-Headers';
+
+    //是否使用ssl
+    private $useSSL = false;
+    private $maxRetries = 3;
+    private $redirects = 0;
+
+    // 用户提供的域名类型,有四种 OSS_HOST_TYPE_NORMAL, OSS_HOST_TYPE_IP, OSS_HOST_TYPE_SPECIAL, OSS_HOST_TYPE_CNAME
+    private $hostType = self::OSS_HOST_TYPE_NORMAL;
+    private $requestUrl;
+    private $accessKeyId;
+    private $accessKeySecret;
+    private $hostname;
+    private $securityToken;
+    private $requestProxy = null;
+    private $enableStsInUrl = false;
+    private $timeout = 0;
+    private $connectTimeout = 0;
+}

+ 32 - 0
addons/alioss/library/OSS/Result/AclResult.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Core\OssException;
+
+/**
+ * Class AclResult getBucketAcl接口返回结果类,封装了
+ * 返回的xml数据的解析
+ *
+ * @package OSS\Result
+ */
+class AclResult extends Result
+{
+    /**
+     * @return string
+     * @throws OssException
+     */
+    protected function parseDataFromResponse()
+    {
+        $content = $this->rawResponse->body;
+        if (empty($content)) {
+            throw new OssException("body is null");
+        }
+        $xml = simplexml_load_string($content);
+        if (isset($xml->AccessControlList->Grant)) {
+            return strval($xml->AccessControlList->Grant);
+        } else {
+            throw new OssException("xml format exception");
+        }
+    }
+}

+ 27 - 0
addons/alioss/library/OSS/Result/AppendResult.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Core\OssException;
+
+/**
+ * Class AppendResult
+ * @package OSS\Result
+ */
+class AppendResult extends Result
+{
+    /**
+     * 结果中part的next-append-position
+     *
+     * @return int
+     * @throws OssException
+     */
+    protected function parseDataFromResponse()
+    {
+        $header = $this->rawResponse->header;
+        if (isset($header["x-oss-next-append-position"])) {
+            return intval($header["x-oss-next-append-position"]);
+        }
+        throw new OssException("cannot get next-append-position");
+    }
+}

+ 19 - 0
addons/alioss/library/OSS/Result/BodyResult.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace OSS\Result;
+
+
+/**
+ * Class BodyResult
+ * @package OSS\Result
+ */
+class BodyResult extends Result
+{
+    /**
+     * @return string
+     */
+    protected function parseDataFromResponse()
+    {
+        return empty($this->rawResponse->body) ? "" : $this->rawResponse->body;
+    }
+}

+ 21 - 0
addons/alioss/library/OSS/Result/CallbackResult.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace OSS\Result;
+
+
+/**
+ * Class CallbackResult
+ * @package OSS\Result
+ */
+class CallbackResult extends PutSetDeleteResult
+{
+    protected function isResponseOk()
+    {
+        $status = $this->rawResponse->status;
+        if ((int)(intval($status) / 100) == 2 && (int)(intval($status)) !== 203) {
+            return true;
+        }
+        return false;
+    }
+
+}

+ 30 - 0
addons/alioss/library/OSS/Result/CopyObjectResult.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace OSS\Result;
+
+
+/**
+ * Class CopyObjectResult
+ * @package OSS\Result
+ */
+class CopyObjectResult extends Result
+{
+    /**
+     * @return array()
+     */
+    protected function parseDataFromResponse()
+    {
+        $body = $this->rawResponse->body;
+        $xml = simplexml_load_string($body); 
+        $result = array();
+        
+        if (isset($xml->LastModified)) {
+            $result[] = $xml->LastModified;
+        }
+        if (isset($xml->ETag)) {
+            $result[] = $xml->ETag;
+        }
+
+         return $result;
+    }
+}

+ 27 - 0
addons/alioss/library/OSS/Result/DeleteObjectsResult.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace OSS\Result;
+
+
+/**
+ * Class DeleteObjectsResult
+ * @package OSS\Result
+ */
+class DeleteObjectsResult extends Result
+{
+    /**
+     * @return array()
+     */
+    protected function parseDataFromResponse()
+    {
+        $body = $this->rawResponse->body;
+        $xml = simplexml_load_string($body); 
+        $objects = array();
+
+        if (isset($xml->Deleted)) {
+            foreach($xml->Deleted as $deleteKey)
+                $objects[] = $deleteKey->Key;
+        }
+        return $objects;
+    }
+}

+ 35 - 0
addons/alioss/library/OSS/Result/ExistResult.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace OSS\Result;
+
+/**
+ * Class ExistResult 检查bucket和object是否存在的返回结果,
+ * 根据返回response的http status判断
+ * @package OSS\Result
+ */
+class ExistResult extends Result
+{
+    /**
+     * @return bool
+     */
+    protected function parseDataFromResponse()
+    {
+        return intval($this->rawResponse->status) === 200 ? true : false;
+    }
+
+    /**
+     * 根据返回http状态码判断,[200-299]即认为是OK, 判断是否存在的接口,404也认为是一种
+     * 有效响应
+     *
+     * @return bool
+     */
+    protected function isResponseOk()
+    {
+        $status = $this->rawResponse->status;
+        if ((int)(intval($status) / 100) == 2 || (int)(intval($status)) === 404) {
+            return true;
+        }
+        return false;
+    }
+
+}

+ 19 - 0
addons/alioss/library/OSS/Result/GetCnameResult.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Model\CnameConfig;
+
+class GetCnameResult extends Result
+{
+    /**
+     * @return CnameConfig
+     */
+    protected function parseDataFromResponse()
+    {
+        $content = $this->rawResponse->body;
+        $config = new CnameConfig();
+        $config->parseFromXml($content);
+        return $config;
+    }
+}

+ 35 - 0
addons/alioss/library/OSS/Result/GetCorsResult.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Model\CorsConfig;
+
+class GetCorsResult extends Result
+{
+    /**
+     * @return CorsConfig
+     */
+    protected function parseDataFromResponse()
+    {
+        $content = $this->rawResponse->body;
+        $config = new CorsConfig();
+        $config->parseFromXml($content);
+        return $config;
+    }
+
+    /**
+     * 根据返回http状态码判断,[200-299]即认为是OK, 获取bucket相关配置的接口,404也认为是一种
+     * 有效响应
+     *
+     * @return bool
+     */
+    protected function isResponseOk()
+    {
+        $status = $this->rawResponse->status;
+        if ((int)(intval($status) / 100) == 2 || (int)(intval($status)) === 404) {
+            return true;
+        }
+        return false;
+    }
+
+}

+ 41 - 0
addons/alioss/library/OSS/Result/GetLifecycleResult.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace OSS\Result;
+
+
+use OSS\Model\LifecycleConfig;
+
+/**
+ * Class GetLifecycleResult
+ * @package OSS\Result
+ */
+class GetLifecycleResult extends Result
+{
+    /**
+     *  解析Lifestyle数据
+     *
+     * @return LifecycleConfig
+     */
+    protected function parseDataFromResponse()
+    {
+        $content = $this->rawResponse->body;
+        $config = new LifecycleConfig();
+        $config->parseFromXml($content);
+        return $config;
+    }
+
+    /**
+     * 根据返回http状态码判断,[200-299]即认为是OK, 获取bucket相关配置的接口,404也认为是一种
+     * 有效响应
+     *
+     * @return bool
+     */
+    protected function isResponseOk()
+    {
+        $status = $this->rawResponse->status;
+        if ((int)(intval($status) / 100) == 2 || (int)(intval($status)) === 404) {
+            return true;
+        }
+        return false;
+    }
+}

+ 19 - 0
addons/alioss/library/OSS/Result/GetLiveChannelHistoryResult.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Model\GetLiveChannelHistory;
+
+class GetLiveChannelHistoryResult extends Result
+{
+    /**
+     * @return
+     */
+    protected function parseDataFromResponse()
+    {
+        $content = $this->rawResponse->body;
+        $channelList = new GetLiveChannelHistory();
+        $channelList->parseFromXml($content);
+        return $channelList;
+    }
+}

+ 19 - 0
addons/alioss/library/OSS/Result/GetLiveChannelInfoResult.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Model\GetLiveChannelInfo;
+
+class GetLiveChannelInfoResult extends Result
+{
+    /**
+     * @return
+     */
+    protected function parseDataFromResponse()
+    {
+        $content = $this->rawResponse->body;
+        $channelList = new GetLiveChannelInfo();
+        $channelList->parseFromXml($content);
+        return $channelList;
+    }
+}

+ 19 - 0
addons/alioss/library/OSS/Result/GetLiveChannelStatusResult.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Model\GetLiveChannelStatus;
+
+class GetLiveChannelStatusResult extends Result
+{
+    /**
+     * @return
+     */
+    protected function parseDataFromResponse()
+    {
+        $content = $this->rawResponse->body;
+        $channelList = new GetLiveChannelStatus();
+        $channelList->parseFromXml($content);
+        return $channelList;
+    }
+}

+ 30 - 0
addons/alioss/library/OSS/Result/GetLocationResult.php

@@ -0,0 +1,30 @@
+<?php
+namespace OSS\Result;
+
+use OSS\Core\OssException;
+
+/**
+ * Class GetLocationResult getBucketLocation接口返回结果类,封装了
+ * 返回的xml数据的解析
+ *
+ * @package OSS\Result
+ */
+class GetLocationResult extends Result
+{
+
+    /**
+     * Parse data from response
+     * 
+     * @return string
+     * @throws OssException
+     */
+    protected function parseDataFromResponse()
+    {
+        $content = $this->rawResponse->body;
+        if (empty($content)) {
+            throw new OssException("body is null");
+        }
+        $xml = simplexml_load_string($content);
+        return $xml;
+    }
+}

+ 41 - 0
addons/alioss/library/OSS/Result/GetLoggingResult.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Model\LoggingConfig;
+
+
+/**
+ * Class GetLoggingResult
+ * @package OSS\Result
+ */
+class GetLoggingResult extends Result
+{
+    /**
+     * 解析LoggingConfig数据
+     *
+     * @return LoggingConfig
+     */
+    protected function parseDataFromResponse()
+    {
+        $content = $this->rawResponse->body;
+        $config = new LoggingConfig();
+        $config->parseFromXml($content);
+        return $config;
+    }
+
+    /**
+     * 根据返回http状态码判断,[200-299]即认为是OK, 获取bucket相关配置的接口,404也认为是一种
+     * 有效响应
+     *
+     * @return bool
+     */
+    protected function isResponseOk()
+    {
+        $status = $this->rawResponse->status;
+        if ((int)(intval($status) / 100) == 2 || (int)(intval($status)) === 404) {
+            return true;
+        }
+        return false;
+    }
+}

+ 41 - 0
addons/alioss/library/OSS/Result/GetRefererResult.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace OSS\Result;
+
+
+use OSS\Model\RefererConfig;
+
+/**
+ * Class GetRefererResult
+ * @package OSS\Result
+ */
+class GetRefererResult extends Result
+{
+    /**
+     * 解析RefererConfig数据
+     *
+     * @return RefererConfig
+     */
+    protected function parseDataFromResponse()
+    {
+        $content = $this->rawResponse->body;
+        $config = new RefererConfig();
+        $config->parseFromXml($content);
+        return $config;
+    }
+
+    /**
+     * 根据返回http状态码判断,[200-299]即认为是OK, 获取bucket相关配置的接口,404也认为是一种
+     * 有效响应
+     *
+     * @return bool
+     */
+    protected function isResponseOk()
+    {
+        $status = $this->rawResponse->status;
+        if ((int)(intval($status) / 100) == 2 || (int)(intval($status)) === 404) {
+            return true;
+        }
+        return false;
+    }
+}

+ 34 - 0
addons/alioss/library/OSS/Result/GetStorageCapacityResult.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Core\OssException;
+
+/**
+ * Class AclResult getBucketAcl接口返回结果类,封装了
+ * 返回的xml数据的解析
+ *
+ * @package OSS\Result
+ */
+class GetStorageCapacityResult extends Result
+{
+    /**
+     * Parse data from response
+     * 
+     * @return string
+     * @throws OssException
+     */
+    protected function parseDataFromResponse()
+    {
+        $content = $this->rawResponse->body;
+        if (empty($content)) {
+            throw new OssException("body is null");
+        }
+        $xml = simplexml_load_string($content);
+        if (isset($xml->StorageCapacity)) {
+            return intval($xml->StorageCapacity);
+        } else {
+            throw new OssException("xml format exception");
+        }
+    }
+}

+ 40 - 0
addons/alioss/library/OSS/Result/GetWebsiteResult.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Model\WebsiteConfig;
+
+/**
+ * Class GetWebsiteResult
+ * @package OSS\Result
+ */
+class GetWebsiteResult extends Result
+{
+    /**
+     * 解析WebsiteConfig数据
+     *
+     * @return WebsiteConfig
+     */
+    protected function parseDataFromResponse()
+    {
+        $content = $this->rawResponse->body;
+        $config = new WebsiteConfig();
+        $config->parseFromXml($content);
+        return $config;
+    }
+
+    /**
+     * 根据返回http状态码判断,[200-299]即认为是OK, 获取bucket相关配置的接口,404也认为是一种
+     * 有效响应
+     *
+     * @return bool
+     */
+    protected function isResponseOk()
+    {
+        $status = $this->rawResponse->status;
+        if ((int)(intval($status) / 100) == 2 || (int)(intval($status)) === 404) {
+            return true;
+        }
+        return false;
+    }
+}

+ 23 - 0
addons/alioss/library/OSS/Result/HeaderResult.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace OSS\Result;
+
+
+/**
+ * Class HeaderResult
+ * @package OSS\Result
+ * @link https://docs.aliyun.com/?spm=5176.383663.13.7.HgUIqL#/pub/oss/api-reference/object&GetObjectMeta
+ */
+class HeaderResult extends Result
+{
+    /**
+     * 把返回的ResponseCore中的header作为返回数据
+     *
+     * @return array
+     */
+    protected function parseDataFromResponse()
+    {
+        return empty($this->rawResponse->header) ? array() : $this->rawResponse->header;
+    }
+
+}

+ 29 - 0
addons/alioss/library/OSS/Result/InitiateMultipartUploadResult.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Core\OssException;
+
+
+/**
+ * Class initiateMultipartUploadResult
+ * @package OSS\Result
+ */
+class InitiateMultipartUploadResult extends Result
+{
+    /**
+     * 结果中获取uploadId并返回
+     *
+     * @throws OssException
+     * @return string
+     */
+    protected function parseDataFromResponse()
+    {
+        $content = $this->rawResponse->body;
+        $xml = simplexml_load_string($content);
+        if (isset($xml->UploadId)) {
+            return strval($xml->UploadId);
+        }
+        throw new OssException("cannot get UploadId");
+    }
+}

+ 33 - 0
addons/alioss/library/OSS/Result/ListBucketsResult.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Model\BucketInfo;
+use OSS\Model\BucketListInfo;
+
+/**
+ * Class ListBucketsResult
+ *
+ * @package OSS\Result
+ */
+class ListBucketsResult extends Result
+{
+    /**
+     * @return BucketListInfo
+     */
+    protected function parseDataFromResponse()
+    {
+        $bucketList = array();
+        $content = $this->rawResponse->body;
+        $xml = new \SimpleXMLElement($content);
+        if (isset($xml->Buckets) && isset($xml->Buckets->Bucket)) {
+            foreach ($xml->Buckets->Bucket as $bucket) {
+                $bucketInfo = new BucketInfo(strval($bucket->Location),
+                    strval($bucket->Name),
+                    strval($bucket->CreationDate));
+                $bucketList[] = $bucketInfo;
+            }
+        }
+        return new BucketListInfo($bucketList);
+    }
+}

+ 16 - 0
addons/alioss/library/OSS/Result/ListLiveChannelResult.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Model\LiveChannelListInfo;
+
+class ListLiveChannelResult extends Result
+{
+    protected function parseDataFromResponse()
+    {
+        $content = $this->rawResponse->body;
+        $channelList = new LiveChannelListInfo();
+        $channelList->parseFromXml($content);
+        return $channelList;
+    }
+}

+ 55 - 0
addons/alioss/library/OSS/Result/ListMultipartUploadResult.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Core\OssUtil;
+use OSS\Model\ListMultipartUploadInfo;
+use OSS\Model\UploadInfo;
+
+
+/**
+ * Class ListMultipartUploadResult
+ * @package OSS\Result
+ */
+class ListMultipartUploadResult extends Result
+{
+    /**
+     * 解析从ListMultipartUpload接口的返回数据
+     *
+     * @return ListMultipartUploadInfo
+     */
+    protected function parseDataFromResponse()
+    {
+        $content = $this->rawResponse->body;
+        $xml = simplexml_load_string($content);
+
+        $encodingType = isset($xml->EncodingType) ? strval($xml->EncodingType) : "";
+        $bucket = isset($xml->Bucket) ? strval($xml->Bucket) : "";
+        $keyMarker = isset($xml->KeyMarker) ? strval($xml->KeyMarker) : "";
+        $keyMarker = OssUtil::decodeKey($keyMarker, $encodingType);
+        $uploadIdMarker = isset($xml->UploadIdMarker) ? strval($xml->UploadIdMarker) : "";
+        $nextKeyMarker = isset($xml->NextKeyMarker) ? strval($xml->NextKeyMarker) : "";
+        $nextKeyMarker = OssUtil::decodeKey($nextKeyMarker, $encodingType);
+        $nextUploadIdMarker = isset($xml->NextUploadIdMarker) ? strval($xml->NextUploadIdMarker) : "";
+        $delimiter = isset($xml->Delimiter) ? strval($xml->Delimiter) : "";
+        $delimiter = OssUtil::decodeKey($delimiter, $encodingType);
+        $prefix = isset($xml->Prefix) ? strval($xml->Prefix) : "";
+        $prefix = OssUtil::decodeKey($prefix, $encodingType);
+        $maxUploads = isset($xml->MaxUploads) ? intval($xml->MaxUploads) : 0;
+        $isTruncated = isset($xml->IsTruncated) ? strval($xml->IsTruncated) : "";
+        $listUpload = array();
+
+        if (isset($xml->Upload)) {
+            foreach ($xml->Upload as $upload) {
+                $key = isset($upload->Key) ? strval($upload->Key) : "";
+                $key = OssUtil::decodeKey($key, $encodingType);
+                $uploadId = isset($upload->UploadId) ? strval($upload->UploadId) : "";
+                $initiated = isset($upload->Initiated) ? strval($upload->Initiated) : "";
+                $listUpload[] = new UploadInfo($key, $uploadId, $initiated);
+            }
+        }
+        return new ListMultipartUploadInfo($bucket, $keyMarker, $uploadIdMarker,
+            $nextKeyMarker, $nextUploadIdMarker,
+            $delimiter, $prefix, $maxUploads, $isTruncated, $listUpload);
+    }
+}

+ 71 - 0
addons/alioss/library/OSS/Result/ListObjectsResult.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Core\OssUtil;
+use OSS\Model\ObjectInfo;
+use OSS\Model\ObjectListInfo;
+use OSS\Model\PrefixInfo;
+
+/**
+ * Class ListObjectsResult
+ * @package OSS\Result
+ */
+class ListObjectsResult extends Result
+{
+    /**
+     * 解析ListObjects接口返回的xml数据
+     *
+     * return ObjectListInfo
+     */
+    protected function parseDataFromResponse()
+    {
+        $xml = new \SimpleXMLElement($this->rawResponse->body);
+        $encodingType = isset($xml->EncodingType) ? strval($xml->EncodingType) : "";
+        $objectList = $this->parseObjectList($xml, $encodingType);
+        $prefixList = $this->parsePrefixList($xml, $encodingType);
+        $bucketName = isset($xml->Name) ? strval($xml->Name) : "";
+        $prefix = isset($xml->Prefix) ? strval($xml->Prefix) : "";
+        $prefix = OssUtil::decodeKey($prefix, $encodingType);
+        $marker = isset($xml->Marker) ? strval($xml->Marker) : "";
+        $marker = OssUtil::decodeKey($marker, $encodingType);
+        $maxKeys = isset($xml->MaxKeys) ? intval($xml->MaxKeys) : 0;
+        $delimiter = isset($xml->Delimiter) ? strval($xml->Delimiter) : "";
+        $delimiter = OssUtil::decodeKey($delimiter, $encodingType);
+        $isTruncated = isset($xml->IsTruncated) ? strval($xml->IsTruncated) : "";
+        $nextMarker = isset($xml->NextMarker) ? strval($xml->NextMarker) : "";
+        $nextMarker = OssUtil::decodeKey($nextMarker, $encodingType);
+        return new ObjectListInfo($bucketName, $prefix, $marker, $nextMarker, $maxKeys, $delimiter, $isTruncated, $objectList, $prefixList);
+    }
+
+    private function parseObjectList($xml, $encodingType)
+    {
+        $retList = array();
+        if (isset($xml->Contents)) {
+            foreach ($xml->Contents as $content) {
+                $key = isset($content->Key) ? strval($content->Key) : "";
+                $key = OssUtil::decodeKey($key, $encodingType);
+                $lastModified = isset($content->LastModified) ? strval($content->LastModified) : "";
+                $eTag = isset($content->ETag) ? strval($content->ETag) : "";
+                $type = isset($content->Type) ? strval($content->Type) : "";
+                $size = isset($content->Size) ? intval($content->Size) : 0;
+                $storageClass = isset($content->StorageClass) ? strval($content->StorageClass) : "";
+                $retList[] = new ObjectInfo($key, $lastModified, $eTag, $type, $size, $storageClass);
+            }
+        }
+        return $retList;
+    }
+
+    private function parsePrefixList($xml, $encodingType)
+    {
+        $retList = array();
+        if (isset($xml->CommonPrefixes)) {
+            foreach ($xml->CommonPrefixes as $commonPrefix) {
+                $prefix = isset($commonPrefix->Prefix) ? strval($commonPrefix->Prefix) : "";
+                $prefix = OssUtil::decodeKey($prefix, $encodingType);
+                $retList[] = new PrefixInfo($prefix);
+            }
+        }
+        return $retList;
+    }
+}

+ 42 - 0
addons/alioss/library/OSS/Result/ListPartsResult.php

@@ -0,0 +1,42 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Model\ListPartsInfo;
+use OSS\Model\PartInfo;
+
+
+/**
+ * Class ListPartsResult
+ * @package OSS\Result
+ */
+class ListPartsResult extends Result
+{
+    /**
+     * 解析ListParts接口返回的xml数据
+     *
+     * @return ListPartsInfo
+     */
+    protected function parseDataFromResponse()
+    {
+        $content = $this->rawResponse->body;
+        $xml = simplexml_load_string($content);
+        $bucket = isset($xml->Bucket) ? strval($xml->Bucket) : "";
+        $key = isset($xml->Key) ? strval($xml->Key) : "";
+        $uploadId = isset($xml->UploadId) ? strval($xml->UploadId) : "";
+        $nextPartNumberMarker = isset($xml->NextPartNumberMarker) ? intval($xml->NextPartNumberMarker) : "";
+        $maxParts = isset($xml->MaxParts) ? intval($xml->MaxParts) : "";
+        $isTruncated = isset($xml->IsTruncated) ? strval($xml->IsTruncated) : "";
+        $partList = array();
+        if (isset($xml->Part)) {
+            foreach ($xml->Part as $part) {
+                $partNumber = isset($part->PartNumber) ? intval($part->PartNumber) : "";
+                $lastModified = isset($part->LastModified) ? strval($part->LastModified) : "";
+                $eTag = isset($part->ETag) ? strval($part->ETag) : "";
+                $size = isset($part->Size) ? intval($part->Size) : "";
+                $partList[] = new PartInfo($partNumber, $lastModified, $eTag, $size);
+            }
+        }
+        return new ListPartsInfo($bucket, $key, $uploadId, $nextPartNumberMarker, $maxParts, $isTruncated, $partList);
+    }
+}

+ 16 - 0
addons/alioss/library/OSS/Result/PutLiveChannelResult.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Model\LiveChannelInfo;
+
+class PutLiveChannelResult extends Result
+{
+    protected function parseDataFromResponse()
+    {
+        $content = $this->rawResponse->body;
+        $channel = new LiveChannelInfo();
+        $channel->parseFromXml($content);
+        return $channel;
+    }
+}

+ 20 - 0
addons/alioss/library/OSS/Result/PutSetDeleteResult.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace OSS\Result;
+
+
+/**
+ * Class PutSetDeleteResult
+ * @package OSS\Result
+ */
+class PutSetDeleteResult extends Result
+{
+    /**
+     * @return array()
+     */
+    protected function parseDataFromResponse()
+    {
+        $body = array('body' => $this->rawResponse->body);
+        return array_merge($this->rawResponse->header, $body);
+    }
+}

+ 175 - 0
addons/alioss/library/OSS/Result/Result.php

@@ -0,0 +1,175 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Core\OssException;
+use OSS\Http\ResponseCore;
+
+
+/**
+ * Class Result, 操作结果类的基类,不同的请求在处理返回数据的时候有不同的逻辑,
+ * 具体的解析逻辑推迟到子类实现
+ *
+ * @package OSS\Model
+ */
+abstract class Result
+{
+    /**
+     * Result constructor.
+     * @param $response ResponseCore
+     * @throws OssException
+     */
+    public function __construct($response)
+    {
+        if ($response === null) {
+            throw new OssException("raw response is null");
+        }
+        $this->rawResponse = $response;
+        $this->parseResponse();
+    }
+
+    /**
+     * 获取requestId
+     *
+     * @return string
+     */
+    public function getRequestId()
+    {
+        if (isset($this->rawResponse) &&
+            isset($this->rawResponse->header) &&
+            isset($this->rawResponse->header['x-oss-request-id'])
+        ) {
+            return $this->rawResponse->header['x-oss-request-id'];
+        } else {
+            return '';
+        }
+    }
+
+    /**
+     * 得到返回数据,不同的请求返回数据格式不同
+     *
+     * $return mixed
+     */
+    public function getData()
+    {
+        return $this->parsedData;
+    }
+
+    /**
+     * 由子类实现,不同的请求返回数据有不同的解析逻辑,由子类实现
+     *
+     * @return mixed
+     */
+    abstract protected function parseDataFromResponse();
+
+    /**
+     * 操作是否成功
+     *
+     * @return mixed
+     */
+    public function isOK()
+    {
+        return $this->isOk;
+    }
+
+    /**
+     * @throws OssException
+     */
+    public function parseResponse()
+    {
+        $this->isOk = $this->isResponseOk();
+        if ($this->isOk) {
+            $this->parsedData = $this->parseDataFromResponse();
+        } else {
+            $httpStatus = strval($this->rawResponse->status);
+            $requestId = strval($this->getRequestId());
+            $code = $this->retrieveErrorCode($this->rawResponse->body);
+            $message = $this->retrieveErrorMessage($this->rawResponse->body);
+            $body = $this->rawResponse->body;
+
+            $details = array(
+                'status' => $httpStatus,
+                'request-id' => $requestId,
+                'code' => $code,
+                'message' => $message,
+                'body' => $body
+            );
+            throw new OssException($details);
+        }
+    }
+
+    /**
+     * 尝试从body中获取错误Message
+     *
+     * @param $body
+     * @return string
+     */
+    private function retrieveErrorMessage($body)
+    {
+        if (empty($body) || false === strpos($body, '<?xml')) {
+            return '';
+        }
+        $xml = simplexml_load_string($body);
+        if (isset($xml->Message)) {
+            return strval($xml->Message);
+        }
+        return '';
+    }
+
+    /**
+     * 尝试从body中获取错误Code
+     *
+     * @param $body
+     * @return string
+     */
+    private function retrieveErrorCode($body)
+    {
+        if (empty($body) || false === strpos($body, '<?xml')) {
+            return '';
+        }
+        $xml = simplexml_load_string($body);
+        if (isset($xml->Code)) {
+            return strval($xml->Code);
+        }
+        return '';
+    }
+
+    /**
+     * 根据返回http状态码判断,[200-299]即认为是OK
+     *
+     * @return bool
+     */
+    protected function isResponseOk()
+    {
+        $status = $this->rawResponse->status;
+        if ((int)(intval($status) / 100) == 2) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 返回原始的返回数据
+     *
+     * @return ResponseCore
+     */
+    public function getRawResponse()
+    {
+        return $this->rawResponse;
+    }
+
+    /**
+     * 标示请求是否成功
+     */
+    protected $isOk = false;
+    /**
+     * 由子类解析过的数据
+     */
+    protected $parsedData = null;
+    /**
+     * 存放auth函数返回的原始Response
+     *
+     * @var ResponseCore
+     */
+    protected $rawResponse;
+}

+ 24 - 0
addons/alioss/library/OSS/Result/SymlinkResult.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Core\OssException;
+use OSS\OssClient;
+
+/**
+ *
+ * @package OSS\Result
+ */
+class SymlinkResult extends Result
+{
+    /**
+     * @return string
+     * @throws OssException
+     */
+    protected function parseDataFromResponse()
+    {
+        $this->rawResponse->header[OssClient::OSS_SYMLINK_TARGET] = rawurldecode($this->rawResponse->header[OssClient::OSS_SYMLINK_TARGET]);
+        return $this->rawResponse->header;
+    }
+}
+

+ 28 - 0
addons/alioss/library/OSS/Result/UploadPartResult.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace OSS\Result;
+
+use OSS\Core\OssException;
+
+/**
+ * Class UploadPartResult
+ * @package OSS\Result
+ */
+class UploadPartResult extends Result
+{
+    /**
+     * 结果中part的ETag
+     *
+     * @return string
+     * @throws OssException
+     */
+    protected function parseDataFromResponse()
+    {
+        $header = $this->rawResponse->header;
+        if (isset($header["etag"])) {
+            return $header["etag"];
+        }
+        throw new OssException("cannot get ETag");
+
+    }
+}

+ 1 - 0
addons/simditor/.addonrc

@@ -0,0 +1 @@
+{"license":"regular","licenseto":"5946","licensekey":"KStDm3uJ8kYizfOn geF7FZzOi+UnaIO5igec5Q=="}

+ 31 - 0
addons/simditor/Simditor.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace addons\simditor;
+
+use think\Addons;
+
+/**
+ * 插件
+ */
+class Simditor extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        return true;
+    }
+
+}

File diff suppressed because it is too large
+ 0 - 0
addons/simditor/assets/css/simditor.min.css


BIN
addons/simditor/assets/images/image.png


File diff suppressed because it is too large
+ 0 - 0
addons/simditor/assets/js/simditor.min.js


+ 48 - 0
addons/simditor/bootstrap.js

@@ -0,0 +1,48 @@
+require.config({
+    paths: {
+        'simditor': '../addons/simditor/js/simditor.min',
+    },
+    shim: {
+        'simditor': [
+            'css!../addons/simditor/css/simditor.min.css'
+        ]
+    }
+});
+require(['form'], function (Form) {
+    var _bindevent = Form.events.bindevent;
+    Form.events.bindevent = function (form) {
+        _bindevent.apply(this, [form]);
+        if ($(".editor", form).size() > 0) {
+            //修改上传的接口调用
+            require(['upload', 'simditor'], function (Upload, Simditor) {
+                var editor, mobileToolbar, toolbar;
+                Simditor.locale = 'zh-CN';
+                Simditor.list = {};
+                toolbar = ['title', 'bold', 'italic', 'underline', 'strikethrough', 'fontScale', 'color', '|', 'ol', 'ul', 'blockquote', 'code', 'table', '|', 'link', 'image', 'hr', '|', 'indent', 'outdent', 'alignment'];
+                mobileToolbar = ["bold", "underline", "strikethrough", "color", "ul", "ol"];
+                $(".editor", form).each(function () {
+                    var id = $(this).attr("id");
+                    editor = new Simditor({
+                        textarea: this,
+                        toolbarFloat: false,
+                        toolbar: toolbar,
+                        pasteImage: true,
+                        defaultImage: Config.__CDN__ + '/assets/addons/simditor/images/image.png',
+                        upload: {url: '/'}
+                    });
+                    editor.uploader.on('beforeupload', function (e, file) {
+                        Upload.api.send(file.obj, function (data) {
+                            var url = Fast.api.cdnurl(data.url);
+                            editor.uploader.trigger("uploadsuccess", [file, {success: true, file_path: url}]);
+                        });
+                        return false;
+                    });
+                    editor.on("blur", function () {
+                        this.textarea.trigger("blur");
+                    });
+                    Simditor.list[id] = editor;
+                });
+            });
+        }
+    }
+});

+ 4 - 0
addons/simditor/build/build.sh

@@ -0,0 +1,4 @@
+#!/bin/bash
+
+/usr/local/bin/node r.js -o ./js.js name=simditor baseUrl=../src/js out=../assets/js/simditor.min.js
+/usr/local/bin/node r.js -o ./css.js cssIn=../src/css/simditor.css out=../assets/css/simditor.min.css

+ 4 - 0
addons/simditor/build/css.js

@@ -0,0 +1,4 @@
+({
+  optimizeCss: "default",
+  optimize: "uglify"
+})

+ 10 - 0
addons/simditor/build/js.js

@@ -0,0 +1,10 @@
+({
+    name: "simditor",
+    paths: {
+        'jquery': 'empty:',
+        'simditor': 'simditor',
+        'simple-module': 'module',
+        'simple-uploader': 'uploader',
+        'simple-hotkeys': 'hotkeys',
+    },
+});

File diff suppressed because it is too large
+ 4699 - 0
addons/simditor/build/r.js


+ 4 - 0
addons/simditor/config.php

@@ -0,0 +1,4 @@
+<?php
+
+return [
+];

+ 10 - 0
addons/simditor/info.ini

@@ -0,0 +1,10 @@
+name = simditor
+title = Simditor
+intro = 简洁清晰的富文本插件
+author = Karson
+website = http://www.fastadmin.net
+version = 1.0.5
+state = 0
+url = /addons/simditor
+license = regular
+licenseto = 5946

+ 460 - 0
addons/simditor/src/css/mobile.css

@@ -0,0 +1,460 @@
+@media screen and (max-device-width: 240px) and (min-device-width: 220px) {
+  body {
+    width: 240px;
+    margin: 0 auto;
+  }
+  body .wrapper {
+    width: 100%;
+  }
+  body .wrapper header {
+    padding: 30px 0 20px;
+  }
+  body .wrapper header h1 {
+    background-size: 200px auto;
+    background-position: 50px 0;
+    padding-top: 90px;
+    height: 45px;
+  }
+  body .wrapper header h1 a {
+    background-size: 160px auto;
+    background-position: 10px 0;
+  }
+  body .wrapper header p.desc {
+    font-size: 16px;
+  }
+  body .wrapper footer {
+    margin: 20px 0;
+  }
+  body .wrapper #page-demo {
+    width: 96%;
+    margin: 0 2%;
+  }
+  body .wrapper #link-fork {
+    z-index: -1;
+    width: 80px;
+    height: auto;
+  }
+  body .wrapper #link-fork img {
+    max-width: 80px;
+    height: auto;
+  }
+
+  nav {
+    display: none;
+  }
+}
+@media screen and (max-device-width: 320px) and (min-device-width: 300px) {
+  body {
+    width: 320px;
+    margin: 0 auto;
+  }
+  body .wrapper {
+    width: 100%;
+  }
+  body .wrapper header {
+    padding: 30px 0 20px;
+  }
+  body .wrapper header h1 {
+    background-size: 200px auto;
+    background-position: 50px 0;
+    padding-top: 90px;
+    height: 45px;
+  }
+  body .wrapper header h1 a {
+    background-size: 160px auto;
+    background-position: 10px 0;
+  }
+  body .wrapper header p.desc {
+    font-size: 16px;
+  }
+  body .wrapper footer {
+    margin: 20px 0;
+  }
+  body .wrapper #page-demo {
+    width: 96%;
+    margin: 0 2%;
+  }
+  body .wrapper #link-fork {
+    z-index: -1;
+    width: 80px;
+    height: auto;
+  }
+  body .wrapper #link-fork img {
+    max-width: 80px;
+    height: auto;
+  }
+
+  nav {
+    display: none;
+  }
+}
+@media screen and (max-device-width: 360px) and (min-device-width: 340px) {
+  body {
+    width: 360px;
+    margin: 0 auto;
+  }
+  body .wrapper {
+    width: 100%;
+  }
+  body .wrapper header {
+    padding: 30px 0 20px;
+  }
+  body .wrapper header h1 {
+    background-size: 200px auto;
+    background-position: 50px 0;
+    padding-top: 90px;
+    height: 45px;
+  }
+  body .wrapper header h1 a {
+    background-size: 160px auto;
+    background-position: 10px 0;
+  }
+  body .wrapper header p.desc {
+    font-size: 16px;
+  }
+  body .wrapper footer {
+    margin: 20px 0;
+  }
+  body .wrapper #page-demo {
+    width: 96%;
+    margin: 0 2%;
+  }
+  body .wrapper #link-fork {
+    z-index: -1;
+    width: 80px;
+    height: auto;
+  }
+  body .wrapper #link-fork img {
+    max-width: 80px;
+    height: auto;
+  }
+
+  nav {
+    display: none;
+  }
+}
+@media screen and (max-device-width: 480px) and (min-device-width: 460px) {
+  body {
+    width: 480px;
+    margin: 0 auto;
+  }
+  body .wrapper {
+    width: 100%;
+  }
+  body .wrapper header {
+    padding: 30px 0 20px;
+  }
+  body .wrapper header h1 {
+    background-size: 200px auto;
+    background-position: 50px 0;
+    padding-top: 90px;
+    height: 45px;
+  }
+  body .wrapper header h1 a {
+    background-size: 160px auto;
+    background-position: 10px 0;
+  }
+  body .wrapper header p.desc {
+    font-size: 16px;
+  }
+  body .wrapper footer {
+    margin: 20px 0;
+  }
+  body .wrapper #page-demo {
+    width: 96%;
+    margin: 0 2%;
+  }
+  body .wrapper #link-fork {
+    z-index: -1;
+    width: 80px;
+    height: auto;
+  }
+  body .wrapper #link-fork img {
+    max-width: 80px;
+    height: auto;
+  }
+
+  nav {
+    display: none;
+  }
+}
+@media screen and (max-device-width: 640px) and (min-device-width: 620px) {
+  body {
+    width: 320px;
+    margin: 0 auto;
+  }
+  body .wrapper {
+    width: 100%;
+  }
+  body .wrapper header {
+    padding: 30px 0 20px;
+  }
+  body .wrapper header h1 {
+    background-size: 200px auto;
+    background-position: 50px 0;
+    padding-top: 90px;
+    height: 45px;
+  }
+  body .wrapper header h1 a {
+    background-size: 160px auto;
+    background-position: 10px 0;
+  }
+  body .wrapper header p.desc {
+    font-size: 16px;
+  }
+  body .wrapper footer {
+    margin: 20px 0;
+  }
+  body .wrapper #page-demo {
+    width: 96%;
+    margin: 0 2%;
+  }
+  body .wrapper #link-fork {
+    z-index: -1;
+    width: 80px;
+    height: auto;
+  }
+  body .wrapper #link-fork img {
+    max-width: 80px;
+    height: auto;
+  }
+
+  nav {
+    display: none;
+  }
+}
+@media screen and (max-device-width: 720px) and (min-device-width: 700px) {
+  body {
+    width: 360px;
+    margin: 0 auto;
+  }
+  body .wrapper {
+    width: 100%;
+  }
+  body .wrapper header {
+    padding: 30px 0 20px;
+  }
+  body .wrapper header h1 {
+    background-size: 200px auto;
+    background-position: 50px 0;
+    padding-top: 90px;
+    height: 45px;
+  }
+  body .wrapper header h1 a {
+    background-size: 160px auto;
+    background-position: 10px 0;
+  }
+  body .wrapper header p.desc {
+    font-size: 16px;
+  }
+  body .wrapper footer {
+    margin: 20px 0;
+  }
+  body .wrapper #page-demo {
+    width: 96%;
+    margin: 0 2%;
+  }
+  body .wrapper #link-fork {
+    z-index: -1;
+    width: 80px;
+    height: auto;
+  }
+  body .wrapper #link-fork img {
+    max-width: 80px;
+    height: auto;
+  }
+
+  nav {
+    display: none;
+  }
+}
+@media screen and (max-device-width: 800px) and (min-device-width: 780px) {
+  body {
+    width: 400px;
+    margin: 0 auto;
+  }
+  body .wrapper {
+    width: 100%;
+  }
+  body .wrapper header {
+    padding: 30px 0 20px;
+  }
+  body .wrapper header h1 {
+    background-size: 200px auto;
+    background-position: 50px 0;
+    padding-top: 90px;
+    height: 45px;
+  }
+  body .wrapper header h1 a {
+    background-size: 160px auto;
+    background-position: 10px 0;
+  }
+  body .wrapper header p.desc {
+    font-size: 16px;
+  }
+  body .wrapper footer {
+    margin: 20px 0;
+  }
+  body .wrapper #page-demo {
+    width: 96%;
+    margin: 0 2%;
+  }
+  body .wrapper #link-fork {
+    z-index: -1;
+    width: 88.8888888889px;
+    height: auto;
+  }
+  body .wrapper #link-fork img {
+    max-width: 88.8888888889px;
+    height: auto;
+  }
+
+  nav {
+    display: none;
+  }
+}
+@media screen and (max-device-width: 960px) and (min-device-width: 940px) {
+  body {
+    width: 480px;
+    margin: 0 auto;
+  }
+  body .wrapper {
+    width: 100%;
+  }
+  body .wrapper header {
+    padding: 30px 0 20px;
+  }
+  body .wrapper header h1 {
+    background-size: 200px auto;
+    background-position: 50px 0;
+    padding-top: 90px;
+    height: 45px;
+  }
+  body .wrapper header h1 a {
+    background-size: 160px auto;
+    background-position: 10px 0;
+  }
+  body .wrapper header p.desc {
+    font-size: 16px;
+  }
+  body .wrapper footer {
+    margin: 20px 0;
+  }
+  body .wrapper #page-demo {
+    width: 96%;
+    margin: 0 2%;
+  }
+  body .wrapper #link-fork {
+    z-index: -1;
+    width: 100px;
+    height: auto;
+  }
+  body .wrapper #link-fork img {
+    max-width: 100px;
+    height: auto;
+  }
+
+  nav {
+    display: none;
+  }
+}
+@media screen and (max-device-width: 1024px) and (min-device-width: 1004px) {
+  body {
+    width: 512px;
+    margin: 0 auto;
+  }
+  body .wrapper {
+    width: 100%;
+  }
+  body .wrapper header {
+    padding: 30px 0 20px;
+  }
+  body .wrapper header h1 {
+    background-size: 200px auto;
+    background-position: 50px 0;
+    padding-top: 90px;
+    height: 45px;
+  }
+  body .wrapper header h1 a {
+    background-size: 160px auto;
+    background-position: 10px 0;
+  }
+  body .wrapper header p.desc {
+    font-size: 16px;
+  }
+  body .wrapper footer {
+    margin: 20px 0;
+  }
+  body .wrapper #page-demo {
+    width: 96%;
+    margin: 0 2%;
+  }
+  body .wrapper #link-fork {
+    z-index: -1;
+    width: 100px;
+    height: auto;
+  }
+  body .wrapper #link-fork img {
+    max-width: 100px;
+    height: auto;
+  }
+
+  nav {
+    display: none;
+  }
+}
+@media screen and (max-device-width: 1280px) and (min-device-width: 1260px) {
+  body {
+    width: 640px;
+    margin: 0 auto;
+  }
+  body .wrapper {
+    width: 100%;
+  }
+  body .wrapper header {
+    padding: 30px 0 20px;
+  }
+  body .wrapper header h1 {
+    background-size: 200px auto;
+    background-position: 50px 0;
+    padding-top: 90px;
+    height: 45px;
+  }
+  body .wrapper header h1 a {
+    background-size: 160px auto;
+    background-position: 10px 0;
+  }
+  body .wrapper header p.desc {
+    font-size: 16px;
+  }
+  body .wrapper footer {
+    margin: 20px 0;
+  }
+  body .wrapper #page-demo {
+    width: 96%;
+    margin: 0 2%;
+  }
+  body .wrapper #link-fork {
+    z-index: -1;
+    width: 100px;
+    height: auto;
+  }
+  body .wrapper #link-fork img {
+    max-width: 100px;
+    height: auto;
+  }
+
+  nav {
+    display: none;
+  }
+}
+@media screen and (device-aspect-ratio: 40 / 71) and (orientation: landscape) {
+  body {
+    width: 568px;
+  }
+}
+@media screen and (device-aspect-ratio: 2 / 3) and (orientation: landscape) {
+  body {
+    width: 480px;
+  }
+}

File diff suppressed because it is too large
+ 2 - 0
addons/simditor/src/css/simditor.css


BIN
addons/simditor/src/images/image.png


+ 241 - 0
addons/simditor/src/js/hotkeys.js

@@ -0,0 +1,241 @@
+(function (root, factory) {
+  if (typeof define === 'function' && define.amd) {
+    // AMD. Register as an anonymous module unless amdModuleId is set
+    define('simple-hotkeys', ["jquery","simple-module"], function ($, SimpleModule) {
+      return (root['hotkeys'] = factory($, SimpleModule));
+    });
+  } else if (typeof exports === 'object') {
+    // Node. Does not work with strict CommonJS, but
+    // only CommonJS-like environments that support module.exports,
+    // like Node.
+    module.exports = factory(require("jquery"),require("simple-module"));
+  } else {
+    root.simple = root.simple || {};
+    root.simple['hotkeys'] = factory(jQuery,SimpleModule);
+  }
+}(this, function ($, SimpleModule) {
+
+var Hotkeys, hotkeys,
+  extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+  hasProp = {}.hasOwnProperty;
+
+Hotkeys = (function(superClass) {
+  extend(Hotkeys, superClass);
+
+  function Hotkeys() {
+    return Hotkeys.__super__.constructor.apply(this, arguments);
+  }
+
+  Hotkeys.count = 0;
+
+  Hotkeys.keyNameMap = {
+    8: "Backspace",
+    9: "Tab",
+    13: "Enter",
+    16: "Shift",
+    17: "Control",
+    18: "Alt",
+    19: "Pause",
+    20: "CapsLock",
+    27: "Esc",
+    32: "Spacebar",
+    33: "PageUp",
+    34: "PageDown",
+    35: "End",
+    36: "Home",
+    37: "Left",
+    38: "Up",
+    39: "Right",
+    40: "Down",
+    45: "Insert",
+    46: "Del",
+    91: "Meta",
+    93: "Meta",
+    48: "0",
+    49: "1",
+    50: "2",
+    51: "3",
+    52: "4",
+    53: "5",
+    54: "6",
+    55: "7",
+    56: "8",
+    57: "9",
+    65: "A",
+    66: "B",
+    67: "C",
+    68: "D",
+    69: "E",
+    70: "F",
+    71: "G",
+    72: "H",
+    73: "I",
+    74: "J",
+    75: "K",
+    76: "L",
+    77: "M",
+    78: "N",
+    79: "O",
+    80: "P",
+    81: "Q",
+    82: "R",
+    83: "S",
+    84: "T",
+    85: "U",
+    86: "V",
+    87: "W",
+    88: "X",
+    89: "Y",
+    90: "Z",
+    96: "0",
+    97: "1",
+    98: "2",
+    99: "3",
+    100: "4",
+    101: "5",
+    102: "6",
+    103: "7",
+    104: "8",
+    105: "9",
+    106: "Multiply",
+    107: "Add",
+    109: "Subtract",
+    110: "Decimal",
+    111: "Divide",
+    112: "F1",
+    113: "F2",
+    114: "F3",
+    115: "F4",
+    116: "F5",
+    117: "F6",
+    118: "F7",
+    119: "F8",
+    120: "F9",
+    121: "F10",
+    122: "F11",
+    123: "F12",
+    124: "F13",
+    125: "F14",
+    126: "F15",
+    127: "F16",
+    128: "F17",
+    129: "F18",
+    130: "F19",
+    131: "F20",
+    132: "F21",
+    133: "F22",
+    134: "F23",
+    135: "F24",
+    59: ";",
+    61: "=",
+    186: ";",
+    187: "=",
+    188: ",",
+    190: ".",
+    191: "/",
+    192: "`",
+    219: "[",
+    220: "\\",
+    221: "]",
+    222: "'"
+  };
+
+  Hotkeys.aliases = {
+    "escape": "esc",
+    "delete": "del",
+    "return": "enter",
+    "ctrl": "control",
+    "space": "spacebar",
+    "ins": "insert",
+    "cmd": "meta",
+    "command": "meta",
+    "wins": "meta",
+    "windows": "meta"
+  };
+
+  Hotkeys.normalize = function(shortcut) {
+    var i, j, key, keyname, keys, len;
+    keys = shortcut.toLowerCase().replace(/\s+/gi, "").split("+");
+    for (i = j = 0, len = keys.length; j < len; i = ++j) {
+      key = keys[i];
+      keys[i] = this.aliases[key] || key;
+    }
+    keyname = keys.pop();
+    keys.sort().push(keyname);
+    return keys.join("_");
+  };
+
+  Hotkeys.prototype.opts = {
+    el: document
+  };
+
+  Hotkeys.prototype._init = function() {
+    this.id = ++this.constructor.count;
+    this._map = {};
+    this._delegate = typeof this.opts.el === "string" ? document : this.opts.el;
+    return $(this._delegate).on("keydown.simple-hotkeys-" + this.id, this.opts.el, (function(_this) {
+      return function(e) {
+        var ref;
+        return (ref = _this._getHander(e)) != null ? ref.call(_this, e) : void 0;
+      };
+    })(this));
+  };
+
+  Hotkeys.prototype._getHander = function(e) {
+    var keyname, shortcut;
+    if (!(keyname = this.constructor.keyNameMap[e.which])) {
+      return;
+    }
+    shortcut = "";
+    if (e.altKey) {
+      shortcut += "alt_";
+    }
+    if (e.ctrlKey) {
+      shortcut += "control_";
+    }
+    if (e.metaKey) {
+      shortcut += "meta_";
+    }
+    if (e.shiftKey) {
+      shortcut += "shift_";
+    }
+    shortcut += keyname.toLowerCase();
+    return this._map[shortcut];
+  };
+
+  Hotkeys.prototype.respondTo = function(subject) {
+    if (typeof subject === 'string') {
+      return this._map[this.constructor.normalize(subject)] != null;
+    } else {
+      return this._getHander(subject) != null;
+    }
+  };
+
+  Hotkeys.prototype.add = function(shortcut, handler) {
+    this._map[this.constructor.normalize(shortcut)] = handler;
+    return this;
+  };
+
+  Hotkeys.prototype.remove = function(shortcut) {
+    delete this._map[this.constructor.normalize(shortcut)];
+    return this;
+  };
+
+  Hotkeys.prototype.destroy = function() {
+    $(this._delegate).off(".simple-hotkeys-" + this.id);
+    this._map = {};
+    return this;
+  };
+
+  return Hotkeys;
+
+})(SimpleModule);
+
+hotkeys = function(opts) {
+  return new Hotkeys(opts);
+};
+
+return hotkeys;
+
+}));
+

+ 172 - 0
addons/simditor/src/js/module.js

@@ -0,0 +1,172 @@
+(function (root, factory) {
+  if (typeof define === 'function' && define.amd) {
+    // AMD. Register as an anonymous module unless amdModuleId is set
+    define('simple-module', ["jquery"], function (a0) {
+      return (root['Module'] = factory(a0));
+    });
+  } else if (typeof exports === 'object') {
+    // Node. Does not work with strict CommonJS, but
+    // only CommonJS-like environments that support module.exports,
+    // like Node.
+    module.exports = factory(require("jquery"));
+  } else {
+    root['SimpleModule'] = factory(jQuery);
+  }
+}(this, function ($) {
+
+var Module,
+  slice = [].slice;
+
+Module = (function() {
+  Module.extend = function(obj) {
+    var key, ref, val;
+    if (!((obj != null) && typeof obj === 'object')) {
+      return;
+    }
+    for (key in obj) {
+      val = obj[key];
+      if (key !== 'included' && key !== 'extended') {
+        this[key] = val;
+      }
+    }
+    return (ref = obj.extended) != null ? ref.call(this) : void 0;
+  };
+
+  Module.include = function(obj) {
+    var key, ref, val;
+    if (!((obj != null) && typeof obj === 'object')) {
+      return;
+    }
+    for (key in obj) {
+      val = obj[key];
+      if (key !== 'included' && key !== 'extended') {
+        this.prototype[key] = val;
+      }
+    }
+    return (ref = obj.included) != null ? ref.call(this) : void 0;
+  };
+
+  Module.connect = function(cls) {
+    if (typeof cls !== 'function') {
+      return;
+    }
+    if (!cls.pluginName) {
+      throw new Error('Module.connect: cannot connect plugin without pluginName');
+      return;
+    }
+    cls.prototype._connected = true;
+    if (!this._connectedClasses) {
+      this._connectedClasses = [];
+    }
+    this._connectedClasses.push(cls);
+    if (cls.pluginName) {
+      return this[cls.pluginName] = cls;
+    }
+  };
+
+  Module.prototype.opts = {};
+
+  function Module(opts) {
+    var base, cls, i, instance, instances, len, name;
+    this.opts = $.extend({}, this.opts, opts);
+    (base = this.constructor)._connectedClasses || (base._connectedClasses = []);
+    instances = (function() {
+      var i, len, ref, results;
+      ref = this.constructor._connectedClasses;
+      results = [];
+      for (i = 0, len = ref.length; i < len; i++) {
+        cls = ref[i];
+        name = cls.pluginName.charAt(0).toLowerCase() + cls.pluginName.slice(1);
+        if (cls.prototype._connected) {
+          cls.prototype._module = this;
+        }
+        results.push(this[name] = new cls());
+      }
+      return results;
+    }).call(this);
+    if (this._connected) {
+      this.opts = $.extend({}, this.opts, this._module.opts);
+    } else {
+      this._init();
+      for (i = 0, len = instances.length; i < len; i++) {
+        instance = instances[i];
+        if (typeof instance._init === "function") {
+          instance._init();
+        }
+      }
+    }
+    this.trigger('initialized');
+  }
+
+  Module.prototype._init = function() {};
+
+  Module.prototype.on = function() {
+    var args, ref;
+    args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+    (ref = $(this)).on.apply(ref, args);
+    return this;
+  };
+
+  Module.prototype.one = function() {
+    var args, ref;
+    args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+    (ref = $(this)).one.apply(ref, args);
+    return this;
+  };
+
+  Module.prototype.off = function() {
+    var args, ref;
+    args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+    (ref = $(this)).off.apply(ref, args);
+    return this;
+  };
+
+  Module.prototype.trigger = function() {
+    var args, ref;
+    args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+    (ref = $(this)).trigger.apply(ref, args);
+    return this;
+  };
+
+  Module.prototype.triggerHandler = function() {
+    var args, ref;
+    args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+    return (ref = $(this)).triggerHandler.apply(ref, args);
+  };
+
+  Module.prototype._t = function() {
+    var args, ref;
+    args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+    return (ref = this.constructor)._t.apply(ref, args);
+  };
+
+  Module._t = function() {
+    var args, key, ref, result;
+    key = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
+    result = ((ref = this.i18n[this.locale]) != null ? ref[key] : void 0) || '';
+    if (!(args.length > 0)) {
+      return result;
+    }
+    result = result.replace(/([^%]|^)%(?:(\d+)\$)?s/g, function(p0, p, position) {
+      if (position) {
+        return p + args[parseInt(position) - 1];
+      } else {
+        return p + args.shift();
+      }
+    });
+    return result.replace(/%%s/g, '%s');
+  };
+
+  Module.i18n = {
+    'zh-CN': {}
+  };
+
+  Module.locale = 'zh-CN';
+
+  return Module;
+
+})();
+
+return Module;
+
+}));

+ 5641 - 0
addons/simditor/src/js/simditor.js

@@ -0,0 +1,5641 @@
+(function (root, factory) {
+  if (typeof define === 'function' && define.amd) {
+    // AMD. Register as an anonymous module unless amdModuleId is set
+    define('simditor', ["jquery","simple-module","simple-hotkeys","simple-uploader"], function ($, SimpleModule, simpleHotkeys, simpleUploader) {
+      return (root['Simditor'] = factory($, SimpleModule, simpleHotkeys, simpleUploader));
+    });
+  } else if (typeof exports === 'object') {
+    // Node. Does not work with strict CommonJS, but
+    // only CommonJS-like environments that support module.exports,
+    // like Node.
+    module.exports = factory(require("jquery"),require("simple-module"),require("simple-hotkeys"),require("simple-uploader"));
+  } else {
+    root['Simditor'] = factory(jQuery,SimpleModule,simple.hotkeys,simple.uploader);
+  }
+}(this, function ($, SimpleModule, simpleHotkeys, simpleUploader) {
+
+var AlignmentButton, BlockquoteButton, BoldButton, Button, Clipboard, CodeButton, CodePopover, ColorButton, FontScaleButton, Formatter, HrButton, ImageButton, ImagePopover, IndentButton, Indentation, InputManager, ItalicButton, Keystroke, LinkButton, LinkPopover, ListButton, OrderListButton, OutdentButton, Popover, Selection, Simditor, StrikethroughButton, TableButton, TitleButton, Toolbar, UnderlineButton, UndoManager, UnorderListButton, Util,
+  extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+  hasProp = {}.hasOwnProperty,
+  indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
+  slice = [].slice;
+
+Selection = (function(superClass) {
+  extend(Selection, superClass);
+
+  function Selection() {
+    return Selection.__super__.constructor.apply(this, arguments);
+  }
+
+  Selection.pluginName = 'Selection';
+
+  Selection.prototype._range = null;
+
+  Selection.prototype._startNodes = null;
+
+  Selection.prototype._endNodes = null;
+
+  Selection.prototype._containerNode = null;
+
+  Selection.prototype._nodes = null;
+
+  Selection.prototype._blockNodes = null;
+
+  Selection.prototype._rootNodes = null;
+
+  Selection.prototype._init = function() {
+    this.editor = this._module;
+    this._selection = document.getSelection();
+    this.editor.on('selectionchanged', (function(_this) {
+      return function(e) {
+        _this.reset();
+        return _this._range = _this._selection.getRangeAt(0);
+      };
+    })(this));
+    this.editor.on('blur', (function(_this) {
+      return function(e) {
+        return _this.reset();
+      };
+    })(this));
+    return this.editor.on('focus', (function(_this) {
+      return function(e) {
+        _this.reset();
+        return _this._range = _this._selection.getRangeAt(0);
+      };
+    })(this));
+  };
+
+  Selection.prototype.reset = function() {
+    this._range = null;
+    this._startNodes = null;
+    this._endNodes = null;
+    this._containerNode = null;
+    this._nodes = null;
+    this._blockNodes = null;
+    return this._rootNodes = null;
+  };
+
+  Selection.prototype.clear = function() {
+    var e;
+    try {
+      this._selection.removeAllRanges();
+    } catch (_error) {
+      e = _error;
+    }
+    return this.reset();
+  };
+
+  Selection.prototype.range = function(range) {
+    var ffOrIE;
+    if (range) {
+      this.clear();
+      this._selection.addRange(range);
+      this._range = range;
+      ffOrIE = this.editor.util.browser.firefox || this.editor.util.browser.msie;
+      if (!this.editor.inputManager.focused && ffOrIE) {
+        this.editor.body.focus();
+      }
+    } else if (!this._range && this.editor.inputManager.focused && this._selection.rangeCount) {
+      this._range = this._selection.getRangeAt(0);
+    }
+    return this._range;
+  };
+
+  Selection.prototype.startNodes = function() {
+    if (this._range) {
+      this._startNodes || (this._startNodes = (function(_this) {
+        return function() {
+          var startNodes;
+          startNodes = $(_this._range.startContainer).parentsUntil(_this.editor.body).get();
+          startNodes.unshift(_this._range.startContainer);
+          return $(startNodes);
+        };
+      })(this)());
+    }
+    return this._startNodes;
+  };
+
+  Selection.prototype.endNodes = function() {
+    var endNodes;
+    if (this._range) {
+      this._endNodes || (this._endNodes = this._range.collapsed ? this.startNodes() : (endNodes = $(this._range.endContainer).parentsUntil(this.editor.body).get(), endNodes.unshift(this._range.endContainer), $(endNodes)));
+    }
+    return this._endNodes;
+  };
+
+  Selection.prototype.containerNode = function() {
+    if (this._range) {
+      this._containerNode || (this._containerNode = $(this._range.commonAncestorContainer));
+    }
+    return this._containerNode;
+  };
+
+  Selection.prototype.nodes = function() {
+    if (this._range) {
+      this._nodes || (this._nodes = (function(_this) {
+        return function() {
+          var nodes;
+          nodes = [];
+          if (_this.startNodes().first().is(_this.endNodes().first())) {
+            nodes = _this.startNodes().get();
+          } else {
+            _this.startNodes().each(function(i, node) {
+              var $endNode, $node, $nodes, endIndex, index, sharedIndex, startIndex;
+              $node = $(node);
+              if (_this.endNodes().index($node) > -1) {
+                return nodes.push(node);
+              } else if ($node.parent().is(_this.editor.body) || (sharedIndex = _this.endNodes().index($node.parent())) > -1) {
+                if (sharedIndex && sharedIndex > -1) {
+                  $endNode = _this.endNodes().eq(sharedIndex - 1);
+                } else {
+                  $endNode = _this.endNodes().last();
+                }
+                $nodes = $node.parent().contents();
+                startIndex = $nodes.index($node);
+                endIndex = $nodes.index($endNode);
+                return $.merge(nodes, $nodes.slice(startIndex, endIndex).get());
+              } else {
+                $nodes = $node.parent().contents();
+                index = $nodes.index($node);
+                return $.merge(nodes, $nodes.slice(index).get());
+              }
+            });
+            _this.endNodes().each(function(i, node) {
+              var $node, $nodes, index;
+              $node = $(node);
+              if ($node.parent().is(_this.editor.body) || _this.startNodes().index($node.parent()) > -1) {
+                nodes.push(node);
+                return false;
+              } else {
+                $nodes = $node.parent().contents();
+                index = $nodes.index($node);
+                return $.merge(nodes, $nodes.slice(0, index + 1));
+              }
+            });
+          }
+          return $($.unique(nodes));
+        };
+      })(this)());
+    }
+    return this._nodes;
+  };
+
+  Selection.prototype.blockNodes = function() {
+    if (!this._range) {
+      return;
+    }
+    this._blockNodes || (this._blockNodes = (function(_this) {
+      return function() {
+        return _this.nodes().filter(function(i, node) {
+          return _this.editor.util.isBlockNode(node);
+        });
+      };
+    })(this)());
+    return this._blockNodes;
+  };
+
+  Selection.prototype.rootNodes = function() {
+    if (!this._range) {
+      return;
+    }
+    this._rootNodes || (this._rootNodes = (function(_this) {
+      return function() {
+        return _this.nodes().filter(function(i, node) {
+          var $parent;
+          $parent = $(node).parent();
+          return $parent.is(_this.editor.body) || $parent.is('blockquote');
+        });
+      };
+    })(this)());
+    return this._rootNodes;
+  };
+
+  Selection.prototype.rangeAtEndOf = function(node, range) {
+    var afterLastNode, beforeLastNode, endNode, endNodeLength, lastNodeIsBr, result;
+    if (range == null) {
+      range = this.range();
+    }
+    if (!(range && range.collapsed)) {
+      return;
+    }
+    node = $(node)[0];
+    endNode = range.endContainer;
+    endNodeLength = this.editor.util.getNodeLength(endNode);
+    beforeLastNode = range.endOffset === endNodeLength - 1;
+    lastNodeIsBr = $(endNode).contents().last().is('br');
+    afterLastNode = range.endOffset === endNodeLength;
+    if (!((beforeLastNode && lastNodeIsBr) || afterLastNode)) {
+      return false;
+    }
+    if (node === endNode) {
+      return true;
+    } else if (!$.contains(node, endNode)) {
+      return false;
+    }
+    result = true;
+    $(endNode).parentsUntil(node).addBack().each(function(i, n) {
+      var $lastChild, beforeLastbr, isLastNode, nodes;
+      nodes = $(n).parent().contents().filter(function() {
+        return !(this !== n && this.nodeType === 3 && !this.nodeValue);
+      });
+      $lastChild = nodes.last();
+      isLastNode = $lastChild.get(0) === n;
+      beforeLastbr = $lastChild.is('br') && $lastChild.prev().get(0) === n;
+      if (!(isLastNode || beforeLastbr)) {
+        result = false;
+        return false;
+      }
+    });
+    return result;
+  };
+
+  Selection.prototype.rangeAtStartOf = function(node, range) {
+    var result, startNode;
+    if (range == null) {
+      range = this.range();
+    }
+    if (!(range && range.collapsed)) {
+      return;
+    }
+    node = $(node)[0];
+    startNode = range.startContainer;
+    if (range.startOffset !== 0) {
+      return false;
+    }
+    if (node === startNode) {
+      return true;
+    } else if (!$.contains(node, startNode)) {
+      return false;
+    }
+    result = true;
+    $(startNode).parentsUntil(node).addBack().each(function(i, n) {
+      var nodes;
+      nodes = $(n).parent().contents().filter(function() {
+        return !(this !== n && this.nodeType === 3 && !this.nodeValue);
+      });
+      if (nodes.first().get(0) !== n) {
+        return result = false;
+      }
+    });
+    return result;
+  };
+
+  Selection.prototype.insertNode = function(node, range) {
+    if (range == null) {
+      range = this.range();
+    }
+    if (!range) {
+      return;
+    }
+    node = $(node)[0];
+    range.insertNode(node);
+    return this.setRangeAfter(node, range);
+  };
+
+  Selection.prototype.setRangeAfter = function(node, range) {
+    if (range == null) {
+      range = this.range();
+    }
+    if (range == null) {
+      return;
+    }
+    node = $(node)[0];
+    range.setEndAfter(node);
+    range.collapse(false);
+    return this.range(range);
+  };
+
+  Selection.prototype.setRangeBefore = function(node, range) {
+    if (range == null) {
+      range = this.range();
+    }
+    if (range == null) {
+      return;
+    }
+    node = $(node)[0];
+    range.setEndBefore(node);
+    range.collapse(false);
+    return this.range(range);
+  };
+
+  Selection.prototype.setRangeAtStartOf = function(node, range) {
+    if (range == null) {
+      range = this.range();
+    }
+    node = $(node).get(0);
+    range.setEnd(node, 0);
+    range.collapse(false);
+    return this.range(range);
+  };
+
+  Selection.prototype.setRangeAtEndOf = function(node, range) {
+    var $lastNode, $node, contents, lastChild, lastChildLength, lastText, nodeLength;
+    if (range == null) {
+      range = this.range();
+    }
+    $node = $(node);
+    node = $node[0];
+    if ($node.is('pre')) {
+      contents = $node.contents();
+      if (contents.length > 0) {
+        lastChild = contents.last();
+        lastText = lastChild.text();
+        lastChildLength = this.editor.util.getNodeLength(lastChild[0]);
+        if (lastText.charAt(lastText.length - 1) === '\n') {
+          range.setEnd(lastChild[0], lastChildLength - 1);
+        } else {
+          range.setEnd(lastChild[0], lastChildLength);
+        }
+      } else {
+        range.setEnd(node, 0);
+      }
+    } else {
+      nodeLength = this.editor.util.getNodeLength(node);
+      if (node.nodeType !== 3 && nodeLength > 0) {
+        $lastNode = $(node).contents().last();
+        if ($lastNode.is('br')) {
+          nodeLength -= 1;
+        } else if ($lastNode[0].nodeType !== 3 && this.editor.util.isEmptyNode($lastNode)) {
+          $lastNode.append(this.editor.util.phBr);
+          node = $lastNode[0];
+          nodeLength = 0;
+        }
+      }
+      range.setEnd(node, nodeLength);
+    }
+    range.collapse(false);
+    return this.range(range);
+  };
+
+  Selection.prototype.deleteRangeContents = function(range) {
+    var atEndOfBody, atStartOfBody, endRange, startRange;
+    if (range == null) {
+      range = this.range();
+    }
+    startRange = range.cloneRange();
+    endRange = range.cloneRange();
+    startRange.collapse(true);
+    endRange.collapse(false);
+    atStartOfBody = this.rangeAtStartOf(this.editor.body, startRange);
+    atEndOfBody = this.rangeAtEndOf(this.editor.body, endRange);
+    if (!range.collapsed && atStartOfBody && atEndOfBody) {
+      this.editor.body.empty();
+      range.setStart(this.editor.body[0], 0);
+      range.collapse(true);
+      this.range(range);
+    } else {
+      range.deleteContents();
+    }
+    return range;
+  };
+
+  Selection.prototype.breakBlockEl = function(el, range) {
+    var $el;
+    if (range == null) {
+      range = this.range();
+    }
+    $el = $(el);
+    if (!range.collapsed) {
+      return $el;
+    }
+    range.setStartBefore($el.get(0));
+    if (range.collapsed) {
+      return $el;
+    }
+    return $el.before(range.extractContents());
+  };
+
+  Selection.prototype.save = function(range) {
+    var endCaret, endRange, startCaret;
+    if (range == null) {
+      range = this.range();
+    }
+    if (this._selectionSaved) {
+      return;
+    }
+    endRange = range.cloneRange();
+    endRange.collapse(false);
+    startCaret = $('<span/>').addClass('simditor-caret-start');
+    endCaret = $('<span/>').addClass('simditor-caret-end');
+    endRange.insertNode(endCaret[0]);
+    range.insertNode(startCaret[0]);
+    this.clear();
+    return this._selectionSaved = true;
+  };
+
+  Selection.prototype.restore = function() {
+    var endCaret, endContainer, endOffset, range, startCaret, startContainer, startOffset;
+    if (!this._selectionSaved) {
+      return false;
+    }
+    startCaret = this.editor.body.find('.simditor-caret-start');
+    endCaret = this.editor.body.find('.simditor-caret-end');
+    if (startCaret.length && endCaret.length) {
+      startContainer = startCaret.parent();
+      startOffset = startContainer.contents().index(startCaret);
+      endContainer = endCaret.parent();
+      endOffset = endContainer.contents().index(endCaret);
+      if (startContainer[0] === endContainer[0]) {
+        endOffset -= 1;
+      }
+      range = document.createRange();
+      range.setStart(startContainer.get(0), startOffset);
+      range.setEnd(endContainer.get(0), endOffset);
+      startCaret.remove();
+      endCaret.remove();
+      this.range(range);
+    } else {
+      startCaret.remove();
+      endCaret.remove();
+    }
+    this._selectionSaved = false;
+    return range;
+  };
+
+  return Selection;
+
+})(SimpleModule);
+
+Formatter = (function(superClass) {
+  extend(Formatter, superClass);
+
+  function Formatter() {
+    return Formatter.__super__.constructor.apply(this, arguments);
+  }
+
+  Formatter.pluginName = 'Formatter';
+
+  Formatter.prototype.opts = {
+    allowedTags: [],
+    allowedAttributes: {},
+    allowedStyles: {}
+  };
+
+  Formatter.prototype._init = function() {
+    this.editor = this._module;
+    this._allowedTags = $.merge(['br', 'span', 'a', 'img', 'b', 'strong', 'i', 'strike', 'u', 'font', 'p', 'ul', 'ol', 'li', 'blockquote', 'pre', 'code', 'h1', 'h2', 'h3', 'h4', 'hr'], this.opts.allowedTags);
+    this._allowedAttributes = $.extend({
+      img: ['src', 'alt', 'width', 'height', 'data-non-image'],
+      a: ['href', 'target'],
+      font: ['color'],
+      code: ['class']
+    }, this.opts.allowedAttributes);
+    this._allowedStyles = $.extend({
+      span: ['color', 'font-size'],
+      b: ['color'],
+      i: ['color'],
+      strong: ['color'],
+      strike: ['color'],
+      u: ['color'],
+      p: ['margin-left', 'text-align'],
+      h1: ['margin-left', 'text-align'],
+      h2: ['margin-left', 'text-align'],
+      h3: ['margin-left', 'text-align'],
+      h4: ['margin-left', 'text-align']
+    }, this.opts.allowedStyles);
+    return this.editor.body.on('click', 'a', function(e) {
+      return false;
+    });
+  };
+
+  Formatter.prototype.decorate = function($el) {
+    if ($el == null) {
+      $el = this.editor.body;
+    }
+    this.editor.trigger('decorate', [$el]);
+    return $el;
+  };
+
+  Formatter.prototype.undecorate = function($el) {
+    if ($el == null) {
+      $el = this.editor.body.clone();
+    }
+    this.editor.trigger('undecorate', [$el]);
+    return $el;
+  };
+
+  Formatter.prototype.autolink = function($el) {
+    var $link, $node, findLinkNode, k, lastIndex, len, linkNodes, match, re, replaceEls, subStr, text, uri;
+    if ($el == null) {
+      $el = this.editor.body;
+    }
+    linkNodes = [];
+    findLinkNode = function($parentNode) {
+      return $parentNode.contents().each(function(i, node) {
+        var $node, text;
+        $node = $(node);
+        if ($node.is('a') || $node.closest('a, pre', $el).length) {
+          return;
+        }
+        if (!$node.is('iframe') && $node.contents().length) {
+          return findLinkNode($node);
+        } else if ((text = $node.text()) && /https?:\/\/|www\./ig.test(text)) {
+          return linkNodes.push($node);
+        }
+      });
+    };
+    findLinkNode($el);
+    re = /(https?:\/\/|www\.)[\w\-\.\?&=\/#%:,@\!\+]+/ig;
+    for (k = 0, len = linkNodes.length; k < len; k++) {
+      $node = linkNodes[k];
+      text = $node.text();
+      replaceEls = [];
+      match = null;
+      lastIndex = 0;
+      while ((match = re.exec(text)) !== null) {
+        subStr = text.substring(lastIndex, match.index);
+        replaceEls.push(document.createTextNode(subStr));
+        lastIndex = re.lastIndex;
+        uri = /^(http(s)?:\/\/|\/)/.test(match[0]) ? match[0] : 'http://' + match[0];
+        $link = $("<a href=\"" + uri + "\" rel=\"nofollow\"></a>").text(match[0]);
+        replaceEls.push($link[0]);
+      }
+      replaceEls.push(document.createTextNode(text.substring(lastIndex)));
+      $node.replaceWith($(replaceEls));
+    }
+    return $el;
+  };
+
+  Formatter.prototype.format = function($el) {
+    var $node, blockNode, k, l, len, len1, n, node, ref, ref1;
+    if ($el == null) {
+      $el = this.editor.body;
+    }
+    if ($el.is(':empty')) {
+      $el.append('<p>' + this.editor.util.phBr + '</p>');
+      return $el;
+    }
+    ref = $el.contents();
+    for (k = 0, len = ref.length; k < len; k++) {
+      n = ref[k];
+      this.cleanNode(n, true);
+    }
+    ref1 = $el.contents();
+    for (l = 0, len1 = ref1.length; l < len1; l++) {
+      node = ref1[l];
+      $node = $(node);
+      if ($node.is('br')) {
+        if (typeof blockNode !== "undefined" && blockNode !== null) {
+          blockNode = null;
+        }
+        $node.remove();
+      } else if (this.editor.util.isBlockNode(node)) {
+        if ($node.is('li')) {
+          if (blockNode && blockNode.is('ul, ol')) {
+            blockNode.append(node);
+          } else {
+            blockNode = $('<ul/>').insertBefore(node);
+            blockNode.append(node);
+          }
+        } else {
+          blockNode = null;
+        }
+      } else {
+        if (!blockNode || blockNode.is('ul, ol')) {
+          blockNode = $('<p/>').insertBefore(node);
+        }
+        blockNode.append(node);
+        if (this.editor.util.isEmptyNode(blockNode)) {
+          blockNode.append(this.editor.util.phBr);
+        }
+      }
+    }
+    return $el;
+  };
+
+  Formatter.prototype.cleanNode = function(node, recursive) {
+    var $blockEls, $childImg, $node, $p, $td, allowedAttributes, attr, contents, isDecoration, k, l, len, len1, n, ref, ref1, text, textNode;
+    $node = $(node);
+    if (!($node.length > 0)) {
+      return;
+    }
+    if ($node[0].nodeType === 3) {
+      text = $node.text().replace(/(\r\n|\n|\r)/gm, '');
+      if (text) {
+        textNode = document.createTextNode(text);
+        $node.replaceWith(textNode);
+      } else {
+        $node.remove();
+      }
+      return;
+    }
+    contents = $node.is('iframe') ? null : $node.contents();
+    isDecoration = this.editor.util.isDecoratedNode($node);
+    if ($node.is(this._allowedTags.join(',')) || isDecoration) {
+      if ($node.is('a') && ($childImg = $node.find('img')).length > 0) {
+        $node.replaceWith($childImg);
+        $node = $childImg;
+        contents = null;
+      }
+      if ($node.is('td') && ($blockEls = $node.find(this.editor.util.blockNodes.join(','))).length > 0) {
+        $blockEls.each((function(_this) {
+          return function(i, blockEl) {
+            return $(blockEl).contents().unwrap();
+          };
+        })(this));
+        contents = $node.contents();
+      }
+      if ($node.is('img') && $node.hasClass('uploading')) {
+        $node.remove();
+      }
+      if (!isDecoration) {
+        allowedAttributes = this._allowedAttributes[$node[0].tagName.toLowerCase()];
+        ref = $.makeArray($node[0].attributes);
+        for (k = 0, len = ref.length; k < len; k++) {
+          attr = ref[k];
+          if (attr.name === 'style') {
+            continue;
+          }
+          if (!((allowedAttributes != null) && (ref1 = attr.name, indexOf.call(allowedAttributes, ref1) >= 0))) {
+            $node.removeAttr(attr.name);
+          }
+        }
+        this._cleanNodeStyles($node);
+        if ($node.is('span') && $node[0].attributes.length === 0) {
+          $node.contents().first().unwrap();
+        }
+      }
+    } else if ($node[0].nodeType === 1 && !$node.is(':empty')) {
+      if ($node.is('div, article, dl, header, footer, tr')) {
+        $node.append('<br/>');
+        contents.first().unwrap();
+      } else if ($node.is('table')) {
+        $p = $('<p/>');
+        $node.find('tr').each(function(i, tr) {
+          return $p.append($(tr).text() + '<br/>');
+        });
+        $node.replaceWith($p);
+        contents = null;
+      } else if ($node.is('thead, tfoot')) {
+        $node.remove();
+        contents = null;
+      } else if ($node.is('th')) {
+        $td = $('<td/>').append($node.contents());
+        $node.replaceWith($td);
+      } else {
+        contents.first().unwrap();
+      }
+    } else {
+      $node.remove();
+      contents = null;
+    }
+    if (recursive && (contents != null) && !$node.is('pre')) {
+      for (l = 0, len1 = contents.length; l < len1; l++) {
+        n = contents[l];
+        this.cleanNode(n, true);
+      }
+    }
+    return null;
+  };
+
+  Formatter.prototype._cleanNodeStyles = function($node) {
+    var allowedStyles, k, len, pair, ref, ref1, style, styleStr, styles;
+    styleStr = $node.attr('style');
+    if (!styleStr) {
+      return;
+    }
+    $node.removeAttr('style');
+    allowedStyles = this._allowedStyles[$node[0].tagName.toLowerCase()];
+    if (!(allowedStyles && allowedStyles.length > 0)) {
+      return $node;
+    }
+    styles = {};
+    ref = styleStr.split(';');
+    for (k = 0, len = ref.length; k < len; k++) {
+      style = ref[k];
+      style = $.trim(style);
+      pair = style.split(':');
+      if (pair.length !== 2) {
+        continue;
+      }
+      if (pair[0] === 'font-size' && pair[1].indexOf('px') > 0) {
+        if (parseInt(pair[1], 10) < 12) {
+          continue;
+        }
+      }
+      if (ref1 = pair[0], indexOf.call(allowedStyles, ref1) >= 0) {
+        styles[$.trim(pair[0])] = $.trim(pair[1]);
+      }
+    }
+    if (Object.keys(styles).length > 0) {
+      $node.css(styles);
+    }
+    return $node;
+  };
+
+  Formatter.prototype.clearHtml = function(html, lineBreak) {
+    var container, contents, result;
+    if (lineBreak == null) {
+      lineBreak = true;
+    }
+    container = $('<div/>').append(html);
+    contents = container.contents();
+    result = '';
+    contents.each((function(_this) {
+      return function(i, node) {
+        var $node, children;
+        if (node.nodeType === 3) {
+          return result += node.nodeValue;
+        } else if (node.nodeType === 1) {
+          $node = $(node);
+          children = $node.is('iframe') ? null : $node.contents();
+          if (children && children.length > 0) {
+            result += _this.clearHtml(children);
+          }
+          if (lineBreak && i < contents.length - 1 && $node.is('br, p, div, li,tr, pre, address, artticle, aside, dl, figcaption, footer, h1, h2,h3, h4, header')) {
+            return result += '\n';
+          }
+        }
+      };
+    })(this));
+    return result;
+  };
+
+  Formatter.prototype.beautify = function($contents) {
+    var uselessP;
+    uselessP = function($el) {
+      return !!($el.is('p') && !$el.text() && $el.children(':not(br)').length < 1);
+    };
+    return $contents.each(function(i, el) {
+      var $el, invalid;
+      $el = $(el);
+      invalid = $el.is(':not(img, br, col, td, hr, [class^="simditor-"]):empty');
+      if (invalid || uselessP($el)) {
+        $el.remove();
+      }
+      return $el.find(':not(img, br, col, td, hr, [class^="simditor-"]):empty').remove();
+    });
+  };
+
+  return Formatter;
+
+})(SimpleModule);
+
+InputManager = (function(superClass) {
+  extend(InputManager, superClass);
+
+  function InputManager() {
+    return InputManager.__super__.constructor.apply(this, arguments);
+  }
+
+  InputManager.pluginName = 'InputManager';
+
+  InputManager.prototype._modifierKeys = [16, 17, 18, 91, 93, 224];
+
+  InputManager.prototype._arrowKeys = [37, 38, 39, 40];
+
+  InputManager.prototype._init = function() {
+    var selectAllKey, submitKey;
+    this.editor = this._module;
+    this.throttledValueChanged = this.editor.util.throttle((function(_this) {
+      return function(params) {
+        return setTimeout(function() {
+          return _this.editor.trigger('valuechanged', params);
+        }, 10);
+      };
+    })(this), 300);
+    this.throttledSelectionChanged = this.editor.util.throttle((function(_this) {
+      return function() {
+        return _this.editor.trigger('selectionchanged');
+      };
+    })(this), 50);
+    $(document).on('selectionchange.simditor' + this.editor.id, (function(_this) {
+      return function(e) {
+        var triggerEvent;
+        if (!(_this.focused && !_this.editor.clipboard.pasting)) {
+          return;
+        }
+        triggerEvent = function() {
+          if (_this._selectionTimer) {
+            clearTimeout(_this._selectionTimer);
+            _this._selectionTimer = null;
+          }
+          if (_this.editor.selection._selection.rangeCount > 0) {
+            return _this.throttledSelectionChanged();
+          } else {
+            return _this._selectionTimer = setTimeout(function() {
+              _this._selectionTimer = null;
+              if (_this.focused) {
+                return triggerEvent();
+              }
+            }, 10);
+          }
+        };
+        return triggerEvent();
+      };
+    })(this));
+    this.editor.on('valuechanged', (function(_this) {
+      return function() {
+        var $rootBlocks;
+        _this.lastCaretPosition = null;
+        $rootBlocks = _this.editor.body.children().filter(function(i, node) {
+          return _this.editor.util.isBlockNode(node);
+        });
+        if (_this.focused && $rootBlocks.length === 0) {
+          _this.editor.selection.save();
+          _this.editor.formatter.format();
+          _this.editor.selection.restore();
+        }
+        _this.editor.body.find('hr, pre, .simditor-table').each(function(i, el) {
+          var $el, formatted;
+          $el = $(el);
+          if ($el.parent().is('blockquote') || $el.parent()[0] === _this.editor.body[0]) {
+            formatted = false;
+            if ($el.next().length === 0) {
+              $('<p/>').append(_this.editor.util.phBr).insertAfter($el);
+              formatted = true;
+            }
+            if ($el.prev().length === 0) {
+              $('<p/>').append(_this.editor.util.phBr).insertBefore($el);
+              formatted = true;
+            }
+            if (formatted) {
+              return _this.throttledValueChanged();
+            }
+          }
+        });
+        _this.editor.body.find('pre:empty').append(_this.editor.util.phBr);
+        if (!_this.editor.util.support.onselectionchange && _this.focused) {
+          return _this.throttledSelectionChanged();
+        }
+      };
+    })(this));
+    this.editor.body.on('keydown', $.proxy(this._onKeyDown, this)).on('keypress', $.proxy(this._onKeyPress, this)).on('keyup', $.proxy(this._onKeyUp, this)).on('mouseup', $.proxy(this._onMouseUp, this)).on('focus', $.proxy(this._onFocus, this)).on('blur', $.proxy(this._onBlur, this)).on('drop', $.proxy(this._onDrop, this)).on('input', $.proxy(this._onInput, this));
+    if (this.editor.util.browser.firefox) {
+      this.editor.hotkeys.add('cmd+left', (function(_this) {
+        return function(e) {
+          e.preventDefault();
+          _this.editor.selection._selection.modify('move', 'backward', 'lineboundary');
+          return false;
+        };
+      })(this));
+      this.editor.hotkeys.add('cmd+right', (function(_this) {
+        return function(e) {
+          e.preventDefault();
+          _this.editor.selection._selection.modify('move', 'forward', 'lineboundary');
+          return false;
+        };
+      })(this));
+      selectAllKey = this.editor.util.os.mac ? 'cmd+a' : 'ctrl+a';
+      this.editor.hotkeys.add(selectAllKey, (function(_this) {
+        return function(e) {
+          var $children, firstBlock, lastBlock, range;
+          $children = _this.editor.body.children();
+          if (!($children.length > 0)) {
+            return;
+          }
+          firstBlock = $children.first().get(0);
+          lastBlock = $children.last().get(0);
+          range = document.createRange();
+          range.setStart(firstBlock, 0);
+          range.setEnd(lastBlock, _this.editor.util.getNodeLength(lastBlock));
+          _this.editor.selection.range(range);
+          return false;
+        };
+      })(this));
+    }
+    submitKey = this.editor.util.os.mac ? 'cmd+enter' : 'ctrl+enter';
+    return this.editor.hotkeys.add(submitKey, (function(_this) {
+      return function(e) {
+        _this.editor.el.closest('form').find('button:submit').click();
+        return false;
+      };
+    })(this));
+  };
+
+  InputManager.prototype._onFocus = function(e) {
+    if (this.editor.clipboard.pasting) {
+      return;
+    }
+    this.editor.el.addClass('focus').removeClass('error');
+    this.focused = true;
+    return setTimeout((function(_this) {
+      return function() {
+        var $blockEl, range;
+        range = _this.editor.selection._selection.getRangeAt(0);
+        if (range.startContainer === _this.editor.body[0]) {
+          if (_this.lastCaretPosition) {
+            _this.editor.undoManager.caretPosition(_this.lastCaretPosition);
+          } else {
+            $blockEl = _this.editor.body.children().first();
+            range = document.createRange();
+            _this.editor.selection.setRangeAtStartOf($blockEl, range);
+          }
+        }
+        _this.lastCaretPosition = null;
+        _this.editor.triggerHandler('focus');
+        if (!_this.editor.util.support.onselectionchange) {
+          return _this.throttledSelectionChanged();
+        }
+      };
+    })(this), 0);
+  };
+
+  InputManager.prototype._onBlur = function(e) {
+    var ref;
+    if (this.editor.clipboard.pasting) {
+      return;
+    }
+    this.editor.el.removeClass('focus');
+    this.editor.sync();
+    this.focused = false;
+    this.lastCaretPosition = (ref = this.editor.undoManager.currentState()) != null ? ref.caret : void 0;
+    return this.editor.triggerHandler('blur');
+  };
+
+  InputManager.prototype._onMouseUp = function(e) {
+    if (!this.editor.util.support.onselectionchange) {
+      return this.throttledSelectionChanged();
+    }
+  };
+
+  InputManager.prototype._onKeyDown = function(e) {
+    var ref, ref1;
+    if (this.editor.triggerHandler(e) === false) {
+      return false;
+    }
+    if (this.editor.hotkeys.respondTo(e)) {
+      return;
+    }
+    if (this.editor.keystroke.respondTo(e)) {
+      this.throttledValueChanged();
+      return false;
+    }
+    if ((ref = e.which, indexOf.call(this._modifierKeys, ref) >= 0) || (ref1 = e.which, indexOf.call(this._arrowKeys, ref1) >= 0)) {
+      return;
+    }
+    if (this.editor.util.metaKey(e) && e.which === 86) {
+      return;
+    }
+    if (!this.editor.util.support.oninput) {
+      this.throttledValueChanged(['typing']);
+    }
+    return null;
+  };
+
+  InputManager.prototype._onKeyPress = function(e) {
+    if (this.editor.triggerHandler(e) === false) {
+      return false;
+    }
+  };
+
+  InputManager.prototype._onKeyUp = function(e) {
+    var p, ref;
+    if (this.editor.triggerHandler(e) === false) {
+      return false;
+    }
+    if (!this.editor.util.support.onselectionchange && (ref = e.which, indexOf.call(this._arrowKeys, ref) >= 0)) {
+      this.throttledValueChanged();
+      return;
+    }
+    if ((e.which === 8 || e.which === 46) && this.editor.util.isEmptyNode(this.editor.body)) {
+      this.editor.body.empty();
+      p = $('<p/>').append(this.editor.util.phBr).appendTo(this.editor.body);
+      this.editor.selection.setRangeAtStartOf(p);
+    }
+  };
+
+  InputManager.prototype._onDrop = function(e) {
+    if (this.editor.triggerHandler(e) === false) {
+      return false;
+    }
+    return this.throttledValueChanged();
+  };
+
+  InputManager.prototype._onInput = function(e) {
+    return this.throttledValueChanged(['oninput']);
+  };
+
+  return InputManager;
+
+})(SimpleModule);
+
+Keystroke = (function(superClass) {
+  extend(Keystroke, superClass);
+
+  function Keystroke() {
+    return Keystroke.__super__.constructor.apply(this, arguments);
+  }
+
+  Keystroke.pluginName = 'Keystroke';
+
+  Keystroke.prototype._init = function() {
+    this.editor = this._module;
+    this._keystrokeHandlers = {};
+    return this._initKeystrokeHandlers();
+  };
+
+  Keystroke.prototype.add = function(key, node, handler) {
+    key = key.toLowerCase();
+    key = this.editor.hotkeys.constructor.aliases[key] || key;
+    if (!this._keystrokeHandlers[key]) {
+      this._keystrokeHandlers[key] = {};
+    }
+    return this._keystrokeHandlers[key][node] = handler;
+  };
+
+  Keystroke.prototype.respondTo = function(e) {
+    var base, key, ref, result;
+    key = (ref = this.editor.hotkeys.constructor.keyNameMap[e.which]) != null ? ref.toLowerCase() : void 0;
+    if (!key) {
+      return;
+    }
+    if (key in this._keystrokeHandlers) {
+      result = typeof (base = this._keystrokeHandlers[key])['*'] === "function" ? base['*'](e) : void 0;
+      if (!result) {
+        this.editor.selection.startNodes().each((function(_this) {
+          return function(i, node) {
+            var handler, ref1;
+            if (node.nodeType !== Node.ELEMENT_NODE) {
+              return;
+            }
+            handler = (ref1 = _this._keystrokeHandlers[key]) != null ? ref1[node.tagName.toLowerCase()] : void 0;
+            result = typeof handler === "function" ? handler(e, $(node)) : void 0;
+            if (result === true || result === false) {
+              return false;
+            }
+          };
+        })(this));
+      }
+      if (result) {
+        return true;
+      }
+    }
+  };
+
+  Keystroke.prototype._initKeystrokeHandlers = function() {
+    var titleEnterHandler;
+    if (this.editor.util.browser.safari) {
+      this.add('enter', '*', (function(_this) {
+        return function(e) {
+          var $blockEl, $br;
+          if (!e.shiftKey) {
+            return;
+          }
+          $blockEl = _this.editor.selection.blockNodes().last();
+          if ($blockEl.is('pre')) {
+            return;
+          }
+          $br = $('<br/>');
+          if (_this.editor.selection.rangeAtEndOf($blockEl)) {
+            _this.editor.selection.insertNode($br);
+            _this.editor.selection.insertNode($('<br/>'));
+            _this.editor.selection.setRangeBefore($br);
+          } else {
+            _this.editor.selection.insertNode($br);
+          }
+          return true;
+        };
+      })(this));
+    }
+    if (this.editor.util.browser.webkit || this.editor.util.browser.msie) {
+      titleEnterHandler = (function(_this) {
+        return function(e, $node) {
+          var $p;
+          if (!_this.editor.selection.rangeAtEndOf($node)) {
+            return;
+          }
+          $p = $('<p/>').append(_this.editor.util.phBr).insertAfter($node);
+          _this.editor.selection.setRangeAtStartOf($p);
+          return true;
+        };
+      })(this);
+      this.add('enter', 'h1', titleEnterHandler);
+      this.add('enter', 'h2', titleEnterHandler);
+      this.add('enter', 'h3', titleEnterHandler);
+      this.add('enter', 'h4', titleEnterHandler);
+      this.add('enter', 'h5', titleEnterHandler);
+      this.add('enter', 'h6', titleEnterHandler);
+    }
+    this.add('backspace', '*', (function(_this) {
+      return function(e) {
+        var $blockEl, $prevBlockEl, $rootBlock, isWebkit;
+        $rootBlock = _this.editor.selection.rootNodes().first();
+        $prevBlockEl = $rootBlock.prev();
+        if ($prevBlockEl.is('hr') && _this.editor.selection.rangeAtStartOf($rootBlock)) {
+          _this.editor.selection.save();
+          $prevBlockEl.remove();
+          _this.editor.selection.restore();
+          return true;
+        }
+        $blockEl = _this.editor.selection.blockNodes().last();
+        isWebkit = _this.editor.util.browser.webkit;
+        if (isWebkit && _this.editor.selection.rangeAtStartOf($blockEl)) {
+          _this.editor.selection.save();
+          _this.editor.formatter.cleanNode($blockEl, true);
+          _this.editor.selection.restore();
+          return null;
+        }
+      };
+    })(this));
+    this.add('enter', 'li', (function(_this) {
+      return function(e, $node) {
+        var $cloneNode, listEl, newBlockEl, newListEl;
+        $cloneNode = $node.clone();
+        $cloneNode.find('ul, ol').remove();
+        if (!(_this.editor.util.isEmptyNode($cloneNode) && $node.is(_this.editor.selection.blockNodes().last()))) {
+          return;
+        }
+        listEl = $node.parent();
+        if ($node.next('li').length > 0) {
+          if (!_this.editor.util.isEmptyNode($node)) {
+            return;
+          }
+          if (listEl.parent('li').length > 0) {
+            newBlockEl = $('<li/>').append(_this.editor.util.phBr).insertAfter(listEl.parent('li'));
+            newListEl = $('<' + listEl[0].tagName + '/>').append($node.nextAll('li'));
+            newBlockEl.append(newListEl);
+          } else {
+            newBlockEl = $('<p/>').append(_this.editor.util.phBr).insertAfter(listEl);
+            newListEl = $('<' + listEl[0].tagName + '/>').append($node.nextAll('li'));
+            newBlockEl.after(newListEl);
+          }
+        } else {
+          if (listEl.parent('li').length > 0) {
+            newBlockEl = $('<li/>').insertAfter(listEl.parent('li'));
+            if ($node.contents().length > 0) {
+              newBlockEl.append($node.contents());
+            } else {
+              newBlockEl.append(_this.editor.util.phBr);
+            }
+          } else {
+            newBlockEl = $('<p/>').append(_this.editor.util.phBr).insertAfter(listEl);
+            if ($node.children('ul, ol').length > 0) {
+              newBlockEl.after($node.children('ul, ol'));
+            }
+          }
+        }
+        if ($node.prev('li').length) {
+          $node.remove();
+        } else {
+          listEl.remove();
+        }
+        _this.editor.selection.setRangeAtStartOf(newBlockEl);
+        return true;
+      };
+    })(this));
+    this.add('enter', 'pre', (function(_this) {
+      return function(e, $node) {
+        var $p, breakNode, range;
+        e.preventDefault();
+        if (e.shiftKey) {
+          $p = $('<p/>').append(_this.editor.util.phBr).insertAfter($node);
+          _this.editor.selection.setRangeAtStartOf($p);
+          return true;
+        }
+        range = _this.editor.selection.range();
+        breakNode = null;
+        range.deleteContents();
+        if (!_this.editor.util.browser.msie && _this.editor.selection.rangeAtEndOf($node)) {
+          breakNode = document.createTextNode('\n\n');
+          range.insertNode(breakNode);
+          range.setEnd(breakNode, 1);
+        } else {
+          breakNode = document.createTextNode('\n');
+          range.insertNode(breakNode);
+          range.setStartAfter(breakNode);
+        }
+        range.collapse(false);
+        _this.editor.selection.range(range);
+        return true;
+      };
+    })(this));
+    this.add('enter', 'blockquote', (function(_this) {
+      return function(e, $node) {
+        var $closestBlock, range;
+        $closestBlock = _this.editor.selection.blockNodes().last();
+        if (!($closestBlock.is('p') && !$closestBlock.next().length && _this.editor.util.isEmptyNode($closestBlock))) {
+          return;
+        }
+        $node.after($closestBlock);
+        range = document.createRange();
+        _this.editor.selection.setRangeAtStartOf($closestBlock, range);
+        return true;
+      };
+    })(this));
+    this.add('backspace', 'li', (function(_this) {
+      return function(e, $node) {
+        var $br, $childList, $newLi, $prevChildList, $prevNode, $textNode, isFF, range, text;
+        $childList = $node.children('ul, ol');
+        $prevNode = $node.prev('li');
+        if (!($childList.length > 0 && $prevNode.length > 0)) {
+          return false;
+        }
+        text = '';
+        $textNode = null;
+        $node.contents().each(function(i, n) {
+          if (n.nodeType === 1 && /UL|OL/.test(n.nodeName)) {
+            return false;
+          }
+          if (n.nodeType === 1 && /BR/.test(n.nodeName)) {
+            return;
+          }
+          if (n.nodeType === 3 && n.nodeValue) {
+            text += n.nodeValue;
+          } else if (n.nodeType === 1) {
+            text += $(n).text();
+          }
+          return $textNode = $(n);
+        });
+        isFF = _this.editor.util.browser.firefox && !$textNode.next('br').length;
+        if ($textNode && text.length === 1 && isFF) {
+          $br = $(_this.editor.util.phBr).insertAfter($textNode);
+          $textNode.remove();
+          _this.editor.selection.setRangeBefore($br);
+          return true;
+        } else if (text.length > 0) {
+          return false;
+        }
+        range = document.createRange();
+        $prevChildList = $prevNode.children('ul, ol');
+        if ($prevChildList.length > 0) {
+          $newLi = $('<li/>').append(_this.editor.util.phBr).appendTo($prevChildList);
+          $prevChildList.append($childList.children('li'));
+          $node.remove();
+          _this.editor.selection.setRangeAtEndOf($newLi, range);
+        } else {
+          _this.editor.selection.setRangeAtEndOf($prevNode, range);
+          $prevNode.append($childList);
+          $node.remove();
+          _this.editor.selection.range(range);
+        }
+        return true;
+      };
+    })(this));
+    this.add('backspace', 'pre', (function(_this) {
+      return function(e, $node) {
+        var $newNode, codeStr, range;
+        if (!_this.editor.selection.rangeAtStartOf($node)) {
+          return;
+        }
+        codeStr = $node.html().replace('\n', '<br/>') || _this.editor.util.phBr;
+        $newNode = $('<p/>').append(codeStr).insertAfter($node);
+        $node.remove();
+        range = document.createRange();
+        _this.editor.selection.setRangeAtStartOf($newNode, range);
+        return true;
+      };
+    })(this));
+    return this.add('backspace', 'blockquote', (function(_this) {
+      return function(e, $node) {
+        var $firstChild, range;
+        if (!_this.editor.selection.rangeAtStartOf($node)) {
+          return;
+        }
+        $firstChild = $node.children().first().unwrap();
+        range = document.createRange();
+        _this.editor.selection.setRangeAtStartOf($firstChild, range);
+        return true;
+      };
+    })(this));
+  };
+
+  return Keystroke;
+
+})(SimpleModule);
+
+UndoManager = (function(superClass) {
+  extend(UndoManager, superClass);
+
+  function UndoManager() {
+    return UndoManager.__super__.constructor.apply(this, arguments);
+  }
+
+  UndoManager.pluginName = 'UndoManager';
+
+  UndoManager.prototype._index = -1;
+
+  UndoManager.prototype._capacity = 20;
+
+  UndoManager.prototype._startPosition = null;
+
+  UndoManager.prototype._endPosition = null;
+
+  UndoManager.prototype._init = function() {
+    var redoShortcut, undoShortcut;
+    this.editor = this._module;
+    this._stack = [];
+    if (this.editor.util.os.mac) {
+      undoShortcut = 'cmd+z';
+      redoShortcut = 'shift+cmd+z';
+    } else if (this.editor.util.os.win) {
+      undoShortcut = 'ctrl+z';
+      redoShortcut = 'ctrl+y';
+    } else {
+      undoShortcut = 'ctrl+z';
+      redoShortcut = 'shift+ctrl+z';
+    }
+    this.editor.hotkeys.add(undoShortcut, (function(_this) {
+      return function(e) {
+        e.preventDefault();
+        _this.undo();
+        return false;
+      };
+    })(this));
+    this.editor.hotkeys.add(redoShortcut, (function(_this) {
+      return function(e) {
+        e.preventDefault();
+        _this.redo();
+        return false;
+      };
+    })(this));
+    this.throttledPushState = this.editor.util.throttle((function(_this) {
+      return function() {
+        return _this._pushUndoState();
+      };
+    })(this), 2000);
+    this.editor.on('valuechanged', (function(_this) {
+      return function(e, src) {
+        if (src === 'undo' || src === 'redo') {
+          return;
+        }
+        return _this.throttledPushState();
+      };
+    })(this));
+    this.editor.on('selectionchanged', (function(_this) {
+      return function(e) {
+        _this.resetCaretPosition();
+        return _this.update();
+      };
+    })(this));
+    this.editor.on('focus', (function(_this) {
+      return function(e) {
+        if (_this._stack.length === 0) {
+          return _this._pushUndoState();
+        }
+      };
+    })(this));
+    return this.editor.on('blur', (function(_this) {
+      return function(e) {
+        return _this.resetCaretPosition();
+      };
+    })(this));
+  };
+
+  UndoManager.prototype.resetCaretPosition = function() {
+    this._startPosition = null;
+    return this._endPosition = null;
+  };
+
+  UndoManager.prototype.startPosition = function() {
+    if (this.editor.selection._range) {
+      this._startPosition || (this._startPosition = this._getPosition('start'));
+    }
+    return this._startPosition;
+  };
+
+  UndoManager.prototype.endPosition = function() {
+    if (this.editor.selection._range) {
+      this._endPosition || (this._endPosition = (function(_this) {
+        return function() {
+          var range;
+          range = _this.editor.selection.range();
+          if (range.collapsed) {
+            return _this._startPosition;
+          }
+          return _this._getPosition('end');
+        };
+      })(this)());
+    }
+    return this._endPosition;
+  };
+
+  UndoManager.prototype._pushUndoState = function() {
+    var caret;
+    if (this.editor.triggerHandler('pushundostate') === false) {
+      return;
+    }
+    caret = this.caretPosition();
+    if (!caret.start) {
+      return;
+    }
+    this._index += 1;
+    this._stack.length = this._index;
+    this._stack.push({
+      html: this.editor.body.html(),
+      caret: this.caretPosition()
+    });
+    if (this._stack.length > this._capacity) {
+      this._stack.shift();
+      return this._index -= 1;
+    }
+  };
+
+  UndoManager.prototype.currentState = function() {
+    if (this._stack.length && this._index > -1) {
+      return this._stack[this._index];
+    } else {
+      return null;
+    }
+  };
+
+  UndoManager.prototype.undo = function() {
+    var state;
+    if (this._index < 1 || this._stack.length < 2) {
+      return;
+    }
+    this.editor.hidePopover();
+    this._index -= 1;
+    state = this._stack[this._index];
+    this.editor.body.get(0).innerHTML = state.html;
+    this.caretPosition(state.caret);
+    this.editor.body.find('.selected').removeClass('selected');
+    this.editor.sync();
+    return this.editor.trigger('valuechanged', ['undo']);
+  };
+
+  UndoManager.prototype.redo = function() {
+    var state;
+    if (this._index < 0 || this._stack.length < this._index + 2) {
+      return;
+    }
+    this.editor.hidePopover();
+    this._index += 1;
+    state = this._stack[this._index];
+    this.editor.body.get(0).innerHTML = state.html;
+    this.caretPosition(state.caret);
+    this.editor.body.find('.selected').removeClass('selected');
+    this.editor.sync();
+    return this.editor.trigger('valuechanged', ['redo']);
+  };
+
+  UndoManager.prototype.update = function() {
+    var currentState;
+    currentState = this.currentState();
+    if (!currentState) {
+      return;
+    }
+    currentState.html = this.editor.body.html();
+    return currentState.caret = this.caretPosition();
+  };
+
+  UndoManager.prototype._getNodeOffset = function(node, index) {
+    var $parent, merging, offset;
+    if ($.isNumeric(index)) {
+      $parent = $(node);
+    } else {
+      $parent = $(node).parent();
+    }
+    offset = 0;
+    merging = false;
+    $parent.contents().each(function(i, child) {
+      if (node === child || (index === i && i === 0)) {
+        return false;
+      }
+      if (child.nodeType === Node.TEXT_NODE) {
+        if (!merging && child.nodeValue.length > 0) {
+          offset += 1;
+          merging = true;
+        }
+      } else {
+        offset += 1;
+        merging = false;
+      }
+      if (index - 1 === i) {
+        return false;
+      }
+      return null;
+    });
+    return offset;
+  };
+
+  UndoManager.prototype._getPosition = function(type) {
+    var $nodes, node, nodes, offset, position, prevNode, range;
+    if (type == null) {
+      type = 'start';
+    }
+    range = this.editor.selection.range();
+    offset = range[type + "Offset"];
+    $nodes = this.editor.selection[type + "Nodes"]();
+    node = $nodes.first()[0];
+    if (node.nodeType === Node.TEXT_NODE) {
+      prevNode = node.previousSibling;
+      while (prevNode && prevNode.nodeType === Node.TEXT_NODE) {
+        node = prevNode;
+        offset += this.editor.util.getNodeLength(prevNode);
+        prevNode = prevNode.previousSibling;
+      }
+      nodes = $nodes.get();
+      nodes[0] = node;
+      $nodes = $(nodes);
+    } else {
+      offset = this._getNodeOffset(node, offset);
+    }
+    position = [offset];
+    $nodes.each((function(_this) {
+      return function(i, node) {
+        return position.unshift(_this._getNodeOffset(node));
+      };
+    })(this));
+    return position;
+  };
+
+  UndoManager.prototype._getNodeByPosition = function(position) {
+    var child, childNodes, i, k, len, node, offset, ref;
+    node = this.editor.body[0];
+    ref = position.slice(0, position.length - 1);
+    for (i = k = 0, len = ref.length; k < len; i = ++k) {
+      offset = ref[i];
+      childNodes = node.childNodes;
+      if (offset > childNodes.length - 1) {
+        if (i === position.length - 2 && $(node).is('pre:empty')) {
+          child = document.createTextNode('');
+          node.appendChild(child);
+          childNodes = node.childNodes;
+        } else {
+          node = null;
+          break;
+        }
+      }
+      node = childNodes[offset];
+    }
+    return node;
+  };
+
+  UndoManager.prototype.caretPosition = function(caret) {
+    var endContainer, endOffset, range, startContainer, startOffset;
+    if (!caret) {
+      range = this.editor.selection.range();
+      caret = this.editor.inputManager.focused && (range != null) ? {
+        start: this.startPosition(),
+        end: this.endPosition(),
+        collapsed: range.collapsed
+      } : {};
+      return caret;
+    } else {
+      if (!caret.start) {
+        return;
+      }
+      startContainer = this._getNodeByPosition(caret.start);
+      startOffset = caret.start[caret.start.length - 1];
+      if (caret.collapsed) {
+        endContainer = startContainer;
+        endOffset = startOffset;
+      } else {
+        endContainer = this._getNodeByPosition(caret.end);
+        endOffset = caret.start[caret.start.length - 1];
+      }
+      if (!startContainer || !endContainer) {
+        if (typeof console !== "undefined" && console !== null) {
+          if (typeof console.warn === "function") {
+            console.warn('simditor: invalid caret state');
+          }
+        }
+        return;
+      }
+
+      range = document.createRange();
+      range.setStart(startContainer, startOffset);
+      range.setEnd(endContainer, endOffset);
+      return this.editor.selection.range(range);
+    }
+  };
+
+  return UndoManager;
+
+})(SimpleModule);
+
+Util = (function(superClass) {
+  extend(Util, superClass);
+
+  function Util() {
+    return Util.__super__.constructor.apply(this, arguments);
+  }
+
+  Util.pluginName = 'Util';
+
+  Util.prototype._init = function() {
+    this.editor = this._module;
+    if (this.browser.msie && this.browser.version < 11) {
+      return this.phBr = '';
+    }
+  };
+
+  Util.prototype.phBr = '<br/>';
+
+  Util.prototype.os = (function() {
+    var os;
+    os = {};
+    if (/Mac/.test(navigator.appVersion)) {
+      os.mac = true;
+    } else if (/Linux/.test(navigator.appVersion)) {
+      os.linux = true;
+    } else if (/Win/.test(navigator.appVersion)) {
+      os.win = true;
+    } else if (/X11/.test(navigator.appVersion)) {
+      os.unix = true;
+    }
+    if (/Mobi/.test(navigator.appVersion)) {
+      os.mobile = true;
+    }
+    return os;
+  })();
+
+  Util.prototype.browser = (function() {
+    var chrome, edge, firefox, ie, ref, ref1, ref2, ref3, ref4, safari, ua;
+    ua = navigator.userAgent;
+    ie = /(msie|trident)/i.test(ua);
+    chrome = /chrome|crios/i.test(ua);
+    safari = /safari/i.test(ua) && !chrome;
+    firefox = /firefox/i.test(ua);
+    edge = /edge/i.test(ua);
+    if (ie) {
+      return {
+        msie: true,
+        version: ((ref = ua.match(/(msie |rv:)(\d+(\.\d+)?)/i)) != null ? ref[2] : void 0) * 1
+      };
+    } else if (edge) {
+      return {
+        edge: true,
+        webkit: true,
+        version: ((ref1 = ua.match(/edge\/(\d+(\.\d+)?)/i)) != null ? ref1[1] : void 0) * 1
+      };
+    } else if (chrome) {
+      return {
+        webkit: true,
+        chrome: true,
+        version: ((ref2 = ua.match(/(?:chrome|crios)\/(\d+(\.\d+)?)/i)) != null ? ref2[1] : void 0) * 1
+      };
+    } else if (safari) {
+      return {
+        webkit: true,
+        safari: true,
+        version: ((ref3 = ua.match(/version\/(\d+(\.\d+)?)/i)) != null ? ref3[1] : void 0) * 1
+      };
+    } else if (firefox) {
+      return {
+        mozilla: true,
+        firefox: true,
+        version: ((ref4 = ua.match(/firefox\/(\d+(\.\d+)?)/i)) != null ? ref4[1] : void 0) * 1
+      };
+    } else {
+      return {};
+    }
+  })();
+
+  Util.prototype.support = (function() {
+    return {
+      onselectionchange: (function() {
+        var e, onselectionchange;
+        onselectionchange = document.onselectionchange;
+        if (onselectionchange !== void 0) {
+          try {
+            document.onselectionchange = 0;
+            return document.onselectionchange === null;
+          } catch (_error) {
+            e = _error;
+          } finally {
+            document.onselectionchange = onselectionchange;
+          }
+        }
+        return false;
+      })(),
+      oninput: (function() {
+        return !/(msie|trident)/i.test(navigator.userAgent);
+      })()
+    };
+  })();
+
+  Util.prototype.reflow = function(el) {
+    if (el == null) {
+      el = document;
+    }
+    return $(el)[0].offsetHeight;
+  };
+
+  Util.prototype.metaKey = function(e) {
+    var isMac;
+    isMac = /Mac/.test(navigator.userAgent);
+    if (isMac) {
+      return e.metaKey;
+    } else {
+      return e.ctrlKey;
+    }
+  };
+
+  Util.prototype.isEmptyNode = function(node) {
+    var $node;
+    $node = $(node);
+    return $node.is(':empty') || (!$node.text() && !$node.find(':not(br, span, div)').length);
+  };
+
+  Util.prototype.isDecoratedNode = function(node) {
+    return $(node).is('[class^="simditor-"]');
+  };
+
+  Util.prototype.blockNodes = ["div", "p", "ul", "ol", "li", "blockquote", "hr", "pre", "h1", "h2", "h3", "h4", "h5", "table"];
+
+  Util.prototype.isBlockNode = function(node) {
+    node = $(node)[0];
+    if (!node || node.nodeType === 3) {
+      return false;
+    }
+    return new RegExp("^(" + (this.blockNodes.join('|')) + ")$").test(node.nodeName.toLowerCase());
+  };
+
+  Util.prototype.getNodeLength = function(node) {
+    node = $(node)[0];
+    switch (node.nodeType) {
+      case 7:
+      case 10:
+        return 0;
+      case 3:
+      case 8:
+        return node.length;
+      default:
+        return node.childNodes.length;
+    }
+  };
+
+  Util.prototype.dataURLtoBlob = function(dataURL) {
+    var BlobBuilder, arrayBuffer, bb, blobArray, byteString, hasArrayBufferViewSupport, hasBlobConstructor, i, intArray, k, mimeString, ref, supportBlob;
+    hasBlobConstructor = window.Blob && (function() {
+      var e;
+      try {
+        return Boolean(new Blob());
+      } catch (_error) {
+        e = _error;
+        return false;
+      }
+    })();
+    hasArrayBufferViewSupport = hasBlobConstructor && window.Uint8Array && (function() {
+      var e;
+      try {
+        return new Blob([new Uint8Array(100)]).size === 100;
+      } catch (_error) {
+        e = _error;
+        return false;
+      }
+    })();
+    BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder;
+    supportBlob = hasBlobConstructor || BlobBuilder;
+    if (!(supportBlob && window.atob && window.ArrayBuffer && window.Uint8Array)) {
+      return false;
+    }
+    if (dataURL.split(',')[0].indexOf('base64') >= 0) {
+      byteString = atob(dataURL.split(',')[1]);
+    } else {
+      byteString = decodeURIComponent(dataURL.split(',')[1]);
+    }
+    arrayBuffer = new ArrayBuffer(byteString.length);
+    intArray = new Uint8Array(arrayBuffer);
+    for (i = k = 0, ref = byteString.length; 0 <= ref ? k <= ref : k >= ref; i = 0 <= ref ? ++k : --k) {
+      intArray[i] = byteString.charCodeAt(i);
+    }
+    mimeString = dataURL.split(',')[0].split(':')[1].split(';')[0];
+    if (hasBlobConstructor) {
+      blobArray = hasArrayBufferViewSupport ? intArray : arrayBuffer;
+      return new Blob([blobArray], {
+        type: mimeString
+      });
+    }
+    bb = new BlobBuilder();
+    bb.append(arrayBuffer);
+    return bb.getBlob(mimeString);
+  };
+
+  Util.prototype.throttle = function(func, wait) {
+    var args, call, ctx, last, rtn, throttled, timeoutID;
+    last = 0;
+    timeoutID = 0;
+    ctx = args = rtn = null;
+    call = function() {
+      timeoutID = 0;
+      last = +new Date();
+      rtn = func.apply(ctx, args);
+      ctx = null;
+      return args = null;
+    };
+    throttled = function() {
+      var delta;
+      ctx = this;
+      args = arguments;
+      delta = new Date() - last;
+      if (!timeoutID) {
+        if (delta >= wait) {
+          call();
+        } else {
+          timeoutID = setTimeout(call, wait - delta);
+        }
+      }
+      return rtn;
+    };
+    throttled.clear = function() {
+      if (!timeoutID) {
+        return;
+      }
+      clearTimeout(timeoutID);
+      return call();
+    };
+    return throttled;
+  };
+
+  Util.prototype.formatHTML = function(html) {
+    var cursor, indentString, lastMatch, level, match, re, repeatString, result, str;
+    re = /<(\/?)(.+?)(\/?)>/g;
+    result = '';
+    level = 0;
+    lastMatch = null;
+    indentString = '  ';
+    repeatString = function(str, n) {
+      return new Array(n + 1).join(str);
+    };
+    while ((match = re.exec(html)) !== null) {
+      match.isBlockNode = $.inArray(match[2], this.blockNodes) > -1;
+      match.isStartTag = match[1] !== '/' && match[3] !== '/';
+      match.isEndTag = match[1] === '/' || match[3] === '/';
+      cursor = lastMatch ? lastMatch.index + lastMatch[0].length : 0;
+      if ((str = html.substring(cursor, match.index)).length > 0 && $.trim(str)) {
+        result += str;
+      }
+      if (match.isBlockNode && match.isEndTag && !match.isStartTag) {
+        level -= 1;
+      }
+      if (match.isBlockNode && match.isStartTag) {
+        if (!(lastMatch && lastMatch.isBlockNode && lastMatch.isEndTag)) {
+          result += '\n';
+        }
+        result += repeatString(indentString, level);
+      }
+      result += match[0];
+      if (match.isBlockNode && match.isEndTag) {
+        result += '\n';
+      }
+      if (match.isBlockNode && match.isStartTag) {
+        level += 1;
+      }
+      lastMatch = match;
+    }
+    return $.trim(result);
+  };
+
+  return Util;
+
+})(SimpleModule);
+
+Toolbar = (function(superClass) {
+  extend(Toolbar, superClass);
+
+  function Toolbar() {
+    return Toolbar.__super__.constructor.apply(this, arguments);
+  }
+
+  Toolbar.pluginName = 'Toolbar';
+
+  Toolbar.prototype.opts = {
+    toolbar: true,
+    toolbarFloat: true,
+    toolbarHidden: false,
+    toolbarFloatOffset: 0
+  };
+
+  Toolbar.prototype._tpl = {
+    wrapper: '<div class="simditor-toolbar"><ul></ul></div>',
+    separator: '<li><span class="separator"></span></li>'
+  };
+
+  Toolbar.prototype._init = function() {
+    var floatInitialized, initToolbarFloat, toolbarHeight;
+    this.editor = this._module;
+    if (!this.opts.toolbar) {
+      return;
+    }
+    if (!$.isArray(this.opts.toolbar)) {
+      this.opts.toolbar = ['bold', 'italic', 'underline', 'strikethrough', '|', 'ol', 'ul', 'blockquote', 'code', '|', 'link', 'image', '|', 'indent', 'outdent'];
+    }
+    this._render();
+    this.list.on('click', function(e) {
+      return false;
+    });
+    this.wrapper.on('mousedown', (function(_this) {
+      return function(e) {
+        return _this.list.find('.menu-on').removeClass('.menu-on');
+      };
+    })(this));
+    $(document).on('mousedown.simditor' + this.editor.id, (function(_this) {
+      return function(e) {
+        return _this.list.find('.menu-on').removeClass('.menu-on');
+      };
+    })(this));
+    if (!this.opts.toolbarHidden && this.opts.toolbarFloat) {
+      this.wrapper.css('top', this.opts.toolbarFloatOffset);
+      toolbarHeight = 0;
+      initToolbarFloat = (function(_this) {
+        return function() {
+          _this.wrapper.css('position', 'static');
+          _this.wrapper.width('auto');
+          _this.editor.util.reflow(_this.wrapper);
+          _this.wrapper.width(_this.wrapper.outerWidth());
+          _this.wrapper.css('left', _this.editor.util.os.mobile ? _this.wrapper.position().left : _this.wrapper.offset().left);
+          _this.wrapper.css('position', '');
+          toolbarHeight = _this.wrapper.outerHeight();
+          _this.editor.placeholderEl.css('top', toolbarHeight);
+          return true;
+        };
+      })(this);
+      floatInitialized = null;
+      $(window).on('resize.simditor-' + this.editor.id, function(e) {
+        return floatInitialized = initToolbarFloat();
+      });
+      $(window).on('scroll.simditor-' + this.editor.id, (function(_this) {
+        return function(e) {
+          var bottomEdge, scrollTop, topEdge;
+          if (!_this.wrapper.is(':visible')) {
+            return;
+          }
+          topEdge = _this.editor.wrapper.offset().top;
+          bottomEdge = topEdge + _this.editor.wrapper.outerHeight() - 80;
+          scrollTop = $(document).scrollTop() + _this.opts.toolbarFloatOffset;
+          if (scrollTop <= topEdge || scrollTop >= bottomEdge) {
+            _this.editor.wrapper.removeClass('toolbar-floating').css('padding-top', '');
+            if (_this.editor.util.os.mobile) {
+              return _this.wrapper.css('top', _this.opts.toolbarFloatOffset);
+            }
+          } else {
+            floatInitialized || (floatInitialized = initToolbarFloat());
+            _this.editor.wrapper.addClass('toolbar-floating').css('padding-top', toolbarHeight);
+            if (_this.editor.util.os.mobile) {
+              return _this.wrapper.css('top', scrollTop - topEdge + _this.opts.toolbarFloatOffset);
+            }
+          }
+        };
+      })(this));
+    }
+    this.editor.on('destroy', (function(_this) {
+      return function() {
+        return _this.buttons.length = 0;
+      };
+    })(this));
+    return $(document).on("mousedown.simditor-" + this.editor.id, (function(_this) {
+      return function(e) {
+        return _this.list.find('li.menu-on').removeClass('menu-on');
+      };
+    })(this));
+  };
+
+  Toolbar.prototype._render = function() {
+    var k, len, name, ref;
+    this.buttons = [];
+    this.wrapper = $(this._tpl.wrapper).prependTo(this.editor.wrapper);
+    this.list = this.wrapper.find('ul');
+    ref = this.opts.toolbar;
+    for (k = 0, len = ref.length; k < len; k++) {
+      name = ref[k];
+      if (name === '|') {
+        $(this._tpl.separator).appendTo(this.list);
+        continue;
+      }
+      if (!this.constructor.buttons[name]) {
+        throw new Error("simditor: invalid toolbar button " + name);
+        continue;
+      }
+      this.buttons.push(new this.constructor.buttons[name]({
+        editor: this.editor
+      }));
+    }
+    if (this.opts.toolbarHidden) {
+      return this.wrapper.hide();
+    }
+  };
+
+  Toolbar.prototype.findButton = function(name) {
+    var button;
+    button = this.list.find('.toolbar-item-' + name).data('button');
+    return button != null ? button : null;
+  };
+
+  Toolbar.addButton = function(btn) {
+    return this.buttons[btn.prototype.name] = btn;
+  };
+
+  Toolbar.buttons = {};
+
+  return Toolbar;
+
+})(SimpleModule);
+
+Indentation = (function(superClass) {
+  extend(Indentation, superClass);
+
+  function Indentation() {
+    return Indentation.__super__.constructor.apply(this, arguments);
+  }
+
+  Indentation.pluginName = 'Indentation';
+
+  Indentation.prototype.opts = {
+    tabIndent: true
+  };
+
+  Indentation.prototype._init = function() {
+    this.editor = this._module;
+    return this.editor.keystroke.add('tab', '*', (function(_this) {
+      return function(e) {
+        var codeButton;
+        codeButton = _this.editor.toolbar.findButton('code');
+        if (!(_this.opts.tabIndent || (codeButton && codeButton.active))) {
+          return;
+        }
+        return _this.indent(e.shiftKey);
+      };
+    })(this));
+  };
+
+  Indentation.prototype.indent = function(isBackward) {
+    var $blockNodes, $endNodes, $startNodes, nodes, result;
+    $startNodes = this.editor.selection.startNodes();
+    $endNodes = this.editor.selection.endNodes();
+    $blockNodes = this.editor.selection.blockNodes();
+    nodes = [];
+    $blockNodes = $blockNodes.each(function(i, node) {
+      var include, j, k, len, n;
+      include = true;
+      for (j = k = 0, len = nodes.length; k < len; j = ++k) {
+        n = nodes[j];
+        if ($.contains(node, n)) {
+          include = false;
+          break;
+        } else if ($.contains(n, node)) {
+          nodes.splice(j, 1, node);
+          include = false;
+          break;
+        }
+      }
+      if (include) {
+        return nodes.push(node);
+      }
+    });
+    $blockNodes = $(nodes);
+    result = false;
+    $blockNodes.each((function(_this) {
+      return function(i, blockEl) {
+        var r;
+        r = isBackward ? _this.outdentBlock(blockEl) : _this.indentBlock(blockEl);
+        if (r) {
+          return result = r;
+        }
+      };
+    })(this));
+    return result;
+  };
+
+  Indentation.prototype.indentBlock = function(blockEl) {
+    var $blockEl, $childList, $nextTd, $nextTr, $parentLi, $pre, $td, $tr, marginLeft, tagName;
+    $blockEl = $(blockEl);
+    if (!$blockEl.length) {
+      return;
+    }
+    if ($blockEl.is('pre')) {
+      $pre = this.editor.selection.containerNode();
+      if (!($pre.is($blockEl) || $pre.closest('pre').is($blockEl))) {
+        return;
+      }
+      this.indentText(this.editor.selection.range());
+    } else if ($blockEl.is('li')) {
+      $parentLi = $blockEl.prev('li');
+      if ($parentLi.length < 1) {
+        return;
+      }
+      this.editor.selection.save();
+      tagName = $blockEl.parent()[0].tagName;
+      $childList = $parentLi.children('ul, ol');
+      if ($childList.length > 0) {
+        $childList.append($blockEl);
+      } else {
+        $('<' + tagName + '/>').append($blockEl).appendTo($parentLi);
+      }
+      this.editor.selection.restore();
+    } else if ($blockEl.is('p, h1, h2, h3, h4')) {
+      marginLeft = parseInt($blockEl.css('margin-left')) || 0;
+      marginLeft = (Math.round(marginLeft / this.opts.indentWidth) + 1) * this.opts.indentWidth;
+      $blockEl.css('margin-left', marginLeft);
+    } else if ($blockEl.is('table') || $blockEl.is('.simditor-table')) {
+      $td = this.editor.selection.containerNode().closest('td, th');
+      $nextTd = $td.next('td, th');
+      if (!($nextTd.length > 0)) {
+        $tr = $td.parent('tr');
+        $nextTr = $tr.next('tr');
+        if ($nextTr.length < 1 && $tr.parent().is('thead')) {
+          $nextTr = $tr.parent('thead').next('tbody').find('tr:first');
+        }
+        $nextTd = $nextTr.find('td:first, th:first');
+      }
+      if (!($td.length > 0 && $nextTd.length > 0)) {
+        return;
+      }
+      this.editor.selection.setRangeAtEndOf($nextTd);
+    } else {
+      return false;
+    }
+    return true;
+  };
+
+  Indentation.prototype.indentText = function(range) {
+    var text, textNode;
+    text = range.toString().replace(/^(?=.+)/mg, '\u00A0\u00A0');
+    textNode = document.createTextNode(text || '\u00A0\u00A0');
+    range.deleteContents();
+    range.insertNode(textNode);
+    if (text) {
+      range.selectNode(textNode);
+      return this.editor.selection.range(range);
+    } else {
+      return this.editor.selection.setRangeAfter(textNode);
+    }
+  };
+
+  Indentation.prototype.outdentBlock = function(blockEl) {
+    var $blockEl, $parent, $parentLi, $pre, $prevTd, $prevTr, $td, $tr, marginLeft, range;
+    $blockEl = $(blockEl);
+    if (!($blockEl && $blockEl.length > 0)) {
+      return;
+    }
+    if ($blockEl.is('pre')) {
+      $pre = this.editor.selection.containerNode();
+      if (!($pre.is($blockEl) || $pre.closest('pre').is($blockEl))) {
+        return;
+      }
+      this.outdentText(range);
+    } else if ($blockEl.is('li')) {
+      $parent = $blockEl.parent();
+      $parentLi = $parent.parent('li');
+      this.editor.selection.save();
+      if ($parentLi.length < 1) {
+        range = document.createRange();
+        range.setStartBefore($parent[0]);
+        range.setEndBefore($blockEl[0]);
+        $parent.before(range.extractContents());
+        $('<p/>').insertBefore($parent).after($blockEl.children('ul, ol')).append($blockEl.contents());
+        $blockEl.remove();
+      } else {
+        if ($blockEl.next('li').length > 0) {
+          $('<' + $parent[0].tagName + '/>').append($blockEl.nextAll('li')).appendTo($blockEl);
+        }
+        $blockEl.insertAfter($parentLi);
+        if ($parent.children('li').length < 1) {
+          $parent.remove();
+        }
+      }
+      this.editor.selection.restore();
+    } else if ($blockEl.is('p, h1, h2, h3, h4')) {
+      marginLeft = parseInt($blockEl.css('margin-left')) || 0;
+      marginLeft = Math.max(Math.round(marginLeft / this.opts.indentWidth) - 1, 0) * this.opts.indentWidth;
+      $blockEl.css('margin-left', marginLeft === 0 ? '' : marginLeft);
+    } else if ($blockEl.is('table') || $blockEl.is('.simditor-table')) {
+      $td = this.editor.selection.containerNode().closest('td, th');
+      $prevTd = $td.prev('td, th');
+      if (!($prevTd.length > 0)) {
+        $tr = $td.parent('tr');
+        $prevTr = $tr.prev('tr');
+        if ($prevTr.length < 1 && $tr.parent().is('tbody')) {
+          $prevTr = $tr.parent('tbody').prev('thead').find('tr:first');
+        }
+        $prevTd = $prevTr.find('td:last, th:last');
+      }
+      if (!($td.length > 0 && $prevTd.length > 0)) {
+        return;
+      }
+      this.editor.selection.setRangeAtEndOf($prevTd);
+    } else {
+      return false;
+    }
+    return true;
+  };
+
+  Indentation.prototype.outdentText = function(range) {};
+
+  return Indentation;
+
+})(SimpleModule);
+
+Clipboard = (function(superClass) {
+  extend(Clipboard, superClass);
+
+  function Clipboard() {
+    return Clipboard.__super__.constructor.apply(this, arguments);
+  }
+
+  Clipboard.pluginName = 'Clipboard';
+
+  Clipboard.prototype.opts = {
+    pasteImage: false,
+    cleanPaste: false
+  };
+
+  Clipboard.prototype._init = function() {
+    this.editor = this._module;
+    if (this.opts.pasteImage && typeof this.opts.pasteImage !== 'string') {
+      this.opts.pasteImage = 'inline';
+    }
+    return this.editor.body.on('paste', (function(_this) {
+      return function(e) {
+        var range;
+        if (_this.pasting || _this._pasteBin) {
+          return;
+        }
+        if (_this.editor.triggerHandler(e) === false) {
+          return false;
+        }
+        range = _this.editor.selection.deleteRangeContents();
+        if (_this.editor.body.html()) {
+          if (!range.collapsed) {
+            range.collapse(true);
+          }
+        } else {
+          _this.editor.formatter.format();
+          _this.editor.selection.setRangeAtStartOf(_this.editor.body.find('p:first'));
+        }
+        if (_this._processPasteByClipboardApi(e)) {
+          return false;
+        }
+        _this.editor.inputManager.throttledValueChanged.clear();
+        _this.editor.inputManager.throttledSelectionChanged.clear();
+        _this.editor.undoManager.throttledPushState.clear();
+        _this.editor.selection.reset();
+        _this.editor.undoManager.resetCaretPosition();
+        _this.pasting = true;
+        return _this._getPasteContent(function(pasteContent) {
+          _this._processPasteContent(pasteContent);
+          _this._pasteInBlockEl = null;
+          _this._pastePlainText = null;
+          return _this.pasting = false;
+        });
+      };
+    })(this));
+  };
+
+  Clipboard.prototype._processPasteByClipboardApi = function(e) {
+    var imageFile, pasteItem, ref, uploadOpt;
+    if (this.editor.util.browser.edge) {
+      return;
+    }
+    if (e.originalEvent.clipboardData && e.originalEvent.clipboardData.items && e.originalEvent.clipboardData.items.length > 0) {
+      pasteItem = e.originalEvent.clipboardData.items[0];
+      if (/^image\//.test(pasteItem.type)) {
+        imageFile = pasteItem.getAsFile();
+        if (!((imageFile != null) && this.opts.pasteImage)) {
+          return;
+        }
+        if (!imageFile.name) {
+          imageFile.name = "Clipboard Image.png";
+        }
+        if (this.editor.triggerHandler('pasting', [imageFile]) === false) {
+          return;
+        }
+        uploadOpt = {};
+        uploadOpt[this.opts.pasteImage] = true;
+        if ((ref = this.editor.uploader) != null) {
+          ref.upload(imageFile, uploadOpt);
+        }
+        return true;
+      }
+    }
+  };
+
+  Clipboard.prototype._getPasteContent = function(callback) {
+    var state;
+    this._pasteBin = $('<div contenteditable="true" />').addClass('simditor-paste-bin').attr('tabIndex', '-1').appendTo(this.editor.el);
+    state = {
+      html: this.editor.body.html(),
+      caret: this.editor.undoManager.caretPosition()
+    };
+    this._pasteBin.focus();
+    return setTimeout((function(_this) {
+      return function() {
+        var pasteContent;
+        _this.editor.hidePopover();
+        _this.editor.body.get(0).innerHTML = state.html;
+        _this.editor.undoManager.caretPosition(state.caret);
+        _this.editor.body.focus();
+        _this.editor.selection.reset();
+        _this.editor.selection.range();
+        _this._pasteInBlockEl = _this.editor.selection.blockNodes().last();
+        _this._pastePlainText = _this.opts.cleanPaste || _this._pasteInBlockEl.is('pre, table');
+        if (_this._pastePlainText) {
+          pasteContent = _this.editor.formatter.clearHtml(_this._pasteBin.html(), true);
+        } else {
+          pasteContent = $('<div/>').append(_this._pasteBin.contents());
+          pasteContent.find('style').remove();
+          pasteContent.find('table colgroup').remove();
+          _this.editor.formatter.format(pasteContent);
+          _this.editor.formatter.decorate(pasteContent);
+          _this.editor.formatter.beautify(pasteContent.children());
+          pasteContent = pasteContent.contents();
+        }
+        _this._pasteBin.remove();
+        _this._pasteBin = null;
+        return callback(pasteContent);
+      };
+    })(this), 0);
+  };
+
+  Clipboard.prototype._processPasteContent = function(pasteContent) {
+    var $blockEl, $img, blob, children, dataURLtoBlob, img, insertPosition, k, l, lastLine, len, len1, len2, len3, len4, line, lines, m, node, o, q, ref, ref1, ref2, uploadOpt, uploader;
+    if (this.editor.triggerHandler('pasting', [pasteContent]) === false) {
+      return;
+    }
+    $blockEl = this._pasteInBlockEl;
+    if (!pasteContent) {
+      return;
+    } else if (this._pastePlainText) {
+      if ($blockEl.is('table')) {
+        lines = pasteContent.split('\n');
+        lastLine = lines.pop();
+        for (k = 0, len = lines.length; k < len; k++) {
+          line = lines[k];
+          this.editor.selection.insertNode(document.createTextNode(line));
+          this.editor.selection.insertNode($('<br/>'));
+        }
+        this.editor.selection.insertNode(document.createTextNode(lastLine));
+      } else {
+        pasteContent = $('<div/>').text(pasteContent);
+        ref = pasteContent.contents();
+        for (l = 0, len1 = ref.length; l < len1; l++) {
+          node = ref[l];
+          this.editor.selection.insertNode($(node)[0]);
+        }
+      }
+    } else if ($blockEl.is(this.editor.body)) {
+      for (m = 0, len2 = pasteContent.length; m < len2; m++) {
+        node = pasteContent[m];
+        this.editor.selection.insertNode(node);
+      }
+    } else if (pasteContent.length < 1) {
+      return;
+    } else if (pasteContent.length === 1) {
+      if (pasteContent.is('p')) {
+        children = pasteContent.contents();
+        if ($blockEl.is('h1, h2, h3, h4, h5')) {
+          if (children.length) {
+            children.css('font-size', '');
+          }
+        }
+        if (children.length === 1 && children.is('img')) {
+          $img = children;
+          if (/^data:image/.test($img.attr('src'))) {
+            if (!this.opts.pasteImage) {
+              return;
+            }
+            blob = this.editor.util.dataURLtoBlob($img.attr("src"));
+            blob.name = "Clipboard Image.png";
+            uploadOpt = {};
+            uploadOpt[this.opts.pasteImage] = true;
+            if ((ref1 = this.editor.uploader) != null) {
+              ref1.upload(blob, uploadOpt);
+            }
+            return;
+          } else if (new RegExp('^blob:' + location.origin + '/').test($img.attr('src'))) {
+            if (!this.opts.pasteImage) {
+              return;
+            }
+            uploadOpt = {};
+            uploadOpt[this.opts.pasteImage] = true;
+            dataURLtoBlob = this.editor.util.dataURLtoBlob;
+            uploader = this.editor.uploader;
+            img = new Image;
+            img.onload = function() {
+              var canvas;
+              canvas = document.createElement('canvas');
+              canvas.width = img.naturalWidth;
+              canvas.height = img.naturalHeight;
+              canvas.getContext('2d').drawImage(img, 0, 0);
+              blob = dataURLtoBlob(canvas.toDataURL('image/png'));
+              blob.name = 'Clipboard Image.png';
+              if (uploader !== null) {
+                uploader.upload(blob, uploadOpt);
+              }
+            };
+            img.src = $img.attr('src');
+            return;
+          } else if ($img.is('img[src^="webkit-fake-url://"]')) {
+            return;
+          }
+        }
+        for (o = 0, len3 = children.length; o < len3; o++) {
+          node = children[o];
+          this.editor.selection.insertNode(node);
+        }
+      } else if ($blockEl.is('p') && this.editor.util.isEmptyNode($blockEl)) {
+        $blockEl.replaceWith(pasteContent);
+        this.editor.selection.setRangeAtEndOf(pasteContent);
+      } else if (pasteContent.is('ul, ol')) {
+        if (pasteContent.find('li').length === 1) {
+          pasteContent = $('<div/>').text(pasteContent.text());
+          ref2 = pasteContent.contents();
+          for (q = 0, len4 = ref2.length; q < len4; q++) {
+            node = ref2[q];
+            this.editor.selection.insertNode($(node)[0]);
+          }
+        } else if ($blockEl.is('li')) {
+          $blockEl.parent().after(pasteContent);
+          this.editor.selection.setRangeAtEndOf(pasteContent);
+        } else {
+          $blockEl.after(pasteContent);
+          this.editor.selection.setRangeAtEndOf(pasteContent);
+        }
+      } else {
+        $blockEl.after(pasteContent);
+        this.editor.selection.setRangeAtEndOf(pasteContent);
+      }
+    } else {
+      if ($blockEl.is('li')) {
+        $blockEl = $blockEl.parent();
+      }
+      if (this.editor.selection.rangeAtStartOf($blockEl)) {
+        insertPosition = 'before';
+      } else if (this.editor.selection.rangeAtEndOf($blockEl)) {
+        insertPosition = 'after';
+      } else {
+        this.editor.selection.breakBlockEl($blockEl);
+        insertPosition = 'before';
+      }
+      $blockEl[insertPosition](pasteContent);
+      this.editor.selection.setRangeAtEndOf(pasteContent.last());
+    }
+    return this.editor.inputManager.throttledValueChanged();
+  };
+
+  return Clipboard;
+
+})(SimpleModule);
+
+Simditor = (function(superClass) {
+  extend(Simditor, superClass);
+
+  function Simditor() {
+    return Simditor.__super__.constructor.apply(this, arguments);
+  }
+
+  Simditor.connect(Util);
+
+  Simditor.connect(InputManager);
+
+  Simditor.connect(Selection);
+
+  Simditor.connect(UndoManager);
+
+  Simditor.connect(Keystroke);
+
+  Simditor.connect(Formatter);
+
+  Simditor.connect(Toolbar);
+
+  Simditor.connect(Indentation);
+
+  Simditor.connect(Clipboard);
+
+  Simditor.count = 0;
+
+  Simditor.prototype.opts = {
+    textarea: null,
+    placeholder: '',
+    defaultImage: 'images/image.png',
+    params: {},
+    upload: false,
+    indentWidth: 40
+  };
+
+  Simditor.prototype._init = function() {
+    var e, editor, uploadOpts;
+    this.textarea = $(this.opts.textarea);
+    this.opts.placeholder = this.opts.placeholder || this.textarea.attr('placeholder');
+    if (!this.textarea.length) {
+      throw new Error('simditor: param textarea is required.');
+      return;
+    }
+    editor = this.textarea.data('simditor');
+    if (editor != null) {
+      editor.destroy();
+    }
+    this.id = ++Simditor.count;
+    this._render();
+    if (simpleHotkeys) {
+      this.hotkeys = simpleHotkeys({
+        el: this.body
+      });
+    } else {
+      throw new Error('simditor: simple-hotkeys is required.');
+      return;
+    }
+    if (this.opts.upload && simpleUploader) {
+      uploadOpts = typeof this.opts.upload === 'object' ? this.opts.upload : {};
+      this.uploader = simpleUploader(uploadOpts);
+    }
+    this.on('initialized', (function(_this) {
+      return function() {
+        if (_this.opts.placeholder) {
+          _this.on('valuechanged', function() {
+            return _this._placeholder();
+          });
+        }
+        _this.setValue(_this.textarea.val().trim() || '');
+        if (_this.textarea.attr('autofocus')) {
+          return _this.focus();
+        }
+      };
+    })(this));
+    if (this.util.browser.mozilla) {
+      this.util.reflow();
+      try {
+        document.execCommand('enableObjectResizing', false, false);
+        return document.execCommand('enableInlineTableEditing', false, false);
+      } catch (_error) {
+        e = _error;
+      }
+    }
+  };
+
+  Simditor.prototype._tpl = "<div class=\"simditor\">\n  <div class=\"simditor-wrapper\">\n    <div class=\"simditor-placeholder\"></div>\n    <div class=\"simditor-body\" contenteditable=\"true\">\n    </div>\n  </div>\n</div>";
+
+  Simditor.prototype._render = function() {
+    var key, ref, results, val;
+    this.el = $(this._tpl).insertBefore(this.textarea);
+    this.wrapper = this.el.find('.simditor-wrapper');
+    this.body = this.wrapper.find('.simditor-body');
+    this.placeholderEl = this.wrapper.find('.simditor-placeholder').append(this.opts.placeholder);
+    this.el.data('simditor', this);
+    this.wrapper.append(this.textarea);
+    this.textarea.data('simditor', this).blur();
+    this.body.attr('tabindex', this.textarea.attr('tabindex'));
+    if (this.util.os.mac) {
+      this.el.addClass('simditor-mac');
+    } else if (this.util.os.linux) {
+      this.el.addClass('simditor-linux');
+    }
+    if (this.util.os.mobile) {
+      this.el.addClass('simditor-mobile');
+    }
+    if (this.opts.params) {
+      ref = this.opts.params;
+      results = [];
+      for (key in ref) {
+        val = ref[key];
+        results.push($('<input/>', {
+          type: 'hidden',
+          name: key,
+          value: val
+        }).insertAfter(this.textarea));
+      }
+      return results;
+    }
+  };
+
+  Simditor.prototype._placeholder = function() {
+    var children;
+    children = this.body.children();
+    if (children.length === 0 || (children.length === 1 && this.util.isEmptyNode(children) && parseInt(children.css('margin-left') || 0) < this.opts.indentWidth)) {
+      return this.placeholderEl.show();
+    } else {
+      return this.placeholderEl.hide();
+    }
+  };
+
+  Simditor.prototype.setValue = function(val) {
+    this.hidePopover();
+    this.textarea.val(val);
+    this.body.get(0).innerHTML = val;
+    this.formatter.format();
+    this.formatter.decorate();
+    this.util.reflow(this.body);
+    this.inputManager.lastCaretPosition = null;
+    return this.trigger('valuechanged');
+  };
+
+  Simditor.prototype.getValue = function() {
+    return this.sync();
+  };
+
+  Simditor.prototype.sync = function() {
+    var children, cloneBody, emptyP, firstP, lastP, val;
+    cloneBody = this.body.clone();
+    this.formatter.undecorate(cloneBody);
+    this.formatter.format(cloneBody);
+    this.formatter.autolink(cloneBody);
+    children = cloneBody.children();
+    lastP = children.last('p');
+    firstP = children.first('p');
+    while (lastP.is('p') && this.util.isEmptyNode(lastP)) {
+      emptyP = lastP;
+      lastP = lastP.prev('p');
+      emptyP.remove();
+    }
+    while (firstP.is('p') && this.util.isEmptyNode(firstP)) {
+      emptyP = firstP;
+      firstP = lastP.next('p');
+      emptyP.remove();
+    }
+    cloneBody.find('img.uploading').remove();
+    val = $.trim(cloneBody.html());
+    this.textarea.val(val);
+    return val;
+  };
+
+  Simditor.prototype.focus = function() {
+    var $blockEl, range;
+    if (!(this.body.is(':visible') && this.body.is('[contenteditable]'))) {
+      this.el.find('textarea:visible').focus();
+      return;
+    }
+    if (this.inputManager.lastCaretPosition) {
+      this.undoManager.caretPosition(this.inputManager.lastCaretPosition);
+      return this.inputManager.lastCaretPosition = null;
+    } else {
+      $blockEl = this.body.children().last();
+      if (!$blockEl.is('p')) {
+        $blockEl = $('<p/>').append(this.util.phBr).appendTo(this.body);
+      }
+      range = document.createRange();
+      return this.selection.setRangeAtEndOf($blockEl, range);
+    }
+  };
+
+  Simditor.prototype.blur = function() {
+    if (this.body.is(':visible') && this.body.is('[contenteditable]')) {
+      return this.body.blur();
+    } else {
+      return this.body.find('textarea:visible').blur();
+    }
+  };
+
+  Simditor.prototype.hidePopover = function() {
+    return this.el.find('.simditor-popover').each(function(i, popover) {
+      popover = $(popover).data('popover');
+      if (popover.active) {
+        return popover.hide();
+      }
+    });
+  };
+
+  Simditor.prototype.destroy = function() {
+    this.triggerHandler('destroy');
+    this.textarea.closest('form').off('.simditor .simditor-' + this.id);
+    this.selection.clear();
+    this.inputManager.focused = false;
+    this.textarea.insertBefore(this.el).hide().val('').removeData('simditor');
+    this.el.remove();
+    $(document).off('.simditor-' + this.id);
+    $(window).off('.simditor-' + this.id);
+    return this.off();
+  };
+
+  return Simditor;
+
+})(SimpleModule);
+
+Simditor.i18n = {
+  'zh-CN': {
+    'blockquote': '引用',
+    'bold': '加粗文字',
+    'code': '插入代码',
+    'color': '文字颜色',
+    'coloredText': '彩色文字',
+    'hr': '分隔线',
+    'image': '插入图片',
+    'externalImage': '外链图片',
+    'selectImage': '选择图片',
+    'uploadImage': '上传图片',
+    'uploadFailed': '上传失败了',
+    'uploadError': '上传出错了',
+    'imageUrl': '图片地址',
+    'imageSize': '图片尺寸',
+    'imageAlt': '图片描述',
+    'restoreImageSize': '还原图片尺寸',
+    'uploading': '正在上传',
+    'indent': '向右缩进',
+    'outdent': '向左缩进',
+    'italic': '斜体文字',
+    'link': '插入链接',
+    'linkText': '链接文字',
+    'linkUrl': '链接地址',
+    'linkTarget': '打开方式',
+    'openLinkInCurrentWindow': '在当前窗口中打开',
+    'openLinkInNewWindow': '在新窗口中打开',
+    'removeLink': '移除链接',
+    'ol': '有序列表',
+    'ul': '无序列表',
+    'strikethrough': '删除线文字',
+    'table': '表格',
+    'deleteRow': '删除行',
+    'insertRowAbove': '在上面插入行',
+    'insertRowBelow': '在下面插入行',
+    'deleteColumn': '删除列',
+    'insertColumnLeft': '在左边插入列',
+    'insertColumnRight': '在右边插入列',
+    'deleteTable': '删除表格',
+    'title': '标题',
+    'normalText': '普通文本',
+    'underline': '下划线文字',
+    'alignment': '水平对齐',
+    'alignCenter': '居中',
+    'alignLeft': '居左',
+    'alignRight': '居右',
+    'selectLanguage': '选择程序语言',
+    'fontScale': '字体大小',
+    'fontScaleXLarge': '超大字体',
+    'fontScaleLarge': '大号字体',
+    'fontScaleNormal': '正常大小',
+    'fontScaleSmall': '小号字体',
+    'fontScaleXSmall': '超小字体'
+  },
+  'en-US': {
+    'blockquote': 'Block Quote',
+    'bold': 'Bold',
+    'code': 'Code',
+    'color': 'Text Color',
+    'coloredText': 'Colored Text',
+    'hr': 'Horizontal Line',
+    'image': 'Insert Image',
+    'externalImage': 'External Image',
+    'selectImage': 'Select Image',
+    'uploadImage': 'Upload Image',
+    'uploadFailed': 'Upload failed',
+    'uploadError': 'Error occurs during upload',
+    'imageUrl': 'Url',
+    'imageSize': 'Size',
+    'imageAlt': 'Alt',
+    'restoreImageSize': 'Restore Origin Size',
+    'uploading': 'Uploading',
+    'indent': 'Indent',
+    'outdent': 'Outdent',
+    'italic': 'Italic',
+    'link': 'Insert Link',
+    'linkText': 'Text',
+    'linkUrl': 'Url',
+    'linkTarget': 'Target',
+    'openLinkInCurrentWindow': 'Open link in current window',
+    'openLinkInNewWindow': 'Open link in new window',
+    'removeLink': 'Remove Link',
+    'ol': 'Ordered List',
+    'ul': 'Unordered List',
+    'strikethrough': 'Strikethrough',
+    'table': 'Table',
+    'deleteRow': 'Delete Row',
+    'insertRowAbove': 'Insert Row Above',
+    'insertRowBelow': 'Insert Row Below',
+    'deleteColumn': 'Delete Column',
+    'insertColumnLeft': 'Insert Column Left',
+    'insertColumnRight': 'Insert Column Right',
+    'deleteTable': 'Delete Table',
+    'title': 'Title',
+    'normalText': 'Text',
+    'underline': 'Underline',
+    'alignment': 'Alignment',
+    'alignCenter': 'Align Center',
+    'alignLeft': 'Align Left',
+    'alignRight': 'Align Right',
+    'selectLanguage': 'Select Language',
+    'fontScale': 'Font Size',
+    'fontScaleXLarge': 'X Large Size',
+    'fontScaleLarge': 'Large Size',
+    'fontScaleNormal': 'Normal Size',
+    'fontScaleSmall': 'Small Size',
+    'fontScaleXSmall': 'X Small Size'
+  }
+};
+
+Button = (function(superClass) {
+  extend(Button, superClass);
+
+  Button.prototype._tpl = {
+    item: '<li><a tabindex="-1" unselectable="on" class="toolbar-item" href="javascript:;"><span></span></a></li>',
+    menuWrapper: '<div class="toolbar-menu"></div>',
+    menuItem: '<li><a tabindex="-1" unselectable="on" class="menu-item" href="javascript:;"><span></span></a></li>',
+    separator: '<li><span class="separator"></span></li>'
+  };
+
+  Button.prototype.name = '';
+
+  Button.prototype.icon = '';
+
+  Button.prototype.title = '';
+
+  Button.prototype.text = '';
+
+  Button.prototype.htmlTag = '';
+
+  Button.prototype.disableTag = '';
+
+  Button.prototype.menu = false;
+
+  Button.prototype.active = false;
+
+  Button.prototype.disabled = false;
+
+  Button.prototype.needFocus = true;
+
+  Button.prototype.shortcut = null;
+
+  function Button(opts) {
+    this.editor = opts.editor;
+    this.title = this._t(this.name);
+    Button.__super__.constructor.call(this, opts);
+  }
+
+  Button.prototype._init = function() {
+    var k, len, ref, tag;
+    this.render();
+    this.el.on('mousedown', (function(_this) {
+      return function(e) {
+        var exceed, noFocus, param;
+        e.preventDefault();
+        noFocus = _this.needFocus && !_this.editor.inputManager.focused;
+        if (_this.el.hasClass('disabled')) {
+          return false;
+        }
+        if (noFocus) {
+          _this.editor.focus();
+        }
+        if (_this.menu) {
+          _this.wrapper.toggleClass('menu-on').siblings('li').removeClass('menu-on');
+          if (_this.wrapper.is('.menu-on')) {
+            exceed = _this.menuWrapper.offset().left + _this.menuWrapper.outerWidth() + 5 - _this.editor.wrapper.offset().left - _this.editor.wrapper.outerWidth();
+            if (exceed > 0) {
+              _this.menuWrapper.css({
+                'left': 'auto',
+                'right': 0
+              });
+            }
+            _this.trigger('menuexpand');
+          }
+          return false;
+        }
+        param = _this.el.data('param');
+        _this.command(param);
+        return false;
+      };
+    })(this));
+    this.wrapper.on('click', 'a.menu-item', (function(_this) {
+      return function(e) {
+        var btn, noFocus, param;
+        e.preventDefault();
+        btn = $(e.currentTarget);
+        _this.wrapper.removeClass('menu-on');
+        noFocus = _this.needFocus && !_this.editor.inputManager.focused;
+        if (btn.hasClass('disabled') || noFocus) {
+          return false;
+        }
+        _this.editor.toolbar.wrapper.removeClass('menu-on');
+        param = btn.data('param');
+        if(btn.hasClass("menu-item-select-image")){
+            parent.Fast.api.open("general/attachment/select?element_id=&multiple=true&mimetype=image/*", "选择", {
+                callback: function (data) {
+                    var urlArr = data.url.split(/\,/);
+                    $.each(urlArr, function () {
+                        var url = Fast.api.cdnurl(this);
+                        _this.command(url);
+                    });
+                }
+            });
+            return false;
+        }else{
+          _this.command(param);
+        }
+        return false;
+      };
+    })(this));
+    this.wrapper.on('mousedown', 'a.menu-item', function(e) {
+      return false;
+    });
+    this.editor.on('blur', (function(_this) {
+      return function() {
+        var editorActive;
+        editorActive = _this.editor.body.is(':visible') && _this.editor.body.is('[contenteditable]');
+        if (!(editorActive && !_this.editor.clipboard.pasting)) {
+          return;
+        }
+        _this.setActive(false);
+        return _this.setDisabled(false);
+      };
+    })(this));
+    if (this.shortcut != null) {
+      this.editor.hotkeys.add(this.shortcut, (function(_this) {
+        return function(e) {
+          _this.el.mousedown();
+          return false;
+        };
+      })(this));
+    }
+    ref = this.htmlTag.split(',');
+    for (k = 0, len = ref.length; k < len; k++) {
+      tag = ref[k];
+      tag = $.trim(tag);
+      if (tag && $.inArray(tag, this.editor.formatter._allowedTags) < 0) {
+        this.editor.formatter._allowedTags.push(tag);
+      }
+    }
+    return this.editor.on('selectionchanged', (function(_this) {
+      return function(e) {
+        if (_this.editor.inputManager.focused) {
+          return _this._status();
+        }
+      };
+    })(this));
+  };
+
+  Button.prototype.iconClassOf = function(icon) {
+    if (icon) {
+      return "simditor-icon simditor-icon-" + icon;
+    } else {
+      return '';
+    }
+  };
+
+  Button.prototype.setIcon = function(icon) {
+    return this.el.find('span').removeClass().addClass(this.iconClassOf(icon)).text(this.text);
+  };
+
+  Button.prototype.render = function() {
+    this.wrapper = $(this._tpl.item).appendTo(this.editor.toolbar.list);
+    this.el = this.wrapper.find('a.toolbar-item');
+    this.el.attr('title', this.title).addClass("toolbar-item-" + this.name).data('button', this);
+    this.setIcon(this.icon);
+    if (!this.menu) {
+      return;
+    }
+    this.menuWrapper = $(this._tpl.menuWrapper).appendTo(this.wrapper);
+    this.menuWrapper.addClass("toolbar-menu-" + this.name);
+    return this.renderMenu();
+  };
+
+  Button.prototype.renderMenu = function() {
+    var $menuBtnEl, $menuItemEl, k, len, menuItem, ref, ref1, results;
+    if (!$.isArray(this.menu)) {
+      return;
+    }
+    this.menuEl = $('<ul/>').appendTo(this.menuWrapper);
+    ref = this.menu;
+    results = [];
+    for (k = 0, len = ref.length; k < len; k++) {
+      menuItem = ref[k];
+      if (menuItem === '|') {
+        $(this._tpl.separator).appendTo(this.menuEl);
+        continue;
+      }
+      $menuItemEl = $(this._tpl.menuItem).appendTo(this.menuEl);
+      $menuBtnEl = $menuItemEl.find('a.menu-item').attr({
+        'title': (ref1 = menuItem.title) != null ? ref1 : menuItem.text,
+        'data-param': menuItem.param
+      }).addClass('menu-item-' + menuItem.name);
+      if (menuItem.icon) {
+        results.push($menuBtnEl.find('span').addClass(this.iconClassOf(menuItem.icon)));
+      } else {
+        results.push($menuBtnEl.find('span').text(menuItem.text));
+      }
+    }
+    return results;
+  };
+
+  Button.prototype.setActive = function(active) {
+    if (active === this.active) {
+      return;
+    }
+    this.active = active;
+    return this.el.toggleClass('active', this.active);
+  };
+
+  Button.prototype.setDisabled = function(disabled) {
+    if (disabled === this.disabled) {
+      return;
+    }
+    this.disabled = disabled;
+    return this.el.toggleClass('disabled', this.disabled);
+  };
+
+  Button.prototype._disableStatus = function() {
+    var disabled, endNodes, startNodes;
+    startNodes = this.editor.selection.startNodes();
+    endNodes = this.editor.selection.endNodes();
+    disabled = startNodes.filter(this.disableTag).length > 0 || endNodes.filter(this.disableTag).length > 0;
+    this.setDisabled(disabled);
+    if (this.disabled) {
+      this.setActive(false);
+    }
+    return this.disabled;
+  };
+
+  Button.prototype._activeStatus = function() {
+    var active, endNode, endNodes, startNode, startNodes;
+    startNodes = this.editor.selection.startNodes();
+    endNodes = this.editor.selection.endNodes();
+    startNode = startNodes.filter(this.htmlTag);
+    endNode = endNodes.filter(this.htmlTag);
+    active = startNode.length > 0 && endNode.length > 0 && startNode.is(endNode);
+    this.node = active ? startNode : null;
+    this.setActive(active);
+    return this.active;
+  };
+
+  Button.prototype._status = function() {
+    this._disableStatus();
+    if (this.disabled) {
+      return;
+    }
+    return this._activeStatus();
+  };
+
+  Button.prototype.command = function(param) {};
+
+  Button.prototype._t = function() {
+    var args, ref, result;
+    args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+    result = Button.__super__._t.apply(this, args);
+    if (!result) {
+      result = (ref = this.editor)._t.apply(ref, args);
+    }
+    return result;
+  };
+
+  return Button;
+
+})(SimpleModule);
+
+Simditor.Button = Button;
+
+Popover = (function(superClass) {
+  extend(Popover, superClass);
+
+  Popover.prototype.offset = {
+    top: 4,
+    left: 0
+  };
+
+  Popover.prototype.target = null;
+
+  Popover.prototype.active = false;
+
+  function Popover(opts) {
+    this.button = opts.button;
+    this.editor = opts.button.editor;
+    Popover.__super__.constructor.call(this, opts);
+  }
+
+  Popover.prototype._init = function() {
+    this.el = $('<div class="simditor-popover"></div>').appendTo(this.editor.el).data('popover', this);
+    this.render();
+    this.el.on('mouseenter', (function(_this) {
+      return function(e) {
+        return _this.el.addClass('hover');
+      };
+    })(this));
+    return this.el.on('mouseleave', (function(_this) {
+      return function(e) {
+        return _this.el.removeClass('hover');
+      };
+    })(this));
+  };
+
+  Popover.prototype.render = function() {};
+
+  Popover.prototype._initLabelWidth = function() {
+    var $fields;
+    $fields = this.el.find('.settings-field');
+    if (!($fields.length > 0)) {
+      return;
+    }
+    this._labelWidth = 0;
+    $fields.each((function(_this) {
+      return function(i, field) {
+        var $field, $label;
+        $field = $(field);
+        $label = $field.find('label');
+        if (!($label.length > 0)) {
+          return;
+        }
+        return _this._labelWidth = Math.max(_this._labelWidth, $label.width());
+      };
+    })(this));
+    return $fields.find('label').width(this._labelWidth);
+  };
+
+  Popover.prototype.show = function($target, position) {
+    if (position == null) {
+      position = 'bottom';
+    }
+    if ($target == null) {
+      return;
+    }
+    this.el.siblings('.simditor-popover').each(function(i, popover) {
+      popover = $(popover).data('popover');
+      if (popover.active) {
+        return popover.hide();
+      }
+    });
+    if (this.active && this.target) {
+      this.target.removeClass('selected');
+    }
+    this.target = $target.addClass('selected');
+    if (this.active) {
+      this.refresh(position);
+      return this.trigger('popovershow');
+    } else {
+      this.active = true;
+      this.el.css({
+        left: -9999
+      }).show();
+      if (!this._labelWidth) {
+        this._initLabelWidth();
+      }
+      this.editor.util.reflow();
+      this.refresh(position);
+      return this.trigger('popovershow');
+    }
+  };
+
+  Popover.prototype.hide = function() {
+    if (!this.active) {
+      return;
+    }
+    if (this.target) {
+      this.target.removeClass('selected');
+    }
+    this.target = null;
+    this.active = false;
+    this.el.hide();
+    return this.trigger('popoverhide');
+  };
+
+  Popover.prototype.refresh = function(position) {
+    var editorOffset, left, maxLeft, targetH, targetOffset, top;
+    if (position == null) {
+      position = 'bottom';
+    }
+    if (!this.active) {
+      return;
+    }
+    editorOffset = this.editor.el.offset();
+    targetOffset = this.target.offset();
+    targetH = this.target.outerHeight();
+    if (position === 'bottom') {
+      top = targetOffset.top - editorOffset.top + targetH;
+    } else if (position === 'top') {
+      top = targetOffset.top - editorOffset.top - this.el.height();
+    }
+    maxLeft = this.editor.wrapper.width() - this.el.outerWidth() - 10;
+    left = Math.min(targetOffset.left - editorOffset.left, maxLeft);
+    return this.el.css({
+      top: top + this.offset.top,
+      left: left + this.offset.left
+    });
+  };
+
+  Popover.prototype.destroy = function() {
+    this.target = null;
+    this.active = false;
+    this.editor.off('.linkpopover');
+    return this.el.remove();
+  };
+
+  Popover.prototype._t = function() {
+    var args, ref, result;
+    args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+    result = Popover.__super__._t.apply(this, args);
+    if (!result) {
+      result = (ref = this.button)._t.apply(ref, args);
+    }
+    return result;
+  };
+
+  return Popover;
+
+})(SimpleModule);
+
+Simditor.Popover = Popover;
+
+TitleButton = (function(superClass) {
+  extend(TitleButton, superClass);
+
+  function TitleButton() {
+    return TitleButton.__super__.constructor.apply(this, arguments);
+  }
+
+  TitleButton.prototype.name = 'title';
+
+  TitleButton.prototype.htmlTag = 'h1, h2, h3, h4, h5';
+
+  TitleButton.prototype.disableTag = 'pre, table';
+
+  TitleButton.prototype._init = function() {
+    this.menu = [
+      {
+        name: 'normal',
+        text: this._t('normalText'),
+        param: 'p'
+      }, '|', {
+        name: 'h1',
+        text: this._t('title') + ' 1',
+        param: 'h1'
+      }, {
+        name: 'h2',
+        text: this._t('title') + ' 2',
+        param: 'h2'
+      }, {
+        name: 'h3',
+        text: this._t('title') + ' 3',
+        param: 'h3'
+      }, {
+        name: 'h4',
+        text: this._t('title') + ' 4',
+        param: 'h4'
+      }, {
+        name: 'h5',
+        text: this._t('title') + ' 5',
+        param: 'h5'
+      }
+    ];
+    return TitleButton.__super__._init.call(this);
+  };
+
+  TitleButton.prototype.setActive = function(active, param) {
+    TitleButton.__super__.setActive.call(this, active);
+    if (active) {
+      param || (param = this.node[0].tagName.toLowerCase());
+    }
+    this.el.removeClass('active-p active-h1 active-h2 active-h3 active-h4 active-h5');
+    if (active) {
+      return this.el.addClass('active active-' + param);
+    }
+  };
+
+  TitleButton.prototype.command = function(param) {
+    var $rootNodes;
+    $rootNodes = this.editor.selection.rootNodes();
+    this.editor.selection.save();
+    $rootNodes.each((function(_this) {
+      return function(i, node) {
+        var $node;
+        $node = $(node);
+        if ($node.is('blockquote') || $node.is(param) || $node.is(_this.disableTag) || _this.editor.util.isDecoratedNode($node)) {
+          return;
+        }
+        return $('<' + param + '/>').append($node.contents()).replaceAll($node);
+      };
+    })(this));
+    this.editor.selection.restore();
+    return this.editor.trigger('valuechanged');
+  };
+
+  return TitleButton;
+
+})(Button);
+
+Simditor.Toolbar.addButton(TitleButton);
+
+FontScaleButton = (function(superClass) {
+  extend(FontScaleButton, superClass);
+
+  function FontScaleButton() {
+    return FontScaleButton.__super__.constructor.apply(this, arguments);
+  }
+
+  FontScaleButton.prototype.name = 'fontScale';
+
+  FontScaleButton.prototype.icon = 'font';
+
+  FontScaleButton.prototype.htmlTag = 'span';
+
+  FontScaleButton.prototype.disableTag = 'pre, h1, h2, h3, h4, h5';
+
+  FontScaleButton.prototype.sizeMap = {
+    'x-large': '1.5em',
+    'large': '1.25em',
+    'small': '.75em',
+    'x-small': '.5em'
+  };
+
+  FontScaleButton.prototype._init = function() {
+    this.menu = [
+      {
+        name: '150%',
+        text: this._t('fontScaleXLarge'),
+        param: '5'
+      }, {
+        name: '125%',
+        text: this._t('fontScaleLarge'),
+        param: '4'
+      }, {
+        name: '100%',
+        text: this._t('fontScaleNormal'),
+        param: '3'
+      }, {
+        name: '75%',
+        text: this._t('fontScaleSmall'),
+        param: '2'
+      }, {
+        name: '50%',
+        text: this._t('fontScaleXSmall'),
+        param: '1'
+      }
+    ];
+    return FontScaleButton.__super__._init.call(this);
+  };
+
+  FontScaleButton.prototype._activeStatus = function() {
+    var active, endNode, endNodes, range, startNode, startNodes;
+    range = this.editor.selection.range();
+    startNodes = this.editor.selection.startNodes();
+    endNodes = this.editor.selection.endNodes();
+    startNode = startNodes.filter('span[style*="font-size"]');
+    endNode = endNodes.filter('span[style*="font-size"]');
+    active = startNodes.length > 0 && endNodes.length > 0 && startNode.is(endNode);
+    this.setActive(active);
+    return this.active;
+  };
+
+  FontScaleButton.prototype.command = function(param) {
+    var $scales, containerNode, range;
+    range = this.editor.selection.range();
+    if (range.collapsed) {
+      return;
+    }
+    this.editor.selection.range(range);
+    document.execCommand('styleWithCSS', false, true);
+    document.execCommand('fontSize', false, param);
+    document.execCommand('styleWithCSS', false, false);
+    this.editor.selection.reset();
+    this.editor.selection.range();
+    containerNode = this.editor.selection.containerNode();
+    if (containerNode[0].nodeType === Node.TEXT_NODE) {
+      $scales = containerNode.closest('span[style*="font-size"]');
+    } else {
+      $scales = containerNode.find('span[style*="font-size"]');
+    }
+    $scales.each((function(_this) {
+      return function(i, n) {
+        var $span, size;
+        $span = $(n);
+        size = n.style.fontSize;
+        if (/large|x-large|small|x-small/.test(size)) {
+          return $span.css('fontSize', _this.sizeMap[size]);
+        } else if (size === 'medium') {
+          return $span.replaceWith($span.contents());
+        }
+      };
+    })(this));
+    return this.editor.trigger('valuechanged');
+  };
+
+  return FontScaleButton;
+
+})(Button);
+
+Simditor.Toolbar.addButton(FontScaleButton);
+
+BoldButton = (function(superClass) {
+  extend(BoldButton, superClass);
+
+  function BoldButton() {
+    return BoldButton.__super__.constructor.apply(this, arguments);
+  }
+
+  BoldButton.prototype.name = 'bold';
+
+  BoldButton.prototype.icon = 'bold';
+
+  BoldButton.prototype.htmlTag = 'b, strong';
+
+  BoldButton.prototype.disableTag = 'pre';
+
+  BoldButton.prototype.shortcut = 'cmd+b';
+
+  BoldButton.prototype._init = function() {
+    if (this.editor.util.os.mac) {
+      this.title = this.title + ' ( Cmd + b )';
+    } else {
+      this.title = this.title + ' ( Ctrl + b )';
+      this.shortcut = 'ctrl+b';
+    }
+    return BoldButton.__super__._init.call(this);
+  };
+
+  BoldButton.prototype._activeStatus = function() {
+    var active;
+    active = document.queryCommandState('bold') === true;
+    this.setActive(active);
+    return this.active;
+  };
+
+  BoldButton.prototype.command = function() {
+    document.execCommand('bold');
+    if (!this.editor.util.support.oninput) {
+      this.editor.trigger('valuechanged');
+    }
+    return $(document).trigger('selectionchange');
+  };
+
+  return BoldButton;
+
+})(Button);
+
+Simditor.Toolbar.addButton(BoldButton);
+
+ItalicButton = (function(superClass) {
+  extend(ItalicButton, superClass);
+
+  function ItalicButton() {
+    return ItalicButton.__super__.constructor.apply(this, arguments);
+  }
+
+  ItalicButton.prototype.name = 'italic';
+
+  ItalicButton.prototype.icon = 'italic';
+
+  ItalicButton.prototype.htmlTag = 'i';
+
+  ItalicButton.prototype.disableTag = 'pre';
+
+  ItalicButton.prototype.shortcut = 'cmd+i';
+
+  ItalicButton.prototype._init = function() {
+    if (this.editor.util.os.mac) {
+      this.title = this.title + " ( Cmd + i )";
+    } else {
+      this.title = this.title + " ( Ctrl + i )";
+      this.shortcut = 'ctrl+i';
+    }
+    return ItalicButton.__super__._init.call(this);
+  };
+
+  ItalicButton.prototype._activeStatus = function() {
+    var active;
+    active = document.queryCommandState('italic') === true;
+    this.setActive(active);
+    return this.active;
+  };
+
+  ItalicButton.prototype.command = function() {
+    document.execCommand('italic');
+    if (!this.editor.util.support.oninput) {
+      this.editor.trigger('valuechanged');
+    }
+    return $(document).trigger('selectionchange');
+  };
+
+  return ItalicButton;
+
+})(Button);
+
+Simditor.Toolbar.addButton(ItalicButton);
+
+UnderlineButton = (function(superClass) {
+  extend(UnderlineButton, superClass);
+
+  function UnderlineButton() {
+    return UnderlineButton.__super__.constructor.apply(this, arguments);
+  }
+
+  UnderlineButton.prototype.name = 'underline';
+
+  UnderlineButton.prototype.icon = 'underline';
+
+  UnderlineButton.prototype.htmlTag = 'u';
+
+  UnderlineButton.prototype.disableTag = 'pre';
+
+  UnderlineButton.prototype.shortcut = 'cmd+u';
+
+  UnderlineButton.prototype.render = function() {
+    if (this.editor.util.os.mac) {
+      this.title = this.title + ' ( Cmd + u )';
+    } else {
+      this.title = this.title + ' ( Ctrl + u )';
+      this.shortcut = 'ctrl+u';
+    }
+    return UnderlineButton.__super__.render.call(this);
+  };
+
+  UnderlineButton.prototype._activeStatus = function() {
+    var active;
+    active = document.queryCommandState('underline') === true;
+    this.setActive(active);
+    return this.active;
+  };
+
+  UnderlineButton.prototype.command = function() {
+    document.execCommand('underline');
+    if (!this.editor.util.support.oninput) {
+      this.editor.trigger('valuechanged');
+    }
+    return $(document).trigger('selectionchange');
+  };
+
+  return UnderlineButton;
+
+})(Button);
+
+Simditor.Toolbar.addButton(UnderlineButton);
+
+ColorButton = (function(superClass) {
+  extend(ColorButton, superClass);
+
+  function ColorButton() {
+    return ColorButton.__super__.constructor.apply(this, arguments);
+  }
+
+  ColorButton.prototype.name = 'color';
+
+  ColorButton.prototype.icon = 'tint';
+
+  ColorButton.prototype.disableTag = 'pre';
+
+  ColorButton.prototype.menu = true;
+
+  ColorButton.prototype.render = function() {
+    var args;
+    args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+    return ColorButton.__super__.render.apply(this, args);
+  };
+
+  ColorButton.prototype.renderMenu = function() {
+    $('<ul class="color-list">\n  <li><a href="javascript:;" class="font-color font-color-1"></a></li>\n  <li><a href="javascript:;" class="font-color font-color-2"></a></li>\n  <li><a href="javascript:;" class="font-color font-color-3"></a></li>\n  <li><a href="javascript:;" class="font-color font-color-4"></a></li>\n  <li><a href="javascript:;" class="font-color font-color-5"></a></li>\n  <li><a href="javascript:;" class="font-color font-color-6"></a></li>\n  <li><a href="javascript:;" class="font-color font-color-7"></a></li>\n  <li><a href="javascript:;" class="font-color font-color-default"></a></li>\n</ul>').appendTo(this.menuWrapper);
+    this.menuWrapper.on('mousedown', '.color-list', function(e) {
+      return false;
+    });
+    return this.menuWrapper.on('click', '.font-color', (function(_this) {
+      return function(e) {
+        var $link, $p, hex, range, rgb, textNode;
+        _this.wrapper.removeClass('menu-on');
+        $link = $(e.currentTarget);
+        if ($link.hasClass('font-color-default')) {
+          $p = _this.editor.body.find('p, li');
+          if (!($p.length > 0)) {
+            return;
+          }
+          rgb = window.getComputedStyle($p[0], null).getPropertyValue('color');
+          hex = _this._convertRgbToHex(rgb);
+        } else {
+          rgb = window.getComputedStyle($link[0], null).getPropertyValue('background-color');
+          hex = _this._convertRgbToHex(rgb);
+        }
+        if (!hex) {
+          return;
+        }
+        range = _this.editor.selection.range();
+        if (!$link.hasClass('font-color-default') && range.collapsed) {
+          textNode = document.createTextNode(_this._t('coloredText'));
+          range.insertNode(textNode);
+          range.selectNodeContents(textNode);
+        }
+        _this.editor.selection.range(range);
+        document.execCommand('styleWithCSS', false, true);
+        document.execCommand('foreColor', false, hex);
+        document.execCommand('styleWithCSS', false, false);
+        if (!_this.editor.util.support.oninput) {
+          return _this.editor.trigger('valuechanged');
+        }
+      };
+    })(this));
+  };
+
+  ColorButton.prototype._convertRgbToHex = function(rgb) {
+    var match, re, rgbToHex;
+    re = /rgb\((\d+),\s?(\d+),\s?(\d+)\)/g;
+    match = re.exec(rgb);
+    if (!match) {
+      return '';
+    }
+    rgbToHex = function(r, g, b) {
+      var componentToHex;
+      componentToHex = function(c) {
+        var hex;
+        hex = c.toString(16);
+        if (hex.length === 1) {
+          return '0' + hex;
+        } else {
+          return hex;
+        }
+      };
+      return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
+    };
+    return rgbToHex(match[1] * 1, match[2] * 1, match[3] * 1);
+  };
+
+  return ColorButton;
+
+})(Button);
+
+Simditor.Toolbar.addButton(ColorButton);
+
+ListButton = (function(superClass) {
+  extend(ListButton, superClass);
+
+  function ListButton() {
+    return ListButton.__super__.constructor.apply(this, arguments);
+  }
+
+  ListButton.prototype.type = '';
+
+  ListButton.prototype.disableTag = 'pre, table';
+
+  ListButton.prototype.command = function(param) {
+    var $list, $rootNodes, anotherType;
+    $rootNodes = this.editor.selection.blockNodes();
+    anotherType = this.type === 'ul' ? 'ol' : 'ul';
+    this.editor.selection.save();
+    $list = null;
+    $rootNodes.each((function(_this) {
+      return function(i, node) {
+        var $node;
+        $node = $(node);
+        if ($node.is('blockquote, li') || $node.is(_this.disableTag) || _this.editor.util.isDecoratedNode($node) || !$.contains(document, node)) {
+          return;
+        }
+        if ($node.is(_this.type)) {
+          $node.children('li').each(function(i, li) {
+            var $childList, $li;
+            $li = $(li);
+            $childList = $li.children('ul, ol').insertAfter($node);
+            return $('<p/>').append($(li).html() || _this.editor.util.phBr).insertBefore($node);
+          });
+          return $node.remove();
+        } else if ($node.is(anotherType)) {
+          return $('<' + _this.type + '/>').append($node.contents()).replaceAll($node);
+        } else if ($list && $node.prev().is($list)) {
+          $('<li/>').append($node.html() || _this.editor.util.phBr).appendTo($list);
+          return $node.remove();
+        } else {
+          $list = $("<" + _this.type + "><li></li></" + _this.type + ">");
+          $list.find('li').append($node.html() || _this.editor.util.phBr);
+          return $list.replaceAll($node);
+        }
+      };
+    })(this));
+    this.editor.selection.restore();
+    return this.editor.trigger('valuechanged');
+  };
+
+  return ListButton;
+
+})(Button);
+
+OrderListButton = (function(superClass) {
+  extend(OrderListButton, superClass);
+
+  function OrderListButton() {
+    return OrderListButton.__super__.constructor.apply(this, arguments);
+  }
+
+  OrderListButton.prototype.type = 'ol';
+
+  OrderListButton.prototype.name = 'ol';
+
+  OrderListButton.prototype.icon = 'list-ol';
+
+  OrderListButton.prototype.htmlTag = 'ol';
+
+  OrderListButton.prototype.shortcut = 'cmd+/';
+
+  OrderListButton.prototype._init = function() {
+    if (this.editor.util.os.mac) {
+      this.title = this.title + ' ( Cmd + / )';
+    } else {
+      this.title = this.title + ' ( ctrl + / )';
+      this.shortcut = 'ctrl+/';
+    }
+    return OrderListButton.__super__._init.call(this);
+  };
+
+  return OrderListButton;
+
+})(ListButton);
+
+UnorderListButton = (function(superClass) {
+  extend(UnorderListButton, superClass);
+
+  function UnorderListButton() {
+    return UnorderListButton.__super__.constructor.apply(this, arguments);
+  }
+
+  UnorderListButton.prototype.type = 'ul';
+
+  UnorderListButton.prototype.name = 'ul';
+
+  UnorderListButton.prototype.icon = 'list-ul';
+
+  UnorderListButton.prototype.htmlTag = 'ul';
+
+  UnorderListButton.prototype.shortcut = 'cmd+.';
+
+  UnorderListButton.prototype._init = function() {
+    if (this.editor.util.os.mac) {
+      this.title = this.title + ' ( Cmd + . )';
+    } else {
+      this.title = this.title + ' ( Ctrl + . )';
+      this.shortcut = 'ctrl+.';
+    }
+    return UnorderListButton.__super__._init.call(this);
+  };
+
+  return UnorderListButton;
+
+})(ListButton);
+
+Simditor.Toolbar.addButton(OrderListButton);
+
+Simditor.Toolbar.addButton(UnorderListButton);
+
+BlockquoteButton = (function(superClass) {
+  extend(BlockquoteButton, superClass);
+
+  function BlockquoteButton() {
+    return BlockquoteButton.__super__.constructor.apply(this, arguments);
+  }
+
+  BlockquoteButton.prototype.name = 'blockquote';
+
+  BlockquoteButton.prototype.icon = 'quote-left';
+
+  BlockquoteButton.prototype.htmlTag = 'blockquote';
+
+  BlockquoteButton.prototype.disableTag = 'pre, table';
+
+  BlockquoteButton.prototype.command = function() {
+    var $rootNodes, clearCache, nodeCache;
+    $rootNodes = this.editor.selection.rootNodes();
+    $rootNodes = $rootNodes.filter(function(i, node) {
+      return !$(node).parent().is('blockquote');
+    });
+    this.editor.selection.save();
+    nodeCache = [];
+    clearCache = (function(_this) {
+      return function() {
+        if (nodeCache.length > 0) {
+          $("<" + _this.htmlTag + "/>").insertBefore(nodeCache[0]).append(nodeCache);
+          return nodeCache.length = 0;
+        }
+      };
+    })(this);
+    $rootNodes.each((function(_this) {
+      return function(i, node) {
+        var $node;
+        $node = $(node);
+        if (!$node.parent().is(_this.editor.body)) {
+          return;
+        }
+        if ($node.is(_this.htmlTag)) {
+          clearCache();
+          return $node.children().unwrap();
+        } else if ($node.is(_this.disableTag) || _this.editor.util.isDecoratedNode($node)) {
+          return clearCache();
+        } else {
+          return nodeCache.push(node);
+        }
+      };
+    })(this));
+    clearCache();
+    this.editor.selection.restore();
+    return this.editor.trigger('valuechanged');
+  };
+
+  return BlockquoteButton;
+
+})(Button);
+
+Simditor.Toolbar.addButton(BlockquoteButton);
+
+CodeButton = (function(superClass) {
+  extend(CodeButton, superClass);
+
+  function CodeButton() {
+    return CodeButton.__super__.constructor.apply(this, arguments);
+  }
+
+  CodeButton.prototype.name = 'code';
+
+  CodeButton.prototype.icon = 'code';
+
+  CodeButton.prototype.htmlTag = 'pre';
+
+  CodeButton.prototype.disableTag = 'ul, ol, table';
+
+  CodeButton.prototype._init = function() {
+    CodeButton.__super__._init.call(this);
+    this.editor.on('decorate', (function(_this) {
+      return function(e, $el) {
+        return $el.find('pre').each(function(i, pre) {
+          return _this.decorate($(pre));
+        });
+      };
+    })(this));
+    return this.editor.on('undecorate', (function(_this) {
+      return function(e, $el) {
+        return $el.find('pre').each(function(i, pre) {
+          return _this.undecorate($(pre));
+        });
+      };
+    })(this));
+  };
+
+  CodeButton.prototype.render = function() {
+    var args;
+    args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+    CodeButton.__super__.render.apply(this, args);
+    return this.popover = new CodePopover({
+      button: this
+    });
+  };
+
+  CodeButton.prototype._checkMode = function() {
+    var $blockNodes, range;
+    range = this.editor.selection.range();
+    if (($blockNodes = $(range.cloneContents()).find(this.editor.util.blockNodes.join(','))) > 0 || (range.collapsed && this.editor.selection.startNodes().filter('code').length === 0)) {
+      this.inlineMode = false;
+      return this.htmlTag = 'pre';
+    } else {
+      this.inlineMode = true;
+      return this.htmlTag = 'code';
+    }
+  };
+
+  CodeButton.prototype._status = function() {
+    this._checkMode();
+    CodeButton.__super__._status.call(this);
+    if (this.inlineMode) {
+      return;
+    }
+    if (this.active) {
+      return this.popover.show(this.node);
+    } else {
+      return this.popover.hide();
+    }
+  };
+
+  CodeButton.prototype.decorate = function($pre) {
+    var $code, lang, ref, ref1;
+    $code = $pre.find('> code');
+    if ($code.length > 0) {
+      lang = (ref = $code.attr('class')) != null ? (ref1 = ref.match(/lang-(\S+)/)) != null ? ref1[1] : void 0 : void 0;
+      $code.contents().unwrap();
+      if (lang) {
+        return $pre.attr('data-lang', lang);
+      }
+    }
+  };
+
+  CodeButton.prototype.undecorate = function($pre) {
+    var $code, lang;
+    lang = $pre.attr('data-lang');
+    $code = $('<code/>');
+    if (lang && lang !== -1) {
+      $code.addClass('lang-' + lang);
+    }
+    return $pre.wrapInner($code).removeAttr('data-lang');
+  };
+
+  CodeButton.prototype.command = function() {
+    if (this.inlineMode) {
+      return this._inlineCommand();
+    } else {
+      return this._blockCommand();
+    }
+  };
+
+  CodeButton.prototype._blockCommand = function() {
+    var $rootNodes, clearCache, nodeCache, resultNodes;
+    $rootNodes = this.editor.selection.rootNodes();
+    nodeCache = [];
+    resultNodes = [];
+    clearCache = (function(_this) {
+      return function() {
+        var $pre;
+        if (!(nodeCache.length > 0)) {
+          return;
+        }
+        $pre = $("<" + _this.htmlTag + "/>").insertBefore(nodeCache[0]).text(_this.editor.formatter.clearHtml(nodeCache));
+        resultNodes.push($pre[0]);
+        return nodeCache.length = 0;
+      };
+    })(this);
+    $rootNodes.each((function(_this) {
+      return function(i, node) {
+        var $node, $p;
+        $node = $(node);
+        if ($node.is(_this.htmlTag)) {
+          clearCache();
+          $p = $('<p/>').append($node.html().replace('\n', '<br/>')).replaceAll($node);
+          return resultNodes.push($p[0]);
+        } else if ($node.is(_this.disableTag) || _this.editor.util.isDecoratedNode($node) || $node.is('blockquote')) {
+          return clearCache();
+        } else {
+          return nodeCache.push(node);
+        }
+      };
+    })(this));
+    clearCache();
+    this.editor.selection.setRangeAtEndOf($(resultNodes).last());
+    return this.editor.trigger('valuechanged');
+  };
+
+  CodeButton.prototype._inlineCommand = function() {
+    var $code, $contents, range;
+    range = this.editor.selection.range();
+    if (this.active) {
+      range.selectNodeContents(this.node[0]);
+      this.editor.selection.save(range);
+      this.node.contents().unwrap();
+      this.editor.selection.restore();
+    } else {
+      $contents = $(range.extractContents());
+      $code = $("<" + this.htmlTag + "/>").append($contents.contents());
+      range.insertNode($code[0]);
+      range.selectNodeContents($code[0]);
+      this.editor.selection.range(range);
+    }
+    return this.editor.trigger('valuechanged');
+  };
+
+  return CodeButton;
+
+})(Button);
+
+CodePopover = (function(superClass) {
+  extend(CodePopover, superClass);
+
+  function CodePopover() {
+    return CodePopover.__super__.constructor.apply(this, arguments);
+  }
+
+  CodePopover.prototype.render = function() {
+    var $option, k, lang, len, ref;
+    this._tpl = "<div class=\"code-settings\">\n  <div class=\"settings-field\">\n    <select class=\"select-lang\">\n      <option value=\"-1\">" + (this._t('selectLanguage')) + "</option>\n    </select>\n  </div>\n</div>";
+    this.langs = this.editor.opts.codeLanguages || [
+      {
+        name: 'Bash',
+        value: 'bash'
+      }, {
+        name: 'C++',
+        value: 'c++'
+      }, {
+        name: 'C#',
+        value: 'cs'
+      }, {
+        name: 'CSS',
+        value: 'css'
+      }, {
+        name: 'Erlang',
+        value: 'erlang'
+      }, {
+        name: 'Less',
+        value: 'less'
+      }, {
+        name: 'Sass',
+        value: 'sass'
+      }, {
+        name: 'Diff',
+        value: 'diff'
+      }, {
+        name: 'CoffeeScript',
+        value: 'coffeescript'
+      }, {
+        name: 'HTML,XML',
+        value: 'html'
+      }, {
+        name: 'JSON',
+        value: 'json'
+      }, {
+        name: 'Java',
+        value: 'java'
+      }, {
+        name: 'JavaScript',
+        value: 'js'
+      }, {
+        name: 'Markdown',
+        value: 'markdown'
+      }, {
+        name: 'Objective C',
+        value: 'oc'
+      }, {
+        name: 'PHP',
+        value: 'php'
+      }, {
+        name: 'Perl',
+        value: 'parl'
+      }, {
+        name: 'Python',
+        value: 'python'
+      }, {
+        name: 'Ruby',
+        value: 'ruby'
+      }, {
+        name: 'SQL',
+        value: 'sql'
+      }
+    ];
+    this.el.addClass('code-popover').append(this._tpl);
+    this.selectEl = this.el.find('.select-lang');
+    ref = this.langs;
+    for (k = 0, len = ref.length; k < len; k++) {
+      lang = ref[k];
+      $option = $('<option/>', {
+        text: lang.name,
+        value: lang.value
+      }).appendTo(this.selectEl);
+    }
+    this.selectEl.on('change', (function(_this) {
+      return function(e) {
+        var selected;
+        _this.lang = _this.selectEl.val();
+        selected = _this.target.hasClass('selected');
+        _this.target.removeClass().removeAttr('data-lang');
+        if (_this.lang !== -1) {
+          _this.target.attr('data-lang', _this.lang);
+        }
+        if (selected) {
+          _this.target.addClass('selected');
+        }
+        return _this.editor.trigger('valuechanged');
+      };
+    })(this));
+    return this.editor.on('valuechanged', (function(_this) {
+      return function(e) {
+        if (_this.active) {
+          return _this.refresh();
+        }
+      };
+    })(this));
+  };
+
+  CodePopover.prototype.show = function() {
+    var args;
+    args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+    CodePopover.__super__.show.apply(this, args);
+    this.lang = this.target.attr('data-lang');
+    if (this.lang != null) {
+      return this.selectEl.val(this.lang);
+    } else {
+      return this.selectEl.val(-1);
+    }
+  };
+
+  return CodePopover;
+
+})(Popover);
+
+Simditor.Toolbar.addButton(CodeButton);
+
+LinkButton = (function(superClass) {
+  extend(LinkButton, superClass);
+
+  function LinkButton() {
+    return LinkButton.__super__.constructor.apply(this, arguments);
+  }
+
+  LinkButton.prototype.name = 'link';
+
+  LinkButton.prototype.icon = 'link';
+
+  LinkButton.prototype.htmlTag = 'a';
+
+  LinkButton.prototype.disableTag = 'pre';
+
+  LinkButton.prototype.render = function() {
+    var args;
+    args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+    LinkButton.__super__.render.apply(this, args);
+    return this.popover = new LinkPopover({
+      button: this
+    });
+  };
+
+  LinkButton.prototype._status = function() {
+    LinkButton.__super__._status.call(this);
+    if (this.active && !this.editor.selection.rangeAtEndOf(this.node)) {
+      return this.popover.show(this.node);
+    } else {
+      return this.popover.hide();
+    }
+  };
+
+  LinkButton.prototype.command = function() {
+    var $contents, $link, $newBlock, linkText, range, txtNode;
+    range = this.editor.selection.range();
+    if (this.active) {
+      txtNode = document.createTextNode(this.node.text());
+      this.node.replaceWith(txtNode);
+      range.selectNode(txtNode);
+    } else {
+      $contents = $(range.extractContents());
+      linkText = this.editor.formatter.clearHtml($contents.contents(), false);
+      $link = $('<a/>', {
+        href: '',
+        target: '_blank',
+        text: linkText || this._t('linkText')
+      });
+      if (this.editor.selection.blockNodes().length > 0) {
+        range.insertNode($link[0]);
+      } else {
+        $newBlock = $('<p/>').append($link);
+        range.insertNode($newBlock[0]);
+      }
+      range.selectNodeContents($link[0]);
+      this.popover.one('popovershow', (function(_this) {
+        return function() {
+          if (linkText) {
+            _this.popover.urlEl.focus();
+            return _this.popover.urlEl[0].select();
+          } else {
+            _this.popover.textEl.focus();
+            return _this.popover.textEl[0].select();
+          }
+        };
+      })(this));
+    }
+    this.editor.selection.range(range);
+    return this.editor.trigger('valuechanged');
+  };
+
+  return LinkButton;
+
+})(Button);
+
+LinkPopover = (function(superClass) {
+  extend(LinkPopover, superClass);
+
+  function LinkPopover() {
+    return LinkPopover.__super__.constructor.apply(this, arguments);
+  }
+
+  LinkPopover.prototype.render = function() {
+    var tpl;
+    tpl = "<div class=\"link-settings\">\n  <div class=\"settings-field\">\n    <label>" + (this._t('linkText')) + "</label>\n    <input class=\"link-text\" type=\"text\"/>\n    <a class=\"btn-unlink\" href=\"javascript:;\" title=\"" + (this._t('removeLink')) + "\"\n      tabindex=\"-1\">\n      <span class=\"simditor-icon simditor-icon-unlink\"></span>\n    </a>\n  </div>\n  <div class=\"settings-field\">\n    <label>" + (this._t('linkUrl')) + "</label>\n    <input class=\"link-url\" type=\"text\"/>\n  </div>\n  <div class=\"settings-field\">\n    <label>" + (this._t('linkTarget')) + "</label>\n    <select class=\"link-target\">\n      <option value=\"_blank\">" + (this._t('openLinkInNewWindow')) + " (_blank)</option>\n      <option value=\"_self\">" + (this._t('openLinkInCurrentWindow')) + " (_self)</option>\n    </select>\n  </div>\n</div>";
+    this.el.addClass('link-popover').append(tpl);
+    this.textEl = this.el.find('.link-text');
+    this.urlEl = this.el.find('.link-url');
+    this.unlinkEl = this.el.find('.btn-unlink');
+    this.selectTarget = this.el.find('.link-target');
+    this.textEl.on('keyup', (function(_this) {
+      return function(e) {
+        if (e.which === 13) {
+          return;
+        }
+        _this.target.text(_this.textEl.val());
+        return _this.editor.inputManager.throttledValueChanged();
+      };
+    })(this));
+    this.urlEl.on('keyup', (function(_this) {
+      return function(e) {
+        var val;
+        if (e.which === 13) {
+          return;
+        }
+        val = _this.urlEl.val();
+        if (!(/https?:\/\/|^\//ig.test(val) || !val)) {
+          val = 'http://' + val;
+        }
+        _this.target.attr('href', val);
+        return _this.editor.inputManager.throttledValueChanged();
+      };
+    })(this));
+    $([this.urlEl[0], this.textEl[0]]).on('keydown', (function(_this) {
+      return function(e) {
+        var range;
+        if (e.which === 13 || e.which === 27 || (!e.shiftKey && e.which === 9 && $(e.target).hasClass('link-url'))) {
+          e.preventDefault();
+          range = document.createRange();
+          _this.editor.selection.setRangeAfter(_this.target, range);
+          _this.hide();
+          return _this.editor.inputManager.throttledValueChanged();
+        }
+      };
+    })(this));
+    this.unlinkEl.on('click', (function(_this) {
+      return function(e) {
+        var range, txtNode;
+        txtNode = document.createTextNode(_this.target.text());
+        _this.target.replaceWith(txtNode);
+        _this.hide();
+        range = document.createRange();
+        _this.editor.selection.setRangeAfter(txtNode, range);
+        return _this.editor.inputManager.throttledValueChanged();
+      };
+    })(this));
+    return this.selectTarget.on('change', (function(_this) {
+      return function(e) {
+        _this.target.attr('target', _this.selectTarget.val());
+        return _this.editor.inputManager.throttledValueChanged();
+      };
+    })(this));
+  };
+
+  LinkPopover.prototype.show = function() {
+    var args;
+    args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+    LinkPopover.__super__.show.apply(this, args);
+    this.textEl.val(this.target.text());
+    return this.urlEl.val(this.target.attr('href'));
+  };
+
+  return LinkPopover;
+
+})(Popover);
+
+Simditor.Toolbar.addButton(LinkButton);
+
+ImageButton = (function(superClass) {
+  extend(ImageButton, superClass);
+
+  function ImageButton() {
+    return ImageButton.__super__.constructor.apply(this, arguments);
+  }
+
+  ImageButton.prototype.name = 'image';
+
+  ImageButton.prototype.icon = 'picture-o';
+
+  ImageButton.prototype.htmlTag = 'img';
+
+  ImageButton.prototype.disableTag = 'pre, table';
+
+  ImageButton.prototype.defaultImage = '';
+
+  ImageButton.prototype.needFocus = false;
+
+  ImageButton.prototype._init = function() {
+    var item, k, len, ref;
+    if (this.editor.opts.imageButton) {
+      if (Array.isArray(this.editor.opts.imageButton)) {
+        this.menu = [];
+        ref = this.editor.opts.imageButton;
+        for (k = 0, len = ref.length; k < len; k++) {
+          item = ref[k];
+          this.menu.push({
+            name: item + '-image',
+            text: this._t(item + 'Image')
+          });
+        }
+      } else {
+        this.menu = false;
+      }
+    } else {
+      if (this.editor.uploader != null) {
+        this.menu = [
+          {
+            name: 'upload-image',
+            text: this._t('uploadImage')
+          }, {
+            name: 'external-image',
+            text: this._t('externalImage')
+          }, {
+              name: 'select-image',
+              text: this._t('selectImage')
+          }
+        ];
+      } else {
+        this.menu = false;
+      }
+    }
+    this.defaultImage = this.editor.opts.defaultImage;
+    this.editor.body.on('click', 'img:not([data-non-image])', (function(_this) {
+      return function(e) {
+        var $img, range;
+        $img = $(e.currentTarget);
+        range = document.createRange();
+        range.selectNode($img[0]);
+        _this.editor.selection.range(range);
+        if (!_this.editor.util.support.onselectionchange) {
+          _this.editor.trigger('selectionchanged');
+        }
+        return false;
+      };
+    })(this));
+    this.editor.body.on('mouseup', 'img:not([data-non-image])', function(e) {
+      return false;
+    });
+    this.editor.on('selectionchanged.image', (function(_this) {
+      return function() {
+        var $contents, $img, range;
+        range = _this.editor.selection.range();
+        if (range == null) {
+          return;
+        }
+        $contents = $(range.cloneContents()).contents();
+        if ($contents.length === 1 && $contents.is('img:not([data-non-image])')) {
+          $img = $(range.startContainer).contents().eq(range.startOffset);
+          return _this.popover.show($img);
+        } else {
+          return _this.popover.hide();
+        }
+      };
+    })(this));
+    this.editor.on('valuechanged.image', (function(_this) {
+      return function() {
+        var $masks;
+        $masks = _this.editor.wrapper.find('.simditor-image-loading');
+        if (!($masks.length > 0)) {
+          return;
+        }
+        return $masks.each(function(i, mask) {
+          var $img, $mask, file;
+          $mask = $(mask);
+          $img = $mask.data('img');
+          if (!($img && $img.parent().length > 0)) {
+            $mask.remove();
+            if ($img) {
+              file = $img.data('file');
+              if (file) {
+                _this.editor.uploader.cancel(file);
+                if (_this.editor.body.find('img.uploading').length < 1) {
+                  return _this.editor.uploader.trigger('uploadready', [file]);
+                }
+              }
+            }
+          }
+        });
+      };
+    })(this));
+    return ImageButton.__super__._init.call(this);
+  };
+
+  ImageButton.prototype.render = function() {
+    var args;
+    args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+    ImageButton.__super__.render.apply(this, args);
+    this.popover = new ImagePopover({
+      button: this
+    });
+    if (this.editor.opts.imageButton === 'upload') {
+      return this._initUploader(this.el);
+    }
+  };
+
+  ImageButton.prototype.renderMenu = function() {
+    ImageButton.__super__.renderMenu.call(this);
+    return this._initUploader();
+  };
+
+  ImageButton.prototype._initUploader = function($uploadItem) {
+    var $input, createInput, uploadProgress;
+    if ($uploadItem == null) {
+      $uploadItem = this.menuEl.find('.menu-item-upload-image');
+    }
+    if (this.editor.uploader == null) {
+      this.el.find('.btn-upload').remove();
+      return;
+    }
+    $input = null;
+    createInput = (function(_this) {
+      return function() {
+        if ($input) {
+          $input.remove();
+        }
+        return $input = $('<input/>', {
+          type: 'file',
+          title: _this._t('uploadImage'),
+          multiple: true,
+          accept: 'image/gif,image/jpeg,image/jpg,image/png,image/svg'
+        }).appendTo($uploadItem);
+      };
+    })(this);
+    createInput();
+    $uploadItem.on('click mousedown', 'input[type=file]', function(e) {
+      return e.stopPropagation();
+    });
+    $uploadItem.on('change', 'input[type=file]', (function(_this) {
+      return function(e) {
+        if (_this.editor.inputManager.focused) {
+          _this.editor.uploader.upload($input, {
+            inline: true
+          });
+          createInput();
+        } else {
+          _this.editor.one('focus', function(e) {
+            _this.editor.uploader.upload($input, {
+              inline: true
+            });
+            return createInput();
+          });
+          _this.editor.focus();
+        }
+        return _this.wrapper.removeClass('menu-on');
+      };
+    })(this));
+    this.editor.uploader.on('beforeupload', (function(_this) {
+      return function(e, file) {
+        var $img;
+        if (!file.inline) {
+          return;
+        }
+        if (file.img) {
+          $img = $(file.img);
+        } else {
+          $img = _this.createImage(file.name);
+          file.img = $img;
+        }
+        $img.addClass('uploading');
+        $img.data('file', file);
+        return _this.editor.uploader.readImageFile(file.obj, function(img) {
+          var src;
+          if (!$img.hasClass('uploading')) {
+            return;
+          }
+          src = img ? img.src : _this.defaultImage;
+          return _this.loadImage($img, src, function() {
+            if (_this.popover.active) {
+              _this.popover.refresh();
+              return _this.popover.srcEl.val(_this._t('uploading')).prop('disabled', true);
+            }
+          });
+        });
+      };
+    })(this));
+    uploadProgress = $.proxy(this.editor.util.throttle(function(e, file, loaded, total) {
+      var $img, $mask, percent;
+      if (!file.inline) {
+        return;
+      }
+      $mask = file.img.data('mask');
+      if (!$mask) {
+        return;
+      }
+      $img = $mask.data('img');
+      if (!($img.hasClass('uploading') && $img.parent().length > 0)) {
+        $mask.remove();
+        return;
+      }
+      percent = loaded / total;
+      percent = (percent * 100).toFixed(0);
+      if (percent > 99) {
+        percent = 99;
+      }
+      return $mask.find('.progress').height((100 - percent) + "%");
+    }, 500), this);
+    this.editor.uploader.on('uploadprogress', uploadProgress);
+    this.editor.uploader.on('uploadsuccess', (function(_this) {
+      return function(e, file, result) {
+        var $img, img_path, msg;
+        if (!file.inline) {
+          return;
+        }
+        $img = file.img;
+        if (!($img.hasClass('uploading') && $img.parent().length > 0)) {
+          return;
+        }
+        if (typeof result !== 'object') {
+          try {
+            result = $.parseJSON(result);
+          } catch (_error) {
+            e = _error;
+            result = {
+              success: false
+            };
+          }
+        }
+        if (result.success === false) {
+          msg = result.msg || _this._t('uploadFailed');
+          alert(msg);
+          img_path = _this.defaultImage;
+        } else {
+          img_path = result.file_path;
+        }
+        _this.loadImage($img, img_path, function() {
+          var $mask;
+          $img.removeData('file');
+          $img.removeClass('uploading').removeClass('loading');
+          $mask = $img.data('mask');
+          if ($mask) {
+            $mask.remove();
+          }
+          $img.removeData('mask');
+          _this.editor.trigger('valuechanged');
+          if (_this.editor.body.find('img.uploading').length < 1) {
+            return _this.editor.uploader.trigger('uploadready', [file, result]);
+          }
+        });
+        if (_this.popover.active) {
+          _this.popover.srcEl.prop('disabled', false);
+          return _this.popover.srcEl.val(result.file_path);
+        }
+      };
+    })(this));
+    return this.editor.uploader.on('uploaderror', (function(_this) {
+      return function(e, file, xhr) {
+        var $img, msg, result;
+        if (!file.inline) {
+          return;
+        }
+        if (xhr.statusText === 'abort') {
+          return;
+        }
+        if (xhr.responseText) {
+          try {
+            result = $.parseJSON(xhr.responseText);
+            msg = result.msg;
+          } catch (_error) {
+            e = _error;
+            msg = _this._t('uploadError');
+          }
+        }
+        $img = file.img;
+        if (!($img.hasClass('uploading') && $img.parent().length > 0)) {
+          return;
+        }
+        _this.loadImage($img, _this.defaultImage, function() {
+          var $mask;
+          $img.removeData('file');
+          $img.removeClass('uploading').removeClass('loading');
+          $mask = $img.data('mask');
+          if ($mask) {
+            $mask.remove();
+          }
+          return $img.removeData('mask');
+        });
+        if (_this.popover.active) {
+          _this.popover.srcEl.prop('disabled', false);
+          _this.popover.srcEl.val(_this.defaultImage);
+        }
+        _this.editor.trigger('valuechanged');
+        if (_this.editor.body.find('img.uploading').length < 1) {
+          return _this.editor.uploader.trigger('uploadready', [file, result]);
+        }
+      };
+    })(this));
+  };
+
+  ImageButton.prototype._status = function() {
+    return this._disableStatus();
+  };
+
+  ImageButton.prototype.loadImage = function($img, src, callback) {
+    var $mask, img, positionMask;
+    positionMask = (function(_this) {
+      return function() {
+        var imgOffset, wrapperOffset;
+        imgOffset = $img.offset();
+        wrapperOffset = _this.editor.wrapper.offset();
+        return $mask.css({
+          top: imgOffset.top - wrapperOffset.top,
+          left: imgOffset.left - wrapperOffset.left,
+          width: $img.width(),
+          height: $img.height()
+        }).show();
+      };
+    })(this);
+    $img.addClass('loading');
+    $mask = $img.data('mask');
+    if (!$mask) {
+      $mask = $('<div class="simditor-image-loading">\n  <div class="progress"></div>\n</div>').hide().appendTo(this.editor.wrapper);
+      positionMask();
+      $img.data('mask', $mask);
+      $mask.data('img', $img);
+    }
+    img = new Image();
+    img.onload = (function(_this) {
+      return function() {
+        var height, width;
+        if (!$img.hasClass('loading') && !$img.hasClass('uploading')) {
+          return;
+        }
+        width = img.width;
+        height = img.height;
+        $img.attr({
+          src: src,
+          width: width,
+          height: height,
+          'data-image-size': width + ',' + height
+        }).removeClass('loading');
+        if ($img.hasClass('uploading')) {
+          _this.editor.util.reflow(_this.editor.body);
+          positionMask();
+        } else {
+          $mask.remove();
+          $img.removeData('mask');
+        }
+        if ($.isFunction(callback)) {
+          return callback(img);
+        }
+      };
+    })(this);
+    img.onerror = function() {
+      if ($.isFunction(callback)) {
+        callback(false);
+      }
+      $mask.remove();
+      return $img.removeData('mask').removeClass('loading');
+    };
+    return img.src = src;
+  };
+
+  ImageButton.prototype.createImage = function(name) {
+    var $img, range;
+    if (name == null) {
+      name = 'Image';
+    }
+    if (!this.editor.inputManager.focused) {
+      this.editor.focus();
+    }
+    range = this.editor.selection.range();
+    range.deleteContents();
+    this.editor.selection.range(range);
+    $img = $('<img/>').attr('alt', name);
+    range.insertNode($img[0]);
+    this.editor.selection.setRangeAfter($img, range);
+    this.editor.trigger('valuechanged');
+    return $img;
+  };
+
+  ImageButton.prototype.command = function(src) {
+    var $img;
+
+    $img = this.createImage();
+    return this.loadImage($img, src || this.defaultImage, (function(_this) {
+      return function() {
+        _this.editor.trigger('valuechanged');
+        _this.editor.util.reflow($img);
+        $img.click();
+        return _this.popover.one('popovershow', function() {
+          _this.popover.srcEl.focus();
+          return _this.popover.srcEl[0].select();
+        });
+      };
+    })(this));
+  };
+
+  return ImageButton;
+
+})(Button);
+
+ImagePopover = (function(superClass) {
+  extend(ImagePopover, superClass);
+
+  function ImagePopover() {
+    return ImagePopover.__super__.constructor.apply(this, arguments);
+  }
+
+  ImagePopover.prototype.offset = {
+    top: 6,
+    left: -4
+  };
+
+  ImagePopover.prototype.render = function() {
+    var tpl;
+    tpl = "<div class=\"link-settings\">\n  <div class=\"settings-field\">\n    <label>" + (this._t('imageUrl')) + "</label>\n    <input class=\"image-src\" type=\"text\" tabindex=\"1\" />\n    <a class=\"btn-upload\" href=\"javascript:;\"\n      title=\"" + (this._t('uploadImage')) + "\" tabindex=\"-1\">\n      <span class=\"simditor-icon simditor-icon-upload\"></span>\n    </a>\n  </div>\n  <div class='settings-field'>\n    <label>" + (this._t('imageAlt')) + "</label>\n    <input class=\"image-alt\" id=\"image-alt\" type=\"text\" tabindex=\"1\" />\n  </div>\n  <div class=\"settings-field\">\n    <label>" + (this._t('imageSize')) + "</label>\n    <input class=\"image-size\" id=\"image-width\" type=\"text\" tabindex=\"2\" />\n    <span class=\"times\">×</span>\n    <input class=\"image-size\" id=\"image-height\" type=\"text\" tabindex=\"3\" />\n    <a class=\"btn-restore\" href=\"javascript:;\"\n      title=\"" + (this._t('restoreImageSize')) + "\" tabindex=\"-1\">\n      <span class=\"simditor-icon simditor-icon-undo\"></span>\n    </a>\n  </div>\n</div>";
+    this.el.addClass('image-popover').append(tpl);
+    this.srcEl = this.el.find('.image-src');
+    this.widthEl = this.el.find('#image-width');
+    this.heightEl = this.el.find('#image-height');
+    this.altEl = this.el.find('#image-alt');
+    this.srcEl.on('keydown', (function(_this) {
+      return function(e) {
+        var range;
+        if (!(e.which === 13 && !_this.target.hasClass('uploading'))) {
+          return;
+        }
+        e.preventDefault();
+        range = document.createRange();
+        _this.button.editor.selection.setRangeAfter(_this.target, range);
+        return _this.hide();
+      };
+    })(this));
+    this.srcEl.on('blur', (function(_this) {
+      return function(e) {
+        return _this._loadImage(_this.srcEl.val());
+      };
+    })(this));
+    this.el.find('.image-size').on('blur', (function(_this) {
+      return function(e) {
+        _this._resizeImg($(e.currentTarget));
+        return _this.el.data('popover').refresh();
+      };
+    })(this));
+    this.el.find('.image-size').on('keyup', (function(_this) {
+      return function(e) {
+        var inputEl;
+        inputEl = $(e.currentTarget);
+        if (!(e.which === 13 || e.which === 27 || e.which === 9)) {
+          return _this._resizeImg(inputEl, true);
+        }
+      };
+    })(this));
+    this.el.find('.image-size').on('keydown', (function(_this) {
+      return function(e) {
+        var $img, inputEl, range;
+        inputEl = $(e.currentTarget);
+        if (e.which === 13 || e.which === 27) {
+          e.preventDefault();
+          if (e.which === 13) {
+            _this._resizeImg(inputEl);
+          } else {
+            _this._restoreImg();
+          }
+          $img = _this.target;
+          _this.hide();
+          range = document.createRange();
+          return _this.button.editor.selection.setRangeAfter($img, range);
+        } else if (e.which === 9) {
+          return _this.el.data('popover').refresh();
+        }
+      };
+    })(this));
+    this.altEl.on('keydown', (function(_this) {
+      return function(e) {
+        var range;
+        if (e.which === 13) {
+          e.preventDefault();
+          range = document.createRange();
+          _this.button.editor.selection.setRangeAfter(_this.target, range);
+          return _this.hide();
+        }
+      };
+    })(this));
+    this.altEl.on('keyup', (function(_this) {
+      return function(e) {
+        if (e.which === 13 || e.which === 27 || e.which === 9) {
+          return;
+        }
+        _this.alt = _this.altEl.val();
+        return _this.target.attr('alt', _this.alt);
+      };
+    })(this));
+    this.el.find('.btn-restore').on('click', (function(_this) {
+      return function(e) {
+        _this._restoreImg();
+        return _this.el.data('popover').refresh();
+      };
+    })(this));
+    this.editor.on('valuechanged', (function(_this) {
+      return function(e) {
+        if (_this.active) {
+          return _this.refresh();
+        }
+      };
+    })(this));
+    return this._initUploader();
+  };
+
+  ImagePopover.prototype._initUploader = function() {
+    var $uploadBtn, createInput;
+    $uploadBtn = this.el.find('.btn-upload');
+    if (this.editor.uploader == null) {
+      $uploadBtn.remove();
+      return;
+    }
+    createInput = (function(_this) {
+      return function() {
+        if (_this.input) {
+          _this.input.remove();
+        }
+        return _this.input = $('<input/>', {
+          type: 'file',
+          title: _this._t('uploadImage'),
+          multiple: true,
+          accept: 'image/gif,image/jpeg,image/jpg,image/png,image/svg'
+        }).appendTo($uploadBtn);
+      };
+    })(this);
+    createInput();
+    this.el.on('click mousedown', 'input[type=file]', function(e) {
+      return e.stopPropagation();
+    });
+    return this.el.on('change', 'input[type=file]', (function(_this) {
+      return function(e) {
+        _this.editor.uploader.upload(_this.input, {
+          inline: true,
+          img: _this.target
+        });
+        return createInput();
+      };
+    })(this));
+  };
+
+  ImagePopover.prototype._resizeImg = function(inputEl, onlySetVal) {
+    var height, value, width;
+    if (onlySetVal == null) {
+      onlySetVal = false;
+    }
+    value = inputEl.val() * 1;
+    if (!(this.target && ($.isNumeric(value) || value < 0))) {
+      return;
+    }
+    if (inputEl.is(this.widthEl)) {
+      width = value;
+      height = this.height * value / this.width;
+      this.heightEl.val(height);
+    } else {
+      height = value;
+      width = this.width * value / this.height;
+      this.widthEl.val(width);
+    }
+    if (!onlySetVal) {
+      this.target.attr({
+        width: width,
+        height: height
+      });
+      return this.editor.trigger('valuechanged');
+    }
+  };
+
+  ImagePopover.prototype._restoreImg = function() {
+    var ref, size;
+    size = ((ref = this.target.data('image-size')) != null ? ref.split(",") : void 0) || [this.width, this.height];
+    this.target.attr({
+      width: size[0] * 1,
+      height: size[1] * 1
+    });
+    this.widthEl.val(size[0]);
+    this.heightEl.val(size[1]);
+    return this.editor.trigger('valuechanged');
+  };
+
+  ImagePopover.prototype._loadImage = function(src, callback) {
+    if (/^data:image/.test(src) && !this.editor.uploader) {
+      if (callback) {
+        callback(false);
+      }
+      return;
+    }
+    if (this.target.attr('src') === src) {
+      return;
+    }
+    return this.button.loadImage(this.target, src, (function(_this) {
+
+      return function(img) {
+        var blob;
+        if (!img) {
+          return;
+        }
+        if (_this.active) {
+          _this.width = img.width;
+          _this.height = img.height;
+          _this.widthEl.val(_this.width);
+          _this.heightEl.val(_this.height);
+        }
+        if (/^data:image/.test(src)) {
+          blob = _this.editor.util.dataURLtoBlob(src);
+          blob.name = "Base64 Image.png";
+          _this.editor.uploader.upload(blob, {
+            inline: true,
+            img: _this.target
+          });
+        } else {
+          _this.editor.trigger('valuechanged');
+        }
+        if (callback) {
+          return callback(img);
+        }
+      };
+    })(this));
+  };
+
+  ImagePopover.prototype.show = function() {
+    var $img, args;
+    args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+    ImagePopover.__super__.show.apply(this, args);
+    $img = this.target;
+    this.width = $img.width();
+    this.height = $img.height();
+    this.alt = $img.attr('alt');
+    if ($img.hasClass('uploading')) {
+      return this.srcEl.val(this._t('uploading')).prop('disabled', true);
+    } else {
+      this.srcEl.val($img.attr('src')).prop('disabled', false);
+      this.widthEl.val(this.width);
+      this.heightEl.val(this.height);
+      return this.altEl.val(this.alt);
+    }
+  };
+
+  return ImagePopover;
+
+})(Popover);
+
+Simditor.Toolbar.addButton(ImageButton);
+
+IndentButton = (function(superClass) {
+  extend(IndentButton, superClass);
+
+  function IndentButton() {
+    return IndentButton.__super__.constructor.apply(this, arguments);
+  }
+
+  IndentButton.prototype.name = 'indent';
+
+  IndentButton.prototype.icon = 'indent';
+
+  IndentButton.prototype._init = function() {
+    var hotkey;
+    hotkey = this.editor.opts.tabIndent === false ? '' : ' (Tab)';
+    this.title = this._t(this.name) + hotkey;
+    return IndentButton.__super__._init.call(this);
+  };
+
+  IndentButton.prototype._status = function() {};
+
+  IndentButton.prototype.command = function() {
+    return this.editor.indentation.indent();
+  };
+
+  return IndentButton;
+
+})(Button);
+
+Simditor.Toolbar.addButton(IndentButton);
+
+OutdentButton = (function(superClass) {
+  extend(OutdentButton, superClass);
+
+  function OutdentButton() {
+    return OutdentButton.__super__.constructor.apply(this, arguments);
+  }
+
+  OutdentButton.prototype.name = 'outdent';
+
+  OutdentButton.prototype.icon = 'outdent';
+
+  OutdentButton.prototype._init = function() {
+    var hotkey;
+    hotkey = this.editor.opts.tabIndent === false ? '' : ' (Shift + Tab)';
+    this.title = this._t(this.name) + hotkey;
+    return OutdentButton.__super__._init.call(this);
+  };
+
+  OutdentButton.prototype._status = function() {};
+
+  OutdentButton.prototype.command = function() {
+    return this.editor.indentation.indent(true);
+  };
+
+  return OutdentButton;
+
+})(Button);
+
+Simditor.Toolbar.addButton(OutdentButton);
+
+HrButton = (function(superClass) {
+  extend(HrButton, superClass);
+
+  function HrButton() {
+    return HrButton.__super__.constructor.apply(this, arguments);
+  }
+
+  HrButton.prototype.name = 'hr';
+
+  HrButton.prototype.icon = 'minus';
+
+  HrButton.prototype.htmlTag = 'hr';
+
+  HrButton.prototype._status = function() {};
+
+  HrButton.prototype.command = function() {
+    var $hr, $newBlock, $nextBlock, $rootBlock;
+    $rootBlock = this.editor.selection.rootNodes().first();
+    $nextBlock = $rootBlock.next();
+    if ($nextBlock.length > 0) {
+      this.editor.selection.save();
+    } else {
+      $newBlock = $('<p/>').append(this.editor.util.phBr);
+    }
+    $hr = $('<hr/>').insertAfter($rootBlock);
+    if ($newBlock) {
+      $newBlock.insertAfter($hr);
+      this.editor.selection.setRangeAtStartOf($newBlock);
+    } else {
+      this.editor.selection.restore();
+    }
+    return this.editor.trigger('valuechanged');
+  };
+
+  return HrButton;
+
+})(Button);
+
+Simditor.Toolbar.addButton(HrButton);
+
+TableButton = (function(superClass) {
+  extend(TableButton, superClass);
+
+  function TableButton() {
+    return TableButton.__super__.constructor.apply(this, arguments);
+  }
+
+  TableButton.prototype.name = 'table';
+
+  TableButton.prototype.icon = 'table';
+
+  TableButton.prototype.htmlTag = 'table';
+
+  TableButton.prototype.disableTag = 'pre, li, blockquote';
+
+  TableButton.prototype.menu = true;
+
+  TableButton.prototype._init = function() {
+    TableButton.__super__._init.call(this);
+    $.merge(this.editor.formatter._allowedTags, ['thead', 'th', 'tbody', 'tr', 'td', 'colgroup', 'col']);
+    $.extend(this.editor.formatter._allowedAttributes, {
+      td: ['rowspan', 'colspan'],
+      col: ['width']
+    });
+    $.extend(this.editor.formatter._allowedStyles, {
+      td: ['text-align'],
+      th: ['text-align']
+    });
+    this._initShortcuts();
+    this.editor.on('decorate', (function(_this) {
+      return function(e, $el) {
+        return $el.find('table').each(function(i, table) {
+          return _this.decorate($(table));
+        });
+      };
+    })(this));
+    this.editor.on('undecorate', (function(_this) {
+      return function(e, $el) {
+        return $el.find('table').each(function(i, table) {
+          return _this.undecorate($(table));
+        });
+      };
+    })(this));
+    this.editor.on('selectionchanged.table', (function(_this) {
+      return function(e) {
+        var $container, range;
+        _this.editor.body.find('.simditor-table td, .simditor-table th').removeClass('active');
+        range = _this.editor.selection.range();
+        if (!range) {
+          return;
+        }
+        $container = _this.editor.selection.containerNode();
+        if (range.collapsed && $container.is('.simditor-table')) {
+          if (_this.editor.selection.rangeAtStartOf($container)) {
+            $container = $container.find('th:first');
+          } else {
+            $container = $container.find('td:last');
+          }
+          _this.editor.selection.setRangeAtEndOf($container);
+        }
+        return $container.closest('td, th', _this.editor.body).addClass('active');
+      };
+    })(this));
+    this.editor.on('blur.table', (function(_this) {
+      return function(e) {
+        return _this.editor.body.find('.simditor-table td, .simditor-table th').removeClass('active');
+      };
+    })(this));
+    this.editor.keystroke.add('up', 'td', (function(_this) {
+      return function(e, $node) {
+        _this._tdNav($node, 'up');
+        return true;
+      };
+    })(this));
+    this.editor.keystroke.add('up', 'th', (function(_this) {
+      return function(e, $node) {
+        _this._tdNav($node, 'up');
+        return true;
+      };
+    })(this));
+    this.editor.keystroke.add('down', 'td', (function(_this) {
+      return function(e, $node) {
+        _this._tdNav($node, 'down');
+        return true;
+      };
+    })(this));
+    return this.editor.keystroke.add('down', 'th', (function(_this) {
+      return function(e, $node) {
+        _this._tdNav($node, 'down');
+        return true;
+      };
+    })(this));
+  };
+
+  TableButton.prototype._tdNav = function($td, direction) {
+    var $anotherTr, $tr, action, anotherTag, index, parentTag, ref;
+    if (direction == null) {
+      direction = 'up';
+    }
+    action = direction === 'up' ? 'prev' : 'next';
+    ref = direction === 'up' ? ['tbody', 'thead'] : ['thead', 'tbody'], parentTag = ref[0], anotherTag = ref[1];
+    $tr = $td.parent('tr');
+    $anotherTr = this["_" + action + "Row"]($tr);
+    if (!($anotherTr.length > 0)) {
+      return true;
+    }
+    index = $tr.find('td, th').index($td);
+    return this.editor.selection.setRangeAtEndOf($anotherTr.find('td, th').eq(index));
+  };
+
+  TableButton.prototype._nextRow = function($tr) {
+    var $nextTr;
+    $nextTr = $tr.next('tr');
+    if ($nextTr.length < 1 && $tr.parent('thead').length > 0) {
+      $nextTr = $tr.parent('thead').next('tbody').find('tr:first');
+    }
+    return $nextTr;
+  };
+
+  TableButton.prototype._prevRow = function($tr) {
+    var $prevTr;
+    $prevTr = $tr.prev('tr');
+    if ($prevTr.length < 1 && $tr.parent('tbody').length > 0) {
+      $prevTr = $tr.parent('tbody').prev('thead').find('tr');
+    }
+    return $prevTr;
+  };
+
+  TableButton.prototype.initResize = function($table) {
+    var $colgroup, $editor, $resizeHandle, $wrapper;
+    $wrapper = $table.parent('.simditor-table');
+    $editor = this.editor;
+    $colgroup = $table.find('colgroup');
+    if ($colgroup.length < 1) {
+      $colgroup = $('<colgroup/>').prependTo($table);
+      $table.find('thead tr th').each(function(i, td) {
+        var $col;
+        return $col = $('<col/>').appendTo($colgroup);
+      });
+      this.refreshTableWidth($table);
+    }
+    $resizeHandle = $('<div />', {
+      "class": 'simditor-resize-handle',
+      contenteditable: 'false'
+    }).appendTo($wrapper);
+    $wrapper.on('mousemove', 'td, th', function(e) {
+      var $col, $td, index, ref, ref1, x;
+      if ($wrapper.hasClass('resizing')) {
+        return;
+      }
+      $td = $(e.currentTarget);
+      x = e.pageX - $(e.currentTarget).offset().left;
+      if (x < 5 && $td.prev().length > 0) {
+        $td = $td.prev();
+      }
+      if ($td.next('td, th').length < 1) {
+        $resizeHandle.hide();
+        return;
+      }
+      if ((ref = $resizeHandle.data('td')) != null ? ref.is($td) : void 0) {
+        $resizeHandle.show();
+        return;
+      }
+      index = $td.parent().find('td, th').index($td);
+      $col = $colgroup.find('col').eq(index);
+      if ((ref1 = $resizeHandle.data('col')) != null ? ref1.is($col) : void 0) {
+        $resizeHandle.show();
+        return;
+      }
+      return $resizeHandle.css('left', $td.position().left + $td.outerWidth() - 5).data('td', $td).data('col', $col).show();
+    });
+    $wrapper.on('mouseleave', function(e) {
+      return $resizeHandle.hide();
+    });
+    return $wrapper.on('mousedown', '.simditor-resize-handle', function(e) {
+      var $handle, $leftCol, $leftTd, $rightCol, $rightTd, minWidth, startHandleLeft, startLeftWidth, startRightWidth, startX, tableWidth;
+      $handle = $(e.currentTarget);
+      $leftTd = $handle.data('td');
+      $leftCol = $handle.data('col');
+      $rightTd = $leftTd.next('td, th');
+      $rightCol = $leftCol.next('col');
+      startX = e.pageX;
+      startLeftWidth = $leftTd.outerWidth() * 1;
+      startRightWidth = $rightTd.outerWidth() * 1;
+      startHandleLeft = parseFloat($handle.css('left'));
+      tableWidth = $leftTd.closest('table').width();
+      minWidth = 50;
+      $(document).on('mousemove.simditor-resize-table', function(e) {
+        var deltaX, leftWidth, rightWidth;
+        deltaX = e.pageX - startX;
+        leftWidth = startLeftWidth + deltaX;
+        rightWidth = startRightWidth - deltaX;
+        if (leftWidth < minWidth) {
+          leftWidth = minWidth;
+          deltaX = minWidth - startLeftWidth;
+          rightWidth = startRightWidth - deltaX;
+        } else if (rightWidth < minWidth) {
+          rightWidth = minWidth;
+          deltaX = startRightWidth - minWidth;
+          leftWidth = startLeftWidth + deltaX;
+        }
+        $leftCol.attr('width', (leftWidth / tableWidth * 100) + '%');
+        $rightCol.attr('width', (rightWidth / tableWidth * 100) + '%');
+        return $handle.css('left', startHandleLeft + deltaX);
+      });
+      $(document).one('mouseup.simditor-resize-table', function(e) {
+        $editor.sync();
+        $(document).off('.simditor-resize-table');
+        return $wrapper.removeClass('resizing');
+      });
+      $wrapper.addClass('resizing');
+      return false;
+    });
+  };
+
+  TableButton.prototype._initShortcuts = function() {
+    this.editor.hotkeys.add('ctrl+alt+up', (function(_this) {
+      return function(e) {
+        _this.editMenu.find('.menu-item[data-param=insertRowAbove]').click();
+        return false;
+      };
+    })(this));
+    this.editor.hotkeys.add('ctrl+alt+down', (function(_this) {
+      return function(e) {
+        _this.editMenu.find('.menu-item[data-param=insertRowBelow]').click();
+        return false;
+      };
+    })(this));
+    this.editor.hotkeys.add('ctrl+alt+left', (function(_this) {
+      return function(e) {
+        _this.editMenu.find('.menu-item[data-param=insertColLeft]').click();
+        return false;
+      };
+    })(this));
+    return this.editor.hotkeys.add('ctrl+alt+right', (function(_this) {
+      return function(e) {
+        _this.editMenu.find('.menu-item[data-param=insertColRight]').click();
+        return false;
+      };
+    })(this));
+  };
+
+  TableButton.prototype.decorate = function($table) {
+    var $headRow, $tbody, $thead;
+    if ($table.parent('.simditor-table').length > 0) {
+      this.undecorate($table);
+    }
+    $table.wrap('<div class="simditor-table"></div>');
+    if ($table.find('thead').length < 1) {
+      $thead = $('<thead />');
+      $headRow = $table.find('tr').first();
+      $thead.append($headRow);
+      this._changeCellTag($headRow, 'th');
+      $tbody = $table.find('tbody');
+      if ($tbody.length > 0) {
+        $tbody.before($thead);
+      } else {
+        $table.prepend($thead);
+      }
+    }
+    this.initResize($table);
+    return $table.parent();
+  };
+
+  TableButton.prototype.undecorate = function($table) {
+    if (!($table.parent('.simditor-table').length > 0)) {
+      return;
+    }
+    return $table.parent().replaceWith($table);
+  };
+
+  TableButton.prototype.renderMenu = function() {
+    var $table;
+    $("<div class=\"menu-create-table\">\n</div>\n<div class=\"menu-edit-table\">\n  <ul>\n    <li>\n      <a tabindex=\"-1\" unselectable=\"on\" class=\"menu-item\"\n        href=\"javascript:;\" data-param=\"deleteRow\">\n        <span>" + (this._t('deleteRow')) + "</span>\n      </a>\n    </li>\n    <li>\n      <a tabindex=\"-1\" unselectable=\"on\" class=\"menu-item\"\n        href=\"javascript:;\" data-param=\"insertRowAbove\">\n        <span>" + (this._t('insertRowAbove')) + " ( Ctrl + Alt + ↑ )</span>\n      </a>\n    </li>\n    <li>\n      <a tabindex=\"-1\" unselectable=\"on\" class=\"menu-item\"\n        href=\"javascript:;\" data-param=\"insertRowBelow\">\n        <span>" + (this._t('insertRowBelow')) + " ( Ctrl + Alt + ↓ )</span>\n      </a>\n    </li>\n    <li><span class=\"separator\"></span></li>\n    <li>\n      <a tabindex=\"-1\" unselectable=\"on\" class=\"menu-item\"\n        href=\"javascript:;\" data-param=\"deleteCol\">\n        <span>" + (this._t('deleteColumn')) + "</span>\n      </a>\n    </li>\n    <li>\n      <a tabindex=\"-1\" unselectable=\"on\" class=\"menu-item\"\n        href=\"javascript:;\" data-param=\"insertColLeft\">\n        <span>" + (this._t('insertColumnLeft')) + " ( Ctrl + Alt + ← )</span>\n      </a>\n    </li>\n    <li>\n      <a tabindex=\"-1\" unselectable=\"on\" class=\"menu-item\"\n        href=\"javascript:;\" data-param=\"insertColRight\">\n        <span>" + (this._t('insertColumnRight')) + " ( Ctrl + Alt + → )</span>\n      </a>\n    </li>\n    <li><span class=\"separator\"></span></li>\n    <li>\n      <a tabindex=\"-1\" unselectable=\"on\" class=\"menu-item\"\n        href=\"javascript:;\" data-param=\"deleteTable\">\n        <span>" + (this._t('deleteTable')) + "</span>\n      </a>\n    </li>\n  </ul>\n</div>").appendTo(this.menuWrapper);
+    this.createMenu = this.menuWrapper.find('.menu-create-table');
+    this.editMenu = this.menuWrapper.find('.menu-edit-table');
+    $table = this.createTable(6, 6).appendTo(this.createMenu);
+    this.createMenu.on('mouseenter', 'td, th', (function(_this) {
+      return function(e) {
+        var $td, $tr, $trs, num;
+        _this.createMenu.find('td, th').removeClass('selected');
+        $td = $(e.currentTarget);
+        $tr = $td.parent();
+        num = $tr.find('td, th').index($td) + 1;
+        $trs = $tr.prevAll('tr').addBack();
+        if ($tr.parent().is('tbody')) {
+          $trs = $trs.add($table.find('thead tr'));
+        }
+        return $trs.find("td:lt(" + num + "), th:lt(" + num + ")").addClass('selected');
+      };
+    })(this));
+    this.createMenu.on('mouseleave', function(e) {
+      return $(e.currentTarget).find('td, th').removeClass('selected');
+    });
+    return this.createMenu.on('mousedown', 'td, th', (function(_this) {
+      return function(e) {
+        var $closestBlock, $td, $tr, colNum, rowNum;
+        _this.wrapper.removeClass('menu-on');
+        if (!_this.editor.inputManager.focused) {
+          return;
+        }
+        $td = $(e.currentTarget);
+        $tr = $td.parent();
+        colNum = $tr.find('td').index($td) + 1;
+        rowNum = $tr.prevAll('tr').length + 1;
+        if ($tr.parent().is('tbody')) {
+          rowNum += 1;
+        }
+        $table = _this.createTable(rowNum, colNum, true);
+        $closestBlock = _this.editor.selection.blockNodes().last();
+        if (_this.editor.util.isEmptyNode($closestBlock)) {
+          $closestBlock.replaceWith($table);
+        } else {
+          $closestBlock.after($table);
+        }
+        _this.decorate($table);
+        _this.editor.selection.setRangeAtStartOf($table.find('th:first'));
+        _this.editor.trigger('valuechanged');
+        return false;
+      };
+    })(this));
+  };
+
+  TableButton.prototype.createTable = function(row, col, phBr) {
+    var $table, $tbody, $td, $thead, $tr, c, k, l, r, ref, ref1;
+    $table = $('<table/>');
+    $thead = $('<thead/>').appendTo($table);
+    $tbody = $('<tbody/>').appendTo($table);
+    for (r = k = 0, ref = row; 0 <= ref ? k < ref : k > ref; r = 0 <= ref ? ++k : --k) {
+      $tr = $('<tr/>');
+      $tr.appendTo(r === 0 ? $thead : $tbody);
+      for (c = l = 0, ref1 = col; 0 <= ref1 ? l < ref1 : l > ref1; c = 0 <= ref1 ? ++l : --l) {
+        $td = $(r === 0 ? '<th/>' : '<td/>').appendTo($tr);
+        if (phBr) {
+          $td.append(this.editor.util.phBr);
+        }
+      }
+    }
+    return $table;
+  };
+
+  TableButton.prototype.refreshTableWidth = function($table) {
+    return setTimeout((function(_this) {
+      return function() {
+        var cols, tableWidth;
+        tableWidth = $table.width();
+        cols = $table.find('col');
+        return $table.find('thead tr th').each(function(i, td) {
+          var $col;
+          $col = cols.eq(i);
+          return $col.attr('width', ($(td).outerWidth() / tableWidth * 100) + '%');
+        });
+      };
+    })(this), 0);
+  };
+
+  TableButton.prototype.setActive = function(active) {
+    TableButton.__super__.setActive.call(this, active);
+    if (active) {
+      this.createMenu.hide();
+      return this.editMenu.show();
+    } else {
+      this.createMenu.show();
+      return this.editMenu.hide();
+    }
+  };
+
+  TableButton.prototype._changeCellTag = function($tr, tagName) {
+    return $tr.find('td, th').each(function(i, cell) {
+      var $cell;
+      $cell = $(cell);
+      return $cell.replaceWith("<" + tagName + ">" + ($cell.html()) + "</" + tagName + ">");
+    });
+  };
+
+  TableButton.prototype.deleteRow = function($td) {
+    var $newTr, $tr, index;
+    $tr = $td.parent('tr');
+    if ($tr.closest('table').find('tr').length < 1) {
+      return this.deleteTable($td);
+    } else {
+      $newTr = this._nextRow($tr);
+      if (!($newTr.length > 0)) {
+        $newTr = this._prevRow($tr);
+      }
+      index = $tr.find('td, th').index($td);
+      if ($tr.parent().is('thead')) {
+        $newTr.appendTo($tr.parent());
+        this._changeCellTag($newTr, 'th');
+      }
+      $tr.remove();
+      return this.editor.selection.setRangeAtEndOf($newTr.find('td, th').eq(index));
+    }
+  };
+
+  TableButton.prototype.insertRow = function($td, direction) {
+    var $newTr, $table, $tr, cellTag, colNum, i, index, k, ref;
+    if (direction == null) {
+      direction = 'after';
+    }
+    $tr = $td.parent('tr');
+    $table = $tr.closest('table');
+    colNum = 0;
+    $table.find('tr').each(function(i, tr) {
+      return colNum = Math.max(colNum, $(tr).find('td').length);
+    });
+    index = $tr.find('td, th').index($td);
+    $newTr = $('<tr/>');
+    cellTag = 'td';
+    if (direction === 'after' && $tr.parent().is('thead')) {
+      $tr.parent().next('tbody').prepend($newTr);
+    } else if (direction === 'before' && $tr.parent().is('thead')) {
+      $tr.before($newTr);
+      $tr.parent().next('tbody').prepend($tr);
+      this._changeCellTag($tr, 'td');
+      cellTag = 'th';
+    } else {
+      $tr[direction]($newTr);
+    }
+    for (i = k = 1, ref = colNum; 1 <= ref ? k <= ref : k >= ref; i = 1 <= ref ? ++k : --k) {
+      $("<" + cellTag + "/>").append(this.editor.util.phBr).appendTo($newTr);
+    }
+    return this.editor.selection.setRangeAtStartOf($newTr.find('td, th').eq(index));
+  };
+
+  TableButton.prototype.deleteCol = function($td) {
+    var $newTd, $table, $tr, index, noOtherCol, noOtherRow;
+    $tr = $td.parent('tr');
+    noOtherRow = $tr.closest('table').find('tr').length < 2;
+    noOtherCol = $td.siblings('td, th').length < 1;
+    if (noOtherRow && noOtherCol) {
+      return this.deleteTable($td);
+    } else {
+      index = $tr.find('td, th').index($td);
+      $newTd = $td.next('td, th');
+      if (!($newTd.length > 0)) {
+        $newTd = $tr.prev('td, th');
+      }
+      $table = $tr.closest('table');
+      $table.find('col').eq(index).remove();
+      $table.find('tr').each(function(i, tr) {
+        return $(tr).find('td, th').eq(index).remove();
+      });
+      this.refreshTableWidth($table);
+      return this.editor.selection.setRangeAtEndOf($newTd);
+    }
+  };
+
+  TableButton.prototype.insertCol = function($td, direction) {
+    var $col, $newCol, $newTd, $table, $tr, index, tableWidth, width;
+    if (direction == null) {
+      direction = 'after';
+    }
+    $tr = $td.parent('tr');
+    index = $tr.find('td, th').index($td);
+    $table = $td.closest('table');
+    $col = $table.find('col').eq(index);
+    $table.find('tr').each((function(_this) {
+      return function(i, tr) {
+        var $newTd, cellTag;
+        cellTag = $(tr).parent().is('thead') ? 'th' : 'td';
+        $newTd = $("<" + cellTag + "/>").append(_this.editor.util.phBr);
+        return $(tr).find('td, th').eq(index)[direction]($newTd);
+      };
+    })(this));
+    $newCol = $('<col/>');
+    $col[direction]($newCol);
+    tableWidth = $table.width();
+    width = Math.max(parseFloat($col.attr('width')) / 2, 50 / tableWidth * 100);
+    $col.attr('width', width + '%');
+    $newCol.attr('width', width + '%');
+    this.refreshTableWidth($table);
+    $newTd = direction === 'after' ? $td.next('td, th') : $td.prev('td, th');
+    return this.editor.selection.setRangeAtStartOf($newTd);
+  };
+
+  TableButton.prototype.deleteTable = function($td) {
+    var $block, $table;
+    $table = $td.closest('.simditor-table');
+    $block = $table.next('p');
+    $table.remove();
+    if ($block.length > 0) {
+      return this.editor.selection.setRangeAtStartOf($block);
+    }
+  };
+
+  TableButton.prototype.command = function(param) {
+    var $td;
+    $td = this.editor.selection.containerNode().closest('td, th');
+    if (!($td.length > 0)) {
+      return;
+    }
+    if (param === 'deleteRow') {
+      this.deleteRow($td);
+    } else if (param === 'insertRowAbove') {
+      this.insertRow($td, 'before');
+    } else if (param === 'insertRowBelow') {
+      this.insertRow($td);
+    } else if (param === 'deleteCol') {
+      this.deleteCol($td);
+    } else if (param === 'insertColLeft') {
+      this.insertCol($td, 'before');
+    } else if (param === 'insertColRight') {
+      this.insertCol($td);
+    } else if (param === 'deleteTable') {
+      this.deleteTable($td);
+    } else {
+      return;
+    }
+    return this.editor.trigger('valuechanged');
+  };
+
+  return TableButton;
+
+})(Button);
+
+Simditor.Toolbar.addButton(TableButton);
+
+StrikethroughButton = (function(superClass) {
+  extend(StrikethroughButton, superClass);
+
+  function StrikethroughButton() {
+    return StrikethroughButton.__super__.constructor.apply(this, arguments);
+  }
+
+  StrikethroughButton.prototype.name = 'strikethrough';
+
+  StrikethroughButton.prototype.icon = 'strikethrough';
+
+  StrikethroughButton.prototype.htmlTag = 'strike';
+
+  StrikethroughButton.prototype.disableTag = 'pre';
+
+  StrikethroughButton.prototype._activeStatus = function() {
+    var active;
+    active = document.queryCommandState('strikethrough') === true;
+    this.setActive(active);
+    return this.active;
+  };
+
+  StrikethroughButton.prototype.command = function() {
+    document.execCommand('strikethrough');
+    if (!this.editor.util.support.oninput) {
+      this.editor.trigger('valuechanged');
+    }
+    return $(document).trigger('selectionchange');
+  };
+
+  return StrikethroughButton;
+
+})(Button);
+
+Simditor.Toolbar.addButton(StrikethroughButton);
+
+AlignmentButton = (function(superClass) {
+  extend(AlignmentButton, superClass);
+
+  function AlignmentButton() {
+    return AlignmentButton.__super__.constructor.apply(this, arguments);
+  }
+
+  AlignmentButton.prototype.name = "alignment";
+
+  AlignmentButton.prototype.icon = 'align-left';
+
+  AlignmentButton.prototype.htmlTag = 'p, h1, h2, h3, h4, td, th';
+
+  AlignmentButton.prototype._init = function() {
+    this.menu = [
+      {
+        name: 'left',
+        text: this._t('alignLeft'),
+        icon: 'align-left',
+        param: 'left'
+      }, {
+        name: 'center',
+        text: this._t('alignCenter'),
+        icon: 'align-center',
+        param: 'center'
+      }, {
+        name: 'right',
+        text: this._t('alignRight'),
+        icon: 'align-right',
+        param: 'right'
+      }
+    ];
+    return AlignmentButton.__super__._init.call(this);
+  };
+
+  AlignmentButton.prototype.setActive = function(active, align) {
+    if (align == null) {
+      align = 'left';
+    }
+    if (align !== 'left' && align !== 'center' && align !== 'right') {
+      align = 'left';
+    }
+    if (align === 'left') {
+      AlignmentButton.__super__.setActive.call(this, false);
+    } else {
+      AlignmentButton.__super__.setActive.call(this, active);
+    }
+    this.el.removeClass('align-left align-center align-right');
+    if (active) {
+      this.el.addClass('align-' + align);
+    }
+    this.setIcon('align-' + align);
+    return this.menuEl.find('.menu-item').show().end().find('.menu-item-' + align).hide();
+  };
+
+  AlignmentButton.prototype._status = function() {
+    this.nodes = this.editor.selection.nodes().filter(this.htmlTag);
+    if (this.nodes.length < 1) {
+      this.setDisabled(true);
+      return this.setActive(false);
+    } else {
+      this.setDisabled(false);
+      return this.setActive(true, this.nodes.first().css('text-align'));
+    }
+  };
+
+  AlignmentButton.prototype.command = function(align) {
+    if (align !== 'left' && align !== 'center' && align !== 'right') {
+      throw new Error("simditor alignment button: invalid align " + align);
+    }
+    this.nodes.css({
+      'text-align': align === 'left' ? '' : align
+    });
+    this.editor.trigger('valuechanged');
+    return this.editor.inputManager.throttledSelectionChanged();
+  };
+
+  return AlignmentButton;
+
+})(Button);
+
+Simditor.Toolbar.addButton(AlignmentButton);
+
+return Simditor;
+
+}));

+ 261 - 0
addons/simditor/src/js/uploader.js

@@ -0,0 +1,261 @@
+(function (root, factory) {
+  if (typeof define === 'function' && define.amd) {
+    // AMD. Register as an anonymous module unless amdModuleId is set
+    define('simple-uploader', ["jquery","simple-module"], function ($, SimpleModule) {
+      return (root['uploader'] = factory($, SimpleModule));
+    });
+  } else if (typeof exports === 'object') {
+    // Node. Does not work with strict CommonJS, but
+    // only CommonJS-like environments that support module.exports,
+    // like Node.
+    module.exports = factory(require("jquery"),require("simple-module"));
+  } else {
+    root.simple = root.simple || {};
+    root.simple['uploader'] = factory(jQuery,SimpleModule);
+  }
+}(this, function ($, SimpleModule) {
+
+var Uploader, uploader,
+  extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+  hasProp = {}.hasOwnProperty;
+
+Uploader = (function(superClass) {
+  extend(Uploader, superClass);
+
+  function Uploader() {
+    return Uploader.__super__.constructor.apply(this, arguments);
+  }
+
+  Uploader.count = 0;
+
+  Uploader.prototype.opts = {
+    url: '',
+    params: null,
+    fileKey: 'upload_file',
+    connectionCount: 3
+  };
+
+  Uploader.prototype._init = function() {
+    this.files = [];
+    this.queue = [];
+    this.id = ++Uploader.count;
+    this.on('uploadcomplete', (function(_this) {
+      return function(e, file) {
+        _this.files.splice($.inArray(file, _this.files), 1);
+        if (_this.queue.length > 0 && _this.files.length < _this.opts.connectionCount) {
+          return _this.upload(_this.queue.shift());
+        } else if (_this.files.length === 0) {
+          return _this.uploading = false;
+        }
+      };
+    })(this));
+    return $(window).on('beforeunload.uploader-' + this.id, (function(_this) {
+      return function(e) {
+        if (!_this.uploading) {
+          return;
+        }
+        e.originalEvent.returnValue = _this._t('leaveConfirm');
+        return _this._t('leaveConfirm');
+      };
+    })(this));
+  };
+
+  Uploader.prototype.generateId = (function() {
+    var id;
+    id = 0;
+    return function() {
+      return id += 1;
+    };
+  })();
+
+  Uploader.prototype.upload = function(file, opts) {
+    var f, i, key, len;
+    if (opts == null) {
+      opts = {};
+    }
+    if (file == null) {
+      return;
+    }
+    if ($.isArray(file) || file instanceof FileList) {
+      for (i = 0, len = file.length; i < len; i++) {
+        f = file[i];
+        this.upload(f, opts);
+      }
+    } else if ($(file).is('input:file')) {
+      key = $(file).attr('name');
+      if (key) {
+        opts.fileKey = key;
+      }
+      this.upload($.makeArray($(file)[0].files), opts);
+    } else if (!file.id || !file.obj) {
+      file = this.getFile(file);
+    }
+    if (!(file && file.obj)) {
+      return;
+    }
+    $.extend(file, opts);
+    if (this.files.length >= this.opts.connectionCount) {
+      this.queue.push(file);
+      return;
+    }
+    if (this.triggerHandler('beforeupload', [file]) === false) {
+      return;
+    }
+    this.files.push(file);
+    this._xhrUpload(file);
+    return this.uploading = true;
+  };
+
+  Uploader.prototype.getFile = function(fileObj) {
+    var name, ref, ref1;
+    if (fileObj instanceof window.File || fileObj instanceof window.Blob) {
+      name = (ref = fileObj.fileName) != null ? ref : fileObj.name;
+    } else {
+      return null;
+    }
+    return {
+      id: this.generateId(),
+      url: this.opts.url,
+      params: this.opts.params,
+      fileKey: this.opts.fileKey,
+      name: name,
+      size: (ref1 = fileObj.fileSize) != null ? ref1 : fileObj.size,
+      ext: name ? name.split('.').pop().toLowerCase() : '',
+      obj: fileObj
+    };
+  };
+
+  Uploader.prototype._xhrUpload = function(file) {
+    var formData, k, ref, v;
+    formData = new FormData();
+    formData.append(file.fileKey, file.obj);
+    formData.append("original_filename", file.name);
+    if (file.params) {
+      ref = file.params;
+      for (k in ref) {
+        v = ref[k];
+        formData.append(k, v);
+      }
+    }
+    return file.xhr = $.ajax({
+      url: file.url,
+      data: formData,
+      processData: false,
+      contentType: false,
+      type: 'POST',
+      headers: {
+        'X-File-Name': encodeURIComponent(file.name)
+      },
+      xhr: function() {
+        var req;
+        req = $.ajaxSettings.xhr();
+        if (req) {
+          req.upload.onprogress = (function(_this) {
+            return function(e) {
+              return _this.progress(e);
+            };
+          })(this);
+        }
+        return req;
+      },
+      progress: (function(_this) {
+        return function(e) {
+          if (!e.lengthComputable) {
+            return;
+          }
+          return _this.trigger('uploadprogress', [file, e.loaded, e.total]);
+        };
+      })(this),
+      error: (function(_this) {
+        return function(xhr, status, err) {
+          return _this.trigger('uploaderror', [file, xhr, status]);
+        };
+      })(this),
+      success: (function(_this) {
+        return function(result) {
+          _this.trigger('uploadprogress', [file, file.size, file.size]);
+          _this.trigger('uploadsuccess', [file, result]);
+          return $(document).trigger('uploadsuccess', [file, result, _this]);
+        };
+      })(this),
+      complete: (function(_this) {
+        return function(xhr, status) {
+          return _this.trigger('uploadcomplete', [file, xhr.responseText]);
+        };
+      })(this)
+    });
+  };
+
+  Uploader.prototype.cancel = function(file) {
+    var f, i, len, ref;
+    if (!file.id) {
+      ref = this.files;
+      for (i = 0, len = ref.length; i < len; i++) {
+        f = ref[i];
+        if (f.id === file * 1) {
+          file = f;
+          break;
+        }
+      }
+    }
+    this.trigger('uploadcancel', [file]);
+    if (file.xhr) {
+      file.xhr.abort();
+    }
+    return file.xhr = null;
+  };
+
+  Uploader.prototype.readImageFile = function(fileObj, callback) {
+    var fileReader, img;
+    if (!$.isFunction(callback)) {
+      return;
+    }
+    img = new Image();
+    img.onload = function() {
+      return callback(img);
+    };
+    img.onerror = function() {
+      return callback();
+    };
+    if (window.FileReader && FileReader.prototype.readAsDataURL && /^image/.test(fileObj.type)) {
+      fileReader = new FileReader();
+      fileReader.onload = function(e) {
+        return img.src = e.target.result;
+      };
+      return fileReader.readAsDataURL(fileObj);
+    } else {
+      return callback();
+    }
+  };
+
+  Uploader.prototype.destroy = function() {
+    var file, i, len, ref;
+    this.queue.length = 0;
+    ref = this.files;
+    for (i = 0, len = ref.length; i < len; i++) {
+      file = ref[i];
+      this.cancel(file);
+    }
+    $(window).off('.uploader-' + this.id);
+    return $(document).off('.uploader-' + this.id);
+  };
+
+  Uploader.i18n = {
+    'zh-CN': {
+      leaveConfirm: '正在上传文件,如果离开上传会自动取消'
+    }
+  };
+
+  Uploader.locale = 'zh-CN';
+
+  return Uploader;
+
+})(SimpleModule);
+
+uploader = function(opts) {
+  return new Uploader(opts);
+};
+
+return uploader;
+
+}));

+ 1 - 0
addons/summernote/.addonrc

@@ -0,0 +1 @@
+{"license":"regular","licenseto":"5946","licensekey":"Vm2flBCLbRg4GsFd Bk5v99Af4QqYk\/cxMTeSt7WBCz2ryEKsGdyzfopO1Lw="}

+ 31 - 0
addons/summernote/Summernote.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace addons\summernote;
+
+use think\Addons;
+
+/**
+ * Summernote富文本编辑器
+ */
+class Summernote extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        return true;
+    }
+
+}

File diff suppressed because it is too large
+ 0 - 0
addons/summernote/assets/css/summernote.css


BIN
addons/summernote/assets/font/summernote.eot


BIN
addons/summernote/assets/font/summernote.ttf


Some files were not shown because too many files changed in this diff