diff --git a/assets/.eslintrc.yaml b/assets/.eslintrc.yaml new file mode 100644 index 00000000..91aaf4c8 --- /dev/null +++ b/assets/.eslintrc.yaml @@ -0,0 +1,37 @@ +extends: + - eslint:recommended + - plugin:react/recommended + - plugin:@typescript-eslint/recommended +parser: '@typescript-eslint/parser' +plugins: + - react + - react-hooks + - '@typescript-eslint' +parserOptions: + sourceType: module + ecmaVersion: 2020 + ecmaFeatures: + jsx: true +env: + es6: true + browser: true + node: true + jest: true + +settings: + react: + version: detect +ignorePatterns: + - node_modules +rules: + react/prop-types: 0 + react-hooks/rules-of-hooks: "error" + # TODO: 修改添加deps后出现的死循环 + react-hooks/exhaustive-deps: 0 + '@typescript-eslint/explicit-function-return-type': 0 + '@typescript-eslint/no-explicit-any': 0 + +overrides: + - files: ['*.js', '*.jsx'] + rules: + '@typescript-eslint/camelcase': 0 diff --git a/assets/.gitignore b/assets/.gitignore new file mode 100644 index 00000000..911e1eef --- /dev/null +++ b/assets/.gitignore @@ -0,0 +1,25 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +dist/ \ No newline at end of file diff --git a/assets/.huskyrc b/assets/.huskyrc new file mode 100644 index 00000000..ecd15357 --- /dev/null +++ b/assets/.huskyrc @@ -0,0 +1 @@ +export PATH="/usr/local/bin:$PATH" diff --git a/assets/.prettierrc b/assets/.prettierrc new file mode 100644 index 00000000..5fcd8a70 --- /dev/null +++ b/assets/.prettierrc @@ -0,0 +1,3 @@ +{ + "tabWidth": 4 +} diff --git a/assets/.travis.yml b/assets/.travis.yml new file mode 100644 index 00000000..1519c100 --- /dev/null +++ b/assets/.travis.yml @@ -0,0 +1,8 @@ +language: node_js +node_js: + - 12.16.3 +before_script: + - yarn install +script: + - CI=false yarn run build + - yarn run test \ No newline at end of file diff --git a/assets/README.md b/assets/README.md new file mode 100644 index 00000000..89b278ae --- /dev/null +++ b/assets/README.md @@ -0,0 +1,68 @@ +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `yarn start` + +Runs the app in the development mode.
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.
+You will also see any lint errors in the console. + +### `yarn test` + +Launches the test runner in the interactive watch mode.
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `yarn build` + +Builds the app for production to the `build` folder.
+It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
+Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `yarn eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). + +### Code Splitting + +This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting + +### Analyzing the Bundle Size + +This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size + +### Making a Progressive Web App + +This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app + +### Advanced Configuration + +This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration + +### Deployment + +This section has moved here: https://facebook.github.io/create-react-app/docs/deployment + +### `yarn build` fails to minify + +This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify diff --git a/assets/config/env.js b/assets/config/env.js new file mode 100644 index 00000000..211711b2 --- /dev/null +++ b/assets/config/env.js @@ -0,0 +1,93 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const paths = require('./paths'); + +// Make sure that including paths.js after env.js will read .env variables. +delete require.cache[require.resolve('./paths')]; + +const NODE_ENV = process.env.NODE_ENV; +if (!NODE_ENV) { + throw new Error( + 'The NODE_ENV environment variable is required but was not specified.' + ); +} + +// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use +const dotenvFiles = [ + `${paths.dotenv}.${NODE_ENV}.local`, + `${paths.dotenv}.${NODE_ENV}`, + // Don't include `.env.local` for `test` environment + // since normally you expect tests to produce the same + // results for everyone + NODE_ENV !== 'test' && `${paths.dotenv}.local`, + paths.dotenv, +].filter(Boolean); + +// Load environment variables from .env* files. Suppress warnings using silent +// if this file is missing. dotenv will never modify any environment variables +// that have already been set. Variable expansion is supported in .env files. +// https://github.com/motdotla/dotenv +// https://github.com/motdotla/dotenv-expand +dotenvFiles.forEach(dotenvFile => { + if (fs.existsSync(dotenvFile)) { + require('dotenv-expand')( + require('dotenv').config({ + path: dotenvFile, + }) + ); + } +}); + +// We support resolving modules according to `NODE_PATH`. +// This lets you use absolute paths in imports inside large monorepos: +// https://github.com/facebook/create-react-app/issues/253. +// It works similar to `NODE_PATH` in Node itself: +// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders +// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. +// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. +// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 +// We also resolve them to make sure all tools using them work consistently. +const appDirectory = fs.realpathSync(process.cwd()); +process.env.NODE_PATH = (process.env.NODE_PATH || '') + .split(path.delimiter) + .filter(folder => folder && !path.isAbsolute(folder)) + .map(folder => path.resolve(appDirectory, folder)) + .join(path.delimiter); + +// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be +// injected into the application via DefinePlugin in Webpack configuration. +const REACT_APP = /^REACT_APP_/i; + +function getClientEnvironment(publicUrl) { + const raw = Object.keys(process.env) + .filter(key => REACT_APP.test(key)) + .reduce( + (env, key) => { + env[key] = process.env[key]; + return env; + }, + { + // Useful for determining whether we’re running in production mode. + // Most importantly, it switches React into the correct mode. + NODE_ENV: process.env.NODE_ENV || 'development', + // Useful for resolving the correct path to static assets in `public`. + // For example, . + // This should only be used as an escape hatch. Normally you would put + // images into the `src` and `import` them in code to get their paths. + PUBLIC_URL: publicUrl, + } + ); + // Stringify all values so we can feed into Webpack DefinePlugin + const stringified = { + 'process.env': Object.keys(raw).reduce((env, key) => { + env[key] = JSON.stringify(raw[key]); + return env; + }, {}), + }; + + return { raw, stringified }; +} + +module.exports = getClientEnvironment; diff --git a/assets/config/jest/cssTransform.js b/assets/config/jest/cssTransform.js new file mode 100644 index 00000000..8f651148 --- /dev/null +++ b/assets/config/jest/cssTransform.js @@ -0,0 +1,14 @@ +'use strict'; + +// This is a custom Jest transformer turning style imports into empty objects. +// http://facebook.github.io/jest/docs/en/webpack.html + +module.exports = { + process() { + return 'module.exports = {};'; + }, + getCacheKey() { + // The output is always the same. + return 'cssTransform'; + }, +}; diff --git a/assets/config/jest/fileTransform.js b/assets/config/jest/fileTransform.js new file mode 100644 index 00000000..aab67618 --- /dev/null +++ b/assets/config/jest/fileTransform.js @@ -0,0 +1,40 @@ +'use strict'; + +const path = require('path'); +const camelcase = require('camelcase'); + +// This is a custom Jest transformer turning file imports into filenames. +// http://facebook.github.io/jest/docs/en/webpack.html + +module.exports = { + process(src, filename) { + const assetFilename = JSON.stringify(path.basename(filename)); + + if (filename.match(/\.svg$/)) { + // Based on how SVGR generates a component name: + // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 + const pascalCaseFilename = camelcase(path.parse(filename).name, { + pascalCase: true, + }); + const componentName = `Svg${pascalCaseFilename}`; + return `const React = require('react'); + module.exports = { + __esModule: true, + default: ${assetFilename}, + ReactComponent: React.forwardRef(function ${componentName}(props, ref) { + return { + $$typeof: Symbol.for('react.element'), + type: 'svg', + ref: ref, + key: null, + props: Object.assign({}, props, { + children: ${assetFilename} + }) + }; + }), + };`; + } + + return `module.exports = ${assetFilename};`; + }, +}; diff --git a/assets/config/modules.js b/assets/config/modules.js new file mode 100644 index 00000000..c84210a8 --- /dev/null +++ b/assets/config/modules.js @@ -0,0 +1,141 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const paths = require('./paths'); +const chalk = require('react-dev-utils/chalk'); +const resolve = require('resolve'); + +/** + * Get additional module paths based on the baseUrl of a compilerOptions object. + * + * @param {Object} options + */ +function getAdditionalModulePaths(options = {}) { + const baseUrl = options.baseUrl; + + // We need to explicitly check for null and undefined (and not a falsy value) because + // TypeScript treats an empty string as `.`. + if (baseUrl == null) { + // If there's no baseUrl set we respect NODE_PATH + // Note that NODE_PATH is deprecated and will be removed + // in the next major release of create-react-app. + + const nodePath = process.env.NODE_PATH || ''; + return nodePath.split(path.delimiter).filter(Boolean); + } + + const baseUrlResolved = path.resolve(paths.appPath, baseUrl); + + // We don't need to do anything if `baseUrl` is set to `node_modules`. This is + // the default behavior. + if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { + return null; + } + + // Allow the user set the `baseUrl` to `appSrc`. + if (path.relative(paths.appSrc, baseUrlResolved) === '') { + return [paths.appSrc]; + } + + // If the path is equal to the root directory we ignore it here. + // We don't want to allow importing from the root directly as source files are + // not transpiled outside of `src`. We do allow importing them with the + // absolute path (e.g. `src/Components/Button.js`) but we set that up with + // an alias. + if (path.relative(paths.appPath, baseUrlResolved) === '') { + return null; + } + + // Otherwise, throw an error. + throw new Error( + chalk.red.bold( + "Your project's `baseUrl` can only be set to `src` or `node_modules`." + + ' Create React App does not support other values at this time.' + ) + ); +} + +/** + * Get webpack aliases based on the baseUrl of a compilerOptions object. + * + * @param {*} options + */ +function getWebpackAliases(options = {}) { + const baseUrl = options.baseUrl; + + if (!baseUrl) { + return {}; + } + + const baseUrlResolved = path.resolve(paths.appPath, baseUrl); + + if (path.relative(paths.appPath, baseUrlResolved) === '') { + return { + src: paths.appSrc, + }; + } +} + +/** + * Get jest aliases based on the baseUrl of a compilerOptions object. + * + * @param {*} options + */ +function getJestAliases(options = {}) { + const baseUrl = options.baseUrl; + + if (!baseUrl) { + return {}; + } + + const baseUrlResolved = path.resolve(paths.appPath, baseUrl); + + if (path.relative(paths.appPath, baseUrlResolved) === '') { + return { + 'src/(.*)$': '/src/$1', + }; + } +} + +function getModules() { + // Check if TypeScript is setup + const hasTsConfig = fs.existsSync(paths.appTsConfig); + const hasJsConfig = fs.existsSync(paths.appJsConfig); + + if (hasTsConfig && hasJsConfig) { + throw new Error( + 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' + ); + } + + let config; + + // If there's a tsconfig.json we assume it's a + // TypeScript project and set up the config + // based on tsconfig.json + if (hasTsConfig) { + const ts = require(resolve.sync('typescript', { + basedir: paths.appNodeModules, + })); + config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config; + // Otherwise we'll check if there is jsconfig.json + // for non TS projects. + } else if (hasJsConfig) { + config = require(paths.appJsConfig); + } + + config = config || {}; + const options = config.compilerOptions || {}; + + const additionalModulePaths = getAdditionalModulePaths(options); + + return { + additionalModulePaths: additionalModulePaths, + webpackAliases: getWebpackAliases(options), + jestAliases: getJestAliases(options), + hasTsConfig, + }; +} + +module.exports = getModules(); diff --git a/assets/config/paths.js b/assets/config/paths.js new file mode 100644 index 00000000..f23c121f --- /dev/null +++ b/assets/config/paths.js @@ -0,0 +1,90 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const url = require('url'); + +// Make sure any symlinks in the project folder are resolved: +// https://github.com/facebook/create-react-app/issues/637 +const appDirectory = fs.realpathSync(process.cwd()); +const resolveApp = relativePath => path.resolve(appDirectory, relativePath); + +const envPublicUrl = process.env.PUBLIC_URL; + +function ensureSlash(inputPath, needsSlash) { + const hasSlash = inputPath.endsWith('/'); + if (hasSlash && !needsSlash) { + return inputPath.substr(0, inputPath.length - 1); + } else if (!hasSlash && needsSlash) { + return `${inputPath}/`; + } else { + return inputPath; + } +} + +const getPublicUrl = appPackageJson => + envPublicUrl || require(appPackageJson).homepage; + +// We use `PUBLIC_URL` environment variable or "homepage" field to infer +// "public path" at which the app is served. +// Webpack needs to know it to put the right + + + +
+ + {siteScript} + diff --git a/assets/public/static/img/cloudreve.svg b/assets/public/static/img/cloudreve.svg new file mode 100644 index 00000000..0a2c6c14 --- /dev/null +++ b/assets/public/static/img/cloudreve.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/public/static/img/cos.png b/assets/public/static/img/cos.png new file mode 100644 index 00000000..1ffb0bed Binary files /dev/null and b/assets/public/static/img/cos.png differ diff --git a/assets/public/static/img/favicon.ico b/assets/public/static/img/favicon.ico new file mode 100644 index 00000000..f0945ec9 Binary files /dev/null and b/assets/public/static/img/favicon.ico differ diff --git a/assets/public/static/img/local.png b/assets/public/static/img/local.png new file mode 100644 index 00000000..9c145e9c Binary files /dev/null and b/assets/public/static/img/local.png differ diff --git a/assets/public/static/img/logo192.png b/assets/public/static/img/logo192.png new file mode 100644 index 00000000..53f3a4e8 Binary files /dev/null and b/assets/public/static/img/logo192.png differ diff --git a/assets/public/static/img/logo512.png b/assets/public/static/img/logo512.png new file mode 100644 index 00000000..5ce033f4 Binary files /dev/null and b/assets/public/static/img/logo512.png differ diff --git a/assets/public/static/img/onedrive.png b/assets/public/static/img/onedrive.png new file mode 100644 index 00000000..a1116adb Binary files /dev/null and b/assets/public/static/img/onedrive.png differ diff --git a/assets/public/static/img/oss.png b/assets/public/static/img/oss.png new file mode 100644 index 00000000..5c715dfc Binary files /dev/null and b/assets/public/static/img/oss.png differ diff --git a/assets/public/static/img/qiniu.png b/assets/public/static/img/qiniu.png new file mode 100644 index 00000000..308f09d4 Binary files /dev/null and b/assets/public/static/img/qiniu.png differ diff --git a/assets/public/static/img/remote.png b/assets/public/static/img/remote.png new file mode 100644 index 00000000..2bfadc9e Binary files /dev/null and b/assets/public/static/img/remote.png differ diff --git a/assets/public/static/img/s3.png b/assets/public/static/img/s3.png new file mode 100644 index 00000000..4709cde5 Binary files /dev/null and b/assets/public/static/img/s3.png differ diff --git a/assets/public/static/img/upyun.png b/assets/public/static/img/upyun.png new file mode 100644 index 00000000..5a3910e7 Binary files /dev/null and b/assets/public/static/img/upyun.png differ diff --git a/assets/public/static/js/uploader/i18n/zh_CN.js b/assets/public/static/js/uploader/i18n/zh_CN.js new file mode 100644 index 00000000..791f2e59 --- /dev/null +++ b/assets/public/static/js/uploader/i18n/zh_CN.js @@ -0,0 +1,40 @@ +// Chinese (China) (zh_CN) +plupload.addI18n({ + "Stop Upload": "停止上传", + "Upload URL might be wrong or doesn't exist.": + "上传的URL可能是错误的或不存在。", + tb: "tb", + Size: "大小", + Close: "关闭", + "Init error.": "初始化错误。", + "Add files to the upload queue and click the start button.": + "将文件添加到上传队列,然后点击”开始上传“按钮。", + Filename: "文件名", + "Image format either wrong or not supported.": "图片格式错误或者不支持。", + Status: "状态", + "HTTP Error.": "HTTP 错误。", + "Start Upload": "开始上传", + mb: "mb", + kb: "kb", + "Duplicate file error.": "重复文件错误。", + "File size error.": "文件大小错误。", + "N/A": "N/A", + gb: "gb", + "Error: Invalid file extension:": "错误:无效的文件扩展名:", + "Select files": "选择文件", + "%s already present in the queue.": "%s 已经在当前队列里。", + "File: %s": "文件: %s", + b: "b", + "Uploaded %d/%d files": "已上传 %d/%d 个文件", + "Upload element accepts only %d file(s) at a time. Extra files were stripped.": + "每次只接受同时上传 %d 个文件,多余的文件将会被删除。", + "%d files queued": "%d 个文件加入到队列", + "File: %s, size: %d, max file size: %d": + "文件: %s, 大小: %d, 最大文件大小: %d", + "Drag files here.": "把文件拖到这里。", + "Runtime ran out of available memory.": "运行时已消耗所有可用内存。", + "File count error.": "文件数量错误。", + "File extension error.": "文件扩展名错误。", + "Error: File too large:": "错误: 文件太大:", + "Add Files": "增加文件", +}); diff --git a/assets/public/static/js/uploader/main.js b/assets/public/static/js/uploader/main.js new file mode 100644 index 00000000..68673f36 --- /dev/null +++ b/assets/public/static/js/uploader/main.js @@ -0,0 +1,268 @@ +// /*global Qiniu */ +// /*global plupload */ +// /*global FileProgress */ +// /*global hljs */ +// function getCookieByString(cookieName){ +// var start = document.cookie.indexOf(cookieName+'='); +// if (start == -1) return false; +// start = start+cookieName.length+1; +// var end = document.cookie.indexOf(';', start); +// if (end == -1) end=document.cookie.length; +// return document.cookie.substring(start, end); +// } + +// if(uploadConfig.saveType == "oss" || uploadConfig.saveType == "upyun" || uploadConfig.saveType == "s3"){ +// ChunkSize = "0"; +// }else{ +// ChunkSize = "4mb"; +// } + +// uploader = Qiniu.uploader({ +// runtimes: 'html5,flash,html4', +// browse_button: 'pickfiles', +// container: 'container', +// drop_element: 'container', +// max_file_size: uploadConfig.maxSize, +// flash_swf_url: '/bower_components/plupload/js/Moxie.swf', +// dragdrop: true, +// chunk_size: ChunkSize, +// filters: { +// mime_types :uploadConfig.allowedType, +// }, +// multi_selection: !(moxie.core.utils.Env.OS.toLowerCase() === "ios"), +// uptoken_url: "/Upload/Token", +// // uptoken_func: function(){ +// // var ajax = new XMLHttpRequest(); +// // ajax.open('GET', $('#uptoken_url').val(), false); +// // ajax.setRequestHeader("If-Modified-Since", "0"); +// // ajax.send(); +// // if (ajax.status === 200) { +// // var res = JSON.parse(ajax.responseText); +// // console.log('custom uptoken_func:' + res.uptoken); +// // return res.uptoken; +// // } else { +// // console.log('custom uptoken_func err'); +// // return ''; +// // } +// // }, +// domain: $('#domain').val(), +// get_new_uptoken: true, +// // downtoken_url: '/downtoken', +// // unique_names: true, +// // save_key: true, +// // x_vars: { +// // 'id': '1234', +// // 'time': function(up, file) { +// // var time = (new Date()).getTime(); +// // // do something with 'time' +// // return time; +// // }, +// // }, +// auto_start: true, +// log_level: 5, +// init: { +// 'FilesAdded': function(up, files) { +// $('table').show(); +// $('#upload_box').show(); +// $('#success').hide(); +// $('#info_box').hide(); + +// $.cookie('path', decodeURI(getCookieByString("path_tmp"))); +// plupload.each(files, function(file) { +// var progress = new FileProgress(file, 'fsUploadProgress'); +// progress.setStatus("等待..."); +// progress.bindUploadCancel(up); +// }); + +// }, +// 'BeforeUpload': function(up, file) { +// var progress = new FileProgress(file, 'fsUploadProgress'); +// var chunk_size = plupload.parseSize(this.getOption('chunk_size')); +// if (up.runtime === 'html5' && chunk_size) { +// progress.setChunkProgess(chunk_size); +// } +// }, +// 'UploadProgress': function(up, file) { +// var progress = new FileProgress(file, 'fsUploadProgress'); +// var chunk_size = plupload.parseSize(this.getOption('chunk_size')); +// progress.setProgress(file.percent + "%", file.speed, chunk_size); +// }, +// 'UploadComplete': function(up, file) { +// $('#success').show(); +// toastr["success"]("队列全部文件处理完毕"); +// getMemory(); +// }, +// 'FileUploaded': function(up, file, info) { +// var progress = new FileProgress(file, 'fsUploadProgress'); +// progress.setComplete(up, info); +// }, +// 'Error': function(up, err, errTip) { +// $('#upload_box').show(); +// $('table').show(); +// $('#info_box').hide(); +// var progress = new FileProgress(err.file, 'fsUploadProgress'); +// progress.setError(); +// progress.setStatus(errTip); +// toastr["error"]("上传时遇到错误"); +// } +// // , +// // 'Key': function(up, file) { +// // var key = ""; +// // // do something with key +// // return key +// // } +// } +// }); + +// uploader.bind('FileUploaded', function(up,file) { +// console.log('a file is uploaded'); +// }); +// $('#container').on( +// 'dragenter', +// function(e) { +// e.preventDefault(); +// $('#container').addClass('draging'); +// e.stopPropagation(); +// } +// ).on('drop', function(e) { +// e.preventDefault(); +// $('#container').removeClass('draging'); +// e.stopPropagation(); +// }).on('dragleave', function(e) { +// e.preventDefault(); +// $('#container').removeClass('draging'); +// e.stopPropagation(); +// }).on('dragover', function(e) { +// e.preventDefault(); +// $('#container').addClass('draging'); +// e.stopPropagation(); +// }); + +// $('#show_code').on('click', function() { +// $('#myModal-code').modal(); +// $('pre code').each(function(i, e) { +// hljs.highlightBlock(e); +// }); +// }); + +// $('body').on('click', 'table button.btn', function() { +// $(this).parents('tr').next().toggle(); +// }); + +// var getRotate = function(url) { +// if (!url) { +// return 0; +// } +// var arr = url.split('/'); +// for (var i = 0, len = arr.length; i < len; i++) { +// if (arr[i] === 'rotate') { +// return parseInt(arr[i + 1], 10); +// } +// } +// return 0; +// }; + +// $('#myModal-img .modal-body-footer').find('a').on('click', function() { +// var img = $('#myModal-img').find('.modal-body img'); +// var key = img.data('key'); +// var oldUrl = img.attr('src'); +// var originHeight = parseInt(img.data('h'), 10); +// var fopArr = []; +// var rotate = getRotate(oldUrl); +// if (!$(this).hasClass('no-disable-click')) { +// $(this).addClass('disabled').siblings().removeClass('disabled'); +// if ($(this).data('imagemogr') !== 'no-rotate') { +// fopArr.push({ +// 'fop': 'imageMogr2', +// 'auto-orient': true, +// 'strip': true, +// 'rotate': rotate, +// 'format': 'png' +// }); +// } +// } else { +// $(this).siblings().removeClass('disabled'); +// var imageMogr = $(this).data('imagemogr'); +// if (imageMogr === 'left') { +// rotate = rotate - 90 < 0 ? rotate + 270 : rotate - 90; +// } else if (imageMogr === 'right') { +// rotate = rotate + 90 > 360 ? rotate - 270 : rotate + 90; +// } +// fopArr.push({ +// 'fop': 'imageMogr2', +// 'auto-orient': true, +// 'strip': true, +// 'rotate': rotate, +// 'format': 'png' +// }); +// } + +// $('#myModal-img .modal-body-footer').find('a.disabled').each(function() { + +// var watermark = $(this).data('watermark'); +// var imageView = $(this).data('imageview'); +// var imageMogr = $(this).data('imagemogr'); + +// if (watermark) { +// fopArr.push({ +// fop: 'watermark', +// mode: 1, +// image: 'http://www.b1.qiniudn.com/images/logo-2.png', +// dissolve: 100, +// gravity: watermark, +// dx: 100, +// dy: 100 +// }); +// } + +// if (imageView) { +// var height; +// switch (imageView) { +// case 'large': +// height = originHeight; +// break; +// case 'middle': +// height = originHeight * 0.5; +// break; +// case 'small': +// height = originHeight * 0.1; +// break; +// default: +// height = originHeight; +// break; +// } +// fopArr.push({ +// fop: 'imageView2', +// mode: 3, +// h: parseInt(height, 10), +// q: 100, +// format: 'png' +// }); +// } + +// if (imageMogr === 'no-rotate') { +// fopArr.push({ +// 'fop': 'imageMogr2', +// 'auto-orient': true, +// 'strip': true, +// 'rotate': 0, +// 'format': 'png' +// }); +// } +// }); + +// var newUrl = Qiniu.pipeline(fopArr, key); + +// var newImg = new Image(); +// img.attr('src', 'images/loading.gif'); +// newImg.onload = function() { +// img.attr('src', newUrl); +// img.parent('a').attr('href', newUrl); +// }; +// newImg.src = newUrl; +// return false; +// }); + +// function t(){ +// uploader.getNewUpToken(); +// } diff --git a/assets/public/static/js/uploader/moxie.js b/assets/public/static/js/uploader/moxie.js new file mode 100644 index 00000000..269f995f --- /dev/null +++ b/assets/public/static/js/uploader/moxie.js @@ -0,0 +1,13049 @@ +var MXI_DEBUG = true; +/** + * mOxie - multi-runtime File API & XMLHttpRequest L2 Polyfill + * v1.5.3 + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + * + * Date: 2017-02-02 + */ +(function (global, factory) { + var extract = function () { + var ctx = {}; + factory.apply(ctx, arguments); + return ctx.moxie; + }; + + if (typeof define === "function" && define.amd) { + define("moxie", [], extract); + } else if (typeof module === "object" && module.exports) { + module.exports = extract(); + } else { + global.moxie = extract(); + } +})(this || window, function () { + /** + * Compiled inline version. (Library mode) + */ + + /*jshint smarttabs:true, undef:true, latedef:true, curly:true, bitwise:true, camelcase:true */ + /*globals $code */ + + (function (exports, undefined) { + "use strict"; + + var modules = {}; + + function require(ids, callback) { + var module, + defs = []; + + for (var i = 0; i < ids.length; ++i) { + module = modules[ids[i]] || resolve(ids[i]); + if (!module) { + throw "module definition dependecy not found: " + ids[i]; + } + + defs.push(module); + } + + callback.apply(null, defs); + } + + function define(id, dependencies, definition) { + if (typeof id !== "string") { + throw "invalid module definition, module id must be defined and be a string"; + } + + if (dependencies === undefined) { + throw "invalid module definition, dependencies must be specified"; + } + + if (definition === undefined) { + throw "invalid module definition, definition function must be specified"; + } + + require(dependencies, function () { + modules[id] = definition.apply(null, arguments); + }); + } + + function defined(id) { + return !!modules[id]; + } + + function resolve(id) { + var target = exports; + var fragments = id.split(/[.\/]/); + + for (var fi = 0; fi < fragments.length; ++fi) { + if (!target[fragments[fi]]) { + return; + } + + target = target[fragments[fi]]; + } + + return target; + } + + function expose(ids) { + for (var i = 0; i < ids.length; i++) { + var target = exports; + var id = ids[i]; + var fragments = id.split(/[.\/]/); + + for (var fi = 0; fi < fragments.length - 1; ++fi) { + if (target[fragments[fi]] === undefined) { + target[fragments[fi]] = {}; + } + + target = target[fragments[fi]]; + } + + target[fragments[fragments.length - 1]] = modules[id]; + } + } + + // Included from: src/javascript/core/utils/Basic.js + + /** + * Basic.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/core/utils/Basic +@public +@static +*/ + define("moxie/core/utils/Basic", [], function () { + /** + Gets the true type of the built-in object (better version of typeof). + @author Angus Croll (http://javascriptweblog.wordpress.com/) + + @method typeOf + @for Utils + @static + @param {Object} o Object to check. + @return {String} Object [[Class]] + */ + function typeOf(o) { + var undef; + + if (o === undef) { + return "undefined"; + } else if (o === null) { + return "null"; + } else if (o.nodeType) { + return "node"; + } + + // the snippet below is awesome, however it fails to detect null, undefined and arguments types in IE lte 8 + return {}.toString + .call(o) + .match(/\s([a-z|A-Z]+)/)[1] + .toLowerCase(); + } + + /** + Extends the specified object with another object(s). + + @method extend + @static + @param {Object} target Object to extend. + @param {Object} [obj]* Multiple objects to extend with. + @return {Object} Same as target, the extended object. + */ + function extend() { + return merge(false, false, arguments); + } + + /** + Extends the specified object with another object(s), but only if the property exists in the target. + + @method extendIf + @static + @param {Object} target Object to extend. + @param {Object} [obj]* Multiple objects to extend with. + @return {Object} Same as target, the extended object. + */ + function extendIf() { + return merge(true, false, arguments); + } + + function extendImmutable() { + return merge(false, true, arguments); + } + + function extendImmutableIf() { + return merge(true, true, arguments); + } + + function shallowCopy(obj) { + switch (typeOf(obj)) { + case "array": + return Array.prototype.slice.call(obj); + + case "object": + return extend({}, obj); + } + return obj; + } + + function merge(strict, immutable, args) { + var undef; + var target = args[0]; + + each(args, function (arg, i) { + if (i > 0) { + each(arg, function (value, key) { + var isComplex = + inArray(typeOf(value), ["array", "object"]) !== + -1; + + if ( + value === undef || + (strict && target[key] === undef) + ) { + return true; + } + + if (isComplex && immutable) { + value = shallowCopy(value); + } + + if ( + typeOf(target[key]) === typeOf(value) && + isComplex + ) { + merge(strict, immutable, [target[key], value]); + } else { + target[key] = value; + } + }); + } + }); + + return target; + } + + /** + A way to inherit one `class` from another in a consisstent way (more or less) + + @method inherit + @static + @since >1.4.1 + @param {Function} child + @param {Function} parent + @return {Function} Prepared constructor + */ + function inherit(child, parent) { + // copy over all parent properties + for (var key in parent) { + if ({}.hasOwnProperty.call(parent, key)) { + child[key] = parent[key]; + } + } + + // give child `class` a place to define its own methods + function ctor() { + this.constructor = child; + } + ctor.prototype = parent.prototype; + child.prototype = new ctor(); + + // keep a way to reference parent methods + child.__parent__ = parent.prototype; + return child; + } + + /** + Executes the callback function for each item in array/object. If you return false in the + callback it will break the loop. + + @method each + @static + @param {Object} obj Object to iterate. + @param {function} callback Callback function to execute for each item. + */ + function each(obj, callback) { + var length, key, i, undef; + + if (obj) { + try { + length = obj.length; + } catch (ex) { + length = undef; + } + + if (length === undef || typeof length !== "number") { + // Loop object items + for (key in obj) { + if (obj.hasOwnProperty(key)) { + if (callback(obj[key], key) === false) { + return; + } + } + } + } else { + // Loop array items + for (i = 0; i < length; i++) { + if (callback(obj[i], i) === false) { + return; + } + } + } + } + } + + /** + Checks if object is empty. + + @method isEmptyObj + @static + @param {Object} o Object to check. + @return {Boolean} + */ + function isEmptyObj(obj) { + var prop; + + if (!obj || typeOf(obj) !== "object") { + return true; + } + + for (prop in obj) { + return false; + } + + return true; + } + + /** + Recieve an array of functions (usually async) to call in sequence, each function + receives a callback as first argument that it should call, when it completes. Finally, + after everything is complete, main callback is called. Passing truthy value to the + callback as a first argument will interrupt the sequence and invoke main callback + immediately. + + @method inSeries + @static + @param {Array} queue Array of functions to call in sequence + @param {Function} cb Main callback that is called in the end, or in case of error + */ + function inSeries(queue, cb) { + var i = 0, + length = queue.length; + + if (typeOf(cb) !== "function") { + cb = function () {}; + } + + if (!queue || !queue.length) { + cb(); + } + + function callNext(i) { + if (typeOf(queue[i]) === "function") { + queue[i](function (error) { + /*jshint expr:true */ + ++i < length && !error ? callNext(i) : cb(error); + }); + } + } + callNext(i); + } + + /** + Recieve an array of functions (usually async) to call in parallel, each function + receives a callback as first argument that it should call, when it completes. After + everything is complete, main callback is called. Passing truthy value to the + callback as a first argument will interrupt the process and invoke main callback + immediately. + + @method inParallel + @static + @param {Array} queue Array of functions to call in sequence + @param {Function} cb Main callback that is called in the end, or in case of erro + */ + function inParallel(queue, cb) { + var count = 0, + num = queue.length, + cbArgs = new Array(num); + + each(queue, function (fn, i) { + fn(function (error) { + if (error) { + return cb(error); + } + + var args = [].slice.call(arguments); + args.shift(); // strip error - undefined or not + + cbArgs[i] = args; + count++; + + if (count === num) { + cbArgs.unshift(null); + cb.apply(this, cbArgs); + } + }); + }); + } + + /** + Find an element in array and return it's index if present, otherwise return -1. + + @method inArray + @static + @param {Mixed} needle Element to find + @param {Array} array + @return {Int} Index of the element, or -1 if not found + */ + function inArray(needle, array) { + if (array) { + if (Array.prototype.indexOf) { + return Array.prototype.indexOf.call(array, needle); + } + + for (var i = 0, length = array.length; i < length; i++) { + if (array[i] === needle) { + return i; + } + } + } + return -1; + } + + /** + Returns elements of first array if they are not present in second. And false - otherwise. + + @private + @method arrayDiff + @param {Array} needles + @param {Array} array + @return {Array|Boolean} + */ + function arrayDiff(needles, array) { + var diff = []; + + if (typeOf(needles) !== "array") { + needles = [needles]; + } + + if (typeOf(array) !== "array") { + array = [array]; + } + + for (var i in needles) { + if (inArray(needles[i], array) === -1) { + diff.push(needles[i]); + } + } + return diff.length ? diff : false; + } + + /** + Find intersection of two arrays. + + @private + @method arrayIntersect + @param {Array} array1 + @param {Array} array2 + @return {Array} Intersection of two arrays or null if there is none + */ + function arrayIntersect(array1, array2) { + var result = []; + each(array1, function (item) { + if (inArray(item, array2) !== -1) { + result.push(item); + } + }); + return result.length ? result : null; + } + + /** + Forces anything into an array. + + @method toArray + @static + @param {Object} obj Object with length field. + @return {Array} Array object containing all items. + */ + function toArray(obj) { + var i, + arr = []; + + for (i = 0; i < obj.length; i++) { + arr[i] = obj[i]; + } + + return arr; + } + + /** + Generates an unique ID. The only way a user would be able to get the same ID is if the two persons + at the same exact millisecond manage to get the same 5 random numbers between 0-65535; it also uses + a counter so each ID is guaranteed to be unique for the given page. It is more probable for the earth + to be hit with an asteroid. + + @method guid + @static + @param {String} prefix to prepend (by default 'o' will be prepended). + @method guid + @return {String} Virtually unique id. + */ + var guid = (function () { + var counter = 0; + + return function (prefix) { + var guid = new Date().getTime().toString(32), + i; + + for (i = 0; i < 5; i++) { + guid += Math.floor(Math.random() * 65535).toString(32); + } + + return (prefix || "o_") + guid + (counter++).toString(32); + }; + })(); + + /** + Trims white spaces around the string + + @method trim + @static + @param {String} str + @return {String} + */ + function trim(str) { + if (!str) { + return str; + } + return String.prototype.trim + ? String.prototype.trim.call(str) + : str.toString().replace(/^\s*/, "").replace(/\s*$/, ""); + } + + /** + Parses the specified size string into a byte value. For example 10kb becomes 10240. + + @method parseSizeStr + @static + @param {String/Number} size String to parse or number to just pass through. + @return {Number} Size in bytes. + */ + function parseSizeStr(size) { + if (typeof size !== "string") { + return size; + } + + var muls = { + t: 1099511627776, + g: 1073741824, + m: 1048576, + k: 1024, + }, + mul; + + size = /^([0-9\.]+)([tmgk]?)$/.exec( + size.toLowerCase().replace(/[^0-9\.tmkg]/g, "") + ); + mul = size[2]; + size = +size[1]; + + if (muls.hasOwnProperty(mul)) { + size *= muls[mul]; + } + return Math.floor(size); + } + + /** + * Pseudo sprintf implementation - simple way to replace tokens with specified values. + * + * @param {String} str String with tokens + * @return {String} String with replaced tokens + */ + function sprintf(str) { + var args = [].slice.call(arguments, 1); + + return str.replace(/%[a-z]/g, function () { + var value = args.shift(); + return typeOf(value) !== "undefined" ? value : ""; + }); + } + + function delay(cb, timeout) { + var self = this; + setTimeout(function () { + cb.call(self); + }, timeout || 1); + } + + return { + guid: guid, + typeOf: typeOf, + extend: extend, + extendIf: extendIf, + extendImmutable: extendImmutable, + extendImmutableIf: extendImmutableIf, + inherit: inherit, + each: each, + isEmptyObj: isEmptyObj, + inSeries: inSeries, + inParallel: inParallel, + inArray: inArray, + arrayDiff: arrayDiff, + arrayIntersect: arrayIntersect, + toArray: toArray, + trim: trim, + sprintf: sprintf, + parseSizeStr: parseSizeStr, + delay: delay, + }; + }); + + // Included from: src/javascript/core/utils/Encode.js + + /** + * Encode.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/core/utils/Encode", [], function () { + /** + @class moxie/core/utils/Encode + */ + + /** + Encode string with UTF-8 + + @method utf8_encode + @for Utils + @static + @param {String} str String to encode + @return {String} UTF-8 encoded string + */ + var utf8_encode = function (str) { + return unescape(encodeURIComponent(str)); + }; + + /** + Decode UTF-8 encoded string + + @method utf8_decode + @static + @param {String} str String to decode + @return {String} Decoded string + */ + var utf8_decode = function (str_data) { + return decodeURIComponent(escape(str_data)); + }; + + /** + Decode Base64 encoded string (uses browser's default method if available), + from: https://raw.github.com/kvz/phpjs/master/functions/url/base64_decode.js + + @method atob + @static + @param {String} data String to decode + @return {String} Decoded string + */ + var atob = function (data, utf8) { + if (typeof window.atob === "function") { + return utf8 + ? utf8_decode(window.atob(data)) + : window.atob(data); + } + + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Thunder.m + // + input by: Aman Gupta + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Onno Marsman + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: Brett Zamir (http://brett-zamir.me) + // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA=='); + // * returns 1: 'Kevin van Zonneveld' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window.atob == 'function') { + // return atob(data); + //} + var b64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, + o2, + o3, + h1, + h2, + h3, + h4, + bits, + i = 0, + ac = 0, + dec = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data += ""; + + do { + // unpack four hexets into three octets using index points in b64 + h1 = b64.indexOf(data.charAt(i++)); + h2 = b64.indexOf(data.charAt(i++)); + h3 = b64.indexOf(data.charAt(i++)); + h4 = b64.indexOf(data.charAt(i++)); + + bits = (h1 << 18) | (h2 << 12) | (h3 << 6) | h4; + + o1 = (bits >> 16) & 0xff; + o2 = (bits >> 8) & 0xff; + o3 = bits & 0xff; + + if (h3 == 64) { + tmp_arr[ac++] = String.fromCharCode(o1); + } else if (h4 == 64) { + tmp_arr[ac++] = String.fromCharCode(o1, o2); + } else { + tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); + } + } while (i < data.length); + + dec = tmp_arr.join(""); + + return utf8 ? utf8_decode(dec) : dec; + }; + + /** + Base64 encode string (uses browser's default method if available), + from: https://raw.github.com/kvz/phpjs/master/functions/url/base64_encode.js + + @method btoa + @static + @param {String} data String to encode + @return {String} Base64 encoded string + */ + var btoa = function (data, utf8) { + if (utf8) { + data = utf8_encode(data); + } + + if (typeof window.btoa === "function") { + return window.btoa(data); + } + + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Bayron Guevara + // + improved by: Thunder.m + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + improved by: Rafał Kukawski (http://kukawski.pl) + // * example 1: base64_encode('Kevin van Zonneveld'); + // * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA==' + // mozilla has this native + // - but breaks in 2.0.0.12! + var b64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, + o2, + o3, + h1, + h2, + h3, + h4, + bits, + i = 0, + ac = 0, + enc = "", + tmp_arr = []; + + if (!data) { + return data; + } + + do { + // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = (o1 << 16) | (o2 << 8) | o3; + + h1 = (bits >> 18) & 0x3f; + h2 = (bits >> 12) & 0x3f; + h3 = (bits >> 6) & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = + b64.charAt(h1) + + b64.charAt(h2) + + b64.charAt(h3) + + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(""); + + var r = data.length % 3; + + return (r ? enc.slice(0, r - 3) : enc) + "===".slice(r || 3); + }; + + return { + utf8_encode: utf8_encode, + utf8_decode: utf8_decode, + atob: atob, + btoa: btoa, + }; + }); + + // Included from: src/javascript/core/utils/Env.js + + /** + * Env.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/core/utils/Env", [ + "moxie/core/utils/Basic", + ], function (Basic) { + /** + * UAParser.js v0.7.7 + * Lightweight JavaScript-based User-Agent string parser + * https://github.com/faisalman/ua-parser-js + * + * Copyright © 2012-2015 Faisal Salman + * Dual licensed under GPLv2 & MIT + */ + var UAParser = (function (undefined) { + ////////////// + // Constants + ///////////// + + var EMPTY = "", + UNKNOWN = "?", + FUNC_TYPE = "function", + UNDEF_TYPE = "undefined", + OBJ_TYPE = "object", + MAJOR = "major", + MODEL = "model", + NAME = "name", + TYPE = "type", + VENDOR = "vendor", + VERSION = "version", + ARCHITECTURE = "architecture", + CONSOLE = "console", + MOBILE = "mobile", + TABLET = "tablet"; + + /////////// + // Helper + ////////// + + var util = { + has: function (str1, str2) { + return ( + str2.toLowerCase().indexOf(str1.toLowerCase()) !== + -1 + ); + }, + lowerize: function (str) { + return str.toLowerCase(); + }, + }; + + /////////////// + // Map helper + ////////////// + + var mapper = { + rgx: function () { + // loop through all regexes maps + for ( + var result, + i = 0, + j, + k, + p, + q, + matches, + match, + args = arguments; + i < args.length; + i += 2 + ) { + var regex = args[i], // even sequence (0,2,4,..) + props = args[i + 1]; // odd sequence (1,3,5,..) + + // construct object barebones + if (typeof result === UNDEF_TYPE) { + result = {}; + for (p in props) { + q = props[p]; + if (typeof q === OBJ_TYPE) { + result[q[0]] = undefined; + } else { + result[q] = undefined; + } + } + } + + // try matching uastring with regexes + for (j = k = 0; j < regex.length; j++) { + matches = regex[j].exec(this.getUA()); + if (!!matches) { + for (p = 0; p < props.length; p++) { + match = matches[++k]; + q = props[p]; + // check if given property is actually array + if ( + typeof q === OBJ_TYPE && + q.length > 0 + ) { + if (q.length == 2) { + if (typeof q[1] == FUNC_TYPE) { + // assign modified match + result[q[0]] = q[1].call( + this, + match + ); + } else { + // assign given value, ignore regex match + result[q[0]] = q[1]; + } + } else if (q.length == 3) { + // check whether function or regex + if ( + typeof q[1] === FUNC_TYPE && + !(q[1].exec && q[1].test) + ) { + // call function (usually string mapper) + result[q[0]] = match + ? q[1].call( + this, + match, + q[2] + ) + : undefined; + } else { + // sanitize match using given regex + result[q[0]] = match + ? match.replace( + q[1], + q[2] + ) + : undefined; + } + } else if (q.length == 4) { + result[q[0]] = match + ? q[3].call( + this, + match.replace( + q[1], + q[2] + ) + ) + : undefined; + } + } else { + result[q] = match + ? match + : undefined; + } + } + break; + } + } + + if (!!matches) break; // break the loop immediately if match found + } + return result; + }, + + str: function (str, map) { + for (var i in map) { + // check if array + if ( + typeof map[i] === OBJ_TYPE && + map[i].length > 0 + ) { + for (var j = 0; j < map[i].length; j++) { + if (util.has(map[i][j], str)) { + return i === UNKNOWN ? undefined : i; + } + } + } else if (util.has(map[i], str)) { + return i === UNKNOWN ? undefined : i; + } + } + return str; + }, + }; + + /////////////// + // String map + ////////////// + + var maps = { + browser: { + oldsafari: { + major: { + 1: ["/8", "/1", "/3"], + 2: "/4", + "?": "/", + }, + version: { + "1.0": "/8", + 1.2: "/1", + 1.3: "/3", + "2.0": "/412", + "2.0.2": "/416", + "2.0.3": "/417", + "2.0.4": "/419", + "?": "/", + }, + }, + }, + + device: { + sprint: { + model: { + "Evo Shift 4G": "7373KT", + }, + vendor: { + HTC: "APA", + Sprint: "Sprint", + }, + }, + }, + + os: { + windows: { + version: { + ME: "4.90", + "NT 3.11": "NT3.51", + "NT 4.0": "NT4.0", + 2000: "NT 5.0", + XP: ["NT 5.1", "NT 5.2"], + Vista: "NT 6.0", + 7: "NT 6.1", + 8: "NT 6.2", + 8.1: "NT 6.3", + RT: "ARM", + }, + }, + }, + }; + + ////////////// + // Regex map + ///////////// + + var regexes = { + browser: [ + [ + // Presto based + /(opera\smini)\/([\w\.-]+)/i, // Opera Mini + /(opera\s[mobiletab]+).+version\/([\w\.-]+)/i, // Opera Mobi/Tablet + /(opera).+version\/([\w\.]+)/i, // Opera > 9.80 + /(opera)[\/\s]+([\w\.]+)/i, // Opera < 9.80 + ], + [NAME, VERSION], + [ + /\s(opr)\/([\w\.]+)/i, // Opera Webkit + ], + [[NAME, "Opera"], VERSION], + [ + // Mixed + /(kindle)\/([\w\.]+)/i, // Kindle + /(lunascape|maxthon|netfront|jasmine|blazer)[\/\s]?([\w\.]+)*/i, + // Lunascape/Maxthon/Netfront/Jasmine/Blazer + + // Trident based + /(avant\s|iemobile|slim|baidu)(?:browser)?[\/\s]?([\w\.]*)/i, + // Avant/IEMobile/SlimBrowser/Baidu + /(?:ms|\()(ie)\s([\w\.]+)/i, // Internet Explorer + + // Webkit/KHTML based + /(rekonq)\/([\w\.]+)*/i, // Rekonq + /(chromium|flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi)\/([\w\.-]+)/i, + // Chromium/Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron + ], + [NAME, VERSION], + [ + /(trident).+rv[:\s]([\w\.]+).+like\sgecko/i, // IE11 + ], + [[NAME, "IE"], VERSION], + [ + /(edge)\/((\d+)?[\w\.]+)/i, // Microsoft Edge + ], + [NAME, VERSION], + [ + /(yabrowser)\/([\w\.]+)/i, // Yandex + ], + [[NAME, "Yandex"], VERSION], + [ + /(comodo_dragon)\/([\w\.]+)/i, // Comodo Dragon + ], + [[NAME, /_/g, " "], VERSION], + [ + /(chrome|omniweb|arora|[tizenoka]{5}\s?browser)\/v?([\w\.]+)/i, + // Chrome/OmniWeb/Arora/Tizen/Nokia + /(uc\s?browser|qqbrowser)[\/\s]?([\w\.]+)/i, + // UCBrowser/QQBrowser + ], + [NAME, VERSION], + [ + /(dolfin)\/([\w\.]+)/i, // Dolphin + ], + [[NAME, "Dolphin"], VERSION], + [ + /((?:android.+)crmo|crios)\/([\w\.]+)/i, // Chrome for Android/iOS + ], + [[NAME, "Chrome"], VERSION], + [ + /XiaoMi\/MiuiBrowser\/([\w\.]+)/i, // MIUI Browser + ], + [VERSION, [NAME, "MIUI Browser"]], + [ + /android.+version\/([\w\.]+)\s+(?:mobile\s?safari|safari)/i, // Android Browser + ], + [VERSION, [NAME, "Android Browser"]], + [ + /FBAV\/([\w\.]+);/i, // Facebook App for iOS + ], + [VERSION, [NAME, "Facebook"]], + [ + /version\/([\w\.]+).+?mobile\/\w+\s(safari)/i, // Mobile Safari + ], + [VERSION, [NAME, "Mobile Safari"]], + [ + /version\/([\w\.]+).+?(mobile\s?safari|safari)/i, // Safari & Safari Mobile + ], + [VERSION, NAME], + [ + /webkit.+?(mobile\s?safari|safari)(\/[\w\.]+)/i, // Safari < 3.0 + ], + [ + NAME, + [ + VERSION, + mapper.str, + maps.browser.oldsafari.version, + ], + ], + [ + /(konqueror)\/([\w\.]+)/i, // Konqueror + /(webkit|khtml)\/([\w\.]+)/i, + ], + [NAME, VERSION], + [ + // Gecko based + /(navigator|netscape)\/([\w\.-]+)/i, // Netscape + ], + [[NAME, "Netscape"], VERSION], + [ + /(swiftfox)/i, // Swiftfox + /(icedragon|iceweasel|camino|chimera|fennec|maemo\sbrowser|minimo|conkeror)[\/\s]?([\w\.\+]+)/i, + // IceDragon/Iceweasel/Camino/Chimera/Fennec/Maemo/Minimo/Conkeror + /(firefox|seamonkey|k-meleon|icecat|iceape|firebird|phoenix)\/([\w\.-]+)/i, + // Firefox/SeaMonkey/K-Meleon/IceCat/IceApe/Firebird/Phoenix + /(mozilla)\/([\w\.]+).+rv\:.+gecko\/\d+/i, // Mozilla + + // Other + /(polaris|lynx|dillo|icab|doris|amaya|w3m|netsurf)[\/\s]?([\w\.]+)/i, + // Polaris/Lynx/Dillo/iCab/Doris/Amaya/w3m/NetSurf + /(links)\s\(([\w\.]+)/i, // Links + /(gobrowser)\/?([\w\.]+)*/i, // GoBrowser + /(ice\s?browser)\/v?([\w\._]+)/i, // ICE Browser + /(mosaic)[\/\s]([\w\.]+)/i, // Mosaic + ], + [NAME, VERSION], + ], + + engine: [ + [ + /windows.+\sedge\/([\w\.]+)/i, // EdgeHTML + ], + [VERSION, [NAME, "EdgeHTML"]], + [ + /(presto)\/([\w\.]+)/i, // Presto + /(webkit|trident|netfront|netsurf|amaya|lynx|w3m)\/([\w\.]+)/i, // WebKit/Trident/NetFront/NetSurf/Amaya/Lynx/w3m + /(khtml|tasman|links)[\/\s]\(?([\w\.]+)/i, // KHTML/Tasman/Links + /(icab)[\/\s]([23]\.[\d\.]+)/i, // iCab + ], + [NAME, VERSION], + [ + /rv\:([\w\.]+).*(gecko)/i, // Gecko + ], + [VERSION, NAME], + ], + + os: [ + [ + // Windows based + /microsoft\s(windows)\s(vista|xp)/i, // Windows (iTunes) + ], + [NAME, VERSION], + [ + /(windows)\snt\s6\.2;\s(arm)/i, // Windows RT + /(windows\sphone(?:\sos)*|windows\smobile|windows)[\s\/]?([ntce\d\.\s]+\w)/i, + ], + [NAME, [VERSION, mapper.str, maps.os.windows.version]], + [/(win(?=3|9|n)|win\s9x\s)([nt\d\.]+)/i], + [ + [NAME, "Windows"], + [VERSION, mapper.str, maps.os.windows.version], + ], + [ + // Mobile/Embedded OS + /\((bb)(10);/i, // BlackBerry 10 + ], + [[NAME, "BlackBerry"], VERSION], + [ + /(blackberry)\w*\/?([\w\.]+)*/i, // Blackberry + /(tizen)[\/\s]([\w\.]+)/i, // Tizen + /(android|webos|palm\os|qnx|bada|rim\stablet\sos|meego|contiki)[\/\s-]?([\w\.]+)*/i, + // Android/WebOS/Palm/QNX/Bada/RIM/MeeGo/Contiki + /linux;.+(sailfish);/i, // Sailfish OS + ], + [NAME, VERSION], + [ + /(symbian\s?os|symbos|s60(?=;))[\/\s-]?([\w\.]+)*/i, // Symbian + ], + [[NAME, "Symbian"], VERSION], + [ + /\((series40);/i, // Series 40 + ], + [NAME], + [ + /mozilla.+\(mobile;.+gecko.+firefox/i, // Firefox OS + ], + [[NAME, "Firefox OS"], VERSION], + [ + // Console + /(nintendo|playstation)\s([wids3portablevu]+)/i, // Nintendo/Playstation + + // GNU/Linux based + /(mint)[\/\s\(]?(\w+)*/i, // Mint + /(mageia|vectorlinux)[;\s]/i, // Mageia/VectorLinux + /(joli|[kxln]?ubuntu|debian|[open]*suse|gentoo|arch|slackware|fedora|mandriva|centos|pclinuxos|redhat|zenwalk|linpus)[\/\s-]?([\w\.-]+)*/i, + // Joli/Ubuntu/Debian/SUSE/Gentoo/Arch/Slackware + // Fedora/Mandriva/CentOS/PCLinuxOS/RedHat/Zenwalk/Linpus + /(hurd|linux)\s?([\w\.]+)*/i, // Hurd/Linux + /(gnu)\s?([\w\.]+)*/i, // GNU + ], + [NAME, VERSION], + [ + /(cros)\s[\w]+\s([\w\.]+\w)/i, // Chromium OS + ], + [[NAME, "Chromium OS"], VERSION], + [ + // Solaris + /(sunos)\s?([\w\.]+\d)*/i, // Solaris + ], + [[NAME, "Solaris"], VERSION], + [ + // BSD based + /\s([frentopc-]{0,4}bsd|dragonfly)\s?([\w\.]+)*/i, // FreeBSD/NetBSD/OpenBSD/PC-BSD/DragonFly + ], + [NAME, VERSION], + [ + /(ip[honead]+)(?:.*os\s*([\w]+)*\slike\smac|;\sopera)/i, // iOS + ], + [ + [NAME, "iOS"], + [VERSION, /_/g, "."], + ], + [ + /(mac\sos\sx)\s?([\w\s\.]+\w)*/i, + /(macintosh|mac(?=_powerpc)\s)/i, // Mac OS + ], + [ + [NAME, "Mac OS"], + [VERSION, /_/g, "."], + ], + [ + // Other + /((?:open)?solaris)[\/\s-]?([\w\.]+)*/i, // Solaris + /(haiku)\s(\w+)/i, // Haiku + /(aix)\s((\d)(?=\.|\)|\s)[\w\.]*)*/i, // AIX + /(plan\s9|minix|beos|os\/2|amigaos|morphos|risc\sos|openvms)/i, + // Plan9/Minix/BeOS/OS2/AmigaOS/MorphOS/RISCOS/OpenVMS + /(unix)\s?([\w\.]+)*/i, // UNIX + ], + [NAME, VERSION], + ], + }; + + ///////////////// + // Constructor + //////////////// + + var UAParser = function (uastring) { + var ua = + uastring || + (window && + window.navigator && + window.navigator.userAgent + ? window.navigator.userAgent + : EMPTY); + + this.getBrowser = function () { + return mapper.rgx.apply(this, regexes.browser); + }; + this.getEngine = function () { + return mapper.rgx.apply(this, regexes.engine); + }; + this.getOS = function () { + return mapper.rgx.apply(this, regexes.os); + }; + this.getResult = function () { + return { + ua: this.getUA(), + browser: this.getBrowser(), + engine: this.getEngine(), + os: this.getOS(), + }; + }; + this.getUA = function () { + return ua; + }; + this.setUA = function (uastring) { + ua = uastring; + return this; + }; + this.setUA(ua); + }; + + return UAParser; + })(); + + function version_compare(v1, v2, operator) { + // From: http://phpjs.org/functions + // + original by: Philippe Jausions (http://pear.php.net/user/jausions) + // + original by: Aidan Lister (http://aidanlister.com/) + // + reimplemented by: Kankrelune (http://www.webfaktory.info/) + // + improved by: Brett Zamir (http://brett-zamir.me) + // + improved by: Scott Baker + // + improved by: Theriault + // * example 1: version_compare('8.2.5rc', '8.2.5a'); + // * returns 1: 1 + // * example 2: version_compare('8.2.50', '8.2.52', '<'); + // * returns 2: true + // * example 3: version_compare('5.3.0-dev', '5.3.0'); + // * returns 3: -1 + // * example 4: version_compare('4.1.0.52','4.01.0.51'); + // * returns 4: 1 + + // Important: compare must be initialized at 0. + var i = 0, + x = 0, + compare = 0, + // vm maps textual PHP versions to negatives so they're less than 0. + // PHP currently defines these as CASE-SENSITIVE. It is important to + // leave these as negatives so that they can come before numerical versions + // and as if no letters were there to begin with. + // (1alpha is < 1 and < 1.1 but > 1dev1) + // If a non-numerical value can't be mapped to this table, it receives + // -7 as its value. + vm = { + dev: -6, + alpha: -5, + a: -5, + beta: -4, + b: -4, + RC: -3, + rc: -3, + "#": -2, + p: 1, + pl: 1, + }, + // This function will be called to prepare each version argument. + // It replaces every _, -, and + with a dot. + // It surrounds any nonsequence of numbers/dots with dots. + // It replaces sequences of dots with a single dot. + // version_compare('4..0', '4.0') == 0 + // Important: A string of 0 length needs to be converted into a value + // even less than an unexisting value in vm (-7), hence [-8]. + // It's also important to not strip spaces because of this. + // version_compare('', ' ') == 1 + prepVersion = function (v) { + v = ("" + v).replace(/[_\-+]/g, "."); + v = v + .replace(/([^.\d]+)/g, ".$1.") + .replace(/\.{2,}/g, "."); + return !v.length ? [-8] : v.split("."); + }, + // This converts a version component to a number. + // Empty component becomes 0. + // Non-numerical component becomes a negative number. + // Numerical component becomes itself as an integer. + numVersion = function (v) { + return !v + ? 0 + : isNaN(v) + ? vm[v] || -7 + : parseInt(v, 10); + }; + + v1 = prepVersion(v1); + v2 = prepVersion(v2); + x = Math.max(v1.length, v2.length); + for (i = 0; i < x; i++) { + if (v1[i] == v2[i]) { + continue; + } + v1[i] = numVersion(v1[i]); + v2[i] = numVersion(v2[i]); + if (v1[i] < v2[i]) { + compare = -1; + break; + } else if (v1[i] > v2[i]) { + compare = 1; + break; + } + } + if (!operator) { + return compare; + } + + // Important: operator is CASE-SENSITIVE. + // "No operator" seems to be treated as "<." + // Any other values seem to make the function return null. + switch (operator) { + case ">": + case "gt": + return compare > 0; + case ">=": + case "ge": + return compare >= 0; + case "<=": + case "le": + return compare <= 0; + case "==": + case "=": + case "eq": + return compare === 0; + case "<>": + case "!=": + case "ne": + return compare !== 0; + case "": + case "<": + case "lt": + return compare < 0; + default: + return null; + } + } + + var can = (function () { + var caps = { + define_property: (function () { + /* // currently too much extra code required, not exactly worth it + try { // as of IE8, getters/setters are supported only on DOM elements + var obj = {}; + if (Object.defineProperty) { + Object.defineProperty(obj, 'prop', { + enumerable: true, + configurable: true + }); + return true; + } + } catch(ex) {} + + if (Object.prototype.__defineGetter__ && Object.prototype.__defineSetter__) { + return true; + }*/ + return false; + })(), + + create_canvas: (function () { + // On the S60 and BB Storm, getContext exists, but always returns undefined + // so we actually have to call getContext() to verify + // github.com/Modernizr/Modernizr/issues/issue/97/ + var el = document.createElement("canvas"); + return !!(el.getContext && el.getContext("2d")); + })(), + + return_response_type: function (responseType) { + try { + if ( + Basic.inArray(responseType, [ + "", + "text", + "document", + ]) !== -1 + ) { + return true; + } else if (window.XMLHttpRequest) { + var xhr = new XMLHttpRequest(); + xhr.open("get", "/"); // otherwise Gecko throws an exception + if ("responseType" in xhr) { + xhr.responseType = responseType; + // as of 23.0.1271.64, Chrome switched from throwing exception to merely logging it to the console (why? o why?) + if (xhr.responseType !== responseType) { + return false; + } + return true; + } + } + } catch (ex) {} + return false; + }, + + // ideas for this heavily come from Modernizr (http://modernizr.com/) + use_data_uri: (function () { + var du = new Image(); + + du.onload = function () { + caps.use_data_uri = + du.width === 1 && du.height === 1; + }; + + setTimeout(function () { + du.src = + "data:image/gif;base64,R0lGODlhAQABAIAAAP8AAAAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="; + }, 1); + return false; + })(), + + use_data_uri_over32kb: function () { + // IE8 + return ( + caps.use_data_uri && + (Env.browser !== "IE" || Env.version >= 9) + ); + }, + + use_data_uri_of: function (bytes) { + return ( + (caps.use_data_uri && bytes < 33000) || + caps.use_data_uri_over32kb() + ); + }, + + use_fileinput: function () { + if ( + navigator.userAgent.match( + /(Android (1.0|1.1|1.5|1.6|2.0|2.1))|(Windows Phone (OS 7|8.0))|(XBLWP)|(ZuneWP)|(w(eb)?OSBrowser)|(webOS)|(Kindle\/(1.0|2.0|2.5|3.0))/ + ) + ) { + return false; + } + + var el = document.createElement("input"); + el.setAttribute("type", "file"); + return !el.disabled; + }, + }; + + return function (cap) { + var args = [].slice.call(arguments); + args.shift(); // shift of cap + return Basic.typeOf(caps[cap]) === "function" + ? caps[cap].apply(this, args) + : !!caps[cap]; + }; + })(); + + var uaResult = new UAParser().getResult(); + + var Env = { + can: can, + + uaParser: UAParser, + + browser: uaResult.browser.name, + version: uaResult.browser.version, + os: uaResult.os.name, // everybody intuitively types it in a lowercase for some reason + osVersion: uaResult.os.version, + + verComp: version_compare, + + swf_url: "../flash/Moxie.swf", + xap_url: "../silverlight/Moxie.xap", + global_event_dispatcher: + "moxie.core.EventTarget.instance.dispatchEvent", + }; + + // for backward compatibility + // @deprecated Use `Env.os` instead + Env.OS = Env.os; + + if (MXI_DEBUG) { + Env.debug = { + runtime: true, + events: false, + }; + + Env.log = function () { + function logObj(data) { + // TODO: this should recursively print out the object in a pretty way + console.appendChild( + document.createTextNode(data + "\n") + ); + } + + var data = arguments[0]; + + if (Basic.typeOf(data) === "string") { + data = Basic.sprintf.apply(this, arguments); + } + + if (window && window.console && window.console.log) { + window.console.log(data); + } else if (document) { + var console = document.getElementById("moxie-console"); + if (!console) { + console = document.createElement("pre"); + console.id = "moxie-console"; + //console.style.display = 'none'; + document.body.appendChild(console); + } + + if ( + Basic.inArray(Basic.typeOf(data), [ + "object", + "array", + ]) !== -1 + ) { + logObj(data); + } else { + console.appendChild( + document.createTextNode(data + "\n") + ); + } + } + }; + } + + return Env; + }); + + // Included from: src/javascript/core/Exceptions.js + + /** + * Exceptions.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/core/Exceptions", [ + "moxie/core/utils/Basic", + ], function (Basic) { + function _findKey(obj, value) { + var key; + for (key in obj) { + if (obj[key] === value) { + return key; + } + } + return null; + } + + /** + @class moxie/core/Exception + */ + return { + RuntimeError: (function () { + var namecodes = { + NOT_INIT_ERR: 1, + EXCEPTION_ERR: 3, + NOT_SUPPORTED_ERR: 9, + JS_ERR: 4, + }; + + function RuntimeError(code, message) { + this.code = code; + this.name = _findKey(namecodes, code); + this.message = + this.name + + (message || ": RuntimeError " + this.code); + } + + Basic.extend(RuntimeError, namecodes); + RuntimeError.prototype = Error.prototype; + return RuntimeError; + })(), + + OperationNotAllowedException: (function () { + function OperationNotAllowedException(code) { + this.code = code; + this.name = "OperationNotAllowedException"; + } + + Basic.extend(OperationNotAllowedException, { + NOT_ALLOWED_ERR: 1, + }); + + OperationNotAllowedException.prototype = Error.prototype; + + return OperationNotAllowedException; + })(), + + ImageError: (function () { + var namecodes = { + WRONG_FORMAT: 1, + MAX_RESOLUTION_ERR: 2, + INVALID_META_ERR: 3, + }; + + function ImageError(code) { + this.code = code; + this.name = _findKey(namecodes, code); + this.message = this.name + ": ImageError " + this.code; + } + + Basic.extend(ImageError, namecodes); + ImageError.prototype = Error.prototype; + + return ImageError; + })(), + + FileException: (function () { + var namecodes = { + NOT_FOUND_ERR: 1, + SECURITY_ERR: 2, + ABORT_ERR: 3, + NOT_READABLE_ERR: 4, + ENCODING_ERR: 5, + NO_MODIFICATION_ALLOWED_ERR: 6, + INVALID_STATE_ERR: 7, + SYNTAX_ERR: 8, + }; + + function FileException(code) { + this.code = code; + this.name = _findKey(namecodes, code); + this.message = + this.name + ": FileException " + this.code; + } + + Basic.extend(FileException, namecodes); + FileException.prototype = Error.prototype; + return FileException; + })(), + + DOMException: (function () { + var namecodes = { + INDEX_SIZE_ERR: 1, + DOMSTRING_SIZE_ERR: 2, + HIERARCHY_REQUEST_ERR: 3, + WRONG_DOCUMENT_ERR: 4, + INVALID_CHARACTER_ERR: 5, + NO_DATA_ALLOWED_ERR: 6, + NO_MODIFICATION_ALLOWED_ERR: 7, + NOT_FOUND_ERR: 8, + NOT_SUPPORTED_ERR: 9, + INUSE_ATTRIBUTE_ERR: 10, + INVALID_STATE_ERR: 11, + SYNTAX_ERR: 12, + INVALID_MODIFICATION_ERR: 13, + NAMESPACE_ERR: 14, + INVALID_ACCESS_ERR: 15, + VALIDATION_ERR: 16, + TYPE_MISMATCH_ERR: 17, + SECURITY_ERR: 18, + NETWORK_ERR: 19, + ABORT_ERR: 20, + URL_MISMATCH_ERR: 21, + QUOTA_EXCEEDED_ERR: 22, + TIMEOUT_ERR: 23, + INVALID_NODE_TYPE_ERR: 24, + DATA_CLONE_ERR: 25, + }; + + function DOMException(code) { + this.code = code; + this.name = _findKey(namecodes, code); + this.message = + this.name + ": DOMException " + this.code; + } + + Basic.extend(DOMException, namecodes); + DOMException.prototype = Error.prototype; + return DOMException; + })(), + + EventException: (function () { + function EventException(code) { + this.code = code; + this.name = "EventException"; + } + + Basic.extend(EventException, { + UNSPECIFIED_EVENT_TYPE_ERR: 0, + }); + + EventException.prototype = Error.prototype; + + return EventException; + })(), + }; + }); + + // Included from: src/javascript/core/utils/Dom.js + + /** + * Dom.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/core/utils/Dom", [ + "moxie/core/utils/Env", + ], function (Env) { + /** + Get DOM Element by it's id. + + @method get + @for Utils + @param {String} id Identifier of the DOM Element + @return {DOMElement} + */ + var get = function (id) { + if (typeof id !== "string") { + return id; + } + return document.getElementById(id); + }; + + /** + Checks if specified DOM element has specified class. + + @method hasClass + @static + @param {Object} obj DOM element like object to add handler to. + @param {String} name Class name + */ + var hasClass = function (obj, name) { + if (!obj.className) { + return false; + } + + var regExp = new RegExp("(^|\\s+)" + name + "(\\s+|$)"); + return regExp.test(obj.className); + }; + + /** + Adds specified className to specified DOM element. + + @method addClass + @static + @param {Object} obj DOM element like object to add handler to. + @param {String} name Class name + */ + var addClass = function (obj, name) { + if (!hasClass(obj, name)) { + obj.className = !obj.className + ? name + : obj.className.replace(/\s+$/, "") + " " + name; + } + }; + + /** + Removes specified className from specified DOM element. + + @method removeClass + @static + @param {Object} obj DOM element like object to add handler to. + @param {String} name Class name + */ + var removeClass = function (obj, name) { + if (obj.className) { + var regExp = new RegExp("(^|\\s+)" + name + "(\\s+|$)"); + obj.className = obj.className.replace( + regExp, + function ($0, $1, $2) { + return $1 === " " && $2 === " " ? " " : ""; + } + ); + } + }; + + /** + Returns a given computed style of a DOM element. + + @method getStyle + @static + @param {Object} obj DOM element like object. + @param {String} name Style you want to get from the DOM element + */ + var getStyle = function (obj, name) { + if (obj.currentStyle) { + return obj.currentStyle[name]; + } else if (window.getComputedStyle) { + return window.getComputedStyle(obj, null)[name]; + } + }; + + /** + Returns the absolute x, y position of an Element. The position will be returned in a object with x, y fields. + + @method getPos + @static + @param {Element} node HTML element or element id to get x, y position from. + @param {Element} root Optional root element to stop calculations at. + @return {object} Absolute position of the specified element object with x, y fields. + */ + var getPos = function (node, root) { + var x = 0, + y = 0, + parent, + doc = document, + nodeRect, + rootRect; + + node = node; + root = root || doc.body; + + // Returns the x, y cordinate for an element on IE 6 and IE 7 + function getIEPos(node) { + var bodyElm, + rect, + x = 0, + y = 0; + + if (node) { + rect = node.getBoundingClientRect(); + bodyElm = + doc.compatMode === "CSS1Compat" + ? doc.documentElement + : doc.body; + x = rect.left + bodyElm.scrollLeft; + y = rect.top + bodyElm.scrollTop; + } + + return { + x: x, + y: y, + }; + } + + // Use getBoundingClientRect on IE 6 and IE 7 but not on IE 8 in standards mode + if ( + node && + node.getBoundingClientRect && + Env.browser === "IE" && + (!doc.documentMode || doc.documentMode < 8) + ) { + nodeRect = getIEPos(node); + rootRect = getIEPos(root); + + return { + x: nodeRect.x - rootRect.x, + y: nodeRect.y - rootRect.y, + }; + } + + parent = node; + while (parent && parent != root && parent.nodeType) { + x += parent.offsetLeft || 0; + y += parent.offsetTop || 0; + parent = parent.offsetParent; + } + + parent = node.parentNode; + while (parent && parent != root && parent.nodeType) { + x -= parent.scrollLeft || 0; + y -= parent.scrollTop || 0; + parent = parent.parentNode; + } + + return { + x: x, + y: y, + }; + }; + + /** + Returns the size of the specified node in pixels. + + @method getSize + @static + @param {Node} node Node to get the size of. + @return {Object} Object with a w and h property. + */ + var getSize = function (node) { + return { + w: node.offsetWidth || node.clientWidth, + h: node.offsetHeight || node.clientHeight, + }; + }; + + return { + get: get, + hasClass: hasClass, + addClass: addClass, + removeClass: removeClass, + getStyle: getStyle, + getPos: getPos, + getSize: getSize, + }; + }); + + // Included from: src/javascript/core/EventTarget.js + + /** + * EventTarget.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/core/EventTarget", [ + "moxie/core/utils/Env", + "moxie/core/Exceptions", + "moxie/core/utils/Basic", + ], function (Env, x, Basic) { + // hash of event listeners by object uid + var eventpool = {}; + + /** + Parent object for all event dispatching components and objects + + @class moxie/core/EventTarget + @constructor EventTarget + */ + function EventTarget() { + /** + Unique id of the event dispatcher, usually overriden by children + + @property uid + @type String + */ + this.uid = Basic.guid(); + } + + Basic.extend(EventTarget.prototype, { + /** + Can be called from within a child in order to acquire uniqie id in automated manner + + @method init + */ + init: function () { + if (!this.uid) { + this.uid = Basic.guid("uid_"); + } + }, + + /** + Register a handler to a specific event dispatched by the object + + @method addEventListener + @param {String} type Type or basically a name of the event to subscribe to + @param {Function} fn Callback function that will be called when event happens + @param {Number} [priority=0] Priority of the event handler - handlers with higher priorities will be called first + @param {Object} [scope=this] A scope to invoke event handler in + */ + addEventListener: function (type, fn, priority, scope) { + var self = this, + list; + + // without uid no event handlers can be added, so make sure we got one + if (!this.hasOwnProperty("uid")) { + this.uid = Basic.guid("uid_"); + } + + type = Basic.trim(type); + + if (/\s/.test(type)) { + // multiple event types were passed for one handler + Basic.each(type.split(/\s+/), function (type) { + self.addEventListener(type, fn, priority, scope); + }); + return; + } + + type = type.toLowerCase(); + priority = parseInt(priority, 10) || 0; + + list = + (eventpool[this.uid] && eventpool[this.uid][type]) || + []; + list.push({ + fn: fn, + priority: priority, + scope: scope || this, + }); + + if (!eventpool[this.uid]) { + eventpool[this.uid] = {}; + } + eventpool[this.uid][type] = list; + }, + + /** + Check if any handlers were registered to the specified event + + @method hasEventListener + @param {String} [type] Type or basically a name of the event to check + @return {Mixed} Returns a handler if it was found and false, if - not + */ + hasEventListener: function (type) { + var list; + if (type) { + type = type.toLowerCase(); + list = eventpool[this.uid] && eventpool[this.uid][type]; + } else { + list = eventpool[this.uid]; + } + return list ? list : false; + }, + + /** + Unregister the handler from the event, or if former was not specified - unregister all handlers + + @method removeEventListener + @param {String} type Type or basically a name of the event + @param {Function} [fn] Handler to unregister + */ + removeEventListener: function (type, fn) { + var self = this, + list, + i; + + type = type.toLowerCase(); + + if (/\s/.test(type)) { + // multiple event types were passed for one handler + Basic.each(type.split(/\s+/), function (type) { + self.removeEventListener(type, fn); + }); + return; + } + + list = eventpool[this.uid] && eventpool[this.uid][type]; + + if (list) { + if (fn) { + for (i = list.length - 1; i >= 0; i--) { + if (list[i].fn === fn) { + list.splice(i, 1); + break; + } + } + } else { + list = []; + } + + // delete event list if it has become empty + if (!list.length) { + delete eventpool[this.uid][type]; + + // and object specific entry in a hash if it has no more listeners attached + if (Basic.isEmptyObj(eventpool[this.uid])) { + delete eventpool[this.uid]; + } + } + } + }, + + /** + Remove all event handlers from the object + + @method removeAllEventListeners + */ + removeAllEventListeners: function () { + if (eventpool[this.uid]) { + delete eventpool[this.uid]; + } + }, + + /** + Dispatch the event + + @method dispatchEvent + @param {String/Object} Type of event or event object to dispatch + @param {Mixed} [...] Variable number of arguments to be passed to a handlers + @return {Boolean} true by default and false if any handler returned false + */ + dispatchEvent: function (type) { + var uid, + list, + args, + tmpEvt, + evt = {}, + result = true, + undef; + + if (Basic.typeOf(type) !== "string") { + // we can't use original object directly (because of Silverlight) + tmpEvt = type; + + if (Basic.typeOf(tmpEvt.type) === "string") { + type = tmpEvt.type; + + if ( + tmpEvt.total !== undef && + tmpEvt.loaded !== undef + ) { + // progress event + evt.total = tmpEvt.total; + evt.loaded = tmpEvt.loaded; + } + evt.async = tmpEvt.async || false; + } else { + throw new x.EventException( + x.EventException.UNSPECIFIED_EVENT_TYPE_ERR + ); + } + } + + // check if event is meant to be dispatched on an object having specific uid + if (type.indexOf("::") !== -1) { + (function (arr) { + uid = arr[0]; + type = arr[1]; + })(type.split("::")); + } else { + uid = this.uid; + } + + type = type.toLowerCase(); + + list = eventpool[uid] && eventpool[uid][type]; + + if (list) { + // sort event list by prority + list.sort(function (a, b) { + return b.priority - a.priority; + }); + + args = [].slice.call(arguments); + + // first argument will be pseudo-event object + args.shift(); + evt.type = type; + args.unshift(evt); + + if (MXI_DEBUG && Env.debug.events) { + Env.log("Event '%s' fired on %u", evt.type, uid); + } + + // Dispatch event to all listeners + var queue = []; + Basic.each(list, function (handler) { + // explicitly set the target, otherwise events fired from shims do not get it + args[0].target = handler.scope; + // if event is marked as async, detach the handler + if (evt.async) { + queue.push(function (cb) { + setTimeout(function () { + cb( + handler.fn.apply( + handler.scope, + args + ) === false + ); + }, 1); + }); + } else { + queue.push(function (cb) { + cb( + handler.fn.apply( + handler.scope, + args + ) === false + ); // if handler returns false stop propagation + }); + } + }); + if (queue.length) { + Basic.inSeries(queue, function (err) { + result = !err; + }); + } + } + return result; + }, + + /** + Register a handler to the event type that will run only once + + @method bindOnce + @since >1.4.1 + @param {String} type Type or basically a name of the event to subscribe to + @param {Function} fn Callback function that will be called when event happens + @param {Number} [priority=0] Priority of the event handler - handlers with higher priorities will be called first + @param {Object} [scope=this] A scope to invoke event handler in + */ + bindOnce: function (type, fn, priority, scope) { + var self = this; + self.bind.call( + this, + type, + function cb() { + self.unbind(type, cb); + return fn.apply(this, arguments); + }, + priority, + scope + ); + }, + + /** + Alias for addEventListener + + @method bind + @protected + */ + bind: function () { + this.addEventListener.apply(this, arguments); + }, + + /** + Alias for removeEventListener + + @method unbind + @protected + */ + unbind: function () { + this.removeEventListener.apply(this, arguments); + }, + + /** + Alias for removeAllEventListeners + + @method unbindAll + @protected + */ + unbindAll: function () { + this.removeAllEventListeners.apply(this, arguments); + }, + + /** + Alias for dispatchEvent + + @method trigger + @protected + */ + trigger: function () { + return this.dispatchEvent.apply(this, arguments); + }, + + /** + Handle properties of on[event] type. + + @method handleEventProps + @private + */ + handleEventProps: function (dispatches) { + var self = this; + + this.bind(dispatches.join(" "), function (e) { + var prop = "on" + e.type.toLowerCase(); + if (Basic.typeOf(this[prop]) === "function") { + this[prop].apply(this, arguments); + } + }); + + // object must have defined event properties, even if it doesn't make use of them + Basic.each(dispatches, function (prop) { + prop = "on" + prop.toLowerCase(prop); + if (Basic.typeOf(self[prop]) === "undefined") { + self[prop] = null; + } + }); + }, + }); + + EventTarget.instance = new EventTarget(); + + return EventTarget; + }); + + // Included from: src/javascript/runtime/Runtime.js + + /** + * Runtime.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/runtime/Runtime", [ + "moxie/core/utils/Env", + "moxie/core/utils/Basic", + "moxie/core/utils/Dom", + "moxie/core/EventTarget", + ], function (Env, Basic, Dom, EventTarget) { + var runtimeConstructors = {}, + runtimes = {}; + + /** + Common set of methods and properties for every runtime instance + + @class moxie/runtime/Runtime + + @param {Object} options + @param {String} type Sanitized name of the runtime + @param {Object} [caps] Set of capabilities that differentiate specified runtime + @param {Object} [modeCaps] Set of capabilities that do require specific operational mode + @param {String} [preferredMode='browser'] Preferred operational mode to choose if no required capabilities were requested + */ + function Runtime(options, type, caps, modeCaps, preferredMode) { + /** + Dispatched when runtime is initialized and ready. + Results in RuntimeInit on a connected component. + + @event Init + */ + + /** + Dispatched when runtime fails to initialize. + Results in RuntimeError on a connected component. + + @event Error + */ + + var self = this, + _shim, + _uid = Basic.guid(type + "_"), + defaultMode = preferredMode || "browser"; + options = options || {}; + + // register runtime in private hash + runtimes[_uid] = this; + + /** + Default set of capabilities, which can be redifined later by specific runtime + + @private + @property caps + @type Object + */ + caps = Basic.extend( + { + // Runtime can: + // provide access to raw binary data of the file + access_binary: false, + // provide access to raw binary data of the image (image extension is optional) + access_image_binary: false, + // display binary data as thumbs for example + display_media: false, + // make cross-domain requests + do_cors: false, + // accept files dragged and dropped from the desktop + drag_and_drop: false, + // filter files in selection dialog by their extensions + filter_by_extension: true, + // resize image (and manipulate it raw data of any file in general) + resize_image: false, + // periodically report how many bytes of total in the file were uploaded (loaded) + report_upload_progress: false, + // provide access to the headers of http response + return_response_headers: false, + // support response of specific type, which should be passed as an argument + // e.g. runtime.can('return_response_type', 'blob') + return_response_type: false, + // return http status code of the response + return_status_code: true, + // send custom http header with the request + send_custom_headers: false, + // pick up the files from a dialog + select_file: false, + // select whole folder in file browse dialog + select_folder: false, + // select multiple files at once in file browse dialog + select_multiple: true, + // send raw binary data, that is generated after image resizing or manipulation of other kind + send_binary_string: false, + // send cookies with http request and therefore retain session + send_browser_cookies: true, + // send data formatted as multipart/form-data + send_multipart: true, + // slice the file or blob to smaller parts + slice_blob: false, + // upload file without preloading it to memory, stream it out directly from disk + stream_upload: false, + // programmatically trigger file browse dialog + summon_file_dialog: false, + // upload file of specific size, size should be passed as argument + // e.g. runtime.can('upload_filesize', '500mb') + upload_filesize: true, + // initiate http request with specific http method, method should be passed as argument + // e.g. runtime.can('use_http_method', 'put') + use_http_method: true, + }, + caps + ); + + // default to the mode that is compatible with preferred caps + if (options.preferred_caps) { + defaultMode = Runtime.getMode( + modeCaps, + options.preferred_caps, + defaultMode + ); + } + + if (MXI_DEBUG && Env.debug.runtime) { + Env.log("\tdefault mode: %s", defaultMode); + } + + // small extension factory here (is meant to be extended with actual extensions constructors) + _shim = (function () { + var objpool = {}; + return { + exec: function (uid, comp, fn, args) { + if (_shim[comp]) { + if (!objpool[uid]) { + objpool[uid] = { + context: this, + instance: new _shim[comp](), + }; + } + if (objpool[uid].instance[fn]) { + return objpool[uid].instance[fn].apply( + this, + args + ); + } + } + }, + + removeInstance: function (uid) { + delete objpool[uid]; + }, + + removeAllInstances: function () { + var self = this; + Basic.each(objpool, function (obj, uid) { + if ( + Basic.typeOf(obj.instance.destroy) === + "function" + ) { + obj.instance.destroy.call(obj.context); + } + self.removeInstance(uid); + }); + }, + }; + })(); + + // public methods + Basic.extend(this, { + /** + Specifies whether runtime instance was initialized or not + + @property initialized + @type {Boolean} + @default false + */ + initialized: false, // shims require this flag to stop initialization retries + + /** + Unique ID of the runtime + + @property uid + @type {String} + */ + uid: _uid, + + /** + Runtime type (e.g. flash, html5, etc) + + @property type + @type {String} + */ + type: type, + + /** + Runtime (not native one) may operate in browser or client mode. + + @property mode + @private + @type {String|Boolean} current mode or false, if none possible + */ + mode: Runtime.getMode( + modeCaps, + options.required_caps, + defaultMode + ), + + /** + id of the DOM container for the runtime (if available) + + @property shimid + @type {String} + */ + shimid: _uid + "_container", + + /** + Number of connected clients. If equal to zero, runtime can be destroyed + + @property clients + @type {Number} + */ + clients: 0, + + /** + Runtime initialization options + + @property options + @type {Object} + */ + options: options, + + /** + Checks if the runtime has specific capability + + @method can + @param {String} cap Name of capability to check + @param {Mixed} [value] If passed, capability should somehow correlate to the value + @param {Object} [refCaps] Set of capabilities to check the specified cap against (defaults to internal set) + @return {Boolean} true if runtime has such capability and false, if - not + */ + can: function (cap, value) { + var refCaps = arguments[2] || caps; + + // if cap var is a comma-separated list of caps, convert it to object (key/value) + if ( + Basic.typeOf(cap) === "string" && + Basic.typeOf(value) === "undefined" + ) { + cap = Runtime.parseCaps(cap); + } + + if (Basic.typeOf(cap) === "object") { + for (var key in cap) { + if (!this.can(key, cap[key], refCaps)) { + return false; + } + } + return true; + } + + // check the individual cap + if (Basic.typeOf(refCaps[cap]) === "function") { + return refCaps[cap].call(this, value); + } else { + return value === refCaps[cap]; + } + }, + + /** + Returns container for the runtime as DOM element + + @method getShimContainer + @return {DOMElement} + */ + getShimContainer: function () { + var container, + shimContainer = Dom.get(this.shimid); + + // if no container for shim, create one + if (!shimContainer) { + container = + Dom.get(this.options.container) || + document.body; + + // create shim container and insert it at an absolute position into the outer container + shimContainer = document.createElement("div"); + shimContainer.id = this.shimid; + shimContainer.className = + "moxie-shim moxie-shim-" + this.type; + + Basic.extend(shimContainer.style, { + position: "absolute", + top: "0px", + left: "0px", + width: "1px", + height: "1px", + overflow: "hidden", + }); + + container.appendChild(shimContainer); + container = null; + } + + return shimContainer; + }, + + /** + Returns runtime as DOM element (if appropriate) + + @method getShim + @return {DOMElement} + */ + getShim: function () { + return _shim; + }, + + /** + Invokes a method within the runtime itself (might differ across the runtimes) + + @method shimExec + @param {Mixed} [] + @protected + @return {Mixed} Depends on the action and component + */ + shimExec: function (component, action) { + var args = [].slice.call(arguments, 2); + return self + .getShim() + .exec.call(this, this.uid, component, action, args); + }, + + /** + Operaional interface that is used by components to invoke specific actions on the runtime + (is invoked in the scope of component) + + @method exec + @param {Mixed} []* + @protected + @return {Mixed} Depends on the action and component + */ + exec: function (component, action) { + // this is called in the context of component, not runtime + var args = [].slice.call(arguments, 2); + + if (self[component] && self[component][action]) { + return self[component][action].apply(this, args); + } + return self.shimExec.apply(this, arguments); + }, + + /** + Destroys the runtime (removes all events and deletes DOM structures) + + @method destroy + */ + destroy: function () { + if (!self) { + return; // obviously already destroyed + } + + var shimContainer = Dom.get(this.shimid); + if (shimContainer) { + shimContainer.parentNode.removeChild(shimContainer); + } + + if (_shim) { + _shim.removeAllInstances(); + } + + this.unbindAll(); + delete runtimes[this.uid]; + this.uid = null; // mark this runtime as destroyed + _uid = self = _shim = shimContainer = null; + }, + }); + + // once we got the mode, test against all caps + if ( + this.mode && + options.required_caps && + !this.can(options.required_caps) + ) { + this.mode = false; + } + } + + /** + Default order to try different runtime types + + @property order + @type String + @static + */ + Runtime.order = "html5,flash,silverlight,html4"; + + /** + Retrieves runtime from private hash by it's uid + + @method getRuntime + @private + @static + @param {String} uid Unique identifier of the runtime + @return {Runtime|Boolean} Returns runtime, if it exists and false, if - not + */ + Runtime.getRuntime = function (uid) { + return runtimes[uid] ? runtimes[uid] : false; + }; + + /** + Register constructor for the Runtime of new (or perhaps modified) type + + @method addConstructor + @static + @param {String} type Runtime type (e.g. flash, html5, etc) + @param {Function} construct Constructor for the Runtime type + */ + Runtime.addConstructor = function (type, constructor) { + constructor.prototype = EventTarget.instance; + runtimeConstructors[type] = constructor; + }; + + /** + Get the constructor for the specified type. + + method getConstructor + @static + @param {String} type Runtime type (e.g. flash, html5, etc) + @return {Function} Constructor for the Runtime type + */ + Runtime.getConstructor = function (type) { + return runtimeConstructors[type] || null; + }; + + /** + Get info about the runtime (uid, type, capabilities) + + @method getInfo + @static + @param {String} uid Unique identifier of the runtime + @return {Mixed} Info object or null if runtime doesn't exist + */ + Runtime.getInfo = function (uid) { + var runtime = Runtime.getRuntime(uid); + + if (runtime) { + return { + uid: runtime.uid, + type: runtime.type, + mode: runtime.mode, + can: function () { + return runtime.can.apply(runtime, arguments); + }, + }; + } + return null; + }; + + /** + Convert caps represented by a comma-separated string to the object representation. + + @method parseCaps + @static + @param {String} capStr Comma-separated list of capabilities + @return {Object} + */ + Runtime.parseCaps = function (capStr) { + var capObj = {}; + + if (Basic.typeOf(capStr) !== "string") { + return capStr || {}; + } + + Basic.each(capStr.split(","), function (key) { + capObj[key] = true; // we assume it to be - true + }); + + return capObj; + }; + + /** + Test the specified runtime for specific capabilities. + + @method can + @static + @param {String} type Runtime type (e.g. flash, html5, etc) + @param {String|Object} caps Set of capabilities to check + @return {Boolean} Result of the test + */ + Runtime.can = function (type, caps) { + var runtime, + constructor = Runtime.getConstructor(type), + mode; + if (constructor) { + runtime = new constructor({ + required_caps: caps, + }); + mode = runtime.mode; + runtime.destroy(); + return !!mode; + } + return false; + }; + + /** + Figure out a runtime that supports specified capabilities. + + @method thatCan + @static + @param {String|Object} caps Set of capabilities to check + @param {String} [runtimeOrder] Comma-separated list of runtimes to check against + @return {String} Usable runtime identifier or null + */ + Runtime.thatCan = function (caps, runtimeOrder) { + var types = (runtimeOrder || Runtime.order).split(/\s*,\s*/); + for (var i in types) { + if (Runtime.can(types[i], caps)) { + return types[i]; + } + } + return null; + }; + + /** + Figure out an operational mode for the specified set of capabilities. + + @method getMode + @static + @param {Object} modeCaps Set of capabilities that depend on particular runtime mode + @param {Object} [requiredCaps] Supplied set of capabilities to find operational mode for + @param {String|Boolean} [defaultMode='browser'] Default mode to use + @return {String|Boolean} Compatible operational mode + */ + Runtime.getMode = function (modeCaps, requiredCaps, defaultMode) { + var mode = null; + + if (Basic.typeOf(defaultMode) === "undefined") { + // only if not specified + defaultMode = "browser"; + } + + if (requiredCaps && !Basic.isEmptyObj(modeCaps)) { + // loop over required caps and check if they do require the same mode + Basic.each(requiredCaps, function (value, cap) { + if (modeCaps.hasOwnProperty(cap)) { + var capMode = modeCaps[cap](value); + + // make sure we always have an array + if (typeof capMode === "string") { + capMode = [capMode]; + } + + if (!mode) { + mode = capMode; + } else if ( + !(mode = Basic.arrayIntersect(mode, capMode)) + ) { + // if cap requires conflicting mode - runtime cannot fulfill required caps + + if (MXI_DEBUG && Env.debug.runtime) { + Env.log( + "\t\t%c: %v (conflicting mode requested: %s)", + cap, + value, + capMode + ); + } + + return (mode = false); + } + } + + if (MXI_DEBUG && Env.debug.runtime) { + Env.log( + "\t\t%c: %v (compatible modes: %s)", + cap, + value, + mode + ); + } + }); + + if (mode) { + return Basic.inArray(defaultMode, mode) !== -1 + ? defaultMode + : mode[0]; + } else if (mode === false) { + return false; + } + } + return defaultMode; + }; + + /** + Capability check that always returns true + + @private + @static + @return {True} + */ + Runtime.capTrue = function () { + return true; + }; + + /** + Capability check that always returns false + + @private + @static + @return {False} + */ + Runtime.capFalse = function () { + return false; + }; + + /** + Evaluate the expression to boolean value and create a function that always returns it. + + @private + @static + @param {Mixed} expr Expression to evaluate + @return {Function} Function returning the result of evaluation + */ + Runtime.capTest = function (expr) { + return function () { + return !!expr; + }; + }; + + return Runtime; + }); + + // Included from: src/javascript/runtime/RuntimeClient.js + + /** + * RuntimeClient.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/runtime/RuntimeClient", [ + "moxie/core/utils/Env", + "moxie/core/Exceptions", + "moxie/core/utils/Basic", + "moxie/runtime/Runtime", + ], function (Env, x, Basic, Runtime) { + /** + Set of methods and properties, required by a component to acquire ability to connect to a runtime + + @class moxie/runtime/RuntimeClient + */ + return function RuntimeClient() { + var runtime; + + Basic.extend(this, { + /** + Connects to the runtime specified by the options. Will either connect to existing runtime or create a new one. + Increments number of clients connected to the specified runtime. + + @private + @method connectRuntime + @param {Mixed} options Can be a runtme uid or a set of key-value pairs defining requirements and pre-requisites + */ + connectRuntime: function (options) { + var comp = this, + ruid; + + function initialize(items) { + var type, constructor; + + // if we ran out of runtimes + if (!items.length) { + comp.trigger( + "RuntimeError", + new x.RuntimeError( + x.RuntimeError.NOT_INIT_ERR + ) + ); + runtime = null; + return; + } + + type = items.shift().toLowerCase(); + constructor = Runtime.getConstructor(type); + if (!constructor) { + if (MXI_DEBUG && Env.debug.runtime) { + Env.log( + "Constructor for '%s' runtime is not available.", + type + ); + } + initialize(items); + return; + } + + if (MXI_DEBUG && Env.debug.runtime) { + Env.log("Trying runtime: %s", type); + Env.log(options); + } + + // try initializing the runtime + runtime = new constructor(options); + + runtime.bind("Init", function () { + // mark runtime as initialized + runtime.initialized = true; + + if (MXI_DEBUG && Env.debug.runtime) { + Env.log( + "Runtime '%s' initialized", + runtime.type + ); + } + + // jailbreak ... + setTimeout(function () { + runtime.clients++; + comp.ruid = runtime.uid; + // this will be triggered on component + comp.trigger("RuntimeInit", runtime); + }, 1); + }); + + runtime.bind("Error", function () { + if (MXI_DEBUG && Env.debug.runtime) { + Env.log( + "Runtime '%s' failed to initialize", + runtime.type + ); + } + + runtime.destroy(); // runtime cannot destroy itself from inside at a right moment, thus we do it here + initialize(items); + }); + + runtime.bind("Exception", function (e, err) { + var message = + err.name + + "(#" + + err.code + + ")" + + (err.message + ? ", from: " + err.message + : ""); + + if (MXI_DEBUG && Env.debug.runtime) { + Env.log( + "Runtime '%s' has thrown an exception: %s", + this.type, + message + ); + } + comp.trigger( + "RuntimeError", + new x.RuntimeError( + x.RuntimeError.EXCEPTION_ERR, + message + ) + ); + }); + + if (MXI_DEBUG && Env.debug.runtime) { + Env.log("\tselected mode: %s", runtime.mode); + } + + // check if runtime managed to pick-up operational mode + if (!runtime.mode) { + runtime.trigger("Error"); + return; + } + + runtime.init(); + } + + // check if a particular runtime was requested + if (Basic.typeOf(options) === "string") { + ruid = options; + } else if (Basic.typeOf(options.ruid) === "string") { + ruid = options.ruid; + } + + if (ruid) { + runtime = Runtime.getRuntime(ruid); + if (runtime) { + comp.ruid = ruid; + runtime.clients++; + return runtime; + } else { + // there should be a runtime and there's none - weird case + throw new x.RuntimeError( + x.RuntimeError.NOT_INIT_ERR + ); + } + } + + // initialize a fresh one, that fits runtime list and required features best + initialize( + (options.runtime_order || Runtime.order).split( + /\s*,\s*/ + ) + ); + }, + + /** + Disconnects from the runtime. Decrements number of clients connected to the specified runtime. + + @private + @method disconnectRuntime + */ + disconnectRuntime: function () { + if (runtime && --runtime.clients <= 0) { + runtime.destroy(); + } + + // once the component is disconnected, it shouldn't have access to the runtime + runtime = null; + }, + + /** + Returns the runtime to which the client is currently connected. + + @method getRuntime + @return {Runtime} Runtime or null if client is not connected + */ + getRuntime: function () { + if (runtime && runtime.uid) { + return runtime; + } + return (runtime = null); // make sure we do not leave zombies rambling around + }, + + /** + Handy shortcut to safely invoke runtime extension methods. + + @private + @method exec + @return {Mixed} Whatever runtime extension method returns + */ + exec: function () { + return runtime + ? runtime.exec.apply(this, arguments) + : null; + }, + + /** + Test runtime client for specific capability + + @method can + @param {String} cap + @return {Bool} + */ + can: function (cap) { + return runtime ? runtime.can(cap) : false; + }, + }); + }; + }); + + // Included from: src/javascript/file/Blob.js + + /** + * Blob.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/file/Blob", [ + "moxie/core/utils/Basic", + "moxie/core/utils/Encode", + "moxie/runtime/RuntimeClient", + ], function (Basic, Encode, RuntimeClient) { + var blobpool = {}; + + /** + @class moxie/file/Blob + @constructor + @param {String} ruid Unique id of the runtime, to which this blob belongs to + @param {Object} blob Object "Native" blob object, as it is represented in the runtime + */ + function Blob(ruid, blob) { + function _sliceDetached(start, end, type) { + var blob, + data = blobpool[this.uid]; + + if (Basic.typeOf(data) !== "string" || !data.length) { + return null; // or throw exception + } + + blob = new Blob(null, { + type: type, + size: end - start, + }); + blob.detach(data.substr(start, blob.size)); + + return blob; + } + + RuntimeClient.call(this); + + if (ruid) { + this.connectRuntime(ruid); + } + + if (!blob) { + blob = {}; + } else if (Basic.typeOf(blob) === "string") { + // dataUrl or binary string + blob = { data: blob }; + } + + Basic.extend(this, { + /** + Unique id of the component + + @property uid + @type {String} + */ + uid: blob.uid || Basic.guid("uid_"), + + /** + Unique id of the connected runtime, if falsy, then runtime will have to be initialized + before this Blob can be used, modified or sent + + @property ruid + @type {String} + */ + ruid: ruid, + + /** + Size of blob + + @property size + @type {Number} + @default 0 + */ + size: blob.size || 0, + + /** + Mime type of blob + + @property type + @type {String} + @default '' + */ + type: blob.type || "", + + /** + @method slice + @param {Number} [start=0] + */ + slice: function (start, end, type) { + if (this.isDetached()) { + return _sliceDetached.apply(this, arguments); + } + return this.getRuntime().exec.call( + this, + "Blob", + "slice", + this.getSource(), + start, + end, + type + ); + }, + + /** + Returns "native" blob object (as it is represented in connected runtime) or null if not found + + @method getSource + @return {Blob} Returns "native" blob object or null if not found + */ + getSource: function () { + if (!blobpool[this.uid]) { + return null; + } + return blobpool[this.uid]; + }, + + /** + Detaches blob from any runtime that it depends on and initialize with standalone value + + @method detach + @protected + @param {DOMString} [data=''] Standalone value + */ + detach: function (data) { + if (this.ruid) { + this.getRuntime().exec.call( + this, + "Blob", + "destroy" + ); + this.disconnectRuntime(); + this.ruid = null; + } + + data = data || ""; + + // if dataUrl, convert to binary string + if (data.substr(0, 5) == "data:") { + var base64Offset = data.indexOf(";base64,"); + this.type = data.substring(5, base64Offset); + data = Encode.atob( + data.substring(base64Offset + 8) + ); + } + + this.size = data.length; + + blobpool[this.uid] = data; + }, + + /** + Checks if blob is standalone (detached of any runtime) + + @method isDetached + @protected + @return {Boolean} + */ + isDetached: function () { + return ( + !this.ruid && + Basic.typeOf(blobpool[this.uid]) === "string" + ); + }, + + /** + Destroy Blob and free any resources it was using + + @method destroy + */ + destroy: function () { + this.detach(); + delete blobpool[this.uid]; + }, + }); + + if (blob.data) { + this.detach(blob.data); // auto-detach if payload has been passed + } else { + blobpool[this.uid] = blob; + } + } + + return Blob; + }); + + // Included from: src/javascript/core/I18n.js + + /** + * I18n.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/core/I18n", ["moxie/core/utils/Basic"], function (Basic) { + var i18n = {}; + + /** + @class moxie/core/I18n + */ + return { + /** + * Extends the language pack object with new items. + * + * @param {Object} pack Language pack items to add. + * @return {Object} Extended language pack object. + */ + addI18n: function (pack) { + return Basic.extend(i18n, pack); + }, + + /** + * Translates the specified string by checking for the english string in the language pack lookup. + * + * @param {String} str String to look for. + * @return {String} Translated string or the input string if it wasn't found. + */ + translate: function (str) { + return i18n[str] || str; + }, + + /** + * Shortcut for translate function + * + * @param {String} str String to look for. + * @return {String} Translated string or the input string if it wasn't found. + */ + _: function (str) { + return this.translate(str); + }, + + /** + * Pseudo sprintf implementation - simple way to replace tokens with specified values. + * + * @param {String} str String with tokens + * @return {String} String with replaced tokens + */ + sprintf: function (str) { + var args = [].slice.call(arguments, 1); + + return str.replace(/%[a-z]/g, function () { + var value = args.shift(); + return Basic.typeOf(value) !== "undefined" ? value : ""; + }); + }, + }; + }); + + // Included from: src/javascript/core/utils/Mime.js + + /** + * Mime.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/core/utils/Mime", [ + "moxie/core/utils/Basic", + "moxie/core/I18n", + ], function (Basic, I18n) { + var mimeData = + "" + + "application/msword,doc dot," + + "application/pdf,pdf," + + "application/pgp-signature,pgp," + + "application/postscript,ps ai eps," + + "application/rtf,rtf," + + "application/vnd.ms-excel,xls xlb," + + "application/vnd.ms-powerpoint,ppt pps pot," + + "application/zip,zip," + + "application/x-shockwave-flash,swf swfl," + + "application/vnd.openxmlformats-officedocument.wordprocessingml.document,docx," + + "application/vnd.openxmlformats-officedocument.wordprocessingml.template,dotx," + + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,xlsx," + + "application/vnd.openxmlformats-officedocument.presentationml.presentation,pptx," + + "application/vnd.openxmlformats-officedocument.presentationml.template,potx," + + "application/vnd.openxmlformats-officedocument.presentationml.slideshow,ppsx," + + "application/x-javascript,js," + + "application/json,json," + + "audio/mpeg,mp3 mpga mpega mp2," + + "audio/x-wav,wav," + + "audio/x-m4a,m4a," + + "audio/ogg,oga ogg," + + "audio/aiff,aiff aif," + + "audio/flac,flac," + + "audio/aac,aac," + + "audio/ac3,ac3," + + "audio/x-ms-wma,wma," + + "image/bmp,bmp," + + "image/gif,gif," + + "image/jpeg,jpg jpeg jpe," + + "image/photoshop,psd," + + "image/png,png," + + "image/svg+xml,svg svgz," + + "image/tiff,tiff tif," + + "text/plain,asc txt text diff log," + + "text/html,htm html xhtml," + + "text/css,css," + + "text/csv,csv," + + "text/rtf,rtf," + + "video/mpeg,mpeg mpg mpe m2v," + + "video/quicktime,qt mov," + + "video/mp4,mp4," + + "video/x-m4v,m4v," + + "video/x-flv,flv," + + "video/x-ms-wmv,wmv," + + "video/avi,avi," + + "video/webm,webm," + + "video/3gpp,3gpp 3gp," + + "video/3gpp2,3g2," + + "video/vnd.rn-realvideo,rv," + + "video/ogg,ogv," + + "video/x-matroska,mkv," + + "application/vnd.oasis.opendocument.formula-template,otf," + + "application/octet-stream,exe"; + + var Mime = { + mimes: {}, + + extensions: {}, + + // Parses the default mime types string into a mimes and extensions lookup maps + addMimeType: function (mimeData) { + var items = mimeData.split(/,/), + i, + ii, + ext; + + for (i = 0; i < items.length; i += 2) { + ext = items[i + 1].split(/ /); + + // extension to mime lookup + for (ii = 0; ii < ext.length; ii++) { + this.mimes[ext[ii]] = items[i]; + } + // mime to extension lookup + this.extensions[items[i]] = ext; + } + }, + + extList2mimes: function (filters, addMissingExtensions) { + var self = this, + ext, + i, + ii, + type, + mimes = []; + + // convert extensions to mime types list + for (i = 0; i < filters.length; i++) { + ext = filters[i].extensions + .toLowerCase() + .split(/\s*,\s*/); + + for (ii = 0; ii < ext.length; ii++) { + // if there's an asterisk in the list, then accept attribute is not required + if (ext[ii] === "*") { + return []; + } + + type = self.mimes[ext[ii]]; + + // future browsers should filter by extension, finally + if (addMissingExtensions && /^\w+$/.test(ext[ii])) { + mimes.push("." + ext[ii]); + } else if ( + type && + Basic.inArray(type, mimes) === -1 + ) { + mimes.push(type); + } else if (!type) { + // if we have no type in our map, then accept all + return []; + } + } + } + return mimes; + }, + + mimes2exts: function (mimes) { + var self = this, + exts = []; + + Basic.each(mimes, function (mime) { + mime = mime.toLowerCase(); + + if (mime === "*") { + exts = []; + return false; + } + + // check if this thing looks like mime type + var m = mime.match(/^(\w+)\/(\*|\w+)$/); + if (m) { + if (m[2] === "*") { + // wildcard mime type detected + Basic.each( + self.extensions, + function (arr, mime) { + if ( + new RegExp("^" + m[1] + "/").test( + mime + ) + ) { + [].push.apply( + exts, + self.extensions[mime] + ); + } + } + ); + } else if (self.extensions[mime]) { + [].push.apply(exts, self.extensions[mime]); + } + } + }); + return exts; + }, + + mimes2extList: function (mimes) { + var accept = [], + exts = []; + + if (Basic.typeOf(mimes) === "string") { + mimes = Basic.trim(mimes).split(/\s*,\s*/); + } + + exts = this.mimes2exts(mimes); + + accept.push({ + title: I18n.translate("Files"), + extensions: exts.length ? exts.join(",") : "*", + }); + + // save original mimes string + accept.mimes = mimes; + + return accept; + }, + + getFileExtension: function (fileName) { + var matches = fileName && fileName.match(/\.([^.]+)$/); + if (matches) { + return matches[1].toLowerCase(); + } + return ""; + }, + + getFileMime: function (fileName) { + return this.mimes[this.getFileExtension(fileName)] || ""; + }, + }; + + Mime.addMimeType(mimeData); + + return Mime; + }); + + // Included from: src/javascript/file/FileInput.js + + /** + * FileInput.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/file/FileInput", [ + "moxie/core/utils/Basic", + "moxie/core/utils/Env", + "moxie/core/utils/Mime", + "moxie/core/utils/Dom", + "moxie/core/Exceptions", + "moxie/core/EventTarget", + "moxie/core/I18n", + "moxie/runtime/Runtime", + "moxie/runtime/RuntimeClient", + ], function (Basic, Env, Mime, Dom, x, EventTarget, I18n, Runtime, RuntimeClient) { + /** + Provides a convenient way to create cross-browser file-picker. Generates file selection dialog on click, + converts selected files to _File_ objects, to be used in conjunction with _Image_, preloaded in memory + with _FileReader_ or uploaded to a server through _XMLHttpRequest_. + + @class moxie/file/FileInput + @constructor + @extends EventTarget + @uses RuntimeClient + @param {Object|String|DOMElement} options If options is string or node, argument is considered as _browse\_button_. + @param {String|DOMElement} options.browse_button DOM Element to turn into file picker. + @param {Array} [options.accept] Array of mime types to accept. By default accepts all. + @param {Boolean} [options.multiple=false] Enable selection of multiple files. + @param {Boolean} [options.directory=false] Turn file input into the folder input (cannot be both at the same time). + @param {String|DOMElement} [options.container] DOM Element to use as a container for file-picker. Defaults to parentNode + for _browse\_button_. + @param {Object|String} [options.required_caps] Set of required capabilities, that chosen runtime must support. + + @example +
+ Browse... +
+ + + */ + var dispatches = [ + /** + Dispatched when runtime is connected and file-picker is ready to be used. + + @event ready + @param {Object} event + */ + "ready", + + /** + Dispatched right after [ready](#event_ready) event, and whenever [refresh()](#method_refresh) is invoked. + Check [corresponding documentation entry](#method_refresh) for more info. + + @event refresh + @param {Object} event + */ + + /** + Dispatched when selection of files in the dialog is complete. + + @event change + @param {Object} event + */ + "change", + + "cancel", // TODO: might be useful + + /** + Dispatched when mouse cursor enters file-picker area. Can be used to style element + accordingly. + + @event mouseenter + @param {Object} event + */ + "mouseenter", + + /** + Dispatched when mouse cursor leaves file-picker area. Can be used to style element + accordingly. + + @event mouseleave + @param {Object} event + */ + "mouseleave", + + /** + Dispatched when functional mouse button is pressed on top of file-picker area. + + @event mousedown + @param {Object} event + */ + "mousedown", + + /** + Dispatched when functional mouse button is released on top of file-picker area. + + @event mouseup + @param {Object} event + */ + "mouseup", + ]; + + function FileInput(options) { + if (MXI_DEBUG) { + Env.log("Instantiating FileInput..."); + } + + var container, browseButton, defaults; + + // if flat argument passed it should be browse_button id + if ( + Basic.inArray(Basic.typeOf(options), ["string", "node"]) !== + -1 + ) { + options = { browse_button: options }; + } + + // this will help us to find proper default container + browseButton = Dom.get(options.browse_button); + if (!browseButton) { + // browse button is required + throw new x.DOMException(x.DOMException.NOT_FOUND_ERR); + } + + // figure out the options + defaults = { + accept: [ + { + title: I18n.translate("All Files"), + extensions: "*", + }, + ], + multiple: false, + required_caps: false, + container: browseButton.parentNode || document.body, + }; + + options = Basic.extend({}, defaults, options); + + // convert to object representation + if (typeof options.required_caps === "string") { + options.required_caps = Runtime.parseCaps( + options.required_caps + ); + } + + // normalize accept option (could be list of mime types or array of title/extensions pairs) + if (typeof options.accept === "string") { + options.accept = Mime.mimes2extList(options.accept); + } + + container = Dom.get(options.container); + // make sure we have container + if (!container) { + container = document.body; + } + + // make container relative, if it's not + if (Dom.getStyle(container, "position") === "static") { + container.style.position = "relative"; + } + + container = browseButton = null; // IE + + RuntimeClient.call(this); + + Basic.extend(this, { + /** + Unique id of the component + + @property uid + @protected + @readOnly + @type {String} + @default UID + */ + uid: Basic.guid("uid_"), + + /** + Unique id of the connected runtime, if any. + + @property ruid + @protected + @type {String} + */ + ruid: null, + + /** + Unique id of the runtime container. Useful to get hold of it for various manipulations. + + @property shimid + @protected + @type {String} + */ + shimid: null, + + /** + Array of selected mOxie.File objects + + @property files + @type {Array} + @default null + */ + files: null, + + /** + Initializes the file-picker, connects it to runtime and dispatches event ready when done. + + @method init + */ + init: function () { + var self = this; + + self.bind("RuntimeInit", function (e, runtime) { + self.ruid = runtime.uid; + self.shimid = runtime.shimid; + + self.bind( + "Ready", + function () { + self.trigger("Refresh"); + }, + 999 + ); + + // re-position and resize shim container + self.bind("Refresh", function () { + var pos, + size, + browseButton, + shimContainer, + zIndex; + + browseButton = Dom.get(options.browse_button); + shimContainer = Dom.get(runtime.shimid); // do not use runtime.getShimContainer(), since it will create container if it doesn't exist + + if (browseButton) { + pos = Dom.getPos( + browseButton, + Dom.get(options.container) + ); + size = Dom.getSize(browseButton); + zIndex = + parseInt( + Dom.getStyle( + browseButton, + "z-index" + ), + 10 + ) || 0; + + if (shimContainer) { + Basic.extend(shimContainer.style, { + top: pos.y + "px", + left: pos.x + "px", + width: size.w + "px", + height: size.h + "px", + zIndex: zIndex + 1, + }); + } + } + shimContainer = browseButton = null; + }); + + runtime.exec.call( + self, + "FileInput", + "init", + options + ); + }); + + // runtime needs: options.required_features, options.runtime_order and options.container + self.connectRuntime( + Basic.extend({}, options, { + required_caps: { + select_file: true, + }, + }) + ); + }, + + /** + * Get current option value by its name + * + * @method getOption + * @param name + * @return {Mixed} + */ + getOption: function (name) { + return options[name]; + }, + + /** + * Sets a new value for the option specified by name + * + * @method setOption + * @param name + * @param value + */ + setOption: function (name, value) { + if (!options.hasOwnProperty(name)) { + return; + } + + var oldValue = options[name]; + + switch (name) { + case "accept": + if (typeof value === "string") { + value = Mime.mimes2extList(value); + } + break; + + case "container": + case "required_caps": + throw new x.FileException( + x.FileException.NO_MODIFICATION_ALLOWED_ERR + ); + } + + options[name] = value; + this.exec("FileInput", "setOption", name, value); + + this.trigger("OptionChanged", name, value, oldValue); + }, + + /** + Disables file-picker element, so that it doesn't react to mouse clicks. + + @method disable + @param {Boolean} [state=true] Disable component if - true, enable if - false + */ + disable: function (state) { + var runtime = this.getRuntime(); + if (runtime) { + this.exec( + "FileInput", + "disable", + Basic.typeOf(state) === "undefined" + ? true + : state + ); + } + }, + + /** + Reposition and resize dialog trigger to match the position and size of browse_button element. + + @method refresh + */ + refresh: function () { + this.trigger("Refresh"); + }, + + /** + Destroy component. + + @method destroy + */ + destroy: function () { + var runtime = this.getRuntime(); + if (runtime) { + runtime.exec.call(this, "FileInput", "destroy"); + this.disconnectRuntime(); + } + + if (Basic.typeOf(this.files) === "array") { + // no sense in leaving associated files behind + Basic.each(this.files, function (file) { + file.destroy(); + }); + } + this.files = null; + + this.unbindAll(); + }, + }); + + this.handleEventProps(dispatches); + } + + FileInput.prototype = EventTarget.instance; + + return FileInput; + }); + + // Included from: src/javascript/file/File.js + + /** + * File.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/file/File", [ + "moxie/core/utils/Basic", + "moxie/core/utils/Mime", + "moxie/file/Blob", + ], function (Basic, Mime, Blob) { + /** + @class moxie/file/File + @extends Blob + @constructor + @param {String} ruid Unique id of the runtime, to which this blob belongs to + @param {Object} file Object "Native" file object, as it is represented in the runtime + */ + function File(ruid, file) { + if (!file) { + // avoid extra errors in case we overlooked something + file = {}; + } + + Blob.apply(this, arguments); + + if (!this.type) { + this.type = Mime.getFileMime(file.name); + } + + // sanitize file name or generate new one + var name; + if (file.name) { + name = file.name.replace(/\\/g, "/"); + name = name.substr(name.lastIndexOf("/") + 1); + } else if (this.type) { + var prefix = this.type.split("/")[0]; + name = Basic.guid((prefix !== "" ? prefix : "file") + "_"); + + if (Mime.extensions[this.type]) { + name += "." + Mime.extensions[this.type][0]; // append proper extension if possible + } + } + + Basic.extend(this, { + /** + File name + + @property name + @type {String} + @default UID + */ + name: name || Basic.guid("file_"), + + /** + Relative path to the file inside a directory + + @property relativePath + @type {String} + @default '' + */ + relativePath: "", + + /** + Date of last modification + + @property lastModifiedDate + @type {String} + @default now + */ + lastModifiedDate: + file.lastModifiedDate || new Date().toLocaleString(), // Thu Aug 23 2012 19:40:00 GMT+0400 (GET) + }); + } + + File.prototype = Blob.prototype; + + return File; + }); + + // Included from: src/javascript/file/FileDrop.js + + /** + * FileDrop.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/file/FileDrop", [ + "moxie/core/I18n", + "moxie/core/utils/Dom", + "moxie/core/Exceptions", + "moxie/core/utils/Basic", + "moxie/core/utils/Env", + "moxie/file/File", + "moxie/runtime/RuntimeClient", + "moxie/core/EventTarget", + "moxie/core/utils/Mime", + ], function (I18n, Dom, x, Basic, Env, File, RuntimeClient, EventTarget, Mime) { + /** + Turn arbitrary DOM element to a drop zone accepting files. Converts selected files to _File_ objects, to be used + in conjunction with _Image_, preloaded in memory with _FileReader_ or uploaded to a server through + _XMLHttpRequest_. + + @example +
+ Drop files here +
+
+
+ + + + @class moxie/file/FileDrop + @constructor + @extends EventTarget + @uses RuntimeClient + @param {Object|String} options If options has typeof string, argument is considered as options.drop_zone + @param {String|DOMElement} options.drop_zone DOM Element to turn into a drop zone + @param {Array} [options.accept] Array of mime types to accept. By default accepts all + @param {Object|String} [options.required_caps] Set of required capabilities, that chosen runtime must support + */ + var dispatches = [ + /** + Dispatched when runtime is connected and drop zone is ready to accept files. + + @event ready + @param {Object} event + */ + "ready", + + /** + Dispatched when dragging cursor enters the drop zone. + + @event dragenter + @param {Object} event + */ + "dragenter", + + /** + Dispatched when dragging cursor leaves the drop zone. + + @event dragleave + @param {Object} event + */ + "dragleave", + + /** + Dispatched when file is dropped onto the drop zone. + + @event drop + @param {Object} event + */ + "drop", + + /** + Dispatched if error occurs. + + @event error + @param {Object} event + */ + "error", + ]; + + function FileDrop(options) { + if (MXI_DEBUG) { + Env.log("Instantiating FileDrop..."); + } + + var self = this, + defaults; + + // if flat argument passed it should be drop_zone id + if (typeof options === "string") { + options = { drop_zone: options }; + } + + // figure out the options + defaults = { + accept: [ + { + title: I18n.translate("All Files"), + extensions: "*", + }, + ], + required_caps: { + drag_and_drop: true, + }, + }; + + options = + typeof options === "object" + ? Basic.extend({}, defaults, options) + : defaults; + + // this will help us to find proper default container + options.container = Dom.get(options.drop_zone) || document.body; + + // make container relative, if it is not + if (Dom.getStyle(options.container, "position") === "static") { + options.container.style.position = "relative"; + } + + // normalize accept option (could be list of mime types or array of title/extensions pairs) + if (typeof options.accept === "string") { + options.accept = Mime.mimes2extList(options.accept); + } + + RuntimeClient.call(self); + + Basic.extend(self, { + uid: Basic.guid("uid_"), + + ruid: null, + + files: null, + + init: function () { + self.bind("RuntimeInit", function (e, runtime) { + self.ruid = runtime.uid; + runtime.exec.call( + self, + "FileDrop", + "init", + options + ); + self.dispatchEvent("ready"); + }); + + // runtime needs: options.required_features, options.runtime_order and options.container + self.connectRuntime(options); // throws RuntimeError + }, + + destroy: function () { + var runtime = this.getRuntime(); + if (runtime) { + runtime.exec.call(this, "FileDrop", "destroy"); + this.disconnectRuntime(); + } + this.files = null; + + this.unbindAll(); + }, + }); + + this.handleEventProps(dispatches); + } + + FileDrop.prototype = EventTarget.instance; + + return FileDrop; + }); + + // Included from: src/javascript/file/FileReader.js + + /** + * FileReader.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/file/FileReader", [ + "moxie/core/utils/Basic", + "moxie/core/utils/Encode", + "moxie/core/Exceptions", + "moxie/core/EventTarget", + "moxie/file/Blob", + "moxie/runtime/RuntimeClient", + ], function (Basic, Encode, x, EventTarget, Blob, RuntimeClient) { + /** + Utility for preloading o.Blob/o.File objects in memory. By design closely follows [W3C FileReader](http://www.w3.org/TR/FileAPI/#dfn-filereader) + interface. Where possible uses native FileReader, where - not falls back to shims. + + @class moxie/file/FileReader + @constructor FileReader + @extends EventTarget + @uses RuntimeClient + */ + var dispatches = [ + /** + Dispatched when the read starts. + + @event loadstart + @param {Object} event + */ + "loadstart", + + /** + Dispatched while reading (and decoding) blob, and reporting partial Blob data (progess.loaded/progress.total). + + @event progress + @param {Object} event + */ + "progress", + + /** + Dispatched when the read has successfully completed. + + @event load + @param {Object} event + */ + "load", + + /** + Dispatched when the read has been aborted. For instance, by invoking the abort() method. + + @event abort + @param {Object} event + */ + "abort", + + /** + Dispatched when the read has failed. + + @event error + @param {Object} event + */ + "error", + + /** + Dispatched when the request has completed (either in success or failure). + + @event loadend + @param {Object} event + */ + "loadend", + ]; + + function FileReader() { + RuntimeClient.call(this); + + Basic.extend(this, { + /** + UID of the component instance. + + @property uid + @type {String} + */ + uid: Basic.guid("uid_"), + + /** + Contains current state of FileReader object. Can take values of FileReader.EMPTY, FileReader.LOADING + and FileReader.DONE. + + @property readyState + @type {Number} + @default FileReader.EMPTY + */ + readyState: FileReader.EMPTY, + + /** + Result of the successful read operation. + + @property result + @type {String} + */ + result: null, + + /** + Stores the error of failed asynchronous read operation. + + @property error + @type {DOMError} + */ + error: null, + + /** + Initiates reading of File/Blob object contents to binary string. + + @method readAsBinaryString + @param {Blob|File} blob Object to preload + */ + readAsBinaryString: function (blob) { + _read.call(this, "readAsBinaryString", blob); + }, + + /** + Initiates reading of File/Blob object contents to dataURL string. + + @method readAsDataURL + @param {Blob|File} blob Object to preload + */ + readAsDataURL: function (blob) { + _read.call(this, "readAsDataURL", blob); + }, + + /** + Initiates reading of File/Blob object contents to string. + + @method readAsText + @param {Blob|File} blob Object to preload + */ + readAsText: function (blob) { + _read.call(this, "readAsText", blob); + }, + + /** + Aborts preloading process. + + @method abort + */ + abort: function () { + this.result = null; + + if ( + Basic.inArray(this.readyState, [ + FileReader.EMPTY, + FileReader.DONE, + ]) !== -1 + ) { + return; + } else if (this.readyState === FileReader.LOADING) { + this.readyState = FileReader.DONE; + } + + this.exec("FileReader", "abort"); + + this.trigger("abort"); + this.trigger("loadend"); + }, + + /** + Destroy component and release resources. + + @method destroy + */ + destroy: function () { + this.abort(); + this.exec("FileReader", "destroy"); + this.disconnectRuntime(); + this.unbindAll(); + }, + }); + + // uid must already be assigned + this.handleEventProps(dispatches); + + this.bind( + "Error", + function (e, err) { + this.readyState = FileReader.DONE; + this.error = err; + }, + 999 + ); + + this.bind( + "Load", + function (e) { + this.readyState = FileReader.DONE; + }, + 999 + ); + + function _read(op, blob) { + var self = this; + + this.trigger("loadstart"); + + if (this.readyState === FileReader.LOADING) { + this.trigger( + "error", + new x.DOMException(x.DOMException.INVALID_STATE_ERR) + ); + this.trigger("loadend"); + return; + } + + // if source is not o.Blob/o.File + if (!(blob instanceof Blob)) { + this.trigger( + "error", + new x.DOMException(x.DOMException.NOT_FOUND_ERR) + ); + this.trigger("loadend"); + return; + } + + this.result = null; + this.readyState = FileReader.LOADING; + + if (blob.isDetached()) { + var src = blob.getSource(); + switch (op) { + case "readAsText": + case "readAsBinaryString": + this.result = src; + break; + case "readAsDataURL": + this.result = + "data:" + + blob.type + + ";base64," + + Encode.btoa(src); + break; + } + this.readyState = FileReader.DONE; + this.trigger("load"); + this.trigger("loadend"); + } else { + this.connectRuntime(blob.ruid); + this.exec("FileReader", "read", op, blob); + } + } + } + + /** + Initial FileReader state + + @property EMPTY + @type {Number} + @final + @static + @default 0 + */ + FileReader.EMPTY = 0; + + /** + FileReader switches to this state when it is preloading the source + + @property LOADING + @type {Number} + @final + @static + @default 1 + */ + FileReader.LOADING = 1; + + /** + Preloading is complete, this is a final state + + @property DONE + @type {Number} + @final + @static + @default 2 + */ + FileReader.DONE = 2; + + FileReader.prototype = EventTarget.instance; + + return FileReader; + }); + + // Included from: src/javascript/core/utils/Url.js + + /** + * Url.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/core/utils/Url", [ + "moxie/core/utils/Basic", + ], function (Basic) { + /** + Parse url into separate components and fill in absent parts with parts from current url, + based on https://raw.github.com/kvz/phpjs/master/functions/url/parse_url.js + + @method parseUrl + @for Utils + @static + @param {String} url Url to parse (defaults to empty string if undefined) + @return {Object} Hash containing extracted uri components + */ + var parseUrl = function (url, currentUrl) { + var key = [ + "source", + "scheme", + "authority", + "userInfo", + "user", + "pass", + "host", + "port", + "relative", + "path", + "directory", + "file", + "query", + "fragment", + ], + i = key.length, + ports = { + http: 80, + https: 443, + }, + uri = {}, + regex = /^(?:([^:\/?#]+):)?(?:\/\/()(?:(?:()(?:([^:@\/]*):?([^:@\/]*))?@)?(\[[\da-fA-F:]+\]|[^:\/?#]*)(?::(\d*))?))?()(?:(()(?:(?:[^?#\/]*\/)*)()(?:[^?#]*))(?:\\?([^#]*))?(?:#(.*))?)/, + m = regex.exec(url || ""), + isRelative, + isSchemeLess = /^\/\/\w/.test(url); + switch (Basic.typeOf(currentUrl)) { + case "undefined": + currentUrl = parseUrl(document.location.href, false); + break; + + case "string": + currentUrl = parseUrl(currentUrl, false); + break; + } + + while (i--) { + if (m[i]) { + uri[key[i]] = m[i]; + } + } + + isRelative = !isSchemeLess && !uri.scheme; + + if (isSchemeLess || isRelative) { + uri.scheme = currentUrl.scheme; + } + + // when url is relative, we set the origin and the path ourselves + if (isRelative) { + uri.host = currentUrl.host; + uri.port = currentUrl.port; + + var path = ""; + // for urls without trailing slash we need to figure out the path + if (/^[^\/]/.test(uri.path)) { + path = currentUrl.path; + // if path ends with a filename, strip it + if (/\/[^\/]*\.[^\/]*$/.test(path)) { + path = path.replace(/\/[^\/]+$/, "/"); + } else { + // avoid double slash at the end (see #127) + path = path.replace(/\/?$/, "/"); + } + } + uri.path = path + (uri.path || ""); // site may reside at domain.com or domain.com/subdir + } + + if (!uri.port) { + uri.port = ports[uri.scheme] || 80; + } + + uri.port = parseInt(uri.port, 10); + + if (!uri.path) { + uri.path = "/"; + } + + delete uri.source; + + return uri; + }; + + /** + Resolve url - among other things will turn relative url to absolute + + @method resolveUrl + @static + @param {String|Object} url Either absolute or relative, or a result of parseUrl call + @return {String} Resolved, absolute url + */ + var resolveUrl = function (url) { + var ports = { + // we ignore default ports + http: 80, + https: 443, + }, + urlp = typeof url === "object" ? url : parseUrl(url); + return ( + urlp.scheme + + "://" + + urlp.host + + (urlp.port !== ports[urlp.scheme] ? ":" + urlp.port : "") + + urlp.path + + (urlp.query ? urlp.query : "") + ); + }; + + /** + Check if specified url has the same origin as the current document + + @method hasSameOrigin + @param {String|Object} url + @return {Boolean} + */ + var hasSameOrigin = function (url) { + function origin(url) { + return [url.scheme, url.host, url.port].join("/"); + } + + if (typeof url === "string") { + url = parseUrl(url); + } + + return origin(parseUrl()) === origin(url); + }; + + return { + parseUrl: parseUrl, + resolveUrl: resolveUrl, + hasSameOrigin: hasSameOrigin, + }; + }); + + // Included from: src/javascript/runtime/RuntimeTarget.js + + /** + * RuntimeTarget.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/runtime/RuntimeTarget", [ + "moxie/core/utils/Basic", + "moxie/runtime/RuntimeClient", + "moxie/core/EventTarget", + ], function (Basic, RuntimeClient, EventTarget) { + /** + Instance of this class can be used as a target for the events dispatched by shims, + when allowing them onto components is for either reason inappropriate + + @class moxie/runtime/RuntimeTarget + @constructor + @protected + @extends EventTarget + */ + function RuntimeTarget() { + this.uid = Basic.guid("uid_"); + + RuntimeClient.call(this); + + this.destroy = function () { + this.disconnectRuntime(); + this.unbindAll(); + }; + } + + RuntimeTarget.prototype = EventTarget.instance; + + return RuntimeTarget; + }); + + // Included from: src/javascript/file/FileReaderSync.js + + /** + * FileReaderSync.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/file/FileReaderSync", [ + "moxie/core/utils/Basic", + "moxie/runtime/RuntimeClient", + "moxie/core/utils/Encode", + ], function (Basic, RuntimeClient, Encode) { + /** + Synchronous FileReader implementation. Something like this is available in WebWorkers environment, here + it can be used to read only preloaded blobs/files and only below certain size (not yet sure what that'd be, + but probably < 1mb). Not meant to be used directly by user. + + @class moxie/file/FileReaderSync + @private + @constructor + */ + return function () { + RuntimeClient.call(this); + + Basic.extend(this, { + uid: Basic.guid("uid_"), + + readAsBinaryString: function (blob) { + return _read.call(this, "readAsBinaryString", blob); + }, + + readAsDataURL: function (blob) { + return _read.call(this, "readAsDataURL", blob); + }, + + /*readAsArrayBuffer: function(blob) { + return _read.call(this, 'readAsArrayBuffer', blob); + },*/ + + readAsText: function (blob) { + return _read.call(this, "readAsText", blob); + }, + }); + + function _read(op, blob) { + if (blob.isDetached()) { + var src = blob.getSource(); + switch (op) { + case "readAsBinaryString": + return src; + case "readAsDataURL": + return ( + "data:" + + blob.type + + ";base64," + + Encode.btoa(src) + ); + case "readAsText": + var txt = ""; + for ( + var i = 0, length = src.length; + i < length; + i++ + ) { + txt += String.fromCharCode(src[i]); + } + return txt; + } + } else { + var result = this.connectRuntime(blob.ruid).exec.call( + this, + "FileReaderSync", + "read", + op, + blob + ); + this.disconnectRuntime(); + return result; + } + } + }; + }); + + // Included from: src/javascript/xhr/FormData.js + + /** + * FormData.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/xhr/FormData", [ + "moxie/core/Exceptions", + "moxie/core/utils/Basic", + "moxie/file/Blob", + ], function (x, Basic, Blob) { + /** + FormData + + @class moxie/xhr/FormData + @constructor + */ + function FormData() { + var _blob, + _fields = []; + + Basic.extend(this, { + /** + Append another key-value pair to the FormData object + + @method append + @param {String} name Name for the new field + @param {String|Blob|Array|Object} value Value for the field + */ + append: function (name, value) { + var self = this, + valueType = Basic.typeOf(value); + + // according to specs value might be either Blob or String + if (value instanceof Blob) { + _blob = { + name: name, + value: value, // unfortunately we can only send single Blob in one FormData + }; + } else if ("array" === valueType) { + name += "[]"; + + Basic.each(value, function (value) { + self.append(name, value); + }); + } else if ("object" === valueType) { + Basic.each(value, function (value, key) { + self.append(name + "[" + key + "]", value); + }); + } else if ( + "null" === valueType || + "undefined" === valueType || + ("number" === valueType && isNaN(value)) + ) { + self.append(name, "false"); + } else { + _fields.push({ + name: name, + value: value.toString(), + }); + } + }, + + /** + Checks if FormData contains Blob. + + @method hasBlob + @return {Boolean} + */ + hasBlob: function () { + return !!this.getBlob(); + }, + + /** + Retrieves blob. + + @method getBlob + @return {Object} Either Blob if found or null + */ + getBlob: function () { + return (_blob && _blob.value) || null; + }, + + /** + Retrieves blob field name. + + @method getBlobName + @return {String} Either Blob field name or null + */ + getBlobName: function () { + return (_blob && _blob.name) || null; + }, + + /** + Loop over the fields in FormData and invoke the callback for each of them. + + @method each + @param {Function} cb Callback to call for each field + */ + each: function (cb) { + Basic.each(_fields, function (field) { + cb(field.value, field.name); + }); + + if (_blob) { + cb(_blob.value, _blob.name); + } + }, + + destroy: function () { + _blob = null; + _fields = []; + }, + }); + } + + return FormData; + }); + + // Included from: src/javascript/xhr/XMLHttpRequest.js + + /** + * XMLHttpRequest.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/xhr/XMLHttpRequest", [ + "moxie/core/utils/Basic", + "moxie/core/Exceptions", + "moxie/core/EventTarget", + "moxie/core/utils/Encode", + "moxie/core/utils/Url", + "moxie/runtime/Runtime", + "moxie/runtime/RuntimeTarget", + "moxie/file/Blob", + "moxie/file/FileReaderSync", + "moxie/xhr/FormData", + "moxie/core/utils/Env", + "moxie/core/utils/Mime", + ], function (Basic, x, EventTarget, Encode, Url, Runtime, RuntimeTarget, Blob, FileReaderSync, FormData, Env, Mime) { + var httpCode = { + 100: "Continue", + 101: "Switching Protocols", + 102: "Processing", + + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 207: "Multi-Status", + 226: "IM Used", + + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 306: "Reserved", + 307: "Temporary Redirect", + + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Request Entity Too Large", + 414: "Request-URI Too Long", + 415: "Unsupported Media Type", + 416: "Requested Range Not Satisfiable", + 417: "Expectation Failed", + 422: "Unprocessable Entity", + 423: "Locked", + 424: "Failed Dependency", + 426: "Upgrade Required", + + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported", + 506: "Variant Also Negotiates", + 507: "Insufficient Storage", + 510: "Not Extended", + }; + + function XMLHttpRequestUpload() { + this.uid = Basic.guid("uid_"); + } + + XMLHttpRequestUpload.prototype = EventTarget.instance; + + /** + Implementation of XMLHttpRequest + + @class moxie/xhr/XMLHttpRequest + @constructor + @uses RuntimeClient + @extends EventTarget + */ + var dispatches = [ + "loadstart", + + "progress", + + "abort", + + "error", + + "load", + + "timeout", + + "loadend", + + // readystatechange (for historical reasons) + ]; + + var NATIVE = 1, + RUNTIME = 2; + + function XMLHttpRequest() { + var self = this, + // this (together with _p() @see below) is here to gracefully upgrade to setter/getter syntax where possible + props = { + /** + The amount of milliseconds a request can take before being terminated. Initially zero. Zero means there is no timeout. + + @property timeout + @type Number + @default 0 + */ + timeout: 0, + + /** + Current state, can take following values: + UNSENT (numeric value 0) + The object has been constructed. + + OPENED (numeric value 1) + The open() method has been successfully invoked. During this state request headers can be set using setRequestHeader() and the request can be made using the send() method. + + HEADERS_RECEIVED (numeric value 2) + All redirects (if any) have been followed and all HTTP headers of the final response have been received. Several response members of the object are now available. + + LOADING (numeric value 3) + The response entity body is being received. + + DONE (numeric value 4) + + @property readyState + @type Number + @default 0 (UNSENT) + */ + readyState: XMLHttpRequest.UNSENT, + + /** + True when user credentials are to be included in a cross-origin request. False when they are to be excluded + in a cross-origin request and when cookies are to be ignored in its response. Initially false. + + @property withCredentials + @type Boolean + @default false + */ + withCredentials: false, + + /** + Returns the HTTP status code. + + @property status + @type Number + @default 0 + */ + status: 0, + + /** + Returns the HTTP status text. + + @property statusText + @type String + */ + statusText: "", + + /** + Returns the response type. Can be set to change the response type. Values are: + the empty string (default), "arraybuffer", "blob", "document", "json", and "text". + + @property responseType + @type String + */ + responseType: "", + + /** + Returns the document response entity body. + + Throws an "InvalidStateError" exception if responseType is not the empty string or "document". + + @property responseXML + @type Document + */ + responseXML: null, + + /** + Returns the text response entity body. + + Throws an "InvalidStateError" exception if responseType is not the empty string or "text". + + @property responseText + @type String + */ + responseText: null, + + /** + Returns the response entity body (http://www.w3.org/TR/XMLHttpRequest/#response-entity-body). + Can become: ArrayBuffer, Blob, Document, JSON, Text + + @property response + @type Mixed + */ + response: null, + }, + _async = true, + _url, + _method, + _headers = {}, + _user, + _password, + _encoding = null, + _mimeType = null, + // flags + _sync_flag = false, + _send_flag = false, + _upload_events_flag = false, + _upload_complete_flag = false, + _error_flag = false, + _same_origin_flag = false, + // times + _start_time, + _timeoutset_time, + _finalMime = null, + _finalCharset = null, + _options = {}, + _xhr, + _responseHeaders = "", + _responseHeadersBag; + + Basic.extend(this, props, { + /** + Unique id of the component + + @property uid + @type String + */ + uid: Basic.guid("uid_"), + + /** + Target for Upload events + + @property upload + @type XMLHttpRequestUpload + */ + upload: new XMLHttpRequestUpload(), + + /** + Sets the request method, request URL, synchronous flag, request username, and request password. + + Throws a "SyntaxError" exception if one of the following is true: + + method is not a valid HTTP method. + url cannot be resolved. + url contains the "user:password" format in the userinfo production. + Throws a "SecurityError" exception if method is a case-insensitive match for CONNECT, TRACE or TRACK. + + Throws an "InvalidAccessError" exception if one of the following is true: + + Either user or password is passed as argument and the origin of url does not match the XMLHttpRequest origin. + There is an associated XMLHttpRequest document and either the timeout attribute is not zero, + the withCredentials attribute is true, or the responseType attribute is not the empty string. + + + @method open + @param {String} method HTTP method to use on request + @param {String} url URL to request + @param {Boolean} [async=true] If false request will be done in synchronous manner. Asynchronous by default. + @param {String} [user] Username to use in HTTP authentication process on server-side + @param {String} [password] Password to use in HTTP authentication process on server-side + */ + open: function (method, url, async, user, password) { + var urlp; + + // first two arguments are required + if (!method || !url) { + throw new x.DOMException(x.DOMException.SYNTAX_ERR); + } + + // 2 - check if any code point in method is higher than U+00FF or after deflating method it does not match the method + if ( + /[\u0100-\uffff]/.test(method) || + Encode.utf8_encode(method) !== method + ) { + throw new x.DOMException(x.DOMException.SYNTAX_ERR); + } + + // 3 + if ( + !!~Basic.inArray(method.toUpperCase(), [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "POST", + "PUT", + "TRACE", + "TRACK", + ]) + ) { + _method = method.toUpperCase(); + } + + // 4 - allowing these methods poses a security risk + if ( + !!~Basic.inArray(_method, [ + "CONNECT", + "TRACE", + "TRACK", + ]) + ) { + throw new x.DOMException( + x.DOMException.SECURITY_ERR + ); + } + + // 5 + url = Encode.utf8_encode(url); + + // 6 - Resolve url relative to the XMLHttpRequest base URL. If the algorithm returns an error, throw a "SyntaxError". + urlp = Url.parseUrl(url); + + _same_origin_flag = Url.hasSameOrigin(urlp); + + // 7 - manually build up absolute url + _url = Url.resolveUrl(url); + + // 9-10, 12-13 + if ((user || password) && !_same_origin_flag) { + throw new x.DOMException( + x.DOMException.INVALID_ACCESS_ERR + ); + } + + _user = user || urlp.user; + _password = password || urlp.pass; + + // 11 + _async = async || true; + + if ( + _async === false && + (_p("timeout") || + _p("withCredentials") || + _p("responseType") !== "") + ) { + throw new x.DOMException( + x.DOMException.INVALID_ACCESS_ERR + ); + } + + // 14 - terminate abort() + + // 15 - terminate send() + + // 18 + _sync_flag = !_async; + _send_flag = false; + _headers = {}; + _reset.call(this); + + // 19 + _p("readyState", XMLHttpRequest.OPENED); + + // 20 + this.dispatchEvent("readystatechange"); + }, + + /** + Appends an header to the list of author request headers, or if header is already + in the list of author request headers, combines its value with value. + + Throws an "InvalidStateError" exception if the state is not OPENED or if the send() flag is set. + Throws a "SyntaxError" exception if header is not a valid HTTP header field name or if value + is not a valid HTTP header field value. + + @method setRequestHeader + @param {String} header + @param {String|Number} value + */ + setRequestHeader: function (header, value) { + var uaHeaders = [ + // these headers are controlled by the user agent + "accept-charset", + "accept-encoding", + "access-control-request-headers", + "access-control-request-method", + "connection", + "content-length", + "cookie", + "cookie2", + "content-transfer-encoding", + "date", + "expect", + "host", + "keep-alive", + "origin", + "referer", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "user-agent", + "via", + ]; + + // 1-2 + if ( + _p("readyState") !== XMLHttpRequest.OPENED || + _send_flag + ) { + throw new x.DOMException( + x.DOMException.INVALID_STATE_ERR + ); + } + + // 3 + if ( + /[\u0100-\uffff]/.test(header) || + Encode.utf8_encode(header) !== header + ) { + throw new x.DOMException(x.DOMException.SYNTAX_ERR); + } + + // 4 + /* this step is seemingly bypassed in browsers, probably to allow various unicode characters in header values + if (/[\u0100-\uffff]/.test(value) || Encode.utf8_encode(value) !== value) { + throw new x.DOMException(x.DOMException.SYNTAX_ERR); + }*/ + + header = Basic.trim(header).toLowerCase(); + + // setting of proxy-* and sec-* headers is prohibited by spec + if ( + !!~Basic.inArray(header, uaHeaders) || + /^(proxy\-|sec\-)/.test(header) + ) { + return false; + } + + // camelize + // browsers lowercase header names (at least for custom ones) + // header = header.replace(/\b\w/g, function($1) { return $1.toUpperCase(); }); + + if (!_headers[header]) { + _headers[header] = value; + } else { + // http://tools.ietf.org/html/rfc2616#section-4.2 (last paragraph) + _headers[header] += ", " + value; + } + return true; + }, + + /** + * Test if the specified header is already set on this request. + * Returns a header value or boolean false if it's not yet set. + * + * @method hasRequestHeader + * @param {String} header Name of the header to test + * @return {Boolean|String} + */ + hasRequestHeader: function (header) { + return ( + (header && _headers[header.toLowerCase()]) || false + ); + }, + + /** + Returns all headers from the response, with the exception of those whose field name is Set-Cookie or Set-Cookie2. + + @method getAllResponseHeaders + @return {String} reponse headers or empty string + */ + getAllResponseHeaders: function () { + return _responseHeaders || ""; + }, + + /** + Returns the header field value from the response of which the field name matches header, + unless the field name is Set-Cookie or Set-Cookie2. + + @method getResponseHeader + @param {String} header + @return {String} value(s) for the specified header or null + */ + getResponseHeader: function (header) { + header = header.toLowerCase(); + + if ( + _error_flag || + !!~Basic.inArray(header, [ + "set-cookie", + "set-cookie2", + ]) + ) { + return null; + } + + if (_responseHeaders && _responseHeaders !== "") { + // if we didn't parse response headers until now, do it and keep for later + if (!_responseHeadersBag) { + _responseHeadersBag = {}; + Basic.each( + _responseHeaders.split(/\r\n/), + function (line) { + var pair = line.split(/:\s+/); + if (pair.length === 2) { + // last line might be empty, omit + pair[0] = Basic.trim(pair[0]); // just in case + _responseHeadersBag[ + pair[0].toLowerCase() + ] = { + // simply to retain header name in original form + header: pair[0], + value: Basic.trim(pair[1]), + }; + } + } + ); + } + if (_responseHeadersBag.hasOwnProperty(header)) { + return ( + _responseHeadersBag[header].header + + ": " + + _responseHeadersBag[header].value + ); + } + } + return null; + }, + + /** + Sets the Content-Type header for the response to mime. + Throws an "InvalidStateError" exception if the state is LOADING or DONE. + Throws a "SyntaxError" exception if mime is not a valid media type. + + @method overrideMimeType + @param String mime Mime type to set + */ + overrideMimeType: function (mime) { + var matches, charset; + + // 1 + if ( + !!~Basic.inArray(_p("readyState"), [ + XMLHttpRequest.LOADING, + XMLHttpRequest.DONE, + ]) + ) { + throw new x.DOMException( + x.DOMException.INVALID_STATE_ERR + ); + } + + // 2 + mime = Basic.trim(mime.toLowerCase()); + + if ( + /;/.test(mime) && + (matches = mime.match( + /^([^;]+)(?:;\scharset\=)?(.*)$/ + )) + ) { + mime = matches[1]; + if (matches[2]) { + charset = matches[2]; + } + } + + if (!Mime.mimes[mime]) { + throw new x.DOMException(x.DOMException.SYNTAX_ERR); + } + + // 3-4 + _finalMime = mime; + _finalCharset = charset; + }, + + /** + Initiates the request. The optional argument provides the request entity body. + The argument is ignored if request method is GET or HEAD. + + Throws an "InvalidStateError" exception if the state is not OPENED or if the send() flag is set. + + @method send + @param {Blob|Document|String|FormData} [data] Request entity body + @param {Object} [options] Set of requirements and pre-requisities for runtime initialization + */ + send: function (data, options) { + if (Basic.typeOf(options) === "string") { + _options = { ruid: options }; + } else if (!options) { + _options = {}; + } else { + _options = options; + } + + // 1-2 + if ( + this.readyState !== XMLHttpRequest.OPENED || + _send_flag + ) { + throw new x.DOMException( + x.DOMException.INVALID_STATE_ERR + ); + } + + // 3 + // sending Blob + if (data instanceof Blob) { + _options.ruid = data.ruid; + _mimeType = data.type || "application/octet-stream"; + } + + // FormData + else if (data instanceof FormData) { + if (data.hasBlob()) { + var blob = data.getBlob(); + _options.ruid = blob.ruid; + _mimeType = + blob.type || "application/octet-stream"; + } + } + + // DOMString + else if (typeof data === "string") { + _encoding = "UTF-8"; + _mimeType = "text/plain;charset=UTF-8"; + + // data should be converted to Unicode and encoded as UTF-8 + data = Encode.utf8_encode(data); + } + + // if withCredentials not set, but requested, set it automatically + if (!this.withCredentials) { + this.withCredentials = + _options.required_caps && + _options.required_caps.send_browser_cookies && + !_same_origin_flag; + } + + // 4 - storage mutex + // 5 + _upload_events_flag = + !_sync_flag && this.upload.hasEventListener(); // DSAP + // 6 + _error_flag = false; + // 7 + _upload_complete_flag = !data; + // 8 - Asynchronous steps + if (!_sync_flag) { + // 8.1 + _send_flag = true; + // 8.2 + // this.dispatchEvent('loadstart'); // will be dispatched either by native or runtime xhr + // 8.3 + //if (!_upload_complete_flag) { + // this.upload.dispatchEvent('loadstart'); // will be dispatched either by native or runtime xhr + //} + } + // 8.5 - Return the send() method call, but continue running the steps in this algorithm. + _doXHR.call(this, data); + }, + + /** + Cancels any network activity. + + @method abort + */ + abort: function () { + _error_flag = true; + _sync_flag = false; + + if ( + !~Basic.inArray(_p("readyState"), [ + XMLHttpRequest.UNSENT, + XMLHttpRequest.OPENED, + XMLHttpRequest.DONE, + ]) + ) { + _p("readyState", XMLHttpRequest.DONE); + _send_flag = false; + + if (_xhr) { + _xhr.getRuntime().exec.call( + _xhr, + "XMLHttpRequest", + "abort", + _upload_complete_flag + ); + } else { + throw new x.DOMException( + x.DOMException.INVALID_STATE_ERR + ); + } + + _upload_complete_flag = true; + } else { + _p("readyState", XMLHttpRequest.UNSENT); + } + }, + + destroy: function () { + if (_xhr) { + if (Basic.typeOf(_xhr.destroy) === "function") { + _xhr.destroy(); + } + _xhr = null; + } + + this.unbindAll(); + + if (this.upload) { + this.upload.unbindAll(); + this.upload = null; + } + }, + }); + + this.handleEventProps(dispatches.concat(["readystatechange"])); // for historical reasons + this.upload.handleEventProps(dispatches); + + /* this is nice, but maybe too lengthy + + // if supported by JS version, set getters/setters for specific properties + o.defineProperty(this, 'readyState', { + configurable: false, + + get: function() { + return _p('readyState'); + } + }); + + o.defineProperty(this, 'timeout', { + configurable: false, + + get: function() { + return _p('timeout'); + }, + + set: function(value) { + + if (_sync_flag) { + throw new x.DOMException(x.DOMException.INVALID_ACCESS_ERR); + } + + // timeout still should be measured relative to the start time of request + _timeoutset_time = (new Date).getTime(); + + _p('timeout', value); + } + }); + + // the withCredentials attribute has no effect when fetching same-origin resources + o.defineProperty(this, 'withCredentials', { + configurable: false, + + get: function() { + return _p('withCredentials'); + }, + + set: function(value) { + // 1-2 + if (!~o.inArray(_p('readyState'), [XMLHttpRequest.UNSENT, XMLHttpRequest.OPENED]) || _send_flag) { + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + + // 3-4 + if (_anonymous_flag || _sync_flag) { + throw new x.DOMException(x.DOMException.INVALID_ACCESS_ERR); + } + + // 5 + _p('withCredentials', value); + } + }); + + o.defineProperty(this, 'status', { + configurable: false, + + get: function() { + return _p('status'); + } + }); + + o.defineProperty(this, 'statusText', { + configurable: false, + + get: function() { + return _p('statusText'); + } + }); + + o.defineProperty(this, 'responseType', { + configurable: false, + + get: function() { + return _p('responseType'); + }, + + set: function(value) { + // 1 + if (!!~o.inArray(_p('readyState'), [XMLHttpRequest.LOADING, XMLHttpRequest.DONE])) { + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + + // 2 + if (_sync_flag) { + throw new x.DOMException(x.DOMException.INVALID_ACCESS_ERR); + } + + // 3 + _p('responseType', value.toLowerCase()); + } + }); + + o.defineProperty(this, 'responseText', { + configurable: false, + + get: function() { + // 1 + if (!~o.inArray(_p('responseType'), ['', 'text'])) { + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + + // 2-3 + if (_p('readyState') !== XMLHttpRequest.DONE && _p('readyState') !== XMLHttpRequest.LOADING || _error_flag) { + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + + return _p('responseText'); + } + }); + + o.defineProperty(this, 'responseXML', { + configurable: false, + + get: function() { + // 1 + if (!~o.inArray(_p('responseType'), ['', 'document'])) { + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + + // 2-3 + if (_p('readyState') !== XMLHttpRequest.DONE || _error_flag) { + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + + return _p('responseXML'); + } + }); + + o.defineProperty(this, 'response', { + configurable: false, + + get: function() { + if (!!~o.inArray(_p('responseType'), ['', 'text'])) { + if (_p('readyState') !== XMLHttpRequest.DONE && _p('readyState') !== XMLHttpRequest.LOADING || _error_flag) { + return ''; + } + } + + if (_p('readyState') !== XMLHttpRequest.DONE || _error_flag) { + return null; + } + + return _p('response'); + } + }); + + */ + + function _p(prop, value) { + if (!props.hasOwnProperty(prop)) { + return; + } + if (arguments.length === 1) { + // get + return Env.can("define_property") + ? props[prop] + : self[prop]; + } else { + // set + if (Env.can("define_property")) { + props[prop] = value; + } else { + self[prop] = value; + } + } + } + + /* + function _toASCII(str, AllowUnassigned, UseSTD3ASCIIRules) { + // TODO: http://tools.ietf.org/html/rfc3490#section-4.1 + return str.toLowerCase(); + } + */ + + function _doXHR(data) { + var self = this; + + _start_time = new Date().getTime(); + + _xhr = new RuntimeTarget(); + + function loadEnd() { + if (_xhr) { + // it could have been destroyed by now + _xhr.destroy(); + _xhr = null; + } + self.dispatchEvent("loadend"); + self = null; + } + + function exec(runtime) { + _xhr.bind("LoadStart", function (e) { + _p("readyState", XMLHttpRequest.LOADING); + self.dispatchEvent("readystatechange"); + + self.dispatchEvent(e); + + if (_upload_events_flag) { + self.upload.dispatchEvent(e); + } + }); + + _xhr.bind("Progress", function (e) { + if (_p("readyState") !== XMLHttpRequest.LOADING) { + _p("readyState", XMLHttpRequest.LOADING); // LoadStart unreliable (in Flash for example) + self.dispatchEvent("readystatechange"); + } + self.dispatchEvent(e); + }); + + _xhr.bind("UploadProgress", function (e) { + if (_upload_events_flag) { + self.upload.dispatchEvent({ + type: "progress", + lengthComputable: false, + total: e.total, + loaded: e.loaded, + }); + } + }); + + _xhr.bind("Load", function (e) { + _p("readyState", XMLHttpRequest.DONE); + _p( + "status", + Number( + runtime.exec.call( + _xhr, + "XMLHttpRequest", + "getStatus" + ) || 0 + ) + ); + _p("statusText", httpCode[_p("status")] || ""); + + _p( + "response", + runtime.exec.call( + _xhr, + "XMLHttpRequest", + "getResponse", + _p("responseType") + ) + ); + + if ( + !!~Basic.inArray(_p("responseType"), [ + "text", + "", + ]) + ) { + _p("responseText", _p("response")); + } else if (_p("responseType") === "document") { + _p("responseXML", _p("response")); + } + + _responseHeaders = runtime.exec.call( + _xhr, + "XMLHttpRequest", + "getAllResponseHeaders" + ); + + self.dispatchEvent("readystatechange"); + + if (_p("status") > 0) { + // status 0 usually means that server is unreachable + if (_upload_events_flag) { + self.upload.dispatchEvent(e); + } + self.dispatchEvent(e); + } else { + _error_flag = true; + self.dispatchEvent("error"); + } + loadEnd(); + }); + + _xhr.bind("Abort", function (e) { + self.dispatchEvent(e); + loadEnd(); + }); + + _xhr.bind("Error", function (e) { + _error_flag = true; + _p("readyState", XMLHttpRequest.DONE); + self.dispatchEvent("readystatechange"); + _upload_complete_flag = true; + self.dispatchEvent(e); + loadEnd(); + }); + + runtime.exec.call( + _xhr, + "XMLHttpRequest", + "send", + { + url: _url, + method: _method, + async: _async, + user: _user, + password: _password, + headers: _headers, + mimeType: _mimeType, + encoding: _encoding, + responseType: self.responseType, + withCredentials: self.withCredentials, + options: _options, + }, + data + ); + } + + // clarify our requirements + if (typeof _options.required_caps === "string") { + _options.required_caps = Runtime.parseCaps( + _options.required_caps + ); + } + + _options.required_caps = Basic.extend( + {}, + _options.required_caps, + { + return_response_type: self.responseType, + } + ); + + if (data instanceof FormData) { + _options.required_caps.send_multipart = true; + } + + if (!Basic.isEmptyObj(_headers)) { + _options.required_caps.send_custom_headers = true; + } + + if (!_same_origin_flag) { + _options.required_caps.do_cors = true; + } + + if (_options.ruid) { + // we do not need to wait if we can connect directly + exec(_xhr.connectRuntime(_options)); + } else { + _xhr.bind("RuntimeInit", function (e, runtime) { + exec(runtime); + }); + _xhr.bind("RuntimeError", function (e, err) { + self.dispatchEvent("RuntimeError", err); + }); + _xhr.connectRuntime(_options); + } + } + + function _reset() { + _p("responseText", ""); + _p("responseXML", null); + _p("response", null); + _p("status", 0); + _p("statusText", ""); + _start_time = _timeoutset_time = null; + } + } + + XMLHttpRequest.UNSENT = 0; + XMLHttpRequest.OPENED = 1; + XMLHttpRequest.HEADERS_RECEIVED = 2; + XMLHttpRequest.LOADING = 3; + XMLHttpRequest.DONE = 4; + + XMLHttpRequest.prototype = EventTarget.instance; + + return XMLHttpRequest; + }); + + // Included from: src/javascript/runtime/Transporter.js + + /** + * Transporter.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/runtime/Transporter", [ + "moxie/core/utils/Basic", + "moxie/core/utils/Encode", + "moxie/runtime/RuntimeClient", + "moxie/core/EventTarget", + ], function (Basic, Encode, RuntimeClient, EventTarget) { + /** + @class moxie/runtime/Transporter + @constructor + */ + function Transporter() { + var mod, _runtime, _data, _size, _pos, _chunk_size; + + RuntimeClient.call(this); + + Basic.extend(this, { + uid: Basic.guid("uid_"), + + state: Transporter.IDLE, + + result: null, + + transport: function (data, type, options) { + var self = this; + + options = Basic.extend( + { + chunk_size: 204798, + }, + options + ); + + // should divide by three, base64 requires this + if ((mod = options.chunk_size % 3)) { + options.chunk_size += 3 - mod; + } + + _chunk_size = options.chunk_size; + + _reset.call(this); + _data = data; + _size = data.length; + + if ( + Basic.typeOf(options) === "string" || + options.ruid + ) { + _run.call(self, type, this.connectRuntime(options)); + } else { + // we require this to run only once + var cb = function (e, runtime) { + self.unbind("RuntimeInit", cb); + _run.call(self, type, runtime); + }; + this.bind("RuntimeInit", cb); + this.connectRuntime(options); + } + }, + + abort: function () { + var self = this; + + self.state = Transporter.IDLE; + if (_runtime) { + _runtime.exec.call(self, "Transporter", "clear"); + self.trigger("TransportingAborted"); + } + + _reset.call(self); + }, + + destroy: function () { + this.unbindAll(); + _runtime = null; + this.disconnectRuntime(); + _reset.call(this); + }, + }); + + function _reset() { + _size = _pos = 0; + _data = this.result = null; + } + + function _run(type, runtime) { + var self = this; + + _runtime = runtime; + + //self.unbind("RuntimeInit"); + + self.bind( + "TransportingProgress", + function (e) { + _pos = e.loaded; + + if ( + _pos < _size && + Basic.inArray(self.state, [ + Transporter.IDLE, + Transporter.DONE, + ]) === -1 + ) { + _transport.call(self); + } + }, + 999 + ); + + self.bind( + "TransportingComplete", + function () { + _pos = _size; + self.state = Transporter.DONE; + _data = null; // clean a bit + self.result = _runtime.exec.call( + self, + "Transporter", + "getAsBlob", + type || "" + ); + }, + 999 + ); + + self.state = Transporter.BUSY; + self.trigger("TransportingStarted"); + _transport.call(self); + } + + function _transport() { + var self = this, + chunk, + bytesLeft = _size - _pos; + + if (_chunk_size > bytesLeft) { + _chunk_size = bytesLeft; + } + + chunk = Encode.btoa(_data.substr(_pos, _chunk_size)); + _runtime.exec.call( + self, + "Transporter", + "receive", + chunk, + _size + ); + } + } + + Transporter.IDLE = 0; + Transporter.BUSY = 1; + Transporter.DONE = 2; + + Transporter.prototype = EventTarget.instance; + + return Transporter; + }); + + // Included from: src/javascript/image/Image.js + + /** + * Image.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/image/Image", [ + "moxie/core/utils/Basic", + "moxie/core/utils/Dom", + "moxie/core/Exceptions", + "moxie/file/FileReaderSync", + "moxie/xhr/XMLHttpRequest", + "moxie/runtime/Runtime", + "moxie/runtime/RuntimeClient", + "moxie/runtime/Transporter", + "moxie/core/utils/Env", + "moxie/core/EventTarget", + "moxie/file/Blob", + "moxie/file/File", + "moxie/core/utils/Encode", + ], function (Basic, Dom, x, FileReaderSync, XMLHttpRequest, Runtime, RuntimeClient, Transporter, Env, EventTarget, Blob, File, Encode) { + /** + Image preloading and manipulation utility. Additionally it provides access to image meta info (Exif, GPS) and raw binary data. + + @class moxie/image/Image + @constructor + @extends EventTarget + */ + var dispatches = [ + "progress", + + /** + Dispatched when loading is complete. + + @event load + @param {Object} event + */ + "load", + + "error", + + /** + Dispatched when resize operation is complete. + + @event resize + @param {Object} event + */ + "resize", + + /** + Dispatched when visual representation of the image is successfully embedded + into the corresponsing container. + + @event embedded + @param {Object} event + */ + "embedded", + ]; + + function Image() { + RuntimeClient.call(this); + + Basic.extend(this, { + /** + Unique id of the component + + @property uid + @type {String} + */ + uid: Basic.guid("uid_"), + + /** + Unique id of the connected runtime, if any. + + @property ruid + @type {String} + */ + ruid: null, + + /** + Name of the file, that was used to create an image, if available. If not equals to empty string. + + @property name + @type {String} + @default "" + */ + name: "", + + /** + Size of the image in bytes. Actual value is set only after image is preloaded. + + @property size + @type {Number} + @default 0 + */ + size: 0, + + /** + Width of the image. Actual value is set only after image is preloaded. + + @property width + @type {Number} + @default 0 + */ + width: 0, + + /** + Height of the image. Actual value is set only after image is preloaded. + + @property height + @type {Number} + @default 0 + */ + height: 0, + + /** + Mime type of the image. Currently only image/jpeg and image/png are supported. Actual value is set only after image is preloaded. + + @property type + @type {String} + @default "" + */ + type: "", + + /** + Holds meta info (Exif, GPS). Is populated only for image/jpeg. Actual value is set only after image is preloaded. + + @property meta + @type {Object} + @default {} + */ + meta: {}, + + /** + Alias for load method, that takes another mOxie.Image object as a source (see load). + + @method clone + @param {Image} src Source for the image + @param {Boolean} [exact=false] Whether to activate in-depth clone mode + */ + clone: function () { + this.load.apply(this, arguments); + }, + + /** + Loads image from various sources. Currently the source for new image can be: mOxie.Image, mOxie.Blob/mOxie.File, + native Blob/File, dataUrl or URL. Depending on the type of the source, arguments - differ. When source is URL, + Image will be downloaded from remote destination and loaded in memory. + + @example + var img = new mOxie.Image(); + img.onload = function() { + var blob = img.getAsBlob(); + + var formData = new mOxie.FormData(); + formData.append('file', blob); + + var xhr = new mOxie.XMLHttpRequest(); + xhr.onload = function() { + // upload complete + }; + xhr.open('post', 'upload.php'); + xhr.send(formData); + }; + img.load("http://www.moxiecode.com/images/mox-logo.jpg"); // notice file extension (.jpg) + + + @method load + @param {Image|Blob|File|String} src Source for the image + @param {Boolean|Object} [mixed] + */ + load: function () { + _load.apply(this, arguments); + }, + + /** + Resizes the image to fit the specified width/height. If crop is specified, image will also be + cropped to the exact dimensions. + + @method resize + @since 3.0 + @param {Object} options + @param {Number} options.width Resulting width + @param {Number} [options.height=width] Resulting height (optional, if not supplied will default to width) + @param {String} [options.type='image/jpeg'] MIME type of the resulting image + @param {Number} [options.quality=90] In the case of JPEG, controls the quality of resulting image + @param {Boolean} [options.crop='cc'] If not falsy, image will be cropped, by default from center + @param {Boolean} [options.fit=true] In case of crop whether to upscale the image to fit the exact dimensions + @param {Boolean} [options.preserveHeaders=true] Whether to preserve meta headers (on JPEGs after resize) + @param {String} [options.resample='default'] Resampling algorithm to use during resize + @param {Boolean} [options.multipass=true] Whether to scale the image in steps (results in better quality) + */ + resize: function (options) { + var self = this; + var orientation; + var scale; + + var srcRect = { + x: 0, + y: 0, + width: self.width, + height: self.height, + }; + + var opts = Basic.extendIf( + { + width: self.width, + height: self.height, + type: self.type || "image/jpeg", + quality: 90, + crop: false, + fit: true, + preserveHeaders: true, + resample: "default", + multipass: true, + }, + options + ); + + try { + if (!self.size) { + // only preloaded image objects can be used as source + throw new x.DOMException( + x.DOMException.INVALID_STATE_ERR + ); + } + + // no way to reliably intercept the crash due to high resolution, so we simply avoid it + if ( + self.width > Image.MAX_RESIZE_WIDTH || + self.height > Image.MAX_RESIZE_HEIGHT + ) { + throw new x.ImageError( + x.ImageError.MAX_RESOLUTION_ERR + ); + } + + // take into account orientation tag + orientation = + (self.meta && + self.meta.tiff && + self.meta.tiff.Orientation) || + 1; + + if ( + Basic.inArray(orientation, [5, 6, 7, 8]) !== -1 + ) { + // values that require 90 degree rotation + var tmp = opts.width; + opts.width = opts.height; + opts.height = tmp; + } + + if (opts.crop) { + scale = Math.max( + opts.width / self.width, + opts.height / self.height + ); + + if (options.fit) { + // first scale it up or down to fit the original image + srcRect.width = Math.min( + Math.ceil(opts.width / scale), + self.width + ); + srcRect.height = Math.min( + Math.ceil(opts.height / scale), + self.height + ); + + // recalculate the scale for adapted dimensions + scale = opts.width / srcRect.width; + } else { + srcRect.width = Math.min( + opts.width, + self.width + ); + srcRect.height = Math.min( + opts.height, + self.height + ); + + // now we do not need to scale it any further + scale = 1; + } + + if (typeof opts.crop === "boolean") { + opts.crop = "cc"; + } + + switch ( + opts.crop.toLowerCase().replace(/_/, "-") + ) { + case "rb": + case "right-bottom": + srcRect.x = self.width - srcRect.width; + srcRect.y = + self.height - srcRect.height; + break; + + case "cb": + case "center-bottom": + srcRect.x = Math.floor( + (self.width - srcRect.width) / 2 + ); + srcRect.y = + self.height - srcRect.height; + break; + + case "lb": + case "left-bottom": + srcRect.x = 0; + srcRect.y = + self.height - srcRect.height; + break; + + case "lt": + case "left-top": + srcRect.x = 0; + srcRect.y = 0; + break; + + case "ct": + case "center-top": + srcRect.x = Math.floor( + (self.width - srcRect.width) / 2 + ); + srcRect.y = 0; + break; + + case "rt": + case "right-top": + srcRect.x = self.width - srcRect.width; + srcRect.y = 0; + break; + + case "rc": + case "right-center": + case "right-middle": + srcRect.x = self.width - srcRect.width; + srcRect.y = Math.floor( + (self.height - srcRect.height) / 2 + ); + break; + + case "lc": + case "left-center": + case "left-middle": + srcRect.x = 0; + srcRect.y = Math.floor( + (self.height - srcRect.height) / 2 + ); + break; + + case "cc": + case "center-center": + case "center-middle": + default: + srcRect.x = Math.floor( + (self.width - srcRect.width) / 2 + ); + srcRect.y = Math.floor( + (self.height - srcRect.height) / 2 + ); + } + + // original image might be smaller than requested crop, so - avoid negative values + srcRect.x = Math.max(srcRect.x, 0); + srcRect.y = Math.max(srcRect.y, 0); + } else { + scale = Math.min( + opts.width / self.width, + opts.height / self.height + ); + } + + this.exec("Image", "resize", srcRect, scale, opts); + } catch (ex) { + // for now simply trigger error event + self.trigger("error", ex.code); + } + }, + + /** + Downsizes the image to fit the specified width/height. If crop is supplied, image will be cropped to exact dimensions. + + @method downsize + @deprecated use resize() + */ + downsize: function (options) { + var defaults = { + width: this.width, + height: this.height, + type: this.type || "image/jpeg", + quality: 90, + crop: false, + preserveHeaders: true, + resample: "default", + }, + opts; + + if (typeof options === "object") { + opts = Basic.extend(defaults, options); + } else { + // for backward compatibility + opts = Basic.extend(defaults, { + width: arguments[0], + height: arguments[1], + crop: arguments[2], + preserveHeaders: arguments[3], + }); + } + + this.resize(opts); + }, + + /** + Alias for downsize(width, height, true). (see downsize) + + @method crop + @param {Number} width Resulting width + @param {Number} [height=width] Resulting height (optional, if not supplied will default to width) + @param {Boolean} [preserveHeaders=true] Whether to preserve meta headers (on JPEGs after resize) + */ + crop: function (width, height, preserveHeaders) { + this.downsize(width, height, true, preserveHeaders); + }, + + getAsCanvas: function () { + if (!Env.can("create_canvas")) { + throw new x.RuntimeError( + x.RuntimeError.NOT_SUPPORTED_ERR + ); + } + return this.exec("Image", "getAsCanvas"); + }, + + /** + Retrieves image in it's current state as mOxie.Blob object. Cannot be run on empty or image in progress (throws + DOMException.INVALID_STATE_ERR). + + @method getAsBlob + @param {String} [type="image/jpeg"] Mime type of resulting blob. Can either be image/jpeg or image/png + @param {Number} [quality=90] Applicable only together with mime type image/jpeg + @return {Blob} Image as Blob + */ + getAsBlob: function (type, quality) { + if (!this.size) { + throw new x.DOMException( + x.DOMException.INVALID_STATE_ERR + ); + } + return this.exec( + "Image", + "getAsBlob", + type || "image/jpeg", + quality || 90 + ); + }, + + /** + Retrieves image in it's current state as dataURL string. Cannot be run on empty or image in progress (throws + DOMException.INVALID_STATE_ERR). + + @method getAsDataURL + @param {String} [type="image/jpeg"] Mime type of resulting blob. Can either be image/jpeg or image/png + @param {Number} [quality=90] Applicable only together with mime type image/jpeg + @return {String} Image as dataURL string + */ + getAsDataURL: function (type, quality) { + if (!this.size) { + throw new x.DOMException( + x.DOMException.INVALID_STATE_ERR + ); + } + return this.exec( + "Image", + "getAsDataURL", + type || "image/jpeg", + quality || 90 + ); + }, + + /** + Retrieves image in it's current state as binary string. Cannot be run on empty or image in progress (throws + DOMException.INVALID_STATE_ERR). + + @method getAsBinaryString + @param {String} [type="image/jpeg"] Mime type of resulting blob. Can either be image/jpeg or image/png + @param {Number} [quality=90] Applicable only together with mime type image/jpeg + @return {String} Image as binary string + */ + getAsBinaryString: function (type, quality) { + var dataUrl = this.getAsDataURL(type, quality); + return Encode.atob( + dataUrl.substring(dataUrl.indexOf("base64,") + 7) + ); + }, + + /** + Embeds a visual representation of the image into the specified node. Depending on the runtime, + it might be a canvas, an img node or a thrid party shim object (Flash or SilverLight - very rare, + can be used in legacy browsers that do not have canvas or proper dataURI support). + + @method embed + @param {DOMElement} el DOM element to insert the image object into + @param {Object} [options] + @param {Number} [options.width] The width of an embed (defaults to the image width) + @param {Number} [options.height] The height of an embed (defaults to the image height) + @param {String} [options.type="image/jpeg"] Mime type + @param {Number} [options.quality=90] Quality of an embed, if mime type is image/jpeg + @param {Boolean} [options.crop=false] Whether to crop an embed to the specified dimensions + */ + embed: function (el, options) { + var self = this, + runtime; // this has to be outside of all the closures to contain proper runtime + + var opts = Basic.extend( + { + width: this.width, + height: this.height, + type: this.type || "image/jpeg", + quality: 90, + }, + options + ); + + function render(type, quality) { + var img = this; + + // if possible, embed a canvas element directly + if (Env.can("create_canvas")) { + var canvas = img.getAsCanvas(); + if (canvas) { + el.appendChild(canvas); + canvas = null; + img.destroy(); + self.trigger("embedded"); + return; + } + } + + var dataUrl = img.getAsDataURL(type, quality); + if (!dataUrl) { + throw new x.ImageError( + x.ImageError.WRONG_FORMAT + ); + } + + if (Env.can("use_data_uri_of", dataUrl.length)) { + el.innerHTML = + ''; + img.destroy(); + self.trigger("embedded"); + } else { + var tr = new Transporter(); + + tr.bind("TransportingComplete", function () { + runtime = self.connectRuntime( + this.result.ruid + ); + + self.bind( + "Embedded", + function () { + // position and size properly + Basic.extend( + runtime.getShimContainer() + .style, + { + //position: 'relative', + top: "0px", + left: "0px", + width: img.width + "px", + height: img.height + "px", + } + ); + + // some shims (Flash/SilverLight) reinitialize, if parent element is hidden, reordered or it's + // position type changes (in Gecko), but since we basically need this only in IEs 6/7 and + // sometimes 8 and they do not have this problem, we can comment this for now + /*tr.bind("RuntimeInit", function(e, runtime) { + tr.destroy(); + runtime.destroy(); + onResize.call(self); // re-feed our image data + });*/ + + runtime = null; // release + }, + 999 + ); + + runtime.exec.call( + self, + "ImageView", + "display", + this.result.uid, + width, + height + ); + img.destroy(); + }); + + tr.transport( + Encode.atob( + dataUrl.substring( + dataUrl.indexOf("base64,") + 7 + ) + ), + type, + { + required_caps: { + display_media: true, + }, + runtime_order: "flash,silverlight", + container: el, + } + ); + } + } + + try { + if (!(el = Dom.get(el))) { + throw new x.DOMException( + x.DOMException.INVALID_NODE_TYPE_ERR + ); + } + + if (!this.size) { + // only preloaded image objects can be used as source + throw new x.DOMException( + x.DOMException.INVALID_STATE_ERR + ); + } + + // high-resolution images cannot be consistently handled across the runtimes + if ( + this.width > Image.MAX_RESIZE_WIDTH || + this.height > Image.MAX_RESIZE_HEIGHT + ) { + //throw new x.ImageError(x.ImageError.MAX_RESOLUTION_ERR); + } + + var imgCopy = new Image(); + + imgCopy.bind("Resize", function () { + render.call(this, opts.type, opts.quality); + }); + + imgCopy.bind("Load", function () { + this.downsize(opts); + }); + + // if embedded thumb data is available and dimensions are big enough, use it + if ( + this.meta.thumb && + this.meta.thumb.width >= opts.width && + this.meta.thumb.height >= opts.height + ) { + imgCopy.load(this.meta.thumb.data); + } else { + imgCopy.clone(this, false); + } + + return imgCopy; + } catch (ex) { + // for now simply trigger error event + this.trigger("error", ex.code); + } + }, + + /** + Properly destroys the image and frees resources in use. If any. Recommended way to dispose mOxie.Image object. + + @method destroy + */ + destroy: function () { + if (this.ruid) { + this.getRuntime().exec.call( + this, + "Image", + "destroy" + ); + this.disconnectRuntime(); + } + if (this.meta && this.meta.thumb) { + // thumb is blob, make sure we destroy it first + this.meta.thumb.data.destroy(); + } + this.unbindAll(); + }, + }); + + // this is here, because in order to bind properly, we need uid, which is created above + this.handleEventProps(dispatches); + + this.bind( + "Load Resize", + function () { + return _updateInfo.call(this); // if operation fails (e.g. image is neither PNG nor JPEG) cancel all pending events + }, + 999 + ); + + function _updateInfo(info) { + try { + if (!info) { + info = this.exec("Image", "getInfo"); + } + + this.size = info.size; + this.width = info.width; + this.height = info.height; + this.type = info.type; + this.meta = info.meta; + + // update file name, only if empty + if (this.name === "") { + this.name = info.name; + } + + return true; + } catch (ex) { + this.trigger("error", ex.code); + return false; + } + } + + function _load(src) { + var srcType = Basic.typeOf(src); + + try { + // if source is Image + if (src instanceof Image) { + if (!src.size) { + // only preloaded image objects can be used as source + throw new x.DOMException( + x.DOMException.INVALID_STATE_ERR + ); + } + _loadFromImage.apply(this, arguments); + } + // if source is o.Blob/o.File + else if (src instanceof Blob) { + if ( + !~Basic.inArray(src.type, [ + "image/jpeg", + "image/png", + ]) + ) { + throw new x.ImageError( + x.ImageError.WRONG_FORMAT + ); + } + _loadFromBlob.apply(this, arguments); + } + // if native blob/file + else if ( + Basic.inArray(srcType, ["blob", "file"]) !== -1 + ) { + _load.call(this, new File(null, src), arguments[1]); + } + // if String + else if (srcType === "string") { + // if dataUrl String + if (src.substr(0, 5) === "data:") { + _load.call( + this, + new Blob(null, { data: src }), + arguments[1] + ); + } + // else assume Url, either relative or absolute + else { + _loadFromUrl.apply(this, arguments); + } + } + // if source seems to be an img node + else if ( + srcType === "node" && + src.nodeName.toLowerCase() === "img" + ) { + _load.call(this, src.src, arguments[1]); + } else { + throw new x.DOMException( + x.DOMException.TYPE_MISMATCH_ERR + ); + } + } catch (ex) { + // for now simply trigger error event + this.trigger("error", ex.code); + } + } + + function _loadFromImage(img, exact) { + var runtime = this.connectRuntime(img.ruid); + this.ruid = runtime.uid; + runtime.exec.call( + this, + "Image", + "loadFromImage", + img, + Basic.typeOf(exact) === "undefined" ? true : exact + ); + } + + function _loadFromBlob(blob, options) { + var self = this; + + self.name = blob.name || ""; + + function exec(runtime) { + self.ruid = runtime.uid; + runtime.exec.call(self, "Image", "loadFromBlob", blob); + } + + if (blob.isDetached()) { + this.bind("RuntimeInit", function (e, runtime) { + exec(runtime); + }); + + // convert to object representation + if ( + options && + typeof options.required_caps === "string" + ) { + options.required_caps = Runtime.parseCaps( + options.required_caps + ); + } + + this.connectRuntime( + Basic.extend( + { + required_caps: { + access_image_binary: true, + resize_image: true, + }, + }, + options + ) + ); + } else { + exec(this.connectRuntime(blob.ruid)); + } + } + + function _loadFromUrl(url, options) { + var self = this, + xhr; + + xhr = new XMLHttpRequest(); + + xhr.open("get", url); + xhr.responseType = "blob"; + + xhr.onprogress = function (e) { + self.trigger(e); + }; + + xhr.onload = function () { + _loadFromBlob.call(self, xhr.response, true); + }; + + xhr.onerror = function (e) { + self.trigger(e); + }; + + xhr.onloadend = function () { + xhr.destroy(); + }; + + xhr.bind("RuntimeError", function (e, err) { + self.trigger("RuntimeError", err); + }); + + xhr.send(null, options); + } + } + + // virtual world will crash on you if image has a resolution higher than this: + Image.MAX_RESIZE_WIDTH = 8192; + Image.MAX_RESIZE_HEIGHT = 8192; + + Image.prototype = EventTarget.instance; + + return Image; + }); + + // Included from: src/javascript/runtime/html5/Runtime.js + + /** + * Runtime.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /*global File:true */ + + /** +Defines constructor for HTML5 runtime. + +@class moxie/runtime/html5/Runtime +@private +*/ + define("moxie/runtime/html5/Runtime", [ + "moxie/core/utils/Basic", + "moxie/core/Exceptions", + "moxie/runtime/Runtime", + "moxie/core/utils/Env", + ], function (Basic, x, Runtime, Env) { + var type = "html5", + extensions = {}; + + function Html5Runtime(options) { + var I = this, + Test = Runtime.capTest, + True = Runtime.capTrue; + var caps = Basic.extend( + { + access_binary: Test( + window.FileReader || + (window.File && window.File.getAsDataURL) + ), + access_image_binary: function () { + return I.can("access_binary") && !!extensions.Image; + }, + display_media: Test( + (Env.can("create_canvas") || + Env.can("use_data_uri_over32kb")) && + defined("moxie/image/Image") + ), + do_cors: Test( + window.XMLHttpRequest && + "withCredentials" in new XMLHttpRequest() + ), + drag_and_drop: Test( + (function () { + // this comes directly from Modernizr: http://www.modernizr.com/ + var div = document.createElement("div"); + // IE has support for drag and drop since version 5, but doesn't support dropping files from desktop + return ( + ("draggable" in div || + ("ondragstart" in div && + "ondrop" in div)) && + (Env.browser !== "IE" || + Env.verComp(Env.version, 9, ">")) + ); + })() + ), + filter_by_extension: Test( + (function () { + // if you know how to feature-detect this, please suggest + return !( + (Env.browser === "Chrome" && + Env.verComp(Env.version, 28, "<")) || + (Env.browser === "IE" && + Env.verComp(Env.version, 10, "<")) || + (Env.browser === "Safari" && + Env.verComp(Env.version, 7, "<")) || + (Env.browser === "Firefox" && + Env.verComp(Env.version, 37, "<")) + ); + })() + ), + return_response_headers: True, + return_response_type: function (responseType) { + if (responseType === "json" && !!window.JSON) { + // we can fake this one even if it's not supported + return true; + } + return Env.can( + "return_response_type", + responseType + ); + }, + return_status_code: True, + report_upload_progress: Test( + window.XMLHttpRequest && new XMLHttpRequest().upload + ), + resize_image: function () { + return ( + I.can("access_binary") && + Env.can("create_canvas") + ); + }, + select_file: function () { + return Env.can("use_fileinput") && window.File; + }, + select_folder: function () { + return ( + I.can("select_file") && + ((Env.browser === "Chrome" && + Env.verComp(Env.version, 21, ">=")) || + (Env.browser === "Firefox" && + Env.verComp(Env.version, 42, ">="))) // https://developer.mozilla.org/en-US/Firefox/Releases/42 + ); + }, + select_multiple: function () { + // it is buggy on Safari Windows and iOS + return ( + I.can("select_file") && + !( + Env.browser === "Safari" && + Env.os === "Windows" + ) && + !( + Env.os === "iOS" && + Env.verComp(Env.osVersion, "7.0.0", ">") && + Env.verComp(Env.osVersion, "8.0.0", "<") + ) + ); + }, + send_binary_string: Test( + window.XMLHttpRequest && + (new XMLHttpRequest().sendAsBinary || + (window.Uint8Array && window.ArrayBuffer)) + ), + send_custom_headers: Test(window.XMLHttpRequest), + send_multipart: function () { + return ( + !!( + window.XMLHttpRequest && + new XMLHttpRequest().upload && + window.FormData + ) || I.can("send_binary_string") + ); + }, + slice_blob: Test( + window.File && + (File.prototype.mozSlice || + File.prototype.webkitSlice || + File.prototype.slice) + ), + stream_upload: function () { + return ( + I.can("slice_blob") && I.can("send_multipart") + ); + }, + summon_file_dialog: function () { + // yeah... some dirty sniffing here... + return ( + I.can("select_file") && + ((Env.browser === "Firefox" && + Env.verComp(Env.version, 4, ">=")) || + (Env.browser === "Opera" && + Env.verComp(Env.version, 12, ">=")) || + (Env.browser === "IE" && + Env.verComp(Env.version, 10, ">=")) || + !!~Basic.inArray(Env.browser, [ + "Chrome", + "Safari", + "Edge", + ])) + ); + }, + upload_filesize: True, + use_http_method: True, + }, + arguments[2] + ); + + Runtime.call(this, options, arguments[1] || type, caps); + + Basic.extend(this, { + init: function () { + this.trigger("Init"); + }, + + destroy: (function (destroy) { + // extend default destroy method + return function () { + destroy.call(I); + destroy = I = null; + }; + })(this.destroy), + }); + + Basic.extend(this.getShim(), extensions); + } + + Runtime.addConstructor(type, Html5Runtime); + + return extensions; + }); + + // Included from: src/javascript/runtime/html5/file/Blob.js + + /** + * Blob.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/html5/file/Blob +@private +*/ + define("moxie/runtime/html5/file/Blob", [ + "moxie/runtime/html5/Runtime", + "moxie/file/Blob", + ], function (extensions, Blob) { + function HTML5Blob() { + function w3cBlobSlice(blob, start, end) { + var blobSlice; + + if (window.File.prototype.slice) { + try { + blob.slice(); // depricated version will throw WRONG_ARGUMENTS_ERR exception + return blob.slice(start, end); + } catch (e) { + // depricated slice method + return blob.slice(start, end - start); + } + // slice method got prefixed: https://bugzilla.mozilla.org/show_bug.cgi?id=649672 + } else if ( + (blobSlice = + window.File.prototype.webkitSlice || + window.File.prototype.mozSlice) + ) { + return blobSlice.call(blob, start, end); + } else { + return null; // or throw some exception + } + } + + this.slice = function () { + return new Blob( + this.getRuntime().uid, + w3cBlobSlice.apply(this, arguments) + ); + }; + } + + return (extensions.Blob = HTML5Blob); + }); + + // Included from: src/javascript/core/utils/Events.js + + /** + * Events.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + define("moxie/core/utils/Events", [ + "moxie/core/utils/Basic", + ], function (Basic) { + var eventhash = {}, + uid = "moxie_" + Basic.guid(); + + // IE W3C like event funcs + function preventDefault() { + this.returnValue = false; + } + + function stopPropagation() { + this.cancelBubble = true; + } + + /** + Adds an event handler to the specified object and store reference to the handler + in objects internal Plupload registry (@see removeEvent). + + @method addEvent + @for Utils + @static + @param {Object} obj DOM element like object to add handler to. + @param {String} name Name to add event listener to. + @param {Function} callback Function to call when event occurs. + @param {String} [key] that might be used to add specifity to the event record. + */ + var addEvent = function (obj, name, callback, key) { + var func, events; + + name = name.toLowerCase(); + + // Add event listener + if (obj.addEventListener) { + func = callback; + + obj.addEventListener(name, func, false); + } else if (obj.attachEvent) { + func = function () { + var evt = window.event; + + if (!evt.target) { + evt.target = evt.srcElement; + } + + evt.preventDefault = preventDefault; + evt.stopPropagation = stopPropagation; + + callback(evt); + }; + + obj.attachEvent("on" + name, func); + } + + // Log event handler to objects internal mOxie registry + if (!obj[uid]) { + obj[uid] = Basic.guid(); + } + + if (!eventhash.hasOwnProperty(obj[uid])) { + eventhash[obj[uid]] = {}; + } + + events = eventhash[obj[uid]]; + + if (!events.hasOwnProperty(name)) { + events[name] = []; + } + + events[name].push({ + func: func, + orig: callback, // store original callback for IE + key: key, + }); + }; + + /** + Remove event handler from the specified object. If third argument (callback) + is not specified remove all events with the specified name. + + @method removeEvent + @static + @param {Object} obj DOM element to remove event listener(s) from. + @param {String} name Name of event listener to remove. + @param {Function|String} [callback] might be a callback or unique key to match. + */ + var removeEvent = function (obj, name, callback) { + var type, undef; + + name = name.toLowerCase(); + + if ( + obj[uid] && + eventhash[obj[uid]] && + eventhash[obj[uid]][name] + ) { + type = eventhash[obj[uid]][name]; + } else { + return; + } + + for (var i = type.length - 1; i >= 0; i--) { + // undefined or not, key should match + if (type[i].orig === callback || type[i].key === callback) { + if (obj.removeEventListener) { + obj.removeEventListener(name, type[i].func, false); + } else if (obj.detachEvent) { + obj.detachEvent("on" + name, type[i].func); + } + + type[i].orig = null; + type[i].func = null; + type.splice(i, 1); + + // If callback was passed we are done here, otherwise proceed + if (callback !== undef) { + break; + } + } + } + + // If event array got empty, remove it + if (!type.length) { + delete eventhash[obj[uid]][name]; + } + + // If mOxie registry has become empty, remove it + if (Basic.isEmptyObj(eventhash[obj[uid]])) { + delete eventhash[obj[uid]]; + + // IE doesn't let you remove DOM object property with - delete + try { + delete obj[uid]; + } catch (e) { + obj[uid] = undef; + } + } + }; + + /** + Remove all kind of events from the specified object + + @method removeAllEvents + @static + @param {Object} obj DOM element to remove event listeners from. + @param {String} [key] unique key to match, when removing events. + */ + var removeAllEvents = function (obj, key) { + if (!obj || !obj[uid]) { + return; + } + + Basic.each(eventhash[obj[uid]], function (events, name) { + removeEvent(obj, name, key); + }); + }; + + return { + addEvent: addEvent, + removeEvent: removeEvent, + removeAllEvents: removeAllEvents, + }; + }); + + // Included from: src/javascript/runtime/html5/file/FileInput.js + + /** + * FileInput.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/html5/file/FileInput +@private +*/ + define("moxie/runtime/html5/file/FileInput", [ + "moxie/runtime/html5/Runtime", + "moxie/file/File", + "moxie/core/utils/Basic", + "moxie/core/utils/Dom", + "moxie/core/utils/Events", + "moxie/core/utils/Mime", + "moxie/core/utils/Env", + ], function (extensions, File, Basic, Dom, Events, Mime, Env) { + function FileInput() { + var _options, _browseBtnZIndex; // save original z-index + + Basic.extend(this, { + init: function (options) { + var comp = this, + I = comp.getRuntime(), + input, + shimContainer, + mimes, + browseButton, + zIndex, + top; + + _options = options; + + // figure out accept string + mimes = + _options.accept.mimes || + Mime.extList2mimes( + _options.accept, + I.can("filter_by_extension") + ); + + shimContainer = I.getShimContainer(); + + shimContainer.innerHTML = + '"; + + input = Dom.get(I.uid); + + // prepare file input to be placed underneath the browse_button element + Basic.extend(input.style, { + position: "absolute", + top: 0, + left: 0, + width: "100%", + height: "100%", + }); + + browseButton = Dom.get(_options.browse_button); + _browseBtnZIndex = + Dom.getStyle(browseButton, "z-index") || "auto"; + + // Route click event to the input[type=file] element for browsers that support such behavior + if (I.can("summon_file_dialog")) { + if ( + Dom.getStyle(browseButton, "position") === + "static" + ) { + browseButton.style.position = "relative"; + } + + Events.addEvent( + browseButton, + "click", + function (e) { + var input = Dom.get(I.uid); + if (input && !input.disabled) { + // for some reason FF (up to 8.0.1 so far) lets to click disabled input[type=file] + input.click(); + } + e.preventDefault(); + }, + comp.uid + ); + + comp.bind("Refresh", function () { + zIndex = parseInt(_browseBtnZIndex, 10) || 1; + + Dom.get( + _options.browse_button + ).style.zIndex = zIndex; + this.getRuntime().getShimContainer().style.zIndex = + zIndex - 1; + }); + } + + /* Since we have to place input[type=file] on top of the browse_button for some browsers, + browse_button loses interactivity, so we restore it here */ + top = I.can("summon_file_dialog") + ? browseButton + : shimContainer; + + Events.addEvent( + top, + "mouseover", + function () { + comp.trigger("mouseenter"); + }, + comp.uid + ); + + Events.addEvent( + top, + "mouseout", + function () { + comp.trigger("mouseleave"); + }, + comp.uid + ); + + Events.addEvent( + top, + "mousedown", + function () { + comp.trigger("mousedown"); + }, + comp.uid + ); + + Events.addEvent( + Dom.get(_options.container), + "mouseup", + function () { + comp.trigger("mouseup"); + }, + comp.uid + ); + + input.onchange = function onChange(e) { + // there should be only one handler for this + comp.files = []; + + Basic.each(this.files, function (file) { + var relativePath = ""; + + if (_options.directory) { + // folders are represented by dots, filter them out (Chrome 11+) + if (file.name == ".") { + // if it looks like a folder... + return true; + } + } + + if (file.webkitRelativePath) { + relativePath = + "/" + + file.webkitRelativePath.replace( + /^\//, + "" + ); + } + + file = new File(I.uid, file); + file.relativePath = relativePath; + + comp.files.push(file); + }); + + // clearing the value enables the user to select the same file again if they want to + if ( + Env.browser !== "IE" && + Env.browser !== "IEMobile" + ) { + this.value = ""; + } else { + // in IE input[type="file"] is read-only so the only way to reset it is to re-insert it + var clone = this.cloneNode(true); + this.parentNode.replaceChild(clone, this); + clone.onchange = onChange; + } + + if (comp.files.length) { + comp.trigger("change"); + } + }; + + // ready event is perfectly asynchronous + comp.trigger({ + type: "ready", + async: true, + }); + + shimContainer = null; + }, + + setOption: function (name, value) { + var I = this.getRuntime(); + var input = Dom.get(I.uid); + + switch (name) { + case "accept": + if (value) { + var mimes = + value.mimes || + Mime.extList2mimes( + value, + I.can("filter_by_extension") + ); + input.setAttribute( + "accept", + mimes.join(",") + ); + } else { + input.removeAttribute("accept"); + } + break; + + case "directory": + if (value && I.can("select_folder")) { + input.setAttribute("directory", ""); + input.setAttribute("webkitdirectory", ""); + } else { + input.removeAttribute("directory"); + input.removeAttribute("webkitdirectory"); + } + break; + + case "multiple": + if (value && I.can("select_multiple")) { + input.setAttribute("multiple", ""); + } else { + input.removeAttribute("multiple"); + } + } + }, + + disable: function (state) { + var I = this.getRuntime(), + input; + + if ((input = Dom.get(I.uid))) { + input.disabled = !!state; + } + }, + + destroy: function () { + var I = this.getRuntime(), + shim = I.getShim(), + shimContainer = I.getShimContainer(), + container = _options && Dom.get(_options.container), + browseButton = + _options && Dom.get(_options.browse_button); + if (container) { + Events.removeAllEvents(container, this.uid); + } + + if (browseButton) { + Events.removeAllEvents(browseButton, this.uid); + browseButton.style.zIndex = _browseBtnZIndex; // reset to original value + } + + if (shimContainer) { + Events.removeAllEvents(shimContainer, this.uid); + shimContainer.innerHTML = ""; + } + + shim.removeInstance(this.uid); + + _options = shimContainer = container = browseButton = shim = null; + }, + }); + } + + return (extensions.FileInput = FileInput); + }); + + // Included from: src/javascript/runtime/html5/file/FileDrop.js + + /** + * FileDrop.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/html5/file/FileDrop +@private +*/ + define("moxie/runtime/html5/file/FileDrop", [ + "moxie/runtime/html5/Runtime", + "moxie/file/File", + "moxie/core/utils/Basic", + "moxie/core/utils/Dom", + "moxie/core/utils/Events", + "moxie/core/utils/Mime", + ], function (extensions, File, Basic, Dom, Events, Mime) { + function FileDrop() { + var _files = [], + _allowedExts = [], + _options, + _ruid; + + Basic.extend(this, { + init: function (options) { + var comp = this, + dropZone; + + _options = options; + _ruid = comp.ruid; // every dropped-in file should have a reference to the runtime + _allowedExts = _extractExts(_options.accept); + dropZone = _options.container; + + Events.addEvent( + dropZone, + "dragover", + function (e) { + if (!_hasFiles(e)) { + return; + } + e.preventDefault(); + e.dataTransfer.dropEffect = "copy"; + }, + comp.uid + ); + + Events.addEvent( + dropZone, + "drop", + function (e) { + if (!_hasFiles(e)) { + return; + } + e.preventDefault(); + + _files = []; + + // Chrome 21+ accepts folders via Drag'n'Drop + if ( + e.dataTransfer.items && + e.dataTransfer.items[0].webkitGetAsEntry + ) { + _readItems( + e.dataTransfer.items, + function () { + comp.files = _files; + comp.trigger("drop"); + } + ); + } else { + Basic.each( + e.dataTransfer.files, + function (file) { + _addFile(file); + } + ); + comp.files = _files; + comp.trigger("drop"); + } + }, + comp.uid + ); + + Events.addEvent( + dropZone, + "dragenter", + function (e) { + comp.trigger("dragenter"); + }, + comp.uid + ); + + Events.addEvent( + dropZone, + "dragleave", + function (e) { + comp.trigger("dragleave"); + }, + comp.uid + ); + }, + + destroy: function () { + Events.removeAllEvents( + _options && Dom.get(_options.container), + this.uid + ); + _ruid = _files = _allowedExts = _options = null; + }, + }); + + function _hasFiles(e) { + if (!e.dataTransfer || !e.dataTransfer.types) { + // e.dataTransfer.files is not available in Gecko during dragover + return false; + } + + var types = Basic.toArray(e.dataTransfer.types || []); + + return ( + Basic.inArray("Files", types) !== -1 || + Basic.inArray("public.file-url", types) !== -1 || // Safari < 5 + Basic.inArray("application/x-moz-file", types) !== -1 // Gecko < 1.9.2 (< Firefox 3.6) + ); + } + + function _addFile(file, relativePath) { + if (_isAcceptable(file)) { + var fileObj = new File(_ruid, file); + fileObj.relativePath = relativePath || ""; + _files.push(fileObj); + } + } + + function _extractExts(accept) { + var exts = []; + for (var i = 0; i < accept.length; i++) { + [].push.apply( + exts, + accept[i].extensions.split(/\s*,\s*/) + ); + } + return Basic.inArray("*", exts) === -1 ? exts : []; + } + + function _isAcceptable(file) { + if (!_allowedExts.length) { + return true; + } + var ext = Mime.getFileExtension(file.name); + return !ext || Basic.inArray(ext, _allowedExts) !== -1; + } + + function _readItems(items, cb) { + var entries = []; + Basic.each(items, function (item) { + var entry = item.webkitGetAsEntry(); + // Address #998 (https://code.google.com/p/chromium/issues/detail?id=332579) + if (entry) { + // file() fails on OSX when the filename contains a special character (e.g. umlaut): see #61 + if (entry.isFile) { + _addFile(item.getAsFile(), entry.fullPath); + } else { + entries.push(entry); + } + } + }); + + if (entries.length) { + _readEntries(entries, cb); + } else { + cb(); + } + } + + function _readEntries(entries, cb) { + var queue = []; + Basic.each(entries, function (entry) { + queue.push(function (cbcb) { + _readEntry(entry, cbcb); + }); + }); + Basic.inSeries(queue, function () { + cb(); + }); + } + + function _readEntry(entry, cb) { + if (entry.isFile) { + entry.file( + function (file) { + _addFile(file, entry.fullPath); + cb(); + }, + function () { + // fire an error event maybe + cb(); + } + ); + } else if (entry.isDirectory) { + _readDirEntry(entry, cb); + } else { + cb(); // not file, not directory? what then?.. + } + } + + function _readDirEntry(dirEntry, cb) { + var entries = [], + dirReader = dirEntry.createReader(); + + // keep quering recursively till no more entries + function getEntries(cbcb) { + dirReader.readEntries(function (moreEntries) { + if (moreEntries.length) { + [].push.apply(entries, moreEntries); + getEntries(cbcb); + } else { + cbcb(); + } + }, cbcb); + } + + // ...and you thought FileReader was crazy... + getEntries(function () { + _readEntries(entries, cb); + }); + } + } + + return (extensions.FileDrop = FileDrop); + }); + + // Included from: src/javascript/runtime/html5/file/FileReader.js + + /** + * FileReader.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/html5/file/FileReader +@private +*/ + define("moxie/runtime/html5/file/FileReader", [ + "moxie/runtime/html5/Runtime", + "moxie/core/utils/Encode", + "moxie/core/utils/Basic", + ], function (extensions, Encode, Basic) { + function FileReader() { + var _fr, + _convertToBinary = false; + + Basic.extend(this, { + read: function (op, blob) { + var comp = this; + + comp.result = ""; + + _fr = new window.FileReader(); + + _fr.addEventListener("progress", function (e) { + comp.trigger(e); + }); + + _fr.addEventListener("load", function (e) { + comp.result = _convertToBinary + ? _toBinary(_fr.result) + : _fr.result; + comp.trigger(e); + }); + + _fr.addEventListener("error", function (e) { + comp.trigger(e, _fr.error); + }); + + _fr.addEventListener("loadend", function (e) { + _fr = null; + comp.trigger(e); + }); + + if (Basic.typeOf(_fr[op]) === "function") { + _convertToBinary = false; + _fr[op](blob.getSource()); + } else if (op === "readAsBinaryString") { + // readAsBinaryString is depricated in general and never existed in IE10+ + _convertToBinary = true; + _fr.readAsDataURL(blob.getSource()); + } + }, + + abort: function () { + if (_fr) { + _fr.abort(); + } + }, + + destroy: function () { + _fr = null; + }, + }); + + function _toBinary(str) { + return Encode.atob( + str.substring(str.indexOf("base64,") + 7) + ); + } + } + + return (extensions.FileReader = FileReader); + }); + + // Included from: src/javascript/runtime/html5/xhr/XMLHttpRequest.js + + /** + * XMLHttpRequest.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /*global ActiveXObject:true */ + + /** +@class moxie/runtime/html5/xhr/XMLHttpRequest +@private +*/ + define("moxie/runtime/html5/xhr/XMLHttpRequest", [ + "moxie/runtime/html5/Runtime", + "moxie/core/utils/Basic", + "moxie/core/utils/Mime", + "moxie/core/utils/Url", + "moxie/file/File", + "moxie/file/Blob", + "moxie/xhr/FormData", + "moxie/core/Exceptions", + "moxie/core/utils/Env", + ], function (extensions, Basic, Mime, Url, File, Blob, FormData, x, Env) { + function XMLHttpRequest() { + var self = this, + _xhr, + _filename; + + Basic.extend(this, { + send: function (meta, data) { + var target = this, + isGecko2_5_6 = + Env.browser === "Mozilla" && + Env.verComp(Env.version, 4, ">=") && + Env.verComp(Env.version, 7, "<"), + isAndroidBrowser = + Env.browser === "Android Browser", + mustSendAsBinary = false; + // extract file name + _filename = meta.url + .replace(/^.+?\/([\w\-\.]+)$/, "$1") + .toLowerCase(); + + _xhr = _getNativeXHR(); + _xhr.open( + meta.method, + meta.url, + meta.async, + meta.user, + meta.password + ); + + // prepare data to be sent + if (data instanceof Blob) { + if (data.isDetached()) { + mustSendAsBinary = true; + } + data = data.getSource(); + } else if (data instanceof FormData) { + if (data.hasBlob()) { + if (data.getBlob().isDetached()) { + data = _prepareMultipart.call(target, data); // _xhr must be instantiated and be in OPENED state + mustSendAsBinary = true; + } else if ( + (isGecko2_5_6 || isAndroidBrowser) && + Basic.typeOf(data.getBlob().getSource()) === + "blob" && + window.FileReader + ) { + // Gecko 2/5/6 can't send blob in FormData: https://bugzilla.mozilla.org/show_bug.cgi?id=649150 + // Android browsers (default one and Dolphin) seem to have the same issue, see: #613 + _preloadAndSend.call(target, meta, data); + return; // _preloadAndSend will reinvoke send() with transmutated FormData =%D + } + } + + // transfer fields to real FormData + if (data instanceof FormData) { + // if still a FormData, e.g. not mangled by _prepareMultipart() + var fd = new window.FormData(); + data.each(function (value, name) { + if (value instanceof Blob) { + fd.append(name, value.getSource()); + } else { + fd.append(name, value); + } + }); + data = fd; + } + } + + // if XHR L2 + if (_xhr.upload) { + if (meta.withCredentials) { + _xhr.withCredentials = true; + } + + _xhr.addEventListener("load", function (e) { + target.trigger(e); + }); + + _xhr.addEventListener("error", function (e) { + target.trigger(e); + }); + + // additionally listen to progress events + _xhr.addEventListener("progress", function (e) { + target.trigger(e); + }); + + _xhr.upload.addEventListener( + "progress", + function (e) { + target.trigger({ + type: "UploadProgress", + loaded: e.loaded, + total: e.total, + }); + } + ); + // ... otherwise simulate XHR L2 + } else { + _xhr.onreadystatechange = function onReadyStateChange() { + // fake Level 2 events + switch (_xhr.readyState) { + case 1: // XMLHttpRequest.OPENED + // readystatechanged is fired twice for OPENED state (in IE and Mozilla) - neu + break; + + // looks like HEADERS_RECEIVED (state 2) is not reported in Opera (or it's old versions) - neu + case 2: // XMLHttpRequest.HEADERS_RECEIVED + break; + + case 3: // XMLHttpRequest.LOADING + // try to fire progress event for not XHR L2 + var total, loaded; + + try { + if (Url.hasSameOrigin(meta.url)) { + // Content-Length not accessible for cross-domain on some browsers + total = + _xhr.getResponseHeader( + "Content-Length" + ) || 0; // old Safari throws an exception here + } + + if (_xhr.responseText) { + // responseText was introduced in IE7 + loaded = + _xhr.responseText.length; + } + } catch (ex) { + total = loaded = 0; + } + + target.trigger({ + type: "progress", + lengthComputable: !!total, + total: parseInt(total, 10), + loaded: loaded, + }); + break; + + case 4: // XMLHttpRequest.DONE + // release readystatechange handler (mostly for IE) + _xhr.onreadystatechange = function () {}; + + // usually status 0 is returned when server is unreachable, but FF also fails to status 0 for 408 timeout + if (_xhr.status === 0) { + target.trigger("error"); + } else { + target.trigger("load"); + } + break; + } + }; + } + + // set request headers + if (!Basic.isEmptyObj(meta.headers)) { + Basic.each(meta.headers, function (value, header) { + _xhr.setRequestHeader(header, value); + }); + } + + // request response type + if ( + "" !== meta.responseType && + "responseType" in _xhr + ) { + if ( + "json" === meta.responseType && + !Env.can("return_response_type", "json") + ) { + // we can fake this one + _xhr.responseType = "text"; + } else { + _xhr.responseType = meta.responseType; + } + } + + // send ... + if (!mustSendAsBinary) { + _xhr.send(data); + } else { + if (_xhr.sendAsBinary) { + // Gecko + _xhr.sendAsBinary(data); + } else { + // other browsers having support for typed arrays + (function () { + // mimic Gecko's sendAsBinary + var ui8a = new Uint8Array(data.length); + for (var i = 0; i < data.length; i++) { + ui8a[i] = data.charCodeAt(i) & 0xff; + } + _xhr.send(ui8a.buffer); + })(); + } + } + + target.trigger("loadstart"); + }, + + getStatus: function () { + // according to W3C spec it should return 0 for readyState < 3, but instead it throws an exception + try { + if (_xhr) { + return _xhr.status; + } + } catch (ex) {} + return 0; + }, + + getResponse: function (responseType) { + var I = this.getRuntime(); + + try { + switch (responseType) { + case "blob": + var file = new File(I.uid, _xhr.response); + + // try to extract file name from content-disposition if possible (might be - not, if CORS for example) + var disposition = _xhr.getResponseHeader( + "Content-Disposition" + ); + if (disposition) { + // extract filename from response header if available + var match = disposition.match( + /filename=([\'\"'])([^\1]+)\1/ + ); + if (match) { + _filename = match[2]; + } + } + file.name = _filename; + + // pre-webkit Opera doesn't set type property on the blob response + if (!file.type) { + file.type = Mime.getFileMime(_filename); + } + return file; + + case "json": + if ( + !Env.can("return_response_type", "json") + ) { + return _xhr.status === 200 && + !!window.JSON + ? JSON.parse(_xhr.responseText) + : null; + } + return _xhr.response; + + case "document": + return _getDocument(_xhr); + + default: + return _xhr.responseText !== "" + ? _xhr.responseText + : null; // against the specs, but for consistency across the runtimes + } + } catch (ex) { + return null; + } + }, + + getAllResponseHeaders: function () { + try { + return _xhr.getAllResponseHeaders(); + } catch (ex) {} + return ""; + }, + + abort: function () { + if (_xhr) { + _xhr.abort(); + } + }, + + destroy: function () { + self = _filename = null; + }, + }); + + // here we go... ugly fix for ugly bug + function _preloadAndSend(meta, data) { + var target = this, + blob, + fr; + + // get original blob + blob = data.getBlob().getSource(); + + // preload blob in memory to be sent as binary string + fr = new window.FileReader(); + fr.onload = function () { + // overwrite original blob + data.append( + data.getBlobName(), + new Blob(null, { + type: blob.type, + data: fr.result, + }) + ); + // invoke send operation again + self.send.call(target, meta, data); + }; + fr.readAsBinaryString(blob); + } + + function _getNativeXHR() { + if ( + window.XMLHttpRequest && + !( + Env.browser === "IE" && + Env.verComp(Env.version, 8, "<") + ) + ) { + // IE7 has native XHR but it's buggy + return new window.XMLHttpRequest(); + } else { + return (function () { + var progIDs = [ + "Msxml2.XMLHTTP.6.0", + "Microsoft.XMLHTTP", + ]; // if 6.0 available, use it, otherwise failback to default 3.0 + for (var i = 0; i < progIDs.length; i++) { + try { + return new ActiveXObject(progIDs[i]); + } catch (ex) {} + } + })(); + } + } + + // @credits Sergey Ilinsky (http://www.ilinsky.com/) + function _getDocument(xhr) { + var rXML = xhr.responseXML; + var rText = xhr.responseText; + + // Try parsing responseText (@see: http://www.ilinsky.com/articles/XMLHttpRequest/#bugs-ie-responseXML-content-type) + if ( + Env.browser === "IE" && + rText && + rXML && + !rXML.documentElement && + /[^\/]+\/[^\+]+\+xml/.test( + xhr.getResponseHeader("Content-Type") + ) + ) { + rXML = new window.ActiveXObject("Microsoft.XMLDOM"); + rXML.async = false; + rXML.validateOnParse = false; + rXML.loadXML(rText); + } + + // Check if there is no error in document + if (rXML) { + if ( + (Env.browser === "IE" && rXML.parseError !== 0) || + !rXML.documentElement || + rXML.documentElement.tagName === "parsererror" + ) { + return null; + } + } + return rXML; + } + + function _prepareMultipart(fd) { + var boundary = "----moxieboundary" + new Date().getTime(), + dashdash = "--", + crlf = "\r\n", + multipart = "", + I = this.getRuntime(); + if (!I.can("send_binary_string")) { + throw new x.RuntimeError( + x.RuntimeError.NOT_SUPPORTED_ERR + ); + } + + _xhr.setRequestHeader( + "Content-Type", + "multipart/form-data; boundary=" + boundary + ); + + // append multipart parameters + fd.each(function (value, name) { + // Firefox 3.6 failed to convert multibyte characters to UTF-8 in sendAsBinary(), + // so we try it here ourselves with: unescape(encodeURIComponent(value)) + if (value instanceof Blob) { + // Build RFC2388 blob + multipart += + dashdash + + boundary + + crlf + + 'Content-Disposition: form-data; name="' + + name + + '"; filename="' + + unescape( + encodeURIComponent(value.name || "blob") + ) + + '"' + + crlf + + "Content-Type: " + + (value.type || "application/octet-stream") + + crlf + + crlf + + value.getSource() + + crlf; + } else { + multipart += + dashdash + + boundary + + crlf + + 'Content-Disposition: form-data; name="' + + name + + '"' + + crlf + + crlf + + unescape(encodeURIComponent(value)) + + crlf; + } + }); + + multipart += dashdash + boundary + dashdash + crlf; + + return multipart; + } + } + + return (extensions.XMLHttpRequest = XMLHttpRequest); + }); + + // Included from: src/javascript/runtime/html5/utils/BinaryReader.js + + /** + * BinaryReader.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/html5/utils/BinaryReader +@private +*/ + define("moxie/runtime/html5/utils/BinaryReader", [ + "moxie/core/utils/Basic", + ], function (Basic) { + function BinaryReader(data) { + if (data instanceof ArrayBuffer) { + ArrayBufferReader.apply(this, arguments); + } else { + UTF16StringReader.apply(this, arguments); + } + } + + Basic.extend(BinaryReader.prototype, { + littleEndian: false, + + read: function (idx, size) { + var sum, mv, i; + + if (idx + size > this.length()) { + throw new Error( + "You are trying to read outside the source boundaries." + ); + } + + mv = this.littleEndian ? 0 : -8 * (size - 1); + + for (i = 0, sum = 0; i < size; i++) { + sum |= this.readByteAt(idx + i) << Math.abs(mv + i * 8); + } + return sum; + }, + + write: function (idx, num, size) { + var mv, + i, + str = ""; + + if (idx > this.length()) { + throw new Error( + "You are trying to write outside the source boundaries." + ); + } + + mv = this.littleEndian ? 0 : -8 * (size - 1); + + for (i = 0; i < size; i++) { + this.writeByteAt( + idx + i, + (num >> Math.abs(mv + i * 8)) & 255 + ); + } + }, + + BYTE: function (idx) { + return this.read(idx, 1); + }, + + SHORT: function (idx) { + return this.read(idx, 2); + }, + + LONG: function (idx) { + return this.read(idx, 4); + }, + + SLONG: function (idx) { + // 2's complement notation + var num = this.read(idx, 4); + return num > 2147483647 ? num - 4294967296 : num; + }, + + CHAR: function (idx) { + return String.fromCharCode(this.read(idx, 1)); + }, + + STRING: function (idx, count) { + return this.asArray("CHAR", idx, count).join(""); + }, + + asArray: function (type, idx, count) { + var values = []; + + for (var i = 0; i < count; i++) { + values[i] = this[type](idx + i); + } + return values; + }, + }); + + function ArrayBufferReader(data) { + var _dv = new DataView(data); + + Basic.extend(this, { + readByteAt: function (idx) { + return _dv.getUint8(idx); + }, + + writeByteAt: function (idx, value) { + _dv.setUint8(idx, value); + }, + + SEGMENT: function (idx, size, value) { + switch (arguments.length) { + case 2: + return data.slice(idx, idx + size); + + case 1: + return data.slice(idx); + + case 3: + if (value === null) { + value = new ArrayBuffer(); + } + + if (value instanceof ArrayBuffer) { + var arr = new Uint8Array( + this.length() - size + value.byteLength + ); + if (idx > 0) { + arr.set( + new Uint8Array(data.slice(0, idx)), + 0 + ); + } + arr.set(new Uint8Array(value), idx); + arr.set( + new Uint8Array(data.slice(idx + size)), + idx + value.byteLength + ); + + this.clear(); + data = arr.buffer; + _dv = new DataView(data); + break; + } + + default: + return data; + } + }, + + length: function () { + return data ? data.byteLength : 0; + }, + + clear: function () { + _dv = data = null; + }, + }); + } + + function UTF16StringReader(data) { + Basic.extend(this, { + readByteAt: function (idx) { + return data.charCodeAt(idx); + }, + + writeByteAt: function (idx, value) { + putstr(String.fromCharCode(value), idx, 1); + }, + + SEGMENT: function (idx, length, segment) { + switch (arguments.length) { + case 1: + return data.substr(idx); + case 2: + return data.substr(idx, length); + case 3: + putstr( + segment !== null ? segment : "", + idx, + length + ); + break; + default: + return data; + } + }, + + length: function () { + return data ? data.length : 0; + }, + + clear: function () { + data = null; + }, + }); + + function putstr(segment, idx, length) { + length = + arguments.length === 3 ? length : data.length - idx - 1; + data = + data.substr(0, idx) + + segment + + data.substr(length + idx); + } + } + + return BinaryReader; + }); + + // Included from: src/javascript/runtime/html5/image/JPEGHeaders.js + + /** + * JPEGHeaders.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/html5/image/JPEGHeaders +@private +*/ + define("moxie/runtime/html5/image/JPEGHeaders", [ + "moxie/runtime/html5/utils/BinaryReader", + "moxie/core/Exceptions", + ], function (BinaryReader, x) { + return function JPEGHeaders(data) { + var headers = [], + _br, + idx, + marker, + length = 0; + + _br = new BinaryReader(data); + + // Check if data is jpeg + if (_br.SHORT(0) !== 0xffd8) { + _br.clear(); + throw new x.ImageError(x.ImageError.WRONG_FORMAT); + } + + idx = 2; + + while (idx <= _br.length()) { + marker = _br.SHORT(idx); + + // omit RST (restart) markers + if (marker >= 0xffd0 && marker <= 0xffd7) { + idx += 2; + continue; + } + + // no headers allowed after SOS marker + if (marker === 0xffda || marker === 0xffd9) { + break; + } + + length = _br.SHORT(idx + 2) + 2; + + // APPn marker detected + if (marker >= 0xffe1 && marker <= 0xffef) { + headers.push({ + hex: marker, + name: "APP" + (marker & 0x000f), + start: idx, + length: length, + segment: _br.SEGMENT(idx, length), + }); + } + + idx += length; + } + + _br.clear(); + + return { + headers: headers, + + restore: function (data) { + var max, i, br; + + br = new BinaryReader(data); + + idx = br.SHORT(2) == 0xffe0 ? 4 + br.SHORT(4) : 2; + + for (i = 0, max = headers.length; i < max; i++) { + br.SEGMENT(idx, 0, headers[i].segment); + idx += headers[i].length; + } + + data = br.SEGMENT(); + br.clear(); + return data; + }, + + strip: function (data) { + var br, headers, jpegHeaders, i; + + jpegHeaders = new JPEGHeaders(data); + headers = jpegHeaders.headers; + jpegHeaders.purge(); + + br = new BinaryReader(data); + + i = headers.length; + while (i--) { + br.SEGMENT(headers[i].start, headers[i].length, ""); + } + + data = br.SEGMENT(); + br.clear(); + return data; + }, + + get: function (name) { + var array = []; + + for (var i = 0, max = headers.length; i < max; i++) { + if (headers[i].name === name.toUpperCase()) { + array.push(headers[i].segment); + } + } + return array; + }, + + set: function (name, segment) { + var array = [], + i, + ii, + max; + + if (typeof segment === "string") { + array.push(segment); + } else { + array = segment; + } + + for (i = ii = 0, max = headers.length; i < max; i++) { + if (headers[i].name === name.toUpperCase()) { + headers[i].segment = array[ii]; + headers[i].length = array[ii].length; + ii++; + } + if (ii >= array.length) { + break; + } + } + }, + + purge: function () { + this.headers = headers = []; + }, + }; + }; + }); + + // Included from: src/javascript/runtime/html5/image/ExifParser.js + + /** + * ExifParser.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/html5/image/ExifParser +@private +*/ + define("moxie/runtime/html5/image/ExifParser", [ + "moxie/core/utils/Basic", + "moxie/runtime/html5/utils/BinaryReader", + "moxie/core/Exceptions", + ], function (Basic, BinaryReader, x) { + function ExifParser(data) { + var __super__, tags, tagDescs, offsets, idx, Tiff; + + BinaryReader.call(this, data); + + tags = { + tiff: { + /* + The image orientation viewed in terms of rows and columns. + + 1 = The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side. + 2 = The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side. + 3 = The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side. + 4 = The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side. + 5 = The 0th row is the visual left-hand side of the image, and the 0th column is the visual top. + 6 = The 0th row is the visual right-hand side of the image, and the 0th column is the visual top. + 7 = The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom. + 8 = The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom. + */ + 0x0112: "Orientation", + 0x010e: "ImageDescription", + 0x010f: "Make", + 0x0110: "Model", + 0x0131: "Software", + 0x8769: "ExifIFDPointer", + 0x8825: "GPSInfoIFDPointer", + }, + exif: { + 0x9000: "ExifVersion", + 0xa001: "ColorSpace", + 0xa002: "PixelXDimension", + 0xa003: "PixelYDimension", + 0x9003: "DateTimeOriginal", + 0x829a: "ExposureTime", + 0x829d: "FNumber", + 0x8827: "ISOSpeedRatings", + 0x9201: "ShutterSpeedValue", + 0x9202: "ApertureValue", + 0x9207: "MeteringMode", + 0x9208: "LightSource", + 0x9209: "Flash", + 0x920a: "FocalLength", + 0xa402: "ExposureMode", + 0xa403: "WhiteBalance", + 0xa406: "SceneCaptureType", + 0xa404: "DigitalZoomRatio", + 0xa408: "Contrast", + 0xa409: "Saturation", + 0xa40a: "Sharpness", + }, + gps: { + 0x0000: "GPSVersionID", + 0x0001: "GPSLatitudeRef", + 0x0002: "GPSLatitude", + 0x0003: "GPSLongitudeRef", + 0x0004: "GPSLongitude", + }, + + thumb: { + 0x0201: "JPEGInterchangeFormat", + 0x0202: "JPEGInterchangeFormatLength", + }, + }; + + tagDescs = { + ColorSpace: { + 1: "sRGB", + 0: "Uncalibrated", + }, + + MeteringMode: { + 0: "Unknown", + 1: "Average", + 2: "CenterWeightedAverage", + 3: "Spot", + 4: "MultiSpot", + 5: "Pattern", + 6: "Partial", + 255: "Other", + }, + + LightSource: { + 1: "Daylight", + 2: "Fliorescent", + 3: "Tungsten", + 4: "Flash", + 9: "Fine weather", + 10: "Cloudy weather", + 11: "Shade", + 12: "Daylight fluorescent (D 5700 - 7100K)", + 13: "Day white fluorescent (N 4600 -5400K)", + 14: "Cool white fluorescent (W 3900 - 4500K)", + 15: "White fluorescent (WW 3200 - 3700K)", + 17: "Standard light A", + 18: "Standard light B", + 19: "Standard light C", + 20: "D55", + 21: "D65", + 22: "D75", + 23: "D50", + 24: "ISO studio tungsten", + 255: "Other", + }, + + Flash: { + 0x0000: "Flash did not fire", + 0x0001: "Flash fired", + 0x0005: "Strobe return light not detected", + 0x0007: "Strobe return light detected", + 0x0009: "Flash fired, compulsory flash mode", + 0x000d: "Flash fired, compulsory flash mode, return light not detected", + 0x000f: "Flash fired, compulsory flash mode, return light detected", + 0x0010: "Flash did not fire, compulsory flash mode", + 0x0018: "Flash did not fire, auto mode", + 0x0019: "Flash fired, auto mode", + 0x001d: "Flash fired, auto mode, return light not detected", + 0x001f: "Flash fired, auto mode, return light detected", + 0x0020: "No flash function", + 0x0041: "Flash fired, red-eye reduction mode", + 0x0045: "Flash fired, red-eye reduction mode, return light not detected", + 0x0047: "Flash fired, red-eye reduction mode, return light detected", + 0x0049: "Flash fired, compulsory flash mode, red-eye reduction mode", + 0x004d: "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected", + 0x004f: "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected", + 0x0059: "Flash fired, auto mode, red-eye reduction mode", + 0x005d: "Flash fired, auto mode, return light not detected, red-eye reduction mode", + 0x005f: "Flash fired, auto mode, return light detected, red-eye reduction mode", + }, + + ExposureMode: { + 0: "Auto exposure", + 1: "Manual exposure", + 2: "Auto bracket", + }, + + WhiteBalance: { + 0: "Auto white balance", + 1: "Manual white balance", + }, + + SceneCaptureType: { + 0: "Standard", + 1: "Landscape", + 2: "Portrait", + 3: "Night scene", + }, + + Contrast: { + 0: "Normal", + 1: "Soft", + 2: "Hard", + }, + + Saturation: { + 0: "Normal", + 1: "Low saturation", + 2: "High saturation", + }, + + Sharpness: { + 0: "Normal", + 1: "Soft", + 2: "Hard", + }, + + // GPS related + GPSLatitudeRef: { + N: "North latitude", + S: "South latitude", + }, + + GPSLongitudeRef: { + E: "East longitude", + W: "West longitude", + }, + }; + + offsets = { + tiffHeader: 10, + }; + + idx = offsets.tiffHeader; + + __super__ = { + clear: this.clear, + }; + + // Public functions + Basic.extend(this, { + read: function () { + try { + return ExifParser.prototype.read.apply( + this, + arguments + ); + } catch (ex) { + throw new x.ImageError( + x.ImageError.INVALID_META_ERR + ); + } + }, + + write: function () { + try { + return ExifParser.prototype.write.apply( + this, + arguments + ); + } catch (ex) { + throw new x.ImageError( + x.ImageError.INVALID_META_ERR + ); + } + }, + + UNDEFINED: function () { + return this.BYTE.apply(this, arguments); + }, + + RATIONAL: function (idx) { + return this.LONG(idx) / this.LONG(idx + 4); + }, + + SRATIONAL: function (idx) { + return this.SLONG(idx) / this.SLONG(idx + 4); + }, + + ASCII: function (idx) { + return this.CHAR(idx); + }, + + TIFF: function () { + return Tiff || null; + }, + + EXIF: function () { + var Exif = null; + + if (offsets.exifIFD) { + try { + Exif = extractTags.call( + this, + offsets.exifIFD, + tags.exif + ); + } catch (ex) { + return null; + } + + // Fix formatting of some tags + if ( + Exif.ExifVersion && + Basic.typeOf(Exif.ExifVersion) === "array" + ) { + for ( + var i = 0, exifVersion = ""; + i < Exif.ExifVersion.length; + i++ + ) { + exifVersion += String.fromCharCode( + Exif.ExifVersion[i] + ); + } + Exif.ExifVersion = exifVersion; + } + } + + return Exif; + }, + + GPS: function () { + var GPS = null; + + if (offsets.gpsIFD) { + try { + GPS = extractTags.call( + this, + offsets.gpsIFD, + tags.gps + ); + } catch (ex) { + return null; + } + + // iOS devices (and probably some others) do not put in GPSVersionID tag (why?..) + if ( + GPS.GPSVersionID && + Basic.typeOf(GPS.GPSVersionID) === "array" + ) { + GPS.GPSVersionID = GPS.GPSVersionID.join("."); + } + } + + return GPS; + }, + + thumb: function () { + if (offsets.IFD1) { + try { + var IFD1Tags = extractTags.call( + this, + offsets.IFD1, + tags.thumb + ); + + if ("JPEGInterchangeFormat" in IFD1Tags) { + return this.SEGMENT( + offsets.tiffHeader + + IFD1Tags.JPEGInterchangeFormat, + IFD1Tags.JPEGInterchangeFormatLength + ); + } + } catch (ex) {} + } + return null; + }, + + setExif: function (tag, value) { + // Right now only setting of width/height is possible + if ( + tag !== "PixelXDimension" && + tag !== "PixelYDimension" + ) { + return false; + } + + return setTag.call(this, "exif", tag, value); + }, + + clear: function () { + __super__.clear(); + data = tags = tagDescs = Tiff = offsets = __super__ = null; + }, + }); + + // Check if that's APP1 and that it has EXIF + if ( + this.SHORT(0) !== 0xffe1 || + this.STRING(4, 5).toUpperCase() !== "EXIF\0" + ) { + throw new x.ImageError(x.ImageError.INVALID_META_ERR); + } + + // Set read order of multi-byte data + this.littleEndian = this.SHORT(idx) == 0x4949; + + // Check if always present bytes are indeed present + if (this.SHORT((idx += 2)) !== 0x002a) { + throw new x.ImageError(x.ImageError.INVALID_META_ERR); + } + + offsets.IFD0 = offsets.tiffHeader + this.LONG((idx += 2)); + Tiff = extractTags.call(this, offsets.IFD0, tags.tiff); + + if ("ExifIFDPointer" in Tiff) { + offsets.exifIFD = offsets.tiffHeader + Tiff.ExifIFDPointer; + delete Tiff.ExifIFDPointer; + } + + if ("GPSInfoIFDPointer" in Tiff) { + offsets.gpsIFD = + offsets.tiffHeader + Tiff.GPSInfoIFDPointer; + delete Tiff.GPSInfoIFDPointer; + } + + if (Basic.isEmptyObj(Tiff)) { + Tiff = null; + } + + // check if we have a thumb as well + var IFD1Offset = this.LONG( + offsets.IFD0 + this.SHORT(offsets.IFD0) * 12 + 2 + ); + if (IFD1Offset) { + offsets.IFD1 = offsets.tiffHeader + IFD1Offset; + } + + function extractTags(IFD_offset, tags2extract) { + var data = this; + var length, + i, + tag, + type, + count, + size, + offset, + value, + values = [], + hash = {}; + + var types = { + 1: "BYTE", + 7: "UNDEFINED", + 2: "ASCII", + 3: "SHORT", + 4: "LONG", + 5: "RATIONAL", + 9: "SLONG", + 10: "SRATIONAL", + }; + + var sizes = { + BYTE: 1, + UNDEFINED: 1, + ASCII: 1, + SHORT: 2, + LONG: 4, + RATIONAL: 8, + SLONG: 4, + SRATIONAL: 8, + }; + + length = data.SHORT(IFD_offset); + + // The size of APP1 including all these elements shall not exceed the 64 Kbytes specified in the JPEG standard. + + for (i = 0; i < length; i++) { + values = []; + + // Set binary reader pointer to beginning of the next tag + offset = IFD_offset + 2 + i * 12; + + tag = tags2extract[data.SHORT(offset)]; + + if (tag === undefined) { + continue; // Not the tag we requested + } + + type = types[data.SHORT((offset += 2))]; + count = data.LONG((offset += 2)); + size = sizes[type]; + + if (!size) { + throw new x.ImageError( + x.ImageError.INVALID_META_ERR + ); + } + + offset += 4; + + // tag can only fit 4 bytes of data, if data is larger we should look outside + if (size * count > 4) { + // instead of data tag contains an offset of the data + offset = data.LONG(offset) + offsets.tiffHeader; + } + + // in case we left the boundaries of data throw an early exception + if (offset + size * count >= this.length()) { + throw new x.ImageError( + x.ImageError.INVALID_META_ERR + ); + } + + // special care for the string + if (type === "ASCII") { + hash[tag] = Basic.trim( + data.STRING(offset, count).replace(/\0$/, "") + ); // strip trailing NULL + continue; + } else { + values = data.asArray(type, offset, count); + value = count == 1 ? values[0] : values; + + if ( + tagDescs.hasOwnProperty(tag) && + typeof value != "object" + ) { + hash[tag] = tagDescs[tag][value]; + } else { + hash[tag] = value; + } + } + } + + return hash; + } + + // At the moment only setting of simple (LONG) values, that do not require offset recalculation, is supported + function setTag(ifd, tag, value) { + var offset, + length, + tagOffset, + valueOffset = 0; + + // If tag name passed translate into hex key + if (typeof tag === "string") { + var tmpTags = tags[ifd.toLowerCase()]; + for (var hex in tmpTags) { + if (tmpTags[hex] === tag) { + tag = hex; + break; + } + } + } + offset = offsets[ifd.toLowerCase() + "IFD"]; + length = this.SHORT(offset); + + for (var i = 0; i < length; i++) { + tagOffset = offset + 12 * i + 2; + + if (this.SHORT(tagOffset) == tag) { + valueOffset = tagOffset + 8; + break; + } + } + + if (!valueOffset) { + return false; + } + + try { + this.write(valueOffset, value, 4); + } catch (ex) { + return false; + } + + return true; + } + } + + ExifParser.prototype = BinaryReader.prototype; + + return ExifParser; + }); + + // Included from: src/javascript/runtime/html5/image/JPEG.js + + /** + * JPEG.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/html5/image/JPEG +@private +*/ + define("moxie/runtime/html5/image/JPEG", [ + "moxie/core/utils/Basic", + "moxie/core/Exceptions", + "moxie/runtime/html5/image/JPEGHeaders", + "moxie/runtime/html5/utils/BinaryReader", + "moxie/runtime/html5/image/ExifParser", + ], function (Basic, x, JPEGHeaders, BinaryReader, ExifParser) { + function JPEG(data) { + var _br, _hm, _ep, _info; + + _br = new BinaryReader(data); + + // check if it is jpeg + if (_br.SHORT(0) !== 0xffd8) { + throw new x.ImageError(x.ImageError.WRONG_FORMAT); + } + + // backup headers + _hm = new JPEGHeaders(data); + + // extract exif info + try { + _ep = new ExifParser(_hm.get("app1")[0]); + } catch (ex) {} + + // get dimensions + _info = _getDimensions.call(this); + + Basic.extend(this, { + type: "image/jpeg", + + size: _br.length(), + + width: (_info && _info.width) || 0, + + height: (_info && _info.height) || 0, + + setExif: function (tag, value) { + if (!_ep) { + return false; // or throw an exception + } + + if (Basic.typeOf(tag) === "object") { + Basic.each(tag, function (value, tag) { + _ep.setExif(tag, value); + }); + } else { + _ep.setExif(tag, value); + } + + // update internal headers + _hm.set("app1", _ep.SEGMENT()); + }, + + writeHeaders: function () { + if (!arguments.length) { + // if no arguments passed, update headers internally + return _hm.restore(data); + } + return _hm.restore(arguments[0]); + }, + + stripHeaders: function (data) { + return _hm.strip(data); + }, + + purge: function () { + _purge.call(this); + }, + }); + + if (_ep) { + this.meta = { + tiff: _ep.TIFF(), + exif: _ep.EXIF(), + gps: _ep.GPS(), + thumb: _getThumb(), + }; + } + + function _getDimensions(br) { + var idx = 0, + marker, + length; + + if (!br) { + br = _br; + } + + // examine all through the end, since some images might have very large APP segments + while (idx <= br.length()) { + marker = br.SHORT((idx += 2)); + + if (marker >= 0xffc0 && marker <= 0xffc3) { + // SOFn + idx += 5; // marker (2 bytes) + length (2 bytes) + Sample precision (1 byte) + return { + height: br.SHORT(idx), + width: br.SHORT((idx += 2)), + }; + } + length = br.SHORT((idx += 2)); + idx += length - 2; + } + return null; + } + + function _getThumb() { + var data = _ep.thumb(), + br, + info; + + if (data) { + br = new BinaryReader(data); + info = _getDimensions(br); + br.clear(); + + if (info) { + info.data = data; + return info; + } + } + return null; + } + + function _purge() { + if (!_ep || !_hm || !_br) { + return; // ignore any repeating purge requests + } + _ep.clear(); + _hm.purge(); + _br.clear(); + _info = _hm = _ep = _br = null; + } + } + + return JPEG; + }); + + // Included from: src/javascript/runtime/html5/image/PNG.js + + /** + * PNG.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/html5/image/PNG +@private +*/ + define("moxie/runtime/html5/image/PNG", [ + "moxie/core/Exceptions", + "moxie/core/utils/Basic", + "moxie/runtime/html5/utils/BinaryReader", + ], function (x, Basic, BinaryReader) { + function PNG(data) { + var _br, _hm, _ep, _info; + + _br = new BinaryReader(data); + + // check if it's png + (function () { + var idx = 0, + i = 0, + signature = [0x8950, 0x4e47, 0x0d0a, 0x1a0a]; + for (i = 0; i < signature.length; i++, idx += 2) { + if (signature[i] != _br.SHORT(idx)) { + throw new x.ImageError(x.ImageError.WRONG_FORMAT); + } + } + })(); + + function _getDimensions() { + var chunk, idx; + + chunk = _getChunkAt.call(this, 8); + + if (chunk.type == "IHDR") { + idx = chunk.start; + return { + width: _br.LONG(idx), + height: _br.LONG((idx += 4)), + }; + } + return null; + } + + function _purge() { + if (!_br) { + return; // ignore any repeating purge requests + } + _br.clear(); + data = _info = _hm = _ep = _br = null; + } + + _info = _getDimensions.call(this); + + Basic.extend(this, { + type: "image/png", + + size: _br.length(), + + width: _info.width, + + height: _info.height, + + purge: function () { + _purge.call(this); + }, + }); + + // for PNG we can safely trigger purge automatically, as we do not keep any data for later + _purge.call(this); + + function _getChunkAt(idx) { + var length, type, start, CRC; + + length = _br.LONG(idx); + type = _br.STRING((idx += 4), 4); + start = idx += 4; + CRC = _br.LONG(idx + length); + + return { + length: length, + type: type, + start: start, + CRC: CRC, + }; + } + } + + return PNG; + }); + + // Included from: src/javascript/runtime/html5/image/ImageInfo.js + + /** + * ImageInfo.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/html5/image/ImageInfo +@private +*/ + define("moxie/runtime/html5/image/ImageInfo", [ + "moxie/core/utils/Basic", + "moxie/core/Exceptions", + "moxie/runtime/html5/image/JPEG", + "moxie/runtime/html5/image/PNG", + ], function (Basic, x, JPEG, PNG) { + /** + Optional image investigation tool for HTML5 runtime. Provides the following features: + - ability to distinguish image type (JPEG or PNG) by signature + - ability to extract image width/height directly from it's internals, without preloading in memory (fast) + - ability to extract APP headers from JPEGs (Exif, GPS, etc) + - ability to replace width/height tags in extracted JPEG headers + - ability to restore APP headers, that were for example stripped during image manipulation + + @class ImageInfo + @constructor + @param {String} data Image source as binary string + */ + return function (data) { + var _cs = [JPEG, PNG], + _img; + + // figure out the format, throw: ImageError.WRONG_FORMAT if not supported + _img = (function () { + for (var i = 0; i < _cs.length; i++) { + try { + return new _cs[i](data); + } catch (ex) { + // console.info(ex); + } + } + throw new x.ImageError(x.ImageError.WRONG_FORMAT); + })(); + + Basic.extend(this, { + /** + Image Mime Type extracted from it's depths + + @property type + @type {String} + @default '' + */ + type: "", + + /** + Image size in bytes + + @property size + @type {Number} + @default 0 + */ + size: 0, + + /** + Image width extracted from image source + + @property width + @type {Number} + @default 0 + */ + width: 0, + + /** + Image height extracted from image source + + @property height + @type {Number} + @default 0 + */ + height: 0, + + /** + Sets Exif tag. Currently applicable only for width and height tags. Obviously works only with JPEGs. + + @method setExif + @param {String} tag Tag to set + @param {Mixed} value Value to assign to the tag + */ + setExif: function () {}, + + /** + Restores headers to the source. + + @method writeHeaders + @param {String} data Image source as binary string + @return {String} Updated binary string + */ + writeHeaders: function (data) { + return data; + }, + + /** + Strip all headers from the source. + + @method stripHeaders + @param {String} data Image source as binary string + @return {String} Updated binary string + */ + stripHeaders: function (data) { + return data; + }, + + /** + Dispose resources. + + @method purge + */ + purge: function () { + data = null; + }, + }); + + Basic.extend(this, _img); + + this.purge = function () { + _img.purge(); + _img = null; + }; + }; + }); + + // Included from: src/javascript/runtime/html5/image/ResizerCanvas.js + + /** + * ResizerCanvas.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** + * Resizes image/canvas using canvas + */ + define("moxie/runtime/html5/image/ResizerCanvas", [], function () { + function scale(image, ratio) { + var sW = image.width; + var dW = Math.floor(sW * ratio); + var scaleCapped = false; + + if (ratio < 0.5 || ratio > 2) { + ratio = ratio < 0.5 ? 0.5 : 2; + scaleCapped = true; + } + + var tCanvas = _scale(image, ratio); + + if (scaleCapped) { + return scale(tCanvas, dW / tCanvas.width); + } else { + return tCanvas; + } + } + + function _scale(image, ratio) { + var sW = image.width; + var sH = image.height; + var dW = Math.floor(sW * ratio); + var dH = Math.floor(sH * ratio); + + var canvas = document.createElement("canvas"); + canvas.width = dW; + canvas.height = dH; + canvas + .getContext("2d") + .drawImage(image, 0, 0, sW, sH, 0, 0, dW, dH); + + image = null; // just in case + return canvas; + } + + return { + scale: scale, + }; + }); + + // Included from: src/javascript/runtime/html5/image/Image.js + + /** + * Image.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/html5/image/Image +@private +*/ + define("moxie/runtime/html5/image/Image", [ + "moxie/runtime/html5/Runtime", + "moxie/core/utils/Basic", + "moxie/core/Exceptions", + "moxie/core/utils/Encode", + "moxie/file/Blob", + "moxie/file/File", + "moxie/runtime/html5/image/ImageInfo", + "moxie/runtime/html5/image/ResizerCanvas", + "moxie/core/utils/Mime", + "moxie/core/utils/Env", + ], function (extensions, Basic, x, Encode, Blob, File, ImageInfo, ResizerCanvas, Mime, Env) { + function HTML5Image() { + var me = this, + _img, + _imgInfo, + _canvas, + _binStr, + _blob, + _modified = false, // is set true whenever image is modified + _preserveHeaders = true; + Basic.extend(this, { + loadFromBlob: function (blob) { + var I = this.getRuntime(), + asBinary = + arguments.length > 1 ? arguments[1] : true; + if (!I.can("access_binary")) { + throw new x.RuntimeError( + x.RuntimeError.NOT_SUPPORTED_ERR + ); + } + + _blob = blob; + + if (blob.isDetached()) { + _binStr = blob.getSource(); + _preload.call(this, _binStr); + return; + } else { + _readAsDataUrl.call( + this, + blob.getSource(), + function (dataUrl) { + if (asBinary) { + _binStr = _toBinary(dataUrl); + } + _preload.call(this, dataUrl); + } + ); + } + }, + + loadFromImage: function (img, exact) { + this.meta = img.meta; + + _blob = new File(null, { + name: img.name, + size: img.size, + type: img.type, + }); + + _preload.call( + this, + exact + ? (_binStr = img.getAsBinaryString()) + : img.getAsDataURL() + ); + }, + + getInfo: function () { + var I = this.getRuntime(), + info; + + if ( + !_imgInfo && + _binStr && + I.can("access_image_binary") + ) { + _imgInfo = new ImageInfo(_binStr); + } + + // this stuff below is definitely having fun with itself + info = { + width: _getImg().width || 0, + height: _getImg().height || 0, + type: _blob.type || Mime.getFileMime(_blob.name), + size: + (_binStr && _binStr.length) || _blob.size || 0, + name: _blob.name || "", + meta: null, + }; + + if (_preserveHeaders) { + info.meta = + (_imgInfo && _imgInfo.meta) || this.meta || {}; + + // if data was taken from ImageInfo it will be a binary string, so we convert it to blob + if ( + info.meta && + info.meta.thumb && + !(info.meta.thumb.data instanceof Blob) + ) { + info.meta.thumb.data = new Blob(null, { + type: "image/jpeg", + data: info.meta.thumb.data, + }); + } + } + + return info; + }, + + resize: function (rect, ratio, options) { + var canvas = document.createElement("canvas"); + canvas.width = rect.width; + canvas.height = rect.height; + + canvas + .getContext("2d") + .drawImage( + _getImg(), + rect.x, + rect.y, + rect.width, + rect.height, + 0, + 0, + canvas.width, + canvas.height + ); + + _canvas = ResizerCanvas.scale(canvas, ratio); + + _preserveHeaders = options.preserveHeaders; + + // rotate if required, according to orientation tag + if (!_preserveHeaders) { + var orientation = + (this.meta && + this.meta.tiff && + this.meta.tiff.Orientation) || + 1; + _canvas = _rotateToOrientaion(_canvas, orientation); + } + + this.width = _canvas.width; + this.height = _canvas.height; + + _modified = true; + + this.trigger("Resize"); + }, + + getAsCanvas: function () { + if (!_canvas) { + _canvas = _getCanvas(); + } + _canvas.id = this.uid + "_canvas"; + return _canvas; + }, + + getAsBlob: function (type, quality) { + if (type !== this.type) { + _modified = true; // reconsider the state + return new File(null, { + name: _blob.name || "", + type: type, + data: me.getAsDataURL(type, quality), + }); + } + return new File(null, { + name: _blob.name || "", + type: type, + data: me.getAsBinaryString(type, quality), + }); + }, + + getAsDataURL: function (type) { + var quality = arguments[1] || 90; + + // if image has not been modified, return the source right away + if (!_modified) { + return _img.src; + } + + // make sure we have a canvas to work with + _getCanvas(); + + if ("image/jpeg" !== type) { + return _canvas.toDataURL("image/png"); + } else { + try { + // older Geckos used to result in an exception on quality argument + return _canvas.toDataURL( + "image/jpeg", + quality / 100 + ); + } catch (ex) { + return _canvas.toDataURL("image/jpeg"); + } + } + }, + + getAsBinaryString: function (type, quality) { + // if image has not been modified, return the source right away + if (!_modified) { + // if image was not loaded from binary string + if (!_binStr) { + _binStr = _toBinary( + me.getAsDataURL(type, quality) + ); + } + return _binStr; + } + + if ("image/jpeg" !== type) { + _binStr = _toBinary(me.getAsDataURL(type, quality)); + } else { + var dataUrl; + + // if jpeg + if (!quality) { + quality = 90; + } + + // make sure we have a canvas to work with + _getCanvas(); + + try { + // older Geckos used to result in an exception on quality argument + dataUrl = _canvas.toDataURL( + "image/jpeg", + quality / 100 + ); + } catch (ex) { + dataUrl = _canvas.toDataURL("image/jpeg"); + } + + _binStr = _toBinary(dataUrl); + + if (_imgInfo) { + _binStr = _imgInfo.stripHeaders(_binStr); + + if (_preserveHeaders) { + // update dimensions info in exif + if (_imgInfo.meta && _imgInfo.meta.exif) { + _imgInfo.setExif({ + PixelXDimension: this.width, + PixelYDimension: this.height, + }); + } + + // re-inject the headers + _binStr = _imgInfo.writeHeaders(_binStr); + } + + // will be re-created from fresh on next getInfo call + _imgInfo.purge(); + _imgInfo = null; + } + } + + _modified = false; + + return _binStr; + }, + + destroy: function () { + me = null; + _purge.call(this); + this.getRuntime().getShim().removeInstance(this.uid); + }, + }); + + function _getImg() { + if (!_canvas && !_img) { + throw new x.ImageError( + x.DOMException.INVALID_STATE_ERR + ); + } + return _canvas || _img; + } + + function _getCanvas() { + var canvas = _getImg(); + if (canvas.nodeName.toLowerCase() == "canvas") { + return canvas; + } + _canvas = document.createElement("canvas"); + _canvas.width = canvas.width; + _canvas.height = canvas.height; + _canvas.getContext("2d").drawImage(canvas, 0, 0); + return _canvas; + } + + function _toBinary(str) { + return Encode.atob( + str.substring(str.indexOf("base64,") + 7) + ); + } + + function _toDataUrl(str, type) { + return ( + "data:" + (type || "") + ";base64," + Encode.btoa(str) + ); + } + + function _preload(str) { + var comp = this; + + _img = new Image(); + _img.onerror = function () { + _purge.call(this); + comp.trigger("error", x.ImageError.WRONG_FORMAT); + }; + _img.onload = function () { + comp.trigger("load"); + }; + + _img.src = + str.substr(0, 5) == "data:" + ? str + : _toDataUrl(str, _blob.type); + } + + function _readAsDataUrl(file, callback) { + var comp = this, + fr; + + // use FileReader if it's available + if (window.FileReader) { + fr = new FileReader(); + fr.onload = function () { + callback.call(comp, this.result); + }; + fr.onerror = function () { + comp.trigger("error", x.ImageError.WRONG_FORMAT); + }; + fr.readAsDataURL(file); + } else { + return callback.call(this, file.getAsDataURL()); + } + } + + /** + * Transform canvas coordination according to specified frame size and orientation + * Orientation value is from EXIF tag + * @author Shinichi Tomita + */ + function _rotateToOrientaion(img, orientation) { + var RADIANS = Math.PI / 180; + var canvas = document.createElement("canvas"); + var ctx = canvas.getContext("2d"); + var width = img.width; + var height = img.height; + + if (Basic.inArray(orientation, [5, 6, 7, 8]) > -1) { + canvas.width = height; + canvas.height = width; + } else { + canvas.width = width; + canvas.height = height; + } + + /** + 1 = The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side. + 2 = The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side. + 3 = The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side. + 4 = The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side. + 5 = The 0th row is the visual left-hand side of the image, and the 0th column is the visual top. + 6 = The 0th row is the visual right-hand side of the image, and the 0th column is the visual top. + 7 = The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom. + 8 = The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom. + */ + switch (orientation) { + case 2: + // horizontal flip + ctx.translate(width, 0); + ctx.scale(-1, 1); + break; + case 3: + // 180 rotate left + ctx.translate(width, height); + ctx.rotate(180 * RADIANS); + break; + case 4: + // vertical flip + ctx.translate(0, height); + ctx.scale(1, -1); + break; + case 5: + // vertical flip + 90 rotate right + ctx.rotate(90 * RADIANS); + ctx.scale(1, -1); + break; + case 6: + // 90 rotate right + ctx.rotate(90 * RADIANS); + ctx.translate(0, -height); + break; + case 7: + // horizontal flip + 90 rotate right + ctx.rotate(90 * RADIANS); + ctx.translate(width, -height); + ctx.scale(-1, 1); + break; + case 8: + // 90 rotate left + ctx.rotate(-90 * RADIANS); + ctx.translate(-width, 0); + break; + } + + ctx.drawImage(img, 0, 0, width, height); + return canvas; + } + + function _purge() { + if (_imgInfo) { + _imgInfo.purge(); + _imgInfo = null; + } + + _binStr = _img = _canvas = _blob = null; + _modified = false; + } + } + + return (extensions.Image = HTML5Image); + }); + + // Included from: src/javascript/runtime/flash/Runtime.js + + /** + * Runtime.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /*global ActiveXObject:true */ + + /** +Defines constructor for Flash runtime. + +@class moxie/runtime/flash/Runtime +@private +*/ + define("moxie/runtime/flash/Runtime", [ + "moxie/core/utils/Basic", + "moxie/core/utils/Env", + "moxie/core/utils/Dom", + "moxie/core/Exceptions", + "moxie/runtime/Runtime", + ], function (Basic, Env, Dom, x, Runtime) { + var type = "flash", + extensions = {}; + + /** + Get the version of the Flash Player + + @method getShimVersion + @private + @return {Number} Flash Player version + */ + function getShimVersion() { + var version; + + try { + version = navigator.plugins["Shockwave Flash"]; + version = version.description; + } catch (e1) { + try { + version = new ActiveXObject( + "ShockwaveFlash.ShockwaveFlash" + ).GetVariable("$version"); + } catch (e2) { + version = "0.0"; + } + } + version = version.match(/\d+/g); + return parseFloat(version[0] + "." + version[1]); + } + + /** + Cross-browser SWF removal + - Especially needed to safely and completely remove a SWF in Internet Explorer + + Originated from SWFObject v2.2 + */ + function removeSWF(id) { + var obj = Dom.get(id); + if (obj && obj.nodeName == "OBJECT") { + if (Env.browser === "IE") { + obj.style.display = "none"; + (function onInit() { + // http://msdn.microsoft.com/en-us/library/ie/ms534360(v=vs.85).aspx + if (obj.readyState == 4) { + removeObjectInIE(id); + } else { + setTimeout(onInit, 10); + } + })(); + } else { + obj.parentNode.removeChild(obj); + } + } + } + + function removeObjectInIE(id) { + var obj = Dom.get(id); + if (obj) { + for (var i in obj) { + if (typeof obj[i] == "function") { + obj[i] = null; + } + } + obj.parentNode.removeChild(obj); + } + } + + /** + Constructor for the Flash Runtime + + @class FlashRuntime + @extends Runtime + */ + function FlashRuntime(options) { + var I = this, + initTimer; + + options = Basic.extend({ swf_url: Env.swf_url }, options); + + Runtime.call( + this, + options, + type, + { + access_binary: function (value) { + return value && I.mode === "browser"; + }, + access_image_binary: function (value) { + return value && I.mode === "browser"; + }, + display_media: Runtime.capTest( + defined("moxie/image/Image") + ), + do_cors: Runtime.capTrue, + drag_and_drop: false, + report_upload_progress: function () { + return I.mode === "client"; + }, + resize_image: Runtime.capTrue, + return_response_headers: false, + return_response_type: function (responseType) { + if (responseType === "json" && !!window.JSON) { + return true; + } + return ( + !Basic.arrayDiff(responseType, [ + "", + "text", + "document", + ]) || I.mode === "browser" + ); + }, + return_status_code: function (code) { + return ( + I.mode === "browser" || + !Basic.arrayDiff(code, [200, 404]) + ); + }, + select_file: Runtime.capTrue, + select_multiple: Runtime.capTrue, + send_binary_string: function (value) { + return value && I.mode === "browser"; + }, + send_browser_cookies: function (value) { + return value && I.mode === "browser"; + }, + send_custom_headers: function (value) { + return value && I.mode === "browser"; + }, + send_multipart: Runtime.capTrue, + slice_blob: function (value) { + return value && I.mode === "browser"; + }, + stream_upload: function (value) { + return value && I.mode === "browser"; + }, + summon_file_dialog: false, + upload_filesize: function (size) { + return ( + Basic.parseSizeStr(size) <= 2097152 || + I.mode === "client" + ); + }, + use_http_method: function (methods) { + return !Basic.arrayDiff(methods, ["GET", "POST"]); + }, + }, + { + // capabilities that require specific mode + access_binary: function (value) { + return value ? "browser" : "client"; + }, + access_image_binary: function (value) { + return value ? "browser" : "client"; + }, + report_upload_progress: function (value) { + return value ? "browser" : "client"; + }, + return_response_type: function (responseType) { + return Basic.arrayDiff(responseType, [ + "", + "text", + "json", + "document", + ]) + ? "browser" + : ["client", "browser"]; + }, + return_status_code: function (code) { + return Basic.arrayDiff(code, [200, 404]) + ? "browser" + : ["client", "browser"]; + }, + send_binary_string: function (value) { + return value ? "browser" : "client"; + }, + send_browser_cookies: function (value) { + return value ? "browser" : "client"; + }, + send_custom_headers: function (value) { + return value ? "browser" : "client"; + }, + slice_blob: function (value) { + return value ? "browser" : "client"; + }, + stream_upload: function (value) { + return value ? "client" : "browser"; + }, + upload_filesize: function (size) { + return Basic.parseSizeStr(size) >= 2097152 + ? "client" + : "browser"; + }, + }, + "client" + ); + + // minimal requirement for Flash Player version + if (getShimVersion() < 11.3) { + if (MXI_DEBUG && Env.debug.runtime) { + Env.log( + "\tFlash didn't meet minimal version requirement (11.3)." + ); + } + + this.mode = false; // with falsy mode, runtime won't operable, no matter what the mode was before + } + + Basic.extend( + this, + { + getShim: function () { + return Dom.get(this.uid); + }, + + shimExec: function (component, action) { + var args = [].slice.call(arguments, 2); + return I.getShim().exec( + this.uid, + component, + action, + args + ); + }, + + init: function () { + var html, el, container; + + container = this.getShimContainer(); + + // if not the minimal height, shims are not initialized in older browsers (e.g FF3.6, IE6,7,8, Safari 4.0,5.0, etc) + Basic.extend(container.style, { + position: "absolute", + top: "-8px", + left: "-8px", + width: "9px", + height: "9px", + overflow: "hidden", + }); + + // insert flash object + html = + '' + + '' + + '' + + '' + + '' + + ""; + + if (Env.browser === "IE") { + el = document.createElement("div"); + container.appendChild(el); + el.outerHTML = html; + el = container = null; // just in case + } else { + container.innerHTML = html; + } + + // Init is dispatched by the shim + initTimer = setTimeout(function () { + if (I && !I.initialized) { + // runtime might be already destroyed by this moment + I.trigger( + "Error", + new x.RuntimeError( + x.RuntimeError.NOT_INIT_ERR + ) + ); + + if (MXI_DEBUG && Env.debug.runtime) { + Env.log( + "\tFlash failed to initialize within a specified period of time (typically 5s)." + ); + } + } + }, 5000); + }, + + destroy: (function (destroy) { + // extend default destroy method + return function () { + removeSWF(I.uid); // SWF removal requires special care in IE + + destroy.call(I); + clearTimeout(initTimer); // initialization check might be still onwait + options = initTimer = destroy = I = null; + }; + })(this.destroy), + }, + extensions + ); + } + + Runtime.addConstructor(type, FlashRuntime); + + return extensions; + }); + + // Included from: src/javascript/runtime/flash/file/Blob.js + + /** + * Blob.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/flash/file/Blob +@private +*/ + define("moxie/runtime/flash/file/Blob", [ + "moxie/runtime/flash/Runtime", + "moxie/file/Blob", + ], function (extensions, Blob) { + var FlashBlob = { + slice: function (blob, start, end, type) { + var self = this.getRuntime(); + + if (start < 0) { + start = Math.max(blob.size + start, 0); + } else if (start > 0) { + start = Math.min(start, blob.size); + } + + if (end < 0) { + end = Math.max(blob.size + end, 0); + } else if (end > 0) { + end = Math.min(end, blob.size); + } + + blob = self.shimExec.call( + this, + "Blob", + "slice", + start, + end, + type || "" + ); + + if (blob) { + blob = new Blob(self.uid, blob); + } + return blob; + }, + }; + + return (extensions.Blob = FlashBlob); + }); + + // Included from: src/javascript/runtime/flash/file/FileInput.js + + /** + * FileInput.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/flash/file/FileInput +@private +*/ + define("moxie/runtime/flash/file/FileInput", [ + "moxie/runtime/flash/Runtime", + "moxie/file/File", + "moxie/core/utils/Basic", + ], function (extensions, File, Basic) { + var FileInput = { + init: function (options) { + var comp = this, + I = this.getRuntime(); + + this.bind( + "Change", + function () { + var files = I.shimExec.call( + comp, + "FileInput", + "getFiles" + ); + comp.files = []; + Basic.each(files, function (file) { + comp.files.push(new File(I.uid, file)); + }); + }, + 999 + ); + + this.getRuntime().shimExec.call(this, "FileInput", "init", { + accept: options.accept, + multiple: options.multiple, + }); + + this.trigger("ready"); + }, + }; + + return (extensions.FileInput = FileInput); + }); + + // Included from: src/javascript/runtime/flash/file/FileReader.js + + /** + * FileReader.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/flash/file/FileReader +@private +*/ + define("moxie/runtime/flash/file/FileReader", [ + "moxie/runtime/flash/Runtime", + "moxie/core/utils/Encode", + ], function (extensions, Encode) { + function _formatData(data, op) { + switch (op) { + case "readAsText": + return Encode.atob(data, "utf8"); + case "readAsBinaryString": + return Encode.atob(data); + case "readAsDataURL": + return data; + } + return null; + } + + var FileReader = { + read: function (op, blob) { + var comp = this; + + comp.result = ""; + + // special prefix for DataURL read mode + if (op === "readAsDataURL") { + comp.result = "data:" + (blob.type || "") + ";base64,"; + } + + comp.bind( + "Progress", + function (e, data) { + if (data) { + comp.result += _formatData(data, op); + } + }, + 999 + ); + + return comp + .getRuntime() + .shimExec.call( + this, + "FileReader", + "readAsBase64", + blob.uid + ); + }, + }; + + return (extensions.FileReader = FileReader); + }); + + // Included from: src/javascript/runtime/flash/file/FileReaderSync.js + + /** + * FileReaderSync.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/flash/file/FileReaderSync +@private +*/ + define("moxie/runtime/flash/file/FileReaderSync", [ + "moxie/runtime/flash/Runtime", + "moxie/core/utils/Encode", + ], function (extensions, Encode) { + function _formatData(data, op) { + switch (op) { + case "readAsText": + return Encode.atob(data, "utf8"); + case "readAsBinaryString": + return Encode.atob(data); + case "readAsDataURL": + return data; + } + return null; + } + + var FileReaderSync = { + read: function (op, blob) { + var result, + self = this.getRuntime(); + + result = self.shimExec.call( + this, + "FileReaderSync", + "readAsBase64", + blob.uid + ); + if (!result) { + return null; // or throw ex + } + + // special prefix for DataURL read mode + if (op === "readAsDataURL") { + result = + "data:" + (blob.type || "") + ";base64," + result; + } + + return _formatData(result, op, blob.type); + }, + }; + + return (extensions.FileReaderSync = FileReaderSync); + }); + + // Included from: src/javascript/runtime/flash/runtime/Transporter.js + + /** + * Transporter.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/flash/runtime/Transporter +@private +*/ + define("moxie/runtime/flash/runtime/Transporter", [ + "moxie/runtime/flash/Runtime", + "moxie/file/Blob", + ], function (extensions, Blob) { + var Transporter = { + getAsBlob: function (type) { + var self = this.getRuntime(), + blob = self.shimExec.call( + this, + "Transporter", + "getAsBlob", + type + ); + if (blob) { + return new Blob(self.uid, blob); + } + return null; + }, + }; + + return (extensions.Transporter = Transporter); + }); + + // Included from: src/javascript/runtime/flash/xhr/XMLHttpRequest.js + + /** + * XMLHttpRequest.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/flash/xhr/XMLHttpRequest +@private +*/ + define("moxie/runtime/flash/xhr/XMLHttpRequest", [ + "moxie/runtime/flash/Runtime", + "moxie/core/utils/Basic", + "moxie/file/Blob", + "moxie/file/File", + "moxie/file/FileReaderSync", + "moxie/runtime/flash/file/FileReaderSync", + "moxie/xhr/FormData", + "moxie/runtime/Transporter", + "moxie/runtime/flash/runtime/Transporter", + ], function (extensions, Basic, Blob, File, FileReaderSync, FileReaderSyncFlash, FormData, Transporter, TransporterFlash) { + var XMLHttpRequest = { + send: function (meta, data) { + var target = this, + self = target.getRuntime(); + + function send() { + meta.transport = self.mode; + self.shimExec.call( + target, + "XMLHttpRequest", + "send", + meta, + data + ); + } + + function appendBlob(name, blob) { + self.shimExec.call( + target, + "XMLHttpRequest", + "appendBlob", + name, + blob.uid + ); + data = null; + send(); + } + + function attachBlob(blob, cb) { + var tr = new Transporter(); + + tr.bind("TransportingComplete", function () { + cb(this.result); + }); + + tr.transport(blob.getSource(), blob.type, { + ruid: self.uid, + }); + } + + // copy over the headers if any + if (!Basic.isEmptyObj(meta.headers)) { + Basic.each(meta.headers, function (value, header) { + self.shimExec.call( + target, + "XMLHttpRequest", + "setRequestHeader", + header, + value.toString() + ); // Silverlight doesn't accept integers into the arguments of type object + }); + } + + // transfer over multipart params and blob itself + if (data instanceof FormData) { + var blobField; + data.each(function (value, name) { + if (value instanceof Blob) { + blobField = name; + } else { + self.shimExec.call( + target, + "XMLHttpRequest", + "append", + name, + value + ); + } + }); + + if (!data.hasBlob()) { + data = null; + send(); + } else { + var blob = data.getBlob(); + if (blob.isDetached()) { + attachBlob(blob, function (attachedBlob) { + blob.destroy(); + appendBlob(blobField, attachedBlob); + }); + } else { + appendBlob(blobField, blob); + } + } + } else if (data instanceof Blob) { + if (data.isDetached()) { + attachBlob(data, function (attachedBlob) { + data.destroy(); + data = attachedBlob.uid; + send(); + }); + } else { + data = data.uid; + send(); + } + } else { + send(); + } + }, + + getResponse: function (responseType) { + var frs, + blob, + self = this.getRuntime(); + + blob = self.shimExec.call( + this, + "XMLHttpRequest", + "getResponseAsBlob" + ); + + if (blob) { + blob = new File(self.uid, blob); + + if ("blob" === responseType) { + return blob; + } + + try { + frs = new FileReaderSync(); + + if (!!~Basic.inArray(responseType, ["", "text"])) { + return frs.readAsText(blob); + } else if ( + "json" === responseType && + !!window.JSON + ) { + return JSON.parse(frs.readAsText(blob)); + } + } finally { + blob.destroy(); + } + } + return null; + }, + + abort: function (upload_complete_flag) { + var self = this.getRuntime(); + + self.shimExec.call(this, "XMLHttpRequest", "abort"); + + this.dispatchEvent("readystatechange"); + // this.dispatchEvent('progress'); + this.dispatchEvent("abort"); + + //if (!upload_complete_flag) { + // this.dispatchEvent('uploadprogress'); + //} + }, + }; + + return (extensions.XMLHttpRequest = XMLHttpRequest); + }); + + // Included from: src/javascript/runtime/flash/image/Image.js + + /** + * Image.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/flash/image/Image +@private +*/ + define("moxie/runtime/flash/image/Image", [ + "moxie/runtime/flash/Runtime", + "moxie/core/utils/Basic", + "moxie/runtime/Transporter", + "moxie/file/Blob", + "moxie/file/FileReaderSync", + ], function (extensions, Basic, Transporter, Blob, FileReaderSync) { + var Image = { + loadFromBlob: function (blob) { + var comp = this, + self = comp.getRuntime(); + + function exec(srcBlob) { + self.shimExec.call( + comp, + "Image", + "loadFromBlob", + srcBlob.uid + ); + comp = self = null; + } + + if (blob.isDetached()) { + // binary string + var tr = new Transporter(); + tr.bind("TransportingComplete", function () { + exec(tr.result.getSource()); + }); + tr.transport(blob.getSource(), blob.type, { + ruid: self.uid, + }); + } else { + exec(blob.getSource()); + } + }, + + loadFromImage: function (img) { + var self = this.getRuntime(); + return self.shimExec.call( + this, + "Image", + "loadFromImage", + img.uid + ); + }, + + getInfo: function () { + var self = this.getRuntime(), + info = self.shimExec.call(this, "Image", "getInfo"); + if ( + info.meta && + info.meta.thumb && + info.meta.thumb.data && + !(self.meta.thumb.data instanceof Blob) + ) { + info.meta.thumb.data = new Blob( + self.uid, + info.meta.thumb.data + ); + } + return info; + }, + + getAsBlob: function (type, quality) { + var self = this.getRuntime(), + blob = self.shimExec.call( + this, + "Image", + "getAsBlob", + type, + quality + ); + if (blob) { + return new Blob(self.uid, blob); + } + return null; + }, + + getAsDataURL: function () { + var self = this.getRuntime(), + blob = self.Image.getAsBlob.apply(this, arguments), + frs; + if (!blob) { + return null; + } + frs = new FileReaderSync(); + return frs.readAsDataURL(blob); + }, + }; + + return (extensions.Image = Image); + }); + + // Included from: src/javascript/runtime/silverlight/Runtime.js + + /** + * RunTime.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /*global ActiveXObject:true */ + + /** +Defines constructor for Silverlight runtime. + +@class moxie/runtime/silverlight/Runtime +@private +*/ + define("moxie/runtime/silverlight/Runtime", [ + "moxie/core/utils/Basic", + "moxie/core/utils/Env", + "moxie/core/utils/Dom", + "moxie/core/Exceptions", + "moxie/runtime/Runtime", + ], function (Basic, Env, Dom, x, Runtime) { + var type = "silverlight", + extensions = {}; + + function isInstalled(version) { + var isVersionSupported = false, + control = null, + actualVer, + actualVerArray, + reqVerArray, + requiredVersionPart, + actualVersionPart, + index = 0; + + try { + try { + control = new ActiveXObject("AgControl.AgControl"); + + if (control.IsVersionSupported(version)) { + isVersionSupported = true; + } + + control = null; + } catch (e) { + var plugin = navigator.plugins["Silverlight Plug-In"]; + + if (plugin) { + actualVer = plugin.description; + + if (actualVer === "1.0.30226.2") { + actualVer = "2.0.30226.2"; + } + + actualVerArray = actualVer.split("."); + + while (actualVerArray.length > 3) { + actualVerArray.pop(); + } + + while (actualVerArray.length < 4) { + actualVerArray.push(0); + } + + reqVerArray = version.split("."); + + while (reqVerArray.length > 4) { + reqVerArray.pop(); + } + + do { + requiredVersionPart = parseInt( + reqVerArray[index], + 10 + ); + actualVersionPart = parseInt( + actualVerArray[index], + 10 + ); + index++; + } while ( + index < reqVerArray.length && + requiredVersionPart === actualVersionPart + ); + + if ( + requiredVersionPart <= actualVersionPart && + !isNaN(requiredVersionPart) + ) { + isVersionSupported = true; + } + } + } + } catch (e2) { + isVersionSupported = false; + } + + return isVersionSupported; + } + + /** + Constructor for the Silverlight Runtime + + @class SilverlightRuntime + @extends Runtime + */ + function SilverlightRuntime(options) { + var I = this, + initTimer; + + options = Basic.extend({ xap_url: Env.xap_url }, options); + + Runtime.call( + this, + options, + type, + { + access_binary: Runtime.capTrue, + access_image_binary: Runtime.capTrue, + display_media: Runtime.capTest( + defined("moxie/image/Image") + ), + do_cors: Runtime.capTrue, + drag_and_drop: false, + report_upload_progress: Runtime.capTrue, + resize_image: Runtime.capTrue, + return_response_headers: function (value) { + return value && I.mode === "client"; + }, + return_response_type: function (responseType) { + if (responseType !== "json") { + return true; + } else { + return !!window.JSON; + } + }, + return_status_code: function (code) { + return ( + I.mode === "client" || + !Basic.arrayDiff(code, [200, 404]) + ); + }, + select_file: Runtime.capTrue, + select_multiple: Runtime.capTrue, + send_binary_string: Runtime.capTrue, + send_browser_cookies: function (value) { + return value && I.mode === "browser"; + }, + send_custom_headers: function (value) { + return value && I.mode === "client"; + }, + send_multipart: Runtime.capTrue, + slice_blob: Runtime.capTrue, + stream_upload: true, + summon_file_dialog: false, + upload_filesize: Runtime.capTrue, + use_http_method: function (methods) { + return ( + I.mode === "client" || + !Basic.arrayDiff(methods, ["GET", "POST"]) + ); + }, + }, + { + // capabilities that require specific mode + return_response_headers: function (value) { + return value ? "client" : "browser"; + }, + return_status_code: function (code) { + return Basic.arrayDiff(code, [200, 404]) + ? "client" + : ["client", "browser"]; + }, + send_browser_cookies: function (value) { + return value ? "browser" : "client"; + }, + send_custom_headers: function (value) { + return value ? "client" : "browser"; + }, + use_http_method: function (methods) { + return Basic.arrayDiff(methods, ["GET", "POST"]) + ? "client" + : ["client", "browser"]; + }, + } + ); + + // minimal requirement + if (!isInstalled("2.0.31005.0") || Env.browser === "Opera") { + if (MXI_DEBUG && Env.debug.runtime) { + Env.log( + "\tSilverlight is not installed or minimal version (2.0.31005.0) requirement not met (not likely)." + ); + } + + this.mode = false; + } + + Basic.extend( + this, + { + getShim: function () { + return Dom.get(this.uid).content.Moxie; + }, + + shimExec: function (component, action) { + var args = [].slice.call(arguments, 2); + return I.getShim().exec( + this.uid, + component, + action, + args + ); + }, + + init: function () { + var container; + + container = this.getShimContainer(); + + container.innerHTML = + '' + + '' + + '' + + '' + + '' + + '' + + ""; + + // Init is dispatched by the shim + initTimer = setTimeout( + function () { + if (I && !I.initialized) { + // runtime might be already destroyed by this moment + I.trigger( + "Error", + new x.RuntimeError( + x.RuntimeError.NOT_INIT_ERR + ) + ); + + if (MXI_DEBUG && Env.debug.runtime) { + Env.log( + "Silverlight failed to initialize within a specified period of time (5-10s)." + ); + } + } + }, + Env.OS !== "Windows" ? 10000 : 5000 + ); // give it more time to initialize in non Windows OS (like Mac) + }, + + destroy: (function (destroy) { + // extend default destroy method + return function () { + destroy.call(I); + clearTimeout(initTimer); // initialization check might be still onwait + options = initTimer = destroy = I = null; + }; + })(this.destroy), + }, + extensions + ); + } + + Runtime.addConstructor(type, SilverlightRuntime); + + return extensions; + }); + + // Included from: src/javascript/runtime/silverlight/file/Blob.js + + /** + * Blob.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/silverlight/file/Blob +@private +*/ + define("moxie/runtime/silverlight/file/Blob", [ + "moxie/runtime/silverlight/Runtime", + "moxie/core/utils/Basic", + "moxie/runtime/flash/file/Blob", + ], function (extensions, Basic, Blob) { + return (extensions.Blob = Basic.extend({}, Blob)); + }); + + // Included from: src/javascript/runtime/silverlight/file/FileInput.js + + /** + * FileInput.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/silverlight/file/FileInput +@private +*/ + define("moxie/runtime/silverlight/file/FileInput", [ + "moxie/runtime/silverlight/Runtime", + "moxie/file/File", + "moxie/core/utils/Basic", + ], function (extensions, File, Basic) { + function toFilters(accept) { + var filter = ""; + for (var i = 0; i < accept.length; i++) { + filter += + (filter !== "" ? "|" : "") + + accept[i].title + + " | *." + + accept[i].extensions.replace(/,/g, ";*."); + } + return filter; + } + + var FileInput = { + init: function (options) { + var comp = this, + I = this.getRuntime(); + + this.bind( + "Change", + function () { + var files = I.shimExec.call( + comp, + "FileInput", + "getFiles" + ); + comp.files = []; + Basic.each(files, function (file) { + comp.files.push(new File(I.uid, file)); + }); + }, + 999 + ); + + I.shimExec.call( + this, + "FileInput", + "init", + toFilters(options.accept), + options.multiple + ); + this.trigger("ready"); + }, + + setOption: function (name, value) { + if (name == "accept") { + value = toFilters(value); + } + this.getRuntime().shimExec.call( + this, + "FileInput", + "setOption", + name, + value + ); + }, + }; + + return (extensions.FileInput = FileInput); + }); + + // Included from: src/javascript/runtime/silverlight/file/FileDrop.js + + /** + * FileDrop.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/silverlight/file/FileDrop +@private +*/ + define("moxie/runtime/silverlight/file/FileDrop", [ + "moxie/runtime/silverlight/Runtime", + "moxie/core/utils/Dom", + "moxie/core/utils/Events", + ], function (extensions, Dom, Events) { + // not exactly useful, since works only in safari (...crickets...) + var FileDrop = { + init: function () { + var comp = this, + self = comp.getRuntime(), + dropZone; + + dropZone = self.getShimContainer(); + + Events.addEvent( + dropZone, + "dragover", + function (e) { + e.preventDefault(); + e.stopPropagation(); + e.dataTransfer.dropEffect = "copy"; + }, + comp.uid + ); + + Events.addEvent( + dropZone, + "dragenter", + function (e) { + e.preventDefault(); + var flag = Dom.get(self.uid).dragEnter(e); + // If handled, then stop propagation of event in DOM + if (flag) { + e.stopPropagation(); + } + }, + comp.uid + ); + + Events.addEvent( + dropZone, + "drop", + function (e) { + e.preventDefault(); + var flag = Dom.get(self.uid).dragDrop(e); + // If handled, then stop propagation of event in DOM + if (flag) { + e.stopPropagation(); + } + }, + comp.uid + ); + + return self.shimExec.call(this, "FileDrop", "init"); + }, + }; + + return (extensions.FileDrop = FileDrop); + }); + + // Included from: src/javascript/runtime/silverlight/file/FileReader.js + + /** + * FileReader.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/silverlight/file/FileReader +@private +*/ + define("moxie/runtime/silverlight/file/FileReader", [ + "moxie/runtime/silverlight/Runtime", + "moxie/core/utils/Basic", + "moxie/runtime/flash/file/FileReader", + ], function (extensions, Basic, FileReader) { + return (extensions.FileReader = Basic.extend({}, FileReader)); + }); + + // Included from: src/javascript/runtime/silverlight/file/FileReaderSync.js + + /** + * FileReaderSync.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/silverlight/file/FileReaderSync +@private +*/ + define("moxie/runtime/silverlight/file/FileReaderSync", [ + "moxie/runtime/silverlight/Runtime", + "moxie/core/utils/Basic", + "moxie/runtime/flash/file/FileReaderSync", + ], function (extensions, Basic, FileReaderSync) { + return (extensions.FileReaderSync = Basic.extend( + {}, + FileReaderSync + )); + }); + + // Included from: src/javascript/runtime/silverlight/runtime/Transporter.js + + /** + * Transporter.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/silverlight/runtime/Transporter +@private +*/ + define("moxie/runtime/silverlight/runtime/Transporter", [ + "moxie/runtime/silverlight/Runtime", + "moxie/core/utils/Basic", + "moxie/runtime/flash/runtime/Transporter", + ], function (extensions, Basic, Transporter) { + return (extensions.Transporter = Basic.extend({}, Transporter)); + }); + + // Included from: src/javascript/runtime/silverlight/xhr/XMLHttpRequest.js + + /** + * XMLHttpRequest.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/silverlight/xhr/XMLHttpRequest +@private +*/ + define("moxie/runtime/silverlight/xhr/XMLHttpRequest", [ + "moxie/runtime/silverlight/Runtime", + "moxie/core/utils/Basic", + "moxie/runtime/flash/xhr/XMLHttpRequest", + "moxie/runtime/silverlight/file/FileReaderSync", + "moxie/runtime/silverlight/runtime/Transporter", + ], function (extensions, Basic, XMLHttpRequest, FileReaderSyncSilverlight, TransporterSilverlight) { + return (extensions.XMLHttpRequest = Basic.extend( + {}, + XMLHttpRequest + )); + }); + + // Included from: src/javascript/runtime/silverlight/image/Image.js + + /** + * Image.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/silverlight/image/Image +@private +*/ + define("moxie/runtime/silverlight/image/Image", [ + "moxie/runtime/silverlight/Runtime", + "moxie/core/utils/Basic", + "moxie/file/Blob", + "moxie/runtime/flash/image/Image", + ], function (extensions, Basic, Blob, Image) { + return (extensions.Image = Basic.extend({}, Image, { + getInfo: function () { + var self = this.getRuntime(), + grps = ["tiff", "exif", "gps", "thumb"], + info = { meta: {} }, + rawInfo = self.shimExec.call(this, "Image", "getInfo"); + if (rawInfo.meta) { + Basic.each(grps, function (grp) { + var meta = rawInfo.meta[grp], + tag, + i, + length, + value; + if (meta && meta.keys) { + info.meta[grp] = {}; + for ( + i = 0, length = meta.keys.length; + i < length; + i++ + ) { + tag = meta.keys[i]; + value = meta[tag]; + if (value) { + // convert numbers + if (/^(\d|[1-9]\d+)$/.test(value)) { + // integer (make sure doesn't start with zero) + value = parseInt(value, 10); + } else if (/^\d*\.\d+$/.test(value)) { + // double + value = parseFloat(value); + } + info.meta[grp][tag] = value; + } + } + } + }); + + // save thumb data as blob + if ( + info.meta && + info.meta.thumb && + info.meta.thumb.data && + !(self.meta.thumb.data instanceof Blob) + ) { + info.meta.thumb.data = new Blob( + self.uid, + info.meta.thumb.data + ); + } + } + + info.width = parseInt(rawInfo.width, 10); + info.height = parseInt(rawInfo.height, 10); + info.size = parseInt(rawInfo.size, 10); + info.type = rawInfo.type; + info.name = rawInfo.name; + + return info; + }, + + resize: function (rect, ratio, opts) { + this.getRuntime().shimExec.call( + this, + "Image", + "resize", + rect.x, + rect.y, + rect.width, + rect.height, + ratio, + opts.preserveHeaders, + opts.resample + ); + }, + })); + }); + + // Included from: src/javascript/runtime/html4/Runtime.js + + /** + * Runtime.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /*global File:true */ + + /** +Defines constructor for HTML4 runtime. + +@class moxie/runtime/html4/Runtime +@private +*/ + define("moxie/runtime/html4/Runtime", [ + "moxie/core/utils/Basic", + "moxie/core/Exceptions", + "moxie/runtime/Runtime", + "moxie/core/utils/Env", + ], function (Basic, x, Runtime, Env) { + var type = "html4", + extensions = {}; + + function Html4Runtime(options) { + var I = this, + Test = Runtime.capTest, + True = Runtime.capTrue; + Runtime.call(this, options, type, { + access_binary: Test( + window.FileReader || (window.File && File.getAsDataURL) + ), + access_image_binary: false, + display_media: Test( + (Env.can("create_canvas") || + Env.can("use_data_uri_over32kb")) && + defined("moxie/image/Image") + ), + do_cors: false, + drag_and_drop: false, + filter_by_extension: Test( + (function () { + // if you know how to feature-detect this, please suggest + return !( + (Env.browser === "Chrome" && + Env.verComp(Env.version, 28, "<")) || + (Env.browser === "IE" && + Env.verComp(Env.version, 10, "<")) || + (Env.browser === "Safari" && + Env.verComp(Env.version, 7, "<")) || + (Env.browser === "Firefox" && + Env.verComp(Env.version, 37, "<")) + ); + })() + ), + resize_image: function () { + return ( + extensions.Image && + I.can("access_binary") && + Env.can("create_canvas") + ); + }, + report_upload_progress: false, + return_response_headers: false, + return_response_type: function (responseType) { + if (responseType === "json" && !!window.JSON) { + return true; + } + return !!~Basic.inArray(responseType, [ + "text", + "document", + "", + ]); + }, + return_status_code: function (code) { + return !Basic.arrayDiff(code, [200, 404]); + }, + select_file: function () { + return Env.can("use_fileinput"); + }, + select_multiple: false, + send_binary_string: false, + send_custom_headers: false, + send_multipart: true, + slice_blob: false, + stream_upload: function () { + return I.can("select_file"); + }, + summon_file_dialog: function () { + // yeah... some dirty sniffing here... + return ( + I.can("select_file") && + ((Env.browser === "Firefox" && + Env.verComp(Env.version, 4, ">=")) || + (Env.browser === "Opera" && + Env.verComp(Env.version, 12, ">=")) || + (Env.browser === "IE" && + Env.verComp(Env.version, 10, ">=")) || + !!~Basic.inArray(Env.browser, [ + "Chrome", + "Safari", + ])) + ); + }, + upload_filesize: True, + use_http_method: function (methods) { + return !Basic.arrayDiff(methods, ["GET", "POST"]); + }, + }); + + Basic.extend(this, { + init: function () { + this.trigger("Init"); + }, + + destroy: (function (destroy) { + // extend default destroy method + return function () { + destroy.call(I); + destroy = I = null; + }; + })(this.destroy), + }); + + Basic.extend(this.getShim(), extensions); + } + + Runtime.addConstructor(type, Html4Runtime); + + return extensions; + }); + + // Included from: src/javascript/runtime/html4/file/FileInput.js + + /** + * FileInput.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/html4/file/FileInput +@private +*/ + define("moxie/runtime/html4/file/FileInput", [ + "moxie/runtime/html4/Runtime", + "moxie/file/File", + "moxie/core/utils/Basic", + "moxie/core/utils/Dom", + "moxie/core/utils/Events", + "moxie/core/utils/Mime", + "moxie/core/utils/Env", + ], function (extensions, File, Basic, Dom, Events, Mime, Env) { + function FileInput() { + var _uid, + _mimes = [], + _options, + _browseBtnZIndex; // save original z-index; + + function addInput() { + var comp = this, + I = comp.getRuntime(), + shimContainer, + browseButton, + currForm, + form, + input, + uid; + + uid = Basic.guid("uid_"); + + shimContainer = I.getShimContainer(); // we get new ref every time to avoid memory leaks in IE + + if (_uid) { + // move previous form out of the view + currForm = Dom.get(_uid + "_form"); + if (currForm) { + Basic.extend(currForm.style, { top: "100%" }); + } + } + + // build form in DOM, since innerHTML version not able to submit file for some reason + form = document.createElement("form"); + form.setAttribute("id", uid + "_form"); + form.setAttribute("method", "post"); + form.setAttribute("enctype", "multipart/form-data"); + form.setAttribute("encoding", "multipart/form-data"); + + Basic.extend(form.style, { + overflow: "hidden", + position: "absolute", + top: 0, + left: 0, + width: "100%", + height: "100%", + }); + + input = document.createElement("input"); + input.setAttribute("id", uid); + input.setAttribute("type", "file"); + input.setAttribute("accept", _mimes.join(",")); + + Basic.extend(input.style, { + fontSize: "999px", + opacity: 0, + }); + + form.appendChild(input); + shimContainer.appendChild(form); + + // prepare file input to be placed underneath the browse_button element + Basic.extend(input.style, { + position: "absolute", + top: 0, + left: 0, + width: "100%", + height: "100%", + }); + + if ( + Env.browser === "IE" && + Env.verComp(Env.version, 10, "<") + ) { + Basic.extend(input.style, { + filter: + "progid:DXImageTransform.Microsoft.Alpha(opacity=0)", + }); + } + + input.onchange = function () { + // there should be only one handler for this + var file; + + if (!this.value) { + return; + } + + if (this.files) { + // check if browser is fresh enough + file = this.files[0]; + + // ignore empty files (IE10 for example hangs if you try to send them via XHR) + if (file.size === 0) { + form.parentNode.removeChild(form); + return; + } + } else { + file = { + name: this.value, + }; + } + + file = new File(I.uid, file); + + // clear event handler + this.onchange = function () {}; + addInput.call(comp); + + comp.files = [file]; + + // substitute all ids with file uids (consider file.uid read-only - we cannot do it the other way around) + input.setAttribute("id", file.uid); + form.setAttribute("id", file.uid + "_form"); + + comp.trigger("change"); + + input = form = null; + }; + + // route click event to the input + if (I.can("summon_file_dialog")) { + browseButton = Dom.get(_options.browse_button); + Events.removeEvent(browseButton, "click", comp.uid); + Events.addEvent( + browseButton, + "click", + function (e) { + if (input && !input.disabled) { + // for some reason FF (up to 8.0.1 so far) lets to click disabled input[type=file] + input.click(); + } + e.preventDefault(); + }, + comp.uid + ); + } + + _uid = uid; + + shimContainer = currForm = browseButton = null; + } + + Basic.extend(this, { + init: function (options) { + var comp = this, + I = comp.getRuntime(), + shimContainer; + + // figure out accept string + _options = options; + _mimes = + options.accept.mimes || + Mime.extList2mimes( + options.accept, + I.can("filter_by_extension") + ); + + shimContainer = I.getShimContainer(); + + (function () { + var browseButton, zIndex, top; + + browseButton = Dom.get(options.browse_button); + _browseBtnZIndex = + Dom.getStyle(browseButton, "z-index") || "auto"; + + // Route click event to the input[type=file] element for browsers that support such behavior + if (I.can("summon_file_dialog")) { + if ( + Dom.getStyle(browseButton, "position") === + "static" + ) { + browseButton.style.position = "relative"; + } + + comp.bind("Refresh", function () { + zIndex = + parseInt(_browseBtnZIndex, 10) || 1; + + Dom.get( + _options.browse_button + ).style.zIndex = zIndex; + this.getRuntime().getShimContainer().style.zIndex = + zIndex - 1; + }); + } + + /* Since we have to place input[type=file] on top of the browse_button for some browsers, + browse_button loses interactivity, so we restore it here */ + top = I.can("summon_file_dialog") + ? browseButton + : shimContainer; + + Events.addEvent( + top, + "mouseover", + function () { + comp.trigger("mouseenter"); + }, + comp.uid + ); + + Events.addEvent( + top, + "mouseout", + function () { + comp.trigger("mouseleave"); + }, + comp.uid + ); + + Events.addEvent( + top, + "mousedown", + function () { + comp.trigger("mousedown"); + }, + comp.uid + ); + + Events.addEvent( + Dom.get(options.container), + "mouseup", + function () { + comp.trigger("mouseup"); + }, + comp.uid + ); + + browseButton = null; + })(); + + addInput.call(this); + + shimContainer = null; + + // trigger ready event asynchronously + comp.trigger({ + type: "ready", + async: true, + }); + }, + + setOption: function (name, value) { + var I = this.getRuntime(); + var input; + + if (name == "accept") { + _mimes = + value.mimes || + Mime.extList2mimes( + value, + I.can("filter_by_extension") + ); + } + + // update current input + input = Dom.get(_uid); + if (input) { + input.setAttribute("accept", _mimes.join(",")); + } + }, + + disable: function (state) { + var input; + + if ((input = Dom.get(_uid))) { + input.disabled = !!state; + } + }, + + destroy: function () { + var I = this.getRuntime(), + shim = I.getShim(), + shimContainer = I.getShimContainer(), + container = _options && Dom.get(_options.container), + browseButton = + _options && Dom.get(_options.browse_button); + if (container) { + Events.removeAllEvents(container, this.uid); + } + + if (browseButton) { + Events.removeAllEvents(browseButton, this.uid); + browseButton.style.zIndex = _browseBtnZIndex; // reset to original value + } + + if (shimContainer) { + Events.removeAllEvents(shimContainer, this.uid); + shimContainer.innerHTML = ""; + } + + shim.removeInstance(this.uid); + + _uid = _mimes = _options = shimContainer = container = browseButton = shim = null; + }, + }); + } + + return (extensions.FileInput = FileInput); + }); + + // Included from: src/javascript/runtime/html4/file/FileReader.js + + /** + * FileReader.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/html4/file/FileReader +@private +*/ + define("moxie/runtime/html4/file/FileReader", [ + "moxie/runtime/html4/Runtime", + "moxie/runtime/html5/file/FileReader", + ], function (extensions, FileReader) { + return (extensions.FileReader = FileReader); + }); + + // Included from: src/javascript/runtime/html4/xhr/XMLHttpRequest.js + + /** + * XMLHttpRequest.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/html4/xhr/XMLHttpRequest +@private +*/ + define("moxie/runtime/html4/xhr/XMLHttpRequest", [ + "moxie/runtime/html4/Runtime", + "moxie/core/utils/Basic", + "moxie/core/utils/Dom", + "moxie/core/utils/Url", + "moxie/core/Exceptions", + "moxie/core/utils/Events", + "moxie/file/Blob", + "moxie/xhr/FormData", + ], function (extensions, Basic, Dom, Url, x, Events, Blob, FormData) { + function XMLHttpRequest() { + var _status, _response, _iframe; + + function cleanup(cb) { + var target = this, + uid, + form, + inputs, + i, + hasFile = false; + + if (!_iframe) { + return; + } + + uid = _iframe.id.replace(/_iframe$/, ""); + + form = Dom.get(uid + "_form"); + if (form) { + inputs = form.getElementsByTagName("input"); + i = inputs.length; + + while (i--) { + switch (inputs[i].getAttribute("type")) { + case "hidden": + inputs[i].parentNode.removeChild(inputs[i]); + break; + case "file": + hasFile = true; // flag the case for later + break; + } + } + inputs = []; + + if (!hasFile) { + // we need to keep the form for sake of possible retries + form.parentNode.removeChild(form); + } + form = null; + } + + // without timeout, request is marked as canceled (in console) + setTimeout(function () { + Events.removeEvent(_iframe, "load", target.uid); + if (_iframe.parentNode) { + // #382 + _iframe.parentNode.removeChild(_iframe); + } + + // check if shim container has any other children, if - not, remove it as well + var shimContainer = target + .getRuntime() + .getShimContainer(); + if (!shimContainer.children.length) { + shimContainer.parentNode.removeChild(shimContainer); + } + + shimContainer = _iframe = null; + cb(); + }, 1); + } + + Basic.extend(this, { + send: function (meta, data) { + var target = this, + I = target.getRuntime(), + uid, + form, + input, + blob; + + _status = _response = null; + + function createIframe() { + var container = + I.getShimContainer() || document.body, + temp = document.createElement("div"); + // IE 6 won't be able to set the name using setAttribute or iframe.name + temp.innerHTML = + ''; + _iframe = temp.firstChild; + container.appendChild(_iframe); + + /* _iframe.onreadystatechange = function() { + console.info(_iframe.readyState); + };*/ + + Events.addEvent( + _iframe, + "load", + function () { + // _iframe.onload doesn't work in IE lte 8 + var el; + + try { + el = + _iframe.contentWindow.document || + _iframe.contentDocument || + window.frames[_iframe.id].document; + + // try to detect some standard error pages + if ( + /^4(0[0-9]|1[0-7]|2[2346])\s/.test( + el.title + ) + ) { + // test if title starts with 4xx HTTP error + _status = el.title.replace( + /^(\d+).*$/, + "$1" + ); + } else { + _status = 200; + // get result + _response = Basic.trim( + el.body.innerHTML + ); + + // we need to fire these at least once + target.trigger({ + type: "progress", + loaded: _response.length, + total: _response.length, + }); + + if (blob) { + // if we were uploading a file + target.trigger({ + type: "uploadprogress", + loaded: blob.size || 1025, + total: blob.size || 1025, + }); + } + } + } catch (ex) { + if (Url.hasSameOrigin(meta.url)) { + // if response is sent with error code, iframe in IE gets redirected to res://ieframe.dll/http_x.htm + // which obviously results to cross domain error (wtf?) + _status = 404; + } else { + cleanup.call(target, function () { + target.trigger("error"); + }); + return; + } + } + + cleanup.call(target, function () { + target.trigger("load"); + }); + }, + target.uid + ); + } // end createIframe + + // prepare data to be sent and convert if required + if (data instanceof FormData && data.hasBlob()) { + blob = data.getBlob(); + uid = blob.uid; + input = Dom.get(uid); + form = Dom.get(uid + "_form"); + if (!form) { + throw new x.DOMException( + x.DOMException.NOT_FOUND_ERR + ); + } + } else { + uid = Basic.guid("uid_"); + + form = document.createElement("form"); + form.setAttribute("id", uid + "_form"); + form.setAttribute("method", meta.method); + form.setAttribute("enctype", "multipart/form-data"); + form.setAttribute( + "encoding", + "multipart/form-data" + ); + + I.getShimContainer().appendChild(form); + } + + // set upload target + form.setAttribute("target", uid + "_iframe"); + + if (data instanceof FormData) { + data.each(function (value, name) { + if (value instanceof Blob) { + if (input) { + input.setAttribute("name", name); + } + } else { + var hidden = document.createElement( + "input" + ); + + Basic.extend(hidden, { + type: "hidden", + name: name, + value: value, + }); + + // make sure that input[type="file"], if it's there, comes last + if (input) { + form.insertBefore(hidden, input); + } else { + form.appendChild(hidden); + } + } + }); + } + + // set destination url + form.setAttribute("action", meta.url); + + createIframe(); + form.submit(); + target.trigger("loadstart"); + }, + + getStatus: function () { + return _status; + }, + + getResponse: function (responseType) { + if ("json" === responseType) { + // strip off
..
tags that might be enclosing the response + if ( + Basic.typeOf(_response) === "string" && + !!window.JSON + ) { + try { + return JSON.parse( + _response + .replace(/^\s*]*>/, "") + .replace(/<\/pre>\s*$/, "") + ); + } catch (ex) { + return null; + } + } + } else if ("document" === responseType) { + } + return _response; + }, + + abort: function () { + var target = this; + + if (_iframe && _iframe.contentWindow) { + if (_iframe.contentWindow.stop) { + // FireFox/Safari/Chrome + _iframe.contentWindow.stop(); + } else if ( + _iframe.contentWindow.document.execCommand + ) { + // IE + _iframe.contentWindow.document.execCommand( + "Stop" + ); + } else { + _iframe.src = "about:blank"; + } + } + + cleanup.call(this, function () { + // target.dispatchEvent('readystatechange'); + target.dispatchEvent("abort"); + }); + }, + }); + } + + return (extensions.XMLHttpRequest = XMLHttpRequest); + }); + + // Included from: src/javascript/runtime/html4/image/Image.js + + /** + * Image.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + /** +@class moxie/runtime/html4/image/Image +@private +*/ + define("moxie/runtime/html4/image/Image", [ + "moxie/runtime/html4/Runtime", + "moxie/runtime/html5/image/Image", + ], function (extensions, Image) { + return (extensions.Image = Image); + }); + + expose([ + "moxie/core/utils/Basic", + "moxie/core/utils/Encode", + "moxie/core/utils/Env", + "moxie/core/Exceptions", + "moxie/core/utils/Dom", + "moxie/core/EventTarget", + "moxie/runtime/Runtime", + "moxie/runtime/RuntimeClient", + "moxie/file/Blob", + "moxie/core/I18n", + "moxie/core/utils/Mime", + "moxie/file/FileInput", + "moxie/file/File", + "moxie/file/FileDrop", + "moxie/file/FileReader", + "moxie/core/utils/Url", + "moxie/runtime/RuntimeTarget", + "moxie/xhr/FormData", + "moxie/xhr/XMLHttpRequest", + "moxie/runtime/Transporter", + "moxie/image/Image", + "moxie/core/utils/Events", + "moxie/runtime/html5/image/ResizerCanvas", + ]); + })(this); +}); diff --git a/assets/public/static/js/uploader/plupload.dev.js b/assets/public/static/js/uploader/plupload.dev.js new file mode 100644 index 00000000..2e3affc9 --- /dev/null +++ b/assets/public/static/js/uploader/plupload.dev.js @@ -0,0 +1,2721 @@ +/** + * Plupload - multi-runtime File Uploader + * v2.3.1 + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + * + * Date: 2017-02-06 + */ +(function (global, factory) { + var extract = function () { + var ctx = {}; + factory.apply(ctx, arguments); + return ctx.plupload; + }; + + if (typeof define === "function" && define.amd) { + define("plupload", ["./moxie"], extract); + } else if (typeof module === "object" && module.exports) { + module.exports = extract(require("../../../js-sdk-master 2/src/moxie")); + } else { + global.plupload = extract(global.moxie); + } +})(this || window, function (moxie) { + /** + * Plupload.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + + (function (exports, o, undef) { + var delay = window.setTimeout; + var fileFilters = {}; + var u = o.core.utils; + var Runtime = o.runtime.Runtime; + + // convert plupload features to caps acceptable by mOxie + function normalizeCaps(settings) { + var features = settings.required_features, + caps = {}; + + function resolve(feature, value, strict) { + // Feature notation is deprecated, use caps (this thing here is required for backward compatibility) + var map = { + chunks: "slice_blob", + jpgresize: "send_binary_string", + pngresize: "send_binary_string", + progress: "report_upload_progress", + multi_selection: "select_multiple", + dragdrop: "drag_and_drop", + drop_element: "drag_and_drop", + headers: "send_custom_headers", + urlstream_upload: "send_binary_string", + canSendBinary: "send_binary", + triggerDialog: "summon_file_dialog", + }; + + if (map[feature]) { + caps[map[feature]] = value; + } else if (!strict) { + caps[feature] = value; + } + } + + if (typeof features === "string") { + plupload.each(features.split(/\s*,\s*/), function (feature) { + resolve(feature, true); + }); + } else if (typeof features === "object") { + plupload.each(features, function (value, feature) { + resolve(feature, value); + }); + } else if (features === true) { + // check settings for required features + if (settings.chunk_size && settings.chunk_size > 0) { + caps.slice_blob = true; + } + + if ( + !plupload.isEmptyObj(settings.resize) || + settings.multipart === false + ) { + caps.send_binary_string = true; + } + + if (settings.http_method) { + caps.use_http_method = settings.http_method; + } + + plupload.each(settings, function (value, feature) { + resolve(feature, !!value, true); // strict check + }); + } + + return caps; + } + + /** + * @module plupload + * @static + */ + var plupload = { + /** + * Plupload version will be replaced on build. + * + * @property VERSION + * @for Plupload + * @static + * @final + */ + VERSION: "2.3.1", + + /** + * The state of the queue before it has started and after it has finished + * + * @property STOPPED + * @static + * @final + */ + STOPPED: 1, + + /** + * Upload process is running + * + * @property STARTED + * @static + * @final + */ + STARTED: 2, + + /** + * File is queued for upload + * + * @property QUEUED + * @static + * @final + */ + QUEUED: 1, + + /** + * File is being uploaded + * + * @property UPLOADING + * @static + * @final + */ + UPLOADING: 2, + + /** + * File has failed to be uploaded + * + * @property FAILED + * @static + * @final + */ + FAILED: 4, + + /** + * File has been uploaded successfully + * + * @property DONE + * @static + * @final + */ + DONE: 5, + + // Error constants used by the Error event + + /** + * Generic error for example if an exception is thrown inside Silverlight. + * + * @property GENERIC_ERROR + * @static + * @final + */ + GENERIC_ERROR: -100, + + /** + * HTTP transport error. For example if the server produces a HTTP status other than 200. + * + * @property HTTP_ERROR + * @static + * @final + */ + HTTP_ERROR: -200, + + /** + * Generic I/O error. For example if it wasn't possible to open the file stream on local machine. + * + * @property IO_ERROR + * @static + * @final + */ + IO_ERROR: -300, + + /** + * @property SECURITY_ERROR + * @static + * @final + */ + SECURITY_ERROR: -400, + + /** + * Initialization error. Will be triggered if no runtime was initialized. + * + * @property INIT_ERROR + * @static + * @final + */ + INIT_ERROR: -500, + + /** + * File size error. If the user selects a file that is too large it will be blocked and an error of this type will be triggered. + * + * @property FILE_SIZE_ERROR + * @static + * @final + */ + FILE_SIZE_ERROR: -600, + + /** + * File extension error. If the user selects a file that isn't valid according to the filters setting. + * + * @property FILE_EXTENSION_ERROR + * @static + * @final + */ + FILE_EXTENSION_ERROR: -601, + + /** + * Duplicate file error. If prevent_duplicates is set to true and user selects the same file again. + * + * @property FILE_DUPLICATE_ERROR + * @static + * @final + */ + FILE_DUPLICATE_ERROR: -602, + + /** + * Runtime will try to detect if image is proper one. Otherwise will throw this error. + * + * @property IMAGE_FORMAT_ERROR + * @static + * @final + */ + IMAGE_FORMAT_ERROR: -700, + + /** + * While working on files runtime may run out of memory and will throw this error. + * + * @since 2.1.2 + * @property MEMORY_ERROR + * @static + * @final + */ + MEMORY_ERROR: -701, + + /** + * Each runtime has an upper limit on a dimension of the image it can handle. If bigger, will throw this error. + * + * @property IMAGE_DIMENSIONS_ERROR + * @static + * @final + */ + IMAGE_DIMENSIONS_ERROR: -702, + + /** + * Mime type lookup table. + * + * @property mimeTypes + * @type Object + * @final + */ + mimeTypes: u.Mime.mimes, + + /** + * In some cases sniffing is the only way around :( + */ + ua: u.Env, + + /** + * Gets the true type of the built-in object (better version of typeof). + * @credits Angus Croll (http://javascriptweblog.wordpress.com/) + * + * @method typeOf + * @static + * @param {Object} o Object to check. + * @return {String} Object [[Class]] + */ + typeOf: u.Basic.typeOf, + + /** + * Extends the specified object with another object. + * + * @method extend + * @static + * @param {Object} target Object to extend. + * @param {Object..} obj Multiple objects to extend with. + * @return {Object} Same as target, the extended object. + */ + extend: u.Basic.extend, + + /** + * Generates an unique ID. This is 99.99% unique since it takes the current time and 5 random numbers. + * The only way a user would be able to get the same ID is if the two persons at the same exact millisecond manages + * to get 5 the same random numbers between 0-65535 it also uses a counter so each call will be guaranteed to be page unique. + * It's more probable for the earth to be hit with an asteriod. You can also if you want to be 100% sure set the plupload.guidPrefix property + * to an user unique key. + * + * @method guid + * @static + * @return {String} Virtually unique id. + */ + guid: u.Basic.guid, + + /** + * Get array of DOM Elements by their ids. + * + * @method get + * @param {String} id Identifier of the DOM Element + * @return {Array} + */ + getAll: function get(ids) { + var els = [], + el; + + if (plupload.typeOf(ids) !== "array") { + ids = [ids]; + } + + var i = ids.length; + while (i--) { + el = plupload.get(ids[i]); + if (el) { + els.push(el); + } + } + + return els.length ? els : null; + }, + + /** + Get DOM element by id + + @method get + @param {String} id Identifier of the DOM Element + @return {Node} + */ + get: u.Dom.get, + + /** + * Executes the callback function for each item in array/object. If you return false in the + * callback it will break the loop. + * + * @method each + * @static + * @param {Object} obj Object to iterate. + * @param {function} callback Callback function to execute for each item. + */ + each: u.Basic.each, + + /** + * Returns the absolute x, y position of an Element. The position will be returned in a object with x, y fields. + * + * @method getPos + * @static + * @param {Element} node HTML element or element id to get x, y position from. + * @param {Element} root Optional root element to stop calculations at. + * @return {object} Absolute position of the specified element object with x, y fields. + */ + getPos: u.Dom.getPos, + + /** + * Returns the size of the specified node in pixels. + * + * @method getSize + * @static + * @param {Node} node Node to get the size of. + * @return {Object} Object with a w and h property. + */ + getSize: u.Dom.getSize, + + /** + * Encodes the specified string. + * + * @method xmlEncode + * @static + * @param {String} s String to encode. + * @return {String} Encoded string. + */ + xmlEncode: function (str) { + var xmlEncodeChars = { + "<": "lt", + ">": "gt", + "&": "amp", + '"': "quot", + "'": "#39", + }, + xmlEncodeRegExp = /[<>&\"\']/g; + + return str + ? ("" + str).replace(xmlEncodeRegExp, function (chr) { + return xmlEncodeChars[chr] + ? "&" + xmlEncodeChars[chr] + ";" + : chr; + }) + : str; + }, + + /** + * Forces anything into an array. + * + * @method toArray + * @static + * @param {Object} obj Object with length field. + * @return {Array} Array object containing all items. + */ + toArray: u.Basic.toArray, + + /** + * Find an element in array and return its index if present, otherwise return -1. + * + * @method inArray + * @static + * @param {mixed} needle Element to find + * @param {Array} array + * @return {Int} Index of the element, or -1 if not found + */ + inArray: u.Basic.inArray, + + /** + Recieve an array of functions (usually async) to call in sequence, each function + receives a callback as first argument that it should call, when it completes. Finally, + after everything is complete, main callback is called. Passing truthy value to the + callback as a first argument will interrupt the sequence and invoke main callback + immediately. + + @method inSeries + @static + @param {Array} queue Array of functions to call in sequence + @param {Function} cb Main callback that is called in the end, or in case of error + */ + inSeries: u.Basic.inSeries, + + /** + * Extends the language pack object with new items. + * + * @method addI18n + * @static + * @param {Object} pack Language pack items to add. + * @return {Object} Extended language pack object. + */ + addI18n: o.core.I18n.addI18n, + + /** + * Translates the specified string by checking for the english string in the language pack lookup. + * + * @method translate + * @static + * @param {String} str String to look for. + * @return {String} Translated string or the input string if it wasn't found. + */ + translate: o.core.I18n.translate, + + /** + * Pseudo sprintf implementation - simple way to replace tokens with specified values. + * + * @param {String} str String with tokens + * @return {String} String with replaced tokens + */ + sprintf: u.Basic.sprintf, + + /** + * Checks if object is empty. + * + * @method isEmptyObj + * @static + * @param {Object} obj Object to check. + * @return {Boolean} + */ + isEmptyObj: u.Basic.isEmptyObj, + + /** + * Checks if specified DOM element has specified class. + * + * @method hasClass + * @static + * @param {Object} obj DOM element like object to add handler to. + * @param {String} name Class name + */ + hasClass: u.Dom.hasClass, + + /** + * Adds specified className to specified DOM element. + * + * @method addClass + * @static + * @param {Object} obj DOM element like object to add handler to. + * @param {String} name Class name + */ + addClass: u.Dom.addClass, + + /** + * Removes specified className from specified DOM element. + * + * @method removeClass + * @static + * @param {Object} obj DOM element like object to add handler to. + * @param {String} name Class name + */ + removeClass: u.Dom.removeClass, + + /** + * Returns a given computed style of a DOM element. + * + * @method getStyle + * @static + * @param {Object} obj DOM element like object. + * @param {String} name Style you want to get from the DOM element + */ + getStyle: u.Dom.getStyle, + + /** + * Adds an event handler to the specified object and store reference to the handler + * in objects internal Plupload registry (@see removeEvent). + * + * @method addEvent + * @static + * @param {Object} obj DOM element like object to add handler to. + * @param {String} name Name to add event listener to. + * @param {Function} callback Function to call when event occurs. + * @param {String} (optional) key that might be used to add specifity to the event record. + */ + addEvent: u.Events.addEvent, + + /** + * Remove event handler from the specified object. If third argument (callback) + * is not specified remove all events with the specified name. + * + * @method removeEvent + * @static + * @param {Object} obj DOM element to remove event listener(s) from. + * @param {String} name Name of event listener to remove. + * @param {Function|String} (optional) might be a callback or unique key to match. + */ + removeEvent: u.Events.removeEvent, + + /** + * Remove all kind of events from the specified object + * + * @method removeAllEvents + * @static + * @param {Object} obj DOM element to remove event listeners from. + * @param {String} (optional) unique key to match, when removing events. + */ + removeAllEvents: u.Events.removeAllEvents, + + /** + * Cleans the specified name from national characters (diacritics). The result will be a name with only a-z, 0-9 and _. + * + * @method cleanName + * @static + * @param {String} s String to clean up. + * @return {String} Cleaned string. + */ + cleanName: function (name) { + var i, lookup; + + // Replace diacritics + lookup = [ + /[\300-\306]/g, + "A", + /[\340-\346]/g, + "a", + /\307/g, + "C", + /\347/g, + "c", + /[\310-\313]/g, + "E", + /[\350-\353]/g, + "e", + /[\314-\317]/g, + "I", + /[\354-\357]/g, + "i", + /\321/g, + "N", + /\361/g, + "n", + /[\322-\330]/g, + "O", + /[\362-\370]/g, + "o", + /[\331-\334]/g, + "U", + /[\371-\374]/g, + "u", + ]; + + for (i = 0; i < lookup.length; i += 2) { + name = name.replace(lookup[i], lookup[i + 1]); + } + + // Replace whitespace + name = name.replace(/\s+/g, "_"); + + // Remove anything else + name = name.replace(/[^a-z0-9_\-\.]+/gi, ""); + + return name; + }, + + /** + * Builds a full url out of a base URL and an object with items to append as query string items. + * + * @method buildUrl + * @static + * @param {String} url Base URL to append query string items to. + * @param {Object} items Name/value object to serialize as a querystring. + * @return {String} String with url + serialized query string items. + */ + buildUrl: function (url, items) { + var query = ""; + + plupload.each(items, function (value, name) { + query += + (query ? "&" : "") + + encodeURIComponent(name) + + "=" + + encodeURIComponent(value); + }); + + if (query) { + url += (url.indexOf("?") > 0 ? "&" : "?") + query; + } + + return url; + }, + + /** + * Formats the specified number as a size string for example 1024 becomes 1 KB. + * + * @method formatSize + * @static + * @param {Number} size Size to format as string. + * @return {String} Formatted size string. + */ + formatSize: function (size) { + if (size === undef || /\D/.test(size)) { + return plupload.translate("N/A"); + } + + function round(num, precision) { + return ( + Math.round(num * Math.pow(10, precision)) / + Math.pow(10, precision) + ); + } + + var boundary = Math.pow(1024, 4); + + // TB + if (size > boundary) { + return ( + round(size / boundary, 1) + + " " + + plupload.translate("tb") + ); + } + + // GB + if (size > (boundary /= 1024)) { + return ( + round(size / boundary, 1) + + " " + + plupload.translate("gb") + ); + } + + // MB + if (size > (boundary /= 1024)) { + return ( + round(size / boundary, 1) + + " " + + plupload.translate("mb") + ); + } + + // KB + if (size > 1024) { + return ( + Math.round(size / 1024) + " " + plupload.translate("kb") + ); + } + + return size + " " + plupload.translate("b"); + }, + + /** + * Parses the specified size string into a byte value. For example 10kb becomes 10240. + * + * @method parseSize + * @static + * @param {String|Number} size String to parse or number to just pass through. + * @return {Number} Size in bytes. + */ + parseSize: u.Basic.parseSizeStr, + + /** + * A way to predict what runtime will be choosen in the current environment with the + * specified settings. + * + * @method predictRuntime + * @static + * @param {Object|String} config Plupload settings to check + * @param {String} [runtimes] Comma-separated list of runtimes to check against + * @return {String} Type of compatible runtime + */ + predictRuntime: function (config, runtimes) { + var up, runtime; + + up = new plupload.Uploader(config); + runtime = Runtime.thatCan( + up.getOption().required_features, + runtimes || config.runtimes + ); + up.destroy(); + return runtime; + }, + + /** + * Registers a filter that will be executed for each file added to the queue. + * If callback returns false, file will not be added. + * + * Callback receives two arguments: a value for the filter as it was specified in settings.filters + * and a file to be filtered. Callback is executed in the context of uploader instance. + * + * @method addFileFilter + * @static + * @param {String} name Name of the filter by which it can be referenced in settings.filters + * @param {String} cb Callback - the actual routine that every added file must pass + */ + addFileFilter: function (name, cb) { + fileFilters[name] = cb; + }, + }; + + plupload.addFileFilter("mime_types", function (filters, file, cb) { + if (filters.length && !filters.regexp.test(file.name)) { + this.trigger("Error", { + code: plupload.FILE_EXTENSION_ERROR, + message: plupload.translate("File extension error."), + file: file, + }); + cb(false); + } else { + cb(true); + } + }); + + plupload.addFileFilter("max_file_size", function (maxSize, file, cb) { + var undef; + + maxSize = plupload.parseSize(maxSize); + + // Invalid file size + if (file.size !== undef && maxSize && file.size > maxSize) { + this.trigger("Error", { + code: plupload.FILE_SIZE_ERROR, + message: plupload.translate("File size error."), + file: file, + }); + cb(false); + } else { + cb(true); + } + }); + + plupload.addFileFilter( + "prevent_duplicates", + function (value, file, cb) { + if (value) { + var ii = this.files.length; + while (ii--) { + // Compare by name and size (size might be 0 or undefined, but still equivalent for both) + if ( + file.name === this.files[ii].name && + file.size === this.files[ii].size + ) { + this.trigger("Error", { + code: plupload.FILE_DUPLICATE_ERROR, + message: plupload.translate( + "Duplicate file error." + ), + file: file, + }); + cb(false); + return; + } + } + } + cb(true); + } + ); + + /** +@class Uploader +@constructor + +@param {Object} settings For detailed information about each option check documentation. + @param {String|DOMElement} settings.browse_button id of the DOM element or DOM element itself to use as file dialog trigger. + @param {Number|String} [settings.chunk_size=0] Chunk size in bytes to slice the file into. Shorcuts with b, kb, mb, gb, tb suffixes also supported. `e.g. 204800 or "204800b" or "200kb"`. By default - disabled. + @param {String|DOMElement} [settings.container] id of the DOM element or DOM element itself that will be used to wrap uploader structures. Defaults to immediate parent of the `browse_button` element. + @param {String|DOMElement} [settings.drop_element] id of the DOM element or DOM element itself to use as a drop zone for Drag-n-Drop. + @param {String} [settings.file_data_name="file"] Name for the file field in Multipart formated message. + @param {Object} [settings.filters={}] Set of file type filters. + @param {String|Number} [settings.filters.max_file_size=0] Maximum file size that the user can pick, in bytes. Optionally supports b, kb, mb, gb, tb suffixes. `e.g. "10mb" or "1gb"`. By default - not set. Dispatches `plupload.FILE_SIZE_ERROR`. + @param {Array} [settings.filters.mime_types=[]] List of file types to accept, each one defined by title and list of extensions. `e.g. {title : "Image files", extensions : "jpg,jpeg,gif,png"}`. Dispatches `plupload.FILE_EXTENSION_ERROR` + @param {Boolean} [settings.filters.prevent_duplicates=false] Do not let duplicates into the queue. Dispatches `plupload.FILE_DUPLICATE_ERROR`. + @param {String} [settings.flash_swf_url] URL of the Flash swf. + @param {Object} [settings.headers] Custom headers to send with the upload. Hash of name/value pairs. + @param {String} [settings.http_method="POST"] HTTP method to use during upload (only PUT or POST allowed). + @param {Number} [settings.max_retries=0] How many times to retry the chunk or file, before triggering Error event. + @param {Boolean} [settings.multipart=true] Whether to send file and additional parameters as Multipart formated message. + @param {Object} [settings.multipart_params] Hash of key/value pairs to send with every file upload. + @param {Boolean} [settings.multi_selection=true] Enable ability to select multiple files at once in file dialog. + @param {String|Object} [settings.required_features] Either comma-separated list or hash of required features that chosen runtime should absolutely possess. + @param {Object} [settings.resize] Enable resizng of images on client-side. Applies to `image/jpeg` and `image/png` only. `e.g. {width : 200, height : 200, quality : 90, crop: true}` + @param {Number} [settings.resize.width] If image is bigger, it will be resized. + @param {Number} [settings.resize.height] If image is bigger, it will be resized. + @param {Number} [settings.resize.quality=90] Compression quality for jpegs (1-100). + @param {Boolean} [settings.resize.crop=false] Whether to crop images to exact dimensions. By default they will be resized proportionally. + @param {String} [settings.runtimes="html5,flash,silverlight,html4"] Comma separated list of runtimes, that Plupload will try in turn, moving to the next if previous fails. + @param {String} [settings.silverlight_xap_url] URL of the Silverlight xap. + @param {Boolean} [settings.send_chunk_number=true] Whether to send chunks and chunk numbers, or total and offset bytes. + @param {Boolean} [settings.send_file_name=true] Whether to send file name as additional argument - 'name' (required for chunked uploads and some other cases where file name cannot be sent via normal ways). + @param {String} settings.url URL of the server-side upload handler. + @param {Boolean} [settings.unique_names=false] If true will generate unique filenames for uploaded files. + +*/ + plupload.Uploader = function (options) { + /** + Fires when the current RunTime has been initialized. + + @event Init + @param {plupload.Uploader} uploader Uploader instance sending the event. + */ + + /** + Fires after the init event incase you need to perform actions there. + + @event PostInit + @param {plupload.Uploader} uploader Uploader instance sending the event. + */ + + /** + Fires when the option is changed in via uploader.setOption(). + + @event OptionChanged + @since 2.1 + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {String} name Name of the option that was changed + @param {Mixed} value New value for the specified option + @param {Mixed} oldValue Previous value of the option + */ + + /** + Fires when the silverlight/flash or other shim needs to move. + + @event Refresh + @param {plupload.Uploader} uploader Uploader instance sending the event. + */ + + /** + Fires when the overall state is being changed for the upload queue. + + @event StateChanged + @param {plupload.Uploader} uploader Uploader instance sending the event. + */ + + /** + Fires when browse_button is clicked and browse dialog shows. + + @event Browse + @since 2.1.2 + @param {plupload.Uploader} uploader Uploader instance sending the event. + */ + + /** + Fires for every filtered file before it is added to the queue. + + @event FileFiltered + @since 2.1 + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {plupload.File} file Another file that has to be added to the queue. + */ + + /** + Fires when the file queue is changed. In other words when files are added/removed to the files array of the uploader instance. + + @event QueueChanged + @param {plupload.Uploader} uploader Uploader instance sending the event. + */ + + /** + Fires after files were filtered and added to the queue. + + @event FilesAdded + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {Array} files Array of file objects that were added to queue by the user. + */ + + /** + Fires when file is removed from the queue. + + @event FilesRemoved + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {Array} files Array of files that got removed. + */ + + /** + Fires just before a file is uploaded. Can be used to cancel the upload for the specified file + by returning false from the handler. + + @event BeforeUpload + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {plupload.File} file File to be uploaded. + */ + + /** + Fires when a file is to be uploaded by the runtime. + + @event UploadFile + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {plupload.File} file File to be uploaded. + */ + + /** + Fires while a file is being uploaded. Use this event to update the current file upload progress. + + @event UploadProgress + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {plupload.File} file File that is currently being uploaded. + */ + + /** + * Fires just before a chunk is uploaded. This event enables you to override settings + * on the uploader instance before the chunk is uploaded. + * + * @event BeforeChunkUpload + * @param {plupload.Uploader} uploader Uploader instance sending the event. + * @param {plupload.File} file File to be uploaded. + * @param {Object} args POST params to be sent. + * @param {Blob} chunkBlob Current blob. + * @param {offset} offset Current offset. + */ + + /** + Fires when file chunk is uploaded. + + @event ChunkUploaded + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {plupload.File} file File that the chunk was uploaded for. + @param {Object} result Object with response properties. + @param {Number} result.offset The amount of bytes the server has received so far, including this chunk. + @param {Number} result.total The size of the file. + @param {String} result.response The response body sent by the server. + @param {Number} result.status The HTTP status code sent by the server. + @param {String} result.responseHeaders All the response headers as a single string. + */ + + /** + Fires when a file is successfully uploaded. + + @event FileUploaded + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {plupload.File} file File that was uploaded. + @param {Object} result Object with response properties. + @param {String} result.response The response body sent by the server. + @param {Number} result.status The HTTP status code sent by the server. + @param {String} result.responseHeaders All the response headers as a single string. + */ + + /** + Fires when all files in a queue are uploaded. + + @event UploadComplete + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {Array} files Array of file objects that was added to queue/selected by the user. + */ + + /** + Fires when a error occurs. + + @event Error + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {Object} error Contains code, message and sometimes file and other details. + @param {Number} error.code The plupload error code. + @param {String} error.message Description of the error (uses i18n). + */ + + /** + Fires when destroy method is called. + + @event Destroy + @param {plupload.Uploader} uploader Uploader instance sending the event. + */ + var uid = plupload.guid(), + settings, + files = [], + preferred_caps = {}, + fileInputs = [], + fileDrops = [], + startTime, + total, + disabled = false, + xhr; + + // Private methods + function uploadNext() { + var file, + count = 0, + i; + + if (this.state == plupload.STARTED) { + // Find first QUEUED file + for (i = 0; i < files.length; i++) { + if (!file && files[i].status == plupload.QUEUED) { + file = files[i]; + if (this.trigger("BeforeUpload", file)) { + file.status = plupload.UPLOADING; + this.trigger("UploadFile", file); + } + } else { + count++; + } + } + + // All files are DONE or FAILED + if (count == files.length) { + if (this.state !== plupload.STOPPED) { + this.state = plupload.STOPPED; + this.trigger("StateChanged"); + } + this.trigger("UploadComplete", files); + } + } + } + + function calcFile(file) { + file.percent = + file.size > 0 + ? Math.ceil((file.loaded / file.size) * 100) + : 100; + calc(); + } + + function calc() { + var i, file; + var loaded; + var loadedDuringCurrentSession = 0; + + // Reset stats + total.reset(); + + // Check status, size, loaded etc on all files + for (i = 0; i < files.length; i++) { + file = files[i]; + + if (file.size !== undef) { + // We calculate totals based on original file size + total.size += file.origSize; + + // Since we cannot predict file size after resize, we do opposite and + // interpolate loaded amount to match magnitude of total + loaded = (file.loaded * file.origSize) / file.size; + + if ( + !file.completeTimestamp || + file.completeTimestamp > startTime + ) { + loadedDuringCurrentSession += loaded; + } + + total.loaded += loaded; + } else { + total.size = undef; + } + + if (file.status == plupload.DONE) { + total.uploaded++; + } else if (file.status == plupload.FAILED) { + total.failed++; + } else { + total.queued++; + } + } + + // If we couldn't calculate a total file size then use the number of files to calc percent + if (total.size === undef) { + total.percent = + files.length > 0 + ? Math.ceil((total.uploaded / files.length) * 100) + : 0; + } else { + total.bytesPerSec = Math.ceil( + loadedDuringCurrentSession / + ((+new Date() - startTime || 1) / 1000.0) + ); + total.percent = + total.size > 0 + ? Math.ceil((total.loaded / total.size) * 100) + : 0; + } + } + + function getRUID() { + var ctrl = fileInputs[0] || fileDrops[0]; + if (ctrl) { + return ctrl.getRuntime().uid; + } + return false; + } + + function runtimeCan(file, cap) { + if (file.ruid) { + var info = Runtime.getInfo(file.ruid); + if (info) { + return info.can(cap); + } + } + return false; + } + + function bindEventListeners() { + this.bind("FilesAdded FilesRemoved", function (up) { + up.trigger("QueueChanged"); + up.refresh(); + }); + + this.bind("CancelUpload", onCancelUpload); + + this.bind("BeforeUpload", onBeforeUpload); + + this.bind("UploadFile", onUploadFile); + + this.bind("UploadProgress", onUploadProgress); + + this.bind("StateChanged", onStateChanged); + + this.bind("QueueChanged", calc); + + this.bind("Error", onError); + + this.bind("FileUploaded", onFileUploaded); + + this.bind("Destroy", onDestroy); + } + + function initControls(settings, cb) { + var self = this, + inited = 0, + queue = []; + + // common settings + var options = { + runtime_order: settings.runtimes, + required_caps: settings.required_features, + preferred_caps: preferred_caps, + swf_url: settings.flash_swf_url, + xap_url: settings.silverlight_xap_url, + }; + + // add runtime specific options if any + plupload.each( + settings.runtimes.split(/\s*,\s*/), + function (runtime) { + if (settings[runtime]) { + options[runtime] = settings[runtime]; + } + } + ); + + let buttonIndex = 0; + // initialize file pickers - there can be many + if (settings.browse_button) { + plupload.each(settings.browse_button, function (el) { + queue.push(function (cb) { + buttonIndex++; + if (buttonIndex === 1) { + var fileInput = new o.file.FileInput( + plupload.extend({}, options, { + accept: settings.filters.mime_types, + name: settings.file_data_name, + multiple: false, + directory: true, + container: settings.container, + browse_button: el, + }) + ); + } else { + var fileInput = new o.file.FileInput( + plupload.extend({}, options, { + accept: settings.filters.mime_types, + name: settings.file_data_name, + multiple: settings.multi_selection, + container: settings.container, + browse_button: el, + }) + ); + } + + fileInput.onready = function () { + var info = Runtime.getInfo(this.ruid); + + // for backward compatibility + plupload.extend(self.features, { + chunks: info.can("slice_blob"), + multipart: info.can("send_multipart"), + multi_selection: info.can( + "select_multiple" + ), + }); + + inited++; + fileInputs.push(this); + cb(); + }; + + fileInput.onchange = function () { + self.addFile(this.files); + }; + + fileInput.bind( + "mouseenter mouseleave mousedown mouseup", + function (e) { + if (!disabled) { + if (settings.browse_button_hover) { + if ("mouseenter" === e.type) { + plupload.addClass( + el, + settings.browse_button_hover + ); + } else if ( + "mouseleave" === e.type + ) { + plupload.removeClass( + el, + settings.browse_button_hover + ); + } + } + + if (settings.browse_button_active) { + if ("mousedown" === e.type) { + plupload.addClass( + el, + settings.browse_button_active + ); + } else if ("mouseup" === e.type) { + plupload.removeClass( + el, + settings.browse_button_active + ); + } + } + } + } + ); + + fileInput.bind("mousedown", function () { + self.trigger("Browse"); + }); + + fileInput.bind("error runtimeerror", function () { + fileInput = null; + cb(); + }); + + fileInput.init(); + }); + }); + } + + // initialize drop zones + if (settings.drop_element) { + plupload.each(settings.drop_element, function (el) { + queue.push(function (cb) { + var fileDrop = new o.file.FileDrop( + plupload.extend({}, options, { + drop_zone: el, + }) + ); + + fileDrop.onready = function () { + var info = Runtime.getInfo(this.ruid); + + // for backward compatibility + plupload.extend(self.features, { + chunks: info.can("slice_blob"), + multipart: info.can("send_multipart"), + dragdrop: info.can("drag_and_drop"), + }); + + inited++; + fileDrops.push(this); + cb(); + }; + + fileDrop.ondrop = function () { + self.addFile(this.files); + }; + + fileDrop.bind("error runtimeerror", function () { + fileDrop = null; + cb(); + }); + + fileDrop.init(); + }); + }); + } + + plupload.inSeries(queue, function () { + if (typeof cb === "function") { + cb(inited); + } + }); + } + + function resizeImage(blob, params, cb) { + var img = new o.image.Image(); + + try { + img.onload = function () { + // no manipulation required if... + if ( + params.width > this.width && + params.height > this.height && + params.quality === undef && + params.preserve_headers && + !params.crop + ) { + this.destroy(); + return cb(blob); + } + // otherwise downsize + img.downsize( + params.width, + params.height, + params.crop, + params.preserve_headers + ); + }; + + img.onresize = function () { + cb(this.getAsBlob(blob.type, params.quality)); + this.destroy(); + }; + + img.onerror = function () { + cb(blob); + }; + + img.load(blob); + } catch (ex) { + cb(blob); + } + } + + function setOption(option, value, init) { + var self = this, + reinitRequired = false; + + function _setOption(option, value, init) { + var oldValue = settings[option]; + + switch (option) { + case "max_file_size": + if (option === "max_file_size") { + settings.max_file_size = settings.filters.max_file_size = value; + } + break; + + case "chunk_size": + if ((value = plupload.parseSize(value))) { + settings[option] = value; + settings.send_file_name = true; + } + break; + + case "multipart": + settings[option] = value; + // if (!value) { + // settings.send_file_name = true; + // } + break; + + case "http_method": + settings[option] = + value.toUpperCase() === "PUT" ? "PUT" : "POST"; + break; + + case "unique_names": + settings[option] = value; + if (value) { + settings.send_file_name = true; + } + break; + + case "filters": + // for sake of backward compatibility + if (plupload.typeOf(value) === "array") { + value = { + mime_types: value, + }; + } + + if (init) { + plupload.extend(settings.filters, value); + } else { + settings.filters = value; + } + + // if file format filters are being updated, regenerate the matching expressions + if (value.mime_types) { + if ( + plupload.typeOf(value.mime_types) === + "string" + ) { + value.mime_types = o.core.utils.Mime.mimes2extList( + value.mime_types + ); + } + + value.mime_types.regexp = (function (filters) { + var extensionsRegExp = []; + + plupload.each(filters, function (filter) { + plupload.each( + filter.extensions.split(/,/), + function (ext) { + if (/^\s*\*\s*$/.test(ext)) { + extensionsRegExp.push( + "\\.*" + ); + } else { + extensionsRegExp.push( + "\\." + + ext.replace( + new RegExp( + "[" + + "/^$.*+?|()[]{}\\".replace( + /./g, + "\\$&" + ) + + "]", + "g" + ), + "\\$&" + ) + ); + } + } + ); + }); + + return new RegExp( + "(" + extensionsRegExp.join("|") + ")$", + "i" + ); + })(value.mime_types); + + settings.filters.mime_types = value.mime_types; + } + break; + + case "resize": + if (value) { + settings.resize = plupload.extend( + { + preserve_headers: true, + crop: false, + }, + value + ); + } else { + settings.resize = false; + } + break; + + case "prevent_duplicates": + settings.prevent_duplicates = settings.filters.prevent_duplicates = !!value; + break; + + // options that require reinitialisation + case "container": + case "browse_button": + case "drop_element": + value = + "container" === option + ? plupload.get(value) + : plupload.getAll(value); + + case "runtimes": + case "multi_selection": + case "flash_swf_url": + case "silverlight_xap_url": + settings[option] = value; + if (!init) { + reinitRequired = true; + } + break; + + default: + settings[option] = value; + } + + if (!init) { + self.trigger("OptionChanged", option, value, oldValue); + } + } + + if (typeof option === "object") { + plupload.each(option, function (value, option) { + _setOption(option, value, init); + }); + } else { + _setOption(option, value, init); + } + + if (init) { + // Normalize the list of required capabilities + settings.required_features = normalizeCaps( + plupload.extend({}, settings) + ); + + // Come up with the list of capabilities that can affect default mode in a multi-mode runtimes + preferred_caps = normalizeCaps( + plupload.extend({}, settings, { + required_features: true, + }) + ); + } else if (reinitRequired) { + self.trigger("Destroy"); + + initControls.call(self, settings, function (inited) { + if (inited) { + self.runtime = Runtime.getInfo(getRUID()).type; + self.trigger("Init", { runtime: self.runtime }); + self.trigger("PostInit"); + } else { + self.trigger("Error", { + code: plupload.INIT_ERROR, + message: plupload.translate("Init error."), + }); + } + }); + } + } + + // Internal event handlers + function onBeforeUpload(up, file) { + // Generate unique target filenames + if (up.settings.unique_names) { + var matches = file.name.match(/\.([^.]+)$/), + ext = "part"; + if (matches) { + ext = matches[1]; + } + file.target_name = file.id + "." + ext; + } + } + + function onUploadFile(up, file) { + var url = up.settings.url, + chunkSize = up.settings.chunk_size, + retries = up.settings.max_retries, + features = up.features, + offset = 0, + blob; + + // make sure we start at a predictable offset + if (file.loaded) { + offset = file.loaded = chunkSize + ? chunkSize * Math.floor(file.loaded / chunkSize) + : 0; + } + + function handleError(err = null) { + if (retries-- > 0) { + delay(uploadNextChunk, 1000); + } else { + file.loaded = offset; // reset all progress + + up.trigger("Error", { + code: plupload.HTTP_ERROR, + message: + err == null + ? plupload.translate("HTTP Error.") + : err, + file: file, + response: xhr.responseText, + status: xhr.status, + responseHeaders: xhr.getAllResponseHeaders(), + }); + } + } + + function uploadNextChunk() { + var chunkBlob, + args = {}, + curChunkSize; + + // make sure that file wasn't cancelled and upload is not stopped in general + if ( + file.status !== plupload.UPLOADING || + up.state === plupload.STOPPED + ) { + return; + } + + // send additional 'name' parameter only if required + if (up.settings.send_file_name) { + args.name = file.target_name || file.name; + } + + if (chunkSize && features.chunks && blob.size > chunkSize) { + // blob will be of type string if it was loaded in memory + curChunkSize = Math.min(chunkSize, blob.size - offset); + chunkBlob = blob.slice(offset, offset + curChunkSize); + } else { + curChunkSize = blob.size; + chunkBlob = blob; + } + + // If chunking is enabled add corresponding args, no matter if file is bigger than chunk or smaller + if (chunkSize && features.chunks) { + // Setup query string arguments + if (up.settings.send_chunk_number) { + args.chunk = Math.ceil(offset / chunkSize); + args.chunks = Math.ceil(blob.size / chunkSize); + } else { + // keep support for experimental chunk format, just in case + args.offset = offset; + args.total = blob.size; + } + } + + if ( + up.trigger( + "BeforeChunkUpload", + file, + args, + chunkBlob, + offset + ) + ) { + uploadChunk(args, chunkBlob, curChunkSize); + } + } + + function uploadChunk(args, chunkBlob, curChunkSize) { + var formData; + + xhr = new o.xhr.XMLHttpRequest(); + + // todo 自动变换此项设置 + //xhr.withCredentials = true; + + // Do we have upload progress support + if (xhr.upload) { + xhr.upload.onprogress = function (e) { + file.loaded = Math.min( + file.size, + offset + e.loaded + ); + up.trigger("UploadProgress", file); + }; + } + + xhr.onload = function () { + // check if upload made itself through + if (xhr.status >= 400) { + handleError(); + return; + } + if (xhr.status == 203) { + handleError(); + return; + } + + // 本地策略/远程/OSS需要读取错误代码以出发错误 + if ( + uploadConfig.saveType === "local" || + uploadConfig.saveType === "remote" || + uploadConfig.saveType === "oss" || + (uploadConfig.saveType === "onedrive" && + file.size <= 4 * 1024 * 1024) + ) { + var res = JSON.parse(xhr.responseText); + if (res.code !== 0) { + handleError(res.msg); + return; + } + } + + retries = up.settings.max_retries; // reset the counter + + // Handle chunk response + if (curChunkSize < blob.size) { + chunkBlob.destroy(); + + offset += curChunkSize; + file.loaded = Math.min(offset, blob.size); + + up.trigger("ChunkUploaded", file, { + offset: file.loaded, + total: blob.size, + response: xhr.responseText, + status: xhr.status, + responseHeaders: xhr.getAllResponseHeaders(), + }); + + // stock Android browser doesn't fire upload progress events, but in chunking mode we can fake them + if (plupload.ua.browser === "Android Browser") { + // doesn't harm in general, but is not required anywhere else + up.trigger("UploadProgress", file); + } + } else { + file.loaded = file.size; + } + + chunkBlob = formData = null; // Free memory + + // Check if file is uploaded + if (!offset || offset >= blob.size) { + // If file was modified, destory the copy + if (file.size != file.origSize) { + blob.destroy(); + blob = null; + } + + up.trigger("UploadProgress", file); + + file.status = plupload.DONE; + file.completeTimestamp = +new Date(); + + up.trigger("FileUploaded", file, { + response: xhr.responseText, + status: xhr.status, + responseHeaders: xhr.getAllResponseHeaders(), + }); + } else { + // Still chunks left + delay(uploadNextChunk, 1); // run detached, otherwise event handlers interfere + } + }; + + xhr.onerror = function () { + handleError(); + }; + + xhr.onloadend = function () { + this.destroy(); + xhr = null; + }; + + // Build multipart request + if (up.settings.multipart && features.multipart) { + xhr.open(up.settings.http_method, url, true); + + // Set custom headers + plupload.each( + up.settings.headers, + function (value, name) { + xhr.setRequestHeader(name, value); + } + ); + + formData = new o.xhr.FormData(); + + // Add multipart params + plupload.each( + plupload.extend(args, up.settings.multipart_params), + function (value, name) { + formData.append(name, value); + } + ); + + // Add file and send it + formData.append(up.settings.file_data_name, chunkBlob); + xhr.send(formData, { + runtime_order: up.settings.runtimes, + required_caps: up.settings.required_features, + preferred_caps: preferred_caps, + swf_url: up.settings.flash_swf_url, + xap_url: up.settings.silverlight_xap_url, + }); + } else { + // if no multipart, send as binary stream + url = plupload.buildUrl( + up.settings.url, + plupload.extend(args, up.settings.multipart_params) + ); + + xhr.open(up.settings.http_method, url, true); + + // Set custom headers + plupload.each( + up.settings.headers, + function (value, name) { + xhr.setRequestHeader(name, value); + } + ); + + // do not set Content-Type, if it was defined previously (see #1203) + if (!xhr.hasRequestHeader("Content-Type")) { + xhr.setRequestHeader( + "Content-Type", + "application/octet-stream" + ); // Binary stream header + } + + xhr.send(chunkBlob, { + runtime_order: up.settings.runtimes, + required_caps: up.settings.required_features, + preferred_caps: preferred_caps, + swf_url: up.settings.flash_swf_url, + xap_url: up.settings.silverlight_xap_url, + }); + } + } + + blob = file.getSource(); + + // Start uploading chunks + if ( + !plupload.isEmptyObj(up.settings.resize) && + runtimeCan(blob, "send_binary_string") && + plupload.inArray(blob.type, ["image/jpeg", "image/png"]) !== + -1 + ) { + // Resize if required + resizeImage.call( + this, + blob, + up.settings.resize, + function (resizedBlob) { + blob = resizedBlob; + file.size = resizedBlob.size; + uploadNextChunk(); + } + ); + } else { + uploadNextChunk(); + } + } + + function onUploadProgress(up, file) { + calcFile(file); + } + + function onStateChanged(up) { + if (up.state == plupload.STARTED) { + // Get start time to calculate bps + startTime = +new Date(); + } else if (up.state == plupload.STOPPED) { + // Reset currently uploading files + for (var i = up.files.length - 1; i >= 0; i--) { + if (up.files[i].status == plupload.UPLOADING) { + up.files[i].status = plupload.QUEUED; + calc(); + } + } + } + } + + function onCancelUpload() { + if (xhr) { + xhr.abort(); + } + } + + function onFileUploaded(up) { + calc(); + + // Upload next file but detach it from the error event + // since other custom listeners might want to stop the queue + delay(function () { + uploadNext.call(up); + }, 1); + } + + function onError(up, err) { + if (err.code === plupload.INIT_ERROR) { + up.destroy(); + } + // Set failed status if an error occured on a file + else if (err.code === plupload.HTTP_ERROR) { + err.file.status = plupload.FAILED; + err.file.completeTimestamp = +new Date(); + calcFile(err.file); + + // Upload next file but detach it from the error event + // since other custom listeners might want to stop the queue + if (up.state == plupload.STARTED) { + // upload in progress + up.trigger("CancelUpload"); + delay(function () { + uploadNext.call(up); + }, 1); + } + } + } + + function onDestroy(up) { + up.stop(); + + // Purge the queue + plupload.each(files, function (file) { + file.destroy(); + }); + files = []; + + if (fileInputs.length) { + plupload.each(fileInputs, function (fileInput) { + fileInput.destroy(); + }); + fileInputs = []; + } + + if (fileDrops.length) { + plupload.each(fileDrops, function (fileDrop) { + fileDrop.destroy(); + }); + fileDrops = []; + } + + preferred_caps = {}; + disabled = false; + startTime = xhr = null; + total.reset(); + } + + // Default settings + settings = { + chunk_size: 0, + file_data_name: "file", + filters: { + mime_types: [], + prevent_duplicates: false, + max_file_size: 0, + }, + flash_swf_url: "js/Moxie.swf", + http_method: "POST", + max_retries: 0, + multipart: true, + multi_selection: true, + resize: false, + runtimes: Runtime.order, + send_file_name: true, + send_chunk_number: true, + silverlight_xap_url: "js/Moxie.xap", + }; + + setOption.call(this, options, null, true); + + // Inital total state + total = new plupload.QueueProgress(); + + // Add public methods + plupload.extend(this, { + /** + * Unique id for the Uploader instance. + * + * @property id + * @type String + */ + id: uid, + uid: uid, // mOxie uses this to differentiate between event targets + + /** + * Current state of the total uploading progress. This one can either be plupload.STARTED or plupload.STOPPED. + * These states are controlled by the stop/start methods. The default value is STOPPED. + * + * @property state + * @type Number + */ + state: plupload.STOPPED, + + /** + * Map of features that are available for the uploader runtime. Features will be filled + * before the init event is called, these features can then be used to alter the UI for the end user. + * Some of the current features that might be in this map is: dragdrop, chunks, jpgresize, pngresize. + * + * @property features + * @type Object + */ + features: {}, + + /** + * Current runtime name. + * + * @property runtime + * @type String + */ + runtime: null, + + /** + * Current upload queue, an array of File instances. + * + * @property files + * @type Array + * @see plupload.File + */ + files: files, + + /** + * Object with name/value settings. + * + * @property settings + * @type Object + */ + settings: settings, + + /** + * Total progess information. How many files has been uploaded, total percent etc. + * + * @property total + * @type plupload.QueueProgress + */ + total: total, + + /** + * Initializes the Uploader instance and adds internal event listeners. + * + * @method init + */ + init: function () { + var self = this, + opt, + preinitOpt, + err; + + preinitOpt = self.getOption("preinit"); + if (typeof preinitOpt == "function") { + preinitOpt(self); + } else { + plupload.each(preinitOpt, function (func, name) { + self.bind(name, func); + }); + } + + bindEventListeners.call(self); + + // Check for required options + plupload.each( + ["container", "browse_button", "drop_element"], + function (el) { + if (self.getOption(el) === null) { + err = { + code: plupload.INIT_ERROR, + message: plupload.sprintf( + plupload.translate( + "%s specified, but cannot be found." + ), + el + ), + }; + return false; + } + } + ); + + if (err) { + return self.trigger("Error", err); + } + + if (!settings.browse_button && !settings.drop_element) { + return self.trigger("Error", { + code: plupload.INIT_ERROR, + message: plupload.translate( + "You must specify either browse_button or drop_element." + ), + }); + } + + initControls.call(self, settings, function (inited) { + var initOpt = self.getOption("init"); + if (typeof initOpt == "function") { + initOpt(self); + } else { + plupload.each(initOpt, function (func, name) { + self.bind(name, func); + }); + } + + if (inited) { + self.runtime = Runtime.getInfo(getRUID()).type; + self.trigger("Init", { runtime: self.runtime }); + self.trigger("PostInit"); + } else { + self.trigger("Error", { + code: plupload.INIT_ERROR, + message: plupload.translate("Init error."), + }); + } + }); + }, + + /** + * Set the value for the specified option(s). + * + * @method setOption + * @since 2.1 + * @param {String|Object} option Name of the option to change or the set of key/value pairs + * @param {Mixed} [value] Value for the option (is ignored, if first argument is object) + */ + setOption: function (option, value) { + setOption.call(this, option, value, !this.runtime); // until runtime not set we do not need to reinitialize + }, + + /** + * Get the value for the specified option or the whole configuration, if not specified. + * + * @method getOption + * @since 2.1 + * @param {String} [option] Name of the option to get + * @return {Mixed} Value for the option or the whole set + */ + getOption: function (option) { + if (!option) { + return settings; + } + return settings[option]; + }, + + /** + * Refreshes the upload instance by dispatching out a refresh event to all runtimes. + * This would for example reposition flash/silverlight shims on the page. + * + * @method refresh + */ + refresh: function () { + if (fileInputs.length) { + plupload.each(fileInputs, function (fileInput) { + fileInput.trigger("Refresh"); + }); + } + this.trigger("Refresh"); + }, + + /** + * Starts uploading the queued files. + * + * @method start + */ + start: function () { + if (this.state != plupload.STARTED) { + this.state = plupload.STARTED; + this.trigger("StateChanged"); + + uploadNext.call(this); + } + }, + + /** + * Stops the upload of the queued files. + * + * @method stop + */ + stop: function () { + if (this.state != plupload.STOPPED) { + this.state = plupload.STOPPED; + this.trigger("StateChanged"); + this.trigger("CancelUpload"); + } + }, + + /** + * Disables/enables browse button on request. + * + * @method disableBrowse + * @param {Boolean} disable Whether to disable or enable (default: true) + */ + disableBrowse: function () { + disabled = arguments[0] !== undef ? arguments[0] : true; + + if (fileInputs.length) { + plupload.each(fileInputs, function (fileInput) { + fileInput.disable(disabled); + }); + } + + this.trigger("DisableBrowse", disabled); + }, + + /** + * Returns the specified file object by id. + * + * @method getFile + * @param {String} id File id to look for. + * @return {plupload.File} File object or undefined if it wasn't found; + */ + getFile: function (id) { + var i; + for (i = files.length - 1; i >= 0; i--) { + if (files[i].id === id) { + return files[i]; + } + } + }, + + /** + * Adds file to the queue programmatically. Can be native file, instance of Plupload.File, + * instance of mOxie.File, input[type="file"] element, or array of these. Fires FilesAdded, + * if any files were added to the queue. Otherwise nothing happens. + * + * @method addFile + * @since 2.0 + * @param {plupload.File|mOxie.File|File|Node|Array} file File or files to add to the queue. + * @param {String} [fileName] If specified, will be used as a name for the file + */ + addFile: function (file, fileName) { + var self = this, + queue = [], + filesAdded = [], + ruid; + + function filterFile(file, cb) { + var queue = []; + plupload.each( + self.settings.filters, + function (rule, name) { + if (fileFilters[name]) { + queue.push(function (cb) { + fileFilters[name].call( + self, + rule, + file, + function (res) { + cb(!res); + } + ); + }); + } + } + ); + plupload.inSeries(queue, cb); + } + + /** + * @method resolveFile + * @private + * @param {moxie.file.File|moxie.file.Blob|plupload.File|File|Blob|input[type="file"]} file + */ + function resolveFile(file) { + var type = plupload.typeOf(file); + + // moxie.file.File + if (file instanceof o.file.File) { + if (!file.ruid && !file.isDetached()) { + if (!ruid) { + // weird case + return false; + } + file.ruid = ruid; + file.connectRuntime(ruid); + } + resolveFile(new plupload.File(file)); + } + // moxie.file.Blob + else if (file instanceof o.file.Blob) { + resolveFile(file.getSource()); + file.destroy(); + } + // plupload.File - final step for other branches + else if (file instanceof plupload.File) { + if (fileName) { + file.name = fileName; + } + + queue.push(function (cb) { + // run through the internal and user-defined filters, if any + filterFile(file, function (err) { + if (!err) { + // make files available for the filters by updating the main queue directly + files.push(file); + // collect the files that will be passed to FilesAdded event + filesAdded.push(file); + + self.trigger("FileFiltered", file); + } + delay(cb, 1); // do not build up recursions or eventually we might hit the limits + }); + }); + } + // native File or blob + else if ( + plupload.inArray(type, ["file", "blob"]) !== -1 + ) { + resolveFile(new o.file.File(null, file)); + } + // input[type="file"] + else if ( + type === "node" && + plupload.typeOf(file.files) === "filelist" + ) { + // if we are dealing with input[type="file"] + plupload.each(file.files, resolveFile); + } + // mixed array of any supported types (see above) + else if (type === "array") { + fileName = null; // should never happen, but unset anyway to avoid funny situations + plupload.each(file, resolveFile); + } + } + + ruid = getRUID(); + + resolveFile(file); + + if (queue.length) { + plupload.inSeries(queue, function () { + // if any files left after filtration, trigger FilesAdded + if (filesAdded.length) { + self.trigger("FilesAdded", filesAdded); + } + }); + } + }, + + /** + * Removes a specific file. + * + * @method removeFile + * @param {plupload.File|String} file File to remove from queue. + */ + removeFile: function (file) { + var id = typeof file === "string" ? file : file.id; + + for (var i = files.length - 1; i >= 0; i--) { + if (files[i].id === id) { + return this.splice(i, 1)[0]; + } + } + }, + + /** + * Removes part of the queue and returns the files removed. This will also trigger the FilesRemoved and QueueChanged events. + * + * @method splice + * @param {Number} start (Optional) Start index to remove from. + * @param {Number} length (Optional) Lengh of items to remove. + * @return {Array} Array of files that was removed. + */ + splice: function (start, length) { + // Splice and trigger events + var removed = files.splice( + start === undef ? 0 : start, + length === undef ? files.length : length + ); + + // if upload is in progress we need to stop it and restart after files are removed + var restartRequired = false; + if (this.state == plupload.STARTED) { + // upload in progress + plupload.each(removed, function (file) { + if (file.status === plupload.UPLOADING) { + restartRequired = true; // do not restart, unless file that is being removed is uploading + return false; + } + }); + + if (restartRequired) { + this.stop(); + } + } + + this.trigger("FilesRemoved", removed); + + // Dispose any resources allocated by those files + plupload.each(removed, function (file) { + file.destroy(); + }); + + if (restartRequired) { + this.start(); + } + + return removed; + }, + + /** + Dispatches the specified event name and its arguments to all listeners. + + @method trigger + @param {String} name Event name to fire. + @param {Object..} Multiple arguments to pass along to the listener functions. + */ + + // override the parent method to match Plupload-like event logic + dispatchEvent: function (type) { + var list, args, result; + + type = type.toLowerCase(); + + list = this.hasEventListener(type); + + if (list) { + // sort event list by priority + list.sort(function (a, b) { + return b.priority - a.priority; + }); + + // first argument should be current plupload.Uploader instance + args = [].slice.call(arguments); + args.shift(); + args.unshift(this); + + for (var i = 0; i < list.length; i++) { + // Fire event, break chain if false is returned + if ( + list[i].fn.apply(list[i].scope, args) === false + ) { + return false; + } + } + } + return true; + }, + + /** + Check whether uploader has any listeners to the specified event. + + @method hasEventListener + @param {String} name Event name to check for. + */ + + /** + Adds an event listener by name. + + @method bind + @param {String} name Event name to listen for. + @param {function} fn Function to call ones the event gets fired. + @param {Object} [scope] Optional scope to execute the specified function in. + @param {Number} [priority=0] Priority of the event handler - handlers with higher priorities will be called first + */ + bind: function (name, fn, scope, priority) { + // adapt moxie EventTarget style to Plupload-like + plupload.Uploader.prototype.bind.call( + this, + name, + fn, + priority, + scope + ); + }, + + /** + Removes the specified event listener. + + @method unbind + @param {String} name Name of event to remove. + @param {function} fn Function to remove from listener. + */ + + /** + Removes all event listeners. + + @method unbindAll + */ + + /** + * Destroys Plupload instance and cleans after itself. + * + * @method destroy + */ + destroy: function () { + this.trigger("Destroy"); + settings = total = null; // purge these exclusively + this.unbindAll(); + }, + }); + }; + + plupload.Uploader.prototype = o.core.EventTarget.instance; + + /** + * Constructs a new file instance. + * + * @class File + * @constructor + * + * @param {Object} file Object containing file properties + * @param {String} file.name Name of the file. + * @param {Number} file.size File size. + */ + plupload.File = (function () { + var filepool = {}; + + function PluploadFile(file) { + plupload.extend(this, { + /** + * File id this is a globally unique id for the specific file. + * + * @property id + * @type String + */ + id: plupload.guid(), + + /** + * File name for example "myfile.gif". + * + * @property name + * @type String + */ + name: file.name || file.fileName, + + /** + * File type, `e.g image/jpeg` + * + * @property type + * @type String + */ + type: file.type || "", + + /** + * File size in bytes (may change after client-side manupilation). + * + * @property size + * @type Number + */ + size: file.size || file.fileSize, + + /** + * Original file size in bytes. + * + * @property origSize + * @type Number + */ + origSize: file.size || file.fileSize, + + /** + * Number of bytes uploaded of the files total size. + * + * @property loaded + * @type Number + */ + loaded: 0, + + /** + * Number of percentage uploaded of the file. + * + * @property percent + * @type Number + */ + percent: 0, + + /** + * Status constant matching the plupload states QUEUED, UPLOADING, FAILED, DONE. + * + * @property status + * @type Number + * @see plupload + */ + status: plupload.QUEUED, + + /** + * Date of last modification. + * + * @property lastModifiedDate + * @type {String} + */ + lastModifiedDate: + file.lastModifiedDate || new Date().toLocaleString(), // Thu Aug 23 2012 19:40:00 GMT+0400 (GET) + + /** + * Set when file becomes plupload.DONE or plupload.FAILED. Is used to calculate proper plupload.QueueProgress.bytesPerSec. + * @private + * @property completeTimestamp + * @type {Number} + */ + completeTimestamp: 0, + + /** + * Returns native window.File object, when it's available. + * + * @method getNative + * @return {window.File} or null, if plupload.File is of different origin + */ + getNative: function () { + var file = this.getSource().getSource(); + return plupload.inArray(plupload.typeOf(file), [ + "blob", + "file", + ]) !== -1 + ? file + : null; + }, + + /** + * Returns mOxie.File - unified wrapper object that can be used across runtimes. + * + * @method getSource + * @return {mOxie.File} or null + */ + getSource: function () { + if (!filepool[this.id]) { + return null; + } + return filepool[this.id]; + }, + + /** + * Destroys plupload.File object. + * + * @method destroy + */ + destroy: function () { + var src = this.getSource(); + if (src) { + src.destroy(); + delete filepool[this.id]; + } + }, + }); + + filepool[this.id] = file; + } + + return PluploadFile; + })(); + + /** + * Constructs a queue progress. + * + * @class QueueProgress + * @constructor + */ + plupload.QueueProgress = function () { + var self = this; // Setup alias for self to reduce code size when it's compressed + + /** + * Total queue file size. + * + * @property size + * @type Number + */ + self.size = 0; + + /** + * Total bytes uploaded. + * + * @property loaded + * @type Number + */ + self.loaded = 0; + + /** + * Number of files uploaded. + * + * @property uploaded + * @type Number + */ + self.uploaded = 0; + + /** + * Number of files failed to upload. + * + * @property failed + * @type Number + */ + self.failed = 0; + + /** + * Number of files yet to be uploaded. + * + * @property queued + * @type Number + */ + self.queued = 0; + + /** + * Total percent of the uploaded bytes. + * + * @property percent + * @type Number + */ + self.percent = 0; + + /** + * Bytes uploaded per second. + * + * @property bytesPerSec + * @type Number + */ + self.bytesPerSec = 0; + + /** + * Resets the progress to its initial values. + * + * @method reset + */ + self.reset = function () { + self.size = self.loaded = self.uploaded = self.failed = self.queued = self.percent = self.bytesPerSec = 0; + }; + }; + + exports.plupload = plupload; + })(this, moxie); +}); diff --git a/assets/public/static/js/uploader/ui.js b/assets/public/static/js/uploader/ui.js new file mode 100644 index 00000000..f0856157 --- /dev/null +++ b/assets/public/static/js/uploader/ui.js @@ -0,0 +1,323 @@ +/*global plupload */ +/*global qiniu */ +function FileProgress(file, targetID) { + this.fileProgressID = file.id; + this.file = file; + + this.opacity = 100; + this.height = 0; + this.fileProgressWrapper = $("#" + this.fileProgressID); + if (!this.fileProgressWrapper.length) { + //
+ //
+ // 20% Complete + //
+ //
+ + this.fileProgressWrapper = $(""); + var Wrappeer = this.fileProgressWrapper; + Wrappeer.attr("id", this.fileProgressID).addClass("progressContainer"); + + var progressText = $(""); + progressText.addClass("progressName").text(file.name); + + var fileSize = plupload.formatSize(file.size).toUpperCase(); + var progressSize = $(""); + progressSize.addClass("progressFileSize").text(fileSize); + + var progressBarTd = $(""); + var progressBarBox = $("
"); + progressBarBox.addClass("info"); + var progressBarWrapper = $("
"); + progressBarWrapper.addClass("progress progress-striped"); + + var progressBar = $("
"); + progressBar + .addClass("progress-bar progress-bar-info") + .attr("role", "progressbar") + .attr("aria-valuemax", 100) + .attr("aria-valuenow", 0) + .attr("aria-valuein", 0) + .width("0%"); + + var progressBarPercent = $(""); + progressBarPercent.text(fileSize); + + var progressCancel = $(""); + progressCancel.show().addClass("progressCancel").text("×"); + + progressBar.append(progressBarPercent); + progressBarWrapper.append(progressBar); + progressBarBox.append(progressBarWrapper); + progressText.append(progressCancel); + + var progressBarStatus = $('
'); + progressBarBox.append(progressBarStatus); + progressBarTd.append(progressBarBox); + + Wrappeer.append(progressText); + Wrappeer.append(progressSize); + Wrappeer.append(progressBarTd); + + $("#" + targetID).append(Wrappeer); + } else { + this.reset(); + } + + this.height = this.fileProgressWrapper.offset().top; + this.setTimer(null); +} + +FileProgress.prototype.setTimer = function (timer) { + this.fileProgressWrapper.FP_TIMER = timer; +}; + +FileProgress.prototype.getTimer = function (timer) { + return this.fileProgressWrapper.FP_TIMER || null; +}; + +FileProgress.prototype.reset = function () { + this.fileProgressWrapper.attr("class", "progressContainer"); + this.fileProgressWrapper + .find("td .progress .progress-bar-info") + .attr("aria-valuenow", 0) + .width("0%") + .find("span") + .text(""); + this.appear(); +}; + +FileProgress.prototype.setChunkProgess = function (chunk_size) { + var chunk_amount = Math.ceil(this.file.size / chunk_size); + if (chunk_amount === 1) { + return false; + } + + var viewProgess = $( + '' + ); + + var progressBarChunkTr = $( + '' + ); + var progressBarChunk = $("
"); + for (var i = 1; i <= chunk_amount; i++) { + var col = $('
'); + var progressBarWrapper = $( + '
"); + progressBar + .addClass("progress-bar progress-bar-info text-left") + .attr("role", "progressbar") + .attr("aria-valuemax", 100) + .attr("aria-valuenow", 0) + .attr("aria-valuein", 0) + .width("0%") + .attr("id", this.file.id + "_" + i) + .text(""); + + var progressBarStatus = $(""); + progressBarStatus.addClass("chunk-status").text(); + + progressBarWrapper.append(progressBar); + progressBarWrapper.append(progressBarStatus); + + col.append(progressBarWrapper); + progressBarChunk.append(col); + } + + if (!this.fileProgressWrapper.find("td:eq(2) .btn-default").length) { + this.fileProgressWrapper.find("td>div").append(viewProgess); + } + progressBarChunkTr.hide().find("td").append(progressBarChunk); + progressBarChunkTr.insertAfter(this.fileProgressWrapper); +}; + +FileProgress.prototype.setProgress = function (percentage, speed, chunk_size) { + this.fileProgressWrapper.attr("class", "progressContainer green"); + + var file = this.file; + var uploaded = file.loaded; + + var size = plupload.formatSize(uploaded).toUpperCase(); + var formatSpeed = plupload.formatSize(speed).toUpperCase(); + var progressbar = this.fileProgressWrapper + .find("td .progress") + .find(".progress-bar-info"); + if (this.fileProgressWrapper.find(".status").text() === "取消上传") { + return; + } + this.fileProgressWrapper + .find(".status") + .text("已上传: " + size + " 上传速度: " + formatSpeed + "/s"); + percentage = parseInt(percentage, 10); + if (file.status !== plupload.DONE && percentage === 100) { + percentage = 99; + } + + progressbar + .attr("aria-valuenow", percentage) + .css("width", percentage + "%"); + + if (chunk_size) { + var chunk_amount = Math.ceil(file.size / chunk_size); + if (chunk_amount === 1) { + return false; + } + var current_uploading_chunk = Math.ceil(uploaded / chunk_size); + var pre_chunk, text; + + for (var index = 0; index < current_uploading_chunk; index++) { + pre_chunk = $("#" + file.id + "_" + index); + pre_chunk + .width("100%") + .removeClass() + .addClass("alert alert-success") + .attr("aria-valuenow", 100); + text = "块" + index + "上传进度100%"; + pre_chunk.next().html(text); + } + + var currentProgessBar = $( + "#" + file.id + "_" + current_uploading_chunk + ); + var current_chunk_percent; + if (current_uploading_chunk < chunk_amount) { + if (uploaded % chunk_size) { + current_chunk_percent = ( + ((uploaded % chunk_size) / chunk_size) * + 100 + ).toFixed(2); + } else { + current_chunk_percent = 100; + currentProgessBar.removeClass().addClass("alert alert-success"); + } + } else { + var last_chunk_size = file.size - chunk_size * (chunk_amount - 1); + var left_file_size = file.size - uploaded; + if (left_file_size % last_chunk_size) { + current_chunk_percent = ( + ((uploaded % chunk_size) / last_chunk_size) * + 100 + ).toFixed(2); + } else { + current_chunk_percent = 100; + currentProgessBar.removeClass().addClass("alert alert-success"); + } + } + currentProgessBar.width(current_chunk_percent + "%"); + currentProgessBar.attr("aria-valuenow", current_chunk_percent); + text = + "块" + + current_uploading_chunk + + "上传进度" + + current_chunk_percent + + "%"; + currentProgessBar.next().html(text); + } + + this.appear(); +}; + +FileProgress.prototype.setComplete = function (up, info) { + var td = this.fileProgressWrapper.find("td:eq(2)"), + tdProgress = td.find(".progress"); + + var res; + var url; + if (uploadConfig.saveType == "oss") { + url = "oss"; + str = "
上传成功
"; + } else { + res = $.parseJSON(info); + if (res.url) { + url = res.url; + str = "
上传成功
"; + } else { + var domain = up.getOption("domain"); + url = domain + encodeURI(res.key); + var link = domain + res.key; + str = "
上传成功
"; + } + } + + tdProgress.html(str).removeClass().next().next(".status").hide(); + this.fileProgressWrapper.find("td:eq(0) .progressCancel").hide(); + td.find(".status").hide(); + angular + .element(document.querySelector("angular-filemanager > div")) + .scope() + .fileNavigator.refresh(); +}; +FileProgress.prototype.setError = function () { + this.fileProgressWrapper.find("td:eq(2)").attr("class", "text-warning"); + this.fileProgressWrapper.find("td:eq(2) .progress").css("width", 0).hide(); + this.fileProgressWrapper.find("button").hide(); + this.fileProgressWrapper.next(".chunk-status-tr").hide(); +}; + +FileProgress.prototype.setCancelled = function (manual) { + var progressContainer = "progressContainer"; + if (!manual) { + progressContainer += " red"; + } + this.fileProgressWrapper.attr("class", progressContainer); + this.fileProgressWrapper.find("td .progress").remove(); + this.fileProgressWrapper.find("td:eq(2) .btn-default").hide(); + this.fileProgressWrapper.find("td:eq(0) .progressCancel").hide(); +}; + +FileProgress.prototype.setStatus = function (status, isUploading) { + if (!isUploading) { + this.fileProgressWrapper + .find(".status") + .text(status) + .attr("class", "status text-left"); + } +}; + +// 绑定取消上传事件 +FileProgress.prototype.bindUploadCancel = function (up) { + var self = this; + if (up) { + self.fileProgressWrapper + .find("td:eq(0) .progressCancel") + .on("click", function () { + self.setCancelled(false); + self.setStatus("取消上传"); + self.fileProgressWrapper.find(".status").css("left", "0"); + up.removeFile(self.file); + }); + } +}; + +FileProgress.prototype.appear = function () { + if (this.getTimer() !== null) { + clearTimeout(this.getTimer()); + this.setTimer(null); + } + + if (this.fileProgressWrapper[0].filters) { + try { + this.fileProgressWrapper[0].filters.item( + "DXImageTransform.Microsoft.Alpha" + ).opacity = 100; + } catch (e) { + // If it is not set initially, the browser will throw an error. This will set it if it is not set yet. + this.fileProgressWrapper.css( + "filter", + "progid:DXImageTransform.Microsoft.Alpha(opacity=100)" + ); + } + } else { + this.fileProgressWrapper.css("opacity", 1); + } + + this.fileProgressWrapper.css("height", ""); + + this.height = this.fileProgressWrapper.offset().top; + this.opacity = 100; + this.fileProgressWrapper.show(); +}; diff --git a/assets/public/static/js/uploader/uploader_cos.js b/assets/public/static/js/uploader/uploader_cos.js new file mode 100644 index 00000000..49fbe19a --- /dev/null +++ b/assets/public/static/js/uploader/uploader_cos.js @@ -0,0 +1,1719 @@ +/*! + * qiniu-js-sdk v@VERSION + * + * Copyright 2015 by Qiniu + * Released under GPL V2 License. + * + * GitHub: http://github.com/qiniu/js-sdk + * + * Date: @DATE + */ +/*! + * + * Rebuild By Aaron@2018 + * + */ +/*global plupload ,mOxie*/ +/*global ActiveXObject */ +/*exported Qiniu */ +/*exported QiniuJsSDK */ + +function getCookieByString(cookieName) { + var start = document.cookie.indexOf(cookieName + "="); + if (start == -1) return false; + start = start + cookieName.length + 1; + var end = document.cookie.indexOf(";", start); + if (end == -1) end = document.cookie.length; + return document.cookie.substring(start, end); +} +(function (global) { + /** + * Creates new cookie or removes cookie with negative expiration + * @param key The key or identifier for the store + * @param value Contents of the store + * @param exp Expiration - creation defaults to 30 days + */ + function createCookie(key, value, exp) { + var date = new Date(); + date.setTime(date.getTime() + exp * 24 * 60 * 60 * 1000); + var expires = "; expires=" + date.toGMTString(); + document.cookie = key + "=" + value + expires + "; path=/"; + } + + /** + * Returns contents of cookie + * @param key The key or identifier for the store + */ + function readCookie(key) { + var nameEQ = key + "="; + var ca = document.cookie.split(";"); + for (var i = 0, max = ca.length; i < max; i++) { + var c = ca[i]; + while (c.charAt(0) === " ") { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return c.substring(nameEQ.length, c.length); + } + } + return null; + } + + // if current browser is not support localStorage + // use cookie to make a polyfill + if (!window.localStorage) { + window.localStorage = { + setItem: function (key, value) { + createCookie(key, value, 30); + }, + getItem: function (key) { + return readCookie(key); + }, + removeItem: function (key) { + createCookie(key, "", -1); + }, + }; + } + + function QiniuJsSDK() { + var that = this; + + /** + * detect IE version + * if current browser is not IE + * it will return false + * else + * it will return version of current IE browser + * @return {Number|Boolean} IE version or false + */ + this.detectIEVersion = function () { + var v = 4, + div = document.createElement("div"), + all = div.getElementsByTagName("i"); + while ( + ((div.innerHTML = + ""), + all[0]) + ) { + v++; + } + return v > 4 ? v : false; + }; + + var logger = { + MUTE: 0, + FATA: 1, + ERROR: 2, + WARN: 3, + INFO: 4, + DEBUG: 5, + TRACE: 6, + level: 0, + }; + + function log(type, args) { + var header = "[Cloudreve-uploader][" + type + "]"; + var msg = header; + for (var i = 0; i < args.length; i++) { + if (typeof args[i] === "string") { + msg += " " + args[i]; + } else { + msg += " " + that.stringifyJSON(args[i]); + } + } + if (that.detectIEVersion()) { + // http://stackoverflow.com/questions/5538972/console-log-apply-not-working-in-ie9 + //var log = Function.prototype.bind.call(console.log, console); + //log.apply(console, args); + console.log(msg); + } else { + args.unshift(header); + console.log.apply(console, args); + } + if (document.getElementById("qiniu-js-sdk-log")) { + document.getElementById("qiniu-js-sdk-log").innerHTML += + "

" + msg + "

"; + } + } + + function makeLogFunc(code) { + var func = code.toLowerCase(); + logger[func] = function () { + // logger[func].history = logger[func].history || []; + // logger[func].history.push(arguments); + if ( + window.console && + window.console.log && + logger.level >= logger[code] + ) { + var args = Array.prototype.slice.call(arguments); + log(func, args); + } + }; + } + + for (var property in logger) { + if ( + logger.hasOwnProperty(property) && + typeof logger[property] === "number" && + !logger.hasOwnProperty(property.toLowerCase()) + ) { + makeLogFunc(property); + } + } + + var qiniuUploadUrl; + + /** + * qiniu upload urls + * 'qiniuUploadUrls' is used to change target when current url is not avaliable + * @type {Array} + */ + + qiniuUploadUrl = uploadConfig.upUrl; + var qiniuUploadUrls = [uploadConfig.upUrl]; + var qiniuUpHosts = { + http: [uploadConfig.upUrl], + https: [uploadConfig.upUrl], + }; + + var changeUrlTimes = 0; + + /** + * reset upload url + * if current page protocal is https + * it will always return 'https://up.qbox.me' + * else + * it will set 'qiniuUploadUrl' value with 'qiniuUploadUrls' looply + */ + this.resetUploadUrl = function () { + var hosts = + window.location.protocol === "https:" + ? qiniuUpHosts.https + : qiniuUpHosts.http; + var i = changeUrlTimes % hosts.length; + qiniuUploadUrl = hosts[i]; + changeUrlTimes++; + logger.debug("resetUploadUrl: " + qiniuUploadUrl); + }; + + // this.resetUploadUrl(); + + /** + * is image + * @param {String} url of a file + * @return {Boolean} file is a image or not + */ + this.isImage = function (url) { + url = url.split(/[?#]/)[0]; + return /\.(png|jpg|jpeg|gif|bmp)$/i.test(url); + }; + + /** + * get file extension + * @param {String} filename + * @return {String} file extension + * @example + * input: test.txt + * output: txt + */ + this.getFileExtension = function (filename) { + var tempArr = filename.split("."); + var ext; + if ( + tempArr.length === 1 || + (tempArr[0] === "" && tempArr.length === 2) + ) { + ext = ""; + } else { + ext = tempArr.pop().toLowerCase(); //get the extension and make it lower-case + } + return ext; + }; + + /** + * encode string by utf8 + * @param {String} string to encode + * @return {String} encoded string + */ + this.utf8_encode = function (argString) { + // http://kevin.vanzonneveld.net + // + original by: Webtoolkit.info (http://www.webtoolkit.info/) + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + improved by: sowberry + // + tweaked by: Jack + // + bugfixed by: Onno Marsman + // + improved by: Yves Sucaet + // + bugfixed by: Onno Marsman + // + bugfixed by: Ulrich + // + bugfixed by: Rafal Kukawski + // + improved by: kirilloid + // + bugfixed by: kirilloid + // * example 1: this.utf8_encode('Kevin van Zonneveld'); + // * returns 1: 'Kevin van Zonneveld' + + if (argString === null || typeof argString === "undefined") { + return ""; + } + + var string = argString + ""; // .replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + var utftext = "", + start, + end, + stringl = 0; + + start = end = 0; + stringl = string.length; + for (var n = 0; n < stringl; n++) { + var c1 = string.charCodeAt(n); + var enc = null; + + if (c1 < 128) { + end++; + } else if (c1 > 127 && c1 < 2048) { + enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); + } else if ((c1 & 0xf800) ^ (0xd800 > 0)) { + enc = String.fromCharCode( + (c1 >> 12) | 224, + ((c1 >> 6) & 63) | 128, + (c1 & 63) | 128 + ); + } else { + // surrogate pairs + if ((c1 & 0xfc00) ^ (0xd800 > 0)) { + throw new RangeError( + "Unmatched trail surrogate at " + n + ); + } + var c2 = string.charCodeAt(++n); + if ((c2 & 0xfc00) ^ (0xdc00 > 0)) { + throw new RangeError( + "Unmatched lead surrogate at " + (n - 1) + ); + } + c1 = ((c1 & 0x3ff) << 10) + (c2 & 0x3ff) + 0x10000; + enc = String.fromCharCode( + (c1 >> 18) | 240, + ((c1 >> 12) & 63) | 128, + ((c1 >> 6) & 63) | 128, + (c1 & 63) | 128 + ); + } + if (enc !== null) { + if (end > start) { + utftext += string.slice(start, end); + } + utftext += enc; + start = end = n + 1; + } + } + + if (end > start) { + utftext += string.slice(start, stringl); + } + + return utftext; + }; + + this.base64_decode = function (data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Thunder.m + // + input by: Aman Gupta + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Onno Marsman + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: Brett Zamir (http://brett-zamir.me) + // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA=='); + // * returns 1: 'Kevin van Zonneveld' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + var b64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, + o2, + o3, + h1, + h2, + h3, + h4, + bits, + i = 0, + ac = 0, + dec = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data += ""; + + do { + // unpack four hexets into three octets using index points in b64 + h1 = b64.indexOf(data.charAt(i++)); + h2 = b64.indexOf(data.charAt(i++)); + h3 = b64.indexOf(data.charAt(i++)); + h4 = b64.indexOf(data.charAt(i++)); + + bits = (h1 << 18) | (h2 << 12) | (h3 << 6) | h4; + + o1 = (bits >> 16) & 0xff; + o2 = (bits >> 8) & 0xff; + o3 = bits & 0xff; + + if (h3 === 64) { + tmp_arr[ac++] = String.fromCharCode(o1); + } else if (h4 === 64) { + tmp_arr[ac++] = String.fromCharCode(o1, o2); + } else { + tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); + } + } while (i < data.length); + + dec = tmp_arr.join(""); + + return dec; + }; + + /** + * encode data by base64 + * @param {String} data to encode + * @return {String} encoded data + */ + this.base64_encode = function (data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Bayron Guevara + // + improved by: Thunder.m + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // - depends on: this.utf8_encode + // * example 1: this.base64_encode('Kevin van Zonneveld'); + // * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA==' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + var b64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, + o2, + o3, + h1, + h2, + h3, + h4, + bits, + i = 0, + ac = 0, + enc = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data = this.utf8_encode(data + ""); + + do { + // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = (o1 << 16) | (o2 << 8) | o3; + + h1 = (bits >> 18) & 0x3f; + h2 = (bits >> 12) & 0x3f; + h3 = (bits >> 6) & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = + b64.charAt(h1) + + b64.charAt(h2) + + b64.charAt(h3) + + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(""); + + switch (data.length % 3) { + case 1: + enc = enc.slice(0, -2) + "=="; + break; + case 2: + enc = enc.slice(0, -1) + "="; + break; + } + + return enc; + }; + + /** + * encode string in url by base64 + * @param {String} string in url + * @return {String} encoded string + */ + this.URLSafeBase64Encode = function (v) { + v = this.base64_encode(v); + return v.replace(/\//g, "_").replace(/\+/g, "-"); + }; + + this.URLSafeBase64Decode = function (v) { + v = v.replace(/_/g, "/").replace(/-/g, "+"); + return this.base64_decode(v); + }; + + // TODO: use mOxie + /** + * craete object used to AJAX + * @return {Object} + */ + this.createAjax = function (argument) { + var xmlhttp = {}; + if (window.XMLHttpRequest) { + xmlhttp = new XMLHttpRequest(); + } else { + xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + return xmlhttp; + }; + + // TODO: enhance IE compatibility + /** + * parse json string to javascript object + * @param {String} json string + * @return {Object} object + */ + this.parseJSON = function (data) { + // Attempt to parse using the native JSON parser first + if (window.JSON && window.JSON.parse) { + return window.JSON.parse(data); + } + + //var rx_one = /^[\],:{}\s]*$/, + // rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + // rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + // rx_four = /(?:^|:|,)(?:\s*\[)+/g, + var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + + //var json; + + var text = String(data); + rx_dangerous.lastIndex = 0; + if (rx_dangerous.test(text)) { + text = text.replace(rx_dangerous, function (a) { + return ( + "\\u" + + ("0000" + a.charCodeAt(0).toString(16)).slice(-4) + ); + }); + } + + // todo 使用一下判断,增加安全性 + //if ( + // rx_one.test( + // text + // .replace(rx_two, '@') + // .replace(rx_three, ']') + // .replace(rx_four, '') + // ) + //) { + // return eval('(' + text + ')'); + //} + + return eval("(" + text + ")"); + }; + + /** + * parse javascript object to json string + * @param {Object} object + * @return {String} json string + */ + this.stringifyJSON = function (obj) { + // Attempt to parse using the native JSON parser first + if (window.JSON && window.JSON.stringify) { + return window.JSON.stringify(obj); + } + switch (typeof obj) { + case "string": + return '"' + obj.replace(/(["\\])/g, "\\$1") + '"'; + case "array": + return "[" + obj.map(that.stringifyJSON).join(",") + "]"; + case "object": + if (obj instanceof Array) { + var strArr = []; + var len = obj.length; + for (var i = 0; i < len; i++) { + strArr.push(that.stringifyJSON(obj[i])); + } + return "[" + strArr.join(",") + "]"; + } else if (obj === null) { + return "null"; + } else { + var string = []; + for (var property in obj) { + if (obj.hasOwnProperty(property)) { + string.push( + that.stringifyJSON(property) + + ":" + + that.stringifyJSON(obj[property]) + ); + } + } + return "{" + string.join(",") + "}"; + } + break; + case "number": + return obj; + case false: + return obj; + case "boolean": + return obj; + } + }; + + /** + * trim space beside text + * @param {String} untrimed string + * @return {String} trimed string + */ + this.trim = function (text) { + return text === null ? "" : text.replace(/^\s+|\s+$/g, ""); + }; + + /** + * create a uploader by QiniuJsSDK + * @param {object} options to create a new uploader + * @return {object} uploader + */ + this.uploader = function (op) { + /********** inner function define start **********/ + + // according the different condition to reset chunk size + // and the upload strategy according with the chunk size + // when chunk size is zero will cause to direct upload + // see the statement binded on 'BeforeUpload' event + var reset_chunk_size = function () { + var ie = that.detectIEVersion(); + var BLOCK_BITS, MAX_CHUNK_SIZE, chunk_size; + // case Safari 5、Windows 7、iOS 7 set isSpecialSafari to true + var isSpecialSafari = + (moxie.core.utils.Env.browser === "Safari" && + moxie.core.utils.Env.version <= 5 && + moxie.core.utils.Env.os === "Windows" && + moxie.core.utils.Env.osVersion === "7") || + (moxie.core.utils.Env.browser === "Safari" && + moxie.core.utils.Env.os === "iOS" && + moxie.core.utils.Env.osVersion === "7"); + // case IE 9-,chunk_size is not empty and flash is included in runtimes + // set op.chunk_size to zero + //if (ie && ie < 9 && op.chunk_size && op.runtimes.indexOf('flash') >= 0) { + if ( + ie && + ie < 9 && + op.chunk_size && + op.runtimes.indexOf("flash") >= 0 + ) { + // link: http://www.plupload.com/docs/Frequently-Asked-Questions#when-to-use-chunking-and-when-not + // when plupload chunk_size setting is't null ,it cause bug in ie8/9 which runs flash runtimes (not support html5) . + op.chunk_size = 0; + } else if (isSpecialSafari) { + // win7 safari / iOS7 safari have bug when in chunk upload mode + // reset chunk_size to 0 + // disable chunk in special version safari + op.chunk_size = 0; + } else { + BLOCK_BITS = 20; + MAX_CHUNK_SIZE = 4 << BLOCK_BITS; //4M + + chunk_size = plupload.parseSize(op.chunk_size); + + // qiniu service max_chunk_size is 4m + // reset chunk_size to max_chunk_size(4m) when chunk_size > 4m + } + // if op.chunk_size set 0 will be cause to direct upload + }; + + var getHosts = function (hosts) { + var result = []; + for (var i = 0; i < hosts.length; i++) { + var host = hosts[i]; + if (host.indexOf("-H") === 0) { + result.push(host.split(" ")[2]); + } else { + result.push(host); + } + } + return result; + }; + + var getPutPolicy = function (uptoken) { + return ""; + }; + + var getUpHosts = function (uptoken) { + var putPolicy = getPutPolicy(uptoken); + // var uphosts_url = "//uc.qbox.me/v1/query?ak="+ak+"&bucket="+putPolicy.scope; + // IE 9- is not support protocal relative url + var uphosts_url = + window.location.protocol + + "//uc.qbox.me/v1/query?ak=" + + putPolicy.ak + + "&bucket=" + + putPolicy.bucket; + logger.debug("putPolicy: ", putPolicy); + logger.debug("get uphosts from: ", uphosts_url); + var ie = that.detectIEVersion(); + var ajax; + if (ie && ie <= 9) { + ajax = new moxie.XMLHttpRequest(); + moxie.core.utils.Env.swf_url = op.flash_swf_url; + } else { + ajax = that.createAjax(); + } + qiniuUpHosts.http = [uploadConfig.upUrl]; + qiniuUpHosts.http = [uploadConfig.upUrl]; + that.resetUploadUrl(); + return; + }; + + var getUptoken = function (file) { + if ( + !that.token || + (op.uptoken_url && that.tokenInfo.isExpired()) + ) { + return getNewUpToken(file); + } else { + return that.token; + } + }; + + // getNewUptoken maybe called at Init Event or BeforeUpload Event + // case Init Event, the file param of getUptken will be set a null value + // if op.uptoken has value, set uptoken with op.uptoken + // else if op.uptoken_url has value, set uptoken from op.uptoken_url + // else if op.uptoken_func has value, set uptoken by result of op.uptoken_func + var getNewUpToken = function (file, callback) { + if (op.uptoken) { + that.token = op.uptoken; + callback(); + } else if (op.uptoken_url) { + logger.debug("get uptoken from: ", that.uptoken_url); + // TODO: use mOxie + var ajax = that.createAjax(); + if (file.size === undefined) { + file.size = 0; + } + ajax.open( + "GET", + that.uptoken_url + + "?path=" + + encodeURIComponent(window.pathCache[file.id]) + + "&size=" + + file.size + + "&name=" + + encodeURIComponent(file.name) + + "&type=cos", + true + ); + ajax.setRequestHeader("If-Modified-Since", "0"); + ajax.send(); + ajax.onload = function (e) { + if (ajax.status === 200) { + var res = that.parseJSON(ajax.responseText); + if (res.code != 0) { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + message: res.msg, + code: 402, + }); + callback(); + return; + } + that.token = res.data.token; + file.callbackURL = res.data.callback; + that.callback = res.data.callback; + that.callback_key = res.data.key; + that.key_time = res.data.key_time; + that.ak = res.data.ak; + that.policy = res.data.policy; + that.path = res.data.path; + } else { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + code: 402, + }); + } + callback(); + }; + + ajax.onerror = function (e) { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + code: 402, + }); + callback(); + logger.error("get uptoken error: ", ajax.responseText); + }; + } else if (op.uptoken_func) { + logger.debug("get uptoken from uptoken_func"); + that.token = op.uptoken_func(file); + logger.debug("get new uptoken: ", that.token); + callback(); + } else { + logger.error( + "one of [uptoken, uptoken_url, uptoken_func] settings in options is required!" + ); + callback(); + } + return that.token; + }; + + // get file key according with the user passed options + var getFileKey = function (up, file, func) { + // WARNING + // When you set the key in putPolicy by "scope": "bucket:key" + // You should understand the risk of override a file in the bucket + // So the code below that automatically get key from uptoken has been commented + // var putPolicy = getPutPolicy(that.token) + // if (putPolicy.key) { + // logger.debug("key is defined in putPolicy.scope: ", putPolicy.key) + // return putPolicy.key + // } + var key = "", + unique_names = false; + if (!op.save_key) { + unique_names = up.getOption && up.getOption("unique_names"); + unique_names = + unique_names || + (up.settings && up.settings.unique_names); + if (unique_names) { + var ext = that.getFileExtension(file.name); + key = ext ? file.id + "." + ext : file.id; + } else if (typeof func === "function") { + key = func(up, file); + } else { + key = file.name; + } + } + if (uploadConfig.saveType == "qiniu") { + return ""; + } + return key; + }; + + /********** inner function define end **********/ + + if (op.log_level) { + logger.level = op.log_level; + } + + if (!op.domain) { + throw "domain setting in options is required!"; + } + + if (!op.browse_button) { + throw "browse_button setting in options is required!"; + } + + if (!op.uptoken && !op.uptoken_url && !op.uptoken_func) { + throw "one of [uptoken, uptoken_url, uptoken_func] settings in options is required!"; + } + + logger.debug("init uploader start"); + + logger.debug("environment: ", moxie.core.utils.Env); + + logger.debug("userAgent: ", navigator.userAgent); + + var option = {}; + + // hold the handler from user passed options + var _Error_Handler = op.init && op.init.Error; + var _FileUploaded_Handler = op.init && op.init.FileUploaded; + + // replace the handler for intercept + op.init.Error = function () {}; + op.init.FileUploaded = function () {}; + + that.uptoken_url = op.uptoken_url; + that.token = ""; + that.key_handler = + typeof op.init.Key === "function" ? op.init.Key : ""; + this.domain = op.domain; + // TODO: ctx is global in scope of a uploader instance + // this maybe cause error + var ctx = ""; + var speedCalInfo = { + isResumeUpload: false, + resumeFilesize: 0, + startTime: "", + currentTime: "", + }; + + reset_chunk_size(); + logger.debug("invoke reset_chunk_size()"); + logger.debug("op.chunk_size: ", op.chunk_size); + + var defaultSetting = { + url: qiniuUploadUrl, + multipart_params: {}, + }; + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + defaultSetting.multipart_params.accept = + "text/plain; charset=utf-8"; + logger.debug("add accept text/plain in multipart params"); + } + + // compose options with user passed options and default setting + plupload.extend(option, op, defaultSetting); + + logger.debug("option: ", option); + + // create a new uploader with composed options + var uploader = new plupload.Uploader(option); + + logger.debug("new plupload.Uploader(option)"); + + // bind getNewUpToken to 'Init' event + uploader.bind("Init", function (up, params) { + logger.debug("Init event activated"); + // if op.get_new_uptoken is not true + // invoke getNewUptoken when uploader init + // else + // getNewUptoken everytime before a new file upload + if (!op.get_new_uptoken) { + getNewUpToken(null); + } + //getNewUpToken(null); + }); + + logger.debug("bind Init event"); + + // bind 'FilesAdded' event + // when file be added and auto_start has set value + // uploader will auto start upload the file + uploader.bind("FilesAdded", function (up, files) { + logger.debug("FilesAdded event activated"); + var auto_start = up.getOption && up.getOption("auto_start"); + auto_start = + auto_start || (up.settings && up.settings.auto_start); + logger.debug("auto_start: ", auto_start); + logger.debug("files: ", files); + + for (var i = 0; i < files.length; i++) { + var file = files[i]; + file.path = decodeURIComponent( + getCookieByString("path_tmp") + ); + } + + // detect is iOS + var is_ios = function () { + if (moxie.core.utils.Env.OS.toLowerCase() === "ios") { + return true; + } else { + return false; + } + }; + + // if current env os is iOS change file name to [time].[ext] + if (is_ios()) { + for (var i = 0; i < files.length; i++) { + var file = files[i]; + var ext = that.getFileExtension(file.name); + file.name = file.id + "." + ext; + } + } + + if (auto_start) { + setTimeout(function () { + up.start(); + logger.debug("invoke up.start()"); + }, 0); + // up.start(); + // plupload.each(files, function(i, file) { + // up.start(); + // logger.debug("invoke up.start()") + // logger.debug("file: ", file); + // }); + } + up.refresh(); // Reposition Flash/Silverlight + }); + + logger.debug("bind FilesAdded event"); + + // bind 'BeforeUpload' event + // intercept the process of upload + // - prepare uptoken + // - according the chunk size to make differnt upload strategy + // - resume upload with the last breakpoint of file + uploader.bind("BeforeUpload", function (up, file) { + logger.debug("BeforeUpload event activated"); + // add a key named speed for file object + file.speed = file.speed || 0; + ctx = ""; + + var directUpload = function (up, file, func) { + speedCalInfo.startTime = new Date().getTime(); + var multipart_params_obj; + if (op.save_key) { + multipart_params_obj = { + token: that.token, + }; + } else { + multipart_params_obj = { + policy: that.policy, + key: that.path, + "x-cos-meta-callback": that.callback, + "x-cos-meta-key": that.callback_key, + "q-sign-algorithm": "sha1", + "q-key-time": that.key_time, + "q-ak": that.ak, + "q-signature": that.token, + }; + } + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + multipart_params_obj.accept = + "text/plain; charset=utf-8"; + logger.debug( + "add accept text/plain in multipart params" + ); + } + + logger.debug( + "directUpload multipart_params_obj: ", + multipart_params_obj + ); + + var x_vars = op.x_vars; + if (x_vars !== undefined && typeof x_vars === "object") { + for (var x_key in x_vars) { + if (x_vars.hasOwnProperty(x_key)) { + if (typeof x_vars[x_key] === "function") { + multipart_params_obj["x:" + x_key] = x_vars[ + x_key + ](up, file); + } else if (typeof x_vars[x_key] !== "object") { + multipart_params_obj["x:" + x_key] = + x_vars[x_key]; + } + } + } + } + + up.setOption({ + url: qiniuUploadUrl, + multipart: true, + chunk_size: is_android_weixin_or_qq() + ? op.max_file_size + : undefined, + multipart_params: multipart_params_obj, + }); + }; + + // detect is weixin or qq inner browser + var is_android_weixin_or_qq = function () { + var ua = navigator.userAgent.toLowerCase(); + if ( + (ua.match(/MicroMessenger/i) || + moxie.core.utils.Env.browser === "QQBrowser" || + ua.match(/V1_AND_SQ/i)) && + moxie.core.utils.Env.OS.toLowerCase() === "android" + ) { + return true; + } else { + return false; + } + }; + + var chunk_size = up.getOption && up.getOption("chunk_size"); + chunk_size = + chunk_size || (up.settings && up.settings.chunk_size); + + logger.debug("uploader.runtime: ", uploader.runtime); + logger.debug("chunk_size: ", chunk_size); + + getNewUpToken(file, () => { + if (that.token) { + getUpHosts(that.token); + } + if ( + (uploader.runtime === "html5" || + uploader.runtime === "flash") && + chunk_size + ) { + if ( + file.size < chunk_size || + is_android_weixin_or_qq() + ) { + logger.debug( + "directUpload because file.size < chunk_size || is_android_weixin_or_qq()" + ); + // direct upload if file size is less then the chunk size + directUpload(up, file, that.key_handler); + } else { + // TODO: need a polifill to make it work in IE 9- + // ISSUE: if file.name is existed in localStorage + // but not the same file maybe cause error + var localFileInfo = localStorage.getItem(file.name); + var blockSize = chunk_size; + if (localFileInfo) { + // TODO: although only the html5 runtime will enter this statement + // but need uniform way to make convertion between string and json + localFileInfo = that.parseJSON(localFileInfo); + var now = new Date().getTime(); + var before = localFileInfo.time || 0; + var aDay = 24 * 60 * 60 * 1000; // milliseconds of one day + // if the last upload time is within one day + // will upload continuously follow the last breakpoint + // else + // will reupload entire file + if (now - before < aDay) { + if (localFileInfo.percent !== 100) { + if (file.size === localFileInfo.total) { + // TODO: if file.name and file.size is the same + // but not the same file will cause error + file.percent = + localFileInfo.percent; + file.loaded = localFileInfo.offset; + ctx = localFileInfo.ctx; + + // set speed info + speedCalInfo.isResumeUpload = true; + speedCalInfo.resumeFilesize = + localFileInfo.offset; + + // set block size + if ( + localFileInfo.offset + + blockSize > + file.size + ) { + blockSize = + file.size - + localFileInfo.offset; + } + } else { + // remove file info when file.size is conflict with file info + localStorage.removeItem(file.name); + } + } else { + // remove file info when upload percent is 100% + // avoid 499 bug + localStorage.removeItem(file.name); + } + } else { + // remove file info when last upload time is over one day + localStorage.removeItem(file.name); + } + } + speedCalInfo.startTime = new Date().getTime(); + var multipart_params_obj = {}; + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + multipart_params_obj.accept = + "text/plain; charset=utf-8"; + logger.debug( + "add accept text/plain in multipart params" + ); + } + // TODO: to support bput + // http://developer.qiniu.com/docs/v6/api/reference/up/bput.html + if (uploadConfig.saveType == "remote") { + up.setOption({ + url: qiniuUploadUrl + "chunk.php", + multipart: false, + chunk_size: chunk_size, + required_features: "chunks", + headers: { + Authorization: getUptoken(file), + }, + multipart_params: multipart_params_obj, + }); + } else { + up.setOption({ + url: qiniuUploadUrl + "/mkblk/" + blockSize, + multipart: false, + chunk_size: chunk_size, + required_features: "chunks", + headers: { + Authorization: + "UpToken " + getUptoken(file), + }, + multipart_params: multipart_params_obj, + }); + } + } + } else { + logger.debug( + "directUpload because uploader.runtime !== 'html5' || uploader.runtime !== 'flash' || !chunk_size" + ); + // direct upload if runtime is not html5 + directUpload(up, file, that.key_handler); + } + if (file.status != plupload.FAILED) { + file.status = plupload.UPLOADING; + up.trigger("UploadFile", file); + } else { + up.stop(); + } + }); + return false; + }); + + logger.debug("bind BeforeUpload event"); + + // bind 'UploadProgress' event + // calculate upload speed + uploader.bind("UploadProgress", function (up, file) { + logger.trace("UploadProgress event activated"); + speedCalInfo.currentTime = new Date().getTime(); + var timeUsed = + speedCalInfo.currentTime - speedCalInfo.startTime; // ms + var fileUploaded = file.loaded || 0; + if (speedCalInfo.isResumeUpload) { + fileUploaded = file.loaded - speedCalInfo.resumeFilesize; + } + file.speed = ((fileUploaded / timeUsed) * 1000).toFixed(0) || 0; // unit: byte/s + }); + + logger.debug("bind UploadProgress event"); + + // bind 'ChunkUploaded' event + // store the chunk upload info and set next chunk upload url + uploader.bind("ChunkUploaded", function (up, file, info) { + logger.debug("ChunkUploaded event activated"); + logger.debug("file: ", file); + logger.debug("info: ", info); + var res = that.parseJSON(info.response); + logger.debug("res: ", res); + // ctx should look like '[chunk01_ctx],[chunk02_ctx],[chunk03_ctx],...' + ctx = ctx ? ctx + "," + res.ctx : res.ctx; + var leftSize = info.total - info.offset; + var chunk_size = up.getOption && up.getOption("chunk_size"); + chunk_size = + chunk_size || (up.settings && up.settings.chunk_size); + if (leftSize < chunk_size) { + up.setOption({ + url: qiniuUploadUrl + "/mkblk/" + leftSize, + }); + if (uploadConfig.saveType == "remote") { + up.setOption({ + url: qiniuUploadUrl + "chunk.php", + }); + } + logger.debug( + "up.setOption url: ", + qiniuUploadUrl + "/mkblk/" + leftSize + ); + } + if (uploadConfig.saveType == "remote") { + up.setOption({ + headers: { + Authorization: getUptoken(file), + }, + }); + } else { + up.setOption({ + headers: { + Authorization: "UpToken " + getUptoken(file), + }, + }); + } + localStorage.setItem( + file.name, + that.stringifyJSON({ + ctx: ctx, + percent: file.percent, + total: info.total, + offset: info.offset, + time: new Date().getTime(), + }) + ); + }); + + logger.debug("bind ChunkUploaded event"); + + var retries = qiniuUploadUrls.length; + + // if error is unkown switch upload url and retry + var unknow_error_retry = function (file) { + return false; + }; + + // bind 'Error' event + // check the err.code and return the errTip + uploader.bind( + "Error", + (function (_Error_Handler) { + return function (up, err) { + logger.error("Error event activated"); + logger.error("err: ", err); + var errTip = ""; + var file = err.file; + if (file) { + switch (err.code) { + case plupload.FAILED: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.FILE_SIZE_ERROR: + var max_file_size = + up.getOption && + up.getOption("max_file_size"); + max_file_size = + max_file_size || + (up.settings && + up.settings.max_file_size); + errTip = + "文件过大,您当前用户组最多可上传" + + max_file_size + + "的文件"; + break; + case plupload.FILE_EXTENSION_ERROR: + errTip = "您当前的用户组不可上传此文件"; + break; + case plupload.HTTP_ERROR: + if (err.response === "") { + // Fix parseJSON error ,when http error is like net::ERR_ADDRESS_UNREACHABLE + errTip = + err.message || "未知网络错误。"; + if (!unknow_error_retry(file)) { + return; + } + break; + } + + var str = err.response; + try { + parser = new DOMParser(); + xmlDoc = parser.parseFromString( + str, + "text/xml" + ); + errTip = "上传失败"; + var errorText = xmlDoc.getElementsByTagName( + "Message" + )[0].innerHTML; + } catch (e) { + errTip = "未知错误"; + var errorText = "Error"; + } + + if (uploadConfig.saveType != "local") { + errTip = + errTip + + "(" + + err.status + + ":" + + errorText + + ")"; + } + break; + case plupload.SECURITY_ERROR: + errTip = "安全配置错误。请联系网站管理员。"; + break; + case plupload.GENERIC_ERROR: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.IO_ERROR: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.INIT_ERROR: + errTip = "网站配置错误。请联系网站管理员。"; + uploader.destroy(); + break; + case 402: + errTip = "无法获取上传凭证"; + if (err.message) { + errTip = err.message; + } + break; + case 403: + errTip = "无法完成文件上传"; + break; + case 404: + var errorObj = that.parseJSON(err.response); + var errorText = errorObj.msg; + errTip = errorText; + break; + default: + errTip = err.message + err.details; + break; + } + if (_Error_Handler) { + _Error_Handler(up, err, errTip); + } + } + up.refresh(); // Reposition Flash/Silverlight + }; + })(_Error_Handler) + ); + + logger.debug("bind Error event"); + + // bind 'FileUploaded' event + // intercept the complete of upload + // - get downtoken from downtoken_url if bucket is private + // - invoke mkfile api to compose chunks if upload strategy is chunk upload + uploader.bind( + "FileUploaded", + (function (_FileUploaded_Handler) { + return function (up, file, info) { + logger.debug("FileUploaded event activated"); + logger.debug("file: ", file); + logger.debug("info: ", info); + if (uploadConfig.saveType == "s3") { + } + var last_step = function (up, file, info) { + if (_FileUploaded_Handler) { + _FileUploaded_Handler(up, file, info); + } + }; + ajax = that.createAjax(); + ajax.open("GET", file.callbackURL, true); + var onreadystatechange = function () { + logger.debug("ajax.readyState: ", ajax.readyState); + if (ajax.readyState === 4) { + var info; + if (ajax.status === 200) { + info = ajax.responseText; + var res = that.parseJSON(info); + if (res.code == 0) { + localStorage.removeItem(file.name); + up.trigger("Fresh"); + last_step(up, file, info); + } else { + info = { + status: ajax.status, + response: ajax.responseText, + file: file, + code: 404, + responseHeaders: ajax.getAllResponseHeaders(), + }; + logger.debug("mkfile is error: ", info); + uploader.trigger("Error", info); + } + } else { + info = { + status: ajax.status, + response: ajax.responseText, + file: file, + code: 403, + responseHeaders: ajax.getAllResponseHeaders(), + }; + logger.debug("mkfile is error: ", info); + uploader.trigger("Error", info); + } + } + }; + ajax.onreadystatechange = onreadystatechange; + ajax.send(info.response); + }; + })(_FileUploaded_Handler) + ); + + logger.debug("bind FileUploaded event"); + + // init uploader + uploader.init(); + + logger.debug("invoke uploader.init()"); + + logger.debug("init uploader end"); + + return uploader; + }; + + /** + * get url by key + * @param {String} key of file + * @return {String} url of file + */ + this.getUrl = function (key) { + if (!key) { + return false; + } + key = encodeURI(key); + var domain = this.domain; + if (domain.slice(domain.length - 1) !== "/") { + domain = domain + "/"; + } + return domain + key; + }; + + /** + * invoke the imageView2 api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.imageView2 = function (op, key) { + if (!/^\d$/.test(op.mode)) { + return false; + } + + var mode = op.mode, + w = op.w || "", + h = op.h || "", + q = op.q || "", + format = op.format || ""; + + if (!w && !h) { + return false; + } + + var imageUrl = "imageView2/" + mode; + imageUrl += w ? "/w/" + w : ""; + imageUrl += h ? "/h/" + h : ""; + imageUrl += q ? "/q/" + q : ""; + imageUrl += format ? "/format/" + format : ""; + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the imageMogr2 api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.imageMogr2 = function (op, key) { + var auto_orient = op["auto-orient"] || "", + thumbnail = op.thumbnail || "", + strip = op.strip || "", + gravity = op.gravity || "", + crop = op.crop || "", + quality = op.quality || "", + rotate = op.rotate || "", + format = op.format || "", + blur = op.blur || ""; + //Todo check option + + var imageUrl = "imageMogr2"; + + imageUrl += auto_orient ? "/auto-orient" : ""; + imageUrl += thumbnail ? "/thumbnail/" + thumbnail : ""; + imageUrl += strip ? "/strip" : ""; + imageUrl += gravity ? "/gravity/" + gravity : ""; + imageUrl += quality ? "/quality/" + quality : ""; + imageUrl += crop ? "/crop/" + crop : ""; + imageUrl += rotate ? "/rotate/" + rotate : ""; + imageUrl += format ? "/format/" + format : ""; + imageUrl += blur ? "/blur/" + blur : ""; + + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the watermark api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.watermark = function (op, key) { + var mode = op.mode; + if (!mode) { + return false; + } + + var imageUrl = "watermark/" + mode; + + if (mode === 1) { + var image = op.image || ""; + if (!image) { + return false; + } + imageUrl += image + ? "/image/" + this.URLSafeBase64Encode(image) + : ""; + } else if (mode === 2) { + var text = op.text ? op.text : "", + font = op.font ? op.font : "", + fontsize = op.fontsize ? op.fontsize : "", + fill = op.fill ? op.fill : ""; + if (!text) { + return false; + } + imageUrl += text + ? "/text/" + this.URLSafeBase64Encode(text) + : ""; + imageUrl += font + ? "/font/" + this.URLSafeBase64Encode(font) + : ""; + imageUrl += fontsize ? "/fontsize/" + fontsize : ""; + imageUrl += fill + ? "/fill/" + this.URLSafeBase64Encode(fill) + : ""; + } else { + // Todo mode3 + return false; + } + + var dissolve = op.dissolve || "", + gravity = op.gravity || "", + dx = op.dx || "", + dy = op.dy || ""; + + imageUrl += dissolve ? "/dissolve/" + dissolve : ""; + imageUrl += gravity ? "/gravity/" + gravity : ""; + imageUrl += dx ? "/dx/" + dx : ""; + imageUrl += dy ? "/dy/" + dy : ""; + + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the imageInfo api of Qiniu + * @param {String} key of file + * @return {Object} image info + */ + this.imageInfo = function (key) { + if (!key) { + return false; + } + var url = this.getUrl(key) + "?imageInfo"; + var xhr = this.createAjax(); + var info; + var that = this; + xhr.open("GET", url, false); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + info = that.parseJSON(xhr.responseText); + } + }; + xhr.send(); + return info; + }; + + /** + * invoke the exif api of Qiniu + * @param {String} key of file + * @return {Object} image exif + */ + this.exif = function (key) { + if (!key) { + return false; + } + var url = this.getUrl(key) + "?exif"; + var xhr = this.createAjax(); + var info; + var that = this; + xhr.open("GET", url, false); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + info = that.parseJSON(xhr.responseText); + } + }; + xhr.send(); + return info; + }; + + /** + * invoke the exif or imageInfo api of Qiniu + * according with type param + * @param {String} ['exif'|'imageInfo']type of info + * @param {String} key of file + * @return {Object} image exif or info + */ + this.get = function (type, key) { + if (!key || !type) { + return false; + } + if (type === "exif") { + return this.exif(key); + } else if (type === "imageInfo") { + return this.imageInfo(key); + } + return false; + }; + + /** + * invoke api of Qiniu like a pipeline + * @param {Array of Object} params of a series api call + * each object in array is options of api which name is set as 'fop' property + * each api's output will be next api's input + * @param {String} key of file + * @return {String|Boolean} url of processed image + */ + this.pipeline = function (arr, key) { + var isArray = + Object.prototype.toString.call(arr) === "[object Array]"; + var option, + errOp, + imageUrl = ""; + if (isArray) { + for (var i = 0, len = arr.length; i < len; i++) { + option = arr[i]; + if (!option.fop) { + return false; + } + switch (option.fop) { + case "watermark": + imageUrl += this.watermark(option) + "|"; + break; + case "imageView2": + imageUrl += this.imageView2(option) + "|"; + break; + case "imageMogr2": + imageUrl += this.imageMogr2(option) + "|"; + break; + default: + errOp = true; + break; + } + if (errOp) { + return false; + } + } + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + var length = imageUrl.length; + if (imageUrl.slice(length - 1) === "|") { + imageUrl = imageUrl.slice(0, length - 1); + } + } + return imageUrl; + } + return false; + }; + } + + var Qiniu = new QiniuJsSDK(); + + global.Qiniu = Qiniu; + + global.QiniuJsSDK = QiniuJsSDK; +})(window); diff --git a/assets/public/static/js/uploader/uploader_local.js b/assets/public/static/js/uploader/uploader_local.js new file mode 100644 index 00000000..9ac8aab9 --- /dev/null +++ b/assets/public/static/js/uploader/uploader_local.js @@ -0,0 +1,2188 @@ +/*! + * qiniu-js-sdk v@VERSION + * + * Copyright 2015 by Qiniu + * Released under GPL V2 License. + * + * GitHub: http://github.com/qiniu/js-sdk + * + * Date: @DATE + */ +/*! + * + * Rebuild By Aaron@2018 + * + */ +/*global plupload ,mOxie*/ +/*global ActiveXObject */ +/*exported Qiniu */ +/*exported QiniuJsSDK */ + +function getCookieByString(cookieName) { + var start = document.cookie.indexOf(cookieName + "="); + if (start == -1) return false; + start = start + cookieName.length + 1; + var end = document.cookie.indexOf(";", start); + if (end == -1) end = document.cookie.length; + return document.cookie.substring(start, end); +} +(function (global) { + /** + * Creates new cookie or removes cookie with negative expiration + * @param key The key or identifier for the store + * @param value Contents of the store + * @param exp Expiration - creation defaults to 30 days + */ + function createCookie(key, value, exp) { + var date = new Date(); + date.setTime(date.getTime() + exp * 24 * 60 * 60 * 1000); + var expires = "; expires=" + date.toGMTString(); + document.cookie = key + "=" + value + expires + "; path=/"; + } + + /** + * Returns contents of cookie + * @param key The key or identifier for the store + */ + function readCookie(key) { + var nameEQ = key + "="; + var ca = document.cookie.split(";"); + for (var i = 0, max = ca.length; i < max; i++) { + var c = ca[i]; + while (c.charAt(0) === " ") { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return c.substring(nameEQ.length, c.length); + } + } + return null; + } + + // if current browser is not support localStorage + // use cookie to make a polyfill + if (!window.localStorage) { + window.localStorage = { + setItem: function (key, value) { + createCookie(key, value, 30); + }, + getItem: function (key) { + return readCookie(key); + }, + removeItem: function (key) { + createCookie(key, "", -1); + }, + }; + } + + function QiniuJsSDK() { + var that = this; + + /** + * detect IE version + * if current browser is not IE + * it will return false + * else + * it will return version of current IE browser + * @return {Number|Boolean} IE version or false + */ + this.detectIEVersion = function () { + var v = 4, + div = document.createElement("div"), + all = div.getElementsByTagName("i"); + while ( + ((div.innerHTML = + ""), + all[0]) + ) { + v++; + } + return v > 4 ? v : false; + }; + + var logger = { + MUTE: 0, + FATA: 1, + ERROR: 2, + WARN: 3, + INFO: 4, + DEBUG: 5, + TRACE: 6, + level: 0, + }; + + function log(type, args) { + var header = "[Cloudreve-uploader][" + type + "]"; + var msg = header; + for (var i = 0; i < args.length; i++) { + if (typeof args[i] === "string") { + msg += " " + args[i]; + } else { + msg += " " + that.stringifyJSON(args[i]); + } + } + if (that.detectIEVersion()) { + // http://stackoverflow.com/questions/5538972/console-log-apply-not-working-in-ie9 + //var log = Function.prototype.bind.call(console.log, console); + //log.apply(console, args); + console.log(msg); + } else { + args.unshift(header); + console.log.apply(console, args); + } + if (document.getElementById("qiniu-js-sdk-log")) { + document.getElementById("qiniu-js-sdk-log").innerHTML += + "

" + msg + "

"; + } + } + + function makeLogFunc(code) { + var func = code.toLowerCase(); + logger[func] = function () { + // logger[func].history = logger[func].history || []; + // logger[func].history.push(arguments); + if ( + window.console && + window.console.log && + logger.level >= logger[code] + ) { + var args = Array.prototype.slice.call(arguments); + log(func, args); + } + }; + } + + for (var property in logger) { + if ( + logger.hasOwnProperty(property) && + typeof logger[property] === "number" && + !logger.hasOwnProperty(property.toLowerCase()) + ) { + makeLogFunc(property); + } + } + + var qiniuUploadUrl; + + /** + * qiniu upload urls + * 'qiniuUploadUrls' is used to change target when current url is not avaliable + * @type {Array} + */ + if (uploadConfig.saveType == "qiniu") { + if (window.location.protocol === "https:") { + qiniuUploadUrl = "https://up.qbox.me"; + } else { + qiniuUploadUrl = "http://upload.qiniu.com"; + } + var qiniuUploadUrls = [ + "http://upload.qiniu.com", + "http://up.qiniu.com", + ]; + + var qiniuUpHosts = { + http: ["http://upload.qiniu.com", "http://up.qiniu.com"], + https: ["https://up.qbox.me"], + }; + //TODO 优化写法 + } else if ( + uploadConfig.saveType == "local" || + uploadConfig.saveType == "oss" || + uploadConfig.saveType == "upyun" || + uploadConfig.saveType == "s3" || + uploadConfig.saveType == "remote" || + uploadConfig.saveType == "onedrive" + ) { + qiniuUploadUrl = uploadConfig.upUrl; + var qiniuUploadUrls = [uploadConfig.upUrl]; + var qiniuUpHosts = { + http: [uploadConfig.upUrl], + https: [uploadConfig.upUrl], + }; + } + + var changeUrlTimes = 0; + + /** + * reset upload url + * if current page protocal is https + * it will always return 'https://up.qbox.me' + * else + * it will set 'qiniuUploadUrl' value with 'qiniuUploadUrls' looply + */ + this.resetUploadUrl = function () { + var hosts = + window.location.protocol === "https:" + ? qiniuUpHosts.https + : qiniuUpHosts.http; + var i = changeUrlTimes % hosts.length; + qiniuUploadUrl = hosts[i]; + changeUrlTimes++; + logger.debug("resetUploadUrl: " + qiniuUploadUrl); + }; + + // this.resetUploadUrl(); + + /** + * is image + * @param {String} url of a file + * @return {Boolean} file is a image or not + */ + this.isImage = function (url) { + url = url.split(/[?#]/)[0]; + return /\.(png|jpg|jpeg|gif|bmp)$/i.test(url); + }; + + /** + * get file extension + * @param {String} filename + * @return {String} file extension + * @example + * input: test.txt + * output: txt + */ + this.getFileExtension = function (filename) { + var tempArr = filename.split("."); + var ext; + if ( + tempArr.length === 1 || + (tempArr[0] === "" && tempArr.length === 2) + ) { + ext = ""; + } else { + ext = tempArr.pop().toLowerCase(); //get the extension and make it lower-case + } + return ext; + }; + + /** + * encode string by utf8 + * @param {String} string to encode + * @return {String} encoded string + */ + this.utf8_encode = function (argString) { + // http://kevin.vanzonneveld.net + // + original by: Webtoolkit.info (http://www.webtoolkit.info/) + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + improved by: sowberry + // + tweaked by: Jack + // + bugfixed by: Onno Marsman + // + improved by: Yves Sucaet + // + bugfixed by: Onno Marsman + // + bugfixed by: Ulrich + // + bugfixed by: Rafal Kukawski + // + improved by: kirilloid + // + bugfixed by: kirilloid + // * example 1: this.utf8_encode('Kevin van Zonneveld'); + // * returns 1: 'Kevin van Zonneveld' + + if (argString === null || typeof argString === "undefined") { + return ""; + } + + var string = argString + ""; // .replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + var utftext = "", + start, + end, + stringl = 0; + + start = end = 0; + stringl = string.length; + for (var n = 0; n < stringl; n++) { + var c1 = string.charCodeAt(n); + var enc = null; + + if (c1 < 128) { + end++; + } else if (c1 > 127 && c1 < 2048) { + enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); + } else if ((c1 & 0xf800) ^ (0xd800 > 0)) { + enc = String.fromCharCode( + (c1 >> 12) | 224, + ((c1 >> 6) & 63) | 128, + (c1 & 63) | 128 + ); + } else { + // surrogate pairs + if ((c1 & 0xfc00) ^ (0xd800 > 0)) { + throw new RangeError( + "Unmatched trail surrogate at " + n + ); + } + var c2 = string.charCodeAt(++n); + if ((c2 & 0xfc00) ^ (0xdc00 > 0)) { + throw new RangeError( + "Unmatched lead surrogate at " + (n - 1) + ); + } + c1 = ((c1 & 0x3ff) << 10) + (c2 & 0x3ff) + 0x10000; + enc = String.fromCharCode( + (c1 >> 18) | 240, + ((c1 >> 12) & 63) | 128, + ((c1 >> 6) & 63) | 128, + (c1 & 63) | 128 + ); + } + if (enc !== null) { + if (end > start) { + utftext += string.slice(start, end); + } + utftext += enc; + start = end = n + 1; + } + } + + if (end > start) { + utftext += string.slice(start, stringl); + } + + return utftext; + }; + + this.base64_decode = function (data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Thunder.m + // + input by: Aman Gupta + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Onno Marsman + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: Brett Zamir (http://brett-zamir.me) + // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA=='); + // * returns 1: 'Kevin van Zonneveld' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + var b64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, + o2, + o3, + h1, + h2, + h3, + h4, + bits, + i = 0, + ac = 0, + dec = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data += ""; + + do { + // unpack four hexets into three octets using index points in b64 + h1 = b64.indexOf(data.charAt(i++)); + h2 = b64.indexOf(data.charAt(i++)); + h3 = b64.indexOf(data.charAt(i++)); + h4 = b64.indexOf(data.charAt(i++)); + + bits = (h1 << 18) | (h2 << 12) | (h3 << 6) | h4; + + o1 = (bits >> 16) & 0xff; + o2 = (bits >> 8) & 0xff; + o3 = bits & 0xff; + + if (h3 === 64) { + tmp_arr[ac++] = String.fromCharCode(o1); + } else if (h4 === 64) { + tmp_arr[ac++] = String.fromCharCode(o1, o2); + } else { + tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); + } + } while (i < data.length); + + dec = tmp_arr.join(""); + + return dec; + }; + + /** + * encode data by base64 + * @param {String} data to encode + * @return {String} encoded data + */ + this.base64_encode = function (data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Bayron Guevara + // + improved by: Thunder.m + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // - depends on: this.utf8_encode + // * example 1: this.base64_encode('Kevin van Zonneveld'); + // * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA==' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + var b64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, + o2, + o3, + h1, + h2, + h3, + h4, + bits, + i = 0, + ac = 0, + enc = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data = this.utf8_encode(data + ""); + + do { + // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = (o1 << 16) | (o2 << 8) | o3; + + h1 = (bits >> 18) & 0x3f; + h2 = (bits >> 12) & 0x3f; + h3 = (bits >> 6) & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = + b64.charAt(h1) + + b64.charAt(h2) + + b64.charAt(h3) + + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(""); + + switch (data.length % 3) { + case 1: + enc = enc.slice(0, -2) + "=="; + break; + case 2: + enc = enc.slice(0, -1) + "="; + break; + } + + return enc; + }; + + /** + * encode string in url by base64 + * @param {String} string in url + * @return {String} encoded string + */ + this.URLSafeBase64Encode = function (v) { + v = this.base64_encode(v); + return v.replace(/\//g, "_").replace(/\+/g, "-"); + }; + + this.URLSafeBase64Decode = function (v) { + v = v.replace(/_/g, "/").replace(/-/g, "+"); + return this.base64_decode(v); + }; + + // TODO: use mOxie + /** + * craete object used to AJAX + * @return {Object} + */ + this.createAjax = function (argument) { + var xmlhttp = {}; + if (window.XMLHttpRequest) { + xmlhttp = new XMLHttpRequest(); + } else { + xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + return xmlhttp; + }; + + // TODO: enhance IE compatibility + /** + * parse json string to javascript object + * @param {String} json string + * @return {Object} object + */ + this.parseJSON = function (data) { + // Attempt to parse using the native JSON parser first + if (window.JSON && window.JSON.parse) { + return window.JSON.parse(data); + } + + //var rx_one = /^[\],:{}\s]*$/, + // rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + // rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + // rx_four = /(?:^|:|,)(?:\s*\[)+/g, + var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + + //var json; + + var text = String(data); + rx_dangerous.lastIndex = 0; + if (rx_dangerous.test(text)) { + text = text.replace(rx_dangerous, function (a) { + return ( + "\\u" + + ("0000" + a.charCodeAt(0).toString(16)).slice(-4) + ); + }); + } + + // todo 使用一下判断,增加安全性 + //if ( + // rx_one.test( + // text + // .replace(rx_two, '@') + // .replace(rx_three, ']') + // .replace(rx_four, '') + // ) + //) { + // return eval('(' + text + ')'); + //} + + return eval("(" + text + ")"); + }; + + /** + * parse javascript object to json string + * @param {Object} object + * @return {String} json string + */ + this.stringifyJSON = function (obj) { + // Attempt to parse using the native JSON parser first + if (window.JSON && window.JSON.stringify) { + return window.JSON.stringify(obj); + } + switch (typeof obj) { + case "string": + return '"' + obj.replace(/(["\\])/g, "\\$1") + '"'; + case "array": + return "[" + obj.map(that.stringifyJSON).join(",") + "]"; + case "object": + if (obj instanceof Array) { + var strArr = []; + var len = obj.length; + for (var i = 0; i < len; i++) { + strArr.push(that.stringifyJSON(obj[i])); + } + return "[" + strArr.join(",") + "]"; + } else if (obj === null) { + return "null"; + } else { + var string = []; + for (var property in obj) { + if (obj.hasOwnProperty(property)) { + string.push( + that.stringifyJSON(property) + + ":" + + that.stringifyJSON(obj[property]) + ); + } + } + return "{" + string.join(",") + "}"; + } + break; + case "number": + return obj; + case false: + return obj; + case "boolean": + return obj; + } + }; + + /** + * trim space beside text + * @param {String} untrimed string + * @return {String} trimed string + */ + this.trim = function (text) { + return text === null ? "" : text.replace(/^\s+|\s+$/g, ""); + }; + + /** + * create a uploader by QiniuJsSDK + * @param {object} options to create a new uploader + * @return {object} uploader + */ + this.uploader = function (op) { + /********** inner function define start **********/ + + // according the different condition to reset chunk size + // and the upload strategy according with the chunk size + // when chunk size is zero will cause to direct upload + // see the statement binded on 'BeforeUpload' event + var reset_chunk_size = function () { + var ie = that.detectIEVersion(); + var BLOCK_BITS, MAX_CHUNK_SIZE, chunk_size; + // case Safari 5、Windows 7、iOS 7 set isSpecialSafari to true + var isSpecialSafari = + (moxie.core.utils.Env.browser === "Safari" && + moxie.core.utils.Env.version <= 5 && + moxie.core.utils.Env.os === "Windows" && + moxie.core.utils.Env.osVersion === "7") || + (moxie.core.utils.Env.browser === "Safari" && + moxie.core.utils.Env.os === "iOS" && + moxie.core.utils.Env.osVersion === "7"); + // case IE 9-,chunk_size is not empty and flash is included in runtimes + // set op.chunk_size to zero + //if (ie && ie < 9 && op.chunk_size && op.runtimes.indexOf('flash') >= 0) { + if ( + ie && + ie < 9 && + op.chunk_size && + op.runtimes.indexOf("flash") >= 0 + ) { + // link: http://www.plupload.com/docs/Frequently-Asked-Questions#when-to-use-chunking-and-when-not + // when plupload chunk_size setting is't null ,it cause bug in ie8/9 which runs flash runtimes (not support html5) . + op.chunk_size = 0; + } else if (isSpecialSafari) { + // win7 safari / iOS7 safari have bug when in chunk upload mode + // reset chunk_size to 0 + // disable chunk in special version safari + op.chunk_size = 0; + } else { + BLOCK_BITS = 20; + MAX_CHUNK_SIZE = 4 << BLOCK_BITS; //4M + + chunk_size = plupload.parseSize(op.chunk_size); + + // qiniu service max_chunk_size is 4m + // reset chunk_size to max_chunk_size(4m) when chunk_size > 4m + } + // if op.chunk_size set 0 will be cause to direct upload + }; + + var getHosts = function (hosts) { + var result = []; + for (var i = 0; i < hosts.length; i++) { + var host = hosts[i]; + if (host.indexOf("-H") === 0) { + result.push(host.split(" ")[2]); + } else { + result.push(host); + } + } + return result; + }; + + var getPutPolicy = function (uptoken) { + if ( + uploadConfig.saveType == "local" || + uploadConfig.saveType == "oss" || + uploadConfig.saveType == "upyun" || + uploadConfig.saveType == "s3" || + uploadConfig.saveType == "remote" + ) { + return "oss"; + } else { + var segments = uptoken.split(":"); + var ak = segments[0]; + var putPolicy = that.parseJSON( + that.URLSafeBase64Decode(segments[2]) + ); + putPolicy.ak = ak; + if (putPolicy.scope.indexOf(":") >= 0) { + putPolicy.bucket = putPolicy.scope.split(":")[0]; + putPolicy.key = putPolicy.scope.split(":")[1]; + } else { + putPolicy.bucket = putPolicy.scope; + } + return putPolicy; + } + }; + + var getUpHosts = function (uptoken) { + var putPolicy = getPutPolicy(uptoken); + // var uphosts_url = "//uc.qbox.me/v1/query?ak="+ak+"&bucket="+putPolicy.scope; + // IE 9- is not support protocal relative url + var uphosts_url = + window.location.protocol + + "//uc.qbox.me/v1/query?ak=" + + putPolicy.ak + + "&bucket=" + + putPolicy.bucket; + logger.debug("putPolicy: ", putPolicy); + logger.debug("get uphosts from: ", uphosts_url); + var ie = that.detectIEVersion(); + var ajax; + if (ie && ie <= 9) { + ajax = new moxie.XMLHttpRequest(); + moxie.core.utils.Env.swf_url = op.flash_swf_url; + } else { + ajax = that.createAjax(); + } + if (uploadConfig.saveType != "qiniu") { + qiniuUpHosts.http = [uploadConfig.upUrl]; + qiniuUpHosts.http = [uploadConfig.upUrl]; + that.resetUploadUrl(); + } else { + ajax.open("GET", uphosts_url, false); + var onreadystatechange = function () { + logger.debug("ajax.readyState: ", ajax.readyState); + if (ajax.readyState === 4) { + logger.debug("ajax.status: ", ajax.status); + if (ajax.status < 400) { + var res = that.parseJSON(ajax.responseText); + qiniuUpHosts.http = getHosts(res.http.up); + qiniuUpHosts.https = getHosts(res.https.up); + logger.debug("get new uphosts: ", qiniuUpHosts); + that.resetUploadUrl(); + } else { + logger.error( + "get uphosts error: ", + ajax.responseText + ); + } + } + }; + if (ie && ie <= 9) { + ajax.bind("readystatechange", onreadystatechange); + } else { + ajax.onreadystatechange = onreadystatechange; + } + ajax.send(); + // ajax.send(); + // if (ajax.status < 400) { + // var res = that.parseJSON(ajax.responseText); + // qiniuUpHosts.http = getHosts(res.http.up); + // qiniuUpHosts.https = getHosts(res.https.up); + // logger.debug("get new uphosts: ", qiniuUpHosts); + // that.resetUploadUrl(); + // } else { + // logger.error("get uphosts error: ", ajax.responseText); + // } + } + return; + }; + + var getUptoken = function (file) { + if (uploadConfig.saveType == "remote") { + return that.token; + } + if ( + !that.token || + (op.uptoken_url && that.tokenInfo.isExpired()) + ) { + return getNewUpToken(file); + } else { + return that.token; + } + }; + + // getNewUptoken maybe called at Init Event or BeforeUpload Event + // case Init Event, the file param of getUptken will be set a null value + // if op.uptoken has value, set uptoken with op.uptoken + // else if op.uptoken_url has value, set uptoken from op.uptoken_url + // else if op.uptoken_func has value, set uptoken by result of op.uptoken_func + var getNewUpToken = function (file, callback) { + if (op.uptoken) { + that.token = op.uptoken; + callback(); + } else if (op.uptoken_url) { + logger.debug("get uptoken from: ", that.uptoken_url); + // TODO: use mOxie + var ajax = that.createAjax(); + if (file.size === undefined) { + file.size = 0; + } + ajax.open( + "GET", + that.uptoken_url + + "?path=" + + encodeURIComponent(window.pathCache[file.id]) + + "&size=" + + file.size + + "&name=" + + encodeURIComponent(file.name) + + "&type=local", + true + ); + ajax.setRequestHeader("If-Modified-Since", "0"); + ajax.send(); + ajax.onload = function (e) { + if (ajax.status === 200) { + var res = that.parseJSON(ajax.responseText); + if (res.code != 0) { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + message: res.msg, + code: 402, + }); + callback(); + return; + } + that.token = res.uptoken; + if (uploadConfig.saveType == "oss") { + var putPolicy = that.token; + that.sign = res.sign; + that.access = res.id; + that.file_name = res.key; + that.callback = res.callback; + } else if (uploadConfig.saveType == "s3") { + var putPolicy = that.token; + that.sign = res.sign; + that.policy = res.policy; + that.file_name = res.key; + that.credential = res.credential; + that.x_amz_date = res.x_amz_date; + that.surl = res.siteUrl; + that.callbackKey = res.callBackKey; + } else if (uploadConfig.saveType == "upyun") { + var putPolicy = that.token; + that.token = res.token; + that.policy = res.policy; + } else if (uploadConfig.saveType == "remote") { + var putPolicy = that.token; + that.policy = res.uptoken; + } else { + var segments = that.token.split(":"); + var putPolicy = that.parseJSON( + that.URLSafeBase64Decode(segments[2]) + ); + if (!that.tokenMap) { + that.tokenMap = {}; + } + var getTimestamp = function (time) { + return Math.ceil(time.getTime() / 1000); + }; + var serverTime = getTimestamp( + new Date(ajax.getResponseHeader("date")) + ); + var clientTime = getTimestamp(new Date()); + that.tokenInfo = { + serverDelay: clientTime - serverTime, + deadline: putPolicy.deadline, + isExpired: function () { + var leftTime = + this.deadline - + getTimestamp(new Date()) + + this.serverDelay; + return leftTime < 600; + }, + }; + logger.debug( + "get token info: ", + that.tokenInfo + ); + } + } else { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + code: 402, + }); + } + callback(); + }; + + ajax.onerror = function (e) { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + code: 402, + }); + callback(); + logger.error("get uptoken error: ", ajax.responseText); + }; + } else if (op.uptoken_func) { + logger.debug("get uptoken from uptoken_func"); + that.token = op.uptoken_func(file); + logger.debug("get new uptoken: ", that.token); + callback(); + } else { + logger.error( + "one of [uptoken, uptoken_url, uptoken_func] settings in options is required!" + ); + callback(); + } + return that.token; + }; + + // get file key according with the user passed options + var getFileKey = function (up, file, func) { + // WARNING + // When you set the key in putPolicy by "scope": "bucket:key" + // You should understand the risk of override a file in the bucket + // So the code below that automatically get key from uptoken has been commented + // var putPolicy = getPutPolicy(that.token) + // if (putPolicy.key) { + // logger.debug("key is defined in putPolicy.scope: ", putPolicy.key) + // return putPolicy.key + // } + var key = "", + unique_names = false; + if (!op.save_key) { + unique_names = up.getOption && up.getOption("unique_names"); + unique_names = + unique_names || + (up.settings && up.settings.unique_names); + if (unique_names) { + var ext = that.getFileExtension(file.name); + key = ext ? file.id + "." + ext : file.id; + } else if (typeof func === "function") { + key = func(up, file); + } else { + key = file.name; + } + } + if (uploadConfig.saveType == "qiniu") { + return ""; + } + return key; + }; + + /********** inner function define end **********/ + + if (op.log_level) { + logger.level = op.log_level; + } + + if (!op.domain) { + throw "domain setting in options is required!"; + } + + if (!op.browse_button) { + throw "browse_button setting in options is required!"; + } + + if (!op.uptoken && !op.uptoken_url && !op.uptoken_func) { + throw "one of [uptoken, uptoken_url, uptoken_func] settings in options is required!"; + } + + logger.debug("init uploader start"); + + logger.debug("environment: ", moxie.core.utils.Env); + + logger.debug("userAgent: ", navigator.userAgent); + + var option = {}; + + // hold the handler from user passed options + var _Error_Handler = op.init && op.init.Error; + var _FileUploaded_Handler = op.init && op.init.FileUploaded; + + // replace the handler for intercept + op.init.Error = function () {}; + op.init.FileUploaded = function () {}; + + that.uptoken_url = op.uptoken_url; + that.token = ""; + that.key_handler = + typeof op.init.Key === "function" ? op.init.Key : ""; + this.domain = op.domain; + // TODO: ctx is global in scope of a uploader instance + // this maybe cause error + var ctx = ""; + var speedCalInfo = { + isResumeUpload: false, + resumeFilesize: 0, + startTime: "", + currentTime: "", + }; + + reset_chunk_size(); + logger.debug("invoke reset_chunk_size()"); + logger.debug("op.chunk_size: ", op.chunk_size); + + var defaultSetting = { + url: qiniuUploadUrl, + multipart_params: {}, + }; + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + defaultSetting.multipart_params.accept = + "text/plain; charset=utf-8"; + logger.debug("add accept text/plain in multipart params"); + } + + // compose options with user passed options and default setting + plupload.extend(option, op, defaultSetting); + + logger.debug("option: ", option); + + // create a new uploader with composed options + var uploader = new plupload.Uploader(option); + + logger.debug("new plupload.Uploader(option)"); + + // bind getNewUpToken to 'Init' event + uploader.bind("Init", function (up, params) { + logger.debug("Init event activated"); + // if op.get_new_uptoken is not true + // invoke getNewUptoken when uploader init + // else + // getNewUptoken everytime before a new file upload + if (!op.get_new_uptoken) { + getNewUpToken(null); + } + //getNewUpToken(null); + }); + + logger.debug("bind Init event"); + + // bind 'FilesAdded' event + // when file be added and auto_start has set value + // uploader will auto start upload the file + uploader.bind("FilesAdded", function (up, files) { + logger.debug("FilesAdded event activated"); + var auto_start = up.getOption && up.getOption("auto_start"); + auto_start = + auto_start || (up.settings && up.settings.auto_start); + logger.debug("auto_start: ", auto_start); + logger.debug("files: ", files); + + // detect is iOS + var is_ios = function () { + if (moxie.core.utils.Env.OS.toLowerCase() === "ios") { + return true; + } else { + return false; + } + }; + + // if current env os is iOS change file name to [time].[ext] + if (is_ios()) { + for (var i = 0; i < files.length; i++) { + var file = files[i]; + var ext = that.getFileExtension(file.name); + file.name = file.id + "." + ext; + } + } + + if (auto_start) { + setTimeout(function () { + up.start(); + logger.debug("invoke up.start()"); + }, 0); + // up.start(); + // plupload.each(files, function(i, file) { + // up.start(); + // logger.debug("invoke up.start()") + // logger.debug("file: ", file); + // }); + } + up.refresh(); // Reposition Flash/Silverlight + }); + + logger.debug("bind FilesAdded event"); + + // bind 'BeforeUpload' event + // intercept the process of upload + // - prepare uptoken + // - according the chunk size to make differnt upload strategy + // - resume upload with the last breakpoint of file + uploader.bind("BeforeUpload", function (up, file) { + logger.debug("BeforeUpload event activated"); + // add a key named speed for file object + file.speed = file.speed || 0; + ctx = ""; + + var directUpload = function (up, file, func) { + speedCalInfo.startTime = new Date().getTime(); + var multipart_params_obj; + if (op.save_key) { + multipart_params_obj = { + token: that.token, + }; + } else { + multipart_params_obj = { + key: getFileKey(up, file, func), + token: that.token, + }; + if (uploadConfig.saveType == "qiniu") { + multipart_params_obj = { + token: that.token, + "x:path": file.path, + }; + } else if (uploadConfig.saveType == "remote") { + multipart_params_obj = { + path: file.path, + token: that.policy, + MAX_FILE_SIZE: 4194304, + }; + } else if (uploadConfig.saveType == "oss") { + multipart_params_obj = { + policy: that.token, + "x:path": file.path, + signature: that.sign, + OSSAccessKeyId: that.access, + "x:fname": file.name, + key: that.file_name.replace( + "${filename}", + file.name + ), + callback: that.callback, + }; + } else if (uploadConfig.saveType == "s3") { + multipart_params_obj = { + policy: that.policy, + key: + that.file_name + + "/" + + (file.path.replace(",", "/") == "" + ? "" + : file.path.replace(",", "/") + "/") + + file.name, + success_action_redirect: + that.surl + + "Callback/S3/key/" + + that.callbackKey, + "x-amz-algorithm": "AWS4-HMAC-SHA256", + "x-amz-credential": that.credential, + "x-amz-date": that.x_amz_date, + "x-amz-signature": that.sign, + "Content-Type": file.type, + }; + } else if (uploadConfig.saveType == "upyun") { + multipart_params_obj = { + authorization: that.token, + policy: that.policy, + }; + } + } + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + multipart_params_obj.accept = + "text/plain; charset=utf-8"; + logger.debug( + "add accept text/plain in multipart params" + ); + } + + logger.debug( + "directUpload multipart_params_obj: ", + multipart_params_obj + ); + + var x_vars = op.x_vars; + if (x_vars !== undefined && typeof x_vars === "object") { + for (var x_key in x_vars) { + if (x_vars.hasOwnProperty(x_key)) { + if (typeof x_vars[x_key] === "function") { + multipart_params_obj["x:" + x_key] = x_vars[ + x_key + ](up, file); + } else if (typeof x_vars[x_key] !== "object") { + multipart_params_obj["x:" + x_key] = + x_vars[x_key]; + } + } + } + } + + // 本地策略时不用multipart + if (uploadConfig.saveType == "local") { + up.setOption({ + url: qiniuUploadUrl, + multipart: false, + send_file_name: false, + headers: { + "X-Path": encodeURIComponent(file.path), + "X-FileName": encodeURIComponent( + getFileKey(up, file, func) + ), + }, + }); + } else { + up.setOption({ + url: qiniuUploadUrl, + multipart: true, + chunk_size: is_android_weixin_or_qq() + ? op.max_file_size + : undefined, + multipart_params: multipart_params_obj, + }); + } + }; + + // detect is weixin or qq inner browser + var is_android_weixin_or_qq = function () { + var ua = navigator.userAgent.toLowerCase(); + if ( + (ua.match(/MicroMessenger/i) || + moxie.core.utils.Env.browser === "QQBrowser" || + ua.match(/V1_AND_SQ/i)) && + moxie.core.utils.Env.OS.toLowerCase() === "android" + ) { + return true; + } else { + return false; + } + }; + + var chunk_size = up.getOption && up.getOption("chunk_size"); + chunk_size = + chunk_size || (up.settings && up.settings.chunk_size); + + logger.debug("uploader.runtime: ", uploader.runtime); + logger.debug("chunk_size: ", chunk_size); + + getNewUpToken(file, () => { + if (that.token) { + getUpHosts(that.token); + } + if ( + (uploader.runtime === "html5" || + uploader.runtime === "flash") && + chunk_size + ) { + if ( + file.size < chunk_size || + is_android_weixin_or_qq() + ) { + logger.debug( + "directUpload because file.size < chunk_size || is_android_weixin_or_qq()" + ); + // direct upload if file size is less then the chunk size + directUpload(up, file, that.key_handler); + } else { + // TODO: need a polifill to make it work in IE 9- + // ISSUE: if file.name is existed in localStorage + // but not the same file maybe cause error + var localFileInfo = localStorage.getItem(file.name); + var blockSize = chunk_size; + if (localFileInfo) { + // TODO: although only the html5 runtime will enter this statement + // but need uniform way to make convertion between string and json + localFileInfo = that.parseJSON(localFileInfo); + var now = new Date().getTime(); + var before = localFileInfo.time || 0; + var aDay = 24 * 60 * 60 * 1000; // milliseconds of one day + // if the last upload time is within one day + // will upload continuously follow the last breakpoint + // else + // will reupload entire file + if (now - before < aDay) { + if (localFileInfo.percent !== 100) { + if (file.size === localFileInfo.total) { + // TODO: if file.name and file.size is the same + // but not the same file will cause error + file.percent = + localFileInfo.percent; + file.loaded = localFileInfo.offset; + ctx = localFileInfo.ctx; + + // set speed info + speedCalInfo.isResumeUpload = true; + speedCalInfo.resumeFilesize = + localFileInfo.offset; + + // set block size + if ( + localFileInfo.offset + + blockSize > + file.size + ) { + blockSize = + file.size - + localFileInfo.offset; + } + } else { + // remove file info when file.size is conflict with file info + localStorage.removeItem(file.name); + } + } else { + // remove file info when upload percent is 100% + // avoid 499 bug + localStorage.removeItem(file.name); + } + } else { + // remove file info when last upload time is over one day + localStorage.removeItem(file.name); + } + } + speedCalInfo.startTime = new Date().getTime(); + var multipart_params_obj = {}; + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + multipart_params_obj.accept = + "text/plain; charset=utf-8"; + logger.debug( + "add accept text/plain in multipart params" + ); + } + // TODO: to support bput + // http://developer.qiniu.com/docs/v6/api/reference/up/bput.html + if (uploadConfig.saveType == "remote") { + up.setOption({ + url: qiniuUploadUrl + "chunk.php", + multipart: false, + chunk_size: chunk_size, + required_features: "chunks", + headers: { + Authorization: getUptoken(file), + }, + multipart_params: multipart_params_obj, + }); + } else { + up.setOption({ + url: qiniuUploadUrl + "/mkblk/" + blockSize, + multipart: false, + chunk_size: chunk_size, + required_features: "chunks", + headers: { + Authorization: + "UpToken " + getUptoken(file), + }, + multipart_params: multipart_params_obj, + }); + } + } + } else { + logger.debug( + "directUpload because uploader.runtime !== 'html5' || uploader.runtime !== 'flash' || !chunk_size" + ); + // direct upload if runtime is not html5 + directUpload(up, file, that.key_handler); + } + if (file.status != plupload.FAILED) { + file.status = plupload.UPLOADING; + up.trigger("UploadFile", file); + } else { + up.stop(); + } + }); + return false; + }); + + logger.debug("bind BeforeUpload event"); + + // bind 'UploadProgress' event + // calculate upload speed + uploader.bind("UploadProgress", function (up, file) { + logger.trace("UploadProgress event activated"); + speedCalInfo.currentTime = new Date().getTime(); + var timeUsed = + speedCalInfo.currentTime - speedCalInfo.startTime; // ms + var fileUploaded = file.loaded || 0; + if (speedCalInfo.isResumeUpload) { + fileUploaded = file.loaded - speedCalInfo.resumeFilesize; + } + file.speed = ((fileUploaded / timeUsed) * 1000).toFixed(0) || 0; // unit: byte/s + }); + + logger.debug("bind UploadProgress event"); + + // bind 'ChunkUploaded' event + // store the chunk upload info and set next chunk upload url + uploader.bind("ChunkUploaded", function (up, file, info) { + logger.debug("ChunkUploaded event activated"); + logger.debug("file: ", file); + logger.debug("info: ", info); + var res = that.parseJSON(info.response); + logger.debug("res: ", res); + // ctx should look like '[chunk01_ctx],[chunk02_ctx],[chunk03_ctx],...' + ctx = ctx ? ctx + "," + res.ctx : res.ctx; + var leftSize = info.total - info.offset; + var chunk_size = up.getOption && up.getOption("chunk_size"); + chunk_size = + chunk_size || (up.settings && up.settings.chunk_size); + if (leftSize < chunk_size) { + up.setOption({ + url: qiniuUploadUrl + "/mkblk/" + leftSize, + }); + if (uploadConfig.saveType == "remote") { + up.setOption({ + url: qiniuUploadUrl + "chunk.php", + }); + } + logger.debug( + "up.setOption url: ", + qiniuUploadUrl + "/mkblk/" + leftSize + ); + } + if (uploadConfig.saveType == "remote") { + up.setOption({ + headers: { + Authorization: getUptoken(file), + }, + }); + } else { + up.setOption({ + headers: { + Authorization: "UpToken " + getUptoken(file), + }, + }); + } + localStorage.setItem( + file.name, + that.stringifyJSON({ + ctx: ctx, + percent: file.percent, + total: info.total, + offset: info.offset, + time: new Date().getTime(), + }) + ); + }); + + logger.debug("bind ChunkUploaded event"); + + var retries = qiniuUploadUrls.length; + + // if error is unkown switch upload url and retry + var unknow_error_retry = function (file) { + if (retries-- > 0) { + setTimeout(function () { + that.resetUploadUrl(); + file.status = plupload.QUEUED; + uploader.stop(); + uploader.start(); + }, 0); + return true; + } else { + retries = qiniuUploadUrls.length; + return false; + } + }; + + // bind 'Error' event + // check the err.code and return the errTip + uploader.bind( + "Error", + (function (_Error_Handler) { + return function (up, err) { + logger.error("Error event activated"); + logger.error("err: ", err); + var errTip = ""; + var file = err.file; + if (file) { + switch (err.code) { + case plupload.FAILED: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.FILE_SIZE_ERROR: + var max_file_size = + up.getOption && + up.getOption("max_file_size"); + max_file_size = + max_file_size || + (up.settings && + up.settings.max_file_size); + errTip = + "文件过大,您当前用户组最多可上传" + + max_file_size + + "的文件"; + break; + case plupload.FILE_EXTENSION_ERROR: + errTip = "您当前的用户组不可上传此文件"; + break; + case plupload.HTTP_ERROR: + if (err.response === "") { + // Fix parseJSON error ,when http error is like net::ERR_ADDRESS_UNREACHABLE + errTip = + err.message || "未知网络错误。"; + if (!unknow_error_retry(file)) { + return; + } + break; + } + if (uploadConfig.saveType == "oss") { + var str = err.response; + try { + parser = new DOMParser(); + xmlDoc = parser.parseFromString( + str, + "text/xml" + ); + } catch (e) { + errTip = "未知错误"; + var errorText = "Error"; + } + errTip = "回调失败"; + var errorText = xmlDoc.getElementsByTagName( + "Message" + )[0].innerHTML; + } else if ( + uploadConfig.saveType == "s3" && + err.status != 401 + ) { + var str = err.response; + parser = new DOMParser(); + xmlDoc = parser.parseFromString( + str, + "text/xml" + ); + errTip = xmlDoc.getElementsByTagName( + "Message" + )[0].childNodes[0].nodeValue; + var errorText = "Error"; + } else { + var errorObj = that.parseJSON( + err.response + ); + var errorText = errorObj.error; + if (err.status == 579) { + var errorObj2 = that.parseJSON( + errorText + ); + errorText = errorObj2.error; + } + switch (err.status) { + case 400: + errTip = "请求报文格式错误。"; + break; + case 401: + errTip = + "客户端认证授权失败。请重试或提交反馈。"; + break; + case 405: + errTip = + "客户端请求错误。请重试或提交反馈。"; + break; + case 579: + errTip = + "资源上传成功,但回调失败。"; + break; + case 599: + errTip = + "网络连接异常。请重试或提交反馈。"; + if (!unknow_error_retry(file)) { + return; + } + break; + case 614: + errTip = "文件已存在。"; + try { + errorObj = that.parseJSON( + errorObj.error + ); + errorText = + errorObj.error || + "file exists"; + } catch (e) { + errorText = + errorObj.error || + "file exists"; + } + break; + case 631: + errTip = "指定空间不存在。"; + break; + case 701: + errTip = + "上传数据块校验出错。请重试或提交反馈。"; + break; + default: + if (err.message) { + errTip = err.message; + } else { + errTip = "未知错误"; + } + if (!unknow_error_retry(file)) { + return; + } + break; + } + } + if (uploadConfig.saveType != "local") { + errTip = + errTip + + "(" + + err.status + + ":" + + errorText + + ")"; + } + break; + case plupload.SECURITY_ERROR: + errTip = "安全配置错误。请联系网站管理员。"; + break; + case plupload.GENERIC_ERROR: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.IO_ERROR: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.INIT_ERROR: + errTip = "网站配置错误。请联系网站管理员。"; + uploader.destroy(); + break; + case 402: + errTip = "无法获取上传凭证"; + if (err.message) { + errTip = err.message; + } + break; + default: + errTip = err.message + err.details; + break; + } + if (_Error_Handler) { + _Error_Handler(up, err, errTip); + } + } + up.refresh(); // Reposition Flash/Silverlight + }; + })(_Error_Handler) + ); + + logger.debug("bind Error event"); + + // bind 'FileUploaded' event + // intercept the complete of upload + // - get downtoken from downtoken_url if bucket is private + // - invoke mkfile api to compose chunks if upload strategy is chunk upload + uploader.bind( + "FileUploaded", + (function (_FileUploaded_Handler) { + return function (up, file, info) { + logger.debug("FileUploaded event activated"); + logger.debug("file: ", file); + logger.debug("info: ", info); + if (uploadConfig.saveType == "s3") { + } + var last_step = function (up, file, info) { + if (op.downtoken_url) { + // if op.dowontoken_url is not empty + // need get downtoken before invoke the _FileUploaded_Handler + var ajax_downtoken = that.createAjax(); + ajax_downtoken.open( + "POST", + op.downtoken_url, + true + ); + ajax_downtoken.setRequestHeader( + "Content-type", + "application/x-www-form-urlencoded" + ); + ajax_downtoken.onreadystatechange = function () { + if (ajax_downtoken.readyState === 4) { + if ( + ajax_downtoken.status === 200 || + ajax_downtoken.status === 204 || + ajax_downtoken.status === 303 + ) { + var res_downtoken; + try { + res_downtoken = that.parseJSON( + ajax_downtoken.responseText + ); + } catch (e) { + throw "invalid json format"; + } + var info_extended = {}; + plupload.extend( + info_extended, + that.parseJSON(info), + res_downtoken + ); + if (_FileUploaded_Handler) { + _FileUploaded_Handler( + up, + file, + that.stringifyJSON( + info_extended + ) + ); + } + } else { + uploader.trigger("Error", { + status: ajax_downtoken.status, + response: + ajax_downtoken.responseText, + file: file, + code: plupload.HTTP_ERROR, + }); + } + } + }; + ajax_downtoken.send( + "key=" + + that.parseJSON(info).key + + "&domain=" + + op.domain + ); + } else if (_FileUploaded_Handler) { + _FileUploaded_Handler(up, file, info); + } + }; + if ( + uploadConfig.saveType == "oss" || + uploadConfig.saveType == "upyun" + ) { + ctx = 0; + } else { + var res = that.parseJSON(info.response); + ctx = ctx ? ctx : res.ctx; + } + // if ctx is not empty + // that means the upload strategy is chunk upload + // befroe the invoke the last_step + // we need request the mkfile to compose all uploaded chunks + // else + // invalke the last_step + logger.debug("ctx: ", ctx); + if (ctx) { + var key = ""; + logger.debug("save_key: ", op.save_key); + if (!op.save_key) { + key = getFileKey(up, file, that.key_handler); + key = key + ? "/key/" + that.URLSafeBase64Encode(key) + : ""; + } + + var fname = + "/fname/" + that.URLSafeBase64Encode(file.name); + if (uploadConfig.saveType == "remote") { + if (!op.save_key) { + key = getFileKey( + up, + file, + that.key_handler + ); + key = key + ? that.URLSafeBase64Encode(key) + : ""; + } + fname = + "" + that.URLSafeBase64Encode(file.name); + op.x_vars = { + path: file.path, + }; + } + logger.debug("op.x_vars: ", op.x_vars); + if (uploadConfig.saveType == "qiniu") { + op.x_vars = { + path: file.path, + }; + } + var x_vars = op.x_vars, + x_val = "", + x_vars_url = ""; + if ( + x_vars !== undefined && + typeof x_vars === "object" + ) { + for (var x_key in x_vars) { + if (x_vars.hasOwnProperty(x_key)) { + if ( + typeof x_vars[x_key] === "function" + ) { + x_val = that.URLSafeBase64Encode( + x_vars[x_key](up, file) + ); + } else if ( + typeof x_vars[x_key] !== "object" + ) { + x_val = that.URLSafeBase64Encode( + x_vars[x_key] + ); + } + x_vars_url += + "/x:" + x_key + "/" + x_val; + } + } + } + local_path = ""; + if ( + uploadConfig.saveType == "local" || + uploadConfig.saveType == "onedrive" + ) { + pathTmp = file.path; + if (file.path == "") { + pathTmp = "ROOTDIR"; + } + local_path = + "/path/" + + that.URLSafeBase64Encode(pathTmp); + } + if (uploadConfig.saveType == "remote") { + pathTmp = file.path; + local_path = that.URLSafeBase64Encode(pathTmp); + var url = + qiniuUploadUrl + + "mkfile.php?size=" + + file.size + + "&key=" + + key + + "&fname=" + + fname + + "&path=" + + local_path; + } else { + var url = + qiniuUploadUrl + + "/mkfile/" + + file.size + + key + + fname + + x_vars_url + + local_path; + } + var ie = that.detectIEVersion(); + var ajax; + if (ie && ie <= 9) { + ajax = new moxie.xhr.XMLHttpRequest(); + moxie.core.utils.Env.swf_url = op.flash_swf_url; + } else { + ajax = that.createAjax(); + } + ajax.open("POST", url, true); + ajax.setRequestHeader( + "Content-Type", + "text/plain;charset=UTF-8" + ); + if (uploadConfig.saveType == "remote") { + ajax.setRequestHeader( + "Authorization", + that.token + ); + } else { + ajax.setRequestHeader( + "Authorization", + "UpToken " + that.token + ); + } + var onreadystatechange = function () { + logger.debug( + "ajax.readyState: ", + ajax.readyState + ); + if (ajax.readyState === 4) { + localStorage.removeItem(file.name); + var info; + if (ajax.status === 200) { + info = ajax.responseText; + logger.debug( + "mkfile is success: ", + info + ); + last_step(up, file, info); + } else { + info = { + status: ajax.status, + response: ajax.responseText, + file: file, + code: -200, + responseHeaders: ajax.getAllResponseHeaders(), + }; + logger.debug("mkfile is error: ", info); + uploader.trigger("Error", info); + } + } + }; + if (ie && ie <= 9) { + ajax.bind( + "readystatechange", + onreadystatechange + ); + } else { + ajax.onreadystatechange = onreadystatechange; + } + ajax.send(ctx); + logger.debug("mkfile: ", url); + } else { + last_step(up, file, info.response); + } + }; + })(_FileUploaded_Handler) + ); + + logger.debug("bind FileUploaded event"); + + // init uploader + uploader.init(); + + logger.debug("invoke uploader.init()"); + + logger.debug("init uploader end"); + + return uploader; + }; + + /** + * get url by key + * @param {String} key of file + * @return {String} url of file + */ + this.getUrl = function (key) { + if (!key) { + return false; + } + key = encodeURI(key); + var domain = this.domain; + if (domain.slice(domain.length - 1) !== "/") { + domain = domain + "/"; + } + return domain + key; + }; + + /** + * invoke the imageView2 api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.imageView2 = function (op, key) { + if (!/^\d$/.test(op.mode)) { + return false; + } + + var mode = op.mode, + w = op.w || "", + h = op.h || "", + q = op.q || "", + format = op.format || ""; + + if (!w && !h) { + return false; + } + + var imageUrl = "imageView2/" + mode; + imageUrl += w ? "/w/" + w : ""; + imageUrl += h ? "/h/" + h : ""; + imageUrl += q ? "/q/" + q : ""; + imageUrl += format ? "/format/" + format : ""; + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the imageMogr2 api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.imageMogr2 = function (op, key) { + var auto_orient = op["auto-orient"] || "", + thumbnail = op.thumbnail || "", + strip = op.strip || "", + gravity = op.gravity || "", + crop = op.crop || "", + quality = op.quality || "", + rotate = op.rotate || "", + format = op.format || "", + blur = op.blur || ""; + //Todo check option + + var imageUrl = "imageMogr2"; + + imageUrl += auto_orient ? "/auto-orient" : ""; + imageUrl += thumbnail ? "/thumbnail/" + thumbnail : ""; + imageUrl += strip ? "/strip" : ""; + imageUrl += gravity ? "/gravity/" + gravity : ""; + imageUrl += quality ? "/quality/" + quality : ""; + imageUrl += crop ? "/crop/" + crop : ""; + imageUrl += rotate ? "/rotate/" + rotate : ""; + imageUrl += format ? "/format/" + format : ""; + imageUrl += blur ? "/blur/" + blur : ""; + + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the watermark api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.watermark = function (op, key) { + var mode = op.mode; + if (!mode) { + return false; + } + + var imageUrl = "watermark/" + mode; + + if (mode === 1) { + var image = op.image || ""; + if (!image) { + return false; + } + imageUrl += image + ? "/image/" + this.URLSafeBase64Encode(image) + : ""; + } else if (mode === 2) { + var text = op.text ? op.text : "", + font = op.font ? op.font : "", + fontsize = op.fontsize ? op.fontsize : "", + fill = op.fill ? op.fill : ""; + if (!text) { + return false; + } + imageUrl += text + ? "/text/" + this.URLSafeBase64Encode(text) + : ""; + imageUrl += font + ? "/font/" + this.URLSafeBase64Encode(font) + : ""; + imageUrl += fontsize ? "/fontsize/" + fontsize : ""; + imageUrl += fill + ? "/fill/" + this.URLSafeBase64Encode(fill) + : ""; + } else { + // Todo mode3 + return false; + } + + var dissolve = op.dissolve || "", + gravity = op.gravity || "", + dx = op.dx || "", + dy = op.dy || ""; + + imageUrl += dissolve ? "/dissolve/" + dissolve : ""; + imageUrl += gravity ? "/gravity/" + gravity : ""; + imageUrl += dx ? "/dx/" + dx : ""; + imageUrl += dy ? "/dy/" + dy : ""; + + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the imageInfo api of Qiniu + * @param {String} key of file + * @return {Object} image info + */ + this.imageInfo = function (key) { + if (!key) { + return false; + } + var url = this.getUrl(key) + "?imageInfo"; + var xhr = this.createAjax(); + var info; + var that = this; + xhr.open("GET", url, false); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + info = that.parseJSON(xhr.responseText); + } + }; + xhr.send(); + return info; + }; + + /** + * invoke the exif api of Qiniu + * @param {String} key of file + * @return {Object} image exif + */ + this.exif = function (key) { + if (!key) { + return false; + } + var url = this.getUrl(key) + "?exif"; + var xhr = this.createAjax(); + var info; + var that = this; + xhr.open("GET", url, false); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + info = that.parseJSON(xhr.responseText); + } + }; + xhr.send(); + return info; + }; + + /** + * invoke the exif or imageInfo api of Qiniu + * according with type param + * @param {String} ['exif'|'imageInfo']type of info + * @param {String} key of file + * @return {Object} image exif or info + */ + this.get = function (type, key) { + if (!key || !type) { + return false; + } + if (type === "exif") { + return this.exif(key); + } else if (type === "imageInfo") { + return this.imageInfo(key); + } + return false; + }; + + /** + * invoke api of Qiniu like a pipeline + * @param {Array of Object} params of a series api call + * each object in array is options of api which name is set as 'fop' property + * each api's output will be next api's input + * @param {String} key of file + * @return {String|Boolean} url of processed image + */ + this.pipeline = function (arr, key) { + var isArray = + Object.prototype.toString.call(arr) === "[object Array]"; + var option, + errOp, + imageUrl = ""; + if (isArray) { + for (var i = 0, len = arr.length; i < len; i++) { + option = arr[i]; + if (!option.fop) { + return false; + } + switch (option.fop) { + case "watermark": + imageUrl += this.watermark(option) + "|"; + break; + case "imageView2": + imageUrl += this.imageView2(option) + "|"; + break; + case "imageMogr2": + imageUrl += this.imageMogr2(option) + "|"; + break; + default: + errOp = true; + break; + } + if (errOp) { + return false; + } + } + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + var length = imageUrl.length; + if (imageUrl.slice(length - 1) === "|") { + imageUrl = imageUrl.slice(0, length - 1); + } + } + return imageUrl; + } + return false; + }; + } + + var Qiniu = new QiniuJsSDK(); + + global.Qiniu = Qiniu; + + global.QiniuJsSDK = QiniuJsSDK; +})(window); diff --git a/assets/public/static/js/uploader/uploader_onedrive.js b/assets/public/static/js/uploader/uploader_onedrive.js new file mode 100644 index 00000000..875f51da --- /dev/null +++ b/assets/public/static/js/uploader/uploader_onedrive.js @@ -0,0 +1,1555 @@ +/*! + * qiniu-js-sdk v@VERSION + * + * Copyright 2015 by Qiniu + * Released under GPL V2 License. + * + * GitHub: http://github.com/qiniu/js-sdk + * + * Date: @DATE + */ +/*! + * + * Rebuild By Aaron@2018 + * + */ +/*global plupload ,mOxie*/ +/*global ActiveXObject */ +/*exported Qiniu */ +/*exported QiniuJsSDK */ + +function getCookieByString(cookieName) { + var start = document.cookie.indexOf(cookieName + "="); + if (start == -1) return false; + start = start + cookieName.length + 1; + var end = document.cookie.indexOf(";", start); + if (end == -1) end = document.cookie.length; + return document.cookie.substring(start, end); +} +(function (global) { + /** + * Creates new cookie or removes cookie with negative expiration + * @param key The key or identifier for the store + * @param value Contents of the store + * @param exp Expiration - creation defaults to 30 days + */ + function createCookie(key, value, exp) { + var date = new Date(); + date.setTime(date.getTime() + exp * 24 * 60 * 60 * 1000); + var expires = "; expires=" + date.toGMTString(); + document.cookie = key + "=" + value + expires + "; path=/"; + } + + /** + * Returns contents of cookie + * @param key The key or identifier for the store + */ + function readCookie(key) { + var nameEQ = key + "="; + var ca = document.cookie.split(";"); + for (var i = 0, max = ca.length; i < max; i++) { + var c = ca[i]; + while (c.charAt(0) === " ") { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return c.substring(nameEQ.length, c.length); + } + } + return null; + } + + // if current browser is not support localStorage + // use cookie to make a polyfill + if (!window.localStorage) { + window.localStorage = { + setItem: function (key, value) { + createCookie(key, value, 30); + }, + getItem: function (key) { + return readCookie(key); + }, + removeItem: function (key) { + createCookie(key, "", -1); + }, + }; + } + + function QiniuJsSDK() { + var that = this; + + /** + * detect IE version + * if current browser is not IE + * it will return false + * else + * it will return version of current IE browser + * @return {Number|Boolean} IE version or false + */ + this.detectIEVersion = function () { + var v = 4, + div = document.createElement("div"), + all = div.getElementsByTagName("i"); + while ( + ((div.innerHTML = + ""), + all[0]) + ) { + v++; + } + return v > 4 ? v : false; + }; + + var logger = { + MUTE: 0, + FATA: 1, + ERROR: 2, + WARN: 3, + INFO: 4, + DEBUG: 5, + TRACE: 6, + level: 0, + }; + + function log(type, args) { + var header = "[Cloudreve-uploader][" + type + "]"; + var msg = header; + for (var i = 0; i < args.length; i++) { + if (typeof args[i] === "string") { + msg += " " + args[i]; + } else { + msg += " " + that.stringifyJSON(args[i]); + } + } + if (that.detectIEVersion()) { + // http://stackoverflow.com/questions/5538972/console-log-apply-not-working-in-ie9 + //var log = Function.prototype.bind.call(console.log, console); + //log.apply(console, args); + console.log(msg); + } else { + args.unshift(header); + console.log.apply(console, args); + } + if (document.getElementById("qiniu-js-sdk-log")) { + document.getElementById("qiniu-js-sdk-log").innerHTML += + "

" + msg + "

"; + } + } + + function makeLogFunc(code) { + var func = code.toLowerCase(); + logger[func] = function () { + // logger[func].history = logger[func].history || []; + // logger[func].history.push(arguments); + if ( + window.console && + window.console.log && + logger.level >= logger[code] + ) { + var args = Array.prototype.slice.call(arguments); + log(func, args); + } + }; + } + + for (var property in logger) { + if ( + logger.hasOwnProperty(property) && + typeof logger[property] === "number" && + !logger.hasOwnProperty(property.toLowerCase()) + ) { + makeLogFunc(property); + } + } + + var qiniuUploadUrl; + + /** + * qiniu upload urls + * 'qiniuUploadUrls' is used to change target when current url is not avaliable + * @type {Array} + */ + + qiniuUploadUrl = uploadConfig.upUrl; + var qiniuUploadUrls = [uploadConfig.upUrl]; + var qiniuUpHosts = { + http: [uploadConfig.upUrl], + https: [uploadConfig.upUrl], + }; + + var changeUrlTimes = 0; + + /** + * reset upload url + * if current page protocal is https + * it will always return 'https://up.qbox.me' + * else + * it will set 'qiniuUploadUrl' value with 'qiniuUploadUrls' looply + */ + this.resetUploadUrl = function () { + var hosts = + window.location.protocol === "https:" + ? qiniuUpHosts.https + : qiniuUpHosts.http; + var i = changeUrlTimes % hosts.length; + qiniuUploadUrl = hosts[i]; + changeUrlTimes++; + logger.debug("resetUploadUrl: " + qiniuUploadUrl); + }; + + // this.resetUploadUrl(); + + /** + * is image + * @param {String} url of a file + * @return {Boolean} file is a image or not + */ + this.isImage = function (url) { + url = url.split(/[?#]/)[0]; + return /\.(png|jpg|jpeg|gif|bmp)$/i.test(url); + }; + + /** + * get file extension + * @param {String} filename + * @return {String} file extension + * @example + * input: test.txt + * output: txt + */ + this.getFileExtension = function (filename) { + var tempArr = filename.split("."); + var ext; + if ( + tempArr.length === 1 || + (tempArr[0] === "" && tempArr.length === 2) + ) { + ext = ""; + } else { + ext = tempArr.pop().toLowerCase(); //get the extension and make it lower-case + } + return ext; + }; + + /** + * encode string by utf8 + * @param {String} string to encode + * @return {String} encoded string + */ + this.utf8_encode = function (argString) { + // http://kevin.vanzonneveld.net + // + original by: Webtoolkit.info (http://www.webtoolkit.info/) + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + improved by: sowberry + // + tweaked by: Jack + // + bugfixed by: Onno Marsman + // + improved by: Yves Sucaet + // + bugfixed by: Onno Marsman + // + bugfixed by: Ulrich + // + bugfixed by: Rafal Kukawski + // + improved by: kirilloid + // + bugfixed by: kirilloid + // * example 1: this.utf8_encode('Kevin van Zonneveld'); + // * returns 1: 'Kevin van Zonneveld' + + if (argString === null || typeof argString === "undefined") { + return ""; + } + + var string = argString + ""; // .replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + var utftext = "", + start, + end, + stringl = 0; + + start = end = 0; + stringl = string.length; + for (var n = 0; n < stringl; n++) { + var c1 = string.charCodeAt(n); + var enc = null; + + if (c1 < 128) { + end++; + } else if (c1 > 127 && c1 < 2048) { + enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); + } else if ((c1 & 0xf800) ^ (0xd800 > 0)) { + enc = String.fromCharCode( + (c1 >> 12) | 224, + ((c1 >> 6) & 63) | 128, + (c1 & 63) | 128 + ); + } else { + // surrogate pairs + if ((c1 & 0xfc00) ^ (0xd800 > 0)) { + throw new RangeError( + "Unmatched trail surrogate at " + n + ); + } + var c2 = string.charCodeAt(++n); + if ((c2 & 0xfc00) ^ (0xdc00 > 0)) { + throw new RangeError( + "Unmatched lead surrogate at " + (n - 1) + ); + } + c1 = ((c1 & 0x3ff) << 10) + (c2 & 0x3ff) + 0x10000; + enc = String.fromCharCode( + (c1 >> 18) | 240, + ((c1 >> 12) & 63) | 128, + ((c1 >> 6) & 63) | 128, + (c1 & 63) | 128 + ); + } + if (enc !== null) { + if (end > start) { + utftext += string.slice(start, end); + } + utftext += enc; + start = end = n + 1; + } + } + + if (end > start) { + utftext += string.slice(start, stringl); + } + + return utftext; + }; + + this.base64_decode = function (data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Thunder.m + // + input by: Aman Gupta + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Onno Marsman + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: Brett Zamir (http://brett-zamir.me) + // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA=='); + // * returns 1: 'Kevin van Zonneveld' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + var b64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, + o2, + o3, + h1, + h2, + h3, + h4, + bits, + i = 0, + ac = 0, + dec = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data += ""; + + do { + // unpack four hexets into three octets using index points in b64 + h1 = b64.indexOf(data.charAt(i++)); + h2 = b64.indexOf(data.charAt(i++)); + h3 = b64.indexOf(data.charAt(i++)); + h4 = b64.indexOf(data.charAt(i++)); + + bits = (h1 << 18) | (h2 << 12) | (h3 << 6) | h4; + + o1 = (bits >> 16) & 0xff; + o2 = (bits >> 8) & 0xff; + o3 = bits & 0xff; + + if (h3 === 64) { + tmp_arr[ac++] = String.fromCharCode(o1); + } else if (h4 === 64) { + tmp_arr[ac++] = String.fromCharCode(o1, o2); + } else { + tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); + } + } while (i < data.length); + + dec = tmp_arr.join(""); + + return dec; + }; + + /** + * encode data by base64 + * @param {String} data to encode + * @return {String} encoded data + */ + this.base64_encode = function (data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Bayron Guevara + // + improved by: Thunder.m + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // - depends on: this.utf8_encode + // * example 1: this.base64_encode('Kevin van Zonneveld'); + // * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA==' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + var b64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, + o2, + o3, + h1, + h2, + h3, + h4, + bits, + i = 0, + ac = 0, + enc = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data = this.utf8_encode(data + ""); + + do { + // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = (o1 << 16) | (o2 << 8) | o3; + + h1 = (bits >> 18) & 0x3f; + h2 = (bits >> 12) & 0x3f; + h3 = (bits >> 6) & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = + b64.charAt(h1) + + b64.charAt(h2) + + b64.charAt(h3) + + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(""); + + switch (data.length % 3) { + case 1: + enc = enc.slice(0, -2) + "=="; + break; + case 2: + enc = enc.slice(0, -1) + "="; + break; + } + + return enc; + }; + + /** + * encode string in url by base64 + * @param {String} string in url + * @return {String} encoded string + */ + this.URLSafeBase64Encode = function (v) { + v = this.base64_encode(v); + return v.replace(/\//g, "_").replace(/\+/g, "-"); + }; + + this.URLSafeBase64Decode = function (v) { + v = v.replace(/_/g, "/").replace(/-/g, "+"); + return this.base64_decode(v); + }; + + // TODO: use mOxie + /** + * craete object used to AJAX + * @return {Object} + */ + this.createAjax = function (argument) { + var xmlhttp = {}; + if (window.XMLHttpRequest) { + xmlhttp = new XMLHttpRequest(); + } else { + xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + return xmlhttp; + }; + + // TODO: enhance IE compatibility + /** + * parse json string to javascript object + * @param {String} json string + * @return {Object} object + */ + this.parseJSON = function (data) { + // Attempt to parse using the native JSON parser first + if (window.JSON && window.JSON.parse) { + return window.JSON.parse(data); + } + + //var rx_one = /^[\],:{}\s]*$/, + // rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + // rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + // rx_four = /(?:^|:|,)(?:\s*\[)+/g, + var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + + //var json; + + var text = String(data); + rx_dangerous.lastIndex = 0; + if (rx_dangerous.test(text)) { + text = text.replace(rx_dangerous, function (a) { + return ( + "\\u" + + ("0000" + a.charCodeAt(0).toString(16)).slice(-4) + ); + }); + } + + // todo 使用一下判断,增加安全性 + //if ( + // rx_one.test( + // text + // .replace(rx_two, '@') + // .replace(rx_three, ']') + // .replace(rx_four, '') + // ) + //) { + // return eval('(' + text + ')'); + //} + + return eval("(" + text + ")"); + }; + + /** + * parse javascript object to json string + * @param {Object} object + * @return {String} json string + */ + this.stringifyJSON = function (obj) { + // Attempt to parse using the native JSON parser first + if (window.JSON && window.JSON.stringify) { + return window.JSON.stringify(obj); + } + switch (typeof obj) { + case "string": + return '"' + obj.replace(/(["\\])/g, "\\$1") + '"'; + case "array": + return "[" + obj.map(that.stringifyJSON).join(",") + "]"; + case "object": + if (obj instanceof Array) { + var strArr = []; + var len = obj.length; + for (var i = 0; i < len; i++) { + strArr.push(that.stringifyJSON(obj[i])); + } + return "[" + strArr.join(",") + "]"; + } else if (obj === null) { + return "null"; + } else { + var string = []; + for (var property in obj) { + if (obj.hasOwnProperty(property)) { + string.push( + that.stringifyJSON(property) + + ":" + + that.stringifyJSON(obj[property]) + ); + } + } + return "{" + string.join(",") + "}"; + } + break; + case "number": + return obj; + case false: + return obj; + case "boolean": + return obj; + } + }; + + /** + * trim space beside text + * @param {String} untrimed string + * @return {String} trimed string + */ + this.trim = function (text) { + return text === null ? "" : text.replace(/^\s+|\s+$/g, ""); + }; + + /** + * create a uploader by QiniuJsSDK + * @param {object} options to create a new uploader + * @return {object} uploader + */ + this.uploader = function (op) { + /********** inner function define start **********/ + + // according the different condition to reset chunk size + // and the upload strategy according with the chunk size + // when chunk size is zero will cause to direct upload + // see the statement binded on 'BeforeUpload' event + var reset_chunk_size = function () { + var ie = that.detectIEVersion(); + var BLOCK_BITS, MAX_CHUNK_SIZE, chunk_size; + // case Safari 5、Windows 7、iOS 7 set isSpecialSafari to true + var isSpecialSafari = + (moxie.core.utils.Env.browser === "Safari" && + moxie.core.utils.Env.version <= 5 && + moxie.core.utils.Env.os === "Windows" && + moxie.core.utils.Env.osVersion === "7") || + (moxie.core.utils.Env.browser === "Safari" && + moxie.core.utils.Env.os === "iOS" && + moxie.core.utils.Env.osVersion === "7"); + // case IE 9-,chunk_size is not empty and flash is included in runtimes + // set op.chunk_size to zero + //if (ie && ie < 9 && op.chunk_size && op.runtimes.indexOf('flash') >= 0) { + if ( + ie && + ie < 9 && + op.chunk_size && + op.runtimes.indexOf("flash") >= 0 + ) { + // link: http://www.plupload.com/docs/Frequently-Asked-Questions#when-to-use-chunking-and-when-not + // when plupload chunk_size setting is't null ,it cause bug in ie8/9 which runs flash runtimes (not support html5) . + op.chunk_size = 0; + } else if (isSpecialSafari) { + // win7 safari / iOS7 safari have bug when in chunk upload mode + // reset chunk_size to 0 + // disable chunk in special version safari + op.chunk_size = 0; + } else { + BLOCK_BITS = 20; + MAX_CHUNK_SIZE = 4 << BLOCK_BITS; //4M + + chunk_size = plupload.parseSize(op.chunk_size); + // qiniu service max_chunk_size is 4m + // reset chunk_size to max_chunk_size(4m) when chunk_size > 4m + } + + // if op.chunk_size set 0 will be cause to direct upload + }; + + var getHosts = function (hosts) { + var result = []; + for (var i = 0; i < hosts.length; i++) { + var host = hosts[i]; + if (host.indexOf("-H") === 0) { + result.push(host.split(" ")[2]); + } else { + result.push(host); + } + } + return result; + }; + + var getPutPolicy = function (uptoken) { + return ""; + }; + + var getUpHosts = function (uptoken) { + var putPolicy = getPutPolicy(uptoken); + // var uphosts_url = "//uc.qbox.me/v1/query?ak="+ak+"&bucket="+putPolicy.scope; + // IE 9- is not support protocal relative url + var uphosts_url = + window.location.protocol + + "//uc.qbox.me/v1/query?ak=" + + putPolicy.ak + + "&bucket=" + + putPolicy.bucket; + logger.debug("putPolicy: ", putPolicy); + logger.debug("get uphosts from: ", uphosts_url); + var ie = that.detectIEVersion(); + var ajax; + if (ie && ie <= 9) { + ajax = new moxie.XMLHttpRequest(); + moxie.core.utils.Env.swf_url = op.flash_swf_url; + } else { + ajax = that.createAjax(); + } + if (uploadConfig.saveType != "qiniu") { + qiniuUpHosts.http = [uploadConfig.upUrl]; + qiniuUpHosts.http = [uploadConfig.upUrl]; + that.resetUploadUrl(); + } else { + ajax.open("GET", uphosts_url, false); + var onreadystatechange = function () { + logger.debug("ajax.readyState: ", ajax.readyState); + if (ajax.readyState === 4) { + logger.debug("ajax.status: ", ajax.status); + if (ajax.status < 400) { + var res = that.parseJSON(ajax.responseText); + qiniuUpHosts.http = getHosts(res.http.up); + qiniuUpHosts.https = getHosts(res.https.up); + logger.debug("get new uphosts: ", qiniuUpHosts); + that.resetUploadUrl(); + } else { + logger.error( + "get uphosts error: ", + ajax.responseText + ); + } + } + }; + if (ie && ie <= 9) { + ajax.bind("readystatechange", onreadystatechange); + } else { + ajax.onreadystatechange = onreadystatechange; + } + ajax.send(); + // ajax.send(); + // if (ajax.status < 400) { + // var res = that.parseJSON(ajax.responseText); + // qiniuUpHosts.http = getHosts(res.http.up); + // qiniuUpHosts.https = getHosts(res.https.up); + // logger.debug("get new uphosts: ", qiniuUpHosts); + // that.resetUploadUrl(); + // } else { + // logger.error("get uphosts error: ", ajax.responseText); + // } + } + return; + }; + + var getUptoken = function (file) { + if (uploadConfig.saveType == "remote") { + return that.token; + } + if ( + !that.token || + (op.uptoken_url && that.tokenInfo.isExpired()) + ) { + return getNewUpToken(file); + } else { + return that.token; + } + }; + + // getNewUptoken maybe called at Init Event or BeforeUpload Event + // case Init Event, the file param of getUptken will be set a null value + // if op.uptoken has value, set uptoken with op.uptoken + // else if op.uptoken_url has value, set uptoken from op.uptoken_url + // else if op.uptoken_func has value, set uptoken by result of op.uptoken_func + var getNewUpToken = function (file, callback) { + if (file.qiniuUpHosts) { + file.qiniuUpHosts = qiniuUpHosts; + that.resetUploadUrl(); + callback(); + return; + } + if (op.uptoken) { + that.token = op.uptoken; + callback(); + } else if (op.uptoken_url) { + logger.debug("get uptoken from: ", that.uptoken_url); + // TODO: use mOxie + var ajax = that.createAjax(); + if (file.size === undefined) { + file.size = 0; + } + ajax.open( + "GET", + that.uptoken_url + + "?path=" + + encodeURIComponent(window.pathCache[file.id]) + + "&size=" + + file.size + + "&name=" + + encodeURIComponent(file.name) + + "&type=onedrive", + true + ); + ajax.setRequestHeader("If-Modified-Since", "0"); + ajax.send(); + ajax.onload = function (e) { + if (ajax.status === 200) { + var res = that.parseJSON(ajax.responseText); + if (res.code != 0) { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + message: res.msg, + code: 402, + }); + callback(); + return; + } + if (res.data.policy) { + qiniuUpHosts.http = [res.data.policy]; + qiniuUpHosts.https = [res.data.policy]; + file.callbackURL = res.data.token; + file.qiniuUpHosts = qiniuUpHosts; + file.offset = 0; + that.resetUploadUrl(); + } else { + qiniuUpHosts.http = [uploadConfig.upUrl]; + qiniuUpHosts.https = [uploadConfig.upUrl]; + that.resetUploadUrl(); + } + } else { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + code: 402, + }); + } + callback(); + }; + + ajax.onerror = function (e) { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + code: 402, + }); + callback(); + logger.error("get uptoken error: ", ajax.responseText); + }; + } else if (op.uptoken_func) { + logger.debug("get uptoken from uptoken_func"); + that.token = op.uptoken_func(file); + logger.debug("get new uptoken: ", that.token); + callback(); + } else { + logger.error( + "one of [uptoken, uptoken_url, uptoken_func] settings in options is required!" + ); + callback(); + } + return that.token; + }; + + // get file key according with the user passed options + var getFileKey = function (up, file, func) { + // WARNING + // When you set the key in putPolicy by "scope": "bucket:key" + // You should understand the risk of override a file in the bucket + // So the code below that automatically get key from uptoken has been commented + // var putPolicy = getPutPolicy(that.token) + // if (putPolicy.key) { + // logger.debug("key is defined in putPolicy.scope: ", putPolicy.key) + // return putPolicy.key + // } + var key = "", + unique_names = false; + if (!op.save_key) { + unique_names = up.getOption && up.getOption("unique_names"); + unique_names = + unique_names || + (up.settings && up.settings.unique_names); + if (unique_names) { + var ext = that.getFileExtension(file.name); + key = ext ? file.id + "." + ext : file.id; + } else if (typeof func === "function") { + key = func(up, file); + } else { + key = file.name; + } + } + if (uploadConfig.saveType == "qiniu") { + return ""; + } + return key; + }; + + /********** inner function define end **********/ + + if (op.log_level) { + logger.level = op.log_level; + } + + if (!op.domain) { + throw "domain setting in options is required!"; + } + + if (!op.browse_button) { + throw "browse_button setting in options is required!"; + } + + if (!op.uptoken && !op.uptoken_url && !op.uptoken_func) { + throw "one of [uptoken, uptoken_url, uptoken_func] settings in options is required!"; + } + + logger.debug("init uploader start"); + + logger.debug("environment: ", moxie.core.utils.Env); + + logger.debug("userAgent: ", navigator.userAgent); + + var option = {}; + + // hold the handler from user passed options + var _Error_Handler = op.init && op.init.Error; + var _FileUploaded_Handler = op.init && op.init.FileUploaded; + + // replace the handler for intercept + op.init.Error = function () {}; + op.init.FileUploaded = function () {}; + + that.uptoken_url = op.uptoken_url; + that.token = ""; + that.key_handler = + typeof op.init.Key === "function" ? op.init.Key : ""; + this.domain = op.domain; + // TODO: ctx is global in scope of a uploader instance + // this maybe cause error + var ctx = ""; + var speedCalInfo = { + isResumeUpload: false, + resumeFilesize: 0, + startTime: "", + currentTime: "", + }; + + reset_chunk_size(); + logger.debug("invoke reset_chunk_size()"); + logger.debug("op.chunk_size: ", op.chunk_size); + + var defaultSetting = { + url: qiniuUploadUrl, + multipart_params: {}, + }; + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + defaultSetting.multipart_params.accept = + "text/plain; charset=utf-8"; + logger.debug("add accept text/plain in multipart params"); + } + + // compose options with user passed options and default setting + plupload.extend(option, op, defaultSetting); + + logger.debug("option: ", option); + + // create a new uploader with composed options + var uploader = new plupload.Uploader(option); + + logger.debug("new plupload.Uploader(option)"); + + // bind getNewUpToken to 'Init' event + uploader.bind("Init", function (up, params) { + logger.debug("Init event activated"); + // if op.get_new_uptoken is not true + // invoke getNewUptoken when uploader init + // else + // getNewUptoken everytime before a new file upload + if (!op.get_new_uptoken) { + getNewUpToken(null); + } + //getNewUpToken(null); + }); + + logger.debug("bind Init event"); + + // bind 'FilesAdded' event + // when file be added and auto_start has set value + // uploader will auto start upload the file + uploader.bind("FilesAdded", function (up, files) { + logger.debug("FilesAdded event activated"); + var auto_start = up.getOption && up.getOption("auto_start"); + auto_start = + auto_start || (up.settings && up.settings.auto_start); + logger.debug("auto_start: ", auto_start); + logger.debug("files: ", files); + + // detect is iOS + var is_ios = function () { + if (moxie.core.utils.Env.OS.toLowerCase() === "ios") { + return true; + } else { + return false; + } + }; + + // if current env os is iOS change file name to [time].[ext] + if (is_ios()) { + for (var i = 0; i < files.length; i++) { + var file = files[i]; + var ext = that.getFileExtension(file.name); + file.name = file.id + "." + ext; + } + } + + if (auto_start) { + setTimeout(function () { + up.start(); + logger.debug("invoke up.start()"); + }, 0); + // up.start(); + // plupload.each(files, function(i, file) { + // up.start(); + // logger.debug("invoke up.start()") + // logger.debug("file: ", file); + // }); + } + up.refresh(); // Reposition Flash/Silverlight + }); + + logger.debug("bind FilesAdded event"); + + // bind 'BeforeUpload' event + // intercept the process of upload + // - prepare uptoken + // - according the chunk size to make differnt upload strategy + // - resume upload with the last breakpoint of file + uploader.bind("BeforeUpload", function (up, file) { + logger.debug("BeforeUpload event activated"); + // add a key named speed for file object + file.speed = file.speed || 0; + ctx = ""; + + var directUpload = function (up, file, func) { + speedCalInfo.startTime = new Date().getTime(); + var multipart_params_obj; + if (op.save_key) { + multipart_params_obj = { + token: that.token, + }; + } else { + multipart_params_obj = { + key: getFileKey(up, file, func), + token: that.token, + }; + } + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + multipart_params_obj.accept = + "text/plain; charset=utf-8"; + logger.debug( + "add accept text/plain in multipart params" + ); + } + + logger.debug( + "directUpload multipart_params_obj: ", + multipart_params_obj + ); + + var x_vars = op.x_vars; + if (x_vars !== undefined && typeof x_vars === "object") { + for (var x_key in x_vars) { + if (x_vars.hasOwnProperty(x_key)) { + if (typeof x_vars[x_key] === "function") { + multipart_params_obj["x:" + x_key] = x_vars[ + x_key + ](up, file); + } else if (typeof x_vars[x_key] !== "object") { + multipart_params_obj["x:" + x_key] = + x_vars[x_key]; + } + } + } + } + + if (file.size > 4 * 1024 * 1024) { + up.setOption({ + url: qiniuUploadUrl, + multipart: false, + send_file_name: false, + chunk_size: op.max_file_size, + }); + } else { + up.setOption({ + http_method: "post", + url: qiniuUploadUrl, + multipart: false, + send_file_name: false, + headers: { + "X-Path": encodeURIComponent(file.path), + "X-FileName": encodeURIComponent( + getFileKey(up, file, func) + ), + }, + }); + } + }; + + // detect is weixin or qq inner browser + var is_android_weixin_or_qq = function () { + var ua = navigator.userAgent.toLowerCase(); + if ( + (ua.match(/MicroMessenger/i) || + moxie.core.utils.Env.browser === "QQBrowser" || + ua.match(/V1_AND_SQ/i)) && + moxie.core.utils.Env.OS.toLowerCase() === "android" + ) { + return true; + } else { + return false; + } + }; + + var chunk_size = up.getOption && up.getOption("chunk_size"); + chunk_size = + chunk_size || (up.settings && up.settings.chunk_size); + + logger.debug("uploader.runtime: ", uploader.runtime); + logger.debug("chunk_size: ", chunk_size); + + getNewUpToken(file, () => { + if (that.token) { + getUpHosts(that.token); + } + if ( + (uploader.runtime === "html5" || + uploader.runtime === "flash") && + chunk_size + ) { + if ( + file.size < 4 * 1024 * 1024 || + is_android_weixin_or_qq() + ) { + logger.debug( + "directUpload because file.size < chunk_size || is_android_weixin_or_qq()" + ); + // direct upload if file size is less then the chunk size + directUpload(up, file, that.key_handler); + } else { + // TODO: need a polifill to make it work in IE 9- + // ISSUE: if file.name is existed in localStorage + // but not the same file maybe cause error + var localFileInfo = localStorage.getItem(file.name); + var blockSize = chunk_size; + var offsetNow = 0; + if (localFileInfo) { + // TODO: although only the html5 runtime will enter this statement + // but need uniform way to make convertion between string and json + localFileInfo = that.parseJSON(localFileInfo); + var now = new Date().getTime(); + var before = localFileInfo.time || 0; + var aDay = 24 * 60 * 60 * 1000; // milliseconds of one day + // if the last upload time is within one day + // will upload continuously follow the last breakpoint + // else + // will reupload entire file + if (now - before < aDay) { + if (localFileInfo.percent !== 100) { + if (file.size === localFileInfo.total) { + // TODO: if file.name and file.size is the same + // but not the same file will cause error + file.percent = + localFileInfo.percent; + file.loaded = localFileInfo.offset; + ctx = localFileInfo.ctx; + offsetNow = localFileInfo.offset; + // set speed info + speedCalInfo.isResumeUpload = true; + speedCalInfo.resumeFilesize = + localFileInfo.offset; + + qiniuUpHosts = localFileInfo.url; + that.resetUploadUrl(); + + // set block size + if ( + localFileInfo.offset + + blockSize > + file.size + ) { + blockSize = + file.size - + localFileInfo.offset; + } + } else { + // remove file info when file.size is conflict with file info + localStorage.removeItem(file.name); + } + } else { + // remove file info when upload percent is 100% + // avoid 499 bug + localStorage.removeItem(file.name); + } + } else { + // remove file info when last upload time is over one day + localStorage.removeItem(file.name); + } + } + speedCalInfo.startTime = new Date().getTime(); + var multipart_params_obj = {}; + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + multipart_params_obj.accept = + "text/plain; charset=utf-8"; + logger.debug( + "add accept text/plain in multipart params" + ); + } + // TODO: to support bput + // http://developer.qiniu.com/docs/v6/api/reference/up/bput.html + var left = file.loaded + blockSize - 1; + if (file.size - file.loaded < chunk_size) { + left = file.size - 1; + } + up.setOption({ + http_method: "put", + url: qiniuUploadUrl, + multipart: false, + chunk_size: chunk_size, + required_features: "chunks", + headers: { + "Content-Range": + "bytes " + + file.loaded + + "-" + + left + + "/" + + file.size, + }, + }); + } + } else { + logger.debug( + "directUpload because uploader.runtime !== 'html5' || uploader.runtime !== 'flash' || !chunk_size" + ); + // direct upload if runtime is not html5 + directUpload(up, file, that.key_handler); + } + if (file.status != plupload.FAILED) { + file.status = plupload.UPLOADING; + up.trigger("UploadFile", file); + } else { + up.stop(); + } + }); + return false; + }); + + logger.debug("bind BeforeUpload event"); + + // bind 'UploadProgress' event + // calculate upload speed + uploader.bind("UploadProgress", function (up, file) { + logger.trace("UploadProgress event activated"); + speedCalInfo.currentTime = new Date().getTime(); + var timeUsed = + speedCalInfo.currentTime - speedCalInfo.startTime; // ms + var fileUploaded = file.loaded || 0; + if (speedCalInfo.isResumeUpload) { + fileUploaded = file.loaded - speedCalInfo.resumeFilesize; + } + file.speed = ((fileUploaded / timeUsed) * 1000).toFixed(0) || 0; // unit: byte/s + }); + + logger.debug("bind UploadProgress event"); + + // bind 'ChunkUploaded' event + // store the chunk upload info and set next chunk upload url + uploader.bind("ChunkUploaded", function (up, file, info) { + logger.debug("ChunkUploaded event activated"); + logger.debug("file: ", file); + logger.debug("info: ", info); + var res = that.parseJSON(info.response); + logger.debug("res: ", res); + // ctx should look like '[chunk01_ctx],[chunk02_ctx],[chunk03_ctx],...' + ctx = ctx ? ctx + "," + res.ctx : res.ctx; + var leftSize = info.total - info.offset; + var chunk_size = up.getOption && up.getOption("chunk_size"); + chunk_size = + chunk_size || (up.settings && up.settings.chunk_size); + if (leftSize < chunk_size) { + up.setOption({ + headers: { + "Content-Range": + "bytes " + + info.offset + + "-" + + (info.total - 1) + + "/" + + info.total, + }, + }); + } else { + up.setOption({ + headers: { + "Content-Range": + "bytes " + + info.offset + + "-" + + (info.offset + chunk_size - 1) + + "/" + + info.total, + }, + }); + } + }); + + logger.debug("bind ChunkUploaded event"); + + var retries = qiniuUploadUrls.length; + + // if error is unkown switch upload url and retry + var unknow_error_retry = function (file) { + console.log("重试"); + if (!file.qiniuUpHosts) { + return false; + } + + setTimeout(function () { + // 查询文件上传状态 + // 是分片上传 + ajax = that.createAjax(); + ajax.open("GET", file.qiniuUpHosts.http, true); + var onreadystatechange = function () { + logger.debug("ajax.readyState: ", ajax.readyState); + if (ajax.readyState === 4) { + var info; + if (ajax.status === 200) { + info = ajax.responseText; + var res = that.parseJSON(info); + if (res.nextExpectedRanges) { + file.status = plupload.QUEUED; + file.loaded = parseInt( + res.nextExpectedRanges[0].split("-")[0], + 10 + ); + uploader.stop(); + uploader.start(); + } else { + info = { + status: ajax.status, + response: ajax.responseText, + file: file, + code: plupload.HTTP_ERROR, + responseHeaders: ajax.getAllResponseHeaders(), + }; + uploader.trigger("Error", info); + } + } else { + info = { + status: ajax.status, + response: ajax.responseText, + file: file, + code: plupload.HTTP_ERROR, + responseHeaders: ajax.getAllResponseHeaders(), + }; + uploader.trigger("Error", info); + } + } + }; + ajax.onreadystatechange = onreadystatechange; + ajax.send(); + }, 3000); + return true; + }; + + // bind 'Error' event + // check the err.code and return the errTip + uploader.bind( + "Error", + (function (_Error_Handler) { + return function (up, err) { + logger.error("Error event activated"); + logger.error("err: ", err); + var errTip = ""; + var file = err.file; + if (file) { + switch (err.code) { + case plupload.FAILED: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.FILE_SIZE_ERROR: + var max_file_size = + up.getOption && + up.getOption("max_file_size"); + max_file_size = + max_file_size || + (up.settings && + up.settings.max_file_size); + errTip = + "文件过大,您当前用户组最多可上传" + + max_file_size + + "的文件"; + break; + case plupload.FILE_EXTENSION_ERROR: + errTip = "您当前的用户组不可上传此文件"; + break; + case plupload.HTTP_ERROR: + if (err.response === "") { + // Fix parseJSON error ,when http error is like net::ERR_ADDRESS_UNREACHABLE + errTip = + err.message + + "3秒钟后会进行重试。" || + "未知网络错误,3秒钟后会进行重试。"; + if (!unknow_error_retry(file)) { + return; + } + break; + } + try { + var errorObj = that.parseJSON( + err.response + ); + if (errorObj.msg) { + var errorText = errorObj.msg; + } else { + var errorText = + errorObj.error.message; + } + } catch (e) { + errorText = err.response; + } + errTip = errorText; + break; + case plupload.SECURITY_ERROR: + errTip = "安全配置错误。请联系网站管理员。"; + break; + case plupload.GENERIC_ERROR: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.IO_ERROR: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.INIT_ERROR: + errTip = "网站配置错误。请联系网站管理员。"; + uploader.destroy(); + break; + case 402: + errTip = "无法获取上传凭证"; + if (err.message) { + errTip = err.message; + } + break; + case 403: + errTip = "无法完成文件上传"; + break; + case 404: + var errorObj = that.parseJSON(err.response); + var errorText = errorObj.msg; + errTip = errorText; + break; + default: + errTip = + err.message + + err.details + + "(3秒钟后会进行重试)"; + if (!unknow_error_retry(file)) { + return; + } + break; + } + if (_Error_Handler) { + _Error_Handler(up, err, errTip); + } + } + up.refresh(); // Reposition Flash/Silverlight + }; + })(_Error_Handler) + ); + + logger.debug("bind Error event"); + + // bind 'FileUploaded' event + // intercept the complete of upload + // - get downtoken from downtoken_url if bucket is private + // - invoke mkfile api to compose chunks if upload strategy is chunk upload + uploader.bind( + "FileUploaded", + (function (_FileUploaded_Handler) { + return function (up, file, info) { + logger.debug("FileUploaded event activated"); + logger.debug("file: ", file); + logger.debug("info: ", info); + + var last_step = function (up, file, info) { + if (_FileUploaded_Handler) { + _FileUploaded_Handler(up, file, info); + } + }; + var res = that.parseJSON(info.response); + if (res.eTag) { + // 是分片上传 + ajax = that.createAjax(); + ajax.open("POST", file.callbackURL, true); + var onreadystatechange = function () { + logger.debug( + "ajax.readyState: ", + ajax.readyState + ); + if (ajax.readyState === 4) { + var info; + if (ajax.status === 200) { + info = ajax.responseText; + var res = that.parseJSON(info); + if (res.code == 0) { + localStorage.removeItem(file.name); + up.trigger("Fresh"); + last_step(up, file, info); + } else { + info = { + status: ajax.status, + response: ajax.responseText, + file: file, + code: 404, + responseHeaders: ajax.getAllResponseHeaders(), + }; + logger.debug( + "mkfile is error: ", + info + ); + uploader.trigger("Error", info); + } + } else { + info = { + status: ajax.status, + response: ajax.responseText, + file: file, + code: 403, + responseHeaders: ajax.getAllResponseHeaders(), + }; + logger.debug("mkfile is error: ", info); + uploader.trigger("Error", info); + } + } + }; + ajax.onreadystatechange = onreadystatechange; + ajax.send(info.response); + } else { + up.trigger("Fresh"); + last_step(up, file, info); + } + }; + })(_FileUploaded_Handler) + ); + + logger.debug("bind FileUploaded event"); + + // init uploader + uploader.init(); + + logger.debug("invoke uploader.init()"); + + logger.debug("init uploader end"); + + return uploader; + }; + } + + var Qiniu = new QiniuJsSDK(); + + global.Qiniu = Qiniu; + + global.QiniuJsSDK = QiniuJsSDK; +})(window); diff --git a/assets/public/static/js/uploader/uploader_oss.js b/assets/public/static/js/uploader/uploader_oss.js new file mode 100644 index 00000000..b54adf93 --- /dev/null +++ b/assets/public/static/js/uploader/uploader_oss.js @@ -0,0 +1,1984 @@ +function getCookieByString(cookieName) { + var start = document.cookie.indexOf(cookieName + "="); + if (start == -1) return false; + start = start + cookieName.length + 1; + var end = document.cookie.indexOf(";", start); + if (end == -1) end = document.cookie.length; + return document.cookie.substring(start, end); +} +(function (global) { + /** + * Creates new cookie or removes cookie with negative expiration + * @param key The key or identifier for the store + * @param value Contents of the store + * @param exp Expiration - creation defaults to 30 days + */ + function createCookie(key, value, exp) { + var date = new Date(); + date.setTime(date.getTime() + exp * 24 * 60 * 60 * 1000); + var expires = "; expires=" + date.toGMTString(); + document.cookie = key + "=" + value + expires + "; path=/"; + } + + /** + * Returns contents of cookie + * @param key The key or identifier for the store + */ + function readCookie(key) { + var nameEQ = key + "="; + var ca = document.cookie.split(";"); + for (var i = 0, max = ca.length; i < max; i++) { + var c = ca[i]; + while (c.charAt(0) === " ") { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return c.substring(nameEQ.length, c.length); + } + } + return null; + } + + // if current browser is not support localStorage + // use cookie to make a polyfill + if (!window.localStorage) { + window.localStorage = { + setItem: function (key, value) { + createCookie(key, value, 30); + }, + getItem: function (key) { + return readCookie(key); + }, + removeItem: function (key) { + createCookie(key, "", -1); + }, + }; + } + + function QiniuJsSDK() { + var that = this; + + /** + * detect IE version + * if current browser is not IE + * it will return false + * else + * it will return version of current IE browser + * @return {Number|Boolean} IE version or false + */ + this.detectIEVersion = function () { + var v = 4, + div = document.createElement("div"), + all = div.getElementsByTagName("i"); + while ( + ((div.innerHTML = + ""), + all[0]) + ) { + v++; + } + return v > 4 ? v : false; + }; + + var logger = { + MUTE: 0, + FATA: 1, + ERROR: 2, + WARN: 3, + INFO: 4, + DEBUG: 5, + TRACE: 6, + level: 0, + }; + + function log(type, args) { + var header = "[Cloudreve-uploader][" + type + "]"; + var msg = header; + for (var i = 0; i < args.length; i++) { + if (typeof args[i] === "string") { + msg += " " + args[i]; + } else { + msg += " " + that.stringifyJSON(args[i]); + } + } + if (that.detectIEVersion()) { + // http://stackoverflow.com/questions/5538972/console-log-apply-not-working-in-ie9 + //var log = Function.prototype.bind.call(console.log, console); + //log.apply(console, args); + console.log(msg); + } else { + args.unshift(header); + console.log.apply(console, args); + } + if (document.getElementById("qiniu-js-sdk-log")) { + document.getElementById("qiniu-js-sdk-log").innerHTML += + "

" + msg + "

"; + } + } + + function makeLogFunc(code) { + var func = code.toLowerCase(); + logger[func] = function () { + // logger[func].history = logger[func].history || []; + // logger[func].history.push(arguments); + if ( + window.console && + window.console.log && + logger.level >= logger[code] + ) { + var args = Array.prototype.slice.call(arguments); + log(func, args); + } + }; + } + + for (var property in logger) { + if ( + logger.hasOwnProperty(property) && + typeof logger[property] === "number" && + !logger.hasOwnProperty(property.toLowerCase()) + ) { + makeLogFunc(property); + } + } + + var qiniuUploadUrl; + + /** + * qiniu upload urls + * 'qiniuUploadUrls' is used to change target when current url is not avaliable + * @type {Array} + */ + if (uploadConfig.saveType == "qiniu") { + if (window.location.protocol === "https:") { + qiniuUploadUrl = "https://up.qbox.me"; + } else { + qiniuUploadUrl = "http://upload.qiniu.com"; + } + var qiniuUploadUrls = [ + "http://upload.qiniu.com", + "http://up.qiniu.com", + ]; + + var qiniuUpHosts = { + http: ["http://upload.qiniu.com", "http://up.qiniu.com"], + https: ["https://up.qbox.me"], + }; + //TODO 优化写法 + } else if ( + uploadConfig.saveType == "local" || + uploadConfig.saveType == "oss" || + uploadConfig.saveType == "upyun" || + uploadConfig.saveType == "s3" || + uploadConfig.saveType == "remote" || + uploadConfig.saveType == "onedrive" + ) { + qiniuUploadUrl = uploadConfig.upUrl; + var qiniuUploadUrls = [uploadConfig.upUrl]; + var qiniuUpHosts = { + http: [uploadConfig.upUrl], + https: [uploadConfig.upUrl], + }; + } + + var changeUrlTimes = 0; + + /** + * reset upload url + * if current page protocal is https + * it will always return 'https://up.qbox.me' + * else + * it will set 'qiniuUploadUrl' value with 'qiniuUploadUrls' looply + */ + this.resetUploadUrl = function () { + var hosts = + window.location.protocol === "https:" + ? qiniuUpHosts.https + : qiniuUpHosts.http; + var i = changeUrlTimes % hosts.length; + qiniuUploadUrl = hosts[i]; + changeUrlTimes++; + logger.debug("resetUploadUrl: " + qiniuUploadUrl); + }; + + // this.resetUploadUrl(); + + /** + * is image + * @param {String} url of a file + * @return {Boolean} file is a image or not + */ + this.isImage = function (url) { + url = url.split(/[?#]/)[0]; + return /\.(png|jpg|jpeg|gif|bmp)$/i.test(url); + }; + + /** + * get file extension + * @param {String} filename + * @return {String} file extension + * @example + * input: test.txt + * output: txt + */ + this.getFileExtension = function (filename) { + var tempArr = filename.split("."); + var ext; + if ( + tempArr.length === 1 || + (tempArr[0] === "" && tempArr.length === 2) + ) { + ext = ""; + } else { + ext = tempArr.pop().toLowerCase(); //get the extension and make it lower-case + } + return ext; + }; + + /** + * encode string by utf8 + * @param {String} string to encode + * @return {String} encoded string + */ + this.utf8_encode = function (argString) { + // http://kevin.vanzonneveld.net + // + original by: Webtoolkit.info (http://www.webtoolkit.info/) + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + improved by: sowberry + // + tweaked by: Jack + // + bugfixed by: Onno Marsman + // + improved by: Yves Sucaet + // + bugfixed by: Onno Marsman + // + bugfixed by: Ulrich + // + bugfixed by: Rafal Kukawski + // + improved by: kirilloid + // + bugfixed by: kirilloid + // * example 1: this.utf8_encode('Kevin van Zonneveld'); + // * returns 1: 'Kevin van Zonneveld' + + if (argString === null || typeof argString === "undefined") { + return ""; + } + + var string = argString + ""; // .replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + var utftext = "", + start, + end, + stringl = 0; + + start = end = 0; + stringl = string.length; + for (var n = 0; n < stringl; n++) { + var c1 = string.charCodeAt(n); + var enc = null; + + if (c1 < 128) { + end++; + } else if (c1 > 127 && c1 < 2048) { + enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); + } else if ((c1 & 0xf800) ^ (0xd800 > 0)) { + enc = String.fromCharCode( + (c1 >> 12) | 224, + ((c1 >> 6) & 63) | 128, + (c1 & 63) | 128 + ); + } else { + // surrogate pairs + if ((c1 & 0xfc00) ^ (0xd800 > 0)) { + throw new RangeError( + "Unmatched trail surrogate at " + n + ); + } + var c2 = string.charCodeAt(++n); + if ((c2 & 0xfc00) ^ (0xdc00 > 0)) { + throw new RangeError( + "Unmatched lead surrogate at " + (n - 1) + ); + } + c1 = ((c1 & 0x3ff) << 10) + (c2 & 0x3ff) + 0x10000; + enc = String.fromCharCode( + (c1 >> 18) | 240, + ((c1 >> 12) & 63) | 128, + ((c1 >> 6) & 63) | 128, + (c1 & 63) | 128 + ); + } + if (enc !== null) { + if (end > start) { + utftext += string.slice(start, end); + } + utftext += enc; + start = end = n + 1; + } + } + + if (end > start) { + utftext += string.slice(start, stringl); + } + + return utftext; + }; + + this.base64_decode = function (data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Thunder.m + // + input by: Aman Gupta + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Onno Marsman + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: Brett Zamir (http://brett-zamir.me) + // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA=='); + // * returns 1: 'Kevin van Zonneveld' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + var b64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, + o2, + o3, + h1, + h2, + h3, + h4, + bits, + i = 0, + ac = 0, + dec = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data += ""; + + do { + // unpack four hexets into three octets using index points in b64 + h1 = b64.indexOf(data.charAt(i++)); + h2 = b64.indexOf(data.charAt(i++)); + h3 = b64.indexOf(data.charAt(i++)); + h4 = b64.indexOf(data.charAt(i++)); + + bits = (h1 << 18) | (h2 << 12) | (h3 << 6) | h4; + + o1 = (bits >> 16) & 0xff; + o2 = (bits >> 8) & 0xff; + o3 = bits & 0xff; + + if (h3 === 64) { + tmp_arr[ac++] = String.fromCharCode(o1); + } else if (h4 === 64) { + tmp_arr[ac++] = String.fromCharCode(o1, o2); + } else { + tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); + } + } while (i < data.length); + + dec = tmp_arr.join(""); + + return dec; + }; + + /** + * encode data by base64 + * @param {String} data to encode + * @return {String} encoded data + */ + this.base64_encode = function (data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Bayron Guevara + // + improved by: Thunder.m + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // - depends on: this.utf8_encode + // * example 1: this.base64_encode('Kevin van Zonneveld'); + // * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA==' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + var b64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, + o2, + o3, + h1, + h2, + h3, + h4, + bits, + i = 0, + ac = 0, + enc = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data = this.utf8_encode(data + ""); + + do { + // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = (o1 << 16) | (o2 << 8) | o3; + + h1 = (bits >> 18) & 0x3f; + h2 = (bits >> 12) & 0x3f; + h3 = (bits >> 6) & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = + b64.charAt(h1) + + b64.charAt(h2) + + b64.charAt(h3) + + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(""); + + switch (data.length % 3) { + case 1: + enc = enc.slice(0, -2) + "=="; + break; + case 2: + enc = enc.slice(0, -1) + "="; + break; + } + + return enc; + }; + + /** + * encode string in url by base64 + * @param {String} string in url + * @return {String} encoded string + */ + this.URLSafeBase64Encode = function (v) { + v = this.base64_encode(v); + return v.replace(/\//g, "_").replace(/\+/g, "-"); + }; + + this.URLSafeBase64Decode = function (v) { + v = v.replace(/_/g, "/").replace(/-/g, "+"); + return this.base64_decode(v); + }; + + // TODO: use mOxie + /** + * craete object used to AJAX + * @return {Object} + */ + this.createAjax = function (argument) { + var xmlhttp = {}; + if (window.XMLHttpRequest) { + xmlhttp = new XMLHttpRequest(); + } else { + xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + return xmlhttp; + }; + + // TODO: enhance IE compatibility + /** + * parse json string to javascript object + * @param {String} json string + * @return {Object} object + */ + this.parseJSON = function (data) { + // Attempt to parse using the native JSON parser first + if (window.JSON && window.JSON.parse) { + return window.JSON.parse(data); + } + + //var rx_one = /^[\],:{}\s]*$/, + // rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + // rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + // rx_four = /(?:^|:|,)(?:\s*\[)+/g, + var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + + //var json; + + var text = String(data); + rx_dangerous.lastIndex = 0; + if (rx_dangerous.test(text)) { + text = text.replace(rx_dangerous, function (a) { + return ( + "\\u" + + ("0000" + a.charCodeAt(0).toString(16)).slice(-4) + ); + }); + } + + // todo 使用一下判断,增加安全性 + //if ( + // rx_one.test( + // text + // .replace(rx_two, '@') + // .replace(rx_three, ']') + // .replace(rx_four, '') + // ) + //) { + // return eval('(' + text + ')'); + //} + + return eval("(" + text + ")"); + }; + + /** + * parse javascript object to json string + * @param {Object} object + * @return {String} json string + */ + this.stringifyJSON = function (obj) { + // Attempt to parse using the native JSON parser first + if (window.JSON && window.JSON.stringify) { + return window.JSON.stringify(obj); + } + switch (typeof obj) { + case "string": + return '"' + obj.replace(/(["\\])/g, "\\$1") + '"'; + case "array": + return "[" + obj.map(that.stringifyJSON).join(",") + "]"; + case "object": + if (obj instanceof Array) { + var strArr = []; + var len = obj.length; + for (var i = 0; i < len; i++) { + strArr.push(that.stringifyJSON(obj[i])); + } + return "[" + strArr.join(",") + "]"; + } else if (obj === null) { + return "null"; + } else { + var string = []; + for (var property in obj) { + if (obj.hasOwnProperty(property)) { + string.push( + that.stringifyJSON(property) + + ":" + + that.stringifyJSON(obj[property]) + ); + } + } + return "{" + string.join(",") + "}"; + } + break; + case "number": + return obj; + case false: + return obj; + case "boolean": + return obj; + } + }; + + /** + * trim space beside text + * @param {String} untrimed string + * @return {String} trimed string + */ + this.trim = function (text) { + return text === null ? "" : text.replace(/^\s+|\s+$/g, ""); + }; + + /** + * create a uploader by QiniuJsSDK + * @param {object} options to create a new uploader + * @return {object} uploader + */ + this.uploader = function (op) { + /********** inner function define start **********/ + + // according the different condition to reset chunk size + // and the upload strategy according with the chunk size + // when chunk size is zero will cause to direct upload + // see the statement binded on 'BeforeUpload' event + var reset_chunk_size = function () { + var ie = that.detectIEVersion(); + var BLOCK_BITS, MAX_CHUNK_SIZE, chunk_size; + // case Safari 5、Windows 7、iOS 7 set isSpecialSafari to true + var isSpecialSafari = + (moxie.core.utils.Env.browser === "Safari" && + moxie.core.utils.Env.version <= 5 && + moxie.core.utils.Env.os === "Windows" && + moxie.core.utils.Env.osVersion === "7") || + (moxie.core.utils.Env.browser === "Safari" && + moxie.core.utils.Env.os === "iOS" && + moxie.core.utils.Env.osVersion === "7"); + // case IE 9-,chunk_size is not empty and flash is included in runtimes + // set op.chunk_size to zero + //if (ie && ie < 9 && op.chunk_size && op.runtimes.indexOf('flash') >= 0) { + if ( + ie && + ie < 9 && + op.chunk_size && + op.runtimes.indexOf("flash") >= 0 + ) { + // link: http://www.plupload.com/docs/Frequently-Asked-Questions#when-to-use-chunking-and-when-not + // when plupload chunk_size setting is't null ,it cause bug in ie8/9 which runs flash runtimes (not support html5) . + op.chunk_size = 0; + } else if (isSpecialSafari) { + // win7 safari / iOS7 safari have bug when in chunk upload mode + // reset chunk_size to 0 + // disable chunk in special version safari + op.chunk_size = 0; + } else { + BLOCK_BITS = 20; + MAX_CHUNK_SIZE = 4 << BLOCK_BITS; //4M + + chunk_size = plupload.parseSize(op.chunk_size); + + // qiniu service max_chunk_size is 4m + // reset chunk_size to max_chunk_size(4m) when chunk_size > 4m + } + // if op.chunk_size set 0 will be cause to direct upload + }; + + var getHosts = function (hosts) { + var result = []; + for (var i = 0; i < hosts.length; i++) { + var host = hosts[i]; + if (host.indexOf("-H") === 0) { + result.push(host.split(" ")[2]); + } else { + result.push(host); + } + } + return result; + }; + + var getPutPolicy = function (uptoken) { + if ( + uploadConfig.saveType == "local" || + uploadConfig.saveType == "oss" || + uploadConfig.saveType == "upyun" || + uploadConfig.saveType == "s3" || + uploadConfig.saveType == "remote" + ) { + return "oss"; + } else { + var segments = uptoken.split(":"); + var ak = segments[0]; + var putPolicy = that.parseJSON( + that.URLSafeBase64Decode(segments[2]) + ); + putPolicy.ak = ak; + if (putPolicy.scope.indexOf(":") >= 0) { + putPolicy.bucket = putPolicy.scope.split(":")[0]; + putPolicy.key = putPolicy.scope.split(":")[1]; + } else { + putPolicy.bucket = putPolicy.scope; + } + return putPolicy; + } + }; + + var getUpHosts = function (uptoken) { + var putPolicy = getPutPolicy(uptoken); + // var uphosts_url = "//uc.qbox.me/v1/query?ak="+ak+"&bucket="+putPolicy.scope; + // IE 9- is not support protocal relative url + var uphosts_url = + window.location.protocol + + "//uc.qbox.me/v1/query?ak=" + + putPolicy.ak + + "&bucket=" + + putPolicy.bucket; + logger.debug("putPolicy: ", putPolicy); + logger.debug("get uphosts from: ", uphosts_url); + var ie = that.detectIEVersion(); + var ajax; + if (ie && ie <= 9) { + ajax = new moxie.XMLHttpRequest(); + moxie.core.utils.Env.swf_url = op.flash_swf_url; + } else { + ajax = that.createAjax(); + } + if (uploadConfig.saveType != "qiniu") { + qiniuUpHosts.http = [uploadConfig.upUrl]; + qiniuUpHosts.http = [uploadConfig.upUrl]; + that.resetUploadUrl(); + } else { + ajax.open("GET", uphosts_url, false); + var onreadystatechange = function () { + logger.debug("ajax.readyState: ", ajax.readyState); + if (ajax.readyState === 4) { + logger.debug("ajax.status: ", ajax.status); + if (ajax.status < 400) { + var res = that.parseJSON(ajax.responseText); + qiniuUpHosts.http = getHosts(res.http.up); + qiniuUpHosts.https = getHosts(res.https.up); + logger.debug("get new uphosts: ", qiniuUpHosts); + that.resetUploadUrl(); + } else { + logger.error( + "get uphosts error: ", + ajax.responseText + ); + } + } + }; + if (ie && ie <= 9) { + ajax.bind("readystatechange", onreadystatechange); + } else { + ajax.onreadystatechange = onreadystatechange; + } + ajax.send(); + // ajax.send(); + // if (ajax.status < 400) { + // var res = that.parseJSON(ajax.responseText); + // qiniuUpHosts.http = getHosts(res.http.up); + // qiniuUpHosts.https = getHosts(res.https.up); + // logger.debug("get new uphosts: ", qiniuUpHosts); + // that.resetUploadUrl(); + // } else { + // logger.error("get uphosts error: ", ajax.responseText); + // } + } + return; + }; + + var getUptoken = function (file) { + if (uploadConfig.saveType == "remote") { + return that.token; + } + if ( + !that.token || + (op.uptoken_url && that.tokenInfo.isExpired()) + ) { + return getNewUpToken(file); + } else { + return that.token; + } + }; + + // getNewUptoken maybe called at Init Event or BeforeUpload Event + // case Init Event, the file param of getUptken will be set a null value + // if op.uptoken has value, set uptoken with op.uptoken + // else if op.uptoken_url has value, set uptoken from op.uptoken_url + // else if op.uptoken_func has value, set uptoken by result of op.uptoken_func + var getNewUpToken = function (file, callback) { + if (op.uptoken) { + that.token = op.uptoken; + callback(); + } else if (op.uptoken_url) { + logger.debug("get uptoken from: ", that.uptoken_url); + // TODO: use mOxie + var ajax = that.createAjax(); + if (file.size === undefined) { + file.size = 0; + } + ajax.open( + "GET", + that.uptoken_url + + "?path=" + + encodeURIComponent(window.pathCache[file.id]) + + "&size=" + + file.size + + "&name=" + + encodeURIComponent(file.name) + + "&type=oss", + true + ); + ajax.setRequestHeader("If-Modified-Since", "0"); + ajax.send(); + ajax.onload = function (e) { + if (ajax.status === 200) { + var res = that.parseJSON(ajax.responseText); + if (res.code != 0) { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + message: res.msg, + code: 402, + }); + callback(); + return; + } + var segments = res.data.policy.split(":"); + that.token = segments[1]; + var putPolicy = that.token; + that.sign = res.data.token; + that.access = res.data.ak; + that.file_name = res.data.path; + that.callback = segments[0]; + } else { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + code: 402, + }); + } + callback(); + }; + + ajax.onerror = function (e) { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + code: 402, + }); + callback(); + logger.error("get uptoken error: ", ajax.responseText); + }; + } else if (op.uptoken_func) { + logger.debug("get uptoken from uptoken_func"); + that.token = op.uptoken_func(file); + logger.debug("get new uptoken: ", that.token); + callback(); + } else { + logger.error( + "one of [uptoken, uptoken_url, uptoken_func] settings in options is required!" + ); + callback(); + } + return that.token; + }; + + // get file key according with the user passed options + var getFileKey = function (up, file, func) { + // WARNING + // When you set the key in putPolicy by "scope": "bucket:key" + // You should understand the risk of override a file in the bucket + // So the code below that automatically get key from uptoken has been commented + // var putPolicy = getPutPolicy(that.token) + // if (putPolicy.key) { + // logger.debug("key is defined in putPolicy.scope: ", putPolicy.key) + // return putPolicy.key + // } + var key = "", + unique_names = false; + if (!op.save_key) { + unique_names = up.getOption && up.getOption("unique_names"); + unique_names = + unique_names || + (up.settings && up.settings.unique_names); + if (unique_names) { + var ext = that.getFileExtension(file.name); + key = ext ? file.id + "." + ext : file.id; + } else if (typeof func === "function") { + key = func(up, file); + } else { + key = file.name; + } + } + if (uploadConfig.saveType == "qiniu") { + return ""; + } + return key; + }; + + /********** inner function define end **********/ + + if (op.log_level) { + logger.level = op.log_level; + } + + if (!op.domain) { + throw "domain setting in options is required!"; + } + + if (!op.browse_button) { + throw "browse_button setting in options is required!"; + } + + if (!op.uptoken && !op.uptoken_url && !op.uptoken_func) { + throw "one of [uptoken, uptoken_url, uptoken_func] settings in options is required!"; + } + + logger.debug("init uploader start"); + + logger.debug("environment: ", moxie.core.utils.Env); + + logger.debug("userAgent: ", navigator.userAgent); + + var option = {}; + + // hold the handler from user passed options + var _Error_Handler = op.init && op.init.Error; + var _FileUploaded_Handler = op.init && op.init.FileUploaded; + + // replace the handler for intercept + op.init.Error = function () {}; + op.init.FileUploaded = function () {}; + + that.uptoken_url = op.uptoken_url; + that.token = ""; + that.key_handler = + typeof op.init.Key === "function" ? op.init.Key : ""; + this.domain = op.domain; + // TODO: ctx is global in scope of a uploader instance + // this maybe cause error + var ctx = ""; + var speedCalInfo = { + isResumeUpload: false, + resumeFilesize: 0, + startTime: "", + currentTime: "", + }; + + reset_chunk_size(); + logger.debug("invoke reset_chunk_size()"); + logger.debug("op.chunk_size: ", op.chunk_size); + + var defaultSetting = { + url: qiniuUploadUrl, + multipart_params: {}, + }; + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + defaultSetting.multipart_params.accept = + "text/plain; charset=utf-8"; + logger.debug("add accept text/plain in multipart params"); + } + + // compose options with user passed options and default setting + plupload.extend(option, op, defaultSetting); + + logger.debug("option: ", option); + + // create a new uploader with composed options + var uploader = new plupload.Uploader(option); + + logger.debug("new plupload.Uploader(option)"); + + // bind getNewUpToken to 'Init' event + uploader.bind("Init", function (up, params) { + logger.debug("Init event activated"); + // if op.get_new_uptoken is not true + // invoke getNewUptoken when uploader init + // else + // getNewUptoken everytime before a new file upload + if (!op.get_new_uptoken) { + getNewUpToken(null); + } + //getNewUpToken(null); + }); + + logger.debug("bind Init event"); + + // bind 'FilesAdded' event + // when file be added and auto_start has set value + // uploader will auto start upload the file + uploader.bind("FilesAdded", function (up, files) { + logger.debug("FilesAdded event activated"); + var auto_start = up.getOption && up.getOption("auto_start"); + auto_start = + auto_start || (up.settings && up.settings.auto_start); + logger.debug("auto_start: ", auto_start); + logger.debug("files: ", files); + + // detect is iOS + var is_ios = function () { + if (moxie.core.utils.Env.OS.toLowerCase() === "ios") { + return true; + } else { + return false; + } + }; + + // if current env os is iOS change file name to [time].[ext] + if (is_ios()) { + for (var i = 0; i < files.length; i++) { + var file = files[i]; + var ext = that.getFileExtension(file.name); + file.name = file.id + "." + ext; + } + } + + if (auto_start) { + setTimeout(function () { + up.start(); + logger.debug("invoke up.start()"); + }, 0); + // up.start(); + // plupload.each(files, function(i, file) { + // up.start(); + // logger.debug("invoke up.start()") + // logger.debug("file: ", file); + // }); + } + up.refresh(); // Reposition Flash/Silverlight + }); + + logger.debug("bind FilesAdded event"); + + // bind 'BeforeUpload' event + // intercept the process of upload + // - prepare uptoken + // - according the chunk size to make differnt upload strategy + // - resume upload with the last breakpoint of file + uploader.bind("BeforeUpload", function (up, file) { + logger.debug("BeforeUpload event activated"); + // add a key named speed for file object + file.speed = file.speed || 0; + ctx = ""; + + var directUpload = function (up, file, func) { + speedCalInfo.startTime = new Date().getTime(); + var multipart_params_obj; + if (op.save_key) { + multipart_params_obj = { + token: that.token, + }; + } else { + multipart_params_obj = { + policy: that.token, + "x:path": file.path, + signature: that.sign, + OSSAccessKeyId: that.access, + "x:fname": file.name, + key: that.file_name.replace( + "${filename}", + file.name + ), + callback: that.callback, + }; + } + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + multipart_params_obj.accept = + "text/plain; charset=utf-8"; + logger.debug( + "add accept text/plain in multipart params" + ); + } + + logger.debug( + "directUpload multipart_params_obj: ", + multipart_params_obj + ); + + var x_vars = op.x_vars; + if (x_vars !== undefined && typeof x_vars === "object") { + for (var x_key in x_vars) { + if (x_vars.hasOwnProperty(x_key)) { + if (typeof x_vars[x_key] === "function") { + multipart_params_obj["x:" + x_key] = x_vars[ + x_key + ](up, file); + } else if (typeof x_vars[x_key] !== "object") { + multipart_params_obj["x:" + x_key] = + x_vars[x_key]; + } + } + } + } + + up.setOption({ + url: qiniuUploadUrl, + multipart: true, + chunk_size: is_android_weixin_or_qq() + ? op.max_file_size + : undefined, + multipart_params: multipart_params_obj, + headers: { + "X-Oss-Forbid-Overwrite": true, + }, + }); + }; + + // detect is weixin or qq inner browser + var is_android_weixin_or_qq = function () { + var ua = navigator.userAgent.toLowerCase(); + if ( + (ua.match(/MicroMessenger/i) || + moxie.core.utils.Env.browser === "QQBrowser" || + ua.match(/V1_AND_SQ/i)) && + moxie.core.utils.Env.OS.toLowerCase() === "android" + ) { + return true; + } else { + return false; + } + }; + + var chunk_size = up.getOption && up.getOption("chunk_size"); + chunk_size = + chunk_size || (up.settings && up.settings.chunk_size); + + logger.debug("uploader.runtime: ", uploader.runtime); + logger.debug("chunk_size: ", chunk_size); + + getNewUpToken(file, () => { + if (that.token) { + getUpHosts(that.token); + } + if ( + (uploader.runtime === "html5" || + uploader.runtime === "flash") && + chunk_size + ) { + if ( + file.size < chunk_size || + is_android_weixin_or_qq() + ) { + logger.debug( + "directUpload because file.size < chunk_size || is_android_weixin_or_qq()" + ); + // direct upload if file size is less then the chunk size + directUpload(up, file, that.key_handler); + } else { + // TODO: need a polifill to make it work in IE 9- + // ISSUE: if file.name is existed in localStorage + // but not the same file maybe cause error + var localFileInfo = localStorage.getItem(file.name); + var blockSize = chunk_size; + if (localFileInfo) { + // TODO: although only the html5 runtime will enter this statement + // but need uniform way to make convertion between string and json + localFileInfo = that.parseJSON(localFileInfo); + var now = new Date().getTime(); + var before = localFileInfo.time || 0; + var aDay = 24 * 60 * 60 * 1000; // milliseconds of one day + // if the last upload time is within one day + // will upload continuously follow the last breakpoint + // else + // will reupload entire file + if (now - before < aDay) { + if (localFileInfo.percent !== 100) { + if (file.size === localFileInfo.total) { + // TODO: if file.name and file.size is the same + // but not the same file will cause error + file.percent = + localFileInfo.percent; + file.loaded = localFileInfo.offset; + ctx = localFileInfo.ctx; + + // set speed info + speedCalInfo.isResumeUpload = true; + speedCalInfo.resumeFilesize = + localFileInfo.offset; + + // set block size + if ( + localFileInfo.offset + + blockSize > + file.size + ) { + blockSize = + file.size - + localFileInfo.offset; + } + } else { + // remove file info when file.size is conflict with file info + localStorage.removeItem(file.name); + } + } else { + // remove file info when upload percent is 100% + // avoid 499 bug + localStorage.removeItem(file.name); + } + } else { + // remove file info when last upload time is over one day + localStorage.removeItem(file.name); + } + } + speedCalInfo.startTime = new Date().getTime(); + var multipart_params_obj = {}; + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + multipart_params_obj.accept = + "text/plain; charset=utf-8"; + logger.debug( + "add accept text/plain in multipart params" + ); + } + // TODO: to support bput + // http://developer.qiniu.com/docs/v6/api/reference/up/bput.html + if (uploadConfig.saveType == "remote") { + up.setOption({ + url: qiniuUploadUrl + "chunk.php", + multipart: false, + chunk_size: chunk_size, + required_features: "chunks", + headers: { + Authorization: getUptoken(file), + }, + multipart_params: multipart_params_obj, + }); + } else { + up.setOption({ + url: qiniuUploadUrl + "/mkblk/" + blockSize, + multipart: false, + chunk_size: chunk_size, + required_features: "chunks", + headers: { + Authorization: + "UpToken " + getUptoken(file), + }, + multipart_params: multipart_params_obj, + }); + } + } + } else { + logger.debug( + "directUpload because uploader.runtime !== 'html5' || uploader.runtime !== 'flash' || !chunk_size" + ); + // direct upload if runtime is not html5 + directUpload(up, file, that.key_handler); + } + if (file.status != plupload.FAILED) { + file.status = plupload.UPLOADING; + up.trigger("UploadFile", file); + } else { + up.stop(); + } + }); + return false; + }); + + logger.debug("bind BeforeUpload event"); + + // bind 'UploadProgress' event + // calculate upload speed + uploader.bind("UploadProgress", function (up, file) { + logger.trace("UploadProgress event activated"); + speedCalInfo.currentTime = new Date().getTime(); + var timeUsed = + speedCalInfo.currentTime - speedCalInfo.startTime; // ms + var fileUploaded = file.loaded || 0; + if (speedCalInfo.isResumeUpload) { + fileUploaded = file.loaded - speedCalInfo.resumeFilesize; + } + file.speed = ((fileUploaded / timeUsed) * 1000).toFixed(0) || 0; // unit: byte/s + }); + + logger.debug("bind UploadProgress event"); + + // bind 'ChunkUploaded' event + // store the chunk upload info and set next chunk upload url + uploader.bind("ChunkUploaded", function (up, file, info) { + logger.debug("ChunkUploaded event activated"); + logger.debug("file: ", file); + logger.debug("info: ", info); + var res = that.parseJSON(info.response); + logger.debug("res: ", res); + // ctx should look like '[chunk01_ctx],[chunk02_ctx],[chunk03_ctx],...' + ctx = ctx ? ctx + "," + res.ctx : res.ctx; + var leftSize = info.total - info.offset; + var chunk_size = up.getOption && up.getOption("chunk_size"); + chunk_size = + chunk_size || (up.settings && up.settings.chunk_size); + if (leftSize < chunk_size) { + up.setOption({ + url: qiniuUploadUrl + "/mkblk/" + leftSize, + }); + if (uploadConfig.saveType == "remote") { + up.setOption({ + url: qiniuUploadUrl + "chunk.php", + }); + } + logger.debug( + "up.setOption url: ", + qiniuUploadUrl + "/mkblk/" + leftSize + ); + } + if (uploadConfig.saveType == "remote") { + up.setOption({ + headers: { + Authorization: getUptoken(file), + }, + }); + } else { + up.setOption({ + headers: { + Authorization: "UpToken " + getUptoken(file), + }, + }); + } + localStorage.setItem( + file.name, + that.stringifyJSON({ + ctx: ctx, + percent: file.percent, + total: info.total, + offset: info.offset, + time: new Date().getTime(), + }) + ); + }); + + logger.debug("bind ChunkUploaded event"); + + var retries = qiniuUploadUrls.length; + + // if error is unkown switch upload url and retry + var unknow_error_retry = function (file) { + if (retries-- > 0) { + setTimeout(function () { + that.resetUploadUrl(); + file.status = plupload.QUEUED; + uploader.stop(); + uploader.start(); + }, 0); + return true; + } else { + retries = qiniuUploadUrls.length; + return false; + } + }; + + // bind 'Error' event + // check the err.code and return the errTip + uploader.bind( + "Error", + (function (_Error_Handler) { + return function (up, err) { + logger.error("Error event activated"); + logger.error("err: ", err); + var errTip = ""; + var file = err.file; + if (file) { + switch (err.code) { + case plupload.FAILED: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.FILE_SIZE_ERROR: + var max_file_size = + up.getOption && + up.getOption("max_file_size"); + max_file_size = + max_file_size || + (up.settings && + up.settings.max_file_size); + errTip = + "文件过大,您当前用户组最多可上传" + + max_file_size + + "的文件"; + break; + case plupload.FILE_EXTENSION_ERROR: + errTip = "您当前的用户组不可上传此文件"; + break; + case plupload.HTTP_ERROR: + if (err.response === "") { + // Fix parseJSON error ,when http error is like net::ERR_ADDRESS_UNREACHABLE + errTip = + err.message || "未知网络错误。"; + if (!unknow_error_retry(file)) { + return; + } + break; + } + if (uploadConfig.saveType == "oss") { + var str = err.response; + try { + parser = new DOMParser(); + xmlDoc = parser.parseFromString( + str, + "text/xml" + ); + var errorText = xmlDoc.getElementsByTagName( + "Message" + )[0].innerHTML; + errTip = "上传失败"; + errTip = + errTip + + "(" + + err.status + + ":" + + errorText + + ")"; + } catch (e) { + errTip = err.message; + errorText = "Error"; + } + } + break; + case plupload.SECURITY_ERROR: + errTip = "安全配置错误。请联系网站管理员。"; + break; + case plupload.GENERIC_ERROR: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.IO_ERROR: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.INIT_ERROR: + errTip = "网站配置错误。请联系网站管理员。"; + uploader.destroy(); + break; + case 402: + errTip = "无法获取上传凭证"; + if (err.message) { + errTip = err.message; + } + break; + default: + errTip = err.message + err.details; + break; + } + if (_Error_Handler) { + _Error_Handler(up, err, errTip); + } + } + up.refresh(); // Reposition Flash/Silverlight + }; + })(_Error_Handler) + ); + + logger.debug("bind Error event"); + + // bind 'FileUploaded' event + // intercept the complete of upload + // - get downtoken from downtoken_url if bucket is private + // - invoke mkfile api to compose chunks if upload strategy is chunk upload + uploader.bind( + "FileUploaded", + (function (_FileUploaded_Handler) { + return function (up, file, info) { + logger.debug("FileUploaded event activated"); + logger.debug("file: ", file); + logger.debug("info: ", info); + if (uploadConfig.saveType == "s3") { + } + var last_step = function (up, file, info) { + if (op.downtoken_url) { + // if op.dowontoken_url is not empty + // need get downtoken before invoke the _FileUploaded_Handler + var ajax_downtoken = that.createAjax(); + ajax_downtoken.open( + "POST", + op.downtoken_url, + true + ); + ajax_downtoken.setRequestHeader( + "Content-type", + "application/x-www-form-urlencoded" + ); + ajax_downtoken.onreadystatechange = function () { + if (ajax_downtoken.readyState === 4) { + if ( + ajax_downtoken.status === 200 || + ajax_downtoken.status === 204 || + ajax_downtoken.status === 303 + ) { + var res_downtoken; + try { + res_downtoken = that.parseJSON( + ajax_downtoken.responseText + ); + } catch (e) { + throw "invalid json format"; + } + var info_extended = {}; + plupload.extend( + info_extended, + that.parseJSON(info), + res_downtoken + ); + if (_FileUploaded_Handler) { + _FileUploaded_Handler( + up, + file, + that.stringifyJSON( + info_extended + ) + ); + } + } else { + uploader.trigger("Error", { + status: ajax_downtoken.status, + response: + ajax_downtoken.responseText, + file: file, + code: plupload.HTTP_ERROR, + }); + } + } + }; + ajax_downtoken.send( + "key=" + + that.parseJSON(info).key + + "&domain=" + + op.domain + ); + } else if (_FileUploaded_Handler) { + _FileUploaded_Handler(up, file, info); + } + }; + if ( + uploadConfig.saveType == "oss" || + uploadConfig.saveType == "upyun" + ) { + ctx = 0; + } else { + var res = that.parseJSON(info.response); + ctx = ctx ? ctx : res.ctx; + } + // if ctx is not empty + // that means the upload strategy is chunk upload + // befroe the invoke the last_step + // we need request the mkfile to compose all uploaded chunks + // else + // invalke the last_step + logger.debug("ctx: ", ctx); + if (ctx) { + var key = ""; + logger.debug("save_key: ", op.save_key); + if (!op.save_key) { + key = getFileKey(up, file, that.key_handler); + key = key + ? "/key/" + that.URLSafeBase64Encode(key) + : ""; + } + + var fname = + "/fname/" + that.URLSafeBase64Encode(file.name); + if (uploadConfig.saveType == "remote") { + if (!op.save_key) { + key = getFileKey( + up, + file, + that.key_handler + ); + key = key + ? that.URLSafeBase64Encode(key) + : ""; + } + fname = + "" + that.URLSafeBase64Encode(file.name); + op.x_vars = { + path: file.path, + }; + } + logger.debug("op.x_vars: ", op.x_vars); + if (uploadConfig.saveType == "qiniu") { + op.x_vars = { + path: file.path, + }; + } + var x_vars = op.x_vars, + x_val = "", + x_vars_url = ""; + if ( + x_vars !== undefined && + typeof x_vars === "object" + ) { + for (var x_key in x_vars) { + if (x_vars.hasOwnProperty(x_key)) { + if ( + typeof x_vars[x_key] === "function" + ) { + x_val = that.URLSafeBase64Encode( + x_vars[x_key](up, file) + ); + } else if ( + typeof x_vars[x_key] !== "object" + ) { + x_val = that.URLSafeBase64Encode( + x_vars[x_key] + ); + } + x_vars_url += + "/x:" + x_key + "/" + x_val; + } + } + } + local_path = ""; + if ( + uploadConfig.saveType == "local" || + uploadConfig.saveType == "onedrive" + ) { + pathTmp = file.path; + if (file.path == "") { + pathTmp = "ROOTDIR"; + } + local_path = + "/path/" + + that.URLSafeBase64Encode(pathTmp); + } + if (uploadConfig.saveType == "remote") { + pathTmp = file.path; + local_path = that.URLSafeBase64Encode(pathTmp); + var url = + qiniuUploadUrl + + "mkfile.php?size=" + + file.size + + "&key=" + + key + + "&fname=" + + fname + + "&path=" + + local_path; + } else { + var url = + qiniuUploadUrl + + "/mkfile/" + + file.size + + key + + fname + + x_vars_url + + local_path; + } + var ie = that.detectIEVersion(); + var ajax; + if (ie && ie <= 9) { + ajax = new moxie.xhr.XMLHttpRequest(); + moxie.core.utils.Env.swf_url = op.flash_swf_url; + } else { + ajax = that.createAjax(); + } + ajax.open("POST", url, true); + ajax.setRequestHeader( + "Content-Type", + "text/plain;charset=UTF-8" + ); + if (uploadConfig.saveType == "remote") { + ajax.setRequestHeader( + "Authorization", + that.token + ); + } else { + ajax.setRequestHeader( + "Authorization", + "UpToken " + that.token + ); + } + var onreadystatechange = function () { + logger.debug( + "ajax.readyState: ", + ajax.readyState + ); + if (ajax.readyState === 4) { + localStorage.removeItem(file.name); + var info; + if (ajax.status === 200) { + info = ajax.responseText; + logger.debug( + "mkfile is success: ", + info + ); + last_step(up, file, info); + } else { + info = { + status: ajax.status, + response: ajax.responseText, + file: file, + code: -200, + responseHeaders: ajax.getAllResponseHeaders(), + }; + logger.debug("mkfile is error: ", info); + uploader.trigger("Error", info); + } + } + }; + if (ie && ie <= 9) { + ajax.bind( + "readystatechange", + onreadystatechange + ); + } else { + ajax.onreadystatechange = onreadystatechange; + } + ajax.send(ctx); + logger.debug("mkfile: ", url); + } else { + last_step(up, file, info.response); + } + }; + })(_FileUploaded_Handler) + ); + + logger.debug("bind FileUploaded event"); + + // init uploader + uploader.init(); + + logger.debug("invoke uploader.init()"); + + logger.debug("init uploader end"); + + return uploader; + }; + + /** + * get url by key + * @param {String} key of file + * @return {String} url of file + */ + this.getUrl = function (key) { + if (!key) { + return false; + } + key = encodeURI(key); + var domain = this.domain; + if (domain.slice(domain.length - 1) !== "/") { + domain = domain + "/"; + } + return domain + key; + }; + + /** + * invoke the imageView2 api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.imageView2 = function (op, key) { + if (!/^\d$/.test(op.mode)) { + return false; + } + + var mode = op.mode, + w = op.w || "", + h = op.h || "", + q = op.q || "", + format = op.format || ""; + + if (!w && !h) { + return false; + } + + var imageUrl = "imageView2/" + mode; + imageUrl += w ? "/w/" + w : ""; + imageUrl += h ? "/h/" + h : ""; + imageUrl += q ? "/q/" + q : ""; + imageUrl += format ? "/format/" + format : ""; + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the imageMogr2 api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.imageMogr2 = function (op, key) { + var auto_orient = op["auto-orient"] || "", + thumbnail = op.thumbnail || "", + strip = op.strip || "", + gravity = op.gravity || "", + crop = op.crop || "", + quality = op.quality || "", + rotate = op.rotate || "", + format = op.format || "", + blur = op.blur || ""; + //Todo check option + + var imageUrl = "imageMogr2"; + + imageUrl += auto_orient ? "/auto-orient" : ""; + imageUrl += thumbnail ? "/thumbnail/" + thumbnail : ""; + imageUrl += strip ? "/strip" : ""; + imageUrl += gravity ? "/gravity/" + gravity : ""; + imageUrl += quality ? "/quality/" + quality : ""; + imageUrl += crop ? "/crop/" + crop : ""; + imageUrl += rotate ? "/rotate/" + rotate : ""; + imageUrl += format ? "/format/" + format : ""; + imageUrl += blur ? "/blur/" + blur : ""; + + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the watermark api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.watermark = function (op, key) { + var mode = op.mode; + if (!mode) { + return false; + } + + var imageUrl = "watermark/" + mode; + + if (mode === 1) { + var image = op.image || ""; + if (!image) { + return false; + } + imageUrl += image + ? "/image/" + this.URLSafeBase64Encode(image) + : ""; + } else if (mode === 2) { + var text = op.text ? op.text : "", + font = op.font ? op.font : "", + fontsize = op.fontsize ? op.fontsize : "", + fill = op.fill ? op.fill : ""; + if (!text) { + return false; + } + imageUrl += text + ? "/text/" + this.URLSafeBase64Encode(text) + : ""; + imageUrl += font + ? "/font/" + this.URLSafeBase64Encode(font) + : ""; + imageUrl += fontsize ? "/fontsize/" + fontsize : ""; + imageUrl += fill + ? "/fill/" + this.URLSafeBase64Encode(fill) + : ""; + } else { + // Todo mode3 + return false; + } + + var dissolve = op.dissolve || "", + gravity = op.gravity || "", + dx = op.dx || "", + dy = op.dy || ""; + + imageUrl += dissolve ? "/dissolve/" + dissolve : ""; + imageUrl += gravity ? "/gravity/" + gravity : ""; + imageUrl += dx ? "/dx/" + dx : ""; + imageUrl += dy ? "/dy/" + dy : ""; + + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the imageInfo api of Qiniu + * @param {String} key of file + * @return {Object} image info + */ + this.imageInfo = function (key) { + if (!key) { + return false; + } + var url = this.getUrl(key) + "?imageInfo"; + var xhr = this.createAjax(); + var info; + var that = this; + xhr.open("GET", url, false); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + info = that.parseJSON(xhr.responseText); + } + }; + xhr.send(); + return info; + }; + + /** + * invoke the exif api of Qiniu + * @param {String} key of file + * @return {Object} image exif + */ + this.exif = function (key) { + if (!key) { + return false; + } + var url = this.getUrl(key) + "?exif"; + var xhr = this.createAjax(); + var info; + var that = this; + xhr.open("GET", url, false); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + info = that.parseJSON(xhr.responseText); + } + }; + xhr.send(); + return info; + }; + + /** + * invoke the exif or imageInfo api of Qiniu + * according with type param + * @param {String} ['exif'|'imageInfo']type of info + * @param {String} key of file + * @return {Object} image exif or info + */ + this.get = function (type, key) { + if (!key || !type) { + return false; + } + if (type === "exif") { + return this.exif(key); + } else if (type === "imageInfo") { + return this.imageInfo(key); + } + return false; + }; + + /** + * invoke api of Qiniu like a pipeline + * @param {Array of Object} params of a series api call + * each object in array is options of api which name is set as 'fop' property + * each api's output will be next api's input + * @param {String} key of file + * @return {String|Boolean} url of processed image + */ + this.pipeline = function (arr, key) { + var isArray = + Object.prototype.toString.call(arr) === "[object Array]"; + var option, + errOp, + imageUrl = ""; + if (isArray) { + for (var i = 0, len = arr.length; i < len; i++) { + option = arr[i]; + if (!option.fop) { + return false; + } + switch (option.fop) { + case "watermark": + imageUrl += this.watermark(option) + "|"; + break; + case "imageView2": + imageUrl += this.imageView2(option) + "|"; + break; + case "imageMogr2": + imageUrl += this.imageMogr2(option) + "|"; + break; + default: + errOp = true; + break; + } + if (errOp) { + return false; + } + } + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + var length = imageUrl.length; + if (imageUrl.slice(length - 1) === "|") { + imageUrl = imageUrl.slice(0, length - 1); + } + } + return imageUrl; + } + return false; + }; + } + + var Qiniu = new QiniuJsSDK(); + + global.Qiniu = Qiniu; + + global.QiniuJsSDK = QiniuJsSDK; +})(window); diff --git a/assets/public/static/js/uploader/uploader_qiniu.js b/assets/public/static/js/uploader/uploader_qiniu.js new file mode 100644 index 00000000..9ef0f5d1 --- /dev/null +++ b/assets/public/static/js/uploader/uploader_qiniu.js @@ -0,0 +1,2087 @@ +function getCookieByString(cookieName) { + var start = document.cookie.indexOf(cookieName + "="); + if (start == -1) return false; + start = start + cookieName.length + 1; + var end = document.cookie.indexOf(";", start); + if (end == -1) end = document.cookie.length; + return document.cookie.substring(start, end); +} +(function (global) { + /** + * Creates new cookie or removes cookie with negative expiration + * @param key The key or identifier for the store + * @param value Contents of the store + * @param exp Expiration - creation defaults to 30 days + */ + function createCookie(key, value, exp) { + var date = new Date(); + date.setTime(date.getTime() + exp * 24 * 60 * 60 * 1000); + var expires = "; expires=" + date.toGMTString(); + document.cookie = key + "=" + value + expires + "; path=/"; + } + + /** + * Returns contents of cookie + * @param key The key or identifier for the store + */ + function readCookie(key) { + var nameEQ = key + "="; + var ca = document.cookie.split(";"); + for (var i = 0, max = ca.length; i < max; i++) { + var c = ca[i]; + while (c.charAt(0) === " ") { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return c.substring(nameEQ.length, c.length); + } + } + return null; + } + + // if current browser is not support localStorage + // use cookie to make a polyfill + if (!window.localStorage) { + window.localStorage = { + setItem: function (key, value) { + createCookie(key, value, 30); + }, + getItem: function (key) { + return readCookie(key); + }, + removeItem: function (key) { + createCookie(key, "", -1); + }, + }; + } + + function QiniuJsSDK() { + var that = this; + + /** + * detect IE version + * if current browser is not IE + * it will return false + * else + * it will return version of current IE browser + * @return {Number|Boolean} IE version or false + */ + this.detectIEVersion = function () { + var v = 4, + div = document.createElement("div"), + all = div.getElementsByTagName("i"); + while ( + ((div.innerHTML = + ""), + all[0]) + ) { + v++; + } + return v > 4 ? v : false; + }; + + var logger = { + MUTE: 0, + FATA: 1, + ERROR: 2, + WARN: 3, + INFO: 4, + DEBUG: 5, + TRACE: 6, + level: 0, + }; + + function log(type, args) { + var header = "[Cloudreve-uploader][" + type + "]"; + var msg = header; + for (var i = 0; i < args.length; i++) { + if (typeof args[i] === "string") { + msg += " " + args[i]; + } else { + msg += " " + that.stringifyJSON(args[i]); + } + } + if (that.detectIEVersion()) { + // http://stackoverflow.com/questions/5538972/console-log-apply-not-working-in-ie9 + //var log = Function.prototype.bind.call(console.log, console); + //log.apply(console, args); + console.log(msg); + } else { + args.unshift(header); + console.log.apply(console, args); + } + if (document.getElementById("qiniu-js-sdk-log")) { + document.getElementById("qiniu-js-sdk-log").innerHTML += + "

" + msg + "

"; + } + } + + function makeLogFunc(code) { + var func = code.toLowerCase(); + logger[func] = function () { + // logger[func].history = logger[func].history || []; + // logger[func].history.push(arguments); + if ( + window.console && + window.console.log && + logger.level >= logger[code] + ) { + var args = Array.prototype.slice.call(arguments); + log(func, args); + } + }; + } + + for (var property in logger) { + if ( + logger.hasOwnProperty(property) && + typeof logger[property] === "number" && + !logger.hasOwnProperty(property.toLowerCase()) + ) { + makeLogFunc(property); + } + } + + var qiniuUploadUrl; + + /** + * qiniu upload urls + * 'qiniuUploadUrls' is used to change target when current url is not avaliable + * @type {Array} + */ + if (uploadConfig.saveType == "qiniu") { + if (window.location.protocol === "https:") { + qiniuUploadUrl = "https://up.qbox.me"; + } else { + qiniuUploadUrl = "http://upload.qiniu.com"; + } + var qiniuUploadUrls = [ + "http://upload.qiniu.com", + "http://up.qiniu.com", + ]; + + var qiniuUpHosts = { + http: ["http://upload.qiniu.com", "http://up.qiniu.com"], + https: ["https://up.qbox.me"], + }; + //TODO 优化写法 + } else if ( + uploadConfig.saveType == "local" || + uploadConfig.saveType == "oss" || + uploadConfig.saveType == "upyun" || + uploadConfig.saveType == "s3" || + uploadConfig.saveType == "remote" || + uploadConfig.saveType == "onedrive" + ) { + qiniuUploadUrl = uploadConfig.upUrl; + var qiniuUploadUrls = [uploadConfig.upUrl]; + var qiniuUpHosts = { + http: [uploadConfig.upUrl], + https: [uploadConfig.upUrl], + }; + } + + var changeUrlTimes = 0; + + /** + * reset upload url + * if current page protocal is https + * it will always return 'https://up.qbox.me' + * else + * it will set 'qiniuUploadUrl' value with 'qiniuUploadUrls' looply + */ + this.resetUploadUrl = function () { + var hosts = + window.location.protocol === "https:" + ? qiniuUpHosts.https + : qiniuUpHosts.http; + var i = changeUrlTimes % hosts.length; + qiniuUploadUrl = hosts[i]; + changeUrlTimes++; + logger.debug("resetUploadUrl: " + qiniuUploadUrl); + }; + + // this.resetUploadUrl(); + + /** + * is image + * @param {String} url of a file + * @return {Boolean} file is a image or not + */ + this.isImage = function (url) { + url = url.split(/[?#]/)[0]; + return /\.(png|jpg|jpeg|gif|bmp)$/i.test(url); + }; + + /** + * get file extension + * @param {String} filename + * @return {String} file extension + * @example + * input: test.txt + * output: txt + */ + this.getFileExtension = function (filename) { + var tempArr = filename.split("."); + var ext; + if ( + tempArr.length === 1 || + (tempArr[0] === "" && tempArr.length === 2) + ) { + ext = ""; + } else { + ext = tempArr.pop().toLowerCase(); //get the extension and make it lower-case + } + return ext; + }; + + /** + * encode string by utf8 + * @param {String} string to encode + * @return {String} encoded string + */ + this.utf8_encode = function (argString) { + // http://kevin.vanzonneveld.net + // + original by: Webtoolkit.info (http://www.webtoolkit.info/) + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + improved by: sowberry + // + tweaked by: Jack + // + bugfixed by: Onno Marsman + // + improved by: Yves Sucaet + // + bugfixed by: Onno Marsman + // + bugfixed by: Ulrich + // + bugfixed by: Rafal Kukawski + // + improved by: kirilloid + // + bugfixed by: kirilloid + // * example 1: this.utf8_encode('Kevin van Zonneveld'); + // * returns 1: 'Kevin van Zonneveld' + + if (argString === null || typeof argString === "undefined") { + return ""; + } + + var string = argString + ""; // .replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + var utftext = "", + start, + end, + stringl = 0; + + start = end = 0; + stringl = string.length; + for (var n = 0; n < stringl; n++) { + var c1 = string.charCodeAt(n); + var enc = null; + + if (c1 < 128) { + end++; + } else if (c1 > 127 && c1 < 2048) { + enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); + } else if ((c1 & 0xf800) ^ (0xd800 > 0)) { + enc = String.fromCharCode( + (c1 >> 12) | 224, + ((c1 >> 6) & 63) | 128, + (c1 & 63) | 128 + ); + } else { + // surrogate pairs + if ((c1 & 0xfc00) ^ (0xd800 > 0)) { + throw new RangeError( + "Unmatched trail surrogate at " + n + ); + } + var c2 = string.charCodeAt(++n); + if ((c2 & 0xfc00) ^ (0xdc00 > 0)) { + throw new RangeError( + "Unmatched lead surrogate at " + (n - 1) + ); + } + c1 = ((c1 & 0x3ff) << 10) + (c2 & 0x3ff) + 0x10000; + enc = String.fromCharCode( + (c1 >> 18) | 240, + ((c1 >> 12) & 63) | 128, + ((c1 >> 6) & 63) | 128, + (c1 & 63) | 128 + ); + } + if (enc !== null) { + if (end > start) { + utftext += string.slice(start, end); + } + utftext += enc; + start = end = n + 1; + } + } + + if (end > start) { + utftext += string.slice(start, stringl); + } + + return utftext; + }; + + this.base64_decode = function (data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Thunder.m + // + input by: Aman Gupta + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Onno Marsman + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: Brett Zamir (http://brett-zamir.me) + // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA=='); + // * returns 1: 'Kevin van Zonneveld' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + var b64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, + o2, + o3, + h1, + h2, + h3, + h4, + bits, + i = 0, + ac = 0, + dec = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data += ""; + + do { + // unpack four hexets into three octets using index points in b64 + h1 = b64.indexOf(data.charAt(i++)); + h2 = b64.indexOf(data.charAt(i++)); + h3 = b64.indexOf(data.charAt(i++)); + h4 = b64.indexOf(data.charAt(i++)); + + bits = (h1 << 18) | (h2 << 12) | (h3 << 6) | h4; + + o1 = (bits >> 16) & 0xff; + o2 = (bits >> 8) & 0xff; + o3 = bits & 0xff; + + if (h3 === 64) { + tmp_arr[ac++] = String.fromCharCode(o1); + } else if (h4 === 64) { + tmp_arr[ac++] = String.fromCharCode(o1, o2); + } else { + tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); + } + } while (i < data.length); + + dec = tmp_arr.join(""); + + return dec; + }; + + /** + * encode data by base64 + * @param {String} data to encode + * @return {String} encoded data + */ + this.base64_encode = function (data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Bayron Guevara + // + improved by: Thunder.m + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // - depends on: this.utf8_encode + // * example 1: this.base64_encode('Kevin van Zonneveld'); + // * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA==' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + var b64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, + o2, + o3, + h1, + h2, + h3, + h4, + bits, + i = 0, + ac = 0, + enc = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data = this.utf8_encode(data + ""); + + do { + // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = (o1 << 16) | (o2 << 8) | o3; + + h1 = (bits >> 18) & 0x3f; + h2 = (bits >> 12) & 0x3f; + h3 = (bits >> 6) & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = + b64.charAt(h1) + + b64.charAt(h2) + + b64.charAt(h3) + + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(""); + + switch (data.length % 3) { + case 1: + enc = enc.slice(0, -2) + "=="; + break; + case 2: + enc = enc.slice(0, -1) + "="; + break; + } + + return enc; + }; + + /** + * encode string in url by base64 + * @param {String} string in url + * @return {String} encoded string + */ + this.URLSafeBase64Encode = function (v) { + v = this.base64_encode(v); + return v.replace(/\//g, "_").replace(/\+/g, "-"); + }; + + this.URLSafeBase64Decode = function (v) { + v = v.replace(/_/g, "/").replace(/-/g, "+"); + return this.base64_decode(v); + }; + + // TODO: use mOxie + /** + * craete object used to AJAX + * @return {Object} + */ + this.createAjax = function (argument) { + var xmlhttp = {}; + if (window.XMLHttpRequest) { + xmlhttp = new XMLHttpRequest(); + } else { + xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + return xmlhttp; + }; + + // TODO: enhance IE compatibility + /** + * parse json string to javascript object + * @param {String} json string + * @return {Object} object + */ + this.parseJSON = function (data) { + // Attempt to parse using the native JSON parser first + if (window.JSON && window.JSON.parse) { + return window.JSON.parse(data); + } + + //var rx_one = /^[\],:{}\s]*$/, + // rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + // rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + // rx_four = /(?:^|:|,)(?:\s*\[)+/g, + var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + + //var json; + + var text = String(data); + rx_dangerous.lastIndex = 0; + if (rx_dangerous.test(text)) { + text = text.replace(rx_dangerous, function (a) { + return ( + "\\u" + + ("0000" + a.charCodeAt(0).toString(16)).slice(-4) + ); + }); + } + + // todo 使用一下判断,增加安全性 + //if ( + // rx_one.test( + // text + // .replace(rx_two, '@') + // .replace(rx_three, ']') + // .replace(rx_four, '') + // ) + //) { + // return eval('(' + text + ')'); + //} + + return eval("(" + text + ")"); + }; + + /** + * parse javascript object to json string + * @param {Object} object + * @return {String} json string + */ + this.stringifyJSON = function (obj) { + // Attempt to parse using the native JSON parser first + if (window.JSON && window.JSON.stringify) { + return window.JSON.stringify(obj); + } + switch (typeof obj) { + case "string": + return '"' + obj.replace(/(["\\])/g, "\\$1") + '"'; + case "array": + return "[" + obj.map(that.stringifyJSON).join(",") + "]"; + case "object": + if (obj instanceof Array) { + var strArr = []; + var len = obj.length; + for (var i = 0; i < len; i++) { + strArr.push(that.stringifyJSON(obj[i])); + } + return "[" + strArr.join(",") + "]"; + } else if (obj === null) { + return "null"; + } else { + var string = []; + for (var property in obj) { + if (obj.hasOwnProperty(property)) { + string.push( + that.stringifyJSON(property) + + ":" + + that.stringifyJSON(obj[property]) + ); + } + } + return "{" + string.join(",") + "}"; + } + break; + case "number": + return obj; + case false: + return obj; + case "boolean": + return obj; + } + }; + + /** + * trim space beside text + * @param {String} untrimed string + * @return {String} trimed string + */ + this.trim = function (text) { + return text === null ? "" : text.replace(/^\s+|\s+$/g, ""); + }; + + /** + * create a uploader by QiniuJsSDK + * @param {object} options to create a new uploader + * @return {object} uploader + */ + this.uploader = function (op) { + /********** inner function define start **********/ + + // according the different condition to reset chunk size + // and the upload strategy according with the chunk size + // when chunk size is zero will cause to direct upload + // see the statement binded on 'BeforeUpload' event + var reset_chunk_size = function () { + var ie = that.detectIEVersion(); + var BLOCK_BITS, MAX_CHUNK_SIZE, chunk_size; + // case Safari 5、Windows 7、iOS 7 set isSpecialSafari to true + var isSpecialSafari = + (moxie.core.utils.Env.browser === "Safari" && + moxie.core.utils.Env.version <= 5 && + moxie.core.utils.Env.os === "Windows" && + moxie.core.utils.Env.osVersion === "7") || + (moxie.core.utils.Env.browser === "Safari" && + moxie.core.utils.Env.os === "iOS" && + moxie.core.utils.Env.osVersion === "7"); + // case IE 9-,chunk_size is not empty and flash is included in runtimes + // set op.chunk_size to zero + //if (ie && ie < 9 && op.chunk_size && op.runtimes.indexOf('flash') >= 0) { + if ( + ie && + ie < 9 && + op.chunk_size && + op.runtimes.indexOf("flash") >= 0 + ) { + // link: http://www.plupload.com/docs/Frequently-Asked-Questions#when-to-use-chunking-and-when-not + // when plupload chunk_size setting is't null ,it cause bug in ie8/9 which runs flash runtimes (not support html5) . + op.chunk_size = 0; + } else if (isSpecialSafari) { + // win7 safari / iOS7 safari have bug when in chunk upload mode + // reset chunk_size to 0 + // disable chunk in special version safari + op.chunk_size = 0; + } else { + BLOCK_BITS = 20; + MAX_CHUNK_SIZE = 4 << BLOCK_BITS; //4M + + chunk_size = plupload.parseSize(op.chunk_size); + + // qiniu service max_chunk_size is 4m + // reset chunk_size to max_chunk_size(4m) when chunk_size > 4m + } + // if op.chunk_size set 0 will be cause to direct upload + }; + + var getHosts = function (hosts) { + var result = []; + for (var i = 0; i < hosts.length; i++) { + var host = hosts[i]; + if (host.indexOf("-H") === 0) { + result.push(host.split(" ")[2]); + } else { + result.push(host); + } + } + return result; + }; + + var getPutPolicy = function (uptoken) { + if ( + uploadConfig.saveType == "local" || + uploadConfig.saveType == "oss" || + uploadConfig.saveType == "upyun" || + uploadConfig.saveType == "s3" || + uploadConfig.saveType == "remote" + ) { + return "oss"; + } else { + var segments = uptoken.split(":"); + var ak = segments[0]; + var putPolicy = that.parseJSON( + that.URLSafeBase64Decode(segments[2]) + ); + putPolicy.ak = ak; + if (putPolicy.scope.indexOf(":") >= 0) { + putPolicy.bucket = putPolicy.scope.split(":")[0]; + putPolicy.key = putPolicy.scope.split(":")[1]; + } else { + putPolicy.bucket = putPolicy.scope; + } + return putPolicy; + } + }; + + var getUpHosts = function (uptoken) { + var putPolicy = getPutPolicy(uptoken); + // var uphosts_url = "//uc.qbox.me/v1/query?ak="+ak+"&bucket="+putPolicy.scope; + // IE 9- is not support protocal relative url + var uphosts_url = + window.location.protocol + + "//uc.qbox.me/v1/query?ak=" + + putPolicy.ak + + "&bucket=" + + putPolicy.bucket; + logger.debug("putPolicy: ", putPolicy); + logger.debug("get uphosts from: ", uphosts_url); + var ie = that.detectIEVersion(); + var ajax; + if (ie && ie <= 9) { + ajax = new moxie.XMLHttpRequest(); + moxie.core.utils.Env.swf_url = op.flash_swf_url; + } else { + ajax = that.createAjax(); + } + if (uploadConfig.saveType != "qiniu") { + qiniuUpHosts.http = [uploadConfig.upUrl]; + qiniuUpHosts.http = [uploadConfig.upUrl]; + that.resetUploadUrl(); + } else { + ajax.open("GET", uphosts_url, false); + var onreadystatechange = function () { + logger.debug("ajax.readyState: ", ajax.readyState); + if (ajax.readyState === 4) { + logger.debug("ajax.status: ", ajax.status); + if (ajax.status < 400) { + var res = that.parseJSON(ajax.responseText); + qiniuUpHosts.http = getHosts(res.http.up); + qiniuUpHosts.https = getHosts(res.https.up); + logger.debug("get new uphosts: ", qiniuUpHosts); + that.resetUploadUrl(); + } else { + logger.error( + "get uphosts error: ", + ajax.responseText + ); + } + } + }; + if (ie && ie <= 9) { + ajax.bind("readystatechange", onreadystatechange); + } else { + ajax.onreadystatechange = onreadystatechange; + } + ajax.send(); + // ajax.send(); + // if (ajax.status < 400) { + // var res = that.parseJSON(ajax.responseText); + // qiniuUpHosts.http = getHosts(res.http.up); + // qiniuUpHosts.https = getHosts(res.https.up); + // logger.debug("get new uphosts: ", qiniuUpHosts); + // that.resetUploadUrl(); + // } else { + // logger.error("get uphosts error: ", ajax.responseText); + // } + } + return; + }; + + var getUptoken = function (file) { + if (uploadConfig.saveType == "remote") { + return that.token; + } + if ( + !that.token || + (op.uptoken_url && that.tokenInfo.isExpired()) + ) { + return getNewUpToken(file); + } else { + return that.token; + } + }; + + // getNewUptoken maybe called at Init Event or BeforeUpload Event + // case Init Event, the file param of getUptken will be set a null value + // if op.uptoken has value, set uptoken with op.uptoken + // else if op.uptoken_url has value, set uptoken from op.uptoken_url + // else if op.uptoken_func has value, set uptoken by result of op.uptoken_func + var getNewUpToken = function (file, callback) { + if (op.uptoken) { + that.token = op.uptoken; + callback(); + } else if (op.uptoken_url) { + logger.debug("get uptoken from: ", that.uptoken_url); + // TODO: use mOxie + var ajax = that.createAjax(); + if (file.size === undefined) { + file.size = 0; + } + ajax.open( + "GET", + that.uptoken_url + + "?path=" + + encodeURIComponent(window.pathCache[file.id]) + + "&size=" + + file.size + + "&name=" + + encodeURIComponent(file.name) + + "&type=qiniu", + true + ); + ajax.setRequestHeader("If-Modified-Since", "0"); + ajax.send(); + ajax.onload = function (e) { + if (ajax.status === 200) { + var res = that.parseJSON(ajax.responseText); + if (res.code != 0) { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + message: res.msg, + code: 402, + }); + callback(); + return; + } + that.token = res.data.token; + + var segments = that.token.split(":"); + var putPolicy = that.parseJSON( + that.URLSafeBase64Decode(segments[2]) + ); + if (!that.tokenMap) { + that.tokenMap = {}; + } + var getTimestamp = function (time) { + return Math.ceil(time.getTime() / 1000); + }; + var serverTime = getTimestamp( + new Date(ajax.getResponseHeader("date")) + ); + var clientTime = getTimestamp(new Date()); + that.tokenInfo = { + serverDelay: clientTime - serverTime, + deadline: putPolicy.deadline, + isExpired: function () { + var leftTime = + this.deadline - + getTimestamp(new Date()) + + this.serverDelay; + return leftTime < 600; + }, + }; + logger.debug("get token info: ", that.tokenInfo); + } else { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + code: 402, + }); + } + callback(); + }; + + ajax.onerror = function (e) { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + code: 402, + }); + callback(); + logger.error("get uptoken error: ", ajax.responseText); + }; + } else if (op.uptoken_func) { + logger.debug("get uptoken from uptoken_func"); + that.token = op.uptoken_func(file); + logger.debug("get new uptoken: ", that.token); + callback(); + } else { + logger.error( + "one of [uptoken, uptoken_url, uptoken_func] settings in options is required!" + ); + callback(); + } + return that.token; + }; + + // get file key according with the user passed options + var getFileKey = function (up, file, func) { + // WARNING + // When you set the key in putPolicy by "scope": "bucket:key" + // You should understand the risk of override a file in the bucket + // So the code below that automatically get key from uptoken has been commented + // var putPolicy = getPutPolicy(that.token) + // if (putPolicy.key) { + // logger.debug("key is defined in putPolicy.scope: ", putPolicy.key) + // return putPolicy.key + // } + var key = "", + unique_names = false; + if (!op.save_key) { + unique_names = up.getOption && up.getOption("unique_names"); + unique_names = + unique_names || + (up.settings && up.settings.unique_names); + if (unique_names) { + var ext = that.getFileExtension(file.name); + key = ext ? file.id + "." + ext : file.id; + } else if (typeof func === "function") { + key = func(up, file); + } else { + key = file.name; + } + } + if (uploadConfig.saveType == "qiniu") { + return ""; + } + return key; + }; + + /********** inner function define end **********/ + + if (op.log_level) { + logger.level = op.log_level; + } + + if (!op.domain) { + throw "domain setting in options is required!"; + } + + if (!op.browse_button) { + throw "browse_button setting in options is required!"; + } + + if (!op.uptoken && !op.uptoken_url && !op.uptoken_func) { + throw "one of [uptoken, uptoken_url, uptoken_func] settings in options is required!"; + } + + logger.debug("init uploader start"); + + logger.debug("environment: ", moxie.core.utils.Env); + + logger.debug("userAgent: ", navigator.userAgent); + + var option = {}; + + // hold the handler from user passed options + var _Error_Handler = op.init && op.init.Error; + var _FileUploaded_Handler = op.init && op.init.FileUploaded; + + // replace the handler for intercept + op.init.Error = function () {}; + op.init.FileUploaded = function () {}; + + that.uptoken_url = op.uptoken_url; + that.token = ""; + that.key_handler = + typeof op.init.Key === "function" ? op.init.Key : ""; + this.domain = op.domain; + // TODO: ctx is global in scope of a uploader instance + // this maybe cause error + var ctx = ""; + var speedCalInfo = { + isResumeUpload: false, + resumeFilesize: 0, + startTime: "", + currentTime: "", + }; + + reset_chunk_size(); + logger.debug("invoke reset_chunk_size()"); + logger.debug("op.chunk_size: ", op.chunk_size); + + var defaultSetting = { + url: qiniuUploadUrl, + multipart_params: {}, + }; + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + defaultSetting.multipart_params.accept = + "text/plain; charset=utf-8"; + logger.debug("add accept text/plain in multipart params"); + } + + // compose options with user passed options and default setting + plupload.extend(option, op, defaultSetting); + + logger.debug("option: ", option); + + // create a new uploader with composed options + var uploader = new plupload.Uploader(option); + + logger.debug("new plupload.Uploader(option)"); + + // bind getNewUpToken to 'Init' event + uploader.bind("Init", function (up, params) { + logger.debug("Init event activated"); + // if op.get_new_uptoken is not true + // invoke getNewUptoken when uploader init + // else + // getNewUptoken everytime before a new file upload + if (!op.get_new_uptoken) { + getNewUpToken(null); + } + //getNewUpToken(null); + }); + + logger.debug("bind Init event"); + + // bind 'FilesAdded' event + // when file be added and auto_start has set value + // uploader will auto start upload the file + uploader.bind("FilesAdded", function (up, files) { + logger.debug("FilesAdded event activated"); + var auto_start = up.getOption && up.getOption("auto_start"); + auto_start = + auto_start || (up.settings && up.settings.auto_start); + logger.debug("auto_start: ", auto_start); + logger.debug("files: ", files); + + // detect is iOS + var is_ios = function () { + if (moxie.core.utils.Env.OS.toLowerCase() === "ios") { + return true; + } else { + return false; + } + }; + + // if current env os is iOS change file name to [time].[ext] + if (is_ios()) { + for (var i = 0; i < files.length; i++) { + var file = files[i]; + var ext = that.getFileExtension(file.name); + file.name = file.id + "." + ext; + } + } + + if (auto_start) { + setTimeout(function () { + up.start(); + logger.debug("invoke up.start()"); + }, 0); + // up.start(); + // plupload.each(files, function(i, file) { + // up.start(); + // logger.debug("invoke up.start()") + // logger.debug("file: ", file); + // }); + } + up.refresh(); // Reposition Flash/Silverlight + }); + + logger.debug("bind FilesAdded event"); + + // bind 'BeforeUpload' event + // intercept the process of upload + // - prepare uptoken + // - according the chunk size to make differnt upload strategy + // - resume upload with the last breakpoint of file + uploader.bind("BeforeUpload", function (up, file) { + logger.debug("BeforeUpload event activated"); + // add a key named speed for file object + file.speed = file.speed || 0; + ctx = ""; + + var directUpload = function (up, file, func) { + speedCalInfo.startTime = new Date().getTime(); + var multipart_params_obj; + if (op.save_key) { + multipart_params_obj = { + token: that.token, + }; + } else { + multipart_params_obj = { + key: getFileKey(up, file, func), + token: that.token, + }; + if (uploadConfig.saveType == "qiniu") { + multipart_params_obj = { + token: that.token, + "x:path": file.path, + }; + } else if (uploadConfig.saveType == "remote") { + multipart_params_obj = { + path: file.path, + token: that.policy, + MAX_FILE_SIZE: 4194304, + }; + } else if (uploadConfig.saveType == "oss") { + multipart_params_obj = { + policy: that.token, + "x:path": file.path, + signature: that.sign, + OSSAccessKeyId: that.access, + "x:fname": file.name, + key: that.file_name.replace( + "${filename}", + file.name + ), + callback: that.callback, + }; + } else if (uploadConfig.saveType == "s3") { + multipart_params_obj = { + policy: that.policy, + key: + that.file_name + + "/" + + (file.path.replace(",", "/") == "" + ? "" + : file.path.replace(",", "/") + "/") + + file.name, + success_action_redirect: + that.surl + + "Callback/S3/key/" + + that.callbackKey, + "x-amz-algorithm": "AWS4-HMAC-SHA256", + "x-amz-credential": that.credential, + "x-amz-date": that.x_amz_date, + "x-amz-signature": that.sign, + "Content-Type": file.type, + }; + } else if (uploadConfig.saveType == "upyun") { + multipart_params_obj = { + authorization: that.token, + policy: that.policy, + }; + } + } + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + multipart_params_obj.accept = + "text/plain; charset=utf-8"; + logger.debug( + "add accept text/plain in multipart params" + ); + } + + logger.debug( + "directUpload multipart_params_obj: ", + multipart_params_obj + ); + + var x_vars = op.x_vars; + if (x_vars !== undefined && typeof x_vars === "object") { + for (var x_key in x_vars) { + if (x_vars.hasOwnProperty(x_key)) { + if (typeof x_vars[x_key] === "function") { + multipart_params_obj["x:" + x_key] = x_vars[ + x_key + ](up, file); + } else if (typeof x_vars[x_key] !== "object") { + multipart_params_obj["x:" + x_key] = + x_vars[x_key]; + } + } + } + } + + // 本地策略时不用multipart + if (uploadConfig.saveType == "local") { + up.setOption({ + url: qiniuUploadUrl, + multipart: false, + send_file_name: false, + headers: { + "X-Path": encodeURIComponent(file.path), + "X-FileName": encodeURIComponent( + getFileKey(up, file, func) + ), + }, + }); + } else { + up.setOption({ + url: qiniuUploadUrl, + multipart: true, + chunk_size: is_android_weixin_or_qq() + ? op.max_file_size + : undefined, + multipart_params: multipart_params_obj, + }); + } + }; + + // detect is weixin or qq inner browser + var is_android_weixin_or_qq = function () { + var ua = navigator.userAgent.toLowerCase(); + if ( + (ua.match(/MicroMessenger/i) || + moxie.core.utils.Env.browser === "QQBrowser" || + ua.match(/V1_AND_SQ/i)) && + moxie.core.utils.Env.OS.toLowerCase() === "android" + ) { + return true; + } else { + return false; + } + }; + + var chunk_size = up.getOption && up.getOption("chunk_size"); + chunk_size = + chunk_size || (up.settings && up.settings.chunk_size); + + logger.debug("uploader.runtime: ", uploader.runtime); + logger.debug("chunk_size: ", chunk_size); + + getNewUpToken(file, () => { + if (that.token) { + getUpHosts(that.token); + } + if ( + (uploader.runtime === "html5" || + uploader.runtime === "flash") && + chunk_size + ) { + if ( + file.size < chunk_size || + is_android_weixin_or_qq() + ) { + logger.debug( + "directUpload because file.size < chunk_size || is_android_weixin_or_qq()" + ); + // direct upload if file size is less then the chunk size + directUpload(up, file, that.key_handler); + } else { + // TODO: need a polifill to make it work in IE 9- + // ISSUE: if file.name is existed in localStorage + // but not the same file maybe cause error + var localFileInfo = localStorage.getItem(file.name); + var blockSize = chunk_size; + if (localFileInfo) { + // TODO: although only the html5 runtime will enter this statement + // but need uniform way to make convertion between string and json + localFileInfo = that.parseJSON(localFileInfo); + var now = new Date().getTime(); + var before = localFileInfo.time || 0; + var aDay = 24 * 60 * 60 * 1000; // milliseconds of one day + // if the last upload time is within one day + // will upload continuously follow the last breakpoint + // else + // will reupload entire file + if (now - before < aDay) { + if (localFileInfo.percent !== 100) { + if (file.size === localFileInfo.total) { + // TODO: if file.name and file.size is the same + // but not the same file will cause error + file.percent = + localFileInfo.percent; + file.loaded = localFileInfo.offset; + ctx = localFileInfo.ctx; + + // set speed info + speedCalInfo.isResumeUpload = true; + speedCalInfo.resumeFilesize = + localFileInfo.offset; + + // set block size + if ( + localFileInfo.offset + + blockSize > + file.size + ) { + blockSize = + file.size - + localFileInfo.offset; + } + } else { + // remove file info when file.size is conflict with file info + localStorage.removeItem(file.name); + } + } else { + // remove file info when upload percent is 100% + // avoid 499 bug + localStorage.removeItem(file.name); + } + } else { + // remove file info when last upload time is over one day + localStorage.removeItem(file.name); + } + } + speedCalInfo.startTime = new Date().getTime(); + var multipart_params_obj = {}; + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + multipart_params_obj.accept = + "text/plain; charset=utf-8"; + logger.debug( + "add accept text/plain in multipart params" + ); + } + // TODO: to support bput + // http://developer.qiniu.com/docs/v6/api/reference/up/bput.html + + up.setOption({ + url: qiniuUploadUrl + "/mkblk/" + blockSize, + multipart: false, + chunk_size: chunk_size, + required_features: "chunks", + headers: { + Authorization: + "UpToken " + getUptoken(file), + }, + multipart_params: multipart_params_obj, + }); + } + } else { + logger.debug( + "directUpload because uploader.runtime !== 'html5' || uploader.runtime !== 'flash' || !chunk_size" + ); + // direct upload if runtime is not html5 + directUpload(up, file, that.key_handler); + } + if (file.status != plupload.FAILED) { + file.status = plupload.UPLOADING; + up.trigger("UploadFile", file); + } else { + up.stop(); + } + }); + return false; + }); + + logger.debug("bind BeforeUpload event"); + + // bind 'UploadProgress' event + // calculate upload speed + uploader.bind("UploadProgress", function (up, file) { + logger.trace("UploadProgress event activated"); + speedCalInfo.currentTime = new Date().getTime(); + var timeUsed = + speedCalInfo.currentTime - speedCalInfo.startTime; // ms + var fileUploaded = file.loaded || 0; + if (speedCalInfo.isResumeUpload) { + fileUploaded = file.loaded - speedCalInfo.resumeFilesize; + } + file.speed = ((fileUploaded / timeUsed) * 1000).toFixed(0) || 0; // unit: byte/s + }); + + logger.debug("bind UploadProgress event"); + + // bind 'ChunkUploaded' event + // store the chunk upload info and set next chunk upload url + uploader.bind("ChunkUploaded", function (up, file, info) { + logger.debug("ChunkUploaded event activated"); + logger.debug("file: ", file); + logger.debug("info: ", info); + var res = that.parseJSON(info.response); + logger.debug("res: ", res); + // ctx should look like '[chunk01_ctx],[chunk02_ctx],[chunk03_ctx],...' + ctx = ctx ? ctx + "," + res.ctx : res.ctx; + var leftSize = info.total - info.offset; + var chunk_size = up.getOption && up.getOption("chunk_size"); + chunk_size = + chunk_size || (up.settings && up.settings.chunk_size); + if (leftSize < chunk_size) { + up.setOption({ + url: qiniuUploadUrl + "/mkblk/" + leftSize, + }); + if (uploadConfig.saveType == "remote") { + up.setOption({ + url: qiniuUploadUrl + "chunk.php", + }); + } + logger.debug( + "up.setOption url: ", + qiniuUploadUrl + "/mkblk/" + leftSize + ); + } + if (uploadConfig.saveType == "remote") { + up.setOption({ + headers: { + Authorization: getUptoken(file), + }, + }); + } else { + up.setOption({ + headers: { + Authorization: "UpToken " + getUptoken(file), + }, + }); + } + localStorage.setItem( + file.name, + that.stringifyJSON({ + ctx: ctx, + percent: file.percent, + total: info.total, + offset: info.offset, + time: new Date().getTime(), + }) + ); + }); + + logger.debug("bind ChunkUploaded event"); + + var retries = qiniuUploadUrls.length; + + // if error is unkown switch upload url and retry + var unknow_error_retry = function (file) { + if (retries-- > 0) { + setTimeout(function () { + that.resetUploadUrl(); + file.status = plupload.QUEUED; + uploader.stop(); + uploader.start(); + }, 0); + return true; + } else { + retries = qiniuUploadUrls.length; + return false; + } + }; + + // bind 'Error' event + // check the err.code and return the errTip + uploader.bind( + "Error", + (function (_Error_Handler) { + return function (up, err) { + logger.error("Error event activated"); + logger.error("err: ", err); + var errTip = ""; + var file = err.file; + if (file) { + switch (err.code) { + case plupload.FAILED: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.FILE_SIZE_ERROR: + var max_file_size = + up.getOption && + up.getOption("max_file_size"); + max_file_size = + max_file_size || + (up.settings && + up.settings.max_file_size); + errTip = + "文件过大,您当前用户组最多可上传" + + max_file_size + + "的文件"; + break; + case plupload.FILE_EXTENSION_ERROR: + errTip = "您当前的用户组不可上传此文件"; + break; + case plupload.HTTP_ERROR: + if (err.response === "") { + // Fix parseJSON error ,when http error is like net::ERR_ADDRESS_UNREACHABLE + errTip = + err.message || "未知网络错误。"; + if (!unknow_error_retry(file)) { + return; + } + break; + } + var errorObj = that.parseJSON(err.response); + if (errorObj.msg) { + var errorText = errorObj.msg; + } else { + var errorText = errorObj.error; + } + if (err.status == 579) { + var errorObj2 = that.parseJSON( + errorText + ); + errorText = errorObj2.error; + } + switch (err.status) { + case 400: + errTip = "请求报文格式错误。"; + break; + case 402: + errTip = ""; + break; + case 401: + errTip = + "客户端认证授权失败。请重试或提交反馈。"; + break; + case 405: + errTip = + "客户端请求错误。请重试或提交反馈。"; + break; + case 579: + errTip = + "资源上传成功,但回调失败。"; + break; + case 599: + errTip = + "网络连接异常。请重试或提交反馈。"; + if (!unknow_error_retry(file)) { + return; + } + break; + case 614: + errTip = "文件已存在。"; + try { + errorObj = that.parseJSON( + errorObj.error + ); + errorText = + errorObj.error || + "file exists"; + } catch (e) { + errorText = + errorObj.error || + "file exists"; + } + break; + case 631: + errTip = "指定空间不存在。"; + break; + case 701: + errTip = + "上传数据块校验出错。请重试或提交反馈。"; + break; + default: + if (err.message) { + errTip = err.message; + } else { + errTip = "未知错误"; + } + break; + } + errTip = + errTip + + "(" + + err.status + + ":" + + errorText + + ")"; + + break; + case plupload.SECURITY_ERROR: + errTip = "安全配置错误。请联系网站管理员。"; + break; + case plupload.GENERIC_ERROR: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.IO_ERROR: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.INIT_ERROR: + errTip = "网站配置错误。请联系网站管理员。"; + uploader.destroy(); + break; + case 402: + errTip = "无法获取上传凭证"; + if (err.message) { + errTip = err.message; + } + break; + default: + errTip = err.message + err.details; + break; + } + if (_Error_Handler) { + _Error_Handler(up, err, errTip); + } + } + up.refresh(); // Reposition Flash/Silverlight + }; + })(_Error_Handler) + ); + + logger.debug("bind Error event"); + + // bind 'FileUploaded' event + // intercept the complete of upload + // - get downtoken from downtoken_url if bucket is private + // - invoke mkfile api to compose chunks if upload strategy is chunk upload + uploader.bind( + "FileUploaded", + (function (_FileUploaded_Handler) { + return function (up, file, info) { + logger.debug("FileUploaded event activated"); + logger.debug("file: ", file); + logger.debug("info: ", info); + if (uploadConfig.saveType == "s3") { + } + var last_step = function (up, file, info) { + if (op.downtoken_url) { + // if op.dowontoken_url is not empty + // need get downtoken before invoke the _FileUploaded_Handler + var ajax_downtoken = that.createAjax(); + ajax_downtoken.open( + "POST", + op.downtoken_url, + true + ); + ajax_downtoken.setRequestHeader( + "Content-type", + "application/x-www-form-urlencoded" + ); + ajax_downtoken.onreadystatechange = function () { + if (ajax_downtoken.readyState === 4) { + if ( + ajax_downtoken.status === 200 || + ajax_downtoken.status === 204 || + ajax_downtoken.status === 303 + ) { + var res_downtoken; + try { + res_downtoken = that.parseJSON( + ajax_downtoken.responseText + ); + } catch (e) { + throw "invalid json format"; + } + var info_extended = {}; + plupload.extend( + info_extended, + that.parseJSON(info), + res_downtoken + ); + if (_FileUploaded_Handler) { + _FileUploaded_Handler( + up, + file, + that.stringifyJSON( + info_extended + ) + ); + } + } else { + uploader.trigger("Error", { + status: ajax_downtoken.status, + response: + ajax_downtoken.responseText, + file: file, + code: plupload.HTTP_ERROR, + }); + } + } + }; + ajax_downtoken.send( + "key=" + + that.parseJSON(info).key + + "&domain=" + + op.domain + ); + } else if (_FileUploaded_Handler) { + _FileUploaded_Handler(up, file, info); + } + }; + if ( + uploadConfig.saveType == "oss" || + uploadConfig.saveType == "upyun" + ) { + ctx = 0; + } else { + var res = that.parseJSON(info.response); + ctx = ctx ? ctx : res.ctx; + } + // if ctx is not empty + // that means the upload strategy is chunk upload + // befroe the invoke the last_step + // we need request the mkfile to compose all uploaded chunks + // else + // invalke the last_step + logger.debug("ctx: ", ctx); + if (ctx) { + var key = ""; + logger.debug("save_key: ", op.save_key); + if (!op.save_key) { + key = getFileKey(up, file, that.key_handler); + key = key + ? "/key/" + that.URLSafeBase64Encode(key) + : ""; + } + + var fname = + "/fname/" + that.URLSafeBase64Encode(file.name); + if (uploadConfig.saveType == "remote") { + if (!op.save_key) { + key = getFileKey( + up, + file, + that.key_handler + ); + key = key + ? that.URLSafeBase64Encode(key) + : ""; + } + fname = + "" + that.URLSafeBase64Encode(file.name); + op.x_vars = { + path: file.path, + }; + } + logger.debug("op.x_vars: ", op.x_vars); + if (uploadConfig.saveType == "qiniu") { + op.x_vars = { + path: file.path, + }; + } + var x_vars = op.x_vars, + x_val = "", + x_vars_url = ""; + if ( + x_vars !== undefined && + typeof x_vars === "object" + ) { + for (var x_key in x_vars) { + if (x_vars.hasOwnProperty(x_key)) { + if ( + typeof x_vars[x_key] === "function" + ) { + x_val = that.URLSafeBase64Encode( + x_vars[x_key](up, file) + ); + } else if ( + typeof x_vars[x_key] !== "object" + ) { + x_val = that.URLSafeBase64Encode( + x_vars[x_key] + ); + } + x_vars_url += + "/x:" + x_key + "/" + x_val; + } + } + } + local_path = ""; + if ( + uploadConfig.saveType == "local" || + uploadConfig.saveType == "onedrive" + ) { + pathTmp = file.path; + if (file.path == "") { + pathTmp = "ROOTDIR"; + } + local_path = + "/path/" + + that.URLSafeBase64Encode(pathTmp); + } + + var url = + qiniuUploadUrl + + "/mkfile/" + + file.size + + key + + fname + + x_vars_url + + local_path; + + var ie = that.detectIEVersion(); + var ajax; + if (ie && ie <= 9) { + ajax = new moxie.xhr.XMLHttpRequest(); + moxie.core.utils.Env.swf_url = op.flash_swf_url; + } else { + ajax = that.createAjax(); + } + ajax.open("POST", url, true); + ajax.setRequestHeader( + "Content-Type", + "text/plain;charset=UTF-8" + ); + if (uploadConfig.saveType == "remote") { + ajax.setRequestHeader( + "Authorization", + that.token + ); + } else { + ajax.setRequestHeader( + "Authorization", + "UpToken " + that.token + ); + } + var onreadystatechange = function () { + logger.debug( + "ajax.readyState: ", + ajax.readyState + ); + if (ajax.readyState === 4) { + localStorage.removeItem(file.name); + var info; + if (ajax.status === 200) { + info = ajax.responseText; + logger.debug( + "mkfile is success: ", + info + ); + uploader.trigger("Fresh"); + last_step(up, file, info); + } else { + info = { + status: ajax.status, + response: ajax.responseText, + file: file, + code: -200, + responseHeaders: ajax.getAllResponseHeaders(), + }; + logger.debug("mkfile is error: ", info); + uploader.trigger("Error", info); + } + } + }; + if (ie && ie <= 9) { + ajax.bind( + "readystatechange", + onreadystatechange + ); + } else { + ajax.onreadystatechange = onreadystatechange; + } + ajax.send(ctx); + logger.debug("mkfile: ", url); + } else { + last_step(up, file, info.response); + } + }; + })(_FileUploaded_Handler) + ); + + logger.debug("bind FileUploaded event"); + + // init uploader + uploader.init(); + + logger.debug("invoke uploader.init()"); + + logger.debug("init uploader end"); + + return uploader; + }; + + /** + * get url by key + * @param {String} key of file + * @return {String} url of file + */ + this.getUrl = function (key) { + if (!key) { + return false; + } + key = encodeURI(key); + var domain = this.domain; + if (domain.slice(domain.length - 1) !== "/") { + domain = domain + "/"; + } + return domain + key; + }; + + /** + * invoke the imageView2 api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.imageView2 = function (op, key) { + if (!/^\d$/.test(op.mode)) { + return false; + } + + var mode = op.mode, + w = op.w || "", + h = op.h || "", + q = op.q || "", + format = op.format || ""; + + if (!w && !h) { + return false; + } + + var imageUrl = "imageView2/" + mode; + imageUrl += w ? "/w/" + w : ""; + imageUrl += h ? "/h/" + h : ""; + imageUrl += q ? "/q/" + q : ""; + imageUrl += format ? "/format/" + format : ""; + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the imageMogr2 api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.imageMogr2 = function (op, key) { + var auto_orient = op["auto-orient"] || "", + thumbnail = op.thumbnail || "", + strip = op.strip || "", + gravity = op.gravity || "", + crop = op.crop || "", + quality = op.quality || "", + rotate = op.rotate || "", + format = op.format || "", + blur = op.blur || ""; + //Todo check option + + var imageUrl = "imageMogr2"; + + imageUrl += auto_orient ? "/auto-orient" : ""; + imageUrl += thumbnail ? "/thumbnail/" + thumbnail : ""; + imageUrl += strip ? "/strip" : ""; + imageUrl += gravity ? "/gravity/" + gravity : ""; + imageUrl += quality ? "/quality/" + quality : ""; + imageUrl += crop ? "/crop/" + crop : ""; + imageUrl += rotate ? "/rotate/" + rotate : ""; + imageUrl += format ? "/format/" + format : ""; + imageUrl += blur ? "/blur/" + blur : ""; + + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the watermark api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.watermark = function (op, key) { + var mode = op.mode; + if (!mode) { + return false; + } + + var imageUrl = "watermark/" + mode; + + if (mode === 1) { + var image = op.image || ""; + if (!image) { + return false; + } + imageUrl += image + ? "/image/" + this.URLSafeBase64Encode(image) + : ""; + } else if (mode === 2) { + var text = op.text ? op.text : "", + font = op.font ? op.font : "", + fontsize = op.fontsize ? op.fontsize : "", + fill = op.fill ? op.fill : ""; + if (!text) { + return false; + } + imageUrl += text + ? "/text/" + this.URLSafeBase64Encode(text) + : ""; + imageUrl += font + ? "/font/" + this.URLSafeBase64Encode(font) + : ""; + imageUrl += fontsize ? "/fontsize/" + fontsize : ""; + imageUrl += fill + ? "/fill/" + this.URLSafeBase64Encode(fill) + : ""; + } else { + // Todo mode3 + return false; + } + + var dissolve = op.dissolve || "", + gravity = op.gravity || "", + dx = op.dx || "", + dy = op.dy || ""; + + imageUrl += dissolve ? "/dissolve/" + dissolve : ""; + imageUrl += gravity ? "/gravity/" + gravity : ""; + imageUrl += dx ? "/dx/" + dx : ""; + imageUrl += dy ? "/dy/" + dy : ""; + + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the imageInfo api of Qiniu + * @param {String} key of file + * @return {Object} image info + */ + this.imageInfo = function (key) { + if (!key) { + return false; + } + var url = this.getUrl(key) + "?imageInfo"; + var xhr = this.createAjax(); + var info; + var that = this; + xhr.open("GET", url, false); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + info = that.parseJSON(xhr.responseText); + } + }; + xhr.send(); + return info; + }; + + /** + * invoke the exif api of Qiniu + * @param {String} key of file + * @return {Object} image exif + */ + this.exif = function (key) { + if (!key) { + return false; + } + var url = this.getUrl(key) + "?exif"; + var xhr = this.createAjax(); + var info; + var that = this; + xhr.open("GET", url, false); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + info = that.parseJSON(xhr.responseText); + } + }; + xhr.send(); + return info; + }; + + /** + * invoke the exif or imageInfo api of Qiniu + * according with type param + * @param {String} ['exif'|'imageInfo']type of info + * @param {String} key of file + * @return {Object} image exif or info + */ + this.get = function (type, key) { + if (!key || !type) { + return false; + } + if (type === "exif") { + return this.exif(key); + } else if (type === "imageInfo") { + return this.imageInfo(key); + } + return false; + }; + + /** + * invoke api of Qiniu like a pipeline + * @param {Array of Object} params of a series api call + * each object in array is options of api which name is set as 'fop' property + * each api's output will be next api's input + * @param {String} key of file + * @return {String|Boolean} url of processed image + */ + this.pipeline = function (arr, key) { + var isArray = + Object.prototype.toString.call(arr) === "[object Array]"; + var option, + errOp, + imageUrl = ""; + if (isArray) { + for (var i = 0, len = arr.length; i < len; i++) { + option = arr[i]; + if (!option.fop) { + return false; + } + switch (option.fop) { + case "watermark": + imageUrl += this.watermark(option) + "|"; + break; + case "imageView2": + imageUrl += this.imageView2(option) + "|"; + break; + case "imageMogr2": + imageUrl += this.imageMogr2(option) + "|"; + break; + default: + errOp = true; + break; + } + if (errOp) { + return false; + } + } + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + var length = imageUrl.length; + if (imageUrl.slice(length - 1) === "|") { + imageUrl = imageUrl.slice(0, length - 1); + } + } + return imageUrl; + } + return false; + }; + } + + var Qiniu = new QiniuJsSDK(); + + global.Qiniu = Qiniu; + + global.QiniuJsSDK = QiniuJsSDK; +})(window); diff --git a/assets/public/static/js/uploader/uploader_remote.js b/assets/public/static/js/uploader/uploader_remote.js new file mode 100644 index 00000000..475b3eda --- /dev/null +++ b/assets/public/static/js/uploader/uploader_remote.js @@ -0,0 +1,1448 @@ +function getCookieByString(cookieName) { + var start = document.cookie.indexOf(cookieName + "="); + if (start == -1) return false; + start = start + cookieName.length + 1; + var end = document.cookie.indexOf(";", start); + if (end == -1) end = document.cookie.length; + return document.cookie.substring(start, end); +} +(function (global) { + /** + * Creates new cookie or removes cookie with negative expiration + * @param key The key or identifier for the store + * @param value Contents of the store + * @param exp Expiration - creation defaults to 30 days + */ + function createCookie(key, value, exp) { + var date = new Date(); + date.setTime(date.getTime() + exp * 24 * 60 * 60 * 1000); + var expires = "; expires=" + date.toGMTString(); + document.cookie = key + "=" + value + expires + "; path=/"; + } + + /** + * Returns contents of cookie + * @param key The key or identifier for the store + */ + function readCookie(key) { + var nameEQ = key + "="; + var ca = document.cookie.split(";"); + for (var i = 0, max = ca.length; i < max; i++) { + var c = ca[i]; + while (c.charAt(0) === " ") { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return c.substring(nameEQ.length, c.length); + } + } + return null; + } + + // if current browser is not support localStorage + // use cookie to make a polyfill + if (!window.localStorage) { + window.localStorage = { + setItem: function (key, value) { + createCookie(key, value, 30); + }, + getItem: function (key) { + return readCookie(key); + }, + removeItem: function (key) { + createCookie(key, "", -1); + }, + }; + } + + function QiniuJsSDK() { + var that = this; + + /** + * detect IE version + * if current browser is not IE + * it will return false + * else + * it will return version of current IE browser + * @return {Number|Boolean} IE version or false + */ + this.detectIEVersion = function () { + var v = 4, + div = document.createElement("div"), + all = div.getElementsByTagName("i"); + while ( + ((div.innerHTML = + ""), + all[0]) + ) { + v++; + } + return v > 4 ? v : false; + }; + + var logger = { + MUTE: 0, + FATA: 1, + ERROR: 2, + WARN: 3, + INFO: 4, + DEBUG: 5, + TRACE: 6, + level: 0, + }; + + function log(type, args) { + var header = "[Cloudreve-uploader][" + type + "]"; + var msg = header; + for (var i = 0; i < args.length; i++) { + if (typeof args[i] === "string") { + msg += " " + args[i]; + } else { + msg += " " + that.stringifyJSON(args[i]); + } + } + if (that.detectIEVersion()) { + // http://stackoverflow.com/questions/5538972/console-log-apply-not-working-in-ie9 + //var log = Function.prototype.bind.call(console.log, console); + //log.apply(console, args); + console.log(msg); + } else { + args.unshift(header); + console.log.apply(console, args); + } + if (document.getElementById("qiniu-js-sdk-log")) { + document.getElementById("qiniu-js-sdk-log").innerHTML += + "

" + msg + "

"; + } + } + + function makeLogFunc(code) { + var func = code.toLowerCase(); + logger[func] = function () { + // logger[func].history = logger[func].history || []; + // logger[func].history.push(arguments); + if ( + window.console && + window.console.log && + logger.level >= logger[code] + ) { + var args = Array.prototype.slice.call(arguments); + log(func, args); + } + }; + } + + for (var property in logger) { + if ( + logger.hasOwnProperty(property) && + typeof logger[property] === "number" && + !logger.hasOwnProperty(property.toLowerCase()) + ) { + makeLogFunc(property); + } + } + + var qiniuUploadUrl; + + qiniuUploadUrl = uploadConfig.upUrl; + var qiniuUploadUrls = [uploadConfig.upUrl]; + var qiniuUpHosts = { + http: [uploadConfig.upUrl], + https: [uploadConfig.upUrl], + }; + + var changeUrlTimes = 0; + + /** + * reset upload url + * if current page protocal is https + * it will always return 'https://up.qbox.me' + * else + * it will set 'qiniuUploadUrl' value with 'qiniuUploadUrls' looply + */ + this.resetUploadUrl = function () { + var hosts = + window.location.protocol === "https:" + ? qiniuUpHosts.https + : qiniuUpHosts.http; + var i = changeUrlTimes % hosts.length; + qiniuUploadUrl = hosts[i]; + changeUrlTimes++; + logger.debug("resetUploadUrl: " + qiniuUploadUrl); + }; + + // this.resetUploadUrl(); + + /** + * is image + * @param {String} url of a file + * @return {Boolean} file is a image or not + */ + this.isImage = function (url) { + url = url.split(/[?#]/)[0]; + return /\.(png|jpg|jpeg|gif|bmp)$/i.test(url); + }; + + /** + * get file extension + * @param {String} filename + * @return {String} file extension + * @example + * input: test.txt + * output: txt + */ + this.getFileExtension = function (filename) { + var tempArr = filename.split("."); + var ext; + if ( + tempArr.length === 1 || + (tempArr[0] === "" && tempArr.length === 2) + ) { + ext = ""; + } else { + ext = tempArr.pop().toLowerCase(); //get the extension and make it lower-case + } + return ext; + }; + + /** + * encode string by utf8 + * @param {String} string to encode + * @return {String} encoded string + */ + this.utf8_encode = function (argString) { + // http://kevin.vanzonneveld.net + // + original by: Webtoolkit.info (http://www.webtoolkit.info/) + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + improved by: sowberry + // + tweaked by: Jack + // + bugfixed by: Onno Marsman + // + improved by: Yves Sucaet + // + bugfixed by: Onno Marsman + // + bugfixed by: Ulrich + // + bugfixed by: Rafal Kukawski + // + improved by: kirilloid + // + bugfixed by: kirilloid + // * example 1: this.utf8_encode('Kevin van Zonneveld'); + // * returns 1: 'Kevin van Zonneveld' + + if (argString === null || typeof argString === "undefined") { + return ""; + } + + var string = argString + ""; // .replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + var utftext = "", + start, + end, + stringl = 0; + + start = end = 0; + stringl = string.length; + for (var n = 0; n < stringl; n++) { + var c1 = string.charCodeAt(n); + var enc = null; + + if (c1 < 128) { + end++; + } else if (c1 > 127 && c1 < 2048) { + enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); + } else if ((c1 & 0xf800) ^ (0xd800 > 0)) { + enc = String.fromCharCode( + (c1 >> 12) | 224, + ((c1 >> 6) & 63) | 128, + (c1 & 63) | 128 + ); + } else { + // surrogate pairs + if ((c1 & 0xfc00) ^ (0xd800 > 0)) { + throw new RangeError( + "Unmatched trail surrogate at " + n + ); + } + var c2 = string.charCodeAt(++n); + if ((c2 & 0xfc00) ^ (0xdc00 > 0)) { + throw new RangeError( + "Unmatched lead surrogate at " + (n - 1) + ); + } + c1 = ((c1 & 0x3ff) << 10) + (c2 & 0x3ff) + 0x10000; + enc = String.fromCharCode( + (c1 >> 18) | 240, + ((c1 >> 12) & 63) | 128, + ((c1 >> 6) & 63) | 128, + (c1 & 63) | 128 + ); + } + if (enc !== null) { + if (end > start) { + utftext += string.slice(start, end); + } + utftext += enc; + start = end = n + 1; + } + } + + if (end > start) { + utftext += string.slice(start, stringl); + } + + return utftext; + }; + + this.base64_decode = function (data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Thunder.m + // + input by: Aman Gupta + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Onno Marsman + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: Brett Zamir (http://brett-zamir.me) + // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA=='); + // * returns 1: 'Kevin van Zonneveld' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + var b64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, + o2, + o3, + h1, + h2, + h3, + h4, + bits, + i = 0, + ac = 0, + dec = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data += ""; + + do { + // unpack four hexets into three octets using index points in b64 + h1 = b64.indexOf(data.charAt(i++)); + h2 = b64.indexOf(data.charAt(i++)); + h3 = b64.indexOf(data.charAt(i++)); + h4 = b64.indexOf(data.charAt(i++)); + + bits = (h1 << 18) | (h2 << 12) | (h3 << 6) | h4; + + o1 = (bits >> 16) & 0xff; + o2 = (bits >> 8) & 0xff; + o3 = bits & 0xff; + + if (h3 === 64) { + tmp_arr[ac++] = String.fromCharCode(o1); + } else if (h4 === 64) { + tmp_arr[ac++] = String.fromCharCode(o1, o2); + } else { + tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); + } + } while (i < data.length); + + dec = tmp_arr.join(""); + + return dec; + }; + + /** + * encode data by base64 + * @param {String} data to encode + * @return {String} encoded data + */ + this.base64_encode = function (data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Bayron Guevara + // + improved by: Thunder.m + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // - depends on: this.utf8_encode + // * example 1: this.base64_encode('Kevin van Zonneveld'); + // * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA==' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + var b64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, + o2, + o3, + h1, + h2, + h3, + h4, + bits, + i = 0, + ac = 0, + enc = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data = this.utf8_encode(data + ""); + + do { + // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = (o1 << 16) | (o2 << 8) | o3; + + h1 = (bits >> 18) & 0x3f; + h2 = (bits >> 12) & 0x3f; + h3 = (bits >> 6) & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = + b64.charAt(h1) + + b64.charAt(h2) + + b64.charAt(h3) + + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(""); + + switch (data.length % 3) { + case 1: + enc = enc.slice(0, -2) + "=="; + break; + case 2: + enc = enc.slice(0, -1) + "="; + break; + } + + return enc; + }; + + /** + * encode string in url by base64 + * @param {String} string in url + * @return {String} encoded string + */ + this.URLSafeBase64Encode = function (v) { + v = this.base64_encode(v); + return v.replace(/\//g, "_").replace(/\+/g, "-"); + }; + + this.URLSafeBase64Decode = function (v) { + v = v.replace(/_/g, "/").replace(/-/g, "+"); + return this.base64_decode(v); + }; + + // TODO: use mOxie + /** + * craete object used to AJAX + * @return {Object} + */ + this.createAjax = function (argument) { + var xmlhttp = {}; + if (window.XMLHttpRequest) { + xmlhttp = new XMLHttpRequest(); + } else { + xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + return xmlhttp; + }; + + // TODO: enhance IE compatibility + /** + * parse json string to javascript object + * @param {String} json string + * @return {Object} object + */ + this.parseJSON = function (data) { + // Attempt to parse using the native JSON parser first + if (window.JSON && window.JSON.parse) { + return window.JSON.parse(data); + } + + //var rx_one = /^[\],:{}\s]*$/, + // rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + // rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + // rx_four = /(?:^|:|,)(?:\s*\[)+/g, + var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + + //var json; + + var text = String(data); + rx_dangerous.lastIndex = 0; + if (rx_dangerous.test(text)) { + text = text.replace(rx_dangerous, function (a) { + return ( + "\\u" + + ("0000" + a.charCodeAt(0).toString(16)).slice(-4) + ); + }); + } + + // todo 使用一下判断,增加安全性 + //if ( + // rx_one.test( + // text + // .replace(rx_two, '@') + // .replace(rx_three, ']') + // .replace(rx_four, '') + // ) + //) { + // return eval('(' + text + ')'); + //} + + return eval("(" + text + ")"); + }; + + /** + * parse javascript object to json string + * @param {Object} object + * @return {String} json string + */ + this.stringifyJSON = function (obj) { + // Attempt to parse using the native JSON parser first + if (window.JSON && window.JSON.stringify) { + return window.JSON.stringify(obj); + } + switch (typeof obj) { + case "string": + return '"' + obj.replace(/(["\\])/g, "\\$1") + '"'; + case "array": + return "[" + obj.map(that.stringifyJSON).join(",") + "]"; + case "object": + if (obj instanceof Array) { + var strArr = []; + var len = obj.length; + for (var i = 0; i < len; i++) { + strArr.push(that.stringifyJSON(obj[i])); + } + return "[" + strArr.join(",") + "]"; + } else if (obj === null) { + return "null"; + } else { + var string = []; + for (var property in obj) { + if (obj.hasOwnProperty(property)) { + string.push( + that.stringifyJSON(property) + + ":" + + that.stringifyJSON(obj[property]) + ); + } + } + return "{" + string.join(",") + "}"; + } + break; + case "number": + return obj; + case false: + return obj; + case "boolean": + return obj; + } + }; + + /** + * trim space beside text + * @param {String} untrimed string + * @return {String} trimed string + */ + this.trim = function (text) { + return text === null ? "" : text.replace(/^\s+|\s+$/g, ""); + }; + + /** + * create a uploader by QiniuJsSDK + * @param {object} options to create a new uploader + * @return {object} uploader + */ + this.uploader = function (op) { + /********** inner function define start **********/ + + // according the different condition to reset chunk size + // and the upload strategy according with the chunk size + // when chunk size is zero will cause to direct upload + // see the statement binded on 'BeforeUpload' event + var reset_chunk_size = function () { + var ie = that.detectIEVersion(); + var BLOCK_BITS, MAX_CHUNK_SIZE, chunk_size; + // case Safari 5、Windows 7、iOS 7 set isSpecialSafari to true + var isSpecialSafari = + (moxie.core.utils.Env.browser === "Safari" && + moxie.core.utils.Env.version <= 5 && + moxie.core.utils.Env.os === "Windows" && + moxie.core.utils.Env.osVersion === "7") || + (moxie.core.utils.Env.browser === "Safari" && + moxie.core.utils.Env.os === "iOS" && + moxie.core.utils.Env.osVersion === "7"); + // case IE 9-,chunk_size is not empty and flash is included in runtimes + // set op.chunk_size to zero + //if (ie && ie < 9 && op.chunk_size && op.runtimes.indexOf('flash') >= 0) { + if ( + ie && + ie < 9 && + op.chunk_size && + op.runtimes.indexOf("flash") >= 0 + ) { + // link: http://www.plupload.com/docs/Frequently-Asked-Questions#when-to-use-chunking-and-when-not + // when plupload chunk_size setting is't null ,it cause bug in ie8/9 which runs flash runtimes (not support html5) . + op.chunk_size = 0; + } else if (isSpecialSafari) { + // win7 safari / iOS7 safari have bug when in chunk upload mode + // reset chunk_size to 0 + // disable chunk in special version safari + op.chunk_size = 0; + } else { + BLOCK_BITS = 20; + MAX_CHUNK_SIZE = 4 << BLOCK_BITS; //4M + + chunk_size = plupload.parseSize(op.chunk_size); + + // qiniu service max_chunk_size is 4m + // reset chunk_size to max_chunk_size(4m) when chunk_size > 4m + } + // if op.chunk_size set 0 will be cause to direct upload + }; + + var getHosts = function (hosts) { + var result = []; + for (var i = 0; i < hosts.length; i++) { + var host = hosts[i]; + if (host.indexOf("-H") === 0) { + result.push(host.split(" ")[2]); + } else { + result.push(host); + } + } + return result; + }; + + var getPutPolicy = function (uptoken) { + if ( + uploadConfig.saveType == "local" || + uploadConfig.saveType == "oss" || + uploadConfig.saveType == "upyun" || + uploadConfig.saveType == "s3" || + uploadConfig.saveType == "remote" + ) { + return "oss"; + } else { + var segments = uptoken.split(":"); + var ak = segments[0]; + var putPolicy = that.parseJSON( + that.URLSafeBase64Decode(segments[2]) + ); + putPolicy.ak = ak; + if (putPolicy.scope.indexOf(":") >= 0) { + putPolicy.bucket = putPolicy.scope.split(":")[0]; + putPolicy.key = putPolicy.scope.split(":")[1]; + } else { + putPolicy.bucket = putPolicy.scope; + } + return putPolicy; + } + }; + + var getUpHosts = function (uptoken) { + var putPolicy = getPutPolicy(uptoken); + // var uphosts_url = "//uc.qbox.me/v1/query?ak="+ak+"&bucket="+putPolicy.scope; + // IE 9- is not support protocal relative url + var uphosts_url = + window.location.protocol + + "//uc.qbox.me/v1/query?ak=" + + putPolicy.ak + + "&bucket=" + + putPolicy.bucket; + logger.debug("putPolicy: ", putPolicy); + logger.debug("get uphosts from: ", uphosts_url); + var ie = that.detectIEVersion(); + var ajax; + if (ie && ie <= 9) { + ajax = new moxie.XMLHttpRequest(); + moxie.core.utils.Env.swf_url = op.flash_swf_url; + } else { + ajax = that.createAjax(); + } + if (uploadConfig.saveType != "qiniu") { + qiniuUpHosts.http = [uploadConfig.upUrl]; + qiniuUpHosts.http = [uploadConfig.upUrl]; + that.resetUploadUrl(); + } else { + ajax.open("GET", uphosts_url, false); + var onreadystatechange = function () { + logger.debug("ajax.readyState: ", ajax.readyState); + if (ajax.readyState === 4) { + logger.debug("ajax.status: ", ajax.status); + if (ajax.status < 400) { + var res = that.parseJSON(ajax.responseText); + qiniuUpHosts.http = getHosts(res.http.up); + qiniuUpHosts.https = getHosts(res.https.up); + logger.debug("get new uphosts: ", qiniuUpHosts); + that.resetUploadUrl(); + } else { + logger.error( + "get uphosts error: ", + ajax.responseText + ); + } + } + }; + if (ie && ie <= 9) { + ajax.bind("readystatechange", onreadystatechange); + } else { + ajax.onreadystatechange = onreadystatechange; + } + ajax.send(); + // ajax.send(); + // if (ajax.status < 400) { + // var res = that.parseJSON(ajax.responseText); + // qiniuUpHosts.http = getHosts(res.http.up); + // qiniuUpHosts.https = getHosts(res.https.up); + // logger.debug("get new uphosts: ", qiniuUpHosts); + // that.resetUploadUrl(); + // } else { + // logger.error("get uphosts error: ", ajax.responseText); + // } + } + return; + }; + + var getUptoken = function (file) { + if (uploadConfig.saveType == "remote") { + return that.token; + } + if ( + !that.token || + (op.uptoken_url && that.tokenInfo.isExpired()) + ) { + return getNewUpToken(file, function () {}); + } else { + return that.token; + } + }; + + // getNewUptoken maybe called at Init Event or BeforeUpload Event + // case Init Event, the file param of getUptken will be set a null value + // if op.uptoken has value, set uptoken with op.uptoken + // else if op.uptoken_url has value, set uptoken from op.uptoken_url + // else if op.uptoken_func has value, set uptoken by result of op.uptoken_func + var getNewUpToken = function (file, callback) { + if (op.uptoken) { + that.token = op.uptoken; + } else if (op.uptoken_url) { + logger.debug("get uptoken from: ", that.uptoken_url); + var ajax = that.createAjax(); + if (file.size === undefined) { + file.size = 0; + } + ajax.open( + "GET", + that.uptoken_url + + "?path=" + + encodeURIComponent(window.pathCache[file.id]) + + "&size=" + + file.size + + "&name=" + + encodeURIComponent(file.name) + + "&type=remote", + true + ); + ajax.setRequestHeader("If-Modified-Since", "0"); + ajax.send(); + ajax.onload = function (e) { + if (ajax.status === 200) { + var res = that.parseJSON(ajax.responseText); + if (res.code != 0) { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + message: res.msg, + code: 402, + }); + callback(); + return; + } + that.token = res.data.token; + that.putPolicy = res.data.policy; + logger.debug("get new uptoken: ", that.token); + logger.debug("get new policy: ", that.putPolicy); + } else { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + code: 402, + }); + logger.error( + "get uptoken error: ", + ajax.responseText + ); + } + callback(); + }; + ajax.onerror = function (e) { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + code: 402, + }); + callback(); + logger.error("get uptoken error: ", ajax.responseText); + }; + } else if (op.uptoken_func) { + logger.debug("get uptoken from uptoken_func"); + that.token = op.uptoken_func(file); + logger.debug("get new uptoken: ", that.token); + callback(); + } else { + logger.error( + "one of [uptoken, uptoken_url, uptoken_func] settings in options is required!" + ); + callback(); + } + return that.token; + }; + + // get file key according with the user passed options + var getFileKey = function (up, file, func) { + // WARNING + // When you set the key in putPolicy by "scope": "bucket:key" + // You should understand the risk of override a file in the bucket + // So the code below that automatically get key from uptoken has been commented + // var putPolicy = getPutPolicy(that.token) + // if (putPolicy.key) { + // logger.debug("key is defined in putPolicy.scope: ", putPolicy.key) + // return putPolicy.key + // } + var key = "", + unique_names = false; + if (!op.save_key) { + unique_names = up.getOption && up.getOption("unique_names"); + unique_names = + unique_names || + (up.settings && up.settings.unique_names); + if (unique_names) { + var ext = that.getFileExtension(file.name); + key = ext ? file.id + "." + ext : file.id; + } else if (typeof func === "function") { + key = func(up, file); + } else { + key = file.name; + } + } + if (uploadConfig.saveType == "qiniu") { + return ""; + } + return key; + }; + + /********** inner function define end **********/ + + if (op.log_level) { + logger.level = op.log_level; + } + + if (!op.domain) { + throw "domain setting in options is required!"; + } + + if (!op.browse_button) { + throw "browse_button setting in options is required!"; + } + + if (!op.uptoken && !op.uptoken_url && !op.uptoken_func) { + throw "one of [uptoken, uptoken_url, uptoken_func] settings in options is required!"; + } + + logger.debug("init uploader start"); + + logger.debug("environment: ", moxie.core.utils.Env); + + logger.debug("userAgent: ", navigator.userAgent); + + var option = {}; + + // hold the handler from user passed options + var _Error_Handler = op.init && op.init.Error; + var _FileUploaded_Handler = op.init && op.init.FileUploaded; + + // replace the handler for intercept + op.init.Error = function () {}; + op.init.FileUploaded = function () {}; + + that.uptoken_url = op.uptoken_url; + that.token = ""; + that.key_handler = + typeof op.init.Key === "function" ? op.init.Key : ""; + this.domain = op.domain; + // TODO: ctx is global in scope of a uploader instance + // this maybe cause error + var ctx = ""; + var speedCalInfo = { + isResumeUpload: false, + resumeFilesize: 0, + startTime: "", + currentTime: "", + }; + + reset_chunk_size(); + logger.debug("invoke reset_chunk_size()"); + logger.debug("op.chunk_size: ", op.chunk_size); + + var defaultSetting = { + url: qiniuUploadUrl, + multipart_params: {}, + }; + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + defaultSetting.multipart_params.accept = + "text/plain; charset=utf-8"; + logger.debug("add accept text/plain in multipart params"); + } + + // compose options with user passed options and default setting + plupload.extend(option, op, defaultSetting); + + logger.debug("option: ", option); + + // create a new uploader with composed options + var uploader = new plupload.Uploader(option); + + logger.debug("new plupload.Uploader(option)"); + + // bind getNewUpToken to 'Init' event + uploader.bind("Init", function (up, params) { + logger.debug("Init event activated"); + // if op.get_new_uptoken is not true + // invoke getNewUptoken when uploader init + // else + // getNewUptoken everytime before a new file upload + if (!op.get_new_uptoken) { + getNewUpToken(null, function () {}); + } + //getNewUpToken(null); + }); + + logger.debug("bind Init event"); + + // bind 'FilesAdded' event + // when file be added and auto_start has set value + // uploader will auto start upload the file + uploader.bind("FilesAdded", function (up, files) { + logger.debug("FilesAdded event activated"); + var auto_start = up.getOption && up.getOption("auto_start"); + auto_start = + auto_start || (up.settings && up.settings.auto_start); + logger.debug("auto_start: ", auto_start); + logger.debug("files: ", files); + + // detect is iOS + var is_ios = function () { + if (moxie.core.utils.Env.OS.toLowerCase() === "ios") { + return true; + } else { + return false; + } + }; + + // if current env os is iOS change file name to [time].[ext] + if (is_ios()) { + for (var i = 0; i < files.length; i++) { + var file = files[i]; + var ext = that.getFileExtension(file.name); + file.name = file.id + "." + ext; + } + } + + if (auto_start) { + setTimeout(function () { + up.start(); + logger.debug("invoke up.start()"); + }, 0); + // up.start(); + // plupload.each(files, function(i, file) { + // up.start(); + // logger.debug("invoke up.start()") + // logger.debug("file: ", file); + // }); + } + up.refresh(); // Reposition Flash/Silverlight + }); + + logger.debug("bind FilesAdded event"); + + // bind 'BeforeUpload' event + // intercept the process of upload + // - prepare uptoken + // - according the chunk size to make differnt upload strategy + // - resume upload with the last breakpoint of file + uploader.bind("BeforeUpload", function (up, file) { + logger.debug("BeforeUpload event activated"); + // add a key named speed for file object + file.speed = file.speed || 0; + ctx = ""; + + var directUpload = function (up, file, func) { + speedCalInfo.startTime = new Date().getTime(); + var multipart_params_obj; + if (op.save_key) { + multipart_params_obj = { + token: that.token, + }; + } else { + multipart_params_obj = { + key: getFileKey(up, file, func), + token: that.token, + }; + } + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + multipart_params_obj.accept = + "text/plain; charset=utf-8"; + logger.debug( + "add accept text/plain in multipart params" + ); + } + + logger.debug( + "directUpload multipart_params_obj: ", + multipart_params_obj + ); + + var x_vars = op.x_vars; + if (x_vars !== undefined && typeof x_vars === "object") { + for (var x_key in x_vars) { + if (x_vars.hasOwnProperty(x_key)) { + if (typeof x_vars[x_key] === "function") { + multipart_params_obj["x:" + x_key] = x_vars[ + x_key + ](up, file); + } else if (typeof x_vars[x_key] !== "object") { + multipart_params_obj["x:" + x_key] = + x_vars[x_key]; + } + } + } + } + + // 远程策略时不用multipart + up.setOption({ + url: qiniuUploadUrl, + multipart: false, + send_file_name: false, + headers: { + "X-Policy": that.putPolicy, + "X-Overwrite": "false", + Authorization: that.token, + "X-FileName": encodeURIComponent( + getFileKey(up, file, func) + ), + }, + }); + }; + + // detect is weixin or qq inner browser + var is_android_weixin_or_qq = function () { + var ua = navigator.userAgent.toLowerCase(); + if ( + (ua.match(/MicroMessenger/i) || + moxie.core.utils.Env.browser === "QQBrowser" || + ua.match(/V1_AND_SQ/i)) && + moxie.core.utils.Env.OS.toLowerCase() === "android" + ) { + return true; + } else { + return false; + } + }; + + var chunk_size = up.getOption && up.getOption("chunk_size"); + chunk_size = + chunk_size || (up.settings && up.settings.chunk_size); + + logger.debug("uploader.runtime: ", uploader.runtime); + logger.debug("chunk_size: ", chunk_size); + + getNewUpToken(file, () => { + if (that.token) { + getUpHosts(that.token); + } + + if ( + (uploader.runtime === "html5" || + uploader.runtime === "flash") && + chunk_size + ) { + if ( + file.size < chunk_size || + is_android_weixin_or_qq() + ) { + logger.debug( + "directUpload because file.size < chunk_size || is_android_weixin_or_qq()" + ); + // direct upload if file size is less then the chunk size + directUpload(up, file, that.key_handler); + } else { + // TODO: need a polifill to make it work in IE 9- + // ISSUE: if file.name is existed in localStorage + // but not the same file maybe cause error + var localFileInfo = localStorage.getItem(file.name); + var blockSize = chunk_size; + if (localFileInfo) { + // TODO: although only the html5 runtime will enter this statement + // but need uniform way to make convertion between string and json + localFileInfo = that.parseJSON(localFileInfo); + var now = new Date().getTime(); + var before = localFileInfo.time || 0; + var aDay = 24 * 60 * 60 * 1000; // milliseconds of one day + // if the last upload time is within one day + // will upload continuously follow the last breakpoint + // else + // will reupload entire file + if (now - before < aDay) { + if (localFileInfo.percent !== 100) { + if (file.size === localFileInfo.total) { + // TODO: if file.name and file.size is the same + // but not the same file will cause error + file.percent = + localFileInfo.percent; + file.loaded = localFileInfo.offset; + ctx = localFileInfo.ctx; + + // set speed info + speedCalInfo.isResumeUpload = true; + speedCalInfo.resumeFilesize = + localFileInfo.offset; + + // set block size + if ( + localFileInfo.offset + + blockSize > + file.size + ) { + blockSize = + file.size - + localFileInfo.offset; + } + } else { + // remove file info when file.size is conflict with file info + localStorage.removeItem(file.name); + } + } else { + // remove file info when upload percent is 100% + // avoid 499 bug + localStorage.removeItem(file.name); + } + } else { + // remove file info when last upload time is over one day + localStorage.removeItem(file.name); + } + } + speedCalInfo.startTime = new Date().getTime(); + var multipart_params_obj = {}; + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + multipart_params_obj.accept = + "text/plain; charset=utf-8"; + logger.debug( + "add accept text/plain in multipart params" + ); + } + // TODO: to support bput + // http://developer.qiniu.com/docs/v6/api/reference/up/bput.html + if (uploadConfig.saveType == "remote") { + up.setOption({ + url: qiniuUploadUrl + "chunk.php", + multipart: false, + chunk_size: chunk_size, + required_features: "chunks", + headers: { + Authorization: getUptoken(file), + }, + multipart_params: multipart_params_obj, + }); + } else { + up.setOption({ + url: qiniuUploadUrl + "/mkblk/" + blockSize, + multipart: false, + chunk_size: chunk_size, + required_features: "chunks", + headers: { + Authorization: + "UpToken " + getUptoken(file), + }, + multipart_params: multipart_params_obj, + }); + } + } + } else { + logger.debug( + "directUpload because uploader.runtime !== 'html5' || uploader.runtime !== 'flash' || !chunk_size" + ); + // direct upload if runtime is not html5 + directUpload(up, file, that.key_handler); + } + if (file.status != plupload.FAILED) { + file.status = plupload.UPLOADING; + up.trigger("UploadFile", file); + } else { + up.stop(); + } + }); + + return false; + }); + + logger.debug("bind BeforeUpload event"); + + // bind 'UploadProgress' event + // calculate upload speed + uploader.bind("UploadProgress", function (up, file) { + logger.trace("UploadProgress event activated"); + speedCalInfo.currentTime = new Date().getTime(); + var timeUsed = + speedCalInfo.currentTime - speedCalInfo.startTime; // ms + var fileUploaded = file.loaded || 0; + if (speedCalInfo.isResumeUpload) { + fileUploaded = file.loaded - speedCalInfo.resumeFilesize; + } + file.speed = ((fileUploaded / timeUsed) * 1000).toFixed(0) || 0; // unit: byte/s + }); + + logger.debug("bind UploadProgress event"); + + var retries = qiniuUploadUrls.length; + + // if error is unkown switch upload url and retry + var unknow_error_retry = function (file) { + if (retries-- > 0) { + setTimeout(function () { + that.resetUploadUrl(); + file.status = plupload.QUEUED; + uploader.stop(); + uploader.start(); + }, 0); + return true; + } else { + retries = qiniuUploadUrls.length; + return false; + } + }; + + // bind 'Error' event + // check the err.code and return the errTip + uploader.bind( + "Error", + (function (_Error_Handler) { + return function (up, err) { + logger.error("Error event activated"); + logger.error("err: ", err); + var errTip = ""; + var file = err.file; + if (file) { + switch (err.code) { + case plupload.FAILED: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.FILE_SIZE_ERROR: + var max_file_size = + up.getOption && + up.getOption("max_file_size"); + max_file_size = + max_file_size || + (up.settings && + up.settings.max_file_size); + errTip = + "文件过大,您当前用户组最多可上传" + + max_file_size + + "的文件"; + break; + case plupload.FILE_EXTENSION_ERROR: + errTip = "您当前的用户组不可上传此文件"; + break; + case plupload.HTTP_ERROR: + if (err.response === "") { + // Fix parseJSON error ,when http error is like net::ERR_ADDRESS_UNREACHABLE + errTip = + err.message || "未知网络错误。"; + if (!unknow_error_retry(file)) { + return; + } + break; + } + var errorObj = that.parseJSON(err.response); + var errorText = errorObj.error; + if (err.status == 579) { + var errorObj2 = that.parseJSON( + errorText + ); + errorText = errorObj2.error; + } + switch (err.status) { + case 400: + errTip = "请求报文格式错误。"; + break; + case 401: + errTip = + "客户端认证授权失败。请重试或提交反馈。"; + break; + case 405: + errTip = + "客户端请求错误。请重试或提交反馈。"; + break; + case 579: + errTip = + "资源上传成功,但回调失败。"; + break; + case 599: + errTip = + "网络连接异常。请重试或提交反馈。"; + if (!unknow_error_retry(file)) { + return; + } + break; + case 614: + errTip = "文件已存在。"; + try { + errorObj = that.parseJSON( + errorObj.error + ); + errorText = + errorObj.error || + "file exists"; + } catch (e) { + errorText = + errorObj.error || + "file exists"; + } + break; + case 631: + errTip = "指定空间不存在。"; + break; + case 701: + errTip = + "上传数据块校验出错。请重试或提交反馈。"; + break; + default: + if (err.message) { + errTip = err.message; + } else { + errTip = "未知错误"; + } + break; + } + break; + case plupload.SECURITY_ERROR: + errTip = "安全配置错误。请联系网站管理员。"; + break; + case plupload.GENERIC_ERROR: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.IO_ERROR: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.INIT_ERROR: + errTip = "网站配置错误。请联系网站管理员。"; + uploader.destroy(); + break; + case 402: + errTip = "无法获取上传凭证"; + if (err.message) { + errTip = err.message; + } + break; + default: + errTip = err.message + err.details; + break; + } + if (_Error_Handler) { + _Error_Handler(up, err, errTip); + } + } + up.refresh(); // Reposition Flash/Silverlight + }; + })(_Error_Handler) + ); + + logger.debug("bind Error event"); + + // bind 'FileUploaded' event + // intercept the complete of upload + // - get downtoken from downtoken_url if bucket is private + // - invoke mkfile api to compose chunks if upload strategy is chunk upload + uploader.bind( + "FileUploaded", + (function (_FileUploaded_Handler) { + return function (up, file, info) { + logger.debug("FileUploaded event activated"); + logger.debug("file: ", file); + logger.debug("info: ", info); + if (uploadConfig.saveType == "s3") { + } + var last_step = function (up, file, info) { + if (_FileUploaded_Handler) { + _FileUploaded_Handler(up, file, info); + } + }; + if ( + uploadConfig.saveType == "oss" || + uploadConfig.saveType == "upyun" + ) { + ctx = 0; + } else { + var res = that.parseJSON(info.response); + ctx = ctx ? ctx : res.ctx; + } + // if ctx is not empty + // that means the upload strategy is chunk upload + // befroe the invoke the last_step + // we need request the mkfile to compose all uploaded chunks + // else + // invalke the last_step + logger.debug("ctx: ", ctx); + last_step(up, file, info.response); + }; + })(_FileUploaded_Handler) + ); + + logger.debug("bind FileUploaded event"); + + // init uploader + uploader.init(); + + logger.debug("invoke uploader.init()"); + + logger.debug("init uploader end"); + + return uploader; + }; + } + + var Qiniu = new QiniuJsSDK(); + + global.Qiniu = Qiniu; + + global.QiniuJsSDK = QiniuJsSDK; +})(window); diff --git a/assets/public/static/js/uploader/uploader_s3.js b/assets/public/static/js/uploader/uploader_s3.js new file mode 100644 index 00000000..1899cd72 --- /dev/null +++ b/assets/public/static/js/uploader/uploader_s3.js @@ -0,0 +1,1985 @@ +/* eslint-disable */ +function getCookieByString(cookieName) { + var start = document.cookie.indexOf(cookieName + "="); + if (start == -1) return false; + start = start + cookieName.length + 1; + var end = document.cookie.indexOf(";", start); + if (end == -1) end = document.cookie.length; + return document.cookie.substring(start, end); +} +(function (global) { + /** + * Creates new cookie or removes cookie with negative expiration + * @param key The key or identifier for the store + * @param value Contents of the store + * @param exp Expiration - creation defaults to 30 days + */ + function createCookie(key, value, exp) { + var date = new Date(); + date.setTime(date.getTime() + exp * 24 * 60 * 60 * 1000); + var expires = "; expires=" + date.toGMTString(); + document.cookie = key + "=" + value + expires + "; path=/"; + } + + /** + * Returns contents of cookie + * @param key The key or identifier for the store + */ + function readCookie(key) { + var nameEQ = key + "="; + var ca = document.cookie.split(";"); + for (var i = 0, max = ca.length; i < max; i++) { + var c = ca[i]; + while (c.charAt(0) === " ") { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return c.substring(nameEQ.length, c.length); + } + } + return null; + } + + // if current browser is not support localStorage + // use cookie to make a polyfill + if (!window.localStorage) { + window.localStorage = { + setItem: function (key, value) { + createCookie(key, value, 30); + }, + getItem: function (key) { + return readCookie(key); + }, + removeItem: function (key) { + createCookie(key, "", -1); + }, + }; + } + + function QiniuJsSDK() { + var that = this; + + /** + * detect IE version + * if current browser is not IE + * it will return false + * else + * it will return version of current IE browser + * @return {Number|Boolean} IE version or false + */ + this.detectIEVersion = function () { + var v = 4, + div = document.createElement("div"), + all = div.getElementsByTagName("i"); + while ( + ((div.innerHTML = + ""), + all[0]) + ) { + v++; + } + return v > 4 ? v : false; + }; + + var logger = { + MUTE: 0, + FATA: 1, + ERROR: 2, + WARN: 3, + INFO: 4, + DEBUG: 5, + TRACE: 6, + level: 0, + }; + + function log(type, args) { + var header = "[Cloudreve-uploader][" + type + "]"; + var msg = header; + for (var i = 0; i < args.length; i++) { + if (typeof args[i] === "string") { + msg += " " + args[i]; + } else { + msg += " " + that.stringifyJSON(args[i]); + } + } + if (that.detectIEVersion()) { + // http://stackoverflow.com/questions/5538972/console-log-apply-not-working-in-ie9 + //var log = Function.prototype.bind.call(console.log, console); + //log.apply(console, args); + console.log(msg); + } else { + args.unshift(header); + console.log.apply(console, args); + } + if (document.getElementById("qiniu-js-sdk-log")) { + document.getElementById("qiniu-js-sdk-log").innerHTML += + "

" + msg + "

"; + } + } + + function makeLogFunc(code) { + var func = code.toLowerCase(); + logger[func] = function () { + // logger[func].history = logger[func].history || []; + // logger[func].history.push(arguments); + if ( + window.console && + window.console.log && + logger.level >= logger[code] + ) { + var args = Array.prototype.slice.call(arguments); + log(func, args); + } + }; + } + + for (var property in logger) { + if ( + logger.hasOwnProperty(property) && + typeof logger[property] === "number" && + !logger.hasOwnProperty(property.toLowerCase()) + ) { + makeLogFunc(property); + } + } + + var qiniuUploadUrl; + + /** + * qiniu upload urls + * 'qiniuUploadUrls' is used to change target when current url is not avaliable + * @type {Array} + */ + if (uploadConfig.saveType == "qiniu") { + if (window.location.protocol === "https:") { + qiniuUploadUrl = "https://up.qbox.me"; + } else { + qiniuUploadUrl = "http://upload.qiniu.com"; + } + var qiniuUploadUrls = [ + "http://upload.qiniu.com", + "http://up.qiniu.com", + ]; + + var qiniuUpHosts = { + http: ["http://upload.qiniu.com", "http://up.qiniu.com"], + https: ["https://up.qbox.me"], + }; + //TODO 优化写法 + } else if ( + uploadConfig.saveType == "local" || + uploadConfig.saveType == "oss" || + uploadConfig.saveType == "upyun" || + uploadConfig.saveType == "s3" || + uploadConfig.saveType == "remote" || + uploadConfig.saveType == "onedrive" + ) { + qiniuUploadUrl = uploadConfig.upUrl; + var qiniuUploadUrls = [uploadConfig.upUrl]; + var qiniuUpHosts = { + http: [uploadConfig.upUrl], + https: [uploadConfig.upUrl], + }; + } + + var changeUrlTimes = 0; + + /** + * reset upload url + * if current page protocal is https + * it will always return 'https://up.qbox.me' + * else + * it will set 'qiniuUploadUrl' value with 'qiniuUploadUrls' looply + */ + this.resetUploadUrl = function () { + var hosts = + window.location.protocol === "https:" + ? qiniuUpHosts.https + : qiniuUpHosts.http; + var i = changeUrlTimes % hosts.length; + qiniuUploadUrl = hosts[i]; + changeUrlTimes++; + logger.debug("resetUploadUrl: " + qiniuUploadUrl); + }; + + // this.resetUploadUrl(); + + /** + * is image + * @param {String} url of a file + * @return {Boolean} file is a image or not + */ + this.isImage = function (url) { + url = url.split(/[?#]/)[0]; + return /\.(png|jpg|jpeg|gif|bmp)$/i.test(url); + }; + + /** + * get file extension + * @param {String} filename + * @return {String} file extension + * @example + * input: test.txt + * output: txt + */ + this.getFileExtension = function (filename) { + var tempArr = filename.split("."); + var ext; + if ( + tempArr.length === 1 || + (tempArr[0] === "" && tempArr.length === 2) + ) { + ext = ""; + } else { + ext = tempArr.pop().toLowerCase(); //get the extension and make it lower-case + } + return ext; + }; + + /** + * encode string by utf8 + * @param {String} string to encode + * @return {String} encoded string + */ + this.utf8_encode = function (argString) { + // http://kevin.vanzonneveld.net + // + original by: Webtoolkit.info (http://www.webtoolkit.info/) + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + improved by: sowberry + // + tweaked by: Jack + // + bugfixed by: Onno Marsman + // + improved by: Yves Sucaet + // + bugfixed by: Onno Marsman + // + bugfixed by: Ulrich + // + bugfixed by: Rafal Kukawski + // + improved by: kirilloid + // + bugfixed by: kirilloid + // * example 1: this.utf8_encode('Kevin van Zonneveld'); + // * returns 1: 'Kevin van Zonneveld' + + if (argString === null || typeof argString === "undefined") { + return ""; + } + + var string = argString + ""; // .replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + var utftext = "", + start, + end, + stringl = 0; + + start = end = 0; + stringl = string.length; + for (var n = 0; n < stringl; n++) { + var c1 = string.charCodeAt(n); + var enc = null; + + if (c1 < 128) { + end++; + } else if (c1 > 127 && c1 < 2048) { + enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); + } else if ((c1 & 0xf800) ^ (0xd800 > 0)) { + enc = String.fromCharCode( + (c1 >> 12) | 224, + ((c1 >> 6) & 63) | 128, + (c1 & 63) | 128 + ); + } else { + // surrogate pairs + if ((c1 & 0xfc00) ^ (0xd800 > 0)) { + throw new RangeError( + "Unmatched trail surrogate at " + n + ); + } + var c2 = string.charCodeAt(++n); + if ((c2 & 0xfc00) ^ (0xdc00 > 0)) { + throw new RangeError( + "Unmatched lead surrogate at " + (n - 1) + ); + } + c1 = ((c1 & 0x3ff) << 10) + (c2 & 0x3ff) + 0x10000; + enc = String.fromCharCode( + (c1 >> 18) | 240, + ((c1 >> 12) & 63) | 128, + ((c1 >> 6) & 63) | 128, + (c1 & 63) | 128 + ); + } + if (enc !== null) { + if (end > start) { + utftext += string.slice(start, end); + } + utftext += enc; + start = end = n + 1; + } + } + + if (end > start) { + utftext += string.slice(start, stringl); + } + + return utftext; + }; + + this.base64_decode = function (data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Thunder.m + // + input by: Aman Gupta + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Onno Marsman + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: Brett Zamir (http://brett-zamir.me) + // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA=='); + // * returns 1: 'Kevin van Zonneveld' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + var b64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, + o2, + o3, + h1, + h2, + h3, + h4, + bits, + i = 0, + ac = 0, + dec = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data += ""; + + do { + // unpack four hexets into three octets using index points in b64 + h1 = b64.indexOf(data.charAt(i++)); + h2 = b64.indexOf(data.charAt(i++)); + h3 = b64.indexOf(data.charAt(i++)); + h4 = b64.indexOf(data.charAt(i++)); + + bits = (h1 << 18) | (h2 << 12) | (h3 << 6) | h4; + + o1 = (bits >> 16) & 0xff; + o2 = (bits >> 8) & 0xff; + o3 = bits & 0xff; + + if (h3 === 64) { + tmp_arr[ac++] = String.fromCharCode(o1); + } else if (h4 === 64) { + tmp_arr[ac++] = String.fromCharCode(o1, o2); + } else { + tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); + } + } while (i < data.length); + + dec = tmp_arr.join(""); + + return dec; + }; + + /** + * encode data by base64 + * @param {String} data to encode + * @return {String} encoded data + */ + this.base64_encode = function (data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Bayron Guevara + // + improved by: Thunder.m + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // - depends on: this.utf8_encode + // * example 1: this.base64_encode('Kevin van Zonneveld'); + // * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA==' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + var b64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, + o2, + o3, + h1, + h2, + h3, + h4, + bits, + i = 0, + ac = 0, + enc = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data = this.utf8_encode(data + ""); + + do { + // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = (o1 << 16) | (o2 << 8) | o3; + + h1 = (bits >> 18) & 0x3f; + h2 = (bits >> 12) & 0x3f; + h3 = (bits >> 6) & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = + b64.charAt(h1) + + b64.charAt(h2) + + b64.charAt(h3) + + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(""); + + switch (data.length % 3) { + case 1: + enc = enc.slice(0, -2) + "=="; + break; + case 2: + enc = enc.slice(0, -1) + "="; + break; + } + + return enc; + }; + + /** + * encode string in url by base64 + * @param {String} string in url + * @return {String} encoded string + */ + this.URLSafeBase64Encode = function (v) { + v = this.base64_encode(v); + return v.replace(/\//g, "_").replace(/\+/g, "-"); + }; + + this.URLSafeBase64Decode = function (v) { + v = v.replace(/_/g, "/").replace(/-/g, "+"); + return this.base64_decode(v); + }; + + // TODO: use mOxie + /** + * craete object used to AJAX + * @return {Object} + */ + this.createAjax = function (argument) { + var xmlhttp = {}; + if (window.XMLHttpRequest) { + xmlhttp = new XMLHttpRequest(); + } else { + xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + return xmlhttp; + }; + + // TODO: enhance IE compatibility + /** + * parse json string to javascript object + * @param {String} json string + * @return {Object} object + */ + this.parseJSON = function (data) { + // Attempt to parse using the native JSON parser first + if (window.JSON && window.JSON.parse) { + return window.JSON.parse(data); + } + + //var rx_one = /^[\],:{}\s]*$/, + // rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + // rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + // rx_four = /(?:^|:|,)(?:\s*\[)+/g, + var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + + //var json; + + var text = String(data); + rx_dangerous.lastIndex = 0; + if (rx_dangerous.test(text)) { + text = text.replace(rx_dangerous, function (a) { + return ( + "\\u" + + ("0000" + a.charCodeAt(0).toString(16)).slice(-4) + ); + }); + } + + // todo 使用一下判断,增加安全性 + //if ( + // rx_one.test( + // text + // .replace(rx_two, '@') + // .replace(rx_three, ']') + // .replace(rx_four, '') + // ) + //) { + // return eval('(' + text + ')'); + //} + + return eval("(" + text + ")"); + }; + + /** + * parse javascript object to json string + * @param {Object} object + * @return {String} json string + */ + this.stringifyJSON = function (obj) { + // Attempt to parse using the native JSON parser first + if (window.JSON && window.JSON.stringify) { + return window.JSON.stringify(obj); + } + switch (typeof obj) { + case "string": + return '"' + obj.replace(/(["\\])/g, "\\$1") + '"'; + case "array": + return "[" + obj.map(that.stringifyJSON).join(",") + "]"; + case "object": + if (obj instanceof Array) { + var strArr = []; + var len = obj.length; + for (var i = 0; i < len; i++) { + strArr.push(that.stringifyJSON(obj[i])); + } + return "[" + strArr.join(",") + "]"; + } else if (obj === null) { + return "null"; + } else { + var string = []; + for (var property in obj) { + if (obj.hasOwnProperty(property)) { + string.push( + that.stringifyJSON(property) + + ":" + + that.stringifyJSON(obj[property]) + ); + } + } + return "{" + string.join(",") + "}"; + } + break; + case "number": + return obj; + case false: + return obj; + case "boolean": + return obj; + } + }; + + /** + * trim space beside text + * @param {String} untrimed string + * @return {String} trimed string + */ + this.trim = function (text) { + return text === null ? "" : text.replace(/^\s+|\s+$/g, ""); + }; + + /** + * create a uploader by QiniuJsSDK + * @param {object} options to create a new uploader + * @return {object} uploader + */ + this.uploader = function (op) { + /********** inner function define start **********/ + + // according the different condition to reset chunk size + // and the upload strategy according with the chunk size + // when chunk size is zero will cause to direct upload + // see the statement binded on 'BeforeUpload' event + var reset_chunk_size = function () { + var ie = that.detectIEVersion(); + var BLOCK_BITS, MAX_CHUNK_SIZE, chunk_size; + // case Safari 5、Windows 7、iOS 7 set isSpecialSafari to true + var isSpecialSafari = + (moxie.core.utils.Env.browser === "Safari" && + moxie.core.utils.Env.version <= 5 && + moxie.core.utils.Env.os === "Windows" && + moxie.core.utils.Env.osVersion === "7") || + (moxie.core.utils.Env.browser === "Safari" && + moxie.core.utils.Env.os === "iOS" && + moxie.core.utils.Env.osVersion === "7"); + // case IE 9-,chunk_size is not empty and flash is included in runtimes + // set op.chunk_size to zero + //if (ie && ie < 9 && op.chunk_size && op.runtimes.indexOf('flash') >= 0) { + if ( + ie && + ie < 9 && + op.chunk_size && + op.runtimes.indexOf("flash") >= 0 + ) { + // link: http://www.plupload.com/docs/Frequently-Asked-Questions#when-to-use-chunking-and-when-not + // when plupload chunk_size setting is't null ,it cause bug in ie8/9 which runs flash runtimes (not support html5) . + op.chunk_size = 0; + } else if (isSpecialSafari) { + // win7 safari / iOS7 safari have bug when in chunk upload mode + // reset chunk_size to 0 + // disable chunk in special version safari + op.chunk_size = 0; + } else { + BLOCK_BITS = 20; + MAX_CHUNK_SIZE = 4 << BLOCK_BITS; //4M + + chunk_size = plupload.parseSize(op.chunk_size); + + // qiniu service max_chunk_size is 4m + // reset chunk_size to max_chunk_size(4m) when chunk_size > 4m + } + // if op.chunk_size set 0 will be cause to direct upload + }; + + var getHosts = function (hosts) { + var result = []; + for (var i = 0; i < hosts.length; i++) { + var host = hosts[i]; + if (host.indexOf("-H") === 0) { + result.push(host.split(" ")[2]); + } else { + result.push(host); + } + } + return result; + }; + + var getPutPolicy = function (uptoken) { + if ( + uploadConfig.saveType == "local" || + uploadConfig.saveType == "oss" || + uploadConfig.saveType == "upyun" || + uploadConfig.saveType == "s3" || + uploadConfig.saveType == "remote" + ) { + return "oss"; + } else { + var segments = uptoken.split(":"); + var ak = segments[0]; + var putPolicy = that.parseJSON( + that.URLSafeBase64Decode(segments[2]) + ); + putPolicy.ak = ak; + if (putPolicy.scope.indexOf(":") >= 0) { + putPolicy.bucket = putPolicy.scope.split(":")[0]; + putPolicy.key = putPolicy.scope.split(":")[1]; + } else { + putPolicy.bucket = putPolicy.scope; + } + return putPolicy; + } + }; + + var getUpHosts = function (uptoken) { + var putPolicy = getPutPolicy(uptoken); + // var uphosts_url = "//uc.qbox.me/v1/query?ak="+ak+"&bucket="+putPolicy.scope; + // IE 9- is not support protocal relative url + var uphosts_url = + window.location.protocol + + "//uc.qbox.me/v1/query?ak=" + + putPolicy.ak + + "&bucket=" + + putPolicy.bucket; + logger.debug("putPolicy: ", putPolicy); + logger.debug("get uphosts from: ", uphosts_url); + var ie = that.detectIEVersion(); + var ajax; + if (ie && ie <= 9) { + ajax = new moxie.XMLHttpRequest(); + moxie.core.utils.Env.swf_url = op.flash_swf_url; + } else { + ajax = that.createAjax(); + } + if (uploadConfig.saveType != "qiniu") { + qiniuUpHosts.http = [uploadConfig.upUrl]; + qiniuUpHosts.http = [uploadConfig.upUrl]; + that.resetUploadUrl(); + } else { + ajax.open("GET", uphosts_url, false); + var onreadystatechange = function () { + logger.debug("ajax.readyState: ", ajax.readyState); + if (ajax.readyState === 4) { + logger.debug("ajax.status: ", ajax.status); + if (ajax.status < 400) { + var res = that.parseJSON(ajax.responseText); + qiniuUpHosts.http = getHosts(res.http.up); + qiniuUpHosts.https = getHosts(res.https.up); + logger.debug("get new uphosts: ", qiniuUpHosts); + that.resetUploadUrl(); + } else { + logger.error( + "get uphosts error: ", + ajax.responseText + ); + } + } + }; + if (ie && ie <= 9) { + ajax.bind("readystatechange", onreadystatechange); + } else { + ajax.onreadystatechange = onreadystatechange; + } + ajax.send(); + // ajax.send(); + // if (ajax.status < 400) { + // var res = that.parseJSON(ajax.responseText); + // qiniuUpHosts.http = getHosts(res.http.up); + // qiniuUpHosts.https = getHosts(res.https.up); + // logger.debug("get new uphosts: ", qiniuUpHosts); + // that.resetUploadUrl(); + // } else { + // logger.error("get uphosts error: ", ajax.responseText); + // } + } + return; + }; + + var getUptoken = function (file) { + if (uploadConfig.saveType == "remote") { + return that.token; + } + if ( + !that.token || + (op.uptoken_url && that.tokenInfo.isExpired()) + ) { + return getNewUpToken(file); + } else { + return that.token; + } + }; + + // getNewUptoken maybe called at Init Event or BeforeUpload Event + // case Init Event, the file param of getUptken will be set a null value + // if op.uptoken has value, set uptoken with op.uptoken + // else if op.uptoken_url has value, set uptoken from op.uptoken_url + // else if op.uptoken_func has value, set uptoken by result of op.uptoken_func + var getNewUpToken = function (file, callback) { + if (op.uptoken) { + that.token = op.uptoken; + callback(); + } else if (op.uptoken_url) { + logger.debug("get uptoken from: ", that.uptoken_url); + // TODO: use mOxie + var ajax = that.createAjax(); + if (file.size === undefined) { + file.size = 0; + } + ajax.open( + "GET", + that.uptoken_url + + "?path=" + + encodeURIComponent(window.pathCache[file.id]) + + "&size=" + + file.size + + "&name=" + + encodeURIComponent(file.name) + + "&type=s3", + true + ); + ajax.setRequestHeader("If-Modified-Since", "0"); + ajax.send(); + ajax.onload = function (e) { + if (ajax.status === 200) { + var res = that.parseJSON(ajax.responseText); + if (res.code != 0) { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + message: res.msg, + code: 402, + }); + callback(); + return; + } + that.policy = res.data.policy; + that.callback = res.data.callback; + that.credential = res.data.ak; + that.key = res.data.path.replace( + "${filename}", + file.name + ); + that.sign = res.data.token; + that.x_amz_date = res.data.key_time; + } else { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + code: 402, + }); + } + callback(); + }; + + ajax.onerror = function (e) { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + code: 402, + }); + callback(); + logger.error("get uptoken error: ", ajax.responseText); + }; + } else if (op.uptoken_func) { + logger.debug("get uptoken from uptoken_func"); + that.token = op.uptoken_func(file); + logger.debug("get new uptoken: ", that.token); + callback(); + } else { + logger.error( + "one of [uptoken, uptoken_url, uptoken_func] settings in options is required!" + ); + callback(); + } + return that.token; + }; + + // get file key according with the user passed options + var getFileKey = function (up, file, func) { + // WARNING + // When you set the key in putPolicy by "scope": "bucket:key" + // You should understand the risk of override a file in the bucket + // So the code below that automatically get key from uptoken has been commented + // var putPolicy = getPutPolicy(that.token) + // if (putPolicy.key) { + // logger.debug("key is defined in putPolicy.scope: ", putPolicy.key) + // return putPolicy.key + // } + var key = "", + unique_names = false; + if (!op.save_key) { + unique_names = up.getOption && up.getOption("unique_names"); + unique_names = + unique_names || + (up.settings && up.settings.unique_names); + if (unique_names) { + var ext = that.getFileExtension(file.name); + key = ext ? file.id + "." + ext : file.id; + } else if (typeof func === "function") { + key = func(up, file); + } else { + key = file.name; + } + } + if (uploadConfig.saveType == "qiniu") { + return ""; + } + return key; + }; + + /********** inner function define end **********/ + + if (op.log_level) { + logger.level = op.log_level; + } + + if (!op.domain) { + throw "domain setting in options is required!"; + } + + if (!op.browse_button) { + throw "browse_button setting in options is required!"; + } + + if (!op.uptoken && !op.uptoken_url && !op.uptoken_func) { + throw "one of [uptoken, uptoken_url, uptoken_func] settings in options is required!"; + } + + logger.debug("init uploader start"); + + logger.debug("environment: ", moxie.core.utils.Env); + + logger.debug("userAgent: ", navigator.userAgent); + + var option = {}; + + // hold the handler from user passed options + var _Error_Handler = op.init && op.init.Error; + var _FileUploaded_Handler = op.init && op.init.FileUploaded; + + // replace the handler for intercept + op.init.Error = function () {}; + op.init.FileUploaded = function () {}; + + that.uptoken_url = op.uptoken_url; + that.token = ""; + that.key_handler = + typeof op.init.Key === "function" ? op.init.Key : ""; + this.domain = op.domain; + // TODO: ctx is global in scope of a uploader instance + // this maybe cause error + var ctx = ""; + var speedCalInfo = { + isResumeUpload: false, + resumeFilesize: 0, + startTime: "", + currentTime: "", + }; + + reset_chunk_size(); + logger.debug("invoke reset_chunk_size()"); + logger.debug("op.chunk_size: ", op.chunk_size); + + var defaultSetting = { + url: qiniuUploadUrl, + multipart_params: {}, + }; + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + defaultSetting.multipart_params.accept = + "text/plain; charset=utf-8"; + logger.debug("add accept text/plain in multipart params"); + } + + // compose options with user passed options and default setting + plupload.extend(option, op, defaultSetting); + + logger.debug("option: ", option); + + // create a new uploader with composed options + var uploader = new plupload.Uploader(option); + + logger.debug("new plupload.Uploader(option)"); + + // bind getNewUpToken to 'Init' event + uploader.bind("Init", function (up, params) { + logger.debug("Init event activated"); + // if op.get_new_uptoken is not true + // invoke getNewUptoken when uploader init + // else + // getNewUptoken everytime before a new file upload + if (!op.get_new_uptoken) { + getNewUpToken(null); + } + //getNewUpToken(null); + }); + + logger.debug("bind Init event"); + + // bind 'FilesAdded' event + // when file be added and auto_start has set value + // uploader will auto start upload the file + uploader.bind("FilesAdded", function (up, files) { + logger.debug("FilesAdded event activated"); + var auto_start = up.getOption && up.getOption("auto_start"); + auto_start = + auto_start || (up.settings && up.settings.auto_start); + logger.debug("auto_start: ", auto_start); + logger.debug("files: ", files); + + // detect is iOS + var is_ios = function () { + if (moxie.core.utils.Env.OS.toLowerCase() === "ios") { + return true; + } else { + return false; + } + }; + + // if current env os is iOS change file name to [time].[ext] + if (is_ios()) { + for (var i = 0; i < files.length; i++) { + var file = files[i]; + var ext = that.getFileExtension(file.name); + file.name = file.id + "." + ext; + } + } + + if (auto_start) { + setTimeout(function () { + up.start(); + logger.debug("invoke up.start()"); + }, 0); + // up.start(); + // plupload.each(files, function(i, file) { + // up.start(); + // logger.debug("invoke up.start()") + // logger.debug("file: ", file); + // }); + } + up.refresh(); // Reposition Flash/Silverlight + }); + + logger.debug("bind FilesAdded event"); + + // bind 'BeforeUpload' event + // intercept the process of upload + // - prepare uptoken + // - according the chunk size to make differnt upload strategy + // - resume upload with the last breakpoint of file + uploader.bind("BeforeUpload", function (up, file) { + logger.debug("BeforeUpload event activated"); + // add a key named speed for file object + file.speed = file.speed || 0; + ctx = ""; + + var directUpload = function (up, file, func) { + speedCalInfo.startTime = new Date().getTime(); + var multipart_params_obj; + if (op.save_key) { + multipart_params_obj = { + token: that.token, + }; + } else { + multipart_params_obj = { + policy: that.policy, + key: that.key, + success_action_redirect: that.callback, + "x-amz-algorithm": "AWS4-HMAC-SHA256", + "x-amz-credential": that.credential, + "x-amz-signature": that.sign, + "x-amz-date": that.x_amz_date, + "Content-Type": file.type, + }; + } + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + multipart_params_obj.accept = + "text/plain; charset=utf-8"; + logger.debug( + "add accept text/plain in multipart params" + ); + } + + logger.debug( + "directUpload multipart_params_obj: ", + multipart_params_obj + ); + + var x_vars = op.x_vars; + if (x_vars !== undefined && typeof x_vars === "object") { + for (var x_key in x_vars) { + if (x_vars.hasOwnProperty(x_key)) { + if (typeof x_vars[x_key] === "function") { + multipart_params_obj["x:" + x_key] = x_vars[ + x_key + ](up, file); + } else if (typeof x_vars[x_key] !== "object") { + multipart_params_obj["x:" + x_key] = + x_vars[x_key]; + } + } + } + } + + up.setOption({ + url: qiniuUploadUrl, + multipart: true, + chunk_size: is_android_weixin_or_qq() + ? op.max_file_size + : undefined, + multipart_params: multipart_params_obj, + headers: { + "X-Oss-Forbid-Overwrite": true, + }, + }); + }; + + // detect is weixin or qq inner browser + var is_android_weixin_or_qq = function () { + var ua = navigator.userAgent.toLowerCase(); + if ( + (ua.match(/MicroMessenger/i) || + moxie.core.utils.Env.browser === "QQBrowser" || + ua.match(/V1_AND_SQ/i)) && + moxie.core.utils.Env.OS.toLowerCase() === "android" + ) { + return true; + } else { + return false; + } + }; + + var chunk_size = up.getOption && up.getOption("chunk_size"); + chunk_size = + chunk_size || (up.settings && up.settings.chunk_size); + + logger.debug("uploader.runtime: ", uploader.runtime); + logger.debug("chunk_size: ", chunk_size); + + getNewUpToken(file, () => { + if (that.token) { + getUpHosts(that.token); + } + if ( + (uploader.runtime === "html5" || + uploader.runtime === "flash") && + chunk_size + ) { + if ( + file.size < chunk_size || + is_android_weixin_or_qq() + ) { + logger.debug( + "directUpload because file.size < chunk_size || is_android_weixin_or_qq()" + ); + // direct upload if file size is less then the chunk size + directUpload(up, file, that.key_handler); + } else { + // TODO: need a polifill to make it work in IE 9- + // ISSUE: if file.name is existed in localStorage + // but not the same file maybe cause error + var localFileInfo = localStorage.getItem(file.name); + var blockSize = chunk_size; + if (localFileInfo) { + // TODO: although only the html5 runtime will enter this statement + // but need uniform way to make convertion between string and json + localFileInfo = that.parseJSON(localFileInfo); + var now = new Date().getTime(); + var before = localFileInfo.time || 0; + var aDay = 24 * 60 * 60 * 1000; // milliseconds of one day + // if the last upload time is within one day + // will upload continuously follow the last breakpoint + // else + // will reupload entire file + if (now - before < aDay) { + if (localFileInfo.percent !== 100) { + if (file.size === localFileInfo.total) { + // TODO: if file.name and file.size is the same + // but not the same file will cause error + file.percent = + localFileInfo.percent; + file.loaded = localFileInfo.offset; + ctx = localFileInfo.ctx; + + // set speed info + speedCalInfo.isResumeUpload = true; + speedCalInfo.resumeFilesize = + localFileInfo.offset; + + // set block size + if ( + localFileInfo.offset + + blockSize > + file.size + ) { + blockSize = + file.size - + localFileInfo.offset; + } + } else { + // remove file info when file.size is conflict with file info + localStorage.removeItem(file.name); + } + } else { + // remove file info when upload percent is 100% + // avoid 499 bug + localStorage.removeItem(file.name); + } + } else { + // remove file info when last upload time is over one day + localStorage.removeItem(file.name); + } + } + speedCalInfo.startTime = new Date().getTime(); + var multipart_params_obj = {}; + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + multipart_params_obj.accept = + "text/plain; charset=utf-8"; + logger.debug( + "add accept text/plain in multipart params" + ); + } + // TODO: to support bput + // http://developer.qiniu.com/docs/v6/api/reference/up/bput.html + if (uploadConfig.saveType == "remote") { + up.setOption({ + url: qiniuUploadUrl + "chunk.php", + multipart: false, + chunk_size: chunk_size, + required_features: "chunks", + headers: { + Authorization: getUptoken(file), + }, + multipart_params: multipart_params_obj, + }); + } else { + up.setOption({ + url: qiniuUploadUrl + "/mkblk/" + blockSize, + multipart: false, + chunk_size: chunk_size, + required_features: "chunks", + headers: { + Authorization: + "UpToken " + getUptoken(file), + }, + multipart_params: multipart_params_obj, + }); + } + } + } else { + logger.debug( + "directUpload because uploader.runtime !== 'html5' || uploader.runtime !== 'flash' || !chunk_size" + ); + // direct upload if runtime is not html5 + directUpload(up, file, that.key_handler); + } + if (file.status != plupload.FAILED) { + file.status = plupload.UPLOADING; + up.trigger("UploadFile", file); + } else { + up.stop(); + } + }); + return false; + }); + + logger.debug("bind BeforeUpload event"); + + // bind 'UploadProgress' event + // calculate upload speed + uploader.bind("UploadProgress", function (up, file) { + logger.trace("UploadProgress event activated"); + speedCalInfo.currentTime = new Date().getTime(); + var timeUsed = + speedCalInfo.currentTime - speedCalInfo.startTime; // ms + var fileUploaded = file.loaded || 0; + if (speedCalInfo.isResumeUpload) { + fileUploaded = file.loaded - speedCalInfo.resumeFilesize; + } + file.speed = ((fileUploaded / timeUsed) * 1000).toFixed(0) || 0; // unit: byte/s + }); + + logger.debug("bind UploadProgress event"); + + // bind 'ChunkUploaded' event + // store the chunk upload info and set next chunk upload url + uploader.bind("ChunkUploaded", function (up, file, info) { + logger.debug("ChunkUploaded event activated"); + logger.debug("file: ", file); + logger.debug("info: ", info); + var res = that.parseJSON(info.response); + logger.debug("res: ", res); + // ctx should look like '[chunk01_ctx],[chunk02_ctx],[chunk03_ctx],...' + ctx = ctx ? ctx + "," + res.ctx : res.ctx; + var leftSize = info.total - info.offset; + var chunk_size = up.getOption && up.getOption("chunk_size"); + chunk_size = + chunk_size || (up.settings && up.settings.chunk_size); + if (leftSize < chunk_size) { + up.setOption({ + url: qiniuUploadUrl + "/mkblk/" + leftSize, + }); + if (uploadConfig.saveType == "remote") { + up.setOption({ + url: qiniuUploadUrl + "chunk.php", + }); + } + logger.debug( + "up.setOption url: ", + qiniuUploadUrl + "/mkblk/" + leftSize + ); + } + if (uploadConfig.saveType == "remote") { + up.setOption({ + headers: { + Authorization: getUptoken(file), + }, + }); + } else { + up.setOption({ + headers: { + Authorization: "UpToken " + getUptoken(file), + }, + }); + } + localStorage.setItem( + file.name, + that.stringifyJSON({ + ctx: ctx, + percent: file.percent, + total: info.total, + offset: info.offset, + time: new Date().getTime(), + }) + ); + }); + + logger.debug("bind ChunkUploaded event"); + + var retries = qiniuUploadUrls.length; + + // if error is unkown switch upload url and retry + var unknow_error_retry = function (file) { + if (retries-- > 0) { + setTimeout(function () { + that.resetUploadUrl(); + file.status = plupload.QUEUED; + uploader.stop(); + uploader.start(); + }, 0); + return true; + } else { + retries = qiniuUploadUrls.length; + return false; + } + }; + + // bind 'Error' event + // check the err.code and return the errTip + uploader.bind( + "Error", + (function (_Error_Handler) { + return function (up, err) { + logger.error("Error event activated"); + logger.error("err: ", err); + var errTip = ""; + var file = err.file; + if (file) { + switch (err.code) { + case plupload.FAILED: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.FILE_SIZE_ERROR: + var max_file_size = + up.getOption && + up.getOption("max_file_size"); + max_file_size = + max_file_size || + (up.settings && + up.settings.max_file_size); + errTip = + "文件过大,您当前用户组最多可上传" + + max_file_size + + "的文件"; + break; + case plupload.FILE_EXTENSION_ERROR: + errTip = "您当前的用户组不可上传此文件"; + break; + case plupload.HTTP_ERROR: + if (err.response === "") { + // Fix parseJSON error ,when http error is like net::ERR_ADDRESS_UNREACHABLE + errTip = + err.message || "未知网络错误。"; + if (!unknow_error_retry(file)) { + return; + } + break; + } + if (uploadConfig.saveType == "oss") { + var str = err.response; + try { + parser = new DOMParser(); + xmlDoc = parser.parseFromString( + str, + "text/xml" + ); + var errorText = xmlDoc.getElementsByTagName( + "Message" + )[0].innerHTML; + errTip = "上传失败"; + errTip = + errTip + + "(" + + err.status + + ":" + + errorText + + ")"; + } catch (e) { + errTip = err.message; + errorText = "Error"; + } + } + break; + case plupload.SECURITY_ERROR: + errTip = "安全配置错误。请联系网站管理员。"; + break; + case plupload.GENERIC_ERROR: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.IO_ERROR: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.INIT_ERROR: + errTip = "网站配置错误。请联系网站管理员。"; + uploader.destroy(); + break; + case 402: + errTip = "无法获取上传凭证"; + if (err.message) { + errTip = err.message; + } + break; + default: + errTip = err.message + err.details; + break; + } + if (_Error_Handler) { + _Error_Handler(up, err, errTip); + } + } + up.refresh(); // Reposition Flash/Silverlight + }; + })(_Error_Handler) + ); + + logger.debug("bind Error event"); + + // bind 'FileUploaded' event + // intercept the complete of upload + // - get downtoken from downtoken_url if bucket is private + // - invoke mkfile api to compose chunks if upload strategy is chunk upload + uploader.bind( + "FileUploaded", + (function (_FileUploaded_Handler) { + return function (up, file, info) { + logger.debug("FileUploaded event activated"); + logger.debug("file: ", file); + logger.debug("info: ", info); + if (uploadConfig.saveType == "s3") { + } + var last_step = function (up, file, info) { + if (op.downtoken_url) { + // if op.dowontoken_url is not empty + // need get downtoken before invoke the _FileUploaded_Handler + var ajax_downtoken = that.createAjax(); + ajax_downtoken.open( + "POST", + op.downtoken_url, + true + ); + ajax_downtoken.setRequestHeader( + "Content-type", + "application/x-www-form-urlencoded" + ); + ajax_downtoken.onreadystatechange = function () { + if (ajax_downtoken.readyState === 4) { + if ( + ajax_downtoken.status === 200 || + ajax_downtoken.status === 204 || + ajax_downtoken.status === 303 + ) { + var res_downtoken; + try { + res_downtoken = that.parseJSON( + ajax_downtoken.responseText + ); + } catch (e) { + throw "invalid json format"; + } + var info_extended = {}; + plupload.extend( + info_extended, + that.parseJSON(info), + res_downtoken + ); + if (_FileUploaded_Handler) { + _FileUploaded_Handler( + up, + file, + that.stringifyJSON( + info_extended + ) + ); + } + } else { + uploader.trigger("Error", { + status: ajax_downtoken.status, + response: + ajax_downtoken.responseText, + file: file, + code: plupload.HTTP_ERROR, + }); + } + } + }; + ajax_downtoken.send( + "key=" + + that.parseJSON(info).key + + "&domain=" + + op.domain + ); + } else if (_FileUploaded_Handler) { + _FileUploaded_Handler(up, file, info); + } + }; + if ( + uploadConfig.saveType == "oss" || + uploadConfig.saveType == "upyun" + ) { + ctx = 0; + } else { + var res = that.parseJSON(info.response); + ctx = ctx ? ctx : res.ctx; + } + // if ctx is not empty + // that means the upload strategy is chunk upload + // befroe the invoke the last_step + // we need request the mkfile to compose all uploaded chunks + // else + // invalke the last_step + logger.debug("ctx: ", ctx); + if (ctx) { + var key = ""; + logger.debug("save_key: ", op.save_key); + if (!op.save_key) { + key = getFileKey(up, file, that.key_handler); + key = key + ? "/key/" + that.URLSafeBase64Encode(key) + : ""; + } + + var fname = + "/fname/" + that.URLSafeBase64Encode(file.name); + if (uploadConfig.saveType == "remote") { + if (!op.save_key) { + key = getFileKey( + up, + file, + that.key_handler + ); + key = key + ? that.URLSafeBase64Encode(key) + : ""; + } + fname = + "" + that.URLSafeBase64Encode(file.name); + op.x_vars = { + path: file.path, + }; + } + logger.debug("op.x_vars: ", op.x_vars); + if (uploadConfig.saveType == "qiniu") { + op.x_vars = { + path: file.path, + }; + } + var x_vars = op.x_vars, + x_val = "", + x_vars_url = ""; + if ( + x_vars !== undefined && + typeof x_vars === "object" + ) { + for (var x_key in x_vars) { + if (x_vars.hasOwnProperty(x_key)) { + if ( + typeof x_vars[x_key] === "function" + ) { + x_val = that.URLSafeBase64Encode( + x_vars[x_key](up, file) + ); + } else if ( + typeof x_vars[x_key] !== "object" + ) { + x_val = that.URLSafeBase64Encode( + x_vars[x_key] + ); + } + x_vars_url += + "/x:" + x_key + "/" + x_val; + } + } + } + local_path = ""; + if ( + uploadConfig.saveType == "local" || + uploadConfig.saveType == "onedrive" + ) { + pathTmp = file.path; + if (file.path == "") { + pathTmp = "ROOTDIR"; + } + local_path = + "/path/" + + that.URLSafeBase64Encode(pathTmp); + } + if (uploadConfig.saveType == "remote") { + pathTmp = file.path; + local_path = that.URLSafeBase64Encode(pathTmp); + var url = + qiniuUploadUrl + + "mkfile.php?size=" + + file.size + + "&key=" + + key + + "&fname=" + + fname + + "&path=" + + local_path; + } else { + var url = + qiniuUploadUrl + + "/mkfile/" + + file.size + + key + + fname + + x_vars_url + + local_path; + } + var ie = that.detectIEVersion(); + var ajax; + if (ie && ie <= 9) { + ajax = new moxie.xhr.XMLHttpRequest(); + moxie.core.utils.Env.swf_url = op.flash_swf_url; + } else { + ajax = that.createAjax(); + } + ajax.open("POST", url, true); + ajax.setRequestHeader( + "Content-Type", + "text/plain;charset=UTF-8" + ); + if (uploadConfig.saveType == "remote") { + ajax.setRequestHeader( + "Authorization", + that.token + ); + } else { + ajax.setRequestHeader( + "Authorization", + "UpToken " + that.token + ); + } + var onreadystatechange = function () { + logger.debug( + "ajax.readyState: ", + ajax.readyState + ); + if (ajax.readyState === 4) { + localStorage.removeItem(file.name); + var info; + if (ajax.status === 200) { + info = ajax.responseText; + logger.debug( + "mkfile is success: ", + info + ); + last_step(up, file, info); + } else { + info = { + status: ajax.status, + response: ajax.responseText, + file: file, + code: -200, + responseHeaders: ajax.getAllResponseHeaders(), + }; + logger.debug("mkfile is error: ", info); + uploader.trigger("Error", info); + } + } + }; + if (ie && ie <= 9) { + ajax.bind( + "readystatechange", + onreadystatechange + ); + } else { + ajax.onreadystatechange = onreadystatechange; + } + ajax.send(ctx); + logger.debug("mkfile: ", url); + } else { + last_step(up, file, info.response); + } + }; + })(_FileUploaded_Handler) + ); + + logger.debug("bind FileUploaded event"); + + // init uploader + uploader.init(); + + logger.debug("invoke uploader.init()"); + + logger.debug("init uploader end"); + + return uploader; + }; + + /** + * get url by key + * @param {String} key of file + * @return {String} url of file + */ + this.getUrl = function (key) { + if (!key) { + return false; + } + key = encodeURI(key); + var domain = this.domain; + if (domain.slice(domain.length - 1) !== "/") { + domain = domain + "/"; + } + return domain + key; + }; + + /** + * invoke the imageView2 api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.imageView2 = function (op, key) { + if (!/^\d$/.test(op.mode)) { + return false; + } + + var mode = op.mode, + w = op.w || "", + h = op.h || "", + q = op.q || "", + format = op.format || ""; + + if (!w && !h) { + return false; + } + + var imageUrl = "imageView2/" + mode; + imageUrl += w ? "/w/" + w : ""; + imageUrl += h ? "/h/" + h : ""; + imageUrl += q ? "/q/" + q : ""; + imageUrl += format ? "/format/" + format : ""; + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the imageMogr2 api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.imageMogr2 = function (op, key) { + var auto_orient = op["auto-orient"] || "", + thumbnail = op.thumbnail || "", + strip = op.strip || "", + gravity = op.gravity || "", + crop = op.crop || "", + quality = op.quality || "", + rotate = op.rotate || "", + format = op.format || "", + blur = op.blur || ""; + //Todo check option + + var imageUrl = "imageMogr2"; + + imageUrl += auto_orient ? "/auto-orient" : ""; + imageUrl += thumbnail ? "/thumbnail/" + thumbnail : ""; + imageUrl += strip ? "/strip" : ""; + imageUrl += gravity ? "/gravity/" + gravity : ""; + imageUrl += quality ? "/quality/" + quality : ""; + imageUrl += crop ? "/crop/" + crop : ""; + imageUrl += rotate ? "/rotate/" + rotate : ""; + imageUrl += format ? "/format/" + format : ""; + imageUrl += blur ? "/blur/" + blur : ""; + + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the watermark api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.watermark = function (op, key) { + var mode = op.mode; + if (!mode) { + return false; + } + + var imageUrl = "watermark/" + mode; + + if (mode === 1) { + var image = op.image || ""; + if (!image) { + return false; + } + imageUrl += image + ? "/image/" + this.URLSafeBase64Encode(image) + : ""; + } else if (mode === 2) { + var text = op.text ? op.text : "", + font = op.font ? op.font : "", + fontsize = op.fontsize ? op.fontsize : "", + fill = op.fill ? op.fill : ""; + if (!text) { + return false; + } + imageUrl += text + ? "/text/" + this.URLSafeBase64Encode(text) + : ""; + imageUrl += font + ? "/font/" + this.URLSafeBase64Encode(font) + : ""; + imageUrl += fontsize ? "/fontsize/" + fontsize : ""; + imageUrl += fill + ? "/fill/" + this.URLSafeBase64Encode(fill) + : ""; + } else { + // Todo mode3 + return false; + } + + var dissolve = op.dissolve || "", + gravity = op.gravity || "", + dx = op.dx || "", + dy = op.dy || ""; + + imageUrl += dissolve ? "/dissolve/" + dissolve : ""; + imageUrl += gravity ? "/gravity/" + gravity : ""; + imageUrl += dx ? "/dx/" + dx : ""; + imageUrl += dy ? "/dy/" + dy : ""; + + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the imageInfo api of Qiniu + * @param {String} key of file + * @return {Object} image info + */ + this.imageInfo = function (key) { + if (!key) { + return false; + } + var url = this.getUrl(key) + "?imageInfo"; + var xhr = this.createAjax(); + var info; + var that = this; + xhr.open("GET", url, false); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + info = that.parseJSON(xhr.responseText); + } + }; + xhr.send(); + return info; + }; + + /** + * invoke the exif api of Qiniu + * @param {String} key of file + * @return {Object} image exif + */ + this.exif = function (key) { + if (!key) { + return false; + } + var url = this.getUrl(key) + "?exif"; + var xhr = this.createAjax(); + var info; + var that = this; + xhr.open("GET", url, false); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + info = that.parseJSON(xhr.responseText); + } + }; + xhr.send(); + return info; + }; + + /** + * invoke the exif or imageInfo api of Qiniu + * according with type param + * @param {String} ['exif'|'imageInfo']type of info + * @param {String} key of file + * @return {Object} image exif or info + */ + this.get = function (type, key) { + if (!key || !type) { + return false; + } + if (type === "exif") { + return this.exif(key); + } else if (type === "imageInfo") { + return this.imageInfo(key); + } + return false; + }; + + /** + * invoke api of Qiniu like a pipeline + * @param {Array of Object} params of a series api call + * each object in array is options of api which name is set as 'fop' property + * each api's output will be next api's input + * @param {String} key of file + * @return {String|Boolean} url of processed image + */ + this.pipeline = function (arr, key) { + var isArray = + Object.prototype.toString.call(arr) === "[object Array]"; + var option, + errOp, + imageUrl = ""; + if (isArray) { + for (var i = 0, len = arr.length; i < len; i++) { + option = arr[i]; + if (!option.fop) { + return false; + } + switch (option.fop) { + case "watermark": + imageUrl += this.watermark(option) + "|"; + break; + case "imageView2": + imageUrl += this.imageView2(option) + "|"; + break; + case "imageMogr2": + imageUrl += this.imageMogr2(option) + "|"; + break; + default: + errOp = true; + break; + } + if (errOp) { + return false; + } + } + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + var length = imageUrl.length; + if (imageUrl.slice(length - 1) === "|") { + imageUrl = imageUrl.slice(0, length - 1); + } + } + return imageUrl; + } + return false; + }; + } + + var Qiniu = new QiniuJsSDK(); + + global.Qiniu = Qiniu; + + global.QiniuJsSDK = QiniuJsSDK; +})(window); diff --git a/assets/public/static/js/uploader/uploader_upyun.js b/assets/public/static/js/uploader/uploader_upyun.js new file mode 100644 index 00000000..bef29a72 --- /dev/null +++ b/assets/public/static/js/uploader/uploader_upyun.js @@ -0,0 +1,1940 @@ +/*! + * qiniu-js-sdk v@VERSION + * + * Copyright 2015 by Qiniu + * Released under GPL V2 License. + * + * GitHub: http://github.com/qiniu/js-sdk + * + * Date: @DATE + */ +/*global plupload ,mOxie*/ +/*global ActiveXObject */ +/*exported Qiniu */ +/*exported QiniuJsSDK */ + +function getCookieByString(cookieName) { + var start = document.cookie.indexOf(cookieName + "="); + if (start == -1) return false; + start = start + cookieName.length + 1; + var end = document.cookie.indexOf(";", start); + if (end == -1) end = document.cookie.length; + return document.cookie.substring(start, end); +} +(function (global) { + /** + * Creates new cookie or removes cookie with negative expiration + * @param key The key or identifier for the store + * @param value Contents of the store + * @param exp Expiration - creation defaults to 30 days + */ + function createCookie(key, value, exp) { + var date = new Date(); + date.setTime(date.getTime() + exp * 24 * 60 * 60 * 1000); + var expires = "; expires=" + date.toGMTString(); + document.cookie = key + "=" + value + expires + "; path=/"; + } + + /** + * Returns contents of cookie + * @param key The key or identifier for the store + */ + function readCookie(key) { + var nameEQ = key + "="; + var ca = document.cookie.split(";"); + for (var i = 0, max = ca.length; i < max; i++) { + var c = ca[i]; + while (c.charAt(0) === " ") { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return c.substring(nameEQ.length, c.length); + } + } + return null; + } + + // if current browser is not support localStorage + // use cookie to make a polyfill + if (!window.localStorage) { + window.localStorage = { + setItem: function (key, value) { + createCookie(key, value, 30); + }, + getItem: function (key) { + return readCookie(key); + }, + removeItem: function (key) { + createCookie(key, "", -1); + }, + }; + } + + function QiniuJsSDK() { + var that = this; + + /** + * detect IE version + * if current browser is not IE + * it will return false + * else + * it will return version of current IE browser + * @return {Number|Boolean} IE version or false + */ + this.detectIEVersion = function () { + var v = 4, + div = document.createElement("div"), + all = div.getElementsByTagName("i"); + while ( + ((div.innerHTML = + ""), + all[0]) + ) { + v++; + } + return v > 4 ? v : false; + }; + + var logger = { + MUTE: 0, + FATA: 1, + ERROR: 2, + WARN: 3, + INFO: 4, + DEBUG: 5, + TRACE: 6, + level: 0, + }; + + function log(type, args) { + var header = "[Cloudreve-uploader][" + type + "]"; + var msg = header; + for (var i = 0; i < args.length; i++) { + if (typeof args[i] === "string") { + msg += " " + args[i]; + } else { + msg += " " + that.stringifyJSON(args[i]); + } + } + if (that.detectIEVersion()) { + // http://stackoverflow.com/questions/5538972/console-log-apply-not-working-in-ie9 + //var log = Function.prototype.bind.call(console.log, console); + //log.apply(console, args); + console.log(msg); + } else { + args.unshift(header); + console.log.apply(console, args); + } + if (document.getElementById("qiniu-js-sdk-log")) { + document.getElementById("qiniu-js-sdk-log").innerHTML += + "

" + msg + "

"; + } + } + + function makeLogFunc(code) { + var func = code.toLowerCase(); + logger[func] = function () { + // logger[func].history = logger[func].history || []; + // logger[func].history.push(arguments); + if ( + window.console && + window.console.log && + logger.level >= logger[code] + ) { + var args = Array.prototype.slice.call(arguments); + log(func, args); + } + }; + } + + for (var property in logger) { + if ( + logger.hasOwnProperty(property) && + typeof logger[property] === "number" && + !logger.hasOwnProperty(property.toLowerCase()) + ) { + makeLogFunc(property); + } + } + + var qiniuUploadUrl; + + /** + * qiniu upload urls + * 'qiniuUploadUrls' is used to change target when current url is not avaliable + * @type {Array} + */ + + qiniuUploadUrl = uploadConfig.upUrl; + var qiniuUploadUrls = [uploadConfig.upUrl]; + var qiniuUpHosts = { + http: [uploadConfig.upUrl], + https: [uploadConfig.upUrl], + }; + + var changeUrlTimes = 0; + + /** + * reset upload url + * if current page protocal is https + * it will always return 'https://up.qbox.me' + * else + * it will set 'qiniuUploadUrl' value with 'qiniuUploadUrls' looply + */ + this.resetUploadUrl = function () { + var hosts = + window.location.protocol === "https:" + ? qiniuUpHosts.https + : qiniuUpHosts.http; + var i = changeUrlTimes % hosts.length; + qiniuUploadUrl = hosts[i]; + changeUrlTimes++; + logger.debug("resetUploadUrl: " + qiniuUploadUrl); + }; + + // this.resetUploadUrl(); + + /** + * is image + * @param {String} url of a file + * @return {Boolean} file is a image or not + */ + this.isImage = function (url) { + url = url.split(/[?#]/)[0]; + return /\.(png|jpg|jpeg|gif|bmp)$/i.test(url); + }; + + /** + * get file extension + * @param {String} filename + * @return {String} file extension + * @example + * input: test.txt + * output: txt + */ + this.getFileExtension = function (filename) { + var tempArr = filename.split("."); + var ext; + if ( + tempArr.length === 1 || + (tempArr[0] === "" && tempArr.length === 2) + ) { + ext = ""; + } else { + ext = tempArr.pop().toLowerCase(); //get the extension and make it lower-case + } + return ext; + }; + + /** + * encode string by utf8 + * @param {String} string to encode + * @return {String} encoded string + */ + this.utf8_encode = function (argString) { + // http://kevin.vanzonneveld.net + // + original by: Webtoolkit.info (http://www.webtoolkit.info/) + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + improved by: sowberry + // + tweaked by: Jack + // + bugfixed by: Onno Marsman + // + improved by: Yves Sucaet + // + bugfixed by: Onno Marsman + // + bugfixed by: Ulrich + // + bugfixed by: Rafal Kukawski + // + improved by: kirilloid + // + bugfixed by: kirilloid + // * example 1: this.utf8_encode('Kevin van Zonneveld'); + // * returns 1: 'Kevin van Zonneveld' + + if (argString === null || typeof argString === "undefined") { + return ""; + } + + var string = argString + ""; // .replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + var utftext = "", + start, + end, + stringl = 0; + + start = end = 0; + stringl = string.length; + for (var n = 0; n < stringl; n++) { + var c1 = string.charCodeAt(n); + var enc = null; + + if (c1 < 128) { + end++; + } else if (c1 > 127 && c1 < 2048) { + enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); + } else if ((c1 & 0xf800) ^ (0xd800 > 0)) { + enc = String.fromCharCode( + (c1 >> 12) | 224, + ((c1 >> 6) & 63) | 128, + (c1 & 63) | 128 + ); + } else { + // surrogate pairs + if ((c1 & 0xfc00) ^ (0xd800 > 0)) { + throw new RangeError( + "Unmatched trail surrogate at " + n + ); + } + var c2 = string.charCodeAt(++n); + if ((c2 & 0xfc00) ^ (0xdc00 > 0)) { + throw new RangeError( + "Unmatched lead surrogate at " + (n - 1) + ); + } + c1 = ((c1 & 0x3ff) << 10) + (c2 & 0x3ff) + 0x10000; + enc = String.fromCharCode( + (c1 >> 18) | 240, + ((c1 >> 12) & 63) | 128, + ((c1 >> 6) & 63) | 128, + (c1 & 63) | 128 + ); + } + if (enc !== null) { + if (end > start) { + utftext += string.slice(start, end); + } + utftext += enc; + start = end = n + 1; + } + } + + if (end > start) { + utftext += string.slice(start, stringl); + } + + return utftext; + }; + + this.base64_decode = function (data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Thunder.m + // + input by: Aman Gupta + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Onno Marsman + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: Brett Zamir (http://brett-zamir.me) + // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA=='); + // * returns 1: 'Kevin van Zonneveld' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + var b64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, + o2, + o3, + h1, + h2, + h3, + h4, + bits, + i = 0, + ac = 0, + dec = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data += ""; + + do { + // unpack four hexets into three octets using index points in b64 + h1 = b64.indexOf(data.charAt(i++)); + h2 = b64.indexOf(data.charAt(i++)); + h3 = b64.indexOf(data.charAt(i++)); + h4 = b64.indexOf(data.charAt(i++)); + + bits = (h1 << 18) | (h2 << 12) | (h3 << 6) | h4; + + o1 = (bits >> 16) & 0xff; + o2 = (bits >> 8) & 0xff; + o3 = bits & 0xff; + + if (h3 === 64) { + tmp_arr[ac++] = String.fromCharCode(o1); + } else if (h4 === 64) { + tmp_arr[ac++] = String.fromCharCode(o1, o2); + } else { + tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); + } + } while (i < data.length); + + dec = tmp_arr.join(""); + + return dec; + }; + + /** + * encode data by base64 + * @param {String} data to encode + * @return {String} encoded data + */ + this.base64_encode = function (data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Bayron Guevara + // + improved by: Thunder.m + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // - depends on: this.utf8_encode + // * example 1: this.base64_encode('Kevin van Zonneveld'); + // * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA==' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + var b64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, + o2, + o3, + h1, + h2, + h3, + h4, + bits, + i = 0, + ac = 0, + enc = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data = this.utf8_encode(data + ""); + + do { + // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = (o1 << 16) | (o2 << 8) | o3; + + h1 = (bits >> 18) & 0x3f; + h2 = (bits >> 12) & 0x3f; + h3 = (bits >> 6) & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = + b64.charAt(h1) + + b64.charAt(h2) + + b64.charAt(h3) + + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(""); + + switch (data.length % 3) { + case 1: + enc = enc.slice(0, -2) + "=="; + break; + case 2: + enc = enc.slice(0, -1) + "="; + break; + } + + return enc; + }; + + /** + * encode string in url by base64 + * @param {String} string in url + * @return {String} encoded string + */ + this.URLSafeBase64Encode = function (v) { + v = this.base64_encode(v); + return v.replace(/\//g, "_").replace(/\+/g, "-"); + }; + + this.URLSafeBase64Decode = function (v) { + v = v.replace(/_/g, "/").replace(/-/g, "+"); + return this.base64_decode(v); + }; + + // TODO: use mOxie + /** + * craete object used to AJAX + * @return {Object} + */ + this.createAjax = function (argument) { + var xmlhttp = {}; + if (window.XMLHttpRequest) { + xmlhttp = new XMLHttpRequest(); + } else { + xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + return xmlhttp; + }; + + // TODO: enhance IE compatibility + /** + * parse json string to javascript object + * @param {String} json string + * @return {Object} object + */ + this.parseJSON = function (data) { + // Attempt to parse using the native JSON parser first + if (window.JSON && window.JSON.parse) { + return window.JSON.parse(data); + } + + //var rx_one = /^[\],:{}\s]*$/, + // rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + // rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + // rx_four = /(?:^|:|,)(?:\s*\[)+/g, + var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + + //var json; + + var text = String(data); + rx_dangerous.lastIndex = 0; + if (rx_dangerous.test(text)) { + text = text.replace(rx_dangerous, function (a) { + return ( + "\\u" + + ("0000" + a.charCodeAt(0).toString(16)).slice(-4) + ); + }); + } + + // todo 使用一下判断,增加安全性 + //if ( + // rx_one.test( + // text + // .replace(rx_two, '@') + // .replace(rx_three, ']') + // .replace(rx_four, '') + // ) + //) { + // return eval('(' + text + ')'); + //} + + return eval("(" + text + ")"); + }; + + /** + * parse javascript object to json string + * @param {Object} object + * @return {String} json string + */ + this.stringifyJSON = function (obj) { + // Attempt to parse using the native JSON parser first + if (window.JSON && window.JSON.stringify) { + return window.JSON.stringify(obj); + } + switch (typeof obj) { + case "string": + return '"' + obj.replace(/(["\\])/g, "\\$1") + '"'; + case "array": + return "[" + obj.map(that.stringifyJSON).join(",") + "]"; + case "object": + if (obj instanceof Array) { + var strArr = []; + var len = obj.length; + for (var i = 0; i < len; i++) { + strArr.push(that.stringifyJSON(obj[i])); + } + return "[" + strArr.join(",") + "]"; + } else if (obj === null) { + return "null"; + } else { + var string = []; + for (var property in obj) { + if (obj.hasOwnProperty(property)) { + string.push( + that.stringifyJSON(property) + + ":" + + that.stringifyJSON(obj[property]) + ); + } + } + return "{" + string.join(",") + "}"; + } + break; + case "number": + return obj; + case false: + return obj; + case "boolean": + return obj; + } + }; + + /** + * trim space beside text + * @param {String} untrimed string + * @return {String} trimed string + */ + this.trim = function (text) { + return text === null ? "" : text.replace(/^\s+|\s+$/g, ""); + }; + + /** + * create a uploader by QiniuJsSDK + * @param {object} options to create a new uploader + * @return {object} uploader + */ + this.uploader = function (op) { + /********** inner function define start **********/ + + // according the different condition to reset chunk size + // and the upload strategy according with the chunk size + // when chunk size is zero will cause to direct upload + // see the statement binded on 'BeforeUpload' event + var reset_chunk_size = function () { + var ie = that.detectIEVersion(); + var BLOCK_BITS, MAX_CHUNK_SIZE, chunk_size; + // case Safari 5、Windows 7、iOS 7 set isSpecialSafari to true + var isSpecialSafari = + (moxie.core.utils.Env.browser === "Safari" && + moxie.core.utils.Env.version <= 5 && + moxie.core.utils.Env.os === "Windows" && + moxie.core.utils.Env.osVersion === "7") || + (moxie.core.utils.Env.browser === "Safari" && + moxie.core.utils.Env.os === "iOS" && + moxie.core.utils.Env.osVersion === "7"); + // case IE 9-,chunk_size is not empty and flash is included in runtimes + // set op.chunk_size to zero + //if (ie && ie < 9 && op.chunk_size && op.runtimes.indexOf('flash') >= 0) { + if ( + ie && + ie < 9 && + op.chunk_size && + op.runtimes.indexOf("flash") >= 0 + ) { + // link: http://www.plupload.com/docs/Frequently-Asked-Questions#when-to-use-chunking-and-when-not + // when plupload chunk_size setting is't null ,it cause bug in ie8/9 which runs flash runtimes (not support html5) . + op.chunk_size = 0; + } else if (isSpecialSafari) { + // win7 safari / iOS7 safari have bug when in chunk upload mode + // reset chunk_size to 0 + // disable chunk in special version safari + op.chunk_size = 0; + } else { + BLOCK_BITS = 20; + MAX_CHUNK_SIZE = 4 << BLOCK_BITS; //4M + + chunk_size = plupload.parseSize(op.chunk_size); + + // qiniu service max_chunk_size is 4m + // reset chunk_size to max_chunk_size(4m) when chunk_size > 4m + } + // if op.chunk_size set 0 will be cause to direct upload + }; + + var getHosts = function (hosts) { + var result = []; + for (var i = 0; i < hosts.length; i++) { + var host = hosts[i]; + if (host.indexOf("-H") === 0) { + result.push(host.split(" ")[2]); + } else { + result.push(host); + } + } + return result; + }; + + var getPutPolicy = function (uptoken) { + return ""; + }; + + var getUpHosts = function (uptoken) { + var putPolicy = getPutPolicy(uptoken); + // var uphosts_url = "//uc.qbox.me/v1/query?ak="+ak+"&bucket="+putPolicy.scope; + // IE 9- is not support protocal relative url + var uphosts_url = + window.location.protocol + + "//uc.qbox.me/v1/query?ak=" + + putPolicy.ak + + "&bucket=" + + putPolicy.bucket; + logger.debug("putPolicy: ", putPolicy); + logger.debug("get uphosts from: ", uphosts_url); + var ie = that.detectIEVersion(); + var ajax; + if (ie && ie <= 9) { + ajax = new moxie.XMLHttpRequest(); + moxie.core.utils.Env.swf_url = op.flash_swf_url; + } else { + ajax = that.createAjax(); + } + if (uploadConfig.saveType != "qiniu") { + qiniuUpHosts.http = [uploadConfig.upUrl]; + qiniuUpHosts.http = [uploadConfig.upUrl]; + that.resetUploadUrl(); + } + return; + }; + + var getUptoken = function (file) { + if ( + !that.token || + (op.uptoken_url && that.tokenInfo.isExpired()) + ) { + return getNewUpToken(file); + } else { + return that.token; + } + }; + + // getNewUptoken maybe called at Init Event or BeforeUpload Event + // case Init Event, the file param of getUptken will be set a null value + // if op.uptoken has value, set uptoken with op.uptoken + // else if op.uptoken_url has value, set uptoken from op.uptoken_url + // else if op.uptoken_func has value, set uptoken by result of op.uptoken_func + var getNewUpToken = function (file, callback) { + if (op.uptoken) { + that.token = op.uptoken; + callback(); + } else if (op.uptoken_url) { + logger.debug("get uptoken from: ", that.uptoken_url); + // TODO: use mOxie + var ajax = that.createAjax(); + if (file.size === undefined) { + file.size = 0; + } + ajax.open( + "GET", + that.uptoken_url + + "?path=" + + encodeURIComponent(window.pathCache[file.id]) + + "&size=" + + file.size + + "&name=" + + encodeURIComponent(file.name) + + "&type=upyun", + true + ); + ajax.setRequestHeader("If-Modified-Since", "0"); + ajax.send(); + ajax.onload = function (e) { + if (ajax.status === 200) { + var res = that.parseJSON(ajax.responseText); + if (res.code != 0) { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + message: res.msg, + code: 402, + }); + callback(); + return; + } + that.token = res.data.token; + that.policy = res.data.policy; + } else { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + code: 402, + }); + } + callback(); + }; + + ajax.onerror = function (e) { + uploader.trigger("Error", { + status: 402, + response: ajax.responseText, + file: file, + code: 402, + }); + callback(); + logger.error("get uptoken error: ", ajax.responseText); + }; + } else if (op.uptoken_func) { + logger.debug("get uptoken from uptoken_func"); + that.token = op.uptoken_func(file); + logger.debug("get new uptoken: ", that.token); + callback(); + } else { + logger.error( + "one of [uptoken, uptoken_url, uptoken_func] settings in options is required!" + ); + callback(); + } + return that.token; + }; + + // get file key according with the user passed options + var getFileKey = function (up, file, func) { + // WARNING + // When you set the key in putPolicy by "scope": "bucket:key" + // You should understand the risk of override a file in the bucket + // So the code below that automatically get key from uptoken has been commented + // var putPolicy = getPutPolicy(that.token) + // if (putPolicy.key) { + // logger.debug("key is defined in putPolicy.scope: ", putPolicy.key) + // return putPolicy.key + // } + var key = "", + unique_names = false; + if (!op.save_key) { + unique_names = up.getOption && up.getOption("unique_names"); + unique_names = + unique_names || + (up.settings && up.settings.unique_names); + if (unique_names) { + var ext = that.getFileExtension(file.name); + key = ext ? file.id + "." + ext : file.id; + } else if (typeof func === "function") { + key = func(up, file); + } else { + key = file.name; + } + } + if (uploadConfig.saveType == "qiniu") { + return ""; + } + return key; + }; + + /********** inner function define end **********/ + + if (op.log_level) { + logger.level = op.log_level; + } + + if (!op.domain) { + throw "domain setting in options is required!"; + } + + if (!op.browse_button) { + throw "browse_button setting in options is required!"; + } + + if (!op.uptoken && !op.uptoken_url && !op.uptoken_func) { + throw "one of [uptoken, uptoken_url, uptoken_func] settings in options is required!"; + } + + logger.debug("init uploader start"); + + logger.debug("environment: ", moxie.core.utils.Env); + + logger.debug("userAgent: ", navigator.userAgent); + + var option = {}; + + // hold the handler from user passed options + var _Error_Handler = op.init && op.init.Error; + var _FileUploaded_Handler = op.init && op.init.FileUploaded; + + // replace the handler for intercept + op.init.Error = function () {}; + op.init.FileUploaded = function () {}; + + that.uptoken_url = op.uptoken_url; + that.token = ""; + that.key_handler = + typeof op.init.Key === "function" ? op.init.Key : ""; + this.domain = op.domain; + // TODO: ctx is global in scope of a uploader instance + // this maybe cause error + var ctx = ""; + var speedCalInfo = { + isResumeUpload: false, + resumeFilesize: 0, + startTime: "", + currentTime: "", + }; + + reset_chunk_size(); + logger.debug("invoke reset_chunk_size()"); + logger.debug("op.chunk_size: ", op.chunk_size); + + var defaultSetting = { + url: qiniuUploadUrl, + multipart_params: {}, + }; + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + defaultSetting.multipart_params.accept = + "text/plain; charset=utf-8"; + logger.debug("add accept text/plain in multipart params"); + } + + // compose options with user passed options and default setting + plupload.extend(option, op, defaultSetting); + + logger.debug("option: ", option); + + // create a new uploader with composed options + var uploader = new plupload.Uploader(option); + + logger.debug("new plupload.Uploader(option)"); + + // bind getNewUpToken to 'Init' event + uploader.bind("Init", function (up, params) { + logger.debug("Init event activated"); + // if op.get_new_uptoken is not true + // invoke getNewUptoken when uploader init + // else + // getNewUptoken everytime before a new file upload + if (!op.get_new_uptoken) { + getNewUpToken(null); + } + //getNewUpToken(null); + }); + + logger.debug("bind Init event"); + + // bind 'FilesAdded' event + // when file be added and auto_start has set value + // uploader will auto start upload the file + uploader.bind("FilesAdded", function (up, files) { + logger.debug("FilesAdded event activated"); + var auto_start = up.getOption && up.getOption("auto_start"); + auto_start = + auto_start || (up.settings && up.settings.auto_start); + logger.debug("auto_start: ", auto_start); + logger.debug("files: ", files); + + // detect is iOS + var is_ios = function () { + if (moxie.core.utils.Env.OS.toLowerCase() === "ios") { + return true; + } else { + return false; + } + }; + + // if current env os is iOS change file name to [time].[ext] + if (is_ios()) { + for (var i = 0; i < files.length; i++) { + var file = files[i]; + var ext = that.getFileExtension(file.name); + file.name = file.id + "." + ext; + } + } + + if (auto_start) { + setTimeout(function () { + up.start(); + logger.debug("invoke up.start()"); + }, 0); + // up.start(); + // plupload.each(files, function(i, file) { + // up.start(); + // logger.debug("invoke up.start()") + // logger.debug("file: ", file); + // }); + } + up.refresh(); // Reposition Flash/Silverlight + }); + + logger.debug("bind FilesAdded event"); + + // bind 'BeforeUpload' event + // intercept the process of upload + // - prepare uptoken + // - according the chunk size to make differnt upload strategy + // - resume upload with the last breakpoint of file + uploader.bind("BeforeUpload", function (up, file) { + logger.debug("BeforeUpload event activated"); + // add a key named speed for file object + file.speed = file.speed || 0; + ctx = ""; + + var directUpload = function (up, file, func) { + speedCalInfo.startTime = new Date().getTime(); + var multipart_params_obj; + if (op.save_key) { + multipart_params_obj = { + token: that.token, + }; + } else { + multipart_params_obj = { + authorization: that.token, + policy: that.policy, + }; + } + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + multipart_params_obj.accept = + "text/plain; charset=utf-8"; + logger.debug( + "add accept text/plain in multipart params" + ); + } + + logger.debug( + "directUpload multipart_params_obj: ", + multipart_params_obj + ); + + var x_vars = op.x_vars; + if (x_vars !== undefined && typeof x_vars === "object") { + for (var x_key in x_vars) { + if (x_vars.hasOwnProperty(x_key)) { + if (typeof x_vars[x_key] === "function") { + multipart_params_obj["x:" + x_key] = x_vars[ + x_key + ](up, file); + } else if (typeof x_vars[x_key] !== "object") { + multipart_params_obj["x:" + x_key] = + x_vars[x_key]; + } + } + } + } + up.setOption({ + url: qiniuUploadUrl, + multipart: true, + chunk_size: is_android_weixin_or_qq() + ? op.max_file_size + : undefined, + multipart_params: multipart_params_obj, + }); + }; + + // detect is weixin or qq inner browser + var is_android_weixin_or_qq = function () { + var ua = navigator.userAgent.toLowerCase(); + if ( + (ua.match(/MicroMessenger/i) || + moxie.core.utils.Env.browser === "QQBrowser" || + ua.match(/V1_AND_SQ/i)) && + moxie.core.utils.Env.OS.toLowerCase() === "android" + ) { + return true; + } else { + return false; + } + }; + + var chunk_size = up.getOption && up.getOption("chunk_size"); + chunk_size = + chunk_size || (up.settings && up.settings.chunk_size); + + logger.debug("uploader.runtime: ", uploader.runtime); + logger.debug("chunk_size: ", chunk_size); + + getNewUpToken(file, () => { + if (that.token) { + getUpHosts(that.token); + } + if ( + (uploader.runtime === "html5" || + uploader.runtime === "flash") && + chunk_size + ) { + if ( + file.size < chunk_size || + is_android_weixin_or_qq() + ) { + logger.debug( + "directUpload because file.size < chunk_size || is_android_weixin_or_qq()" + ); + // direct upload if file size is less then the chunk size + directUpload(up, file, that.key_handler); + } else { + // TODO: need a polifill to make it work in IE 9- + // ISSUE: if file.name is existed in localStorage + // but not the same file maybe cause error + var localFileInfo = localStorage.getItem(file.name); + var blockSize = chunk_size; + if (localFileInfo) { + // TODO: although only the html5 runtime will enter this statement + // but need uniform way to make convertion between string and json + localFileInfo = that.parseJSON(localFileInfo); + var now = new Date().getTime(); + var before = localFileInfo.time || 0; + var aDay = 24 * 60 * 60 * 1000; // milliseconds of one day + // if the last upload time is within one day + // will upload continuously follow the last breakpoint + // else + // will reupload entire file + if (now - before < aDay) { + if (localFileInfo.percent !== 100) { + if (file.size === localFileInfo.total) { + // TODO: if file.name and file.size is the same + // but not the same file will cause error + file.percent = + localFileInfo.percent; + file.loaded = localFileInfo.offset; + ctx = localFileInfo.ctx; + + // set speed info + speedCalInfo.isResumeUpload = true; + speedCalInfo.resumeFilesize = + localFileInfo.offset; + + // set block size + if ( + localFileInfo.offset + + blockSize > + file.size + ) { + blockSize = + file.size - + localFileInfo.offset; + } + } else { + // remove file info when file.size is conflict with file info + localStorage.removeItem(file.name); + } + } else { + // remove file info when upload percent is 100% + // avoid 499 bug + localStorage.removeItem(file.name); + } + } else { + // remove file info when last upload time is over one day + localStorage.removeItem(file.name); + } + } + speedCalInfo.startTime = new Date().getTime(); + var multipart_params_obj = {}; + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + multipart_params_obj.accept = + "text/plain; charset=utf-8"; + logger.debug( + "add accept text/plain in multipart params" + ); + } + // TODO: to support bput + // http://developer.qiniu.com/docs/v6/api/reference/up/bput.html + if (uploadConfig.saveType == "remote") { + up.setOption({ + url: qiniuUploadUrl + "chunk.php", + multipart: false, + chunk_size: chunk_size, + required_features: "chunks", + headers: { + Authorization: getUptoken(file), + }, + multipart_params: multipart_params_obj, + }); + } else { + up.setOption({ + url: qiniuUploadUrl + "/mkblk/" + blockSize, + multipart: false, + chunk_size: chunk_size, + required_features: "chunks", + headers: { + Authorization: + "UpToken " + getUptoken(file), + }, + multipart_params: multipart_params_obj, + }); + } + } + } else { + logger.debug( + "directUpload because uploader.runtime !== 'html5' || uploader.runtime !== 'flash' || !chunk_size" + ); + // direct upload if runtime is not html5 + directUpload(up, file, that.key_handler); + } + if (file.status != plupload.FAILED) { + file.status = plupload.UPLOADING; + up.trigger("UploadFile", file); + } else { + up.stop(); + } + }); + return false; + }); + + logger.debug("bind BeforeUpload event"); + + // bind 'UploadProgress' event + // calculate upload speed + uploader.bind("UploadProgress", function (up, file) { + logger.trace("UploadProgress event activated"); + speedCalInfo.currentTime = new Date().getTime(); + var timeUsed = + speedCalInfo.currentTime - speedCalInfo.startTime; // ms + var fileUploaded = file.loaded || 0; + if (speedCalInfo.isResumeUpload) { + fileUploaded = file.loaded - speedCalInfo.resumeFilesize; + } + file.speed = ((fileUploaded / timeUsed) * 1000).toFixed(0) || 0; // unit: byte/s + }); + + logger.debug("bind UploadProgress event"); + + // bind 'ChunkUploaded' event + // store the chunk upload info and set next chunk upload url + uploader.bind("ChunkUploaded", function (up, file, info) { + logger.debug("ChunkUploaded event activated"); + logger.debug("file: ", file); + logger.debug("info: ", info); + var res = that.parseJSON(info.response); + logger.debug("res: ", res); + // ctx should look like '[chunk01_ctx],[chunk02_ctx],[chunk03_ctx],...' + ctx = ctx ? ctx + "," + res.ctx : res.ctx; + var leftSize = info.total - info.offset; + var chunk_size = up.getOption && up.getOption("chunk_size"); + chunk_size = + chunk_size || (up.settings && up.settings.chunk_size); + if (leftSize < chunk_size) { + up.setOption({ + url: qiniuUploadUrl + "/mkblk/" + leftSize, + }); + if (uploadConfig.saveType == "remote") { + up.setOption({ + url: qiniuUploadUrl + "chunk.php", + }); + } + logger.debug( + "up.setOption url: ", + qiniuUploadUrl + "/mkblk/" + leftSize + ); + } + if (uploadConfig.saveType == "remote") { + up.setOption({ + headers: { + Authorization: getUptoken(file), + }, + }); + } else { + up.setOption({ + headers: { + Authorization: "UpToken " + getUptoken(file), + }, + }); + } + localStorage.setItem( + file.name, + that.stringifyJSON({ + ctx: ctx, + percent: file.percent, + total: info.total, + offset: info.offset, + time: new Date().getTime(), + }) + ); + }); + + logger.debug("bind ChunkUploaded event"); + + var retries = qiniuUploadUrls.length; + + // if error is unkown switch upload url and retry + var unknow_error_retry = function (file) { + if (retries-- > 0) { + setTimeout(function () { + that.resetUploadUrl(); + file.status = plupload.QUEUED; + uploader.stop(); + uploader.start(); + }, 0); + return true; + } else { + retries = qiniuUploadUrls.length; + return false; + } + }; + + // bind 'Error' event + // check the err.code and return the errTip + uploader.bind( + "Error", + (function (_Error_Handler) { + return function (up, err) { + logger.error("Error event activated"); + logger.error("err: ", err); + var errTip = ""; + var file = err.file; + if (file) { + switch (err.code) { + case plupload.FAILED: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.FILE_SIZE_ERROR: + var max_file_size = + up.getOption && + up.getOption("max_file_size"); + max_file_size = + max_file_size || + (up.settings && + up.settings.max_file_size); + errTip = + "文件过大,您当前用户组最多可上传" + + max_file_size + + "的文件"; + break; + case plupload.FILE_EXTENSION_ERROR: + errTip = "您当前的用户组不可上传此文件"; + break; + case plupload.HTTP_ERROR: + if (err.response === "") { + // Fix parseJSON error ,when http error is like net::ERR_ADDRESS_UNREACHABLE + errTip = + err.message || "未知网络错误。"; + if (!unknow_error_retry(file)) { + return; + } + break; + } + + var errorObj = that.parseJSON(err.response); + var errorText = errorObj.message; + switch (err.status) { + case 400: + errTip = "请求报文格式错误。"; + break; + case 401: + errTip = + "客户端认证授权失败。请重试或提交反馈。"; + break; + case 405: + errTip = + "客户端请求错误。请重试或提交反馈。"; + break; + case 579: + errTip = + "资源上传成功,但回调失败。"; + break; + case 599: + errTip = + "网络连接异常。请重试或提交反馈。"; + if (!unknow_error_retry(file)) { + return; + } + break; + case 614: + errTip = "文件已存在。"; + try { + errorObj = that.parseJSON( + errorObj.error + ); + errorText = + errorObj.error || + "file exists"; + } catch (e) { + errorText = + errorObj.error || + "file exists"; + } + break; + case 631: + errTip = "指定空间不存在。"; + break; + case 701: + errTip = + "上传数据块校验出错。请重试或提交反馈。"; + break; + default: + if (err.message) { + errTip = err.message; + } else { + errTip = "未知错误"; + } + if (!unknow_error_retry(file)) { + return; + } + break; + } + errTip = + errTip + + "(" + + err.status + + ":" + + errorText + + ")"; + + break; + case plupload.SECURITY_ERROR: + errTip = "安全配置错误。请联系网站管理员。"; + break; + case plupload.GENERIC_ERROR: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.IO_ERROR: + errTip = "上传失败。请稍后再试。"; + break; + case plupload.INIT_ERROR: + errTip = "网站配置错误。请联系网站管理员。"; + uploader.destroy(); + break; + case 402: + errTip = "无法获取上传凭证"; + if (err.message) { + errTip = err.message; + } + break; + default: + errTip = err.message + err.details; + break; + } + if (_Error_Handler) { + _Error_Handler(up, err, errTip); + } + } + up.refresh(); // Reposition Flash/Silverlight + }; + })(_Error_Handler) + ); + + logger.debug("bind Error event"); + + // bind 'FileUploaded' event + // intercept the complete of upload + // - get downtoken from downtoken_url if bucket is private + // - invoke mkfile api to compose chunks if upload strategy is chunk upload + uploader.bind( + "FileUploaded", + (function (_FileUploaded_Handler) { + return function (up, file, info) { + logger.debug("FileUploaded event activated"); + logger.debug("file: ", file); + logger.debug("info: ", info); + if (uploadConfig.saveType == "s3") { + } + var last_step = function (up, file, info) { + if (op.downtoken_url) { + // if op.dowontoken_url is not empty + // need get downtoken before invoke the _FileUploaded_Handler + var ajax_downtoken = that.createAjax(); + ajax_downtoken.open( + "POST", + op.downtoken_url, + true + ); + ajax_downtoken.setRequestHeader( + "Content-type", + "application/x-www-form-urlencoded" + ); + ajax_downtoken.onreadystatechange = function () { + if (ajax_downtoken.readyState === 4) { + if ( + ajax_downtoken.status === 200 || + ajax_downtoken.status === 204 || + ajax_downtoken.status === 303 + ) { + var res_downtoken; + try { + res_downtoken = that.parseJSON( + ajax_downtoken.responseText + ); + } catch (e) { + throw "invalid json format"; + } + var info_extended = {}; + plupload.extend( + info_extended, + that.parseJSON(info), + res_downtoken + ); + if (_FileUploaded_Handler) { + _FileUploaded_Handler( + up, + file, + that.stringifyJSON( + info_extended + ) + ); + } + } else { + uploader.trigger("Error", { + status: ajax_downtoken.status, + response: + ajax_downtoken.responseText, + file: file, + code: plupload.HTTP_ERROR, + }); + } + } + }; + ajax_downtoken.send( + "key=" + + that.parseJSON(info).key + + "&domain=" + + op.domain + ); + } else if (_FileUploaded_Handler) { + _FileUploaded_Handler(up, file, info); + } + }; + if ( + uploadConfig.saveType == "oss" || + uploadConfig.saveType == "upyun" + ) { + ctx = 0; + } else { + var res = that.parseJSON(info.response); + ctx = ctx ? ctx : res.ctx; + } + // if ctx is not empty + // that means the upload strategy is chunk upload + // befroe the invoke the last_step + // we need request the mkfile to compose all uploaded chunks + // else + // invalke the last_step + logger.debug("ctx: ", ctx); + if (ctx) { + var key = ""; + logger.debug("save_key: ", op.save_key); + if (!op.save_key) { + key = getFileKey(up, file, that.key_handler); + key = key + ? "/key/" + that.URLSafeBase64Encode(key) + : ""; + } + + var fname = + "/fname/" + that.URLSafeBase64Encode(file.name); + if (uploadConfig.saveType == "remote") { + if (!op.save_key) { + key = getFileKey( + up, + file, + that.key_handler + ); + key = key + ? that.URLSafeBase64Encode(key) + : ""; + } + fname = + "" + that.URLSafeBase64Encode(file.name); + op.x_vars = { + path: file.path, + }; + } + logger.debug("op.x_vars: ", op.x_vars); + if (uploadConfig.saveType == "qiniu") { + op.x_vars = { + path: file.path, + }; + } + var x_vars = op.x_vars, + x_val = "", + x_vars_url = ""; + if ( + x_vars !== undefined && + typeof x_vars === "object" + ) { + for (var x_key in x_vars) { + if (x_vars.hasOwnProperty(x_key)) { + if ( + typeof x_vars[x_key] === "function" + ) { + x_val = that.URLSafeBase64Encode( + x_vars[x_key](up, file) + ); + } else if ( + typeof x_vars[x_key] !== "object" + ) { + x_val = that.URLSafeBase64Encode( + x_vars[x_key] + ); + } + x_vars_url += + "/x:" + x_key + "/" + x_val; + } + } + } + local_path = ""; + if ( + uploadConfig.saveType == "local" || + uploadConfig.saveType == "onedrive" + ) { + pathTmp = file.path; + if (file.path == "") { + pathTmp = "ROOTDIR"; + } + local_path = + "/path/" + + that.URLSafeBase64Encode(pathTmp); + } + if (uploadConfig.saveType == "remote") { + pathTmp = file.path; + local_path = that.URLSafeBase64Encode(pathTmp); + var url = + qiniuUploadUrl + + "mkfile.php?size=" + + file.size + + "&key=" + + key + + "&fname=" + + fname + + "&path=" + + local_path; + } else { + var url = + qiniuUploadUrl + + "/mkfile/" + + file.size + + key + + fname + + x_vars_url + + local_path; + } + var ie = that.detectIEVersion(); + var ajax; + if (ie && ie <= 9) { + ajax = new moxie.xhr.XMLHttpRequest(); + moxie.core.utils.Env.swf_url = op.flash_swf_url; + } else { + ajax = that.createAjax(); + } + ajax.open("POST", url, true); + ajax.setRequestHeader( + "Content-Type", + "text/plain;charset=UTF-8" + ); + if (uploadConfig.saveType == "remote") { + ajax.setRequestHeader( + "Authorization", + that.token + ); + } else { + ajax.setRequestHeader( + "Authorization", + "UpToken " + that.token + ); + } + var onreadystatechange = function () { + logger.debug( + "ajax.readyState: ", + ajax.readyState + ); + if (ajax.readyState === 4) { + localStorage.removeItem(file.name); + var info; + if (ajax.status === 200) { + info = ajax.responseText; + logger.debug( + "mkfile is success: ", + info + ); + last_step(up, file, info); + } else { + info = { + status: ajax.status, + response: ajax.responseText, + file: file, + code: -200, + responseHeaders: ajax.getAllResponseHeaders(), + }; + logger.debug("mkfile is error: ", info); + uploader.trigger("Error", info); + } + } + }; + if (ie && ie <= 9) { + ajax.bind( + "readystatechange", + onreadystatechange + ); + } else { + ajax.onreadystatechange = onreadystatechange; + } + ajax.send(ctx); + logger.debug("mkfile: ", url); + } else { + last_step(up, file, info.response); + } + }; + })(_FileUploaded_Handler) + ); + + logger.debug("bind FileUploaded event"); + + // init uploader + uploader.init(); + + logger.debug("invoke uploader.init()"); + + logger.debug("init uploader end"); + + return uploader; + }; + + /** + * get url by key + * @param {String} key of file + * @return {String} url of file + */ + this.getUrl = function (key) { + if (!key) { + return false; + } + key = encodeURI(key); + var domain = this.domain; + if (domain.slice(domain.length - 1) !== "/") { + domain = domain + "/"; + } + return domain + key; + }; + + /** + * invoke the imageView2 api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.imageView2 = function (op, key) { + if (!/^\d$/.test(op.mode)) { + return false; + } + + var mode = op.mode, + w = op.w || "", + h = op.h || "", + q = op.q || "", + format = op.format || ""; + + if (!w && !h) { + return false; + } + + var imageUrl = "imageView2/" + mode; + imageUrl += w ? "/w/" + w : ""; + imageUrl += h ? "/h/" + h : ""; + imageUrl += q ? "/q/" + q : ""; + imageUrl += format ? "/format/" + format : ""; + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the imageMogr2 api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.imageMogr2 = function (op, key) { + var auto_orient = op["auto-orient"] || "", + thumbnail = op.thumbnail || "", + strip = op.strip || "", + gravity = op.gravity || "", + crop = op.crop || "", + quality = op.quality || "", + rotate = op.rotate || "", + format = op.format || "", + blur = op.blur || ""; + //Todo check option + + var imageUrl = "imageMogr2"; + + imageUrl += auto_orient ? "/auto-orient" : ""; + imageUrl += thumbnail ? "/thumbnail/" + thumbnail : ""; + imageUrl += strip ? "/strip" : ""; + imageUrl += gravity ? "/gravity/" + gravity : ""; + imageUrl += quality ? "/quality/" + quality : ""; + imageUrl += crop ? "/crop/" + crop : ""; + imageUrl += rotate ? "/rotate/" + rotate : ""; + imageUrl += format ? "/format/" + format : ""; + imageUrl += blur ? "/blur/" + blur : ""; + + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the watermark api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.watermark = function (op, key) { + var mode = op.mode; + if (!mode) { + return false; + } + + var imageUrl = "watermark/" + mode; + + if (mode === 1) { + var image = op.image || ""; + if (!image) { + return false; + } + imageUrl += image + ? "/image/" + this.URLSafeBase64Encode(image) + : ""; + } else if (mode === 2) { + var text = op.text ? op.text : "", + font = op.font ? op.font : "", + fontsize = op.fontsize ? op.fontsize : "", + fill = op.fill ? op.fill : ""; + if (!text) { + return false; + } + imageUrl += text + ? "/text/" + this.URLSafeBase64Encode(text) + : ""; + imageUrl += font + ? "/font/" + this.URLSafeBase64Encode(font) + : ""; + imageUrl += fontsize ? "/fontsize/" + fontsize : ""; + imageUrl += fill + ? "/fill/" + this.URLSafeBase64Encode(fill) + : ""; + } else { + // Todo mode3 + return false; + } + + var dissolve = op.dissolve || "", + gravity = op.gravity || "", + dx = op.dx || "", + dy = op.dy || ""; + + imageUrl += dissolve ? "/dissolve/" + dissolve : ""; + imageUrl += gravity ? "/gravity/" + gravity : ""; + imageUrl += dx ? "/dx/" + dx : ""; + imageUrl += dy ? "/dy/" + dy : ""; + + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the imageInfo api of Qiniu + * @param {String} key of file + * @return {Object} image info + */ + this.imageInfo = function (key) { + if (!key) { + return false; + } + var url = this.getUrl(key) + "?imageInfo"; + var xhr = this.createAjax(); + var info; + var that = this; + xhr.open("GET", url, false); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + info = that.parseJSON(xhr.responseText); + } + }; + xhr.send(); + return info; + }; + + /** + * invoke the exif api of Qiniu + * @param {String} key of file + * @return {Object} image exif + */ + this.exif = function (key) { + if (!key) { + return false; + } + var url = this.getUrl(key) + "?exif"; + var xhr = this.createAjax(); + var info; + var that = this; + xhr.open("GET", url, false); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + info = that.parseJSON(xhr.responseText); + } + }; + xhr.send(); + return info; + }; + + /** + * invoke the exif or imageInfo api of Qiniu + * according with type param + * @param {String} ['exif'|'imageInfo']type of info + * @param {String} key of file + * @return {Object} image exif or info + */ + this.get = function (type, key) { + if (!key || !type) { + return false; + } + if (type === "exif") { + return this.exif(key); + } else if (type === "imageInfo") { + return this.imageInfo(key); + } + return false; + }; + + /** + * invoke api of Qiniu like a pipeline + * @param {Array of Object} params of a series api call + * each object in array is options of api which name is set as 'fop' property + * each api's output will be next api's input + * @param {String} key of file + * @return {String|Boolean} url of processed image + */ + this.pipeline = function (arr, key) { + var isArray = + Object.prototype.toString.call(arr) === "[object Array]"; + var option, + errOp, + imageUrl = ""; + if (isArray) { + for (var i = 0, len = arr.length; i < len; i++) { + option = arr[i]; + if (!option.fop) { + return false; + } + switch (option.fop) { + case "watermark": + imageUrl += this.watermark(option) + "|"; + break; + case "imageView2": + imageUrl += this.imageView2(option) + "|"; + break; + case "imageMogr2": + imageUrl += this.imageMogr2(option) + "|"; + break; + default: + errOp = true; + break; + } + if (errOp) { + return false; + } + } + if (key) { + imageUrl = this.getUrl(key) + "?" + imageUrl; + var length = imageUrl.length; + if (imageUrl.slice(length - 1) === "|") { + imageUrl = imageUrl.slice(0, length - 1); + } + } + return imageUrl; + } + return false; + }; + } + + var Qiniu = new QiniuJsSDK(); + + global.Qiniu = Qiniu; + + global.QiniuJsSDK = QiniuJsSDK; +})(window); diff --git a/assets/scripts/build.js b/assets/scripts/build.js new file mode 100644 index 00000000..58c5447e --- /dev/null +++ b/assets/scripts/build.js @@ -0,0 +1,199 @@ +'use strict'; + +// Do this as the first thing so that any code reading it knows the right env. +process.env.BABEL_ENV = 'production'; +process.env.NODE_ENV = 'production'; + +// Makes the script crash on unhandled rejections instead of silently +// ignoring them. In the future, promise rejections that are not handled will +// terminate the Node.js process with a non-zero exit code. +process.on('unhandledRejection', err => { + throw err; +}); + +// Ensure environment variables are read. +require('../config/env'); + + +const path = require('path'); +const chalk = require('react-dev-utils/chalk'); +const fs = require('fs-extra'); +const webpack = require('webpack'); +const configFactory = require('../config/webpack.config'); +const paths = require('../config/paths'); +const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); +const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); +const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); +const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); +const printBuildError = require('react-dev-utils/printBuildError'); + +const measureFileSizesBeforeBuild = + FileSizeReporter.measureFileSizesBeforeBuild; +const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; +const useYarn = fs.existsSync(paths.yarnLockFile); + +// These sizes are pretty large. We'll warn for bundles exceeding them. +const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; +const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; + +const isInteractive = process.stdout.isTTY; + +// Warn and crash if required files are missing +if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { + process.exit(1); +} + +// Generate configuration +const config = configFactory('production'); + +// We require that you explicitly set browsers and do not fall back to +// browserslist defaults. +const { checkBrowsers } = require('react-dev-utils/browsersHelper'); +checkBrowsers(paths.appPath, isInteractive) + .then(() => { + // First, read the current file sizes in build directory. + // This lets us display how much they changed later. + return measureFileSizesBeforeBuild(paths.appBuild); + }) + .then(previousFileSizes => { + // Remove all content but keep the directory so that + // if you're in it, you don't end up in Trash + fs.emptyDirSync(paths.appBuild); + // Merge with the public folder + copyPublicFolder(); + // Start the webpack build + return build(previousFileSizes); + }) + .then( + ({ stats, previousFileSizes, warnings }) => { + if (warnings.length) { + console.log(chalk.yellow('Compiled with warnings.\n')); + console.log(warnings.join('\n\n')); + console.log( + '\nSearch for the ' + + chalk.underline(chalk.yellow('keywords')) + + ' to learn more about each warning.' + ); + console.log( + 'To ignore, add ' + + chalk.cyan('// eslint-disable-next-line') + + ' to the line before.\n' + ); + } else { + console.log(chalk.green('Compiled successfully.\n')); + } + + console.log('File sizes after gzip:\n'); + printFileSizesAfterBuild( + stats, + previousFileSizes, + paths.appBuild, + WARN_AFTER_BUNDLE_GZIP_SIZE, + WARN_AFTER_CHUNK_GZIP_SIZE + ); + console.log(); + + const appPackage = require(paths.appPackageJson); + const publicUrl = paths.publicUrl; + const publicPath = config.output.publicPath; + const buildFolder = path.relative(process.cwd(), paths.appBuild); + printHostingInstructions( + appPackage, + publicUrl, + publicPath, + buildFolder, + useYarn + ); + }, + err => { + const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true'; + if (tscCompileOnError) { + console.log(chalk.yellow( + 'Compiled with the following type errors (you may want to check these before deploying your app):\n' + )); + printBuildError(err); + } else { + console.log(chalk.red('Failed to compile.\n')); + printBuildError(err); + process.exit(1); + } + } + ) + .catch(err => { + if (err && err.message) { + console.log(err.message); + } + process.exit(1); + }); + +// Create the production build and print the deployment instructions. +function build(previousFileSizes) { + // We used to support resolving modules according to `NODE_PATH`. + // This now has been deprecated in favor of jsconfig/tsconfig.json + // This lets you use absolute paths in imports inside large monorepos: + if (process.env.NODE_PATH) { + console.log( + chalk.yellow( + 'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.' + ) + ); + console.log(); + } + + console.log('Creating an optimized production build...'); + + const compiler = webpack(config); + return new Promise((resolve, reject) => { + compiler.run((err, stats) => { + let messages; + if (err) { + if (!err.message) { + return reject(err); + } + messages = formatWebpackMessages({ + errors: [err.message], + warnings: [], + }); + } else { + messages = formatWebpackMessages( + stats.toJson({ all: false, warnings: true, errors: true }) + ); + } + if (messages.errors.length) { + // Only keep the first error. Others are often indicative + // of the same problem, but confuse the reader with noise. + if (messages.errors.length > 1) { + messages.errors.length = 1; + } + return reject(new Error(messages.errors.join('\n\n'))); + } + if ( + process.env.CI && + (typeof process.env.CI !== 'string' || + process.env.CI.toLowerCase() !== 'false') && + messages.warnings.length + ) { + console.log( + chalk.yellow( + '\nTreating warnings as errors because process.env.CI = true.\n' + + 'Most CI servers set it automatically.\n' + ) + ); + return reject(new Error(messages.warnings.join('\n\n'))); + } + + return resolve({ + stats, + previousFileSizes, + warnings: messages.warnings, + }); + }); + }); +} + +function copyPublicFolder() { + fs.copySync(paths.appPublic, paths.appBuild, { + dereference: true, + filter: file => file !== paths.appHtml, + }); +} diff --git a/assets/scripts/start.js b/assets/scripts/start.js new file mode 100644 index 00000000..dd89084f --- /dev/null +++ b/assets/scripts/start.js @@ -0,0 +1,147 @@ +'use strict'; + +// Do this as the first thing so that any code reading it knows the right env. +process.env.BABEL_ENV = 'development'; +process.env.NODE_ENV = 'development'; + +// Makes the script crash on unhandled rejections instead of silently +// ignoring them. In the future, promise rejections that are not handled will +// terminate the Node.js process with a non-zero exit code. +process.on('unhandledRejection', err => { + throw err; +}); + +// Ensure environment variables are read. +require('../config/env'); + + +const fs = require('fs'); +const chalk = require('react-dev-utils/chalk'); +const webpack = require('webpack'); +const WebpackDevServer = require('webpack-dev-server'); +const clearConsole = require('react-dev-utils/clearConsole'); +const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); +const { + choosePort, + createCompiler, + prepareProxy, + prepareUrls, +} = require('react-dev-utils/WebpackDevServerUtils'); +const openBrowser = require('react-dev-utils/openBrowser'); +const paths = require('../config/paths'); +const configFactory = require('../config/webpack.config'); +const createDevServerConfig = require('../config/webpackDevServer.config'); + +const useYarn = fs.existsSync(paths.yarnLockFile); +const isInteractive = process.stdout.isTTY; + +// Warn and crash if required files are missing +if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { + process.exit(1); +} + +// Tools like Cloud9 rely on this. +const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; +const HOST = process.env.HOST || '0.0.0.0'; + +if (process.env.HOST) { + console.log( + chalk.cyan( + `Attempting to bind to HOST environment variable: ${chalk.yellow( + chalk.bold(process.env.HOST) + )}` + ) + ); + console.log( + `If this was unintentional, check that you haven't mistakenly set it in your shell.` + ); + console.log( + `Learn more here: ${chalk.yellow('https://bit.ly/CRA-advanced-config')}` + ); + console.log(); +} + +// We require that you explicitly set browsers and do not fall back to +// browserslist defaults. +const { checkBrowsers } = require('react-dev-utils/browsersHelper'); +checkBrowsers(paths.appPath, isInteractive) + .then(() => { + // We attempt to use the default port but if it is busy, we offer the user to + // run on a different port. `choosePort()` Promise resolves to the next free port. + return choosePort(HOST, DEFAULT_PORT); + }) + .then(port => { + if (port == null) { + // We have not found a port. + return; + } + const config = configFactory('development'); + const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; + const appName = require(paths.appPackageJson).name; + const useTypeScript = fs.existsSync(paths.appTsConfig); + const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true'; + const urls = prepareUrls(protocol, HOST, port); + const devSocket = { + warnings: warnings => + devServer.sockWrite(devServer.sockets, 'warnings', warnings), + errors: errors => + devServer.sockWrite(devServer.sockets, 'errors', errors), + }; + // Create a webpack compiler that is configured with custom messages. + const compiler = createCompiler({ + appName, + config, + devSocket, + urls, + useYarn, + useTypeScript, + tscCompileOnError, + webpack, + }); + // Load proxy config + const proxySetting = require(paths.appPackageJson).proxy; + const proxyConfig = prepareProxy(proxySetting, paths.appPublic); + // Serve webpack assets generated by the compiler over a web server. + const serverConfig = createDevServerConfig( + proxyConfig, + urls.lanUrlForConfig + ); + const devServer = new WebpackDevServer(compiler, serverConfig); + // Launch WebpackDevServer. + devServer.listen(port, HOST, err => { + if (err) { + return console.log(err); + } + if (isInteractive) { + clearConsole(); + } + + // We used to support resolving modules according to `NODE_PATH`. + // This now has been deprecated in favor of jsconfig/tsconfig.json + // This lets you use absolute paths in imports inside large monorepos: + if (process.env.NODE_PATH) { + console.log( + chalk.yellow( + 'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.' + ) + ); + console.log(); + } + + console.log(chalk.cyan('Starting the development server...\n')); + openBrowser(urls.localUrlForBrowser); + }); + + ['SIGINT', 'SIGTERM'].forEach(function(sig) { + process.on(sig, function() { + devServer.close(); + process.exit(); + }); + }); + }) + .catch(err => { + if (err && err.message) { + console.log(err.message); + } + process.exit(1); + }); diff --git a/assets/scripts/test.js b/assets/scripts/test.js new file mode 100644 index 00000000..b57cb383 --- /dev/null +++ b/assets/scripts/test.js @@ -0,0 +1,53 @@ +'use strict'; + +// Do this as the first thing so that any code reading it knows the right env. +process.env.BABEL_ENV = 'test'; +process.env.NODE_ENV = 'test'; +process.env.PUBLIC_URL = ''; + +// Makes the script crash on unhandled rejections instead of silently +// ignoring them. In the future, promise rejections that are not handled will +// terminate the Node.js process with a non-zero exit code. +process.on('unhandledRejection', err => { + throw err; +}); + +// Ensure environment variables are read. +require('../config/env'); + + +const jest = require('jest'); +const execSync = require('child_process').execSync; +let argv = process.argv.slice(2); + +function isInGitRepository() { + try { + execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); + return true; + } catch (e) { + return false; + } +} + +function isInMercurialRepository() { + try { + execSync('hg --cwd . root', { stdio: 'ignore' }); + return true; + } catch (e) { + return false; + } +} + +// Watch unless on CI or explicitly running all tests +if ( + !process.env.CI && + argv.indexOf('--watchAll') === -1 && + argv.indexOf('--watchAll=false') === -1 +) { + // https://github.com/facebook/create-react-app/issues/5210 + const hasSourceControl = isInGitRepository() || isInMercurialRepository(); + argv.push(hasSourceControl ? '--watch' : '--watchAll'); +} + + +jest.run(argv); diff --git a/assets/src/Admin.js b/assets/src/Admin.js new file mode 100644 index 00000000..8b50bd2f --- /dev/null +++ b/assets/src/Admin.js @@ -0,0 +1,194 @@ +import React, { useEffect, useState } from "react"; +import { CssBaseline, makeStyles } from "@material-ui/core"; +import AlertBar from "./component/Common/Snackbar"; +import Dashboard from "./component/Admin/Dashboard"; +import { useHistory } from "react-router"; +import Auth from "./middleware/Auth"; +import { Route, Switch } from "react-router-dom"; +import { ThemeProvider } from "@material-ui/styles"; +import createMuiTheme from "@material-ui/core/styles/createMuiTheme"; +import { zhCN } from "@material-ui/core/locale"; + +import Index from "./component/Admin/Index"; +import SiteInformation from "./component/Admin/Setting/SiteInformation"; +import Access from "./component/Admin/Setting/Access"; +import Mail from "./component/Admin/Setting/Mail"; +import UploadDownload from "./component/Admin/Setting/UploadDownload"; +import Theme from "./component/Admin/Setting/Theme"; +import Aria2 from "./component/Admin/Setting/Aria2"; +import ImageSetting from "./component/Admin/Setting/Image"; +import Policy from "./component/Admin/Policy/Policy"; +import AddPolicy from "./component/Admin/Policy/AddPolicy"; +import EditPolicyPreload from "./component/Admin/Policy/EditPolicy"; +import Group from "./component/Admin/Group/Group"; +import GroupForm from "./component/Admin/Group/GroupForm"; +import EditGroupPreload from "./component/Admin/Group/EditGroup"; +import User from "./component/Admin/User/User"; +import UserForm from "./component/Admin/User/UserForm"; +import EditUserPreload from "./component/Admin/User/EditUser"; +import File from "./component/Admin/File/File"; +import Share from "./component/Admin/Share/Share"; +import Download from "./component/Admin/Task/Download"; +import Task from "./component/Admin/Task/Task"; +import Import from "./component/Admin/File/Import"; +import Captcha from "./component/Admin/Setting/Captcha"; + +const useStyles = makeStyles((theme) => ({ + root: { + display: "flex", + }, + content: { + flexGrow: 1, + padding: 0, + minWidth: 0, + }, + toolbar: theme.mixins.toolbar, +})); + +const theme = createMuiTheme( + { + palette: { + background: {}, + }, + }, + zhCN +); + +export default function Admin() { + const classes = useStyles(); + const history = useHistory(); + const [show, setShow] = useState(false); + + useEffect(() => { + const user = Auth.GetUser(); + if (user && user.group) { + if (user.group.id !== 1) { + history.push("/home"); + return; + } + setShow(true); + return; + } + history.push("/login"); + // eslint-disable-next-line + }, []); + + return ( + + +
+ + + {show && ( + ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )} + /> + )} +
+
+
+ ); +} diff --git a/assets/src/App.js b/assets/src/App.js new file mode 100644 index 00000000..3e10f681 --- /dev/null +++ b/assets/src/App.js @@ -0,0 +1,214 @@ +import React, { Suspense } from "react"; +import AuthRoute from "./middleware/AuthRoute"; +import Navbar from "./component/Navbar/Navbar.js"; +import useMediaQuery from "@material-ui/core/useMediaQuery"; +import AlertBar from "./component/Common/Snackbar"; +import { createMuiTheme, lighten } from "@material-ui/core/styles"; +import { useSelector } from "react-redux"; +import { Redirect, Route, Switch, useRouteMatch } from "react-router-dom"; +import Auth from "./middleware/Auth"; +import { CssBaseline, makeStyles, ThemeProvider } from "@material-ui/core"; +import { changeThemeColor } from "./utils"; +import NotFound from "./component/Share/NotFound"; +// Lazy loads +import LoginForm from "./component/Login/LoginForm"; +import FileManager from "./component/FileManager/FileManager.js"; +import VideoPreview from "./component/Viewer/Video.js"; +import SearchResult from "./component/Share/SearchResult"; +import MyShare from "./component/Share/MyShare"; +import Download from "./component/Download/Download"; +import SharePreload from "./component/Share/SharePreload"; +import DocViewer from "./component/Viewer/Doc"; +import TextViewer from "./component/Viewer/Text"; +import WebDAV from "./component/Setting/WebDAV"; +import Tasks from "./component/Setting/Tasks"; +import Profile from "./component/Setting/Profile"; +import UserSetting from "./component/Setting/UserSetting"; +import Register from "./component/Login/Register"; +import Activation from "./component/Login/Activication"; +import ResetForm from "./component/Login/ResetForm"; +import Reset from "./component/Login/Reset"; +import PageLoading from "./component/Placeholder/PageLoading"; +import CodeViewer from "./component/Viewer/Code"; +const PDFViewer = React.lazy(() => + import(/* webpackChunkName: "pdf" */ "./component/Viewer/PDF") +); + +export default function App() { + const themeConfig = useSelector((state) => state.siteConfig.theme); + const isLogin = useSelector((state) => state.viewUpdate.isLogin); + const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); + + const theme = React.useMemo(() => { + themeConfig.palette.type = prefersDarkMode ? "dark" : "light"; + const prefer = Auth.GetPreference("theme_mode"); + if (prefer) { + themeConfig.palette.type = prefer; + } + const theme = createMuiTheme({ + ...themeConfig, + palette: { + ...themeConfig.palette, + primary: { + ...themeConfig.palette.primary, + main: + themeConfig.palette.type === "dark" + ? lighten(themeConfig.palette.primary.main, 0.3) + : themeConfig.palette.primary.main, + }, + }, + }); + changeThemeColor( + themeConfig.palette.type === "dark" + ? theme.palette.background.default + : theme.palette.primary.main + ); + return theme; + }, [prefersDarkMode, themeConfig]); + + const useStyles = makeStyles((theme) => ({ + root: { + display: "flex", + }, + content: { + flexGrow: 1, + padding: theme.spacing(0), + minWidth: 0, + }, + toolbar: theme.mixins.toolbar, + })); + + const classes = useStyles(); + + const { path } = useRouteMatch(); + return ( + + +
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + }> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + }> + + + + + + + + + + + + +
+
+
+
+ ); +} diff --git a/assets/src/actions/explorer.js b/assets/src/actions/explorer.js new file mode 100644 index 00000000..32cd3fb3 --- /dev/null +++ b/assets/src/actions/explorer.js @@ -0,0 +1,240 @@ +import { isMac } from "../utils"; +import pathHelper from "../utils/page"; +import Auth from "../middleware/Auth"; +import { + changeContextMenu, + openLoadingDialog, + openMusicDialog, + showImgPreivew, + toggleSnackbar, +} from "./index"; +import { isPreviewable } from "../config"; +import { push } from "connected-react-router"; + +export const removeSelectedTargets = (fileIds) => { + return { + type: "RMOVE_SELECTED_TARGETS", + fileIds, + }; +}; + +export const addSelectedTargets = (targets) => { + return { + type: "ADD_SELECTED_TARGETS", + targets, + }; +}; + +export const setSelectedTarget = (targets) => { + return { + type: "SET_SELECTED_TARGET", + targets, + }; +}; + +export const setLastSelect = (file, index) => { + return { + type: "SET_LAST_SELECT", + file, + index, + }; +}; + +export const setShiftSelectedIds = (shiftSelectedIds) => { + return { + type: "SET_SHIFT_SELECTED_IDS", + shiftSelectedIds, + }; +}; + +export const openPreview = () => { + return (dispatch, getState) => { + const { + explorer: { selected }, + router: { + location: { pathname }, + }, + } = getState(); + const isShare = pathHelper.isSharePage(pathname); + if (isShare) { + const user = Auth.GetUser(); + if (!Auth.Check() && user && !user.group.shareDownload) { + dispatch(toggleSnackbar("top", "right", "请先登录", "warning")); + dispatch(changeContextMenu("file", false)); + return; + } + } + + dispatch(changeContextMenu("file", false)); + const previewPath = + selected[0].path === "/" + ? selected[0].path + selected[0].name + : selected[0].path + "/" + selected[0].name; + switch (isPreviewable(selected[0].name)) { + case "img": + dispatch(showImgPreivew(selected[0])); + return; + case "msDoc": + if (isShare) { + dispatch( + push( + selected[0].key + + "/doc?name=" + + encodeURIComponent(selected[0].name) + + "&share_path=" + + encodeURIComponent(previewPath) + ) + ); + return; + } + dispatch( + push( + "/doc?p=" + + encodeURIComponent(previewPath) + + "&id=" + + selected[0].id + ) + ); + return; + case "audio": + dispatch(openMusicDialog()); + return; + case "video": + if (isShare) { + dispatch( + push( + selected[0].key + + "/video?name=" + + encodeURIComponent(selected[0].name) + + "&share_path=" + + encodeURIComponent(previewPath) + ) + ); + return; + } + dispatch( + push( + "/video?p=" + + encodeURIComponent(previewPath) + + "&id=" + + selected[0].id + ) + ); + return; + case "pdf": + if (isShare) { + dispatch( + push( + selected[0].key + + "/pdf?name=" + + encodeURIComponent(selected[0].name) + + "&share_path=" + + encodeURIComponent(previewPath) + ) + ); + return; + } + dispatch( + push( + "/pdf?p=" + + encodeURIComponent(previewPath) + + "&id=" + + selected[0].id + ) + ); + return; + case "edit": + if (isShare) { + dispatch( + push( + selected[0].key + + "/text?name=" + + encodeURIComponent(selected[0].name) + + "&share_path=" + + encodeURIComponent(previewPath) + ) + ); + return; + } + dispatch( + push( + "/text?p=" + + encodeURIComponent(previewPath) + + "&id=" + + selected[0].id + ) + ); + return; + case "code": + if (isShare) { + dispatch( + push( + selected[0].key + + "/code?name=" + + encodeURIComponent(selected[0].name) + + "&share_path=" + + encodeURIComponent(previewPath) + ) + ); + return; + } + dispatch( + push( + "/code?p=" + + encodeURIComponent(previewPath) + + "&id=" + + selected[0].id + ) + ); + return; + default: + dispatch(openLoadingDialog("获取下载地址...")); + return; + } + }; +}; + +export const selectFile = (file, event, fileIndex) => { + const { ctrlKey, metaKey, shiftKey } = event; + return (dispatch, getState) => { + // 多种组合操作忽略 + if ([ctrlKey, metaKey, shiftKey].filter(Boolean).length > 1) { + return; + } + const isMacbook = isMac(); + const { explorer } = getState(); + const { selected, lastSelect, dirList, fileList } = explorer; + if ( + shiftKey && + !ctrlKey && + !metaKey && + selected.length !== 0 && + // 点击类型一样 + file.type === lastSelect.file.type + ) { + // shift 多选 + // 取消原有选择 + dispatch(removeSelectedTargets(selected.map((v) => v.id))); + // 添加新选择 + const begin = Math.min(lastSelect.index, fileIndex); + const end = Math.max(lastSelect.index, fileIndex); + const list = file.type === "dir" ? dirList : fileList; + const newShiftSelected = list.slice(begin, end + 1); + return dispatch(addSelectedTargets(newShiftSelected)); + } + dispatch(setLastSelect(file, fileIndex)); + dispatch(setShiftSelectedIds([])); + if ((ctrlKey && !isMacbook) || (metaKey && isMacbook)) { + // Ctrl/Command 单击添加/删除 + const presentIndex = selected.findIndex((value) => { + return value.id === file.id; + }); + if (presentIndex !== -1) { + return dispatch(removeSelectedTargets([file.id])); + } + return dispatch(addSelectedTargets([file])); + } + // 单选 + return dispatch(setSelectedTarget([file])); + }; +}; diff --git a/assets/src/actions/index.js b/assets/src/actions/index.js new file mode 100644 index 00000000..c01912e9 --- /dev/null +++ b/assets/src/actions/index.js @@ -0,0 +1,267 @@ +export * from "./explorer"; + +export const setNavigator = (path, navigatorLoading) => { + return { + type: "SET_NAVIGATOR", + path, + navigatorLoading, + }; +}; + +export const navigateTo = (path) => { + return (dispatch, getState) => { + const state = getState(); + const navigatorLoading = path !== state.navigator.path; + dispatch(setNavigator(path, navigatorLoading)); + }; +}; + +export const navigateUp = () => { + return (dispatch, getState) => { + const state = getState(); + const pathSplit = state.navigator.path.split("/"); + pathSplit.pop(); + const newPath = pathSplit.length === 1 ? "/" : pathSplit.join("/"); + const navigatorLoading = newPath !== state.navigator.path; + dispatch(setNavigator(newPath, navigatorLoading)); + }; +}; + +export const drawerToggleAction = (open) => { + return { + type: "DRAWER_TOGGLE", + open: open, + }; +}; + +export const dragAndDrop = (source, target) => { + return { + type: "DRAG_AND_DROP", + source: source, + target: target, + }; +}; + +export const changeViewMethod = (method) => { + return { + type: "CHANGE_VIEW_METHOD", + method: method, + }; +}; + +export const toggleDaylightMode = () => { + return { + type: "TOGGLE_DAYLIGHT_MODE", + }; +}; + +export const changeContextMenu = (type, open) => { + return { + type: "CHANGE_CONTEXT_MENU", + menuType: type, + open: open, + }; +}; + +export const setNavigatorLoadingStatus = (status) => { + return { + type: "SET_NAVIGATOR_LOADING_STATUE", + status: status, + }; +}; + +export const setNavigatorError = (status, msg) => { + return { + type: "SET_NAVIGATOR_ERROR", + status: status, + msg: msg, + }; +}; + +export const openCreateFolderDialog = () => { + return { + type: "OPEN_CREATE_FOLDER_DIALOG", + }; +}; + +export const openCreateFileDialog = () => { + return { + type: "OPEN_CREATE_FILE_DIALOG", + }; +}; + +export const setUserPopover = (anchor) => { + return { + type: "SET_USER_POPOVER", + anchor: anchor, + }; +}; + +export const setShareUserPopover = (anchor) => { + return { + type: "SET_SHARE_USER_POPOVER", + anchor: anchor, + }; +}; + +export const openRenameDialog = () => { + return { + type: "OPEN_RENAME_DIALOG", + }; +}; + +export const openResaveDialog = (key) => { + return { + type: "OPEN_RESAVE_DIALOG", + key: key, + }; +}; + +export const openMoveDialog = () => { + return { + type: "OPEN_MOVE_DIALOG", + }; +}; + +export const openRemoveDialog = () => { + return { + type: "OPEN_REMOVE_DIALOG", + }; +}; + +export const openShareDialog = () => { + return { + type: "OPEN_SHARE_DIALOG", + }; +}; + +export const applyThemes = (theme) => { + return { + type: "APPLY_THEME", + theme: theme, + }; +}; + +export const setSessionStatus = (status) => { + return { + type: "SET_SESSION_STATUS", + status: status, + }; +}; + +export const openMusicDialog = () => { + return { + type: "OPEN_MUSIC_DIALOG", + }; +}; + +export const openRemoteDownloadDialog = () => { + return { + type: "OPEN_REMOTE_DOWNLOAD_DIALOG", + }; +}; + +export const openTorrentDownloadDialog = () => { + return { + type: "OPEN_TORRENT_DOWNLOAD_DIALOG", + }; +}; + +export const openDecompressDialog = () => { + return { + type: "OPEN_DECOMPRESS_DIALOG", + }; +}; + +export const openCompressDialog = () => { + return { + type: "OPEN_COMPRESS_DIALOG", + }; +}; + +export const openGetSourceDialog = () => { + return { + type: "OPEN_GET_SOURCE_DIALOG", + }; +}; + +export const openCopyDialog = () => { + return { + type: "OPEN_COPY_DIALOG", + }; +}; + +export const openLoadingDialog = (text) => { + return { + type: "OPEN_LOADING_DIALOG", + text: text, + }; +}; + +export const closeAllModals = () => { + return { + type: "CLOSE_ALL_MODALS", + }; +}; + +export const toggleSnackbar = (vertical, horizontal, msg, color) => { + return { + type: "TOGGLE_SNACKBAR", + vertical: vertical, + horizontal: horizontal, + msg: msg, + color: color, + }; +}; + +export const enableLoadUploader = () => { + return { + type: "ENABLE_LOAD_UPLOADER", + }; +}; + +export const setModalsLoading = (status) => { + return { + type: "SET_MODALS_LOADING", + status: status, + }; +}; + +export const refreshFileList = () => { + return { + type: "REFRESH_FILE_LIST", + }; +}; + +export const searchMyFile = (keywords) => { + return { + type: "SEARCH_MY_FILE", + keywords: keywords, + }; +}; + +export const showImgPreivew = (first) => { + return { + type: "SHOW_IMG_PREIVEW", + first: first, + }; +}; + +export const refreshStorage = () => { + return { + type: "REFRESH_STORAGE", + }; +}; + +export const saveFile = () => { + return { + type: "SAVE_FILE", + }; +}; + +export const setSiteConfig = (config) => { + return { + type: "SET_SITE_CONFIG", + config: config, + }; +}; diff --git a/assets/src/component/Admin/Common/DomainInput.js b/assets/src/component/Admin/Common/DomainInput.js new file mode 100644 index 00000000..dc8e2ff0 --- /dev/null +++ b/assets/src/component/Admin/Common/DomainInput.js @@ -0,0 +1,77 @@ +import React, { useEffect, useState } from "react"; +import FormControl from "@material-ui/core/FormControl"; +import InputLabel from "@material-ui/core/InputLabel"; +import Input from "@material-ui/core/Input"; +import InputAdornment from "@material-ui/core/InputAdornment"; +import Select from "@material-ui/core/Select"; +import MenuItem from "@material-ui/core/MenuItem"; +import FormHelperText from "@material-ui/core/FormHelperText"; + +export default function DomainInput({ onChange, value, required, label }) { + const [domain, setDomain] = useState(""); + const [protocol, setProtocol] = useState("https://"); + const [error, setError] = useState(); + + useState(() => { + value = value ? value : ""; + if (value.startsWith("https://")) { + setDomain(value.replace("https://", "")); + setProtocol("https://"); + } else { + if (value !== "") { + setDomain(value.replace("http://", "")); + setProtocol("http://"); + } + } + }, [value]); + + useEffect(() => { + if (protocol === "http://" && window.location.protocol === "https:") { + setError( + "您当前站点启用了 HTTPS ,此处选择 HTTP 可能会导致无法连接。" + ); + } else { + setError(""); + } + }, [protocol]); + + return ( + + {label} + { + setDomain(e.target.value); + onChange({ + target: { + value: protocol + e.target.value, + }, + }); + }} + required={required} + startAdornment={ + + + + } + /> + {error !== "" && ( + {error} + )} + + ); +} diff --git a/assets/src/component/Admin/Common/SizeInput.js b/assets/src/component/Admin/Common/SizeInput.js new file mode 100644 index 00000000..89a54c74 --- /dev/null +++ b/assets/src/component/Admin/Common/SizeInput.js @@ -0,0 +1,128 @@ +import FormControl from "@material-ui/core/FormControl"; +import Input from "@material-ui/core/Input"; +import InputAdornment from "@material-ui/core/InputAdornment"; +import InputLabel from "@material-ui/core/InputLabel"; +import MenuItem from "@material-ui/core/MenuItem"; +import Select from "@material-ui/core/Select"; +import React, { useCallback, useState } from "react"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../actions"; + +const unitTransform = (v) => { + if (v < 1024) { + return [Math.round(v), 1]; + } + if (v < 1024 * 1024) { + return [Math.round(v / 1024), 1024]; + } + if (v < 1024 * 1024 * 1024) { + return [Math.round(v / (1024 * 1024)), 1024 * 1024]; + } + if (v < 1024 * 1024 * 1024 * 1024) { + return [Math.round(v / (1024 * 1024 * 1024)), 1024 * 1024 * 1024]; + } + return [ + Math.round(v / (1024 * 1024 * 1024 * 1024)), + 1024 * 1024 * 1024 * 1024, + ]; +}; + +export default function SizeInput({ + onChange, + min, + value, + required, + label, + max, + suffix, +}) { + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const [unit, setUnit] = useState(1); + let first = true; + + const transform = useCallback(() => { + const res = unitTransform(value); + if (first && value !== 0) { + setUnit(res[1]); + first = false; + } + return res; + }, [value]); + + return ( + + {label} + { + if (e.target.value * unit < max) { + onChange({ + target: { + value: (e.target.value * unit).toString(), + }, + }); + } else { + ToggleSnackbar( + "top", + "right", + "超出最大尺寸限制", + "warning" + ); + } + }} + required={required} + endAdornment={ + + + + } + /> + + ); +} diff --git a/assets/src/component/Admin/Dashboard.js b/assets/src/component/Admin/Dashboard.js new file mode 100644 index 00000000..dd0fd09d --- /dev/null +++ b/assets/src/component/Admin/Dashboard.js @@ -0,0 +1,455 @@ +import { withStyles } from "@material-ui/core"; +import AppBar from "@material-ui/core/AppBar"; +import Divider from "@material-ui/core/Divider"; +import Drawer from "@material-ui/core/Drawer"; +import MuiExpansionPanel from "@material-ui/core/ExpansionPanel"; +import MuiExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails"; +import MuiExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; +import IconButton from "@material-ui/core/IconButton"; +import List from "@material-ui/core/List"; +import ListItem from "@material-ui/core/ListItem"; +import ListItemIcon from "@material-ui/core/ListItemIcon"; +import ListItemText from "@material-ui/core/ListItemText"; +import { lighten, makeStyles, useTheme } from "@material-ui/core/styles"; +import Toolbar from "@material-ui/core/Toolbar"; +import Typography from "@material-ui/core/Typography"; +import { + Assignment, + Category, + CloudDownload, + Contacts, + Group, + Home, + Image, + InsertDriveFile, + Language, + ListAlt, + Mail, + Palette, + Person, + Settings, + SettingsEthernet, + Share, + Storage, +} from "@material-ui/icons"; +import ChevronLeftIcon from "@material-ui/icons/ChevronLeft"; +import ChevronRightIcon from "@material-ui/icons/ChevronRight"; +import MenuIcon from "@material-ui/icons/Menu"; +import clsx from "clsx"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { useHistory, useLocation } from "react-router"; +import { useRouteMatch } from "react-router-dom"; +import { changeSubTitle } from "../../redux/viewUpdate/action"; +import pathHelper from "../../utils/page"; +import UserAvatar from "../Navbar/UserAvatar"; + +const ExpansionPanel = withStyles({ + root: { + maxWidth: "100%", + boxShadow: "none", + "&:not(:last-child)": { + borderBottom: 0, + }, + "&:before": { + display: "none", + }, + "&$expanded": { margin: 0 }, + }, + expanded: {}, +})(MuiExpansionPanel); + +const ExpansionPanelSummary = withStyles({ + root: { + minHeight: 0, + padding: 0, + + "&$expanded": { + minHeight: 0, + }, + }, + content: { + maxWidth: "100%", + margin: 0, + display: "block", + "&$expanded": { + margin: "0", + }, + }, + expanded: {}, +})(MuiExpansionPanelSummary); + +const ExpansionPanelDetails = withStyles((theme) => ({ + root: { + display: "block", + padding: theme.spacing(0), + }, +}))(MuiExpansionPanelDetails); + +const drawerWidth = 240; + +const useStyles = makeStyles((theme) => ({ + root: { + display: "flex", + width: "100%", + }, + appBar: { + zIndex: theme.zIndex.drawer + 1, + transition: theme.transitions.create(["width", "margin"], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + }, + appBarShift: { + marginLeft: drawerWidth, + width: `calc(100% - ${drawerWidth}px)`, + transition: theme.transitions.create(["width", "margin"], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + }, + menuButton: { + marginRight: 36, + }, + hide: { + display: "none", + }, + drawer: { + width: drawerWidth, + flexShrink: 0, + whiteSpace: "nowrap", + }, + drawerOpen: { + width: drawerWidth, + transition: theme.transitions.create("width", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + }, + drawerClose: { + transition: theme.transitions.create("width", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + overflowX: "hidden", + width: theme.spacing(7) + 1, + [theme.breakpoints.up("sm")]: { + width: theme.spacing(9) + 1, + }, + }, + title: { + flexGrow: 1, + }, + toolbar: { + display: "flex", + alignItems: "center", + justifyContent: "flex-end", + padding: theme.spacing(0, 1), + ...theme.mixins.toolbar, + }, + content: { + flexGrow: 1, + padding: theme.spacing(3), + }, + sub: { + paddingLeft: 36, + color: theme.palette.text.secondary, + }, + subMenu: { + backgroundColor: theme.palette.background.default, + paddingTop: 0, + paddingBottom: 0, + }, + active: { + backgroundColor: lighten(theme.palette.primary.main, 0.8), + color: theme.palette.primary.main, + "&:hover": { + backgroundColor: lighten(theme.palette.primary.main, 0.7), + }, + }, + activeText: { + fontWeight: 500, + }, + activeIcon: { + color: theme.palette.primary.main, + }, +})); + +const items = [ + { + title: "面板首页", + icon: , + path: "home", + }, + { + title: "参数设置", + icon: , + sub: [ + { + title: "站点信息", + path: "basic", + icon: , + }, + { + title: "注册与登录", + path: "access", + icon: , + }, + { + title: "邮件", + path: "mail", + icon: , + }, + { + title: "上传与下载", + path: "upload", + icon: , + }, + { + title: "外观", + path: "theme", + icon: , + }, + { + title: "离线下载", + path: "aria2", + icon: , + }, + { + title: "图像处理", + path: "image", + icon: , + }, + { + title: "验证码", + path: "captcha", + icon: , + }, + ], + }, + { + title: "存储策略", + icon: , + path: "policy", + }, + { + title: "用户组", + icon: , + path: "group", + }, + { + title: "用户", + icon: , + path: "user", + }, + { + title: "文件", + icon: , + path: "file", + }, + { + title: "分享", + icon: , + path: "share", + }, + { + title: "持久任务", + icon: , + sub: [ + { + title: "离线下载", + path: "download", + icon: , + }, + { + title: "常规任务", + path: "task", + icon: , + }, + ], + }, +]; + +export default function Dashboard({ content }) { + const classes = useStyles(); + const theme = useTheme(); + const [open, setOpen] = useState(!pathHelper.isMobile()); + const [menuOpen, setMenuOpen] = useState(null); + const history = useHistory(); + const location = useLocation(); + + const handleDrawerOpen = () => { + setOpen(true); + }; + + const handleDrawerClose = () => { + setOpen(false); + }; + + const dispatch = useDispatch(); + const SetSubTitle = useCallback( + (title) => dispatch(changeSubTitle(title)), + [dispatch] + ); + + useEffect(() => { + SetSubTitle("仪表盘"); + }, []); + + useEffect(() => { + return () => { + SetSubTitle(); + }; + }, []); + + const { path } = useRouteMatch(); + + return ( +
+ + + + + + + 管理后台 仪表盘 + + + + + +
+ + {theme.direction === "rtl" ? ( + + ) : ( + + )} + +
+ + + {items.map((item) => { + if (item.path !== undefined) { + return ( + + history.push("/admin/" + item.path) + } + button + className={clsx({ + [classes.active]: location.pathname.startsWith( + "/admin/" + item.path + ), + })} + key={item.title} + > + + {item.icon} + + + + ); + } + return ( + { + setMenuOpen(isExpanded ? item.title : null); + }} + > + + + {item.icon} + + + + + + {item.sub.map((sub) => ( + + history.push( + "/admin/" + sub.path + ) + } + className={clsx({ + [classes.sub]: open, + [classes.active]: location.pathname.startsWith( + "/admin/" + sub.path + ), + })} + button + key={sub.title} + > + + {sub.icon} + + + + ))} + + + + ); + })} + +
+
+
+ {content(path)} +
+
+ ); +} diff --git a/assets/src/component/Admin/Dialogs/AddGroupk.js b/assets/src/component/Admin/Dialogs/AddGroupk.js new file mode 100644 index 00000000..5b42e99b --- /dev/null +++ b/assets/src/component/Admin/Dialogs/AddGroupk.js @@ -0,0 +1,246 @@ +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogContentText from "@material-ui/core/DialogContentText"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import FormControl from "@material-ui/core/FormControl"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import Input from "@material-ui/core/Input"; +import InputLabel from "@material-ui/core/InputLabel"; +import MenuItem from "@material-ui/core/MenuItem"; +import Select from "@material-ui/core/Select"; +import { makeStyles } from "@material-ui/core/styles"; +import Switch from "@material-ui/core/Switch"; +import React, { useEffect, useState } from "react"; +import API from "../../../middleware/Api"; + +const useStyles = makeStyles(() => ({ + formContainer: { + margin: "8px 0 8px 0", + }, +})); + +export default function AddGroup({ open, onClose, onSubmit }) { + const classes = useStyles(); + const [groups, setGroups] = useState([]); + const [group, setGroup] = useState({ + name: "", + group_id: 2, + time: "", + price: "", + score: "", + des: "", + highlight: false, + }); + + useEffect(() => { + if (open && groups.length === 0) { + API.get("/admin/groups") + .then((response) => { + setGroups(response.data); + }) + // eslint-disable-next-line @typescript-eslint/no-empty-function + .catch(() => {}); + } + // eslint-disable-next-line + }, [open]); + + const handleChange = (name) => (event) => { + setGroup({ + ...group, + [name]: event.target.value, + }); + }; + + const handleCheckChange = (name) => (event) => { + setGroup({ + ...group, + [name]: event.target.checked, + }); + }; + + const submit = (e) => { + e.preventDefault(); + const groupCopy = { ...group }; + groupCopy.time = parseInt(groupCopy.time) * 86400; + groupCopy.price = parseInt(groupCopy.price) * 100; + groupCopy.score = parseInt(groupCopy.score); + groupCopy.id = new Date().valueOf(); + groupCopy.des = groupCopy.des.split("\n"); + onSubmit(groupCopy); + }; + + return ( + +
+ + 添加可购用户组 + + + +
+ + + 名称 + + + + 商品展示名称 + + +
+ +
+ + + 用户组 + + + + 购买后升级的用户组 + + +
+ +
+ + + 有效期 (天) + + + + 单位购买时间的有效期 + + +
+ +
+ + + 单价 (元) + + + + 用户组的单价 + + +
+ +
+ + + 单价 (积分) + + + + 使用积分购买时的价格,填写为 0 + 表示不能使用积分购买 + + +
+ +
+ + + 商品描述 (一行一个) + + + + 购买页面展示的商品描述 + + +
+ +
+ + + } + label="突出展示" + /> + + 开启后,在商品选择页面会被突出展示 + + +
+
+
+ + + + +
+
+ ); +} diff --git a/assets/src/component/Admin/Dialogs/AddPack.js b/assets/src/component/Admin/Dialogs/AddPack.js new file mode 100644 index 00000000..2e606039 --- /dev/null +++ b/assets/src/component/Admin/Dialogs/AddPack.js @@ -0,0 +1,169 @@ +import React, { useState } from "react"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogContentText from "@material-ui/core/DialogContentText"; +import DialogActions from "@material-ui/core/DialogActions"; +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import InputLabel from "@material-ui/core/InputLabel"; +import Input from "@material-ui/core/Input"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import FormControl from "@material-ui/core/FormControl"; +import SizeInput from "../Common/SizeInput"; +import { makeStyles } from "@material-ui/core/styles"; + +const useStyles = makeStyles(() => ({ + formContainer: { + margin: "8px 0 8px 0", + }, +})); + +export default function AddPack({ open, onClose, onSubmit }) { + const classes = useStyles(); + const [pack, setPack] = useState({ + name: "", + size: "1073741824", + time: "", + price: "", + score: "", + }); + + const handleChange = (name) => (event) => { + setPack({ + ...pack, + [name]: event.target.value, + }); + }; + + const submit = (e) => { + e.preventDefault(); + const packCopy = { ...pack }; + packCopy.size = parseInt(packCopy.size); + packCopy.time = parseInt(packCopy.time) * 86400; + packCopy.price = parseInt(packCopy.price) * 100; + packCopy.score = parseInt(packCopy.score); + packCopy.id = new Date().valueOf(); + onSubmit(packCopy); + }; + + return ( + +
+ 添加容量包 + + +
+ + + 名称 + + + + 商品展示名称 + + +
+ +
+ + + + 容量包的大小 + + +
+ +
+ + + 有效期 (天) + + + + 每个容量包的有效期 + + +
+ +
+ + + 单价 (元) + + + + 容量包的单价 + + +
+ +
+ + + 单价 (积分) + + + + 使用积分购买时的价格,填写为 0 + 表示不能使用积分购买 + + +
+
+
+ + + + +
+
+ ); +} diff --git a/assets/src/component/Admin/Dialogs/AddPolicy.js b/assets/src/component/Admin/Dialogs/AddPolicy.js new file mode 100644 index 00000000..1d40e26c --- /dev/null +++ b/assets/src/component/Admin/Dialogs/AddPolicy.js @@ -0,0 +1,142 @@ +import Button from "@material-ui/core/Button"; +import Card from "@material-ui/core/Card"; +import CardActionArea from "@material-ui/core/CardActionArea"; +import CardContent from "@material-ui/core/CardContent"; +import CardMedia from "@material-ui/core/CardMedia"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import Grid from "@material-ui/core/Grid"; +import { makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import React from "react"; +import { useHistory } from "react-router"; + +const useStyles = makeStyles((theme) => ({ + cardContainer: { + display: "flex", + }, + cover: { + width: 100, + height: 60, + }, + card: {}, + content: { + flex: "1 0 auto", + }, + bg: { + backgroundColor: theme.palette.background.default, + padding: "24px 24px", + }, + dialogFooter: { + justifyContent: "space-between", + }, +})); + +const policies = [ + { + name: "本机存储", + img: "local.png", + path: "/admin/policy/add/local", + }, + { + name: "从机存储", + img: "remote.png", + path: "/admin/policy/add/remote", + }, + { + name: "七牛", + img: "qiniu.png", + path: "/admin/policy/add/qiniu", + }, + { + name: "阿里云 OSS", + img: "oss.png", + path: "/admin/policy/add/oss", + }, + { + name: "又拍云", + img: "upyun.png", + path: "/admin/policy/add/upyun", + }, + { + name: "腾讯云 COS", + img: "cos.png", + path: "/admin/policy/add/cos", + }, + { + name: "OneDrive", + img: "onedrive.png", + path: "/admin/policy/add/onedrive", + }, + { + name: "Amazon S3", + img: "s3.png", + path: "/admin/policy/add/s3", + }, +]; + +export default function AddPolicy({ open, onClose }) { + const classes = useStyles(); + + const location = useHistory(); + + return ( + + 选择存储方式 + + + {policies.map((v) => ( + + + { + location.push(v.path); + onClose(); + }} + className={classes.cardContainer} + > + + + + {v.name} + + + + + + ))} + + + + + + + + ); +} diff --git a/assets/src/component/Admin/Dialogs/AddRedeem.js b/assets/src/component/Admin/Dialogs/AddRedeem.js new file mode 100644 index 00000000..4ae350bb --- /dev/null +++ b/assets/src/component/Admin/Dialogs/AddRedeem.js @@ -0,0 +1,175 @@ +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogContentText from "@material-ui/core/DialogContentText"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import FormControl from "@material-ui/core/FormControl"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import Input from "@material-ui/core/Input"; +import InputLabel from "@material-ui/core/InputLabel"; +import MenuItem from "@material-ui/core/MenuItem"; +import Select from "@material-ui/core/Select"; +import { makeStyles } from "@material-ui/core/styles"; +import React, { useCallback, useState } from "react"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; + +const useStyles = makeStyles(() => ({ + formContainer: { + margin: "8px 0 8px 0", + }, +})); + +export default function AddRedeem({ open, onClose, products, onSuccess }) { + const classes = useStyles(); + const [input, setInput] = useState({ + num: 1, + id: 0, + time: 1, + }); + const [loading, setLoading] = useState(false); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const handleChange = (name) => (event) => { + setInput({ + ...input, + [name]: event.target.value, + }); + }; + + const submit = (e) => { + e.preventDefault(); + setLoading(true); + input.num = parseInt(input.num); + input.id = parseInt(input.id); + input.time = parseInt(input.time); + input.type = 2; + for (let i = 0; i < products.length; i++) { + if (products[i].id === input.id) { + if (products[i].group_id !== undefined) { + input.type = 1; + } else { + input.type = 0; + } + break; + } + } + + API.post("/admin/redeem", input) + .then((response) => { + onSuccess(response.data); + onClose(); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + return ( + +
+ 生成兑换码 + + +
+ + + 生成数量 + + + + 激活码批量生成数量 + + +
+ +
+ + + 对应商品 + + + +
+ +
+ + + 商品数量 + + + + 对于积分类商品,此处为积分数量,其他商品为时长倍数 + + +
+
+
+ + + + +
+
+ ); +} diff --git a/assets/src/component/Admin/Dialogs/Alert.js b/assets/src/component/Admin/Dialogs/Alert.js new file mode 100644 index 00000000..b838835f --- /dev/null +++ b/assets/src/component/Admin/Dialogs/Alert.js @@ -0,0 +1,31 @@ +import React from "react"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogContentText from "@material-ui/core/DialogContentText"; +import Typography from "@material-ui/core/Typography"; +import DialogActions from "@material-ui/core/DialogActions"; +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; + +export default function AlertDialog({ title, msg, open, onClose }) { + return ( + + {title} + + + {msg} + + + + + + + ); +} diff --git a/assets/src/component/Admin/Dialogs/CreateTheme.js b/assets/src/component/Admin/Dialogs/CreateTheme.js new file mode 100644 index 00000000..7e93ecf9 --- /dev/null +++ b/assets/src/component/Admin/Dialogs/CreateTheme.js @@ -0,0 +1,348 @@ +import AppBar from "@material-ui/core/AppBar"; +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import Fab from "@material-ui/core/Fab"; +import Grid from "@material-ui/core/Grid"; +import IconButton from "@material-ui/core/IconButton"; +import { createMuiTheme, makeStyles } from "@material-ui/core/styles"; +import TextField from "@material-ui/core/TextField"; +import Toolbar from "@material-ui/core/Toolbar"; +import Typography from "@material-ui/core/Typography"; +import { Add, Menu } from "@material-ui/icons"; +import { ThemeProvider } from "@material-ui/styles"; +import React, { useCallback, useState } from "react"; +import { CompactPicker } from "react-color"; + +const useStyles = makeStyles((theme) => ({ + picker: { + "& div": { + boxShadow: "none !important", + }, + marginTop: theme.spacing(1), + }, + "@global": { + ".compact-picker:parent ": { + boxShadow: "none !important", + }, + }, + statusBar: { + height: 24, + width: "100%", + }, + fab: { + textAlign: "right", + }, +})); + +export default function CreateTheme({ open, onClose, onSubmit }) { + const classes = useStyles(); + const [theme, setTheme] = useState({ + palette: { + primary: { + main: "#3f51b5", + contrastText: "#fff", + }, + secondary: { + main: "#d81b60", + contrastText: "#fff", + }, + }, + }); + + const subTheme = useCallback(() => { + return createMuiTheme(theme); + }, [theme]); + + return ( + + + + + + + 主色调 + + { + setTheme({ + ...theme, + palette: { + ...theme.palette, + primary: { + ...theme.palette.primary, + main: e.target.value, + }, + }, + }); + }} + fullWidth + /> +
+ { + setTheme({ + ...theme, + palette: { + ...theme.palette, + primary: { + ...theme.palette.primary, + main: c.hex, + }, + }, + }); + }} + /> +
+
+ + + 辅色调 + + { + setTheme({ + ...theme, + palette: { + ...theme.palette, + secondary: { + ...theme.palette.secondary, + main: e.target.value, + }, + }, + }); + }} + fullWidth + /> +
+ { + setTheme({ + ...theme, + palette: { + ...theme.palette, + secondary: { + ...theme.palette.secondary, + main: c.hex, + }, + }, + }); + }} + /> +
+
+ + + 主色调文字 + + { + setTheme({ + ...theme, + palette: { + ...theme.palette, + primary: { + ...theme.palette.primary, + contrastText: e.target.value, + }, + }, + }); + }} + fullWidth + /> +
+ { + setTheme({ + ...theme, + palette: { + ...theme.palette, + primary: { + ...theme.palette.primary, + contrastText: c.hex, + }, + }, + }); + }} + /> +
+
+ + + 辅色调文字 + + { + setTheme({ + ...theme, + palette: { + ...theme.palette, + secondary: { + ...theme.palette.secondary, + contrastText: e.target.value, + }, + }, + }); + }} + fullWidth + /> +
+ { + setTheme({ + ...theme, + palette: { + ...theme.palette, + secondary: { + ...theme.palette.secondary, + contrastText: c.hex, + }, + }, + }); + }} + /> +
+
+
+ + +
+ + + + + + + Color + + + +
+ +
+ + + +
+
+ + + + + + + + +
+ ); +} diff --git a/assets/src/component/Admin/Dialogs/FileFilter.js b/assets/src/component/Admin/Dialogs/FileFilter.js new file mode 100644 index 00000000..ffca7930 --- /dev/null +++ b/assets/src/component/Admin/Dialogs/FileFilter.js @@ -0,0 +1,129 @@ +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import FormControl from "@material-ui/core/FormControl"; +import InputLabel from "@material-ui/core/InputLabel"; +import MenuItem from "@material-ui/core/MenuItem"; +import Select from "@material-ui/core/Select"; +import TextField from "@material-ui/core/TextField"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; + +export default function FileFilter({ setFilter, setSearch, open, onClose }) { + const [input, setInput] = useState({ + policy_id: "all", + user_id: "", + }); + const [policies, setPolicies] = useState([]); + const [keywords, setKeywords] = useState(""); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const handleChange = (name) => (event) => { + setInput({ ...input, [name]: event.target.value }); + }; + + useEffect(() => { + API.post("/admin/policy/list", { + page: 1, + page_size: 10000, + order_by: "id asc", + conditions: {}, + }) + .then((response) => { + setPolicies(response.data.items); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }, []); + + const submit = () => { + const res = {}; + Object.keys(input).forEach((v) => { + if (input[v] !== "all" && input[v] !== "") { + res[v] = input[v]; + } + }); + setFilter(res); + if (keywords !== "") { + setSearch({ + name: keywords, + }); + } else { + setSearch({}); + } + onClose(); + }; + + return ( + + 过滤条件 + + + + 存储策略 + + + + + + + + setKeywords(e.target.value)} + id="standard-basic" + label="搜索 文件名" + /> + + + + + + + + ); +} diff --git a/assets/src/component/Admin/Dialogs/MagicVar.js b/assets/src/component/Admin/Dialogs/MagicVar.js new file mode 100644 index 00000000..a2b4a83b --- /dev/null +++ b/assets/src/component/Admin/Dialogs/MagicVar.js @@ -0,0 +1,158 @@ +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableContainer from "@material-ui/core/TableContainer"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import React from "react"; + +export default function MagicVar({ isFile, open, onClose, isSlave }) { + return ( + + + {isFile ? "文件名魔法变量" : "路径魔法变量"} + + + + + + + 魔法变量 + 描述 + 示例 + + + + + + {"{randomkey16}"} + + 16位随机字符 + N6IimT5XZP324ACK + + + + {"{randomkey8}"} + + 8位随机字符 + gWz78q30 + + + + {"{timestamp}"} + + 秒级时间戳 + 1582692933 + + + + {"{timestamp_nano}"} + + 纳秒级时间戳 + 1582692933231834600 + + {!isSlave && ( + + + {"{uid}"} + + 用户ID + 1 + + )} + {isFile && ( + + + {"{originname}"} + + 原始文件名 + MyPico.mp4 + + )} + {!isFile && !isSlave && ( + + + {"{path}"} + + 用户上传路径 + /我的文件/学习资料/ + + )} + + + {"{date}"} + + 日期 + 20060102 + + + + {"{datetime}"} + + 日期时间 + 20060102150405 + + + + {"{year}"} + + 年份 + 2006 + + + + {"{month}"} + + 月份 + 01 + + + + {"{day}"} + + + 02 + + + + {"{hour}"} + + 小时 + 15 + + + + {"{minute}"} + + 分钟 + 04 + + + + {"{second}"} + + + 05 + + +
+
+
+ + + +
+ ); +} diff --git a/assets/src/component/Admin/Dialogs/ShareFilter.js b/assets/src/component/Admin/Dialogs/ShareFilter.js new file mode 100644 index 00000000..ef9dc38a --- /dev/null +++ b/assets/src/component/Admin/Dialogs/ShareFilter.js @@ -0,0 +1,95 @@ +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import FormControl from "@material-ui/core/FormControl"; +import InputLabel from "@material-ui/core/InputLabel"; +import MenuItem from "@material-ui/core/MenuItem"; +import Select from "@material-ui/core/Select"; +import TextField from "@material-ui/core/TextField"; +import React, { useState } from "react"; + +export default function ShareFilter({ setFilter, setSearch, open, onClose }) { + const [input, setInput] = useState({ + is_dir: "all", + user_id: "", + }); + const [keywords, setKeywords] = useState(""); + + const handleChange = (name) => (event) => { + setInput({ ...input, [name]: event.target.value }); + }; + + const submit = () => { + const res = {}; + Object.keys(input).forEach((v) => { + if (input[v] !== "all" && input[v] !== "") { + res[v] = input[v]; + } + }); + setFilter(res); + if (keywords !== "") { + setSearch({ + source_name: keywords, + }); + } else { + setSearch({}); + } + onClose(); + }; + + return ( + + 过滤条件 + + + + 源文件类型 + + + + + + + + setKeywords(e.target.value)} + id="standard-basic" + label="搜索 文件名" + /> + + + + + + + + ); +} diff --git a/assets/src/component/Admin/Dialogs/UserFilter.js b/assets/src/component/Admin/Dialogs/UserFilter.js new file mode 100644 index 00000000..79e5baca --- /dev/null +++ b/assets/src/component/Admin/Dialogs/UserFilter.js @@ -0,0 +1,134 @@ +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import FormControl from "@material-ui/core/FormControl"; +import InputLabel from "@material-ui/core/InputLabel"; +import MenuItem from "@material-ui/core/MenuItem"; +import Select from "@material-ui/core/Select"; +import TextField from "@material-ui/core/TextField"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; + +export default function UserFilter({ setFilter, setSearch, open, onClose }) { + const [input, setInput] = useState({ + group_id: "all", + status: "all", + }); + const [groups, setGroups] = useState([]); + const [keywords, setKeywords] = useState(""); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const handleChange = (name) => (event) => { + setInput({ ...input, [name]: event.target.value }); + }; + + useEffect(() => { + API.get("/admin/groups") + .then((response) => { + setGroups(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }, []); + + const submit = () => { + const res = {}; + Object.keys(input).forEach((v) => { + if (input[v] !== "all") { + res[v] = input[v]; + } + }); + setFilter(res); + if (keywords !== "") { + setSearch({ + nick: keywords, + email: keywords, + }); + } else { + setSearch({}); + } + onClose(); + }; + + return ( + + 过滤条件 + + + + 用户组 + + + + + + 用户状态 + + + + + setKeywords(e.target.value)} + id="standard-basic" + label="搜索 昵称 / 用户名" + /> + + + + + + + + ); +} diff --git a/assets/src/component/Admin/File/File.js b/assets/src/component/Admin/File/File.js new file mode 100644 index 00000000..dbb5581d --- /dev/null +++ b/assets/src/component/Admin/File/File.js @@ -0,0 +1,466 @@ +import { lighten } from "@material-ui/core"; +import Badge from "@material-ui/core/Badge"; +import Button from "@material-ui/core/Button"; +import Checkbox from "@material-ui/core/Checkbox"; +import IconButton from "@material-ui/core/IconButton"; +import Link from "@material-ui/core/Link"; +import Paper from "@material-ui/core/Paper"; +import { makeStyles } from "@material-ui/core/styles"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableContainer from "@material-ui/core/TableContainer"; +import TableHead from "@material-ui/core/TableHead"; +import TablePagination from "@material-ui/core/TablePagination"; +import TableRow from "@material-ui/core/TableRow"; +import TableSortLabel from "@material-ui/core/TableSortLabel"; +import Toolbar from "@material-ui/core/Toolbar"; +import Tooltip from "@material-ui/core/Tooltip"; +import Typography from "@material-ui/core/Typography"; +import { Delete, DeleteForever, FilterList } from "@material-ui/icons"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { useHistory } from "react-router"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; +import { sizeToString } from "../../../utils"; +import FileFilter from "../Dialogs/FileFilter"; +import { formatLocalTime } from "../../../utils/datetime"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + content: { + padding: theme.spacing(2), + }, + container: { + overflowX: "auto", + }, + tableContainer: { + marginTop: 16, + }, + header: { + display: "flex", + justifyContent: "space-between", + alignItems: "flex-start", + }, + headerRight: {}, + highlight: + theme.palette.type === "light" + ? { + color: theme.palette.secondary.main, + backgroundColor: lighten(theme.palette.secondary.light, 0.85), + } + : { + color: theme.palette.text.primary, + backgroundColor: theme.palette.secondary.dark, + }, + visuallyHidden: { + border: 0, + clip: "rect(0 0 0 0)", + height: 1, + margin: -1, + overflow: "hidden", + padding: 0, + position: "absolute", + top: 20, + width: 1, + }, +})); + +export default function File() { + const classes = useStyles(); + const [files, setFiles] = useState([]); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [total, setTotal] = useState(0); + const [filter, setFilter] = useState({}); + const [users, setUsers] = useState({}); + const [search, setSearch] = useState({}); + const [orderBy, setOrderBy] = useState(["id", "desc"]); + const [filterDialog, setFilterDialog] = useState(false); + const [selected, setSelected] = useState([]); + const [loading, setLoading] = useState(false); + + const history = useHistory(); + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const loadList = () => { + API.post("/admin/file/list", { + page: page, + page_size: pageSize, + order_by: orderBy.join(" "), + conditions: filter, + searches: search, + }) + .then((response) => { + setFiles(response.data.items); + setTotal(response.data.total); + setSelected([]); + setUsers(response.data.users); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + + useEffect(() => { + loadList(); + }, [page, pageSize, orderBy, filter, search]); + + const deletePolicy = (id) => { + setLoading(true); + API.post("/admin/file/delete", { id: [id] }) + .then(() => { + loadList(); + ToggleSnackbar( + "top", + "right", + "删除任务将在后台执行", + "success" + ); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const deleteBatch = (force) => () => { + setLoading(true); + API.post("/admin/file/delete", { id: selected, force: force }) + .then(() => { + loadList(); + ToggleSnackbar( + "top", + "right", + "删除任务将在后台执行", + "success" + ); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const handleSelectAllClick = (event) => { + if (event.target.checked) { + const newSelecteds = files.map((n) => n.ID); + setSelected(newSelecteds); + return; + } + setSelected([]); + }; + + const handleClick = (event, name) => { + const selectedIndex = selected.indexOf(name); + let newSelected = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, name); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)); + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat( + selected.slice(0, selectedIndex), + selected.slice(selectedIndex + 1) + ); + } + + setSelected(newSelected); + }; + + const isSelected = (id) => selected.indexOf(id) !== -1; + + return ( +
+ setFilterDialog(false)} + setSearch={setSearch} + setFilter={setFilter} + /> +
+ +
+ + setFilterDialog(true)} + > + + + + + + +
+
+ + + {selected.length > 0 && ( + + + 已选择 {selected.length} 个对象 + + + + + + + + + + + + + )} + + + + + + 0 && + selected.length < files.length + } + checked={ + files.length > 0 && + selected.length === files.length + } + onChange={handleSelectAllClick} + inputProps={{ + "aria-label": "select all desserts", + }} + /> + + + + setOrderBy([ + "id", + orderBy[1] === "asc" + ? "desc" + : "asc", + ]) + } + > + # + {orderBy[0] === "id" ? ( + + {orderBy[1] === "desc" + ? "sorted descending" + : "sorted ascending"} + + ) : null} + + + + + setOrderBy([ + "name", + orderBy[1] === "asc" + ? "desc" + : "asc", + ]) + } + > + 文件名 + {orderBy[0] === "name" ? ( + + {orderBy[1] === "desc" + ? "sorted descending" + : "sorted ascending"} + + ) : null} + + + + + setOrderBy([ + "size", + orderBy[1] === "asc" + ? "desc" + : "asc", + ]) + } + > + 大小 + {orderBy[0] === "size" ? ( + + {orderBy[1] === "desc" + ? "sorted descending" + : "sorted ascending"} + + ) : null} + + + + 上传者 + + + 上传于 + + + 操作 + + + + + {files.map((row) => ( + + + + handleClick(event, row.ID) + } + checked={isSelected(row.ID)} + /> + + {row.ID} + + + {row.Name} + + + + {sizeToString(row.Size)} + + + + {users[row.UserID] + ? users[row.UserID].Nick + : "未知"} + + + + {formatLocalTime( + row.CreatedAt, + "YYYY-MM-DD H:mm:ss" + )} + + + + + deletePolicy(row.ID) + } + size={"small"} + > + + + + + + ))} + +
+
+ setPage(p + 1)} + onChangeRowsPerPage={(e) => { + setPageSize(e.target.value); + setPage(1); + }} + /> +
+
+ ); +} diff --git a/assets/src/component/Admin/File/Import.js b/assets/src/component/Admin/File/Import.js new file mode 100644 index 00000000..6c434629 --- /dev/null +++ b/assets/src/component/Admin/File/Import.js @@ -0,0 +1,471 @@ +import { Dialog } from "@material-ui/core"; +import Button from "@material-ui/core/Button"; +import Chip from "@material-ui/core/Chip"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import Fade from "@material-ui/core/Fade"; +import FormControl from "@material-ui/core/FormControl"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import Input from "@material-ui/core/Input"; +import InputAdornment from "@material-ui/core/InputAdornment"; +import InputLabel from "@material-ui/core/InputLabel"; +import MenuItem from "@material-ui/core/MenuItem"; +import Paper from "@material-ui/core/Paper"; +import Popper from "@material-ui/core/Popper"; +import Select from "@material-ui/core/Select"; +import { makeStyles } from "@material-ui/core/styles"; +import Switch from "@material-ui/core/Switch"; +import Typography from "@material-ui/core/Typography"; +import Alert from "@material-ui/lab/Alert"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { useHistory } from "react-router"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; +import PathSelector from "../../FileManager/PathSelector"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + form: { + maxWidth: 400, + marginTop: 20, + marginBottom: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, + userSelect: { + width: 400, + borderRadius: 0, + }, +})); + +function useDebounce(value, delay) { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + return () => { + clearTimeout(handler); + }; + }, [value]); + + return debouncedValue; +} + +export default function Import() { + const classes = useStyles(); + const [loading, setLoading] = useState(false); + const [options, setOptions] = useState({ + policy: 1, + userInput: "", + src: "", + dst: "", + recursive: true, + }); + const [anchorEl, setAnchorEl] = useState(null); + const [policies, setPolicies] = useState({}); + const [users, setUsers] = useState([]); + const [user, setUser] = useState(null); + const [selectRemote, setSelectRemote] = useState(false); + const [selectLocal, setSelectLocal] = useState(false); + + const handleChange = (name) => (event) => { + setOptions({ + ...options, + [name]: event.target.value, + }); + }; + + const handleCheckChange = (name) => (event) => { + setOptions({ + ...options, + [name]: event.target.checked, + }); + }; + + const history = useHistory(); + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const submit = (e) => { + e.preventDefault(); + if (user === null) { + ToggleSnackbar("top", "right", "请先选择目标用户", "warning"); + return; + } + setLoading(true); + API.post("/admin/task/import", { + uid: user.ID, + policy_id: parseInt(options.policy), + src: options.src, + dst: options.dst, + recursive: options.recursive, + }) + .then(() => { + setLoading(false); + history.push("/admin/file"); + ToggleSnackbar( + "top", + "right", + "导入任务已创建,您可以在“持久任务”中查看执行情况", + "success" + ); + }) + .catch((error) => { + setLoading(false); + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + + const debouncedSearchTerm = useDebounce(options.userInput, 500); + + useEffect(() => { + if (debouncedSearchTerm !== "") { + API.post("/admin/user/list", { + page: 1, + page_size: 10000, + order_by: "id asc", + searches: { + nick: debouncedSearchTerm, + email: debouncedSearchTerm, + }, + }) + .then((response) => { + setUsers(response.data.items); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + } + }, [debouncedSearchTerm]); + + useEffect(() => { + API.post("/admin/policy/list", { + page: 1, + page_size: 10000, + order_by: "id asc", + conditions: {}, + }) + .then((response) => { + const res = {}; + response.data.items.forEach((v) => { + res[v.ID] = v; + }); + setPolicies(res); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + // eslint-disable-next-line + }, []); + + const selectUser = (u) => { + setOptions({ + ...options, + userInput: "", + }); + setUser(u); + }; + + const setMoveTarget = (setter) => (folder) => { + const path = + folder.path === "/" + ? folder.path + folder.name + : folder.path + "/" + folder.name; + setter(path === "//" ? "/" : path); + }; + + const openPathSelector = (isSrcSelect) => { + if (isSrcSelect) { + if ( + !policies[options.policy] || + policies[options.policy].Type === "local" || + policies[options.policy].Type === "remote" + ) { + ToggleSnackbar( + "top", + "right", + "选择的存储策略只支持手动输入路径", + "warning" + ); + return; + } + setSelectRemote(true); + } else { + if (user === null) { + ToggleSnackbar("top", "right", "请先选择目标用户", "warning"); + return; + } + setSelectLocal(true); + } + }; + + return ( +
+ setSelectRemote(false)} + aria-labelledby="form-dialog-title" + > + 选择目录 + + setOptions({ + ...options, + src: p, + }) + )} + /> + + + + + + setSelectLocal(false)} + aria-labelledby="form-dialog-title" + > + 选择目录 + + setOptions({ + ...options, + dst: p, + }) + )} + /> + + + + + +
+
+ + 导入外部目录 + +
+
+ + 您可以将存储策略中已有文件、目录结构导入到 + Cloudreve + 中,导入操作不会额外占用物理存储空间,但仍会正常扣除用户已用容量空间,空间不足时将停止导入。 + +
+
+ + + 存储策略 + + + + 选择要导入文件目前存储所在的存储策略 + + +
+
+ + + 目标用户 + + { + handleChange("userInput")(e); + setAnchorEl(e.currentTarget); + }} + startAdornment={ + user !== null && ( + + { + setUser(null); + }} + label={user.Nick} + /> + + ) + } + disabled={user !== null} + /> + 0 + } + anchorEl={anchorEl} + placement={"bottom"} + transition + > + {({ TransitionProps }) => ( + + + {users.map((u) => ( + + selectUser(u) + } + > + {u.Nick}{" "} + {"<" + u.Email + ">"} + + ))} + + + )} + + + 选择要将文件导入到哪个用户的文件系统中,可通过昵称、邮箱搜索用户 + + +
+ +
+ + + 原始目录路径 + + + { + handleChange("src")(e); + setAnchorEl(e.currentTarget); + }} + required + endAdornment={ + + } + /> + + + 要导入的目录在存储端的路径 + + +
+ +
+ + + 目的目录路径 + + + { + handleChange("dst")(e); + setAnchorEl(e.currentTarget); + }} + required + endAdornment={ + + } + /> + + + 要将目录导入到用户文件系统中的路径 + + +
+ +
+ + + } + label="递归导入子目录" + /> + + 是否将目录下的所有子目录递归导入 + + +
+
+
+ +
+ +
+
+
+ ); +} diff --git a/assets/src/component/Admin/Group/EditGroup.js b/assets/src/component/Admin/Group/EditGroup.js new file mode 100644 index 00000000..a1001999 --- /dev/null +++ b/assets/src/component/Admin/Group/EditGroup.js @@ -0,0 +1,84 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { useParams } from "react-router"; +import API from "../../../middleware/Api"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../actions"; +import GroupForm from "./GroupForm"; + +export default function EditGroupPreload() { + const [group, setGroup] = useState({}); + + const { id } = useParams(); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + useEffect(() => { + setGroup({}); + API.get("/admin/group/" + id) + .then((response) => { + // 布尔值转换 + ["ShareEnabled", "WebDAVEnabled"].forEach((v) => { + response.data[v] = response.data[v] ? "true" : "false"; + }); + [ + "archive_download", + "archive_task", + "one_time_download", + "share_download", + "aria2", + ].forEach((v) => { + if (response.data.OptionsSerialized[v] !== undefined) { + response.data.OptionsSerialized[v] = response.data + .OptionsSerialized[v] + ? "true" + : "false"; + } + }); + + // 整型转换 + ["MaxStorage", "SpeedLimit"].forEach((v) => { + response.data[v] = response.data[v].toString(); + }); + ["compress_size", "decompress_size"].forEach((v) => { + if (response.data.OptionsSerialized[v] !== undefined) { + response.data.OptionsSerialized[ + v + ] = response.data.OptionsSerialized[v].toString(); + } + }); + response.data.PolicyList = response.data.PolicyList[0]; + + // JSON转换 + if ( + response.data.OptionsSerialized.aria2_options === undefined + ) { + response.data.OptionsSerialized.aria2_options = "{}"; + } else { + try { + response.data.OptionsSerialized.aria2_options = JSON.stringify( + response.data.OptionsSerialized.aria2_options + ); + } catch (e) { + ToggleSnackbar( + "top", + "right", + "Aria2 设置项格式错误", + "warning" + ); + return; + } + } + setGroup(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }, [id]); + + return
{group.ID !== undefined && }
; +} diff --git a/assets/src/component/Admin/Group/Group.js b/assets/src/component/Admin/Group/Group.js new file mode 100644 index 00000000..66f1ffa5 --- /dev/null +++ b/assets/src/component/Admin/Group/Group.js @@ -0,0 +1,248 @@ +import Button from "@material-ui/core/Button"; +import IconButton from "@material-ui/core/IconButton"; +import Paper from "@material-ui/core/Paper"; +import { makeStyles } from "@material-ui/core/styles"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableContainer from "@material-ui/core/TableContainer"; +import TableHead from "@material-ui/core/TableHead"; +import TablePagination from "@material-ui/core/TablePagination"; +import TableRow from "@material-ui/core/TableRow"; +import Tooltip from "@material-ui/core/Tooltip"; +import { Delete, Edit } from "@material-ui/icons"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { useHistory, useLocation } from "react-router"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; +import { sizeToString } from "../../../utils"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + content: { + padding: theme.spacing(2), + }, + container: { + overflowX: "auto", + }, + tableContainer: { + marginTop: 16, + }, + header: { + display: "flex", + justifyContent: "space-between", + }, + headerRight: {}, +})); + +const columns = [ + { id: "#", label: "#", minWidth: 50 }, + { id: "name", label: "名称", minWidth: 170 }, + { id: "type", label: "存储策略", minWidth: 170 }, + { + id: "count", + label: "下属用户数", + minWidth: 50, + align: "right", + }, + { + id: "size", + label: "最大容量", + minWidth: 100, + align: "right", + }, + { + id: "action", + label: "操作", + minWidth: 170, + align: "right", + }, +]; + +function useQuery() { + return new URLSearchParams(useLocation().search); +} + +export default function Group() { + const classes = useStyles(); + const [groups, setGroups] = useState([]); + const [statics, setStatics] = useState([]); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [total, setTotal] = useState(0); + const [policies, setPolicies] = React.useState({}); + + const location = useLocation(); + const history = useHistory(); + const query = useQuery(); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const loadList = () => { + API.post("/admin/group/list", { + page: page, + page_size: pageSize, + order_by: "id desc", + }) + .then((response) => { + setGroups(response.data.items); + setStatics(response.data.statics); + setTotal(response.data.total); + setPolicies(response.data.policies); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + + useEffect(() => { + if (query.get("code") === "0") { + ToggleSnackbar("top", "right", "授权成功", "success"); + } else if (query.get("msg") && query.get("msg") !== "") { + ToggleSnackbar( + "top", + "right", + query.get("msg") + ", " + query.get("err"), + "warning" + ); + } + }, [location]); + + useEffect(() => { + loadList(); + }, [page, pageSize]); + + const deletePolicy = (id) => { + API.delete("/admin/group/" + id) + .then(() => { + loadList(); + ToggleSnackbar("top", "right", "用户组已删除", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + + return ( +
+
+ +
+ +
+
+ + + + + + + {columns.map((column) => ( + + {column.label} + + ))} + + + + {groups.map((row) => ( + + {row.ID} + {row.Name} + + {row.PolicyList !== null && + row.PolicyList.map((pid, key) => { + let res = ""; + if (policies[pid]) { + res += policies[pid].Name; + } + if ( + key !== + row.PolicyList.length - 1 + ) { + res += " / "; + } + return res; + })} + + + {statics[row.ID] !== undefined && + statics[row.ID].toLocaleString()} + + + {statics[row.ID] !== undefined && + sizeToString(row.MaxStorage)} + + + + + history.push( + "/admin/group/edit/" + + row.ID + ) + } + size={"small"} + > + + + + + + deletePolicy(row.ID) + } + size={"small"} + > + + + + + + ))} + +
+
+ setPage(p + 1)} + onChangeRowsPerPage={(e) => { + setPageSize(e.target.value); + setPage(1); + }} + /> +
+
+ ); +} diff --git a/assets/src/component/Admin/Group/GroupForm.js b/assets/src/component/Admin/Group/GroupForm.js new file mode 100644 index 00000000..2bbd0987 --- /dev/null +++ b/assets/src/component/Admin/Group/GroupForm.js @@ -0,0 +1,553 @@ +import Button from "@material-ui/core/Button"; +import Collapse from "@material-ui/core/Collapse"; +import FormControl from "@material-ui/core/FormControl"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import Input from "@material-ui/core/Input"; +import InputLabel from "@material-ui/core/InputLabel"; +import MenuItem from "@material-ui/core/MenuItem"; +import Select from "@material-ui/core/Select"; +import { makeStyles } from "@material-ui/core/styles"; +import Switch from "@material-ui/core/Switch"; +import Typography from "@material-ui/core/Typography"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { useHistory } from "react-router"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; +import SizeInput from "../Common/SizeInput"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + form: { + maxWidth: 400, + marginTop: 20, + marginBottom: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, +})); + +// function getStyles(name, personName, theme) { +// return { +// fontWeight: +// personName.indexOf(name) === -1 +// ? theme.typography.fontWeightRegular +// : theme.typography.fontWeightMedium +// }; +// } + +export default function GroupForm(props) { + const classes = useStyles(); + const [loading, setLoading] = useState(false); + const [group, setGroup] = useState( + props.group + ? props.group + : { + ID: 0, + Name: "", + MaxStorage: "1073741824", // 转换类型 + ShareEnabled: "true", // 转换类型 + WebDAVEnabled: "true", // 转换类型 + SpeedLimit: "0", // 转换类型 + PolicyList: 1, // 转换类型,至少选择一个 + OptionsSerialized: { + // 批量转换类型 + share_download: "true", + aria2_options: "{}", // json decode + compress_size: "0", + decompress_size: "0", + }, + } + ); + const [policies, setPolicies] = useState({}); + + const history = useHistory(); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + useEffect(() => { + API.post("/admin/policy/list", { + page: 1, + page_size: 10000, + order_by: "id asc", + conditions: {}, + }) + .then((response) => { + const res = {}; + response.data.items.forEach((v) => { + res[v.ID] = v.Name; + }); + setPolicies(res); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }, []); + + const handleChange = (name) => (event) => { + setGroup({ + ...group, + [name]: event.target.value, + }); + }; + + const handleCheckChange = (name) => (event) => { + const value = event.target.checked ? "true" : "false"; + setGroup({ + ...group, + [name]: value, + }); + }; + + const handleOptionCheckChange = (name) => (event) => { + const value = event.target.checked ? "true" : "false"; + setGroup({ + ...group, + OptionsSerialized: { + ...group.OptionsSerialized, + [name]: value, + }, + }); + }; + + const handleOptionChange = (name) => (event) => { + setGroup({ + ...group, + OptionsSerialized: { + ...group.OptionsSerialized, + [name]: event.target.value, + }, + }); + }; + + const submit = (e) => { + e.preventDefault(); + const groupCopy = { + ...group, + OptionsSerialized: { ...group.OptionsSerialized }, + }; + + // 布尔值转换 + ["ShareEnabled", "WebDAVEnabled"].forEach((v) => { + groupCopy[v] = groupCopy[v] === "true"; + }); + [ + "archive_download", + "archive_task", + "one_time_download", + "share_download", + "aria2", + ].forEach((v) => { + if (groupCopy.OptionsSerialized[v] !== undefined) { + groupCopy.OptionsSerialized[v] = + groupCopy.OptionsSerialized[v] === "true"; + } + }); + + // 整型转换 + ["MaxStorage", "SpeedLimit"].forEach((v) => { + groupCopy[v] = parseInt(groupCopy[v]); + }); + ["compress_size", "decompress_size"].forEach((v) => { + if (groupCopy.OptionsSerialized[v] !== undefined) { + groupCopy.OptionsSerialized[v] = parseInt( + groupCopy.OptionsSerialized[v] + ); + } + }); + groupCopy.PolicyList = [parseInt(groupCopy.PolicyList)]; + // JSON转换 + try { + groupCopy.OptionsSerialized.aria2_options = JSON.parse( + groupCopy.OptionsSerialized.aria2_options + ); + } catch (e) { + ToggleSnackbar("top", "right", "Aria2 设置项格式错误", "warning"); + return; + } + + setLoading(true); + API.post("/admin/group", { + group: groupCopy, + }) + .then(() => { + history.push("/admin/group"); + ToggleSnackbar( + "top", + "right", + "用户组已" + (props.group ? "保存" : "添加"), + "success" + ); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + return ( +
+
+
+ + {group.ID === 0 && "新建用户组"} + {group.ID !== 0 && "编辑 " + group.Name} + + +
+ {group.ID !== 3 && ( + <> +
+ + + 用户组名 + + + + 用户组的名称 + + +
+ +
+ + + 存储策略 + + + + 指定用户组的存储策略。 + + +
+ +
+ + + + + 用户组下的用户初始可用最大容量 + +
+ + )} + +
+ + + + + 填写为 0 表示不限制。开启限制后, + 此用户组下的用户下载所有支持限速的存储策略下的文件时,下载最大速度会被限制。 + +
+ + {group.ID !== 3 && ( +
+ + + } + label="允许创建分享" + /> + + 关闭后,用户无法创建分享链接 + + +
+ )} + +
+ + + } + label="允许下载分享" + /> + + 关闭后,用户无法下载别人创建的文件分享 + + +
+ + {group.ID !== 3 && ( +
+ + + } + label="WebDAV" + /> + + 关闭后,用户无法通过 WebDAV + 协议连接至网盘 + + +
+ )} + +
+ + + } + label="禁止多次下载请求" + /> + + 只针对本机存储策略有效。开启后,用户无法使用多线程下载工具。 + + +
+ + {group.ID !== 3 && ( +
+ + + } + label="离线下载" + /> + + 是否允许用户创建离线下载任务 + + +
+ )} + + +
+ + + Aria2 任务参数 + + + + 此用户组创建离线下载任务时额外携带的参数,以 + JSON + 编码后的格式书写,您可也可以将这些设置写在 + Aria2 配置文件里,可用参数请查阅官方文档 + + +
+
+ +
+ + + } + label="打包下载" + /> + + 是否允许用户多选文件打包下载 + + +
+ + {group.ID !== 3 && ( +
+ + + } + label="压缩/解压缩 任务" + /> + + 是否用户创建 压缩/解压缩 任务 + + +
+ )} + + +
+ + + + + 用户可创建的压缩任务的文件最大总大小,填写为 + 0 表示不限制 + +
+ +
+ + + + + 用户可创建的解压缩任务的文件最大总大小,填写为 + 0 表示不限制 + +
+
+
+
+
+ +
+
+
+ ); +} diff --git a/assets/src/component/Admin/Index.js b/assets/src/component/Admin/Index.js new file mode 100644 index 00000000..7fff3a67 --- /dev/null +++ b/assets/src/component/Admin/Index.js @@ -0,0 +1,509 @@ +import Avatar from "@material-ui/core/Avatar"; +import Button from "@material-ui/core/Button"; +import Chip from "@material-ui/core/Chip"; +import { blue, green, red, yellow } from "@material-ui/core/colors"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogContentText from "@material-ui/core/DialogContentText"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import Divider from "@material-ui/core/Divider"; +import Grid from "@material-ui/core/Grid"; +import List from "@material-ui/core/List"; +import ListItem from "@material-ui/core/ListItem"; +import ListItemAvatar from "@material-ui/core/ListItemAvatar"; +import ListItemIcon from "@material-ui/core/ListItemIcon"; +import ListItemText from "@material-ui/core/ListItemText"; +import Paper from "@material-ui/core/Paper"; +import { makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import { + Description, + Favorite, + FileCopy, + Forum, + GitHub, + Home, + Launch, + Lock, + People, + Public, + Telegram, +} from "@material-ui/icons"; +import axios from "axios"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { + CartesianGrid, + Legend, + Line, + LineChart, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { ResponsiveContainer } from "recharts/lib/component/ResponsiveContainer"; +import TimeAgo from "timeago-react"; +import { toggleSnackbar } from "../../actions"; +import API from "../../middleware/Api"; +import pathHelper from "../../utils/page"; + +const useStyles = makeStyles((theme) => ({ + paper: { + padding: theme.spacing(3), + height: "100%", + }, + logo: { + width: 70, + }, + logoContainer: { + padding: theme.spacing(3), + display: "flex", + }, + title: { + marginLeft: 16, + }, + cloudreve: { + fontSize: 25, + color: theme.palette.text.secondary, + }, + version: { + color: theme.palette.text.hint, + }, + links: { + padding: theme.spacing(3), + }, + iconRight: { + minWidth: 0, + }, + userIcon: { + backgroundColor: blue[100], + color: blue[600], + }, + fileIcon: { + backgroundColor: yellow[100], + color: yellow[800], + }, + publicIcon: { + backgroundColor: green[100], + color: green[800], + }, + secretIcon: { + backgroundColor: red[100], + color: red[800], + }, +})); + +export default function Index() { + const classes = useStyles(); + const [lineData, setLineData] = useState([]); + const [news, setNews] = useState([]); + const [newsUsers, setNewsUsers] = useState({}); + const [open, setOpen] = React.useState(false); + const [siteURL, setSiteURL] = React.useState(""); + const [statistics, setStatistics] = useState({ + fileTotal: 0, + userTotal: 0, + publicShareTotal: 0, + secretShareTotal: 0, + }); + const [version, setVersion] = useState({ + backend: "-", + }); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const ResetSiteURL = () => { + setOpen(false); + API.patch("/admin/setting", { + options: [ + { + key: "siteURL", + value: window.location.origin, + }, + ], + }) + .then(() => { + setSiteURL(window.location.origin); + ToggleSnackbar("top", "right", "设置已更改", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + + useEffect(() => { + API.get("/admin/summary") + .then((response) => { + const data = []; + response.data.date.forEach((v, k) => { + data.push({ + name: v, + file: response.data.files[k], + user: response.data.users[k], + share: response.data.shares[k], + }); + }); + setLineData(data); + setStatistics({ + fileTotal: response.data.fileTotal, + userTotal: response.data.userTotal, + publicShareTotal: response.data.publicShareTotal, + secretShareTotal: response.data.secretShareTotal, + }); + setVersion(response.data.version); + setSiteURL(response.data.siteURL); + if ( + response.data.siteURL === "" || + response.data.siteURL !== window.location.origin + ) { + setOpen(true); + } + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + + axios + .get("/api/v3/admin/news") + .then((response) => { + setNews(response.data.data); + const res = {}; + response.data.included.forEach((v) => { + if (v.type === "users") { + res[v.id] = v.attributes; + } + }); + setNewsUsers(res); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }, []); + + return ( + + setOpen(false)} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + + {"确定站点URL设置"} + + + + + {siteURL === "" && + "您尚未设定站点URL,是否要将其设定为当前的 " + + window.location.origin + + " ?"} + {siteURL !== "" && + "您设置的站点URL与当前实际不一致,是否要将其设定为当前的 " + + window.location.origin + + " ?"} + + + 此设置非常重要,请确保其与您站点的实际地址一致。你可以在 + 参数设置 - 站点信息 中更改此设置。 + + + + + + + + + + + + 趋势 + + + + + + + + + + + + + + + + + + + 总计 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 网盘 +
+ + Cloudreve + + + {version.backend}{" "} + {version.is_pro === "true" && ( + + )} + +
+
+ +
+ + + window.open("https://cloudreve.org") + } + > + + + + + + + + + + window.open( + "https://github.com/cloudreve/cloudreve" + ) + } + > + + + + + + + + + + window.open("https://docs.cloudreve.org/") + } + > + + + + + + + + + + window.open("https://forum.cloudreve.org") + } + > + + + + + + + + + + window.open( + "https://t.me/cloudreve_official" + ) + } + > + + + + + + + + + + window.open( + "https://docs.cloudreve.org/use/pro/jie-shao" + ) + } + > + + + + + + + + + +
+
+
+ + + + {news && + news.map((v) => ( + <> + + window.open( + "https://forum.cloudreve.org/d/" + + v.id + ) + } + > + + + + + + {newsUsers[ + v.relationships + .startUser.data + .id + ] && + newsUsers[ + v.relationships + .startUser + .data.id + ].username}{" "} + + 发表于{" "} + + + } + /> + + + + ))} + + + +
+ ); +} diff --git a/assets/src/component/Admin/Policy/AddPolicy.js b/assets/src/component/Admin/Policy/AddPolicy.js new file mode 100644 index 00000000..fdad8101 --- /dev/null +++ b/assets/src/component/Admin/Policy/AddPolicy.js @@ -0,0 +1,45 @@ +import Paper from "@material-ui/core/Paper"; +import { makeStyles } from "@material-ui/core/styles"; +import React from "react"; +import { useParams } from "react-router"; +import COSGuide from "./Guid/COSGuide"; +import LocalGuide from "./Guid/LocalGuide"; +import OneDriveGuide from "./Guid/OneDriveGuide"; +import OSSGuide from "./Guid/OSSGuide"; +import QiniuGuide from "./Guid/QiniuGuide"; +import RemoteGuide from "./Guid/RemoteGuide"; +import UpyunGuide from "./Guid/UpyunGuide"; +import S3Guide from "./Guid/S3Guide"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + content: { + padding: theme.spacing(2), + }, +})); + +export default function AddPolicyParent() { + const classes = useStyles(); + + const { type } = useParams(); + + return ( +
+ + {type === "local" && } + {type === "remote" && } + {type === "qiniu" && } + {type === "oss" && } + {type === "upyun" && } + {type === "cos" && } + {type === "onedrive" && } + {type === "s3" && } + +
+ ); +} diff --git a/assets/src/component/Admin/Policy/EditPolicy.js b/assets/src/component/Admin/Policy/EditPolicy.js new file mode 100644 index 00000000..b91fb168 --- /dev/null +++ b/assets/src/component/Admin/Policy/EditPolicy.js @@ -0,0 +1,93 @@ +import Paper from "@material-ui/core/Paper"; +import { makeStyles } from "@material-ui/core/styles"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { useParams } from "react-router"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; +import COSGuide from "./Guid/COSGuide"; +import EditPro from "./Guid/EditPro"; +import LocalGuide from "./Guid/LocalGuide"; +import OneDriveGuide from "./Guid/OneDriveGuide"; +import OSSGuide from "./Guid/OSSGuide"; +import QiniuGuide from "./Guid/QiniuGuide"; +import RemoteGuide from "./Guid/RemoteGuide"; +import UpyunGuide from "./Guid/UpyunGuide"; +import S3Guide from "./Guid/S3Guide"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + content: { + padding: theme.spacing(2), + }, +})); + +export default function EditPolicyPreload() { + const classes = useStyles(); + const [type, setType] = useState(""); + const [policy, setPolicy] = useState({}); + + const { mode, id } = useParams(); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + useEffect(() => { + setType(""); + API.get("/admin/policy/" + id) + .then((response) => { + response.data.IsOriginLinkEnable = response.data + .IsOriginLinkEnable + ? "true" + : "false"; + response.data.AutoRename = response.data.AutoRename + ? "true" + : "false"; + response.data.MaxSize = response.data.MaxSize.toString(); + response.data.IsPrivate = response.data.IsPrivate + ? "true" + : "false"; + response.data.OptionsSerialized.file_type = response.data + .OptionsSerialized.file_type + ? response.data.OptionsSerialized.file_type.join(",") + : ""; + setPolicy(response.data); + setType(response.data.Type); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }, [id]); + + return ( +
+ + {mode === "guide" && ( + <> + {type === "local" && } + {type === "remote" && } + {type === "qiniu" && } + {type === "oss" && } + {type === "upyun" && } + {type === "cos" && } + {type === "onedrive" && ( + + )} + {type === "s3" && } + + )} + + {mode === "pro" && type !== "" && } + +
+ ); +} diff --git a/assets/src/component/Admin/Policy/Guid/COSGuide.js b/assets/src/component/Admin/Policy/Guid/COSGuide.js new file mode 100644 index 00000000..adac62b9 --- /dev/null +++ b/assets/src/component/Admin/Policy/Guid/COSGuide.js @@ -0,0 +1,1178 @@ +import Button from "@material-ui/core/Button"; +import Collapse from "@material-ui/core/Collapse"; +import FormControl from "@material-ui/core/FormControl"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Input from "@material-ui/core/Input"; +import InputLabel from "@material-ui/core/InputLabel"; +import Link from "@material-ui/core/Link"; +import MenuItem from "@material-ui/core/MenuItem"; +import Radio from "@material-ui/core/Radio"; +import RadioGroup from "@material-ui/core/RadioGroup"; +import Select from "@material-ui/core/Select"; +import Step from "@material-ui/core/Step"; +import StepLabel from "@material-ui/core/StepLabel"; +import Stepper from "@material-ui/core/Stepper"; +import { lighten, makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import React, { useCallback, useState } from "react"; +import { useDispatch } from "react-redux"; +import { useHistory } from "react-router"; +import { toggleSnackbar } from "../../../../actions"; +import API from "../../../../middleware/Api"; +import { getNumber } from "../../../../utils"; +import DomainInput from "../../Common/DomainInput"; +import SizeInput from "../../Common/SizeInput"; +import MagicVar from "../../Dialogs/MagicVar"; + +const useStyles = makeStyles((theme) => ({ + stepContent: { + padding: "16px 32px 16px 32px", + }, + form: { + maxWidth: 400, + marginTop: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, + subStepContainer: { + display: "flex", + marginBottom: 20, + padding: 10, + transition: theme.transitions.create("background-color", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + "&:focus-within": { + backgroundColor: theme.palette.background.default, + }, + }, + stepNumber: { + width: 20, + height: 20, + backgroundColor: lighten(theme.palette.secondary.light, 0.2), + color: theme.palette.secondary.contrastText, + textAlign: "center", + borderRadius: " 50%", + }, + stepNumberContainer: { + marginRight: 10, + }, + stepFooter: { + marginTop: 32, + }, + button: { + marginRight: theme.spacing(1), + }, + viewButtonLabel: { textTransform: "none" }, + "@global": { + code: { + color: "rgba(0, 0, 0, 0.87)", + display: "inline-block", + padding: "2px 6px", + fontFamily: + ' Consolas, "Liberation Mono", Menlo, Courier, monospace', + borderRadius: "2px", + backgroundColor: "rgba(255,229,100,0.1)", + }, + }, +})); + +const steps = [ + { + title: "存储空间", + optional: false, + }, + { + title: "上传路径", + optional: false, + }, + { + title: "直链设置", + optional: false, + }, + { + title: "上传限制", + optional: false, + }, + { + title: "跨域策略", + optional: true, + }, + { + title: "云函数回调", + optional: true, + }, + { + title: "完成", + optional: false, + }, +]; + +export default function COSGuide(props) { + const classes = useStyles(); + const history = useHistory(); + + const [activeStep, setActiveStep] = useState(0); + const [loading, setLoading] = useState(false); + const [skipped, setSkipped] = React.useState(new Set()); + const [magicVar, setMagicVar] = useState(""); + const [useCDN, setUseCDN] = useState("false"); + const [policy, setPolicy] = useState( + props.policy + ? props.policy + : { + Type: "cos", + Name: "", + SecretKey: "", + AccessKey: "", + BaseURL: "", + Server: "", + IsPrivate: "true", + DirNameRule: "uploads/{year}/{month}/{day}", + AutoRename: "true", + FileNameRule: "{randomkey8}_{originname}", + IsOriginLinkEnable: "false", + MaxSize: "0", + OptionsSerialized: { + file_type: "", + }, + } + ); + const [policyID, setPolicyID] = useState( + props.policy ? props.policy.ID : 0 + ); + const [region, setRegion] = useState("ap-chengdu"); + + const handleChange = (name) => (event) => { + setPolicy({ + ...policy, + [name]: event.target.value, + }); + }; + + const handleOptionChange = (name) => (event) => { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + [name]: event.target.value, + }, + }); + }; + + const isStepSkipped = (step) => { + return skipped.has(step); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const submitPolicy = (e) => { + e.preventDefault(); + setLoading(true); + + const policyCopy = { ...policy }; + policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized }; + + if (useCDN === "false") { + policyCopy.BaseURL = policy.Server; + } + + // 类型转换 + policyCopy.AutoRename = policyCopy.AutoRename === "true"; + policyCopy.IsOriginLinkEnable = + policyCopy.IsOriginLinkEnable === "true"; + policyCopy.IsPrivate = policyCopy.IsPrivate === "true"; + policyCopy.MaxSize = parseInt(policyCopy.MaxSize); + policyCopy.OptionsSerialized.file_type = policyCopy.OptionsSerialized.file_type.split( + "," + ); + if ( + policyCopy.OptionsSerialized.file_type.length === 1 && + policyCopy.OptionsSerialized.file_type[0] === "" + ) { + policyCopy.OptionsSerialized.file_type = []; + } + + API.post("/admin/policy", { + policy: policyCopy, + }) + .then((response) => { + ToggleSnackbar( + "top", + "right", + "存储策略已" + (props.policy ? "保存" : "添加"), + "success" + ); + setActiveStep(4); + setPolicyID(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + + setLoading(false); + }; + + const createCORS = () => { + setLoading(true); + API.post("/admin/policy/cors", { + id: policyID, + }) + .then(() => { + ToggleSnackbar("top", "right", "跨域策略已添加", "success"); + setActiveStep(5); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const creatCallback = () => { + setLoading(true); + API.post("/admin/policy/scf", { + id: policyID, + region: region, + }) + .then(() => { + ToggleSnackbar("top", "right", "回调云函数已添加", "success"); + setActiveStep(6); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + return ( +
+ + {props.policy ? "修改" : "添加"} 腾讯云 COS 存储策略 + + + {steps.map((label, index) => { + const stepProps = {}; + const labelProps = {}; + if (label.optional) { + labelProps.optional = ( + 可选 + ); + } + if (isStepSkipped(index)) { + stepProps.completed = false; + } + return ( + + {label.title} + + ); + })} + + + {activeStep === 0 && ( +
{ + e.preventDefault(); + setActiveStep(1); + }} + > +
+
+
0
+
+
+ + 在使用 腾讯云 COS 储策略前,请确保您在 参数设置 + - 站点信息 - 站点URL 中填写的 + 地址与实际相符,并且 + 能够被外网正常访问。 + +
+
+ +
+
+
1
+
+
+ + 前往 + + COS 管理控制台 + + 创建存储桶。 + +
+
+ +
+
+
2
+
+
+ + 转到所创建存储桶的基础配置页面,将 + 空间名称填写在下方: + +
+ + + 空间名称 + + + +
+
+
+ +
+
+
3
+
+
+ + 在下方选择您创建的空间的访问权限类型,推荐选择 + 私有读写 + 以获得更高的安全性,私有空间无法开启“获取直链”功能。 + +
+ + + + } + label="私有读写" + /> + + } + label="公共读私有写" + /> + + +
+
+
+ +
+
+
4
+
+
+ + 转到所创建 Bucket 的基础配置,填写 + 基本信息栏目下 给出的{" "} + 访问域名 + +
+ +
+
+
+ +
+
+
5
+
+
+ + 是否要使用配套的 腾讯云CDN 加速 COS 访问? + +
+ + { + setUseCDN(e.target.value); + }} + row + > + + } + label="使用" + /> + + } + label="不使用" + /> + + +
+
+
+ + +
+
+
6
+
+
+ + 前往 + + 腾讯云 CDN 管理控制台 + + 创建 CDN 加速域名,并设定源站为刚创建的 COS + 存储桶。在下方填写 CDN + 加速域名,并选择是否使用 HTTPS: + +
+ +
+
+
+
+ +
+
+
+ {getNumber(6, [useCDN === "true"])} +
+
+
+ + 在腾讯云 + + 访问密钥 + + 页面获取 + 一对访问密钥,并填写在下方。请确保这对密钥拥有 + COS 和 SCF 服务的访问权限。 + +
+ + + SecretId + + + +
+
+ + + SecretKey + + + +
+
+
+ +
+
+
+ {getNumber(7, [useCDN === "true"])} +
+
+
+ + 为此存储策略命名: + +
+ + + 存储策略名 + + + +
+
+
+ +
+ +
+
+ )} + + {activeStep === 1 && ( +
{ + e.preventDefault(); + setActiveStep(2); + }} + > +
+
+
1
+
+
+ + 请在下方输入文件的存储目录路径,可以为绝对路径或相对路径(相对于 + 从机的 + Cloudreve)。路径中可以使用魔法变量,文件在上传时会自动替换这些变量为相应值; + 可用魔法变量可参考{" "} + { + e.preventDefault(); + setMagicVar("path"); + }} + > + 路径魔法变量列表 + {" "} + 。 + +
+ + + 存储目录 + + + +
+
+
+ +
+
+
2
+
+
+ + 是否需要对存储的物理文件进行重命名?此处的重命名不会影响最终呈现给用户的 + 文件名。文件名也可使用魔法变量, + 可用魔法变量可参考{" "} + { + e.preventDefault(); + setMagicVar("file"); + }} + > + 文件名魔法变量列表 + {" "} + 。 + +
+ + + + } + label="开启重命名" + /> + + } + label="不开启" + /> + + +
+ + +
+ + + 命名规则 + + + +
+
+
+
+ +
+ + +
+
+ )} + + {activeStep === 2 && ( +
{ + e.preventDefault(); + setActiveStep(3); + }} + > +
+
+
1
+
+
+ + 是否允许获取文件永久直链? +
+ 开启后,用户可以请求获得能直接访问到文件内容的直链,适用于图床应用或自用。 +
+ +
+ + { + if ( + policy.IsPrivate === "true" && + e.target.value === "true" + ) { + ToggleSnackbar( + "top", + "right", + "私有空间无法开启此功能", + "warning" + ); + return; + } + handleChange("IsOriginLinkEnable")( + e + ); + }} + row + > + + } + label="允许" + /> + + } + label="禁止" + /> + + +
+
+
+ +
+ {" "} + +
+
+ )} + + {activeStep === 3 && ( +
+
+
+
1
+
+
+ + 是否限制上传的单文件大小? + + +
+ + { + if (e.target.value === "true") { + setPolicy({ + ...policy, + MaxSize: "10485760", + }); + } else { + setPolicy({ + ...policy, + MaxSize: "0", + }); + } + }} + row + > + + } + label="限制" + /> + + } + label="不限制" + /> + + +
+
+
+ + +
+
+
2
+
+
+ + 输入限制: + +
+ +
+
+
+
+ +
+
+
+ {policy.MaxSize !== "0" ? "3" : "2"} +
+
+
+ + 是否限制上传文件扩展名? + + +
+ + { + if (e.target.value === "true") { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + file_type: + "jpg,png,mp4,zip,rar", + }, + }); + } else { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + file_type: "", + }, + }); + } + }} + row + > + + } + label="限制" + /> + + } + label="不限制" + /> + + +
+
+
+ + +
+
+
+ {policy.MaxSize !== "0" ? "4" : "3"} +
+
+
+ + 输入允许上传的文件扩展名,多个请以半角逗号 , + 隔开 + +
+ + + 扩展名列表 + + + +
+
+
+
+ +
+ {" "} + +
+
+ )} + + {activeStep === 4 && ( +
+
+
+
+ + COS 存储桶 需要正确配置跨域策略后才能使用 Web + 端上传文件,Cloudreve + 可以帮您自动设置,您也可以参考文档步骤手动设置。如果您已设置过此 + Bucket 的跨域策略,此步骤可以跳过。 + +
+ +
+
+
+
+ {" "} +
+ + )} + + {activeStep === 5 && ( +
+
+
+
+ + COS 存储桶 客户端直传需要借助腾讯云的 + + 云函数 + + 产品以确保上传回调可控。如果您打算将此存储策略自用,或者分配给可信赖用户组,此步骤可以跳过。 + 如果是作为公有使用,请务必创建回调云函数。 +
+
+
+ + Cloudreve 可以尝试帮你自动创建回调云函数,请选择 + COS 存储桶 所在地域后继续。 + 创建可能会花费数秒钟,请耐心等待。创建前请确保您的腾讯云账号已开启云函数服务。 + + +
+ + + 存储桶所在地区 + + + +
+ +
+ +
+
+
+
+ {" "} +
+ + )} + + {activeStep === 6 && ( + <> +
+ + 存储策略已{props.policy ? "保存" : "添加"}! + + + 要使用此存储策略,请到用户组管理页面,为相应用户组绑定此存储策略。 + +
+
+ +
+ + )} + + setMagicVar("")} + /> + setMagicVar("")} + /> +
+ ); +} diff --git a/assets/src/component/Admin/Policy/Guid/EditPro.js b/assets/src/component/Admin/Policy/Guid/EditPro.js new file mode 100644 index 00000000..13b914aa --- /dev/null +++ b/assets/src/component/Admin/Policy/Guid/EditPro.js @@ -0,0 +1,548 @@ +import Button from "@material-ui/core/Button"; +import FormControl from "@material-ui/core/FormControl"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Input from "@material-ui/core/Input"; +import Radio from "@material-ui/core/Radio"; +import RadioGroup from "@material-ui/core/RadioGroup"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableContainer from "@material-ui/core/TableContainer"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import Typography from "@material-ui/core/Typography"; +import React, { useCallback, useState } from "react"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../../actions"; +import API from "../../../../middleware/Api"; + +export default function EditPro(props) { + const [, setLoading] = useState(false); + const [policy, setPolicy] = useState(props.policy); + + const handleChange = (name) => (event) => { + setPolicy({ + ...policy, + [name]: event.target.value, + }); + }; + + const handleOptionChange = (name) => (event) => { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + [name]: event.target.value, + }, + }); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const submitPolicy = (e) => { + e.preventDefault(); + setLoading(true); + + const policyCopy = { ...policy }; + policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized }; + + // 类型转换 + policyCopy.AutoRename = policyCopy.AutoRename === "true"; + policyCopy.IsPrivate = policyCopy.IsPrivate === "true"; + policyCopy.IsOriginLinkEnable = + policyCopy.IsOriginLinkEnable === "true"; + policyCopy.MaxSize = parseInt(policyCopy.MaxSize); + policyCopy.OptionsSerialized.file_type = policyCopy.OptionsSerialized.file_type.split( + "," + ); + if ( + policyCopy.OptionsSerialized.file_type.length === 1 && + policyCopy.OptionsSerialized.file_type[0] === "" + ) { + policyCopy.OptionsSerialized.file_type = []; + } + + API.post("/admin/policy", { + policy: policyCopy, + }) + .then(() => { + ToggleSnackbar( + "top", + "right", + "存储策略已" + (props.policy ? "保存" : "添加"), + "success" + ); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + + setLoading(false); + }; + + return ( +
+ 编辑存储策略 + +
+ + + + 设置项 + + 描述 + + + + + + ID + + {policy.ID} + 存储策略编号 + + + + 类型 + + {policy.Type} + 存储策略类型 + + + + 名称 + + + + + + + 存储策名称 + + + + Server + + + + + + + 存储端 Endpoint + + + + BucketName + + + + + + + 存储桶标识 + + + + 私有空间 + + + + + + } + label="是" + /> + + } + label="否" + /> + + + + 是否为私有空间 + + + + 文件资源根URL + + + + + + + + 预览/获取文件外链时生成URL的前缀 + + + + + AccessKey + + + + + + + AccessKey / 刷新Token + + + + SecretKey + + + + + + + SecretKey + + + + 最大单文件尺寸 (Bytes) + + + + + + + + 最大可上传的文件尺寸,填写为0表示不限制 + + + + + 自动重命名 + + + + + + } + label="是" + /> + + } + label="否" + /> + + + + + 是否根据规则对上传物理文件重命名 + + + + + 存储路径 + + + + + + + 文件物理存储路径 + + + + 存储文件名 + + + + + + + 文件物理存储文件名 + + + + 允许获取外链 + + + + + + } + label="是" + /> + + } + label="否" + /> + + + + + 是否允许获取外链。注意,某些存储策略类型不支持,即使在此开启,获取的外链也无法使用。 + + + + + 又拍云防盗链 Token + + + + + + + 仅对又拍云存储策略有效 + + + + 允许文件扩展名 + + + + + + + 留空表示不限制 + + + + 允许的 MimeType + + + + + + + 仅对七牛存储策略有效 + + + + OneDrive 重定向地址 + + + + + + + 一般添加后无需修改 + + + + OneDrive 反代服务器地址 + + + + + + + + 仅对 OneDrive 存储策略有效 + + + + + OneDrive/SharePoint 驱动器资源标识 + + + + + + + + 仅对 OneDrive + 存储策略有效,留空则使用用户的默认 OneDrive + 驱动器 + + + + + Amazon S3 Region + + + + + + + + 仅对 Amazon S3 存储策略有效 + + + + + 内网 EndPoint + + + + + + + 仅对 OSS 存储策略有效 + + +
+ +
+
+
+ ); +} diff --git a/assets/src/component/Admin/Policy/Guid/LocalGuide.js b/assets/src/component/Admin/Policy/Guid/LocalGuide.js new file mode 100644 index 00000000..8d541fa8 --- /dev/null +++ b/assets/src/component/Admin/Policy/Guid/LocalGuide.js @@ -0,0 +1,776 @@ +import Button from "@material-ui/core/Button"; +import Collapse from "@material-ui/core/Collapse"; +import FormControl from "@material-ui/core/FormControl"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Input from "@material-ui/core/Input"; +import InputLabel from "@material-ui/core/InputLabel"; +import Link from "@material-ui/core/Link"; +import Radio from "@material-ui/core/Radio"; +import RadioGroup from "@material-ui/core/RadioGroup"; +import Step from "@material-ui/core/Step"; +import StepLabel from "@material-ui/core/StepLabel"; +import Stepper from "@material-ui/core/Stepper"; +import { lighten, makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import React, { useCallback, useState } from "react"; +import { useDispatch } from "react-redux"; +import { useHistory } from "react-router"; +import { toggleSnackbar } from "../../../../actions"; +import API from "../../../../middleware/Api"; +import DomainInput from "../../Common/DomainInput"; +import SizeInput from "../../Common/SizeInput"; +import MagicVar from "../../Dialogs/MagicVar"; + +const useStyles = makeStyles((theme) => ({ + stepContent: { + padding: "16px 32px 16px 32px", + }, + form: { + maxWidth: 400, + marginTop: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, + subStepContainer: { + display: "flex", + marginBottom: 20, + padding: 10, + transition: theme.transitions.create("background-color", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + "&:focus-within": { + backgroundColor: theme.palette.background.default, + }, + }, + stepNumber: { + width: 20, + height: 20, + backgroundColor: lighten(theme.palette.secondary.light, 0.2), + color: theme.palette.secondary.contrastText, + textAlign: "center", + borderRadius: " 50%", + }, + stepNumberContainer: { + marginRight: 10, + }, + stepFooter: { + marginTop: 32, + }, + button: { + marginRight: theme.spacing(1), + }, +})); + +const steps = [ + { + title: "上传路径", + optional: false, + }, + { + title: "直链设置", + optional: false, + }, + { + title: "上传限制", + optional: false, + }, + { + title: "完成", + optional: false, + }, +]; + +export default function LocalGuide(props) { + const classes = useStyles(); + const history = useHistory(); + + const [activeStep, setActiveStep] = useState(0); + const [loading, setLoading] = useState(false); + const [skipped] = React.useState(new Set()); + const [magicVar, setMagicVar] = useState(""); + const [useCDN, setUseCDN] = useState("false"); + const [policy, setPolicy] = useState( + props.policy + ? props.policy + : { + Type: "local", + Name: "", + DirNameRule: "uploads/{uid}/{path}", + AutoRename: "true", + FileNameRule: "{randomkey8}_{originname}", + IsOriginLinkEnable: "false", + BaseURL: "", + IsPrivate: "true", + MaxSize: "0", + OptionsSerialized: { + file_type: "", + }, + } + ); + + const handleChange = (name) => (event) => { + setPolicy({ + ...policy, + [name]: event.target.value, + }); + }; + + const handleOptionChange = (name) => (event) => { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + [name]: event.target.value, + }, + }); + }; + + const isStepSkipped = (step) => { + return skipped.has(step); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const checkPathSetting = (e) => { + e.preventDefault(); + setLoading(true); + + // 测试路径是否可用 + API.post("/admin/policy/test/path", { + path: policy.DirNameRule, + }) + .then(() => { + setActiveStep(1); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const submitPolicy = (e) => { + e.preventDefault(); + setLoading(true); + + const policyCopy = { ...policy }; + policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized }; + + // 处理存储策略 + if (useCDN === "false" || policy.IsOriginLinkEnable === "false") { + policyCopy.BaseURL = ""; + } + + // 类型转换 + policyCopy.AutoRename = policyCopy.AutoRename === "true"; + policyCopy.IsOriginLinkEnable = + policyCopy.IsOriginLinkEnable === "true"; + policyCopy.MaxSize = parseInt(policyCopy.MaxSize); + policyCopy.IsPrivate = policyCopy.IsPrivate === "true"; + policyCopy.OptionsSerialized.file_type = policyCopy.OptionsSerialized.file_type.split( + "," + ); + if ( + policyCopy.OptionsSerialized.file_type.length === 1 && + policyCopy.OptionsSerialized.file_type[0] === "" + ) { + policyCopy.OptionsSerialized.file_type = []; + } + + API.post("/admin/policy", { + policy: policyCopy, + }) + .then(() => { + ToggleSnackbar( + "top", + "right", + "存储策略已" + (props.policy ? "保存" : "添加"), + "success" + ); + setActiveStep(4); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + + setLoading(false); + }; + + return ( +
+ + {props.policy ? "修改" : "添加"}本机存储策略 + + + {steps.map((label, index) => { + const stepProps = {}; + const labelProps = {}; + if (label.optional) { + labelProps.optional = ( + 可选 + ); + } + if (isStepSkipped(index)) { + stepProps.completed = false; + } + return ( + + {label.title} + + ); + })} + + {activeStep === 0 && ( +
+
+
+
1
+
+
+ + 请在下方输入文件的存储目录路径,可以为绝对路径或相对路径(相对于 + Cloudreve)。路径中可以使用魔法变量,文件在上传时会自动替换这些变量为相应值; + 可用魔法变量可参考{" "} + { + e.preventDefault(); + setMagicVar("path"); + }} + > + 路径魔法变量列表 + {" "} + 。 + +
+ + + 存储目录 + + + +
+
+
+ +
+
+
2
+
+
+ + 是否需要对存储的物理文件进行重命名?此处的重命名不会影响最终呈现给用户的 + 文件名。文件名也可使用魔法变量, + 可用魔法变量可参考{" "} + { + e.preventDefault(); + setMagicVar("file"); + }} + > + 文件名魔法变量列表 + {" "} + 。 + +
+ + + + } + label="开启重命名" + /> + + } + label="不开启" + /> + + +
+ + +
+ + + 命名规则 + + + +
+
+
+
+ +
+ +
+
+ )} + + {activeStep === 1 && ( +
{ + e.preventDefault(); + setActiveStep(2); + }} + > +
+
+
1
+
+
+ + 是否允许获取文件永久直链? +
+ 开启后,用户可以请求获得能直接访问到文件内容的直链,适用于图床应用或自用。 +
+ +
+ + + + } + label="允许" + /> + + } + label="禁止" + /> + + +
+
+
+ + +
+
+
2
+
+
+ + 是否要对下载/直链使用 CDN? +
+ 开启后,用户访问文件时的 URL + 中的域名部分会被替换为 CDN 域名。 +
+ +
+ + { + if ( + e.target.value === "false" + ) { + setPolicy({ + ...policy, + BaseURL: "", + }); + } + setUseCDN(e.target.value); + }} + row + > + + } + label="使用" + /> + + } + label="不使用" + /> + + +
+
+
+ + +
+
+
3
+
+
+ + 选择协议并填写 CDN 域名 + + +
+ +
+
+
+
+
+ +
+ {" "} + +
+
+ )} + + {activeStep === 2 && ( +
{ + e.preventDefault(); + setActiveStep(3); + }} + > +
+
+
1
+
+
+ + 是否限制上传的单文件大小? + + +
+ + { + if (e.target.value === "true") { + setPolicy({ + ...policy, + MaxSize: "10485760", + }); + } else { + setPolicy({ + ...policy, + MaxSize: "0", + }); + } + }} + row + > + + } + label="限制" + /> + + } + label="不限制" + /> + + +
+
+
+ + +
+
+
2
+
+
+ + 输入限制: + +
+ +
+
+
+
+ +
+
+
+ {policy.MaxSize !== "0" ? "3" : "2"} +
+
+
+ + 是否限制上传文件扩展名? + + +
+ + { + if (e.target.value === "true") { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + file_type: + "jpg,png,mp4,zip,rar", + }, + }); + } else { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + file_type: "", + }, + }); + } + }} + row + > + + } + label="限制" + /> + + } + label="不限制" + /> + + +
+
+
+ + +
+
+
+ {policy.MaxSize !== "0" ? "4" : "3"} +
+
+
+ + 输入允许上传的文件扩展名,多个请以半角逗号 , + 隔开 + +
+ + + 扩展名列表 + + + +
+
+
+
+ +
+ {" "} + +
+
+ )} + + {activeStep === 3 && ( +
+
+
+
+ + 最后一步,为此存储策略命名: + +
+ + + 存储策略名 + + + +
+
+
+
+ {" "} + +
+
+ )} + + {activeStep === 4 && ( + <> +
+ + 存储策略已{props.policy ? "保存" : "添加"}! + + + 要使用此存储策略,请到用户组管理页面,为相应用户组绑定此存储策略。 + +
+
+ +
+ + )} + + setMagicVar("")} + /> + setMagicVar("")} + /> +
+ ); +} diff --git a/assets/src/component/Admin/Policy/Guid/OSSGuide.js b/assets/src/component/Admin/Policy/Guid/OSSGuide.js new file mode 100644 index 00000000..c07ffd65 --- /dev/null +++ b/assets/src/component/Admin/Policy/Guid/OSSGuide.js @@ -0,0 +1,1128 @@ +import Button from "@material-ui/core/Button"; +import Collapse from "@material-ui/core/Collapse"; +import FormControl from "@material-ui/core/FormControl"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Input from "@material-ui/core/Input"; +import InputLabel from "@material-ui/core/InputLabel"; +import Link from "@material-ui/core/Link"; +import Radio from "@material-ui/core/Radio"; +import RadioGroup from "@material-ui/core/RadioGroup"; +import Step from "@material-ui/core/Step"; +import StepLabel from "@material-ui/core/StepLabel"; +import Stepper from "@material-ui/core/Stepper"; +import { lighten, makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import React, { useCallback, useState } from "react"; +import { useDispatch } from "react-redux"; +import { useHistory } from "react-router"; +import { toggleSnackbar } from "../../../../actions"; +import API from "../../../../middleware/Api"; +import { getNumber } from "../../../../utils"; +import DomainInput from "../../Common/DomainInput"; +import SizeInput from "../../Common/SizeInput"; +import MagicVar from "../../Dialogs/MagicVar"; + +const useStyles = makeStyles((theme) => ({ + stepContent: { + padding: "16px 32px 16px 32px", + }, + form: { + maxWidth: 400, + marginTop: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, + subStepContainer: { + display: "flex", + marginBottom: 20, + padding: 10, + transition: theme.transitions.create("background-color", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + "&:focus-within": { + backgroundColor: theme.palette.background.default, + }, + }, + stepNumber: { + width: 20, + height: 20, + backgroundColor: lighten(theme.palette.secondary.light, 0.2), + color: theme.palette.secondary.contrastText, + textAlign: "center", + borderRadius: " 50%", + }, + stepNumberContainer: { + marginRight: 10, + }, + stepFooter: { + marginTop: 32, + }, + button: { + marginRight: theme.spacing(1), + }, + viewButtonLabel: { textTransform: "none" }, + "@global": { + code: { + color: "rgba(0, 0, 0, 0.87)", + display: "inline-block", + padding: "2px 6px", + fontFamily: + ' Consolas, "Liberation Mono", Menlo, Courier, monospace', + borderRadius: "2px", + backgroundColor: "rgba(255,229,100,0.1)", + }, + }, +})); + +const steps = [ + { + title: "存储空间", + optional: false, + }, + { + title: "上传路径", + optional: false, + }, + { + title: "直链设置", + optional: false, + }, + { + title: "上传限制", + optional: false, + }, + { + title: "跨域策略", + optional: true, + }, + { + title: "完成", + optional: false, + }, +]; + +export default function OSSGuide(props) { + const classes = useStyles(); + const history = useHistory(); + + const [activeStep, setActiveStep] = useState(0); + const [loading, setLoading] = useState(false); + const [skipped, setSkipped] = React.useState(new Set()); + const [magicVar, setMagicVar] = useState(""); + const [useCDN, setUseCDN] = useState("false"); + const [useLanEndpoint, setUseLanEndpoint] = useState( + props.policy && props.policy.OptionsSerialized.server_side_endpoint + ? props.policy.OptionsSerialized.server_side_endpoint !== "" + : false + ); + const [policy, setPolicy] = useState( + props.policy + ? props.policy + : { + Type: "oss", + Name: "", + SecretKey: "", + AccessKey: "", + BaseURL: "", + Server: "", + IsPrivate: "true", + DirNameRule: "uploads/{year}/{month}/{day}", + AutoRename: "true", + FileNameRule: "{randomkey8}_{originname}", + IsOriginLinkEnable: "false", + MaxSize: "0", + OptionsSerialized: { + file_type: "", + server_side_endpoint: "", + }, + } + ); + const [policyID, setPolicyID] = useState( + props.policy ? props.policy.ID : 0 + ); + + const handleChange = (name) => (event) => { + setPolicy({ + ...policy, + [name]: event.target.value, + }); + }; + + const handleOptionChange = (name) => (event) => { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + [name]: event.target.value, + }, + }); + }; + + const isStepSkipped = (step) => { + return skipped.has(step); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const submitPolicy = (e) => { + e.preventDefault(); + setLoading(true); + + const policyCopy = { ...policy }; + policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized }; + + if (useCDN === "false") { + policyCopy.BaseURL = ""; + } + + if (!useLanEndpoint) { + policyCopy.OptionsSerialized.server_side_endpoint = ""; + } + + // 类型转换 + policyCopy.AutoRename = policyCopy.AutoRename === "true"; + policyCopy.IsOriginLinkEnable = + policyCopy.IsOriginLinkEnable === "true"; + policyCopy.IsPrivate = policyCopy.IsPrivate === "true"; + policyCopy.MaxSize = parseInt(policyCopy.MaxSize); + policyCopy.OptionsSerialized.file_type = policyCopy.OptionsSerialized.file_type.split( + "," + ); + if ( + policyCopy.OptionsSerialized.file_type.length === 1 && + policyCopy.OptionsSerialized.file_type[0] === "" + ) { + policyCopy.OptionsSerialized.file_type = []; + } + + API.post("/admin/policy", { + policy: policyCopy, + }) + .then((response) => { + ToggleSnackbar( + "top", + "right", + "存储策略已" + (props.policy ? "保存" : "添加"), + "success" + ); + setActiveStep(4); + setPolicyID(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + + setLoading(false); + }; + + const createCORS = () => { + setLoading(true); + API.post("/admin/policy/cors", { + id: policyID, + }) + .then(() => { + ToggleSnackbar("top", "right", "跨域策略已添加", "success"); + setActiveStep(5); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + return ( +
+ + {props.policy ? "修改" : "添加"} 阿里云 OSS 存储策略 + + + {steps.map((label, index) => { + const stepProps = {}; + const labelProps = {}; + if (label.optional) { + labelProps.optional = ( + 可选 + ); + } + if (isStepSkipped(index)) { + stepProps.completed = false; + } + return ( + + {label.title} + + ); + })} + + + {activeStep === 0 && ( +
{ + e.preventDefault(); + setActiveStep(1); + }} + > +
+
+
0
+
+
+ + 在使用 阿里云 OSS 储策略前,请确保您在 参数设置 + - 站点信息 - 站点URL 中填写的 + 地址与实际相符,并且 + 能够被外网正常访问。 + +
+
+ +
+
+
1
+
+
+ + 前往 + + OSS 管理控制台 + + 创建 Bucket。注意:创建空间类型只能选择 + 标准存储低频访问 + ,暂不支持归档存储 + +
+
+ +
+
+
2
+
+
+ + 在下方填写您创建 Bucket 时指定的 + Bucket 名称: + +
+ + + Bucket 名称 + + + +
+
+
+ +
+
+
3
+
+
+ + 在下方选择您创建的空间的读写权限类型,推荐选择“私有”以获得更高的安全性,私有空间无法开启“获取直链”功能。 + +
+ + + + } + label="私有" + /> + + } + label="公共读" + /> + + +
+
+
+ +
+
+
4
+
+
+ + 转到所创建 Bucket 的概览页面,填写 + 访问域名栏目下 + 外网访问 一行中间的{" "} + EndPoint(地域节点) + +
+ + + EndPoint + + [\\w\\-]*)(?:\\.))?(?[\\w\\-]*))\\.(?[\\w\\-]*)", + title: + "格式不合法,只需输入域名部分即可", + }} + /> + +
+
+
+ +
+
+
5
+
+
+ + 如果您的 Cloudreve + 部署在阿里云计算服务中,并且与 OSS + 处在同一可用区下,您可以额外指定使用内网 + EndPoint + 以节省流量开始。是否要在服务端发送请求时使用 OSS + 内网 EndPoint? + +
+ + { + setUseLanEndpoint( + e.target.value === "true" + ); + }} + row + > + + } + label="使用" + /> + + } + label="不使用" + /> + + +
+ +
+ + + 内网 EndPoint + + [\\w\\-]*)(?:\\.))?(?[\\w\\-]*))\\.(?[\\w\\-]*)", + title: + "格式不合法,只需输入域名部分即可", + }} + /> + +
+
+
+
+ +
+
+
6
+
+
+ + 是否要使用配套的 阿里云CDN 加速 OSS 访问? + +
+ + { + setUseCDN(e.target.value); + }} + row + > + + } + label="使用" + /> + + } + label="不使用" + /> + + +
+
+
+ + +
+
+
7
+
+
+ + 前往 + + 阿里云 CDN 管理控制台 + + 创建 CDN 加速域名,并设定源站为刚创建的 OSS + Bucket。在下方填写 CDN + 加速域名,并选择是否使用 HTTPS: + +
+ +
+
+
+
+ +
+
+
+ {getNumber(7, [useCDN === "true"])} +
+
+
+ + 在阿里云 + + 安全信息管理 + + 页面获取 用户 AccessKey,并填写在下方。 + +
+ + + AccessKey ID + + + +
+
+ + + Access Key Secret + + + +
+
+
+ +
+
+
+ {getNumber(8, [useCDN === "true"])} +
+
+
+ + 为此存储策略命名: + +
+ + + 存储策略名 + + + +
+
+
+ +
+ +
+
+ )} + + {activeStep === 1 && ( +
{ + e.preventDefault(); + setActiveStep(2); + }} + > +
+
+
1
+
+
+ + 请在下方输入文件的存储目录路径,可以为绝对路径或相对路径(相对于 + 从机的 + Cloudreve)。路径中可以使用魔法变量,文件在上传时会自动替换这些变量为相应值; + 可用魔法变量可参考{" "} + { + e.preventDefault(); + setMagicVar("path"); + }} + > + 路径魔法变量列表 + {" "} + 。 + +
+ + + 存储目录 + + + +
+
+
+ +
+
+
2
+
+
+ + 是否需要对存储的物理文件进行重命名?此处的重命名不会影响最终呈现给用户的 + 文件名。文件名也可使用魔法变量, + 可用魔法变量可参考{" "} + { + e.preventDefault(); + setMagicVar("file"); + }} + > + 文件名魔法变量列表 + {" "} + 。 + +
+ + + + } + label="开启重命名" + /> + + } + label="不开启" + /> + + +
+ + +
+ + + 命名规则 + + + +
+
+
+
+ +
+ + +
+
+ )} + + {activeStep === 2 && ( +
{ + e.preventDefault(); + setActiveStep(3); + }} + > +
+
+
1
+
+
+ + 是否允许获取文件永久直链? +
+ 开启后,用户可以请求获得能直接访问到文件内容的直链,适用于图床应用或自用。 +
+ +
+ + { + if ( + policy.IsPrivate === "true" && + e.target.value === "true" + ) { + ToggleSnackbar( + "top", + "right", + "私有空间无法开启此功能", + "warning" + ); + return; + } + handleChange("IsOriginLinkEnable")( + e + ); + }} + row + > + + } + label="允许" + /> + + } + label="禁止" + /> + + +
+
+
+ +
+ {" "} + +
+
+ )} + + {activeStep === 3 && ( +
+
+
+
1
+
+
+ + 是否限制上传的单文件大小? + + +
+ + { + if (e.target.value === "true") { + setPolicy({ + ...policy, + MaxSize: "10485760", + }); + } else { + setPolicy({ + ...policy, + MaxSize: "0", + }); + } + }} + row + > + + } + label="限制" + /> + + } + label="不限制" + /> + + +
+
+
+ + +
+
+
2
+
+
+ + 输入限制: + +
+ +
+
+
+
+ +
+
+
+ {policy.MaxSize !== "0" ? "3" : "2"} +
+
+
+ + 是否限制上传文件扩展名? + + +
+ + { + if (e.target.value === "true") { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + file_type: + "jpg,png,mp4,zip,rar", + }, + }); + } else { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + file_type: "", + }, + }); + } + }} + row + > + + } + label="限制" + /> + + } + label="不限制" + /> + + +
+
+
+ + +
+
+
+ {policy.MaxSize !== "0" ? "4" : "3"} +
+
+
+ + 输入允许上传的文件扩展名,多个请以半角逗号 , + 隔开 + +
+ + + 扩展名列表 + + + +
+
+
+
+ +
+ {" "} + +
+
+ )} + + {activeStep === 4 && ( +
+
+
+
+ + OSS Bucket 需要正确配置跨域策略后才能使用 Web + 端上传文件,Cloudreve + 可以帮您自动设置,您也可以参考文档步骤手动设置。如果您已设置过此 + Bucket 的跨域策略,此步骤可以跳过。 + +
+ +
+
+
+
+ {" "} +
+ + )} + + {activeStep === 5 && ( + <> +
+ + 存储策略已{props.policy ? "保存" : "添加"}! + + + 要使用此存储策略,请到用户组管理页面,为相应用户组绑定此存储策略。 + +
+
+ +
+ + )} + + setMagicVar("")} + /> + setMagicVar("")} + /> +
+ ); +} diff --git a/assets/src/component/Admin/Policy/Guid/OneDriveGuide.js b/assets/src/component/Admin/Policy/Guid/OneDriveGuide.js new file mode 100644 index 00000000..f259db78 --- /dev/null +++ b/assets/src/component/Admin/Policy/Guid/OneDriveGuide.js @@ -0,0 +1,1093 @@ +import Button from "@material-ui/core/Button"; +import Collapse from "@material-ui/core/Collapse"; +import FormControl from "@material-ui/core/FormControl"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Input from "@material-ui/core/Input"; +import InputLabel from "@material-ui/core/InputLabel"; +import Link from "@material-ui/core/Link"; +import Radio from "@material-ui/core/Radio"; +import RadioGroup from "@material-ui/core/RadioGroup"; +import Step from "@material-ui/core/Step"; +import StepLabel from "@material-ui/core/StepLabel"; +import Stepper from "@material-ui/core/Stepper"; +import { lighten, makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { useHistory } from "react-router"; +import { toggleSnackbar } from "../../../../actions"; +import API from "../../../../middleware/Api"; +import SizeInput from "../../Common/SizeInput"; +import AlertDialog from "../../Dialogs/Alert"; +import MagicVar from "../../Dialogs/MagicVar"; +import DomainInput from "../../Common/DomainInput"; + +const useStyles = makeStyles((theme) => ({ + stepContent: { + padding: "16px 32px 16px 32px", + }, + form: { + maxWidth: 400, + marginTop: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, + subStepContainer: { + display: "flex", + marginBottom: 20, + padding: 10, + transition: theme.transitions.create("background-color", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + "&:focus-within": { + backgroundColor: theme.palette.background.default, + }, + }, + stepNumber: { + width: 20, + height: 20, + backgroundColor: lighten(theme.palette.secondary.light, 0.2), + color: theme.palette.secondary.contrastText, + textAlign: "center", + borderRadius: " 50%", + }, + stepNumberContainer: { + marginRight: 10, + }, + stepFooter: { + marginTop: 32, + }, + button: { + marginRight: theme.spacing(1), + }, + viewButtonLabel: { textTransform: "none" }, + "@global": { + code: { + color: "rgba(0, 0, 0, 0.87)", + display: "inline-block", + padding: "2px 6px", + fontFamily: + ' Consolas, "Liberation Mono", Menlo, Courier, monospace', + borderRadius: "2px", + backgroundColor: "rgba(255,229,100,0.1)", + }, + }, +})); + +const steps = [ + { + title: "应用授权", + optional: false, + }, + { + title: "上传路径", + optional: false, + }, + { + title: "直链设置", + optional: false, + }, + { + title: "上传限制", + optional: false, + }, + { + title: "账号授权", + optional: false, + }, + { + title: "完成", + optional: false, + }, +]; + +export default function OneDriveGuide(props) { + const classes = useStyles(); + const history = useHistory(); + + const [activeStep, setActiveStep] = useState(0); + const [loading, setLoading] = useState(false); + const [skipped] = React.useState(new Set()); + const [magicVar, setMagicVar] = useState(""); + const [useCDN, setUseCDN] = useState( + props.policy && props.policy.OptionsSerialized.od_proxy + ? props.policy.OptionsSerialized.od_proxy !== "" + : false + ); + const [useSharePoint, setUseSharePoint] = useState( + props.policy && props.policy.OptionsSerialized.od_driver + ? props.policy.OptionsSerialized.od_driver !== "" + : false + ); + const [policy, setPolicy] = useState( + props.policy + ? props.policy + : { + Type: "onedrive", + Name: "", + BucketName: "", + SecretKey: "", + AccessKey: "", + BaseURL: "", + Server: "https://graph.microsoft.com/v1.0", + IsPrivate: "true", + DirNameRule: "uploads/{year}/{month}/{day}", + AutoRename: "true", + FileNameRule: "{randomkey8}_{originname}", + IsOriginLinkEnable: "false", + MaxSize: "0", + OptionsSerialized: { + file_type: "", + od_redirect: "", + od_proxy: "", + od_driver: "", + }, + } + ); + const [policyID, setPolicyID] = useState( + props.policy ? props.policy.ID : 0 + ); + const [httpsAlert, setHttpsAlert] = useState(false); + + const handleChange = (name) => (event) => { + setPolicy({ + ...policy, + [name]: event.target.value, + }); + }; + + const handleOptionChange = (name) => (event) => { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + [name]: event.target.value, + }, + }); + }; + + const isStepSkipped = (step) => { + return skipped.has(step); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + useEffect(() => { + API.post("/admin/setting", { + keys: ["siteURL"], + }) + .then((response) => { + if (!response.data.siteURL.startsWith("https://")) { + setHttpsAlert(true); + } + if (policy.OptionsSerialized.od_redirect === "") { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + od_redirect: new URL( + "/api/v3/callback/onedrive/auth", + response.data.siteURL + ).toString(), + }, + }); + } + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }, []); + + const statOAuth = () => { + setLoading(true); + API.get("/admin/policy/" + policyID + "/oauth") + .then((response) => { + window.location.href = response.data; + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + setLoading(false); + }); + }; + + const submitPolicy = (e) => { + e.preventDefault(); + setLoading(true); + + const policyCopy = { ...policy }; + policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized }; + + // baseURL处理 + if (policyCopy.Server === "https://graph.microsoft.com/v1.0") { + policyCopy.BaseURL = + "https://login.microsoftonline.com/common/oauth2/v2.0"; + } else { + policyCopy.BaseURL = "https://login.chinacloudapi.cn/common/oauth2"; + } + + if (!useCDN) { + policyCopy.OptionsSerialized.od_proxy = ""; + } + + if (!useSharePoint) { + policyCopy.OptionsSerialized.od_driver = ""; + } + + // 类型转换 + policyCopy.AutoRename = policyCopy.AutoRename === "true"; + policyCopy.IsOriginLinkEnable = + policyCopy.IsOriginLinkEnable === "true"; + policyCopy.IsPrivate = policyCopy.IsPrivate === "true"; + policyCopy.MaxSize = parseInt(policyCopy.MaxSize); + policyCopy.OptionsSerialized.file_type = policyCopy.OptionsSerialized.file_type.split( + "," + ); + if ( + policyCopy.OptionsSerialized.file_type.length === 1 && + policyCopy.OptionsSerialized.file_type[0] === "" + ) { + policyCopy.OptionsSerialized.file_type = []; + } + + API.post("/admin/policy", { + policy: policyCopy, + }) + .then((response) => { + ToggleSnackbar( + "top", + "right", + "存储策略已" + (props.policy ? "保存" : "添加"), + "success" + ); + setActiveStep(4); + setPolicyID(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + + setLoading(false); + }; + + return ( +
+ setHttpsAlert(false)} + title={"警告"} + msg={ + "您必须启用 HTTPS 才能使用 OneDrive/SharePoint 存储策略;启用后同步更改 参数设置 - 站点信息 - 站点URL。" + } + /> + + {props.policy ? "修改" : "添加"} OneDrive/SharePoint 存储策略 + + + {steps.map((label, index) => { + const stepProps = {}; + const labelProps = {}; + if (label.optional) { + labelProps.optional = ( + 可选 + ); + } + if (isStepSkipped(index)) { + stepProps.completed = false; + } + return ( + + {label.title} + + ); + })} + + + {activeStep === 0 && ( +
{ + e.preventDefault(); + setActiveStep(1); + }} + > +
+
+
1
+
+
+ + 前往 + + Azure Active Directory 控制台 (国际版账号) + {" "} + 或者{" "} + + Azure Active Directory 控制台 (世纪互联账号) + + 并登录,登录后进入 + Azure Active Directory管理面板。 + +
+
+ +
+
+
2
+
+
+ + 进入左侧 应用注册 菜单,并点击{" "} + 新注册 按钮。 + +
+
+ +
+
+
3
+
+
+ + 填写应用注册表单。其中,名称可任取; + 受支持的帐户类型 选择为 + + 任何组织目录(任何 Azure AD 目录 - + 多租户)中的帐户 + + ;重定向 URI (可选) + 请选择Web,并填写 + + {policy.OptionsSerialized.od_redirect} + + ; 其他保持默认即可 + +
+
+ +
+
+
4
+
+
+ + 创建完成后进入应用管理的概览 + 页面,复制应用程序(客户端) ID + 并填写在下方: + +
+ + + 应用程序(客户端) ID + + + +
+
+
+ +
+
+
5
+
+
+ + 进入应用管理页面左侧的证书和密码 + 菜单,点击 + 新建客户端密码 + 按钮,截止期限选择为 + 从不 + 。创建完成后将客户端密码的值填写在下方: + +
+ + + 客户端密码 + + + +
+
+
+ +
+
+
6
+
+
+ + 选择您的 Microsoft 365 账号类型: + +
+ + + + } + label="国际版" + /> + + } + label="世纪互联版" + /> + + +
+
+
+ +
+
+
7
+
+
+ + 是否将文件存放在 SharePoint 中? + +
+ + { + setUseSharePoint( + e.target.value === "true" + ); + }} + row + > + + } + label="存到指定 SharePoint 中" + /> + + } + label="存到账号默认 OneDrive 驱动器中" + /> + + +
+ +
+ + + SharePoint 站点地址 + + + +
+
+
+
+ +
+
+
8
+
+
+ + 是否要在文件下载时替换为使用自建的反代服务器? + +
+ + { + setUseCDN( + e.target.value === "true" + ); + }} + row + > + + } + label="使用" + /> + + } + label="不使用" + /> + + +
+ +
+ + + +
+
+
+
+ +
+
+
9
+
+
+ + 为此存储策略命名: + +
+ + + 存储策略名 + + + +
+
+
+ +
+ +
+
+ )} + + {activeStep === 1 && ( +
{ + e.preventDefault(); + setActiveStep(2); + }} + > +
+
+
1
+
+
+ + 请在下方输入文件的存储目录路径,可以为绝对路径或相对路径(相对于 + 从机的 + Cloudreve)。路径中可以使用魔法变量,文件在上传时会自动替换这些变量为相应值; + 可用魔法变量可参考{" "} + { + e.preventDefault(); + setMagicVar("path"); + }} + > + 路径魔法变量列表 + {" "} + 。 + +
+ + + 存储目录 + + + +
+
+
+ +
+
+
2
+
+
+ + 是否需要对存储的物理文件进行重命名?此处的重命名不会影响最终呈现给用户的 + 文件名。文件名也可使用魔法变量, + 可用魔法变量可参考{" "} + { + e.preventDefault(); + setMagicVar("file"); + }} + > + 文件名魔法变量列表 + {" "} + 。 + +
+ + + + } + label="开启重命名" + /> + + } + label="不开启" + /> + + +
+ + +
+ + + 命名规则 + + + +
+
+
+
+ +
+ + +
+
+ )} + + {activeStep === 2 && ( +
{ + e.preventDefault(); + setActiveStep(3); + }} + > +
+
+
1
+
+
+ + 是否允许获取文件永久直链? +
+ 开启后,用户可以请求获得能直接访问到文件内容的直链,适用于图床应用或自用。 +
+ +
+ + { + handleChange("IsOriginLinkEnable")( + e + ); + }} + row + > + + } + label="允许" + /> + + } + label="禁止" + /> + + +
+
+
+ +
+ {" "} + +
+
+ )} + + {activeStep === 3 && ( +
+
+
+
1
+
+
+ + 是否限制上传的单文件大小? + + +
+ + { + if (e.target.value === "true") { + setPolicy({ + ...policy, + MaxSize: "10485760", + }); + } else { + setPolicy({ + ...policy, + MaxSize: "0", + }); + } + }} + row + > + + } + label="限制" + /> + + } + label="不限制" + /> + + +
+
+
+ + +
+
+
2
+
+
+ + 输入限制: + +
+ +
+
+
+
+ +
+
+
+ {policy.MaxSize !== "0" ? "3" : "2"} +
+
+
+ + 是否限制上传文件扩展名? + + +
+ + { + if (e.target.value === "true") { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + file_type: + "jpg,png,mp4,zip,rar", + }, + }); + } else { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + file_type: "", + }, + }); + } + }} + row + > + + } + label="限制" + /> + + } + label="不限制" + /> + + +
+
+
+ + +
+
+
+ {policy.MaxSize !== "0" ? "4" : "3"} +
+
+
+ + 输入允许上传的文件扩展名,多个请以半角逗号 , + 隔开 + +
+ + + 扩展名列表 + + + +
+
+
+
+ +
+ {" "} + +
+
+ )} + + {activeStep === 4 && ( +
+
+
+
+ + 存储策略已{props.policy ? "保存" : "添加"} + ,但是你需要点击下方按钮,并使用 OneDrive + 登录授权以完成初始化后才能使用。 + 日后你可以在存储策略列表页面重新进行授权。 + +
+ +
+
+
+
+ + )} + + {activeStep === 5 && ( + <> +
+ 存储策略已添加! + + 要使用此存储策略,请到用户组管理页面,为相应用户组绑定此存储策略。 + +
+
+ +
+ + )} + + setMagicVar("")} + /> + setMagicVar("")} + /> +
+ ); +} diff --git a/assets/src/component/Admin/Policy/Guid/QiniuGuide.js b/assets/src/component/Admin/Policy/Guid/QiniuGuide.js new file mode 100644 index 00000000..34bbb8a1 --- /dev/null +++ b/assets/src/component/Admin/Policy/Guid/QiniuGuide.js @@ -0,0 +1,977 @@ +import Button from "@material-ui/core/Button"; +import Collapse from "@material-ui/core/Collapse"; +import FormControl from "@material-ui/core/FormControl"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Input from "@material-ui/core/Input"; +import InputLabel from "@material-ui/core/InputLabel"; +import Link from "@material-ui/core/Link"; +import Radio from "@material-ui/core/Radio"; +import RadioGroup from "@material-ui/core/RadioGroup"; +import Step from "@material-ui/core/Step"; +import StepLabel from "@material-ui/core/StepLabel"; +import Stepper from "@material-ui/core/Stepper"; +import { lighten, makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import React, { useCallback, useState } from "react"; +import { useDispatch } from "react-redux"; +import { useHistory } from "react-router"; +import { toggleSnackbar } from "../../../../actions"; +import API from "../../../../middleware/Api"; +import { getNumber } from "../../../../utils"; +import DomainInput from "../../Common/DomainInput"; +import SizeInput from "../../Common/SizeInput"; +import MagicVar from "../../Dialogs/MagicVar"; + +const useStyles = makeStyles((theme) => ({ + stepContent: { + padding: "16px 32px 16px 32px", + }, + form: { + maxWidth: 400, + marginTop: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, + subStepContainer: { + display: "flex", + marginBottom: 20, + padding: 10, + transition: theme.transitions.create("background-color", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + "&:focus-within": { + backgroundColor: theme.palette.background.default, + }, + }, + stepNumber: { + width: 20, + height: 20, + backgroundColor: lighten(theme.palette.secondary.light, 0.2), + color: theme.palette.secondary.contrastText, + textAlign: "center", + borderRadius: " 50%", + }, + stepNumberContainer: { + marginRight: 10, + }, + stepFooter: { + marginTop: 32, + }, + button: { + marginRight: theme.spacing(1), + }, +})); + +const steps = [ + { + title: "存储空间", + optional: false, + }, + { + title: "上传路径", + optional: false, + }, + { + title: "直链设置", + optional: false, + }, + { + title: "上传限制", + optional: false, + }, + { + title: "完成", + optional: false, + }, +]; + +export default function RemoteGuide(props) { + const classes = useStyles(); + const history = useHistory(); + + const [activeStep, setActiveStep] = useState(0); + const [loading, setLoading] = useState(false); + const [skipped] = React.useState(new Set()); + const [magicVar, setMagicVar] = useState(""); + // const [useCDN, setUseCDN] = useState("false"); + const [policy, setPolicy] = useState( + props.policy + ? props.policy + : { + Type: "qiniu", + Name: "", + SecretKey: "", + AccessKey: "", + BaseURL: "", + IsPrivate: "true", + DirNameRule: "uploads/{year}/{month}/{day}", + AutoRename: "true", + FileNameRule: "{randomkey8}_{originname}", + IsOriginLinkEnable: "false", + MaxSize: "0", + OptionsSerialized: { + file_type: "", + mimetype: "", + }, + } + ); + + const handleChange = (name) => (event) => { + setPolicy({ + ...policy, + [name]: event.target.value, + }); + }; + + const handleOptionChange = (name) => (event) => { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + [name]: event.target.value, + }, + }); + }; + + const isStepSkipped = (step) => { + return skipped.has(step); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const submitPolicy = (e) => { + e.preventDefault(); + setLoading(true); + + const policyCopy = { ...policy }; + policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized }; + + // 类型转换 + policyCopy.AutoRename = policyCopy.AutoRename === "true"; + policyCopy.IsOriginLinkEnable = + policyCopy.IsOriginLinkEnable === "true"; + policyCopy.IsPrivate = policyCopy.IsPrivate === "true"; + policyCopy.MaxSize = parseInt(policyCopy.MaxSize); + policyCopy.OptionsSerialized.file_type = policyCopy.OptionsSerialized.file_type.split( + "," + ); + if ( + policyCopy.OptionsSerialized.file_type.length === 1 && + policyCopy.OptionsSerialized.file_type[0] === "" + ) { + policyCopy.OptionsSerialized.file_type = []; + } + + API.post("/admin/policy", { + policy: policyCopy, + }) + .then(() => { + ToggleSnackbar( + "top", + "right", + "存储策略已" + (props.policy ? "保存" : "添加"), + "success" + ); + setActiveStep(5); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + + setLoading(false); + }; + + return ( +
+ + {props.policy ? "修改" : "添加"} 七牛 存储策略 + + + {steps.map((label, index) => { + const stepProps = {}; + const labelProps = {}; + if (label.optional) { + labelProps.optional = ( + 可选 + ); + } + if (isStepSkipped(index)) { + stepProps.completed = false; + } + return ( + + {label.title} + + ); + })} + + + {activeStep === 0 && ( +
{ + e.preventDefault(); + setActiveStep(1); + }} + > +
+
+
0
+
+
+ + 在使用七牛存储策略前,请确保您在 参数设置 - + 站点信息 - 站点URL 中填写的 地址与实际相符,并且 + 能够被外网正常访问。 + +
+
+ +
+
+
1
+
+
+ + 前往 + + 七牛控制面板 + + 创建对象存储资源。 + +
+
+ +
+
+
2
+
+
+ + 在下方填写您在七牛创建存储空间时指定的“存储空间名称”: + +
+ + + 存储空间名称 + + + +
+
+
+ +
+
+
3
+
+
+ + 在下方选择您创建的空间类型,推荐选择“私有空间”以获得更高的安全性,私有空间无法开启“获取直链”功能。 + +
+ + + + } + label="私有" + /> + + } + label="公有" + /> + + +
+
+
+ +
+
+
4
+
+
+ + 填写您为存储空间绑定的 CDN 加速域名。 + +
+ +
+
+
+ +
+
+
5
+
+
+ + 在七牛控制面板进入 个人中心 - + 密钥管理,在下方填写获得到的 AK、SK。 + +
+ + + AK + + + +
+
+ + + SK + + + +
+
+
+ +
+ +
+
+ )} + + {activeStep === 1 && ( +
{ + e.preventDefault(); + setActiveStep(2); + }} + > +
+
+
1
+
+
+ + 请在下方输入文件的存储目录路径,可以为绝对路径或相对路径(相对于 + 从机的 + Cloudreve)。路径中可以使用魔法变量,文件在上传时会自动替换这些变量为相应值; + 可用魔法变量可参考{" "} + { + e.preventDefault(); + setMagicVar("path"); + }} + > + 路径魔法变量列表 + {" "} + 。 + +
+ + + 存储目录 + + + +
+
+
+ +
+
+
2
+
+
+ + 是否需要对存储的物理文件进行重命名?此处的重命名不会影响最终呈现给用户的 + 文件名。文件名也可使用魔法变量, + 可用魔法变量可参考{" "} + { + e.preventDefault(); + setMagicVar("file"); + }} + > + 文件名魔法变量列表 + {" "} + 。 + +
+ + + + } + label="开启重命名" + /> + + } + label="不开启" + /> + + +
+ + +
+ + + 命名规则 + + + +
+
+
+
+ +
+ + +
+
+ )} + + {activeStep === 2 && ( +
{ + e.preventDefault(); + setActiveStep(3); + }} + > +
+
+
1
+
+
+ + 是否允许获取文件永久直链? +
+ 开启后,用户可以请求获得能直接访问到文件内容的直链,适用于图床应用或自用。 +
+ +
+ + { + if ( + policy.IsPrivate === "true" && + e.target.value === "true" + ) { + ToggleSnackbar( + "top", + "right", + "私有空间无法开启此功能", + "warning" + ); + return; + } + handleChange("IsOriginLinkEnable")( + e + ); + }} + row + > + + } + label="允许" + /> + + } + label="禁止" + /> + + +
+
+
+ +
+ {" "} + +
+
+ )} + + {activeStep === 3 && ( +
{ + e.preventDefault(); + setActiveStep(4); + }} + > +
+
+
1
+
+
+ + 是否限制上传的单文件大小? + + +
+ + { + if (e.target.value === "true") { + setPolicy({ + ...policy, + MaxSize: "10485760", + }); + } else { + setPolicy({ + ...policy, + MaxSize: "0", + }); + } + }} + row + > + + } + label="限制" + /> + + } + label="不限制" + /> + + +
+
+
+ + +
+
+
2
+
+
+ + 输入限制: + +
+ +
+
+
+
+ +
+
+
+ {policy.MaxSize !== "0" ? "3" : "2"} +
+
+
+ + 是否限制上传文件扩展名? + + +
+ + { + if (e.target.value === "true") { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + file_type: + "jpg,png,mp4,zip,rar", + }, + }); + } else { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + file_type: "", + }, + }); + } + }} + row + > + + } + label="限制" + /> + + } + label="不限制" + /> + + +
+
+
+ + +
+
+
+ {policy.MaxSize !== "0" ? "4" : "3"} +
+
+
+ + 输入允许上传的文件扩展名,多个请以半角逗号 , + 隔开 + +
+ + + 扩展名列表 + + + +
+
+
+
+ +
+
+
+ {getNumber(3, [ + policy.MaxSize !== "0", + policy.OptionsSerialized.file_type !== "", + ])} +
+
+
+ + 是否限制上传文件 MimeType? + + +
+ + { + if (e.target.value === "true") { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + mimetype: "image/*", + }, + }); + } else { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + mimetype: "", + }, + }); + } + }} + row + > + + } + label="限制" + /> + + } + label="不限制" + /> + + +
+
+
+ + +
+
+
+ {getNumber(4, [ + policy.MaxSize !== "0", + policy.OptionsSerialized.file_type !== + "", + ])} +
+
+
+ + 输入允许上传的 MimeType,多个请以半角逗号 , + 隔开。七牛服务器会侦测文件内容以判断 + MimeType,再用判断值跟指定值进行匹配,匹配成功则允许上传 + +
+ + + MimeType 列表 + + + +
+
+
+
+ +
+ {" "} + +
+
+ )} + + {activeStep === 4 && ( +
+
+
+
+ + 最后一步,为此存储策略命名: + +
+ + + 存储策略名 + + + +
+
+
+
+ {" "} + +
+
+ )} + + {activeStep === 5 && ( + <> +
+ + 存储策略已{props.policy ? "保存" : "添加"}! + + + 要使用此存储策略,请到用户组管理页面,为相应用户组绑定此存储策略。 + +
+
+ +
+ + )} + + setMagicVar("")} + /> + setMagicVar("")} + /> +
+ ); +} diff --git a/assets/src/component/Admin/Policy/Guid/RemoteGuide.js b/assets/src/component/Admin/Policy/Guid/RemoteGuide.js new file mode 100644 index 00000000..d226b9c8 --- /dev/null +++ b/assets/src/component/Admin/Policy/Guid/RemoteGuide.js @@ -0,0 +1,992 @@ +import Button from "@material-ui/core/Button"; +import Collapse from "@material-ui/core/Collapse"; +import FormControl from "@material-ui/core/FormControl"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Input from "@material-ui/core/Input"; +import InputLabel from "@material-ui/core/InputLabel"; +import Link from "@material-ui/core/Link"; +import Radio from "@material-ui/core/Radio"; +import RadioGroup from "@material-ui/core/RadioGroup"; +import Step from "@material-ui/core/Step"; +import StepLabel from "@material-ui/core/StepLabel"; +import Stepper from "@material-ui/core/Stepper"; +import { lighten, makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import Alert from "@material-ui/lab/Alert"; +import React, { useCallback, useState } from "react"; +import { useDispatch } from "react-redux"; +import { useHistory } from "react-router"; +import { toggleSnackbar } from "../../../../actions"; +import API from "../../../../middleware/Api"; +import { randomStr } from "../../../../utils"; +import DomainInput from "../../Common/DomainInput"; +import SizeInput from "../../Common/SizeInput"; +import MagicVar from "../../Dialogs/MagicVar"; + +const useStyles = makeStyles((theme) => ({ + stepContent: { + padding: "16px 32px 16px 32px", + }, + form: { + maxWidth: 400, + marginTop: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, + subStepContainer: { + display: "flex", + marginBottom: 20, + padding: 10, + transition: theme.transitions.create("background-color", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + "&:focus-within": { + backgroundColor: theme.palette.background.default, + }, + }, + stepNumber: { + width: 20, + height: 20, + backgroundColor: lighten(theme.palette.secondary.light, 0.2), + color: theme.palette.secondary.contrastText, + textAlign: "center", + borderRadius: " 50%", + }, + stepNumberContainer: { + marginRight: 10, + }, + stepFooter: { + marginTop: 32, + }, + button: { + marginRight: theme.spacing(1), + }, + "@global": { + code: { + color: "rgba(0, 0, 0, 0.87)", + display: "inline-block", + padding: "2px 6px", + fontSize: "14px", + fontFamily: + ' Consolas, "Liberation Mono", Menlo, Courier, monospace', + borderRadius: "2px", + backgroundColor: "rgba(255,229,100,0.1)", + }, + pre: { + margin: "24px 0", + padding: "12px 18px", + overflow: "auto", + direction: "ltr", + borderRadius: "4px", + backgroundColor: "#272c34", + color: "#fff", + }, + }, +})); + +const steps = [ + { + title: "存储端配置", + optional: false, + }, + { + title: "上传路径", + optional: false, + }, + { + title: "直链设置", + optional: false, + }, + { + title: "上传限制", + optional: false, + }, + { + title: "完成", + optional: false, + }, +]; + +export default function RemoteGuide(props) { + const classes = useStyles(); + const history = useHistory(); + + const [activeStep, setActiveStep] = useState(0); + const [loading, setLoading] = useState(false); + const [skipped] = React.useState(new Set()); + const [magicVar, setMagicVar] = useState(""); + const [useCDN, setUseCDN] = useState("false"); + const [policy, setPolicy] = useState( + props.policy + ? props.policy + : { + Type: "remote", + Name: "", + Server: "https://example.com:5212", + SecretKey: randomStr(64), + DirNameRule: "uploads/{year}/{month}/{day}", + AutoRename: "true", + FileNameRule: "{randomkey8}_{originname}", + IsOriginLinkEnable: "false", + BaseURL: "", + IsPrivate: "true", + MaxSize: "0", + OptionsSerialized: { + file_type: "", + }, + } + ); + + const handleChange = (name) => (event) => { + setPolicy({ + ...policy, + [name]: event.target.value, + }); + }; + + const handleOptionChange = (name) => (event) => { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + [name]: event.target.value, + }, + }); + }; + + const isStepSkipped = (step) => { + return skipped.has(step); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const testSlave = () => { + setLoading(true); + + // 测试路径是否可用 + API.post("/admin/policy/test/slave", { + server: policy.Server, + secret: policy.SecretKey, + }) + .then(() => { + ToggleSnackbar("top", "right", "通信正常", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const submitPolicy = (e) => { + e.preventDefault(); + setLoading(true); + + const policyCopy = { ...policy }; + policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized }; + + // 处理存储策略 + if (useCDN === "false" || policy.IsOriginLinkEnable === "false") { + policyCopy.BaseURL = ""; + } + + // 类型转换 + policyCopy.AutoRename = policyCopy.AutoRename === "true"; + policyCopy.IsOriginLinkEnable = + policyCopy.IsOriginLinkEnable === "true"; + policyCopy.MaxSize = parseInt(policyCopy.MaxSize); + policyCopy.IsPrivate = policyCopy.IsPrivate === "true"; + policyCopy.OptionsSerialized.file_type = policyCopy.OptionsSerialized.file_type.split( + "," + ); + if ( + policyCopy.OptionsSerialized.file_type.length === 1 && + policyCopy.OptionsSerialized.file_type[0] === "" + ) { + policyCopy.OptionsSerialized.file_type = []; + } + + API.post("/admin/policy", { + policy: policyCopy, + }) + .then(() => { + ToggleSnackbar( + "top", + "right", + "存储策略已" + (props.policy ? "保存" : "添加"), + "success" + ); + setActiveStep(5); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + + setLoading(false); + }; + + return ( +
+ + {props.policy ? "修改" : "添加"}从机存储策略 + + + {steps.map((label, index) => { + const stepProps = {}; + const labelProps = {}; + if (label.optional) { + labelProps.optional = ( + 可选 + ); + } + if (isStepSkipped(index)) { + stepProps.completed = false; + } + return ( + + {label.title} + + ); + })} + + + {activeStep === 0 && ( +
{ + e.preventDefault(); + setActiveStep(1); + }} + > + + 从机存储策略允许你使用同样运行了 Cloudreve + 的服务器作为存储端, 用户上传下载流量通过 HTTP 直传。 + + +
+
+
1
+
+
+ + 将和主站相同版本的 Cloudreve + 程序拷贝至要作为从机的服务器上。 + +
+
+ +
+
+
2
+
+
+ + 下方为系统为您随机生成的从机端密钥,一般无需改动,如果有自定义需求, + 可将您的密钥填入下方: + +
+ + + 从机密钥 + + + +
+
+
+ +
+
+
3
+
+
+ + 修改从机配置文件。 +
+ 在从机端 Cloudreve 的同级目录下新建 + conf.ini + 文件,填入从机配置,启动/重启从机端 Cloudreve。 + 以下为一个可供参考的配置例子,其中密钥部分已帮您填写为上一步所生成的。 +
+
+                                [System]
+                                
+ Mode = slave +
+ Listen = :5212 +
+
+ [Slave] +
+ Secret = {policy.SecretKey} +
+
+ [CORS] +
+ AllowOrigins = *
+ AllowMethods = OPTIONS,GET,POST +
+ AllowHeaders = *
+
+ + 从机端配置文件格式大致与主站端相同,区别在于: +
    +
  • + System + 分区下的 + mode + 字段必须更改为slave +
  • +
  • + 必须指定Slave分区下的 + Secret + 字段,其值为第二步里填写或生成的密钥。 +
  • +
  • + 必须启动跨域配置,即CORS + 字段的内容, + 具体可参考上文范例或官方文档。如果配置不正确,用户将无法通过 + Web 端向从机上传文件。 +
  • +
+
+
+
+ +
+
+
4
+
+
+ + 填写从机地址。 +
+ 如果主站启用了 + HTTPS,从机也需要启用,并在下方填入 HTTPS + 协议的地址。 +
+
+ + + 从机地址 + + + +
+
+
+ +
+
+
5
+
+
+ + 完成以上步骤后,你可以点击下方的测试按钮测试通信是否正常。 + +
+ +
+
+
+ +
+ +
+
+ )} + + {activeStep === 1 && ( +
{ + e.preventDefault(); + setActiveStep(2); + }} + > +
+
+
1
+
+
+ + 请在下方输入文件的存储目录路径,可以为绝对路径或相对路径(相对于 + 从机的 + Cloudreve)。路径中可以使用魔法变量,文件在上传时会自动替换这些变量为相应值; + 可用魔法变量可参考{" "} + { + e.preventDefault(); + setMagicVar("path"); + }} + > + 路径魔法变量列表 + {" "} + 。 + +
+ + + 存储目录 + + + +
+
+
+ +
+
+
2
+
+
+ + 是否需要对存储的物理文件进行重命名?此处的重命名不会影响最终呈现给用户的 + 文件名。文件名也可使用魔法变量, + 可用魔法变量可参考{" "} + { + e.preventDefault(); + setMagicVar("file"); + }} + > + 文件名魔法变量列表 + {" "} + 。 + +
+ + + + } + label="开启重命名" + /> + + } + label="不开启" + /> + + +
+ + +
+ + + 命名规则 + + + +
+
+
+
+ +
+ + +
+
+ )} + + {activeStep === 2 && ( +
{ + e.preventDefault(); + setActiveStep(3); + }} + > +
+
+
1
+
+
+ + 是否允许获取文件永久直链? +
+ 开启后,用户可以请求获得能直接访问到文件内容的直链,适用于图床应用或自用。 +
+ +
+ + + + } + label="允许" + /> + + } + label="禁止" + /> + + +
+
+
+ + +
+
+
2
+
+
+ + 是否要对下载/直链使用 CDN? +
+ 开启后,用户访问文件时的 URL + 中的域名部分会被替换为 CDN 域名。 +
+ +
+ + { + if ( + e.target.value === "false" + ) { + setPolicy({ + ...policy, + BaseURL: "", + }); + } + setUseCDN(e.target.value); + }} + row + > + + } + label="使用" + /> + + } + label="不使用" + /> + + +
+
+
+ + +
+
+
3
+
+
+ + 选择协议并填写 CDN 域名 + + +
+ +
+
+
+
+
+ +
+ {" "} + +
+
+ )} + + {activeStep === 3 && ( +
{ + e.preventDefault(); + setActiveStep(4); + }} + > +
+
+
1
+
+
+ + 是否限制上传的单文件大小? + + +
+ + { + if (e.target.value === "true") { + setPolicy({ + ...policy, + MaxSize: "10485760", + }); + } else { + setPolicy({ + ...policy, + MaxSize: "0", + }); + } + }} + row + > + + } + label="限制" + /> + + } + label="不限制" + /> + + +
+
+
+ + +
+
+
2
+
+
+ + 输入限制: + +
+ +
+
+
+
+ +
+
+
+ {policy.MaxSize !== "0" ? "3" : "2"} +
+
+
+ + 是否限制上传文件扩展名? + + +
+ + { + if (e.target.value === "true") { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + file_type: + "jpg,png,mp4,zip,rar", + }, + }); + } else { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + file_type: "", + }, + }); + } + }} + row + > + + } + label="限制" + /> + + } + label="不限制" + /> + + +
+
+
+ + +
+
+
+ {policy.MaxSize !== "0" ? "4" : "3"} +
+
+
+ + 输入允许上传的文件扩展名,多个请以半角逗号 , + 隔开 + +
+ + + 扩展名列表 + + + +
+
+
+
+ +
+ {" "} + +
+
+ )} + + {activeStep === 4 && ( +
+
+
+
+ + 最后一步,为此存储策略命名: + +
+ + + 存储策略名 + + + +
+
+
+
+ {" "} + +
+
+ )} + + {activeStep === 5 && ( + <> +
+ + 存储策略已{props.policy ? "保存" : "添加"}! + + + 要使用此存储策略,请到用户组管理页面,为相应用户组绑定此存储策略。 + +
+
+ +
+ + )} + + setMagicVar("")} + /> + setMagicVar("")} + /> +
+ ); +} diff --git a/assets/src/component/Admin/Policy/Guid/S3Guide.js b/assets/src/component/Admin/Policy/Guid/S3Guide.js new file mode 100644 index 00000000..c6bd2c62 --- /dev/null +++ b/assets/src/component/Admin/Policy/Guid/S3Guide.js @@ -0,0 +1,1088 @@ +import { lighten, makeStyles } from "@material-ui/core/styles"; +import React, { useCallback, useState } from "react"; +import Stepper from "@material-ui/core/Stepper"; +import StepLabel from "@material-ui/core/StepLabel"; +import Step from "@material-ui/core/Step"; +import Typography from "@material-ui/core/Typography"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../../actions"; +import Link from "@material-ui/core/Link"; +import FormControl from "@material-ui/core/FormControl"; +import InputLabel from "@material-ui/core/InputLabel"; +import Input from "@material-ui/core/Input"; +import RadioGroup from "@material-ui/core/RadioGroup"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Radio from "@material-ui/core/Radio"; +import Collapse from "@material-ui/core/Collapse"; +import Button from "@material-ui/core/Button"; +import API from "../../../../middleware/Api"; +import MagicVar from "../../Dialogs/MagicVar"; +import DomainInput from "../../Common/DomainInput"; +import SizeInput from "../../Common/SizeInput"; +import { useHistory } from "react-router"; +import { getNumber } from "../../../../utils"; +import Autocomplete from "@material-ui/lab/Autocomplete"; +import TextField from "@material-ui/core/TextField"; +import AlertDialog from "../../Dialogs/Alert"; + +const useStyles = makeStyles((theme) => ({ + stepContent: { + padding: "16px 32px 16px 32px", + }, + form: { + maxWidth: 400, + marginTop: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, + subStepContainer: { + display: "flex", + marginBottom: 20, + padding: 10, + transition: theme.transitions.create("background-color", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + "&:focus-within": { + backgroundColor: theme.palette.background.default, + }, + }, + stepNumber: { + width: 20, + height: 20, + backgroundColor: lighten(theme.palette.secondary.light, 0.2), + color: theme.palette.secondary.contrastText, + textAlign: "center", + borderRadius: " 50%", + }, + stepNumberContainer: { + marginRight: 10, + }, + stepFooter: { + marginTop: 32, + }, + button: { + marginRight: theme.spacing(1), + }, + viewButtonLabel: { textTransform: "none" }, + "@global": { + code: { + color: "rgba(0, 0, 0, 0.87)", + display: "inline-block", + padding: "2px 6px", + fontFamily: + ' Consolas, "Liberation Mono", Menlo, Courier, monospace', + borderRadius: "2px", + backgroundColor: "rgba(255,229,100,0.1)", + }, + pre: { + margin: "24px 0", + padding: "12px 18px", + overflow: "auto", + direction: "ltr", + borderRadius: "4px", + backgroundColor: "#272c34", + color: "#fff", + }, + }, +})); + +const steps = [ + { + title: "存储空间", + optional: false, + }, + { + title: "上传路径", + optional: false, + }, + { + title: "直链设置", + optional: false, + }, + { + title: "上传限制", + optional: false, + }, + { + title: "跨域策略", + optional: true, + }, + { + title: "完成", + optional: false, + }, +]; + +const regions = { + "us-east-2": "US East (Ohio)", + "us-east-1": "US East (N. Virginia)", + "us-west-1": "US West (N. California)", + "us-west-2": "US West (Oregon)", + "af-south-1": "Africa (Cape Town)", + "ap-east-1": "Asia Pacific (Hong Kong)", + "ap-south-1": "Asia Pacific (Mumbai)", + "ap-northeast-3": "Asia Pacific (Osaka-Local)", + "ap-northeast-2": "Asia Pacific (Seoul)", + "ap-southeast-1": "Asia Pacific (Singapore)", + "ap-southeast-2": "Asia Pacific (Sydney)", + "ap-northeast-1": "Asia Pacific (Tokyo)", + "ca-central-1": "Canada (Central)", + "cn-north-1": "China (Beijing)", + "cn-northwest-1": "China (Ningxia)", + "eu-central-1": "Europe (Frankfurt)", + "eu-west-1": "Europe (Ireland)", + "eu-west-2": "Europe (London)", + "eu-south-1": "Europe (Milan)", + "eu-west-3": "Europe (Paris)", + "eu-north-1": "Europe (Stockholm)", + "me-south-1": "Middle East (Bahrain)", + "sa-east-1": "South America (São Paulo)", +}; + +export default function S3Guide(props) { + const classes = useStyles(); + const history = useHistory(); + + const [activeStep, setActiveStep] = useState(0); + const [loading, setLoading] = useState(false); + const [alertOpen, setAlertOpen] = useState(true); + const [skipped, setSkipped] = React.useState(new Set()); + const [magicVar, setMagicVar] = useState(""); + const [useCDN, setUseCDN] = useState("false"); + const [policy, setPolicy] = useState( + props.policy + ? props.policy + : { + Type: "s3", + Name: "", + SecretKey: "", + AccessKey: "", + BaseURL: "", + Server: "", + IsPrivate: "true", + DirNameRule: "uploads/{year}/{month}/{day}", + AutoRename: "true", + FileNameRule: "{randomkey8}_{originname}", + IsOriginLinkEnable: "false", + MaxSize: "0", + OptionsSerialized: { + file_type: "", + region: "us-east-2", + }, + } + ); + const [policyID, setPolicyID] = useState( + props.policy ? props.policy.ID : 0 + ); + + const handleChange = (name) => (event) => { + setPolicy({ + ...policy, + [name]: event.target.value, + }); + }; + + const handleOptionChange = (name) => (event) => { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + [name]: event.target.value, + }, + }); + }; + + const isStepSkipped = (step) => { + return skipped.has(step); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const submitPolicy = (e) => { + e.preventDefault(); + setLoading(true); + + const policyCopy = { ...policy }; + policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized }; + + if (useCDN === "false") { + policyCopy.BaseURL = ""; + } + + // 类型转换 + policyCopy.AutoRename = policyCopy.AutoRename === "true"; + policyCopy.IsOriginLinkEnable = + policyCopy.IsOriginLinkEnable === "true"; + policyCopy.IsPrivate = policyCopy.IsPrivate === "true"; + policyCopy.MaxSize = parseInt(policyCopy.MaxSize); + policyCopy.OptionsSerialized.file_type = policyCopy.OptionsSerialized.file_type.split( + "," + ); + if ( + policyCopy.OptionsSerialized.file_type.length === 1 && + policyCopy.OptionsSerialized.file_type[0] === "" + ) { + policyCopy.OptionsSerialized.file_type = []; + } + + API.post("/admin/policy", { + policy: policyCopy, + }) + .then((response) => { + ToggleSnackbar( + "top", + "right", + "存储策略已" + (props.policy ? "保存" : "添加"), + "success" + ); + setActiveStep(4); + setPolicyID(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + + setLoading(false); + }; + + const createCORS = () => { + setLoading(true); + API.post("/admin/policy/cors", { + id: policyID, + }) + .then(() => { + ToggleSnackbar("top", "right", "跨域策略已添加", "success"); + setActiveStep(5); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + return ( +
+ setAlertOpen(false)} + title={"警告"} + msg={ + "S3 类型存储策略目前仅可用于自己使用,或者是给受信任的用户组使用。" + } + /> + + {props.policy ? "修改" : "添加"} Amazon S3 存储策略 + + + {steps.map((label, index) => { + const stepProps = {}; + const labelProps = {}; + if (label.optional) { + labelProps.optional = ( + 可选 + ); + } + if (isStepSkipped(index)) { + stepProps.completed = false; + } + return ( + + {label.title} + + ); + })} + + + {activeStep === 0 && ( +
{ + e.preventDefault(); + setActiveStep(1); + }} + > +
+
+
0
+
+
+ + Cloudreve 主站需要启用 CORS 支持,具体步骤如下: +
+ 修改 Cloudreve 配置文件,新增以下 CORS + 配置项,保存并重启 Cloudreve。 +
+                                    [CORS]
+                                    
+ AllowOrigins = *
+ AllowMethods = OPTIONS,GET,POST +
+ AllowHeaders = *
+
+
+
+
+ +
+
+
1
+
+
+ + 前往 AWS S3 + 控制台创建存储桶,在下方填写您创建存储桶时指定的 + 存储桶名称: + +
+ + + 存储桶名称 + + + +
+
+
+ +
+
+
2
+
+
+ + 在下方选择您创建的空间的公共访问权限类型,推荐选择“私有”以获得更高的安全性,私有空间无法开启“获取直链”功能。 + +
+ + + + } + label="阻止全部公共访问权限" + /> + + } + label="允许公共读取" + /> + + +
+
+
+ +
+
+
3
+
+
+ + (可选) 指定存储桶的 EndPoint(地域节点), + 填写为完整的 URL 格式,比如{" "} + https://bucket.region.example.com。 + 留空则将使用系统生成的默认接入点。 + +
+ + + EndPoint + + + +
+
+
+ +
+
+
4
+
+
+ + 选择存储桶所在的区域,或者手动输入区域代码 + +
+ + + handleOptionChange("region")({ + target: { value: value }, + }) + } + renderOption={(option) => ( + + {regions[option]} + + )} + renderInput={(params) => ( + + )} + /> + +
+
+
+ +
+
+
5
+
+
+ + 是否要使用 CDN 加速访问? + +
+ + { + setUseCDN(e.target.value); + }} + row + > + + } + label="使用" + /> + + } + label="不使用" + /> + + +
+
+
+ + +
+
+
6
+
+
+ + CDN 加速域名 + +
+ +
+
+
+
+ +
+
+
+ {getNumber(6, [useCDN === "true"])} +
+
+
+ + 获取访问密钥,并填写在下方。 + +
+ + + AccessKey + + + +
+
+ + + SecretKey + + + +
+
+
+ +
+
+
+ {getNumber(7, [useCDN === "true"])} +
+
+
+ + 为此存储策略命名: + +
+ + + 存储策略名 + + + +
+
+
+ +
+ +
+
+ )} + + {activeStep === 1 && ( +
{ + e.preventDefault(); + setActiveStep(2); + }} + > +
+
+
1
+
+
+ + 请在下方输入文件的存储目录路径,可以为绝对路径或相对路径(相对于 + 从机的 + Cloudreve)。路径中可以使用魔法变量,文件在上传时会自动替换这些变量为相应值; + 可用魔法变量可参考{" "} + setMagicVar("path")} + > + 路径魔法变量列表 + {" "} + 。 + +
+ + + 存储目录 + + + +
+
+
+ +
+
+
2
+
+
+ + 是否需要对存储的物理文件进行重命名?此处的重命名不会影响最终呈现给用户的 + 文件名。文件名也可使用魔法变量, + 可用魔法变量可参考{" "} + setMagicVar("file")} + > + 文件名魔法变量列表 + {" "} + 。 + +
+ + + + } + label="开启重命名" + /> + + } + label="不开启" + /> + + +
+ + +
+ + + 命名规则 + + + +
+
+
+
+ +
+ + +
+
+ )} + + {activeStep === 2 && ( +
{ + e.preventDefault(); + setActiveStep(3); + }} + > +
+
+
1
+
+
+ + 是否允许获取文件永久直链? +
+ 开启后,用户可以请求获得能直接访问到文件内容的直链,适用于图床应用或自用。 +
+ +
+ + { + if ( + policy.IsPrivate === "true" && + e.target.value === "true" + ) { + ToggleSnackbar( + "top", + "right", + "私有空间无法开启此功能", + "warning" + ); + return; + } + handleChange("IsOriginLinkEnable")( + e + ); + }} + row + > + + } + label="允许" + /> + + } + label="禁止" + /> + + +
+
+
+ +
+ {" "} + +
+
+ )} + + {activeStep === 3 && ( +
+
+
+
1
+
+
+ + 是否限制上传的单文件大小? + + +
+ + { + if (e.target.value === "true") { + setPolicy({ + ...policy, + MaxSize: "10485760", + }); + } else { + setPolicy({ + ...policy, + MaxSize: "0", + }); + } + }} + row + > + + } + label="限制" + /> + + } + label="不限制" + /> + + +
+
+
+ + +
+
+
2
+
+
+ + 输入限制: + +
+ +
+
+
+
+ +
+
+
+ {policy.MaxSize !== "0" ? "3" : "2"} +
+
+
+ + 是否限制上传文件扩展名? + + +
+ + { + if (e.target.value === "true") { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + file_type: + "jpg,png,mp4,zip,rar", + }, + }); + } else { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + file_type: "", + }, + }); + } + }} + row + > + + } + label="限制" + /> + + } + label="不限制" + /> + + +
+
+
+ + +
+
+
+ {policy.MaxSize !== "0" ? "4" : "3"} +
+
+
+ + 输入允许上传的文件扩展名,多个请以半角逗号 , + 隔开 + +
+ + + 扩展名列表 + + + +
+
+
+
+ +
+ {" "} + +
+
+ )} + + {activeStep === 4 && ( +
+
+
+
+ + S3 Bucket 需要正确配置跨域策略后才能使用 Web + 端上传文件,Cloudreve + 可以帮您自动设置,您也可以参考文档步骤手动设置。如果您已设置过此 + Bucket 的跨域策略,此步骤可以跳过。 + +
+ +
+
+
+
+ {" "} +
+ + )} + + {activeStep === 5 && ( + <> +
+ + 存储策略已{props.policy ? "保存" : "添加"}! + + + 要使用此存储策略,请到用户组管理页面,为相应用户组绑定此存储策略。 + +
+
+ +
+ + )} + + setMagicVar("")} + /> + setMagicVar("")} + /> +
+ ); +} diff --git a/assets/src/component/Admin/Policy/Guid/UpyunGuide.js b/assets/src/component/Admin/Policy/Guid/UpyunGuide.js new file mode 100644 index 00000000..e2ea8692 --- /dev/null +++ b/assets/src/component/Admin/Policy/Guid/UpyunGuide.js @@ -0,0 +1,909 @@ +import Button from "@material-ui/core/Button"; +import Collapse from "@material-ui/core/Collapse"; +import FormControl from "@material-ui/core/FormControl"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Input from "@material-ui/core/Input"; +import InputLabel from "@material-ui/core/InputLabel"; +import Link from "@material-ui/core/Link"; +import Radio from "@material-ui/core/Radio"; +import RadioGroup from "@material-ui/core/RadioGroup"; +import Step from "@material-ui/core/Step"; +import StepLabel from "@material-ui/core/StepLabel"; +import Stepper from "@material-ui/core/Stepper"; +import { lighten, makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import React, { useCallback, useState } from "react"; +import { useDispatch } from "react-redux"; +import { useHistory } from "react-router"; +import { toggleSnackbar } from "../../../../actions"; +import API from "../../../../middleware/Api"; +import DomainInput from "../../Common/DomainInput"; +import SizeInput from "../../Common/SizeInput"; +import MagicVar from "../../Dialogs/MagicVar"; + +const useStyles = makeStyles((theme) => ({ + stepContent: { + padding: "16px 32px 16px 32px", + }, + form: { + maxWidth: 400, + marginTop: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, + subStepContainer: { + display: "flex", + marginBottom: 20, + padding: 10, + transition: theme.transitions.create("background-color", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + "&:focus-within": { + backgroundColor: theme.palette.background.default, + }, + }, + stepNumber: { + width: 20, + height: 20, + backgroundColor: lighten(theme.palette.secondary.light, 0.2), + color: theme.palette.secondary.contrastText, + textAlign: "center", + borderRadius: " 50%", + }, + stepNumberContainer: { + marginRight: 10, + }, + stepFooter: { + marginTop: 32, + }, + button: { + marginRight: theme.spacing(1), + }, +})); + +const steps = [ + { + title: "存储空间", + optional: false, + }, + { + title: "上传路径", + optional: false, + }, + { + title: "直链设置", + optional: false, + }, + { + title: "上传限制", + optional: false, + }, + { + title: "完成", + optional: false, + }, +]; + +export default function UpyunGuide(props) { + const classes = useStyles(); + const history = useHistory(); + + const [activeStep, setActiveStep] = useState(0); + const [loading, setLoading] = useState(false); + const [skipped] = React.useState(new Set()); + const [magicVar, setMagicVar] = useState(""); + const [policy, setPolicy] = useState( + props.policy + ? props.policy + : { + Type: "upyun", + Name: "", + SecretKey: "", + AccessKey: "", + BaseURL: "", + IsPrivate: "false", + DirNameRule: "uploads/{year}/{month}/{day}", + AutoRename: "true", + FileNameRule: "{randomkey8}_{originname}", + IsOriginLinkEnable: "false", + MaxSize: "0", + OptionsSerialized: { + file_type: "", + token: "", + }, + } + ); + + const handleChange = (name) => (event) => { + setPolicy({ + ...policy, + [name]: event.target.value, + }); + }; + + const handleOptionChange = (name) => (event) => { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + [name]: event.target.value, + }, + }); + }; + + const isStepSkipped = (step) => { + return skipped.has(step); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const submitPolicy = (e) => { + e.preventDefault(); + setLoading(true); + + const policyCopy = { ...policy }; + policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized }; + + // 类型转换 + policyCopy.AutoRename = policyCopy.AutoRename === "true"; + policyCopy.IsOriginLinkEnable = + policyCopy.IsOriginLinkEnable === "true"; + policyCopy.IsPrivate = policyCopy.IsPrivate === "true"; + policyCopy.MaxSize = parseInt(policyCopy.MaxSize); + policyCopy.OptionsSerialized.file_type = policyCopy.OptionsSerialized.file_type.split( + "," + ); + if ( + policyCopy.OptionsSerialized.file_type.length === 1 && + policyCopy.OptionsSerialized.file_type[0] === "" + ) { + policyCopy.OptionsSerialized.file_type = []; + } + + API.post("/admin/policy", { + policy: policyCopy, + }) + .then(() => { + ToggleSnackbar( + "top", + "right", + "存储策略已" + (props.policy ? "保存" : "添加"), + "success" + ); + setActiveStep(5); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + + setLoading(false); + }; + + return ( +
+ + {props.policy ? "修改" : "添加"} 又拍云 存储策略 + + + {steps.map((label, index) => { + const stepProps = {}; + const labelProps = {}; + if (label.optional) { + labelProps.optional = ( + 可选 + ); + } + if (isStepSkipped(index)) { + stepProps.completed = false; + } + return ( + + {label.title} + + ); + })} + + + {activeStep === 0 && ( +
{ + e.preventDefault(); + setActiveStep(1); + }} + > +
+
+
0
+
+
+ + 在使用又拍云存储策略前,请确保您在 参数设置 - + 站点信息 - 站点URL 中填写的 地址与实际相符,并且 + 能够被外网正常访问。 + +
+
+ +
+
+
1
+
+
+ + 前往 + + 又拍云面板 + + 创建云存储服务。 + +
+
+ +
+
+
2
+
+
+ + 在下方填写所创建的服务名称: + +
+ + + 服务名称 + + + +
+
+
+ +
+
+
3
+
+
+ + 为此服务创建或授权有读取、写入、删除权限的操作员,然后将操作员信息填写在下方: + +
+ + + 操作员名 + + + +
+
+ + + 操作员密码 + + + +
+
+
+ +
+
+
4
+
+
+ + 填写为云存储服务绑定的域名,并根据实际情况选择是否使用 + HTTPS: + +
+ +
+
+
+ +
+
+
5
+
+
+ + 此步骤可保持默认并跳过,但是强烈建议您跟随此步骤操作。 +
+ 前往所创建云存储服务的 功能配置 面板,转到 + 访问配置 选项卡,开启 Token 防盗链并设定密码。 +
+
+ + + + } + label="已开启 Token 防盗链" + /> + + } + label="未开启 Token 防盗链" + /> + + +
+
+
+ + +
+
+
6
+
+
+ + 填写您所设置的 Token 防盗链 密钥 + +
+ + + Token 防盗链 密钥 + + + +
+
+
+
+ +
+ +
+
+ )} + + {activeStep === 1 && ( +
{ + e.preventDefault(); + setActiveStep(2); + }} + > +
+
+
1
+
+
+ + 请在下方输入文件的存储目录路径,可以为绝对路径或相对路径(相对于 + 从机的 + Cloudreve)。路径中可以使用魔法变量,文件在上传时会自动替换这些变量为相应值; + 可用魔法变量可参考{" "} + { + e.preventDefault(); + setMagicVar("path"); + }} + > + 路径魔法变量列表 + {" "} + 。 + +
+ + + 存储目录 + + + +
+
+
+ +
+
+
2
+
+
+ + 是否需要对存储的物理文件进行重命名?此处的重命名不会影响最终呈现给用户的 + 文件名。文件名也可使用魔法变量, + 可用魔法变量可参考{" "} + { + e.preventDefault(); + setMagicVar("file"); + }} + > + 文件名魔法变量列表 + {" "} + 。 + +
+ + + + } + label="开启重命名" + /> + + } + label="不开启" + /> + + +
+ + +
+ + + 命名规则 + + + +
+
+
+
+ +
+ + +
+
+ )} + + {activeStep === 2 && ( +
{ + e.preventDefault(); + setActiveStep(3); + }} + > +
+
+
1
+
+
+ + 是否允许获取文件永久直链? +
+ 开启后,用户可以请求获得能直接访问到文件内容的直链,适用于图床应用或自用。 +
+ +
+ + { + if ( + policy.IsPrivate === "true" && + e.target.value === "true" + ) { + ToggleSnackbar( + "top", + "right", + "开启 Token 防盗链后无法使用直链功能", + "warning" + ); + return; + } + handleChange("IsOriginLinkEnable")( + e + ); + }} + row + > + + } + label="允许" + /> + + } + label="禁止" + /> + + +
+
+
+ +
+ {" "} + +
+
+ )} + + {activeStep === 3 && ( +
{ + e.preventDefault(); + setActiveStep(4); + }} + > +
+
+
1
+
+
+ + 是否限制上传的单文件大小? + + +
+ + { + if (e.target.value === "true") { + setPolicy({ + ...policy, + MaxSize: "10485760", + }); + } else { + setPolicy({ + ...policy, + MaxSize: "0", + }); + } + }} + row + > + + } + label="限制" + /> + + } + label="不限制" + /> + + +
+
+
+ + +
+
+
2
+
+
+ + 输入限制: + +
+ +
+
+
+
+ +
+
+
+ {policy.MaxSize !== "0" ? "3" : "2"} +
+
+
+ + 是否限制上传文件扩展名? + + +
+ + { + if (e.target.value === "true") { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + file_type: + "jpg,png,mp4,zip,rar", + }, + }); + } else { + setPolicy({ + ...policy, + OptionsSerialized: { + ...policy.OptionsSerialized, + file_type: "", + }, + }); + } + }} + row + > + + } + label="限制" + /> + + } + label="不限制" + /> + + +
+
+
+ + +
+
+
+ {policy.MaxSize !== "0" ? "4" : "3"} +
+
+
+ + 输入允许上传的文件扩展名,多个请以半角逗号 , + 隔开 + +
+ + + 扩展名列表 + + + +
+
+
+
+ +
+ {" "} + +
+
+ )} + + {activeStep === 4 && ( +
+
+
+
+ + 最后一步,为此存储策略命名: + +
+ + + 存储策略名 + + + +
+
+
+
+ {" "} + +
+
+ )} + + {activeStep === 5 && ( + <> +
+ + 存储策略已{props.policy ? "保存" : "添加"}! + + + 要使用此存储策略,请到用户组管理页面,为相应用户组绑定此存储策略。 + +
+
+ +
+ + )} + + setMagicVar("")} + /> + setMagicVar("")} + /> +
+ ); +} diff --git a/assets/src/component/Admin/Policy/Policy.js b/assets/src/component/Admin/Policy/Policy.js new file mode 100644 index 00000000..33408fa6 --- /dev/null +++ b/assets/src/component/Admin/Policy/Policy.js @@ -0,0 +1,296 @@ +import Button from "@material-ui/core/Button"; +import IconButton from "@material-ui/core/IconButton"; +import Menu from "@material-ui/core/Menu"; +import MenuItem from "@material-ui/core/MenuItem"; +import Paper from "@material-ui/core/Paper"; +import Select from "@material-ui/core/Select"; +import { makeStyles } from "@material-ui/core/styles"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableContainer from "@material-ui/core/TableContainer"; +import TableHead from "@material-ui/core/TableHead"; +import TablePagination from "@material-ui/core/TablePagination"; +import TableRow from "@material-ui/core/TableRow"; +import Tooltip from "@material-ui/core/Tooltip"; +import { Delete, Edit } from "@material-ui/icons"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { useHistory, useLocation } from "react-router"; +import { toggleSnackbar } from "../../../actions"; +import { policyTypeMap } from "../../../config"; +import API from "../../../middleware/Api"; +import { sizeToString } from "../../../utils"; +import AddPolicy from "../Dialogs/AddPolicy"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + content: { + padding: theme.spacing(2), + }, + container: { + overflowX: "auto", + }, + tableContainer: { + marginTop: 16, + }, + header: { + display: "flex", + justifyContent: "space-between", + }, + headerRight: {}, +})); + +const columns = [ + { id: "#", label: "#", minWidth: 50 }, + { id: "name", label: "名称", minWidth: 170 }, + { id: "type", label: "类型", minWidth: 170 }, + { + id: "count", + label: "下属文件数", + minWidth: 50, + align: "right", + }, + { + id: "size", + label: "数据量", + minWidth: 100, + align: "right", + }, + { + id: "action", + label: "操作", + minWidth: 170, + align: "right", + }, +]; + +function useQuery() { + return new URLSearchParams(useLocation().search); +} + +export default function Policy() { + const classes = useStyles(); + // const [loading, setLoading] = useState(false); + // const [tab, setTab] = useState(0); + const [policies, setPolicies] = useState([]); + const [statics, setStatics] = useState([]); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [total, setTotal] = useState(0); + const [addDialog, setAddDialog] = useState(false); + const [filter, setFilter] = useState("all"); + const [anchorEl, setAnchorEl] = React.useState(null); + const [editID, setEditID] = React.useState(0); + + const location = useLocation(); + const history = useHistory(); + const query = useQuery(); + + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + useEffect(() => { + if (query.get("code") === "0") { + ToggleSnackbar("top", "right", "授权成功", "success"); + } else if (query.get("msg") && query.get("msg") !== "") { + ToggleSnackbar( + "top", + "right", + query.get("msg") + ", " + query.get("err"), + "warning" + ); + } + }, [location]); + + const loadList = () => { + API.post("/admin/policy/list", { + page: page, + page_size: pageSize, + order_by: "id desc", + conditions: filter === "all" ? {} : { type: filter }, + }) + .then((response) => { + setPolicies(response.data.items); + setStatics(response.data.statics); + setTotal(response.data.total); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + + useEffect(() => { + loadList(); + }, [page, pageSize, filter]); + + const deletePolicy = (id) => { + API.delete("/admin/policy/" + id) + .then(() => { + loadList(); + ToggleSnackbar("top", "right", "存储策略已删除", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + + const open = Boolean(anchorEl); + + return ( +
+ setAddDialog(false)} /> +
+ +
+ + +
+
+ + + + + + + {columns.map((column) => ( + + {column.label} + + ))} + + + + {policies.map((row) => ( + + {row.ID} + {row.Name} + + {policyTypeMap[row.Type] !== + undefined && + policyTypeMap[row.Type]} + + + {statics[row.ID] !== undefined && + statics[row.ID][0].toLocaleString()} + + + {statics[row.ID] !== undefined && + sizeToString(statics[row.ID][1])} + + + + + deletePolicy(row.ID) + } + size={"small"} + > + + + + + { + setEditID(row.ID); + handleClick(e); + }} + size={"small"} + > + + + + + + ))} + +
+
+ setPage(p + 1)} + onChangeRowsPerPage={(e) => { + setPageSize(e.target.value); + setPage(1); + }} + /> +
+ + { + handleClose(e); + history.push("/admin/policy/edit/pro/" + editID); + }} + > + 专家模式编辑 + + { + handleClose(e); + history.push("/admin/policy/edit/guide/" + editID); + }} + > + 向导模式编辑 + + +
+ ); +} diff --git a/assets/src/component/Admin/Setting/Access.js b/assets/src/component/Admin/Setting/Access.js new file mode 100644 index 00000000..0abc148b --- /dev/null +++ b/assets/src/component/Admin/Setting/Access.js @@ -0,0 +1,325 @@ +import Button from "@material-ui/core/Button"; +import FormControl from "@material-ui/core/FormControl"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import InputLabel from "@material-ui/core/InputLabel"; +import MenuItem from "@material-ui/core/MenuItem"; +import Select from "@material-ui/core/Select"; +import { makeStyles } from "@material-ui/core/styles"; +import Switch from "@material-ui/core/Switch"; +import Typography from "@material-ui/core/Typography"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; +import AlertDialog from "../Dialogs/Alert"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + form: { + maxWidth: 400, + marginTop: 20, + marginBottom: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, +})); + +export default function Access() { + const classes = useStyles(); + const [loading, setLoading] = useState(false); + const [options, setOptions] = useState({ + register_enabled: "1", + default_group: "1", + email_active: "0", + login_captcha: "0", + reg_captcha: "0", + forget_captcha: "0", + authn_enabled: "0", + }); + const [siteURL, setSiteURL] = useState(""); + const [groups, setGroups] = useState([]); + const [httpAlert, setHttpAlert] = useState(false); + + const handleChange = (name) => (event) => { + let value = event.target.value; + if (event.target.checked !== undefined) { + value = event.target.checked ? "1" : "0"; + } + setOptions({ + ...options, + [name]: value, + }); + }; + + const handleInputChange = (name) => (event) => { + const value = event.target.value; + setOptions({ + ...options, + [name]: value, + }); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + useEffect(() => { + API.post("/admin/setting", { + keys: [...Object.keys(options), "siteURL"], + }) + .then((response) => { + setSiteURL(response.data.siteURL); + delete response.data.siteURL; + setOptions(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + + API.get("/admin/groups") + .then((response) => { + setGroups(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + // eslint-disable-next-line + }, []); + + const submit = (e) => { + e.preventDefault(); + setLoading(true); + const option = []; + Object.keys(options).forEach((k) => { + option.push({ + key: k, + value: options[k], + }); + }); + API.patch("/admin/setting", { + options: option, + }) + .then(() => { + ToggleSnackbar("top", "right", "设置已更改", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + return ( +
+ setHttpAlert(false)} + open={httpAlert} + /> +
+
+ + 注册与登录 + +
+
+ + + } + label="允许新用户注册" + /> + + 关闭后,无法再通过前台注册新的用户 + + +
+ +
+ + + } + label="邮件激活" + /> + + 开启后,新用户注册需要点击邮件中的激活链接才能完成。请确认邮件发送设置是否正确,否则激活邮件无法送达 + + +
+ +
+ + + } + label="注册验证码" + /> + + 是否启用注册表单验证码 + + +
+ +
+ + + } + label="登录验证码" + /> + + 是否启用登录表单验证码 + + +
+ +
+ + + } + label="找回密码验证码" + /> + + 是否启用找回密码表单验证码 + + +
+ +
+ + { + if ( + !siteURL.startsWith( + "https://" + ) + ) { + setHttpAlert(true); + return; + } + handleChange("authn_enabled")( + e + ); + }} + /> + } + label="Web Authn" + /> + + 是否允许用户使用绑定的外部验证器登录,站点必须启动 + HTTPS 才能使用。 + + +
+ +
+ + + 默认用户组 + + + + 用户注册后的初始用户组 + + +
+
+
+ +
+ +
+
+
+ ); +} diff --git a/assets/src/component/Admin/Setting/Aria2.js b/assets/src/component/Admin/Setting/Aria2.js new file mode 100644 index 00000000..a62f79d0 --- /dev/null +++ b/assets/src/component/Admin/Setting/Aria2.js @@ -0,0 +1,305 @@ +import Button from "@material-ui/core/Button"; +import FormControl from "@material-ui/core/FormControl"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import Input from "@material-ui/core/Input"; +import InputLabel from "@material-ui/core/InputLabel"; +import Link from "@material-ui/core/Link"; +import { makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import Alert from "@material-ui/lab/Alert"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + form: { + maxWidth: 400, + marginTop: 20, + marginBottom: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, +})); + +export default function Aria2() { + const classes = useStyles(); + const [loading, setLoading] = useState(false); + const [options, setOptions] = useState({ + aria2_rpcurl: "", + aria2_token: "", + aria2_temp_path: "", + aria2_options: "", + aria2_interval: "0", + aria2_call_timeout: "0", + }); + + const handleChange = (name) => (event) => { + setOptions({ + ...options, + [name]: event.target.value, + }); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + useEffect(() => { + API.post("/admin/setting", { + keys: Object.keys(options), + }) + .then((response) => { + setOptions(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + // eslint-disable-next-line + }, []); + + const reload = () => { + API.get("/admin/reload/aria2") + // eslint-disable-next-line @typescript-eslint/no-empty-function + .then(() => {}) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + // eslint-disable-next-line @typescript-eslint/no-empty-function + .then(() => {}); + }; + + const test = () => { + setLoading(true); + API.post("/admin/aria2/test", { + server: options.aria2_rpcurl, + token: options.aria2_token, + }) + .then((response) => { + ToggleSnackbar( + "top", + "right", + "连接成功,Aria2 版本为:" + response.data, + "success" + ); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const submit = (e) => { + e.preventDefault(); + setLoading(true); + const option = []; + Object.keys(options).forEach((k) => { + option.push({ + key: k, + value: options[k], + }); + }); + API.patch("/admin/setting", { + options: option, + }) + .then(() => { + ToggleSnackbar("top", "right", "设置已更改", "success"); + reload(); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + return ( +
+
+
+ + Aria2 + + +
+
+ + + Cloudreve 的离线下载功能由{" "} + + Aria2 + {" "} + 驱动。如需使用,请在同一设备上以和运行 + Cloudreve 相同的用户身份启动 Aria2, 并在 + Aria2 的配置文件中开启 RPC + 服务。更多信息及指引请参考文档的{" "} + + 离线下载 + {" "} + 章节。 + + +
+ +
+ + + RPC 服务器地址 + + + + 包含端口的完整 RPC + 服务器地址,例如:http://127.0.0.1:6800/,留空表示不启用 + Aria2 服务 + + +
+ +
+ + + RPC Secret + + + + RPC 授权令牌,与 Aria2 + 配置文件中保持一致,未设置请留空。 + + +
+ +
+ + + 临时下载目录 + + + + 离线下载临时下载目录的 + 绝对路径,Cloudreve + 进程需要此目录的读、写、执行权限。 + + +
+ +
+ + + 状态刷新间隔 (秒) + + + + Cloudreve 向 Aria2 请求刷新任务状态的间隔。 + + +
+ +
+ + + RPC 调用超时 (秒) + + + + 调用 RPC 服务时最长等待时间 + + +
+ +
+ + + 全局任务参数 + + + + 创建下载任务时携带的额外设置参数,以 JSON + 编码后的格式书写,您可也可以将这些设置写在 + Aria2 配置文件里,可用参数请查阅官方文档 + + +
+
+
+ +
+ + +
+
+
+ ); +} diff --git a/assets/src/component/Admin/Setting/Captcha.js b/assets/src/component/Admin/Setting/Captcha.js new file mode 100644 index 00000000..349461fc --- /dev/null +++ b/assets/src/component/Admin/Setting/Captcha.js @@ -0,0 +1,408 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import InputLabel from "@material-ui/core/InputLabel"; +import FormControl from "@material-ui/core/FormControl"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import Button from "@material-ui/core/Button"; +import API from "../../../middleware/Api"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../actions"; +import Select from "@material-ui/core/Select"; +import MenuItem from "@material-ui/core/MenuItem"; +import Input from "@material-ui/core/Input"; +import Link from "@material-ui/core/Link"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + form: { + maxWidth: 400, + marginTop: 20, + marginBottom: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, +})); + +export default function Captcha() { + const classes = useStyles(); + const [loading, setLoading] = useState(false); + const [options, setOptions] = useState({ + captcha_type: "normal", + captcha_height: "1", + captcha_width: "1", + captcha_mode: "3", + captcha_CaptchaLen: "", + captcha_ReCaptchaKey: "", + captcha_ReCaptchaSecret: "", + captcha_TCaptcha_CaptchaAppId: "", + captcha_TCaptcha_AppSecretKey: "", + captcha_TCaptcha_SecretId: "", + captcha_TCaptcha_SecretKey: "", + }); + + const handleChange = (name) => (event) => { + setOptions({ + ...options, + [name]: event.target.value, + }); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + useEffect(() => { + API.post("/admin/setting", { + keys: Object.keys(options), + }) + .then((response) => { + setOptions(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + // eslint-disable-next-line + }, []); + + const submit = (e) => { + e.preventDefault(); + setLoading(true); + const option = []; + Object.keys(options).forEach((k) => { + option.push({ + key: k, + value: options[k], + }); + }); + API.patch("/admin/setting", { + options: option, + }) + .then(() => { + ToggleSnackbar("top", "right", "设置已更改", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + return ( +
+
+
+ + 验证码 + +
+
+ + + 验证码类型 + + + + 验证码类型 + + +
+
+
+ + {options.captcha_type === "normal" && ( +
+ + 普通验证码 + +
+
+ + + 宽度 + + + +
+ +
+ + + 高度 + + + +
+ +
+ + + 模式 + + + + 验证码的形式 + + +
+
+
+ )} + + {options.captcha_type === "recaptcha" && ( +
+ + reCAPTCHA V2 + +
+
+
+ + + Site KEY + + + + + 应用管理页面 + {" "} + 获取到的的 网站密钥 + + +
+ +
+ + + Secret + + + + + 应用管理页面 + {" "} + 获取到的的 秘钥 + + +
+
+
+
+ )} + + {options.captcha_type === "tcaptcha" && ( +
+ + 腾讯云验证码 + +
+
+
+ + + SecretId + + + + + 访问密钥页面 + {" "} + 获取到的的 SecretId + + +
+ +
+ + + SecretKey + + + + + 访问密钥页面 + {" "} + 获取到的的 SecretKey + + +
+ +
+ + + APPID + + + + + 图形验证页面 + {" "} + 获取到的的 APPID + + +
+ +
+ + + App Secret Key + + + + + 图形验证页面 + {" "} + 获取到的的 App Secret Key + + +
+
+
+
+ )} + +
+ +
+
+
+ ); +} diff --git a/assets/src/component/Admin/Setting/Image.js b/assets/src/component/Admin/Setting/Image.js new file mode 100644 index 00000000..b8cac0df --- /dev/null +++ b/assets/src/component/Admin/Setting/Image.js @@ -0,0 +1,270 @@ +import Button from "@material-ui/core/Button"; +import FormControl from "@material-ui/core/FormControl"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import Input from "@material-ui/core/Input"; +import InputLabel from "@material-ui/core/InputLabel"; +import { makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; +import SizeInput from "../Common/SizeInput"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + form: { + maxWidth: 400, + marginTop: 20, + marginBottom: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, +})); + +export default function ImageSetting() { + const classes = useStyles(); + const [loading, setLoading] = useState(false); + const [options, setOptions] = useState({ + gravatar_server: "", + avatar_path: "", + avatar_size: "", + avatar_size_l: "", + avatar_size_m: "", + avatar_size_s: "", + thumb_width: "", + thumb_height: "", + }); + + const handleChange = (name) => (event) => { + setOptions({ + ...options, + [name]: event.target.value, + }); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + useEffect(() => { + API.post("/admin/setting", { + keys: Object.keys(options), + }) + .then((response) => { + setOptions(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + // eslint-disable-next-line + }, []); + + const submit = (e) => { + e.preventDefault(); + setLoading(true); + const option = []; + Object.keys(options).forEach((k) => { + option.push({ + key: k, + value: options[k], + }); + }); + API.patch("/admin/setting", { + options: option, + }) + .then(() => { + ToggleSnackbar("top", "right", "设置已更改", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + return ( +
+
+
+ + 头像 + +
+
+ + + Gravatar 服务器 + + + + Gravatar 服务器地址,可选择使用国内镜像 + + +
+ +
+ + + 头像存储路径 + + + + 用户上传自定义头像的存储路径 + + +
+ +
+ + + + 用户可上传头像文件的最大大小 + + +
+ +
+ + + 小头像尺寸 + + + +
+ +
+ + + 中头像尺寸 + + + +
+ +
+ + + 大头像尺寸 + + + +
+
+
+ +
+ + 缩略图 + + +
+
+ + + 宽度 + + + +
+
+ +
+
+ + + 高度 + + + +
+
+
+ +
+ +
+
+
+ ); +} diff --git a/assets/src/component/Admin/Setting/Mail.js b/assets/src/component/Admin/Setting/Mail.js new file mode 100644 index 00000000..74c75fc0 --- /dev/null +++ b/assets/src/component/Admin/Setting/Mail.js @@ -0,0 +1,433 @@ +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogContentText from "@material-ui/core/DialogContentText"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import FormControl from "@material-ui/core/FormControl"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import Input from "@material-ui/core/Input"; +import InputLabel from "@material-ui/core/InputLabel"; +import { makeStyles } from "@material-ui/core/styles"; +import TextField from "@material-ui/core/TextField"; +import Typography from "@material-ui/core/Typography"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Switch from "@material-ui/core/Switch"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + form: { + maxWidth: 400, + marginTop: 20, + marginBottom: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, + buttonMargin: { + marginLeft: 8, + }, +})); + +export default function Mail() { + const classes = useStyles(); + const [loading, setLoading] = useState(false); + const [test, setTest] = useState(false); + const [tesInput, setTestInput] = useState(""); + const [options, setOptions] = useState({ + fromName: "", + fromAdress: "", + smtpHost: "", + smtpPort: "", + replyTo: "", + smtpUser: "", + smtpPass: "", + smtpEncryption: "", + mail_keepalive: "30", + mail_activation_template: "", + mail_reset_pwd_template: "", + }); + + const handleChange = (name) => (event) => { + setOptions({ + ...options, + [name]: event.target.value, + }); + }; + + const handleCheckChange = (name) => (event) => { + let value = event.target.value; + if (event.target.checked !== undefined) { + value = event.target.checked ? "1" : "0"; + } + setOptions({ + ...options, + [name]: value, + }); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + useEffect(() => { + API.post("/admin/setting", { + keys: Object.keys(options), + }) + .then((response) => { + setOptions(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + // eslint-disable-next-line + }, []); + + const sendTestMail = () => { + setLoading(true); + API.post("/admin/mailTest", { + to: tesInput, + }) + .then(() => { + ToggleSnackbar("top", "right", "测试邮件已发送", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const reload = () => { + API.get("/admin/reload/email") + // eslint-disable-next-line @typescript-eslint/no-empty-function + .then(() => {}) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + // eslint-disable-next-line @typescript-eslint/no-empty-function + .then(() => {}); + }; + + const submit = (e) => { + e.preventDefault(); + setLoading(true); + const option = []; + Object.keys(options).forEach((k) => { + option.push({ + key: k, + value: options[k], + }); + }); + API.patch("/admin/setting", { + options: option, + }) + .then(() => { + ToggleSnackbar("top", "right", "设置已更改", "success"); + reload(); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + return ( +
+ setTest(false)} + aria-labelledby="form-dialog-title" + > + 发件测试 + + + + 发送测试邮件前,请先保存已更改的邮件设置; + + + 邮件发送结果不会立即反馈,如果您长时间未收到测试邮件,请检查 + Cloudreve 在终端输出的错误日志。 + + + setTestInput(e.target.value)} + type="email" + fullWidth + /> + + + + + + + +
+
+ + 发信 + + +
+
+ + + 发件人名 + + + + 邮件中展示的发件人姓名 + + +
+ +
+ + + 发件人邮箱 + + + + 发件邮箱的地址 + + +
+ +
+ + + SMTP 服务器 + + + + 发件服务器地址,不含端口号 + + +
+ +
+ + + SMTP 端口 + + + + 发件服务器地址端口号 + + +
+ +
+ + + SMTP 用户名 + + + + 发信邮箱用户名,一般与邮箱地址相同 + + +
+ +
+ + + SMTP 密码 + + + + 发信邮箱密码 + + +
+ +
+ + + 回信邮箱 + + + + 用户回复系统发送的邮件时,用于接收回信的邮箱 + + +
+ +
+ + + } + label="强制使用 SSL 连接" + /> + + 是否强制使用 SSL + 加密连接。如果无法发送邮件,可关闭此项, + Cloudreve 会尝试使用 STARTTLS + 并决定是否使用加密连接 + + +
+ +
+ + + SMTP 连接有效期 (秒) + + + + 有效期内建立的 SMTP + 连接会被新邮件发送请求复用 + + +
+
+
+ +
+ + 邮件模板 + + +
+
+ + + 新用户激活 + + + + 新用户注册后激活邮件的模板 + + +
+ +
+ + + 重置密码 + + + + 密码重置邮件模板 + + +
+
+
+ +
+ + {" "} + +
+
+
+ ); +} diff --git a/assets/src/component/Admin/Setting/SiteInformation.js b/assets/src/component/Admin/Setting/SiteInformation.js new file mode 100644 index 00000000..21fbb5df --- /dev/null +++ b/assets/src/component/Admin/Setting/SiteInformation.js @@ -0,0 +1,323 @@ +import Button from "@material-ui/core/Button"; +import FormControl from "@material-ui/core/FormControl"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import Input from "@material-ui/core/Input"; +import InputLabel from "@material-ui/core/InputLabel"; +import MenuItem from "@material-ui/core/MenuItem"; +import Select from "@material-ui/core/Select"; +import { makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + form: { + maxWidth: 400, + marginTop: 20, + marginBottom: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, +})); + +export default function SiteInformation() { + const classes = useStyles(); + const [loading, setLoading] = useState(false); + const [options, setOptions] = useState({ + siteURL: "", + siteName: "", + siteTitle: "", + siteDes: "", + siteICPId: "", + siteScript: "", + pwa_small_icon: "", + pwa_medium_icon: "", + pwa_large_icon: "", + pwa_display: "", + pwa_theme_color: "", + pwa_background_color: "", + }); + + const handleChange = (name) => (event) => { + setOptions({ + ...options, + [name]: event.target.value, + }); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + useEffect(() => { + API.post("/admin/setting", { + keys: Object.keys(options), + }) + .then((response) => { + setOptions(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + // eslint-disable-next-line + }, []); + + const submit = (e) => { + e.preventDefault(); + setLoading(true); + const option = []; + Object.keys(options).forEach((k) => { + option.push({ + key: k, + value: options[k], + }); + }); + API.patch("/admin/setting", { + options: option, + }) + .then(() => { + ToggleSnackbar("top", "right", "设置已更改", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + return ( +
+
+
+ + 基本信息 + +
+
+ + + 主标题 + + + + 站点的主标题 + + +
+
+ + + 副标题 + + + + 站点的副标题 + + +
+
+ + + 站点描述 + + + + 站点描述信息,可能会在分享页面摘要内展示 + + +
+
+ + + 站点URL + + + + 非常重要,请确保与实际情况一致。使用云存储策略、支付平台时,请填入可以被外网访问的地址。 + + +
+
+ + + 网站备案号 + + + + 工信部网站ICP备案号 + + +
+
+ + + 页脚代码 + + + + 在页面底部插入的自定义HTML代码 + + +
+
+
+
+ + 渐进式应用 (PWA) + +
+
+ + + 小图标 + + + + 扩展名为 ico 的小图标地址 + + +
+
+ + + 中图标 + + + + 192x192 的中等图标地址,png 格式 + + +
+
+ + + 大图标 + + + + 512x512 的大图标地址,png 格式 + + +
+
+ + + 展示模式 + + + + PWA 应用添加后的展示模式 + + +
+
+ + + 主题色 + + + + CSS 色值,影响 PWA + 启动画面上状态栏、内容页中状态栏、地址栏的颜色 + + +
+
+
+
+ + + 背景色 + + + + CSS 色值 + + +
+
+
+
+ +
+
+
+ ); +} diff --git a/assets/src/component/Admin/Setting/Theme.js b/assets/src/component/Admin/Setting/Theme.js new file mode 100644 index 00000000..4fa1ecc2 --- /dev/null +++ b/assets/src/component/Admin/Setting/Theme.js @@ -0,0 +1,446 @@ +import Button from "@material-ui/core/Button"; +import FormControl from "@material-ui/core/FormControl"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import IconButton from "@material-ui/core/IconButton"; +import InputLabel from "@material-ui/core/InputLabel"; +import Link from "@material-ui/core/Link"; +import MenuItem from "@material-ui/core/MenuItem"; +import Select from "@material-ui/core/Select"; +import { makeStyles } from "@material-ui/core/styles"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import TextField from "@material-ui/core/TextField"; +import Typography from "@material-ui/core/Typography"; +import { Delete } from "@material-ui/icons"; +import Alert from "@material-ui/lab/Alert"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; +import CreateTheme from "../Dialogs/CreateTheme"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + form: { + maxWidth: 500, + marginTop: 20, + marginBottom: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, + colorContainer: { + display: "flex", + }, + colorDot: { + width: 20, + height: 20, + borderRadius: "50%", + marginLeft: 6, + }, +})); + +export default function Theme() { + const classes = useStyles(); + const [loading, setLoading] = useState(false); + const [theme, setTheme] = useState({}); + const [options, setOptions] = useState({ + themes: "{}", + defaultTheme: "", + home_view_method: "icon", + share_view_method: "list", + }); + const [themeConfig, setThemeConfig] = useState({}); + const [themeConfigError, setThemeConfigError] = useState({}); + const [create, setCreate] = useState(false); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const deleteTheme = (color) => { + if (color === options.defaultTheme) { + ToggleSnackbar("top", "right", "不能删除默认配色", "warning"); + return; + } + if (Object.keys(theme).length <= 1) { + ToggleSnackbar("top", "right", "请至少保留一个配色方案", "warning"); + return; + } + const themeCopy = { ...theme }; + delete themeCopy[color]; + const resStr = JSON.stringify(themeCopy); + setOptions({ + ...options, + themes: resStr, + }); + }; + + const addTheme = (newTheme) => { + setCreate(false); + if (theme[newTheme.palette.primary.main] !== undefined) { + ToggleSnackbar( + "top", + "right", + "主色调不能与已有配色重复", + "warning" + ); + return; + } + const res = { + ...theme, + [newTheme.palette.primary.main]: newTheme, + }; + const resStr = JSON.stringify(res); + setOptions({ + ...options, + themes: resStr, + }); + }; + + useEffect(() => { + const res = JSON.parse(options.themes); + const themeString = {}; + + Object.keys(res).forEach((k) => { + themeString[k] = JSON.stringify(res[k]); + }); + + setTheme(res); + setThemeConfig(themeString); + }, [options.themes]); + + const handleChange = (name) => (event) => { + setOptions({ + ...options, + [name]: event.target.value, + }); + }; + + useEffect(() => { + API.post("/admin/setting", { + keys: Object.keys(options), + }) + .then((response) => { + setOptions(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + // eslint-disable-next-line + }, []); + + const submit = (e) => { + e.preventDefault(); + setLoading(true); + const option = []; + Object.keys(options).forEach((k) => { + option.push({ + key: k, + value: options[k], + }); + }); + API.patch("/admin/setting", { + options: option, + }) + .then(() => { + ToggleSnackbar("top", "right", "设置已更改", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + return ( +
+
+
+ + 主题配色 + +
+
+ + + + 关键色 + 色彩配置 + 操作 + + + + {Object.keys(theme).map((k) => ( + + +
+
+
+
+ + + { + setThemeConfig({ + ...themeConfig, + [k]: e.target.value, + }); + }} + onBlur={(e) => { + try { + const res = JSON.parse( + e.target.value + ); + if ( + !( + "palette" in + res + ) || + !( + "primary" in + res.palette + ) || + !( + "main" in + res.palette + .primary + ) || + !( + "secondary" in + res.palette + ) || + !( + "main" in + res.palette + .secondary + ) + ) { + throw e; + } + setTheme({ + ...theme, + [k]: res, + }); + } catch (e) { + setThemeConfigError( + { + ...themeConfigError, + [k]: true, + } + ); + return; + } + setThemeConfigError({ + ...themeConfigError, + [k]: false, + }); + }} + value={themeConfig[k]} + /> + + + + deleteTheme(k) + } + > + + + + + ))} + +
+
+ +
+ + + 完整的配置项可在{" "} + + 默认主题 - Material-UI + {" "} + 查阅。 + + +
+ +
+ + + 默认配色 + + + + 用户未指定偏好配色时,站点默认使用的配色方案 + + +
+
+
+ +
+ + 界面 + + +
+
+ + + 个人文件列表默认样式 + + + + 用户未指定偏好样式时,个人文件页面列表默认样式 + + +
+
+ +
+
+ + + 目录分享页列表默认样式 + + + + 用户未指定偏好样式时,目录分享页面的默认样式 + + +
+
+
+ +
+ +
+
+ + setCreate(false)} + /> +
+ ); +} diff --git a/assets/src/component/Admin/Setting/UploadDownload.js b/assets/src/component/Admin/Setting/UploadDownload.js new file mode 100644 index 00000000..27a2f45c --- /dev/null +++ b/assets/src/component/Admin/Setting/UploadDownload.js @@ -0,0 +1,504 @@ +import Button from "@material-ui/core/Button"; +import FormControl from "@material-ui/core/FormControl"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import Input from "@material-ui/core/Input"; +import InputLabel from "@material-ui/core/InputLabel"; +import { makeStyles } from "@material-ui/core/styles"; +import Switch from "@material-ui/core/Switch"; +import Typography from "@material-ui/core/Typography"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; +import SizeInput from "../Common/SizeInput"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + form: { + maxWidth: 400, + marginTop: 20, + marginBottom: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, +})); + +export default function UploadDownload() { + const classes = useStyles(); + const [loading, setLoading] = useState(false); + const [options, setOptions] = useState({ + max_worker_num: "1", + max_parallel_transfer: "1", + temp_path: "", + maxEditSize: "0", + onedrive_chunk_retries: "0", + archive_timeout: "0", + download_timeout: "0", + preview_timeout: "0", + doc_preview_timeout: "0", + upload_credential_timeout: "0", + upload_session_timeout: "0", + slave_api_timeout: "0", + onedrive_monitor_timeout: "0", + share_download_session_timeout: "0", + onedrive_callback_check: "0", + reset_after_upload_failed: "0", + onedrive_source_timeout: "0", + }); + + const handleCheckChange = (name) => (event) => { + const value = event.target.checked ? "1" : "0"; + setOptions({ + ...options, + [name]: value, + }); + }; + + const handleChange = (name) => (event) => { + setOptions({ + ...options, + [name]: event.target.value, + }); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + useEffect(() => { + API.post("/admin/setting", { + keys: Object.keys(options), + }) + .then((response) => { + setOptions(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + // eslint-disable-next-line + }, []); + + const submit = (e) => { + e.preventDefault(); + setLoading(true); + const option = []; + Object.keys(options).forEach((k) => { + option.push({ + key: k, + value: options[k], + }); + }); + API.patch("/admin/setting", { + options: option, + }) + .then(() => { + ToggleSnackbar("top", "right", "设置已更改", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + return ( +
+
+
+ + 存储与传输 + +
+
+ + + Worker 数量 + + + + 任务队列最多并行执行的任务数,保存后需要重启 + Cloudreve 生效 + + +
+ +
+ + + 中转并行传输 + + + + 任务队列中转任务传输时,最大并行协程数 + + +
+ +
+ + + 临时目录 + + + + 用于存放打包下载、解压缩、压缩等任务产生的临时文件的目录路径 + + +
+ +
+ + + + 文本文件可在线编辑的最大大小,超出此大小的文件无法在线编辑 + + +
+ +
+ + + OneDrive 分片错误重试 + + + + OneDrive + 存储策略分片上传失败后重试的最大次数,只适用于服务端上传或中转 + + +
+ +
+ + + } + label="上传校验失败时强制重置连接" + /> + + 开启后,如果本次策略、头像等数据上传校验失败,服务器会强制重置连接 + + +
+
+
+ +
+ + 有效期 (秒) + + +
+
+ + + 打包下载 + + + +
+ +
+ + + 下载会话 + + + +
+ +
+ + + 预览链接 + + + +
+ +
+ + + Office 文档预览连接 + + + +
+ +
+ + + 上传凭证 + + + +
+ +
+ + + 上传会话 + + + + 超出后不再处理此上传的回调请求 + + +
+ +
+ + + 从机API请求 + + + +
+ +
+ + + 分享下载会话 + + + + 设定时间内重复下载分享文件,不会被记入总下载次数 + + +
+ +
+ + + OneDrive 客户端上传监控间隔 + + + + 每间隔所设定时间,Cloudreve 会向 OneDrive + 请求检查客户端上传情况已确保客户端上传可控 + + +
+ +
+ + + OneDrive 回调等待 + + + + OneDrive + 客户端上传完成后,等待回调的最大时间,如果超出会被认为上传失败 + + +
+ +
+ + + OneDrive 下载请求缓存 + + + + OneDrive 获取文件下载 URL + 后可将结果缓存,减轻热门文件下载API请求频率 + + +
+
+
+ +
+ +
+
+
+ ); +} diff --git a/assets/src/component/Admin/Share/Share.js b/assets/src/component/Admin/Share/Share.js new file mode 100644 index 00000000..e3361568 --- /dev/null +++ b/assets/src/component/Admin/Share/Share.js @@ -0,0 +1,487 @@ +import { lighten } from "@material-ui/core"; +import Badge from "@material-ui/core/Badge"; +import Button from "@material-ui/core/Button"; +import Checkbox from "@material-ui/core/Checkbox"; +import IconButton from "@material-ui/core/IconButton"; +import Link from "@material-ui/core/Link"; +import Paper from "@material-ui/core/Paper"; +import { makeStyles } from "@material-ui/core/styles"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableContainer from "@material-ui/core/TableContainer"; +import TableHead from "@material-ui/core/TableHead"; +import TablePagination from "@material-ui/core/TablePagination"; +import TableRow from "@material-ui/core/TableRow"; +import TableSortLabel from "@material-ui/core/TableSortLabel"; +import Toolbar from "@material-ui/core/Toolbar"; +import Tooltip from "@material-ui/core/Tooltip"; +import Typography from "@material-ui/core/Typography"; +import { Delete, FilterList } from "@material-ui/icons"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; +import ShareFilter from "../Dialogs/ShareFilter"; +import { formatLocalTime } from "../../../utils/datetime"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + content: { + padding: theme.spacing(2), + }, + container: { + overflowX: "auto", + }, + tableContainer: { + marginTop: 16, + }, + header: { + display: "flex", + justifyContent: "space-between", + }, + headerRight: {}, + highlight: + theme.palette.type === "light" + ? { + color: theme.palette.secondary.main, + backgroundColor: lighten(theme.palette.secondary.light, 0.85), + } + : { + color: theme.palette.text.primary, + backgroundColor: theme.palette.secondary.dark, + }, + visuallyHidden: { + border: 0, + clip: "rect(0 0 0 0)", + height: 1, + margin: -1, + overflow: "hidden", + padding: 0, + position: "absolute", + top: 20, + width: 1, + }, +})); + +export default function Share() { + const classes = useStyles(); + const [shares, setShares] = useState([]); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [total, setTotal] = useState(0); + const [filter, setFilter] = useState({}); + const [users, setUsers] = useState({}); + const [ids, setIds] = useState({}); + const [search, setSearch] = useState({}); + const [orderBy, setOrderBy] = useState(["id", "desc"]); + const [filterDialog, setFilterDialog] = useState(false); + const [selected, setSelected] = useState([]); + const [loading, setLoading] = useState(false); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + const loadList = () => { + API.post("/admin/share/list", { + page: page, + page_size: pageSize, + order_by: orderBy.join(" "), + conditions: filter, + searches: search, + }) + .then((response) => { + setUsers(response.data.users); + setIds(response.data.ids); + setShares(response.data.items); + setTotal(response.data.total); + setSelected([]); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + + useEffect(() => { + loadList(); + }, [page, pageSize, orderBy, filter, search]); + + const deletePolicy = (id) => { + setLoading(true); + API.post("/admin/share/delete", { id: [id] }) + .then(() => { + loadList(); + ToggleSnackbar("top", "right", "分享已删除", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const deleteBatch = () => { + setLoading(true); + API.post("/admin/share/delete", { id: selected }) + .then(() => { + loadList(); + ToggleSnackbar("top", "right", "分享已删除", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const handleSelectAllClick = (event) => { + if (event.target.checked) { + const newSelecteds = shares.map((n) => n.ID); + setSelected(newSelecteds); + return; + } + setSelected([]); + }; + + const handleClick = (event, name) => { + const selectedIndex = selected.indexOf(name); + let newSelected = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, name); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)); + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat( + selected.slice(0, selectedIndex), + selected.slice(selectedIndex + 1) + ); + } + + setSelected(newSelected); + }; + + const isSelected = (id) => selected.indexOf(id) !== -1; + + return ( +
+ setFilterDialog(false)} + setSearch={setSearch} + setFilter={setFilter} + /> +
+
+ + setFilterDialog(true)} + > + + + + + + +
+
+ + + {selected.length > 0 && ( + + + 已选择 {selected.length} 个对象 + + + + + + + + )} + + + + + + 0 && + selected.length < shares.length + } + checked={ + shares.length > 0 && + selected.length === shares.length + } + onChange={handleSelectAllClick} + inputProps={{ + "aria-label": "select all desserts", + }} + /> + + + + setOrderBy([ + "id", + orderBy[1] === "asc" + ? "desc" + : "asc", + ]) + } + > + # + {orderBy[0] === "id" ? ( + + {orderBy[1] === "desc" + ? "sorted descending" + : "sorted ascending"} + + ) : null} + + + + + setOrderBy([ + "source_name", + orderBy[1] === "asc" + ? "desc" + : "asc", + ]) + } + > + 对象名 + {orderBy[0] === "source_name" ? ( + + {orderBy[1] === "desc" + ? "sorted descending" + : "sorted ascending"} + + ) : null} + + + + 类型 + + + + setOrderBy([ + "views", + orderBy[1] === "asc" + ? "desc" + : "asc", + ]) + } + > + 浏览 + {orderBy[0] === "views" ? ( + + {orderBy[1] === "desc" + ? "sorted descending" + : "sorted ascending"} + + ) : null} + + + + + setOrderBy([ + "downloads", + orderBy[1] === "asc" + ? "desc" + : "asc", + ]) + } + > + 下载 + {orderBy[0] === "downloads" ? ( + + {orderBy[1] === "desc" + ? "sorted descending" + : "sorted ascending"} + + ) : null} + + + + 自动过期 + + + 分享者 + + + 分享于 + + + 操作 + + + + + {shares.map((row) => ( + + + + handleClick(event, row.ID) + } + checked={isSelected(row.ID)} + /> + + {row.ID} + + + + {row.Views} + + + {row.Downloads} + + + {row.RemainDownloads > -1 && + row.RemainDownloads + " 次下载后"} + {row.RemainDownloads === -1 && "无"} + + + + {users[row.UserID] + ? users[row.UserID].Nick + : "未知"} + + + + {formatLocalTime( + row.CreatedAt, + "YYYY-MM-DD H:mm:ss" + )} + + + + + deletePolicy(row.ID) + } + size={"small"} + > + + + + + + ))} + +
+
+ setPage(p + 1)} + onChangeRowsPerPage={(e) => { + setPageSize(e.target.value); + setPage(1); + }} + /> +
+
+ ); +} diff --git a/assets/src/component/Admin/Task/Download.js b/assets/src/component/Admin/Task/Download.js new file mode 100644 index 00000000..6272590d --- /dev/null +++ b/assets/src/component/Admin/Task/Download.js @@ -0,0 +1,397 @@ +import { lighten } from "@material-ui/core"; +import Button from "@material-ui/core/Button"; +import Checkbox from "@material-ui/core/Checkbox"; +import IconButton from "@material-ui/core/IconButton"; +import Link from "@material-ui/core/Link"; +import Paper from "@material-ui/core/Paper"; +import { makeStyles } from "@material-ui/core/styles"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableContainer from "@material-ui/core/TableContainer"; +import TableHead from "@material-ui/core/TableHead"; +import TablePagination from "@material-ui/core/TablePagination"; +import TableRow from "@material-ui/core/TableRow"; +import TableSortLabel from "@material-ui/core/TableSortLabel"; +import Toolbar from "@material-ui/core/Toolbar"; +import Tooltip from "@material-ui/core/Tooltip"; +import Typography from "@material-ui/core/Typography"; +import { Delete } from "@material-ui/icons"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; +import { sizeToString } from "../../../utils"; +import ShareFilter from "../Dialogs/ShareFilter"; +import { formatLocalTime } from "../../../utils/datetime"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + content: { + padding: theme.spacing(2), + }, + container: { + overflowX: "auto", + }, + tableContainer: { + marginTop: 16, + }, + header: { + display: "flex", + justifyContent: "space-between", + }, + headerRight: {}, + highlight: + theme.palette.type === "light" + ? { + color: theme.palette.secondary.main, + backgroundColor: lighten(theme.palette.secondary.light, 0.85), + } + : { + color: theme.palette.text.primary, + backgroundColor: theme.palette.secondary.dark, + }, + visuallyHidden: { + border: 0, + clip: "rect(0 0 0 0)", + height: 1, + margin: -1, + overflow: "hidden", + padding: 0, + position: "absolute", + top: 20, + width: 1, + }, +})); + +export default function Download() { + const classes = useStyles(); + const [downloads, setDownloads] = useState([]); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [total, setTotal] = useState(0); + const [filter, setFilter] = useState({}); + const [users, setUsers] = useState({}); + const [search, setSearch] = useState({}); + const [orderBy, setOrderBy] = useState(["id", "desc"]); + const [filterDialog, setFilterDialog] = useState(false); + const [selected, setSelected] = useState([]); + const [loading, setLoading] = useState(false); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const loadList = () => { + API.post("/admin/download/list", { + page: page, + page_size: pageSize, + order_by: orderBy.join(" "), + conditions: filter, + searches: search, + }) + .then((response) => { + setUsers(response.data.users); + setDownloads(response.data.items); + setTotal(response.data.total); + setSelected([]); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + + useEffect(() => { + loadList(); + }, [page, pageSize, orderBy, filter, search]); + + const deletePolicy = (id) => { + setLoading(true); + API.post("/admin/download/delete", { id: [id] }) + .then(() => { + loadList(); + ToggleSnackbar("top", "right", "任务已删除", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const deleteBatch = () => { + setLoading(true); + API.post("/admin/download/delete", { id: selected }) + .then(() => { + loadList(); + ToggleSnackbar("top", "right", "任务已删除", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const handleSelectAllClick = (event) => { + if (event.target.checked) { + const newSelecteds = downloads.map((n) => n.ID); + setSelected(newSelecteds); + return; + } + setSelected([]); + }; + + const handleClick = (event, name) => { + const selectedIndex = selected.indexOf(name); + let newSelected = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, name); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)); + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat( + selected.slice(0, selectedIndex), + selected.slice(selectedIndex + 1) + ); + } + + setSelected(newSelected); + }; + + const isSelected = (id) => selected.indexOf(id) !== -1; + + return ( +
+ setFilterDialog(false)} + setSearch={setSearch} + setFilter={setFilter} + /> +
+
+ +
+
+ + + {selected.length > 0 && ( + + + 已选择 {selected.length} 个对象 + + + + + + + + )} + + + + + + 0 && + selected.length < downloads.length + } + checked={ + downloads.length > 0 && + selected.length === downloads.length + } + onChange={handleSelectAllClick} + inputProps={{ + "aria-label": "select all desserts", + }} + /> + + + + setOrderBy([ + "id", + orderBy[1] === "asc" + ? "desc" + : "asc", + ]) + } + > + # + {orderBy[0] === "id" ? ( + + {orderBy[1] === "desc" + ? "sorted descending" + : "sorted ascending"} + + ) : null} + + + + 源地址 + + + 状态 + + + + setOrderBy([ + "total_size", + orderBy[1] === "asc" + ? "desc" + : "asc", + ]) + } + > + 大小 + {orderBy[0] === "total_size" ? ( + + {orderBy[1] === "desc" + ? "sorted descending" + : "sorted ascending"} + + ) : null} + + + + 创建者 + + + 创建于 + + + 操作 + + + + + {downloads.map((row) => ( + + + + handleClick(event, row.ID) + } + checked={isSelected(row.ID)} + /> + + {row.ID} + + {row.Source} + + + {row.Status === 0 && "就绪"} + {row.Status === 1 && "下载中"} + {row.Status === 2 && "暂停中"} + {row.Status === 3 && "出错"} + {row.Status === 4 && "完成"} + {row.Status === 5 && "取消/停止"} + {row.Status === 6 && "未知"} + + + {sizeToString(row.TotalSize)} + + + + {users[row.UserID] + ? users[row.UserID].Nick + : "未知"} + + + + {formatLocalTime( + row.CreatedAt, + "YYYY-MM-DD H:mm:ss" + )} + + + + + deletePolicy(row.ID) + } + size={"small"} + > + + + + + + ))} + +
+
+ setPage(p + 1)} + onChangeRowsPerPage={(e) => { + setPageSize(e.target.value); + setPage(1); + }} + /> +
+
+ ); +} diff --git a/assets/src/component/Admin/Task/Task.js b/assets/src/component/Admin/Task/Task.js new file mode 100644 index 00000000..a5e9f489 --- /dev/null +++ b/assets/src/component/Admin/Task/Task.js @@ -0,0 +1,385 @@ +import { lighten } from "@material-ui/core"; +import Button from "@material-ui/core/Button"; +import Checkbox from "@material-ui/core/Checkbox"; +import IconButton from "@material-ui/core/IconButton"; +import Link from "@material-ui/core/Link"; +import Paper from "@material-ui/core/Paper"; +import { makeStyles } from "@material-ui/core/styles"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableContainer from "@material-ui/core/TableContainer"; +import TableHead from "@material-ui/core/TableHead"; +import TablePagination from "@material-ui/core/TablePagination"; +import TableRow from "@material-ui/core/TableRow"; +import TableSortLabel from "@material-ui/core/TableSortLabel"; +import Toolbar from "@material-ui/core/Toolbar"; +import Tooltip from "@material-ui/core/Tooltip"; +import Typography from "@material-ui/core/Typography"; +import { Delete } from "@material-ui/icons"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../actions"; +import { getTaskProgress, getTaskStatus, getTaskType } from "../../../config"; +import API from "../../../middleware/Api"; +import ShareFilter from "../Dialogs/ShareFilter"; +import { formatLocalTime } from "../../../utils/datetime"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + content: { + padding: theme.spacing(2), + }, + container: { + overflowX: "auto", + }, + tableContainer: { + marginTop: 16, + }, + header: { + display: "flex", + justifyContent: "space-between", + }, + headerRight: {}, + highlight: + theme.palette.type === "light" + ? { + color: theme.palette.secondary.main, + backgroundColor: lighten(theme.palette.secondary.light, 0.85), + } + : { + color: theme.palette.text.primary, + backgroundColor: theme.palette.secondary.dark, + }, + visuallyHidden: { + border: 0, + clip: "rect(0 0 0 0)", + height: 1, + margin: -1, + overflow: "hidden", + padding: 0, + position: "absolute", + top: 20, + width: 1, + }, +})); + +export default function Task() { + const classes = useStyles(); + const [tasks, setTasks] = useState([]); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [total, setTotal] = useState(0); + const [filter, setFilter] = useState({}); + const [users, setUsers] = useState({}); + const [search, setSearch] = useState({}); + const [orderBy, setOrderBy] = useState(["id", "desc"]); + const [filterDialog, setFilterDialog] = useState(false); + const [selected, setSelected] = useState([]); + const [loading, setLoading] = useState(false); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const loadList = () => { + API.post("/admin/task/list", { + page: page, + page_size: pageSize, + order_by: orderBy.join(" "), + conditions: filter, + searches: search, + }) + .then((response) => { + setUsers(response.data.users); + setTasks(response.data.items); + setTotal(response.data.total); + setSelected([]); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + + useEffect(() => { + loadList(); + }, [page, pageSize, orderBy, filter, search]); + + const deletePolicy = (id) => { + setLoading(true); + API.post("/admin/task/delete", { id: [id] }) + .then(() => { + loadList(); + ToggleSnackbar("top", "right", "任务已删除", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const deleteBatch = () => { + setLoading(true); + API.post("/admin/task/delete", { id: selected }) + .then(() => { + loadList(); + ToggleSnackbar("top", "right", "任务已删除", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const handleSelectAllClick = (event) => { + if (event.target.checked) { + const newSelecteds = tasks.map((n) => n.ID); + setSelected(newSelecteds); + return; + } + setSelected([]); + }; + + const handleClick = (event, name) => { + const selectedIndex = selected.indexOf(name); + let newSelected = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, name); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)); + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat( + selected.slice(0, selectedIndex), + selected.slice(selectedIndex + 1) + ); + } + + setSelected(newSelected); + }; + + const getError = (error) => { + if (error === "") { + return "-"; + } + try { + const res = JSON.parse(error); + return res.msg; + } catch (e) { + return "未知"; + } + }; + + const isSelected = (id) => selected.indexOf(id) !== -1; + + return ( +
+ setFilterDialog(false)} + setSearch={setSearch} + setFilter={setFilter} + /> +
+
+ +
+
+ + + {selected.length > 0 && ( + + + 已选择 {selected.length} 个对象 + + + + + + + + )} + + + + + + 0 && + selected.length < tasks.length + } + checked={ + tasks.length > 0 && + selected.length === tasks.length + } + onChange={handleSelectAllClick} + inputProps={{ + "aria-label": "select all desserts", + }} + /> + + + + setOrderBy([ + "id", + orderBy[1] === "asc" + ? "desc" + : "asc", + ]) + } + > + # + {orderBy[0] === "id" ? ( + + {orderBy[1] === "desc" + ? "sorted descending" + : "sorted ascending"} + + ) : null} + + + + 类型 + + + 状态 + + + 最后进度 + + + 错误信息 + + + 创建者 + + + 创建于 + + + 操作 + + + + + {tasks.map((row) => ( + + + + handleClick(event, row.ID) + } + checked={isSelected(row.ID)} + /> + + {row.ID} + + {getTaskType(row.Type)} + + + {getTaskStatus(row.Status)} + + + {getTaskProgress( + row.Type, + row.Progress + )} + + + {getError(row.Error)} + + + + {users[row.UserID] + ? users[row.UserID].Nick + : "未知"} + + + + {formatLocalTime( + row.CreatedAt, + "YYYY-MM-DD H:mm:ss" + )} + + + + + deletePolicy(row.ID) + } + size={"small"} + > + + + + + + ))} + +
+
+ setPage(p + 1)} + onChangeRowsPerPage={(e) => { + setPageSize(e.target.value); + setPage(1); + }} + /> +
+
+ ); +} diff --git a/assets/src/component/Admin/User/EditUser.js b/assets/src/component/Admin/User/EditUser.js new file mode 100644 index 00000000..6b8b0617 --- /dev/null +++ b/assets/src/component/Admin/User/EditUser.js @@ -0,0 +1,36 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { useParams } from "react-router"; +import API from "../../../middleware/Api"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../../actions"; +import UserForm from "./UserForm"; + +export default function EditUserPreload() { + const [user, setUser] = useState({}); + + const { id } = useParams(); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + useEffect(() => { + setUser({}); + API.get("/admin/user/" + id) + .then((response) => { + // 整型转换 + ["Status", "GroupID"].forEach((v) => { + response.data[v] = response.data[v].toString(); + }); + setUser(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }, [id]); + + return
{user.ID !== undefined && }
; +} diff --git a/assets/src/component/Admin/User/User.js b/assets/src/component/Admin/User/User.js new file mode 100644 index 00000000..5acfaa33 --- /dev/null +++ b/assets/src/component/Admin/User/User.js @@ -0,0 +1,535 @@ +import { lighten } from "@material-ui/core"; +import Badge from "@material-ui/core/Badge"; +import Button from "@material-ui/core/Button"; +import Checkbox from "@material-ui/core/Checkbox"; +import IconButton from "@material-ui/core/IconButton"; +import Link from "@material-ui/core/Link"; +import Paper from "@material-ui/core/Paper"; +import { makeStyles, useTheme } from "@material-ui/core/styles"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableContainer from "@material-ui/core/TableContainer"; +import TableHead from "@material-ui/core/TableHead"; +import TablePagination from "@material-ui/core/TablePagination"; +import TableRow from "@material-ui/core/TableRow"; +import TableSortLabel from "@material-ui/core/TableSortLabel"; +import Toolbar from "@material-ui/core/Toolbar"; +import Tooltip from "@material-ui/core/Tooltip"; +import Typography from "@material-ui/core/Typography"; +import { Block, Delete, Edit, FilterList } from "@material-ui/icons"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { useHistory } from "react-router"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; +import { sizeToString } from "../../../utils"; +import UserFilter from "../Dialogs/UserFilter"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + content: { + padding: theme.spacing(2), + }, + container: { + overflowX: "auto", + }, + tableContainer: { + marginTop: 16, + }, + header: { + display: "flex", + justifyContent: "space-between", + }, + headerRight: {}, + highlight: + theme.palette.type === "light" + ? { + color: theme.palette.secondary.main, + backgroundColor: lighten(theme.palette.secondary.light, 0.85), + } + : { + color: theme.palette.text.primary, + backgroundColor: theme.palette.secondary.dark, + }, + visuallyHidden: { + border: 0, + clip: "rect(0 0 0 0)", + height: 1, + margin: -1, + overflow: "hidden", + padding: 0, + position: "absolute", + top: 20, + width: 1, + }, +})); + +export default function Group() { + const classes = useStyles(); + const [users, setUsers] = useState([]); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [total, setTotal] = useState(0); + const [filter, setFilter] = useState({}); + const [search, setSearch] = useState({}); + const [orderBy, setOrderBy] = useState(["id", "desc"]); + const [filterDialog, setFilterDialog] = useState(false); + const [selected, setSelected] = useState([]); + const [loading, setLoading] = useState(false); + + const history = useHistory(); + const theme = useTheme(); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const loadList = () => { + API.post("/admin/user/list", { + page: page, + page_size: pageSize, + order_by: orderBy.join(" "), + conditions: filter, + searches: search, + }) + .then((response) => { + setUsers(response.data.items); + setTotal(response.data.total); + setSelected([]); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + + useEffect(() => { + loadList(); + }, [page, pageSize, orderBy, filter, search]); + + const deletePolicy = (id) => { + setLoading(true); + API.post("/admin/user/delete", { id: [id] }) + .then(() => { + loadList(); + ToggleSnackbar("top", "right", "用户已删除", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const deleteBatch = () => { + setLoading(true); + API.post("/admin/user/delete", { id: selected }) + .then(() => { + loadList(); + ToggleSnackbar("top", "right", "用户已删除", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const block = (id) => { + setLoading(true); + API.patch("/admin/user/ban/" + id) + .then((response) => { + setUsers( + users.map((v) => { + if (v.ID === id) { + const newUser = { ...v, Status: response.data }; + return newUser; + } + return v; + }) + ); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const handleSelectAllClick = (event) => { + if (event.target.checked) { + const newSelecteds = users.map((n) => n.ID); + setSelected(newSelecteds); + return; + } + setSelected([]); + }; + + const handleClick = (event, name) => { + const selectedIndex = selected.indexOf(name); + let newSelected = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, name); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)); + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat( + selected.slice(0, selectedIndex), + selected.slice(selectedIndex + 1) + ); + } + + setSelected(newSelected); + }; + + const isSelected = (id) => selected.indexOf(id) !== -1; + + return ( +
+ setFilterDialog(false)} + setSearch={setSearch} + setFilter={setFilter} + /> +
+ +
+ + setFilterDialog(true)} + > + + + + + + +
+
+ + + {selected.length > 0 && ( + + + 已选择 {selected.length} 个对象 + + + + + + + + )} + + + + + + 0 && + selected.length < users.length + } + checked={ + users.length > 0 && + selected.length === users.length + } + onChange={handleSelectAllClick} + inputProps={{ + "aria-label": "select all desserts", + }} + /> + + + + setOrderBy([ + "id", + orderBy[1] === "asc" + ? "desc" + : "asc", + ]) + } + > + # + {orderBy[0] === "id" ? ( + + {orderBy[1] === "desc" + ? "sorted descending" + : "sorted ascending"} + + ) : null} + + + + + setOrderBy([ + "nick", + orderBy[1] === "asc" + ? "desc" + : "asc", + ]) + } + > + 昵称 + {orderBy[0] === "nick" ? ( + + {orderBy[1] === "desc" + ? "sorted descending" + : "sorted ascending"} + + ) : null} + + + + + setOrderBy([ + "email", + orderBy[1] === "asc" + ? "desc" + : "asc", + ]) + } + > + Email + {orderBy[0] === "email" ? ( + + {orderBy[1] === "desc" + ? "sorted descending" + : "sorted ascending"} + + ) : null} + + + + 用户组 + + + 状态 + + + + setOrderBy([ + "storage", + orderBy[1] === "asc" + ? "desc" + : "asc", + ]) + } + > + 已用空间 + {orderBy[0] === "storage" ? ( + + {orderBy[1] === "desc" + ? "sorted descending" + : "sorted ascending"} + + ) : null} + + + + 操作 + + + + + {users.map((row) => ( + + + + handleClick(event, row.ID) + } + checked={isSelected(row.ID)} + /> + + {row.ID} + {row.Nick} + {row.Email} + + + {row.Group.Name} + + + + {row.Status === 0 && ( + + 正常 + + )} + {row.Status === 1 && ( + + 未激活 + + )} + {row.Status === 2 && ( + + 被封禁 + + )} + {row.Status === 3 && ( + + 超额封禁 + + )} + + + {sizeToString(row.Storage)} + + + + + history.push( + "/admin/user/edit/" + + row.ID + ) + } + size={"small"} + > + + + + + block(row.ID)} + size={"small"} + > + + + + + + deletePolicy(row.ID) + } + size={"small"} + > + + + + + + ))} + +
+
+ setPage(p + 1)} + onChangeRowsPerPage={(e) => { + setPageSize(e.target.value); + setPage(1); + }} + /> +
+
+ ); +} diff --git a/assets/src/component/Admin/User/UserForm.js b/assets/src/component/Admin/User/UserForm.js new file mode 100644 index 00000000..cd09f648 --- /dev/null +++ b/assets/src/component/Admin/User/UserForm.js @@ -0,0 +1,226 @@ +import Button from "@material-ui/core/Button"; +import FormControl from "@material-ui/core/FormControl"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import Input from "@material-ui/core/Input"; +import InputLabel from "@material-ui/core/InputLabel"; +import MenuItem from "@material-ui/core/MenuItem"; +import Select from "@material-ui/core/Select"; +import { makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { useHistory } from "react-router"; +import { toggleSnackbar } from "../../../actions"; +import API from "../../../middleware/Api"; + +const useStyles = makeStyles((theme) => ({ + root: { + [theme.breakpoints.up("md")]: { + marginLeft: 100, + }, + marginBottom: 40, + }, + form: { + maxWidth: 400, + marginTop: 20, + marginBottom: 20, + }, + formContainer: { + [theme.breakpoints.up("md")]: { + padding: "0px 24px 0 24px", + }, + }, +})); +export default function UserForm(props) { + const classes = useStyles(); + const [loading, setLoading] = useState(false); + const [user, setUser] = useState( + props.user + ? props.user + : { + ID: 0, + Email: "", + Nick: "", + Password: "", // 为空时只读 + Status: "0", // 转换类型 + GroupID: "2", // 转换类型 + } + ); + const [groups, setGroups] = useState([]); + + const history = useHistory(); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + useEffect(() => { + API.get("/admin/groups") + .then((response) => { + setGroups(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }, []); + + const handleChange = (name) => (event) => { + setUser({ + ...user, + [name]: event.target.value, + }); + }; + + const submit = (e) => { + e.preventDefault(); + const userCopy = { ...user }; + + // 整型转换 + ["Status", "GroupID", "Score"].forEach((v) => { + userCopy[v] = parseInt(userCopy[v]); + }); + + setLoading(true); + API.post("/admin/user", { + user: userCopy, + password: userCopy.Password, + }) + .then(() => { + history.push("/admin/user"); + ToggleSnackbar( + "top", + "right", + "用户已" + (props.user ? "保存" : "添加"), + "success" + ); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + return ( +
+
+
+ + {user.ID === 0 && "创建用户"} + {user.ID !== 0 && "编辑 " + user.Nick} + + +
+
+ + + 邮箱 + + + +
+ +
+ + + 昵称 + + + +
+ +
+ + + 密码 + + + + {user.ID !== 0 && "留空表示不修改"} + + +
+ +
+ + + 用户组 + + + + 用户所属用户组 + + +
+ +
+ + + 状态 + + + +
+
+
+
+ +
+
+
+ ); +} diff --git a/assets/src/component/Common/ICPFooter.js b/assets/src/component/Common/ICPFooter.js new file mode 100644 index 00000000..056eaafe --- /dev/null +++ b/assets/src/component/Common/ICPFooter.js @@ -0,0 +1,43 @@ +import { Link, makeStyles } from "@material-ui/core"; +import React, { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { useLocation } from "react-router"; +import pageHelper from "../../utils/page"; + +const useStyles = makeStyles(() => ({ + icp: { + padding: "8px 24px", + position: "absolute", + bottom: 0, + }, +})); + +export const ICPFooter = () => { + const siteICPId = useSelector((state) => state.siteConfig.siteICPId); + const classes = useStyles(); + const location = useLocation(); + const [show, setShow] = useState(true); + + useEffect(() => { + // 只在分享和登录界面显示 + const isSharePage = pageHelper.isSharePage(location.pathname); + const isLoginPage = pageHelper.isLoginPage(location.pathname); + setShow(siteICPId && (isSharePage || isLoginPage)); + }, [siteICPId, location]); + + if (!show) { + return <>; + } + return ( +
+ {`备案号: `} + + {siteICPId} + +
+ ); +}; diff --git a/assets/src/component/Common/Snackbar.js b/assets/src/component/Common/Snackbar.js new file mode 100644 index 00000000..06ed2c3f --- /dev/null +++ b/assets/src/component/Common/Snackbar.js @@ -0,0 +1,154 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import {} from "../../actions"; +import classNames from "classnames"; +import ErrorIcon from "@material-ui/icons/Error"; +import InfoIcon from "@material-ui/icons/Info"; +import CloseIcon from "@material-ui/icons/Close"; +import CheckCircleIcon from "@material-ui/icons/CheckCircle"; +import WarningIcon from "@material-ui/icons/Warning"; + +import { green, amber } from "@material-ui/core/colors"; + +import { + withStyles, + SnackbarContent, + Snackbar, + IconButton, +} from "@material-ui/core"; + +const mapStateToProps = (state) => { + return { + snackbar: state.viewUpdate.snackbar, + }; +}; + +const mapDispatchToProps = () => { + return {}; +}; + +const variantIcon = { + success: CheckCircleIcon, + warning: WarningIcon, + error: ErrorIcon, + info: InfoIcon, +}; + +const styles1 = (theme) => ({ + success: { + backgroundColor: theme.palette.success.main, + }, + error: { + backgroundColor: theme.palette.error.dark, + }, + info: { + backgroundColor: theme.palette.info.main, + }, + warning: { + backgroundColor: theme.palette.warning.main, + }, + icon: { + fontSize: 20, + }, + iconVariant: { + opacity: 0.9, + marginRight: theme.spacing(1), + }, + message: { + display: "flex", + alignItems: "center", + }, +}); + +function MySnackbarContent(props) { + const { classes, className, message, onClose, variant, ...other } = props; + const Icon = variantIcon[variant]; + + return ( + + + {message} + + } + action={[ + + + , + ]} + {...other} + /> + ); +} +MySnackbarContent.propTypes = { + classes: PropTypes.object.isRequired, + className: PropTypes.string, + message: PropTypes.node, + onClose: PropTypes.func, + variant: PropTypes.oneOf(["alert", "success", "warning", "error", "info"]) + .isRequired, +}; + +const MySnackbarContentWrapper = withStyles(styles1)(MySnackbarContent); +const styles = (theme) => ({ + margin: { + margin: theme.spacing(1), + }, +}); +class SnackbarCompoment extends Component { + state = { + open: false, + }; + + UNSAFE_componentWillReceiveProps = (nextProps) => { + if (nextProps.snackbar.toggle !== this.props.snackbar.toggle) { + this.setState({ open: true }); + } + }; + + handleClose = () => { + this.setState({ open: false }); + }; + + render() { + return ( + + + + ); + } +} + +const AlertBar = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(SnackbarCompoment)); + +export default AlertBar; diff --git a/assets/src/component/Dial/Aria2.js b/assets/src/component/Dial/Aria2.js new file mode 100644 index 00000000..40349d1f --- /dev/null +++ b/assets/src/component/Dial/Aria2.js @@ -0,0 +1,45 @@ +import React, { useCallback } from "react"; +import { openRemoteDownloadDialog } from "../../actions"; +import { useDispatch } from "react-redux"; +import AutoHidden from "./AutoHidden"; +import { makeStyles } from "@material-ui/core"; +import Fab from "@material-ui/core/Fab"; +import { Add } from "@material-ui/icons"; +import Modals from "../FileManager/Modals"; + +const useStyles = makeStyles(() => ({ + fab: { + margin: 0, + top: "auto", + right: 20, + bottom: 20, + left: "auto", + zIndex: 5, + position: "fixed", + }, +})); + +export default function RemoteDownloadButton() { + const classes = useStyles(); + const dispatch = useDispatch(); + + const OpenRemoteDownloadDialog = useCallback( + () => dispatch(openRemoteDownloadDialog()), + [dispatch] + ); + + return ( + <> + + + OpenRemoteDownloadDialog()} + > + + + + + ); +} diff --git a/assets/src/component/Dial/AutoHidden.js b/assets/src/component/Dial/AutoHidden.js new file mode 100644 index 00000000..5fa7a422 --- /dev/null +++ b/assets/src/component/Dial/AutoHidden.js @@ -0,0 +1,37 @@ +import React, { useState, useEffect } from "react"; +import Zoom from "@material-ui/core/Zoom"; + +function AutoHidden({ children, enable }) { + const [hidden, setHidden] = useState(false); + + let prev = window.scrollY; + let lastUpdate = window.scrollY; + const show = 50; + + useEffect(() => { + const handleNavigation = (e) => { + const window = e.currentTarget; + + if (prev > window.scrollY) { + if (lastUpdate - window.scrollY > show) { + lastUpdate = window.scrollY; + setHidden(false); + } + } else if (prev < window.scrollY) { + if (window.scrollY - lastUpdate > show) { + lastUpdate = window.scrollY; + setHidden(true); + } + } + prev = window.scrollY; + }; + if (enable) { + window.addEventListener("scroll", (e) => handleNavigation(e)); + } + // eslint-disable-next-line + }, [enable]); + + return {children}; +} + +export default AutoHidden; diff --git a/assets/src/component/Dial/Create.js b/assets/src/component/Dial/Create.js new file mode 100644 index 00000000..cb804fae --- /dev/null +++ b/assets/src/component/Dial/Create.js @@ -0,0 +1,164 @@ +import React, { useCallback, useState, useEffect } from "react"; +import { makeStyles, Badge } from "@material-ui/core"; +import SpeedDial from "@material-ui/lab/SpeedDial"; +import SpeedDialIcon from "@material-ui/lab/SpeedDialIcon"; +import SpeedDialAction from "@material-ui/lab/SpeedDialAction"; +import CreateNewFolderIcon from "@material-ui/icons/CreateNewFolder"; +import PublishIcon from "@material-ui/icons/Publish"; +import { + openCreateFileDialog, + openCreateFolderDialog, + toggleSnackbar, +} from "../../actions"; +import { useDispatch } from "react-redux"; +import AutoHidden from "./AutoHidden"; +import statusHelper from "../../utils/page"; +import Backdrop from "@material-ui/core/Backdrop"; +import { FolderUpload, FilePlus } from "mdi-material-ui"; + +const useStyles = makeStyles(() => ({ + fab: { + margin: 0, + top: "auto", + right: 20, + bottom: 20, + left: "auto", + zIndex: 5, + position: "fixed", + }, + badge: { + position: "absolute", + bottom: 26, + top: "auto", + zIndex: 9999, + right: 7, + }, + "@global": { + ".MuiSpeedDialAction-staticTooltipLabel": { + width: 100, + }, + }, +})); + +export default function UploadButton(props) { + const [open, setOpen] = useState(false); + const [queued, setQueued] = useState(5); + const classes = useStyles(); + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + const OpenNewFolderDialog = useCallback( + () => dispatch(openCreateFolderDialog()), + [dispatch] + ); + const OpenNewFileDialog = useCallback( + () => dispatch(openCreateFileDialog()), + [dispatch] + ); + + useEffect(() => { + setQueued(props.Queued); + }, [props.Queued]); + + const openUpload = (id) => { + const uploadButton = document.getElementsByClassName(id)[0]; + if (document.body.contains(uploadButton)) { + uploadButton.click(); + } else { + ToggleSnackbar("top", "right", "上传组件还未加载完成", "warning"); + } + }; + const uploadClicked = () => { + if (open) { + if (queued !== 0) { + props.openFileList(); + } else { + openUpload("uploadFileForm"); + } + } + }; + + const handleOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + return ( + + + + + + + ); +} diff --git a/assets/src/component/Dial/Save.js b/assets/src/component/Dial/Save.js new file mode 100644 index 00000000..104a18ee --- /dev/null +++ b/assets/src/component/Dial/Save.js @@ -0,0 +1,84 @@ +import React from "react"; +import { makeStyles } from "@material-ui/core"; +import SaveIcon from "@material-ui/icons/Save"; +import CheckIcon from "@material-ui/icons/Check"; +import AutoHidden from "./AutoHidden"; +import statusHelper from "../../utils/page"; +import Fab from "@material-ui/core/Fab"; +import Tooltip from "@material-ui/core/Tooltip"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import { green } from "@material-ui/core/colors"; +import clsx from "clsx"; + +const useStyles = makeStyles((theme) => ({ + fab: { + margin: 0, + top: "auto", + right: 20, + bottom: 20, + left: "auto", + zIndex: 5, + position: "fixed", + }, + badge: { + position: "absolute", + bottom: 26, + top: "auto", + zIndex: 9999, + right: 7, + }, + fabProgress: { + color: green[500], + position: "absolute", + top: -6, + left: -6, + zIndex: 1, + }, + wrapper: { + margin: theme.spacing(1), + position: "relative", + }, + buttonSuccess: { + backgroundColor: green[500], + "&:hover": { + backgroundColor: green[700], + }, + }, +})); + +export default function SaveButton(props) { + const classes = useStyles(); + const buttonClassname = clsx({ + [classes.buttonSuccess]: props.status === "success", + }); + + return ( + +
+
+ + + {props.status === "success" ? ( + + ) : ( + + )} + + + {props.status === "loading" && ( + + )} +
+
+
+ ); +} diff --git a/assets/src/component/Download/Download.js b/assets/src/component/Download/Download.js new file mode 100644 index 00000000..d40b2e18 --- /dev/null +++ b/assets/src/component/Download/Download.js @@ -0,0 +1,200 @@ +import { Button, IconButton, Typography, withStyles } from "@material-ui/core"; +import RefreshIcon from "@material-ui/icons/Refresh"; +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { toggleSnackbar } from "../../actions"; +import API from "../../middleware/Api"; +import DownloadingCard from "./DownloadingCard"; +import FinishedCard from "./FinishedCard"; +import RemoteDownloadButton from "../Dial/Aria2"; +import Auth from "../../middleware/Auth"; + +const styles = (theme) => ({ + actions: { + display: "flex", + }, + title: { + marginTop: "20px", + }, + layout: { + width: "auto", + marginTop: "30px", + marginLeft: theme.spacing(3), + marginRight: theme.spacing(3), + [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { + width: 700, + marginLeft: "auto", + marginRight: "auto", + }, + }, + shareTitle: { + maxWidth: "200px", + }, + avatarFile: { + backgroundColor: theme.palette.primary.light, + }, + avatarFolder: { + backgroundColor: theme.palette.secondary.light, + }, + gird: { + marginTop: "30px", + }, + hide: { + display: "none", + }, + loadingAnimation: { + borderRadius: "6px 6px 0 0", + }, + shareFix: { + marginLeft: "20px", + }, + loadMore: { + textAlign: "center", + marginTop: "20px", + marginBottom: "20px", + }, + margin: { + marginTop: theme.spacing(2), + }, +}); +const mapStateToProps = () => { + return {}; +}; + +const mapDispatchToProps = (dispatch) => { + return { + toggleSnackbar: (vertical, horizontal, msg, color) => { + dispatch(toggleSnackbar(vertical, horizontal, msg, color)); + }, + }; +}; + +class DownloadComponent extends Component { + page = 0; + interval = 0; + + state = { + downloading: [], + loading: false, + finishedList: [], + continue: true, + }; + + componentDidMount = () => { + this.loadDownloading(); + this.loadMore(); + }; + + componentWillUnmount() { + clearTimeout(this.interval); + } + + loadDownloading = () => { + this.setState({ + loading: true, + }); + API.get("/aria2/downloading") + .then((response) => { + this.setState({ + downloading: response.data, + loading: false, + }); + // 设定自动更新 + clearTimeout(this.interval); + if (response.data.length > 0) { + this.interval = setTimeout( + this.loadDownloading, + 1000 * response.data[0].interval + ); + } + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + }); + }; + + loadMore = () => { + this.setState({ + loading: true, + }); + API.get("/aria2/finished?page=" + ++this.page) + .then((response) => { + this.setState({ + finishedList: [ + ...this.state.finishedList, + ...response.data, + ], + loading: false, + continue: response.data.length >= 10, + }); + }) + .catch(() => { + this.props.toggleSnackbar("top", "right", "加载失败", "error"); + this.setState({ + loading: false, + }); + }); + }; + + render() { + const { classes } = this.props; + const user = Auth.GetUser(); + + return ( +
+ {user.group.allowRemoteDownload && } + + 进行中 + + + + + {this.state.downloading.map((value, k) => ( + + ))} + + 已完成 + +
+ {this.state.finishedList.map((value, k) => { + if (value.files) { + return ; + } + return null; + })} + +
+
+ ); + } +} + +const Download = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(DownloadComponent)); + +export default Download; diff --git a/assets/src/component/Download/DownloadingCard.js b/assets/src/component/Download/DownloadingCard.js new file mode 100644 index 00000000..c20e6f71 --- /dev/null +++ b/assets/src/component/Download/DownloadingCard.js @@ -0,0 +1,690 @@ +import { + Card, + CardContent, + darken, + IconButton, + lighten, + LinearProgress, + makeStyles, + Typography, + useTheme, +} from "@material-ui/core"; +import Badge from "@material-ui/core/Badge"; +import Button from "@material-ui/core/Button"; +import Divider from "@material-ui/core/Divider"; +import MuiExpansionPanel from "@material-ui/core/ExpansionPanel"; +import MuiExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails"; +import MuiExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; +import Grid from "@material-ui/core/Grid"; +import withStyles from "@material-ui/core/styles/withStyles"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableRow from "@material-ui/core/TableRow"; +import Tooltip from "@material-ui/core/Tooltip"; +import { ExpandMore, HighlightOff } from "@material-ui/icons"; +import PermMediaIcon from "@material-ui/icons/PermMedia"; +import classNames from "classnames"; +import React, { useCallback, useEffect } from "react"; +import { useDispatch } from "react-redux"; +import TimeAgo from "timeago-react"; +import { toggleSnackbar } from "../../actions"; +import API from "../../middleware/Api"; +import { hex2bin, sizeToString } from "../../utils"; +import TypeIcon from "../FileManager/TypeIcon"; +import SelectFileDialog from "../Modals/SelectFile"; +import { useHistory } from "react-router"; +const ExpansionPanel = withStyles({ + root: { + maxWidth: "100%", + boxShadow: "none", + "&:not(:last-child)": { + borderBottom: 0, + }, + "&:before": { + display: "none", + }, + "&$expanded": {}, + }, + expanded: {}, +})(MuiExpansionPanel); + +const ExpansionPanelSummary = withStyles({ + root: { + minHeight: 0, + padding: 0, + + "&$expanded": { + minHeight: 56, + }, + }, + content: { + maxWidth: "100%", + margin: 0, + display: "flex", + "&$expanded": { + margin: "0", + }, + }, + expanded: {}, +})(MuiExpansionPanelSummary); + +const ExpansionPanelDetails = withStyles((theme) => ({ + root: { + display: "block", + padding: theme.spacing(0), + }, +}))(MuiExpansionPanelDetails); + +const useStyles = makeStyles((theme) => ({ + card: { + marginTop: "20px", + justifyContent: "space-between", + }, + iconContainer: { + width: "90px", + height: "96px", + padding: " 35px 29px 29px 29px", + paddingLeft: "35px", + [theme.breakpoints.down("sm")]: { + display: "none", + }, + }, + content: { + width: "100%", + minWidth: 0, + [theme.breakpoints.up("sm")]: { + borderInlineStart: "1px " + theme.palette.divider + " solid", + }, + }, + contentSide: { + minWidth: 0, + paddingTop: "24px", + paddingRight: "28px", + [theme.breakpoints.down("sm")]: { + display: "none", + }, + }, + iconBig: { + fontSize: "30px", + }, + iconMultiple: { + fontSize: "30px", + color: "#607D8B", + }, + progress: { + marginTop: 8, + marginBottom: 4, + }, + expand: { + transition: ".15s transform ease-in-out", + }, + expanded: { + transform: "rotate(180deg)", + }, + subFileName: { + display: "flex", + }, + subFileIcon: { + marginRight: "20px", + }, + scroll: { + overflowY: "auto", + }, + action: { + padding: theme.spacing(2), + textAlign: "right", + }, + actionButton: { + marginLeft: theme.spacing(1), + }, + info: { + padding: theme.spacing(2), + }, + infoTitle: { + fontWeight: 700, + }, + infoValue: { + color: theme.palette.text.secondary, + }, + bitmap: { + width: "100%", + height: "50px", + backgroundColor: theme.palette.background.default, + }, +})); + +export default function DownloadingCard(props) { + const canvasRef = React.createRef(); + const classes = useStyles(); + const theme = useTheme(); + const history = useHistory(); + + const [expanded, setExpanded] = React.useState(""); + const [task, setTask] = React.useState(props.task); + const [loading, setLoading] = React.useState(false); + const [selectDialogOpen, setSelectDialogOpen] = React.useState(false); + const [selectFileOption, setSelectFileOption] = React.useState([]); + + const handleChange = (panel) => (event, newExpanded) => { + setExpanded(newExpanded ? panel : false); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + useEffect(() => { + setTask(props.task); + }, [props.task]); + + useEffect(() => { + if (task.info.bitfield === "") { + return; + } + let result = ""; + task.info.bitfield.match(/.{1,2}/g).forEach((str) => { + result += hex2bin(str); + }); + const canvas = canvasRef.current; + const context = canvas.getContext("2d"); + context.clearRect(0, 0, canvas.width, canvas.height); + context.strokeStyle = theme.palette.primary.main; + for (let i = 0; i < canvas.width; i++) { + let bit = + result[ + Math.round(((i + 1) / canvas.width) * task.info.numPieces) + ]; + bit = bit ? bit : result.slice(-1); + if (bit === "1") { + context.beginPath(); + context.moveTo(i, 0); + context.lineTo(i, canvas.height); + context.stroke(); + } + } + // eslint-disable-next-line + }, [task.info.bitfield, task.info.numPieces, theme]); + + const getPercent = (completed, total) => { + if (total === 0) { + return 0; + } + return (completed / total) * 100; + }; + + const activeFiles = useCallback(() => { + return task.info.files.filter((v) => v.selected === "true"); + }, [task.info.files]); + + const deleteFile = (index) => { + setLoading(true); + const current = activeFiles(); + const newIndex = []; + const newFiles = []; + // eslint-disable-next-line + current.map((v) => { + if (v.index !== index && v.selected) { + newIndex.push(parseInt(v.index)); + newFiles.push({ + ...v, + selected: "true", + }); + } else { + newFiles.push({ + ...v, + selected: "false", + }); + } + }); + API.put("/aria2/select/" + task.info.gid, { + indexes: newIndex, + }) + .then(() => { + setTask({ + ...task, + info: { + ...task.info, + files: newFiles, + }, + }); + ToggleSnackbar("top", "right", "文件已删除", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const getDownloadName = useCallback(() => { + if (task.info.bittorrent.info.name !== "") { + return task.info.bittorrent.info.name; + } + return task.name === "." ? "[未知]" : task.name; + }, [task]); + + const getIcon = useCallback(() => { + if (task.info.bittorrent.mode === "multi") { + return ( + + + + ); + } else { + return ( + + ); + } + // eslint-disable-next-line + }, [task, classes]); + + const cancel = () => { + setLoading(true); + API.delete("/aria2/task/" + task.info.gid) + .then(() => { + ToggleSnackbar( + "top", + "right", + "任务已取消,状态会在稍后更新", + "success" + ); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const changeSelectedFile = (fileIndex) => { + setLoading(true); + API.put("/aria2/select/" + task.info.gid, { + indexes: fileIndex, + }) + .then(() => { + ToggleSnackbar( + "top", + "right", + "操作成功,状态会在稍后更新", + "success" + ); + setSelectDialogOpen(false); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + return ( + + setSelectDialogOpen(false)} + modalsLoading={loading} + files={selectFileOption} + onSubmit={changeSelectedFile} + /> + + +
{getIcon()}
+ + + + {getDownloadName()} + + + + + {task.total > 0 && ( + + {getPercent( + task.downloaded, + task.total + ).toFixed(2)} + % -{" "} + {task.downloaded === 0 + ? "0Bytes" + : sizeToString(task.downloaded)} + / + {task.total === 0 + ? "0Bytes" + : sizeToString(task.total)}{" "} + -{" "} + {task.speed === "0" + ? "0B/s" + : sizeToString(task.speed) + "/s"} + + )} + {task.total === 0 && - } + + + + + + + +
+ + + {task.info.bittorrent.mode === "multi" && ( +
+ + + {activeFiles().map((value) => { + return ( + + + + + {value.path} + + + + + {" "} + {sizeToString( + value.length + )} + + + + + {getPercent( + value.completedLength, + value.length + ).toFixed(2)} + % + + + + + + deleteFile( + value.index + ) + } + disabled={loading} + size={"small"} + > + + + + + + ); + })} + +
+
+ )} + +
+ + {task.info.bittorrent.mode === "multi" && ( + + )} + +
+ +
+ {task.info.bitfield !== "" && ( + + )} + + + + + 更新于: + + + + + + + + 上传大小: + + + {sizeToString(task.info.uploadLength)} + + + + + 上传速度: + + + {sizeToString(task.info.uploadSpeed)} / s + + + {task.info.bittorrent.mode !== "" && ( + <> + + + InfoHash: + + + {task.info.infoHash} + + + + + 做种者: + + + {task.info.numSeeders} + + + + + 做种中: + + + {task.info.seeder === "true" + ? "是" + : "否"} + + + + )} + + + 分片大小: + + + {sizeToString(task.info.pieceLength)} + + + + + 分片数量: + + + {task.info.numPieces} + + + +
+
+
+
+ ); +} diff --git a/assets/src/component/Download/FinishedCard.js b/assets/src/component/Download/FinishedCard.js new file mode 100644 index 00000000..f6a4b451 --- /dev/null +++ b/assets/src/component/Download/FinishedCard.js @@ -0,0 +1,440 @@ +import React, { useCallback } from "react"; +import { + Card, + CardContent, + IconButton, + makeStyles, + Typography, + useTheme, +} from "@material-ui/core"; +import { sizeToString } from "../../utils"; +import PermMediaIcon from "@material-ui/icons/PermMedia"; +import TypeIcon from "../FileManager/TypeIcon"; +import MuiExpansionPanel from "@material-ui/core/ExpansionPanel"; +import MuiExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; +import MuiExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails"; +import withStyles from "@material-ui/core/styles/withStyles"; +import Divider from "@material-ui/core/Divider"; +import { ExpandMore } from "@material-ui/icons"; +import classNames from "classnames"; +import TableRow from "@material-ui/core/TableRow"; +import TableCell from "@material-ui/core/TableCell"; +import TableBody from "@material-ui/core/TableBody"; +import Table from "@material-ui/core/Table"; +import Badge from "@material-ui/core/Badge"; +import Tooltip from "@material-ui/core/Tooltip"; +import Button from "@material-ui/core/Button"; +import Grid from "@material-ui/core/Grid"; +import API from "../../middleware/Api"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../actions"; +import { useHistory } from "react-router"; +import { formatLocalTime } from "../../utils/datetime"; + +const ExpansionPanel = withStyles({ + root: { + maxWidth: "100%", + boxShadow: "none", + "&:not(:last-child)": { + borderBottom: 0, + }, + "&:before": { + display: "none", + }, + "&$expanded": {}, + }, + expanded: {}, +})(MuiExpansionPanel); + +const ExpansionPanelSummary = withStyles({ + root: { + minHeight: 0, + padding: 0, + + "&$expanded": { + minHeight: 56, + }, + }, + content: { + maxWidth: "100%", + margin: 0, + display: "flex", + "&$expanded": { + margin: "0", + }, + }, + expanded: {}, +})(MuiExpansionPanelSummary); + +const ExpansionPanelDetails = withStyles((theme) => ({ + root: { + display: "block", + padding: theme.spacing(0), + }, +}))(MuiExpansionPanelDetails); + +const useStyles = makeStyles((theme) => ({ + card: { + marginTop: "20px", + justifyContent: "space-between", + }, + iconContainer: { + width: "90px", + height: "96px", + padding: " 35px 29px 29px 29px", + paddingLeft: "35px", + [theme.breakpoints.down("sm")]: { + display: "none", + }, + }, + content: { + width: "100%", + minWidth: 0, + [theme.breakpoints.up("sm")]: { + borderInlineStart: "1px " + theme.palette.divider + " solid", + }, + textAlign: "left", + }, + contentSide: { + minWidth: 0, + paddingTop: "24px", + paddingRight: "28px", + [theme.breakpoints.down("sm")]: { + display: "none", + }, + }, + iconBig: { + fontSize: "30px", + }, + iconMultiple: { + fontSize: "30px", + color: "#607D8B", + }, + progress: { + marginTop: 8, + marginBottom: 4, + }, + expand: { + transition: ".15s transform ease-in-out", + }, + expanded: { + transform: "rotate(180deg)", + }, + subFileName: { + display: "flex", + }, + subFileIcon: { + marginRight: "20px", + }, + scroll: { + overflowY: "auto", + }, + action: { + padding: theme.spacing(2), + textAlign: "right", + }, + actionButton: { + marginLeft: theme.spacing(1), + }, + info: { + padding: theme.spacing(2), + }, + infoTitle: { + fontWeight: 700, + }, + infoValue: { + color: theme.palette.text.secondary, + }, +})); + +export default function FinishedCard(props) { + const classes = useStyles(); + const theme = useTheme(); + const history = useHistory(); + + const [expanded, setExpanded] = React.useState(false); + const [loading, setLoading] = React.useState(false); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const handleChange = () => (event, newExpanded) => { + setExpanded(!!newExpanded); + }; + + const getPercent = (completed, total) => { + if (total === 0) { + return 0; + } + return (completed / total) * 100; + }; + + const cancel = () => { + setLoading(true); + API.delete("/aria2/task/" + props.task.gid) + .then(() => { + ToggleSnackbar("top", "right", "删除成功", "success"); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + window.location.reload(); + }); + }; + + const getDownloadName = useCallback(() => { + return props.task.name === "." ? "[未知]" : props.task.name; + }, [props.task.name]); + + const activeFiles = useCallback(() => { + return props.task.files.filter((v) => v.selected === "true"); + }, [props.task.files]); + + const getIcon = useCallback(() => { + if (props.task.files.length > 1) { + return ( + + + + ); + } else { + return ( + + ); + } + }, [props.task, classes]); + + const getTaskError = (error) => { + try { + const res = JSON.parse(error); + return res.msg + ":" + res.error; + } catch (e) { + return "文件转存失败"; + } + }; + + return ( + + + +
{getIcon()}
+ + + + {getDownloadName()} + + + {props.task.status === 3 && ( + + + 下载出错:{props.task.error} + + + )} + {props.task.status === 5 && ( + + 已取消 + {props.task.error !== "" && ( + :{props.task.error} + )} + + )} + {props.task.status === 4 && + props.task.task_status === 4 && ( + + 已完成 + + )} + {props.task.status === 4 && + props.task.task_status === 0 && ( + + 已完成,转存排队中 + + )} + {props.task.status === 4 && + props.task.task_status === 1 && ( + + 已完成,转存处理中 + + )} + {props.task.status === 4 && + props.task.task_status === 2 && ( + + {getTaskError(props.task.task_error)} + + )} + + + + + + +
+ + + {props.task.files.length > 1 && ( +
+ + + {activeFiles().map((value) => { + return ( + + + + + {value.path} + + + + + {" "} + {sizeToString( + value.length + )} + + + + + {getPercent( + value.completedLength, + value.length + ).toFixed(2)} + % + + + + ); + })} + +
+
+ )} + +
+ + +
+ +
+ + + + 创建日期: + + + {formatLocalTime( + props.task.create, + "YYYY-MM-DD H:mm:ss" + )} + + + + + 最后更新: + + + {formatLocalTime( + props.task.update, + "YYYY-MM-DD H:mm:ss" + )} + + + +
+
+
+
+ ); +} diff --git a/assets/src/component/FileManager/ContextMenu.js b/assets/src/component/FileManager/ContextMenu.js new file mode 100644 index 00000000..7886f0fe --- /dev/null +++ b/assets/src/component/FileManager/ContextMenu.js @@ -0,0 +1,686 @@ +import { + Divider, + ListItemIcon, + MenuItem, + Typography, + withStyles, +} from "@material-ui/core"; +import Menu from "@material-ui/core/Menu"; +import { Archive, InfoOutlined, Unarchive } from "@material-ui/icons"; +import RenameIcon from "@material-ui/icons/BorderColor"; +import DownloadIcon from "@material-ui/icons/CloudDownload"; +import UploadIcon from "@material-ui/icons/CloudUpload"; +import NewFolderIcon from "@material-ui/icons/CreateNewFolder"; +import DeleteIcon from "@material-ui/icons/Delete"; +import FileCopyIcon from "@material-ui/icons/FileCopy"; +import OpenFolderIcon from "@material-ui/icons/FolderOpen"; +import MoveIcon from "@material-ui/icons/Input"; +import LinkIcon from "@material-ui/icons/InsertLink"; +import OpenIcon from "@material-ui/icons/OpenInNew"; +import ShareIcon from "@material-ui/icons/Share"; +import { FolderUpload, MagnetOn, FilePlus } from "mdi-material-ui"; +import PropTypes from "prop-types"; +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { withRouter } from "react-router-dom"; +import { + openCompressDialog, + openCreateFileDialog, + refreshFileList, +} from "../../actions"; +import { + changeContextMenu, + navigateTo, + openCopyDialog, + openCreateFolderDialog, + openDecompressDialog, + openGetSourceDialog, + openLoadingDialog, + openMoveDialog, + openMusicDialog, + openRemoteDownloadDialog, + openRemoveDialog, + openRenameDialog, + openShareDialog, + openTorrentDownloadDialog, + setNavigatorLoadingStatus, + setSelectedTarget, + showImgPreivew, + toggleSnackbar, +} from "../../actions/index"; +import { isCompressFile, isPreviewable, isTorrent } from "../../config"; +import Auth from "../../middleware/Auth"; +import { allowSharePreview } from "../../utils/index"; +import pathHelper from "../../utils/page"; +import RefreshIcon from "@material-ui/icons/Refresh"; +import { openPreview } from "../../actions"; +import { + setSideBar, + toggleObjectInfoSidebar, +} from "../../redux/explorer/action"; + +const styles = () => ({ + propover: {}, + divider: { + marginTop: 4, + marginBottom: 4, + }, +}); + +const StyledListItemIcon = withStyles({ + root: { + minWidth: 38, + }, +})(ListItemIcon); + +const mapStateToProps = (state) => { + return { + menuType: state.viewUpdate.contextType, + menuOpen: state.viewUpdate.contextOpen, + isMultiple: state.explorer.selectProps.isMultiple, + withFolder: state.explorer.selectProps.withFolder, + withFile: state.explorer.selectProps.withFile, + path: state.navigator.path, + selected: state.explorer.selected, + keywords: state.explorer.keywords, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + changeContextMenu: (type, open) => { + dispatch(changeContextMenu(type, open)); + }, + setNavigatorLoadingStatus: (status) => { + dispatch(setNavigatorLoadingStatus(status)); + }, + setSelectedTarget: (targets) => { + dispatch(setSelectedTarget(targets)); + }, + navigateTo: (path) => { + dispatch(navigateTo(path)); + }, + openCreateFolderDialog: () => { + dispatch(openCreateFolderDialog()); + }, + openCreateFileDialog: () => { + dispatch(openCreateFileDialog()); + }, + openRenameDialog: () => { + dispatch(openRenameDialog()); + }, + openMoveDialog: () => { + dispatch(openMoveDialog()); + }, + openRemoveDialog: () => { + dispatch(openRemoveDialog()); + }, + openShareDialog: () => { + dispatch(openShareDialog()); + }, + showImgPreivew: (first) => { + dispatch(showImgPreivew(first)); + }, + openMusicDialog: () => { + dispatch(openMusicDialog()); + }, + toggleSnackbar: (vertical, horizontal, msg, color) => { + dispatch(toggleSnackbar(vertical, horizontal, msg, color)); + }, + openRemoteDownloadDialog: () => { + dispatch(openRemoteDownloadDialog()); + }, + openTorrentDownloadDialog: () => { + dispatch(openTorrentDownloadDialog()); + }, + openGetSourceDialog: () => { + dispatch(openGetSourceDialog()); + }, + openCopyDialog: () => { + dispatch(openCopyDialog()); + }, + openLoadingDialog: (text) => { + dispatch(openLoadingDialog(text)); + }, + openDecompressDialog: () => { + dispatch(openDecompressDialog()); + }, + openCompressDialog: () => { + dispatch(openCompressDialog()); + }, + refreshFileList: () => { + dispatch(refreshFileList()); + }, + openPreview: () => { + dispatch(openPreview()); + }, + toggleObjectInfoSidebar: (open) => { + dispatch(toggleObjectInfoSidebar(open)); + }, + }; +}; + +class ContextMenuCompoment extends Component { + X = 0; + Y = 0; + + state = {}; + + componentDidMount = () => { + window.document.addEventListener("mousemove", this.setPoint); + }; + + setPoint = (e) => { + this.Y = e.clientY; + this.X = e.clientX; + }; + + openArchiveDownload = () => { + this.props.changeContextMenu("file", false); + this.props.openLoadingDialog("打包中..."); + }; + + openDownload = () => { + if (!allowSharePreview()) { + this.props.toggleSnackbar( + "top", + "right", + "未登录用户无法预览", + "warning" + ); + this.props.changeContextMenu("file", false); + return; + } + this.props.changeContextMenu("file", false); + this.props.openLoadingDialog("获取下载地址..."); + }; + + enterFolder = () => { + this.props.navigateTo( + this.props.path === "/" + ? this.props.path + this.props.selected[0].name + : this.props.path + "/" + this.props.selected[0].name + ); + }; + + clickUpload = (id) => { + this.props.changeContextMenu("empty", false); + const uploadButton = document.getElementsByClassName(id)[0]; + if (document.body.contains(uploadButton)) { + uploadButton.click(); + } else { + this.props.toggleSnackbar( + "top", + "right", + "上传组件还未加载完成", + "warning" + ); + } + }; + + // 暂时只对空白处右键菜单使用这个函数,疑似有bug会导致的一个菜单被默认选中。 + // 相关issue: https://github.com/mui-org/material-ui/issues/23747 + renderMenuItems = (items) => { + const res = []; + let key = 0; + + ["top", "center", "bottom"].forEach((position) => { + let visibleCount = 0; + items[position].forEach((item) => { + if (item.condition) { + res.push( + + {item.icon} + + {item.text} + + + ); + key++; + visibleCount++; + } + }); + if (visibleCount > 0 && position != "bottom") { + res.push( + + ); + key++; + } + }); + + return res; + }; + + render() { + const { classes } = this.props; + const user = Auth.GetUser(); + const isHomePage = pathHelper.isHomePage(this.props.location.pathname); + const emptyMenuList = { + top: [ + { + condition: true, + onClick: () => { + this.props.refreshFileList(); + this.props.changeContextMenu( + this.props.menuType, + false + ); + }, + icon: , + text: "刷新", + }, + ], + center: [ + { + condition: true, + onClick: () => this.clickUpload("uploadFileForm"), + icon: , + text: "上传文件", + }, + { + condition: true, + onClick: () => this.clickUpload("uploadFolderForm"), + icon: , + text: "上传目录", + }, + { + condition: user.group.allowRemoteDownload, + onClick: () => this.props.openRemoteDownloadDialog(), + icon: , + text: "离线下载", + }, + ], + bottom: [ + { + condition: true, + onClick: () => this.props.openCreateFolderDialog(), + icon: , + text: "创建文件夹", + }, + { + condition: true, + onClick: () => this.props.openCreateFileDialog(), + icon: , + text: "创建文件", + }, + ], + }; + + return ( +
+ + this.props.changeContextMenu(this.props.menuType, false) + } + anchorReference="anchorPosition" + anchorPosition={{ top: this.Y, left: this.X }} + anchorOrigin={{ + vertical: "top", + horizontal: "left", + }} + transformOrigin={{ + vertical: "top", + horizontal: "left", + }} + > + {this.props.menuType === "empty" && ( +
+ { + this.props.refreshFileList(); + this.props.changeContextMenu( + this.props.menuType, + false + ); + }} + > + + + + 刷新 + + + + this.clickUpload("uploadFileForm") + } + > + + + + + 上传文件 + + + + this.clickUpload("uploadFolderForm") + } + > + + + + + 上传目录 + + + {user.group.allowRemoteDownload && ( + + this.props.openRemoteDownloadDialog() + } + > + + + + + 离线下载 + + + )} + + + + this.props.openCreateFolderDialog() + } + > + + + + + 创建文件夹 + + + + this.props.openCreateFileDialog() + } + > + + + + + 创建文件 + + +
+ )} + {this.props.menuType !== "empty" && ( +
+ {!this.props.isMultiple && this.props.withFolder && ( +
+ + + + + + 进入 + + + {isHomePage && ( + + )} +
+ )} + {!this.props.isMultiple && + this.props.withFile && + (!this.props.share || + this.props.share.preview) && + isPreviewable(this.props.selected[0].name) && ( +
+ + this.props.openPreview() + } + > + + + + + 打开 + + +
+ )} + + {!this.props.isMultiple && this.props.withFile && ( +
+ this.openDownload()} + > + + + + + 下载 + + + {isHomePage && ( + + )} +
+ )} + + {(this.props.isMultiple || this.props.withFolder) && + (user.group.allowArchiveDownload || + !isHomePage) && ( + + this.openArchiveDownload() + } + > + + + + + 打包下载 + + + )} + + {!this.props.isMultiple && + this.props.withFile && + isHomePage && + user.policy.allowSource && ( + + this.props.openGetSourceDialog() + } + > + + + + + 获取外链 + + + )} + + {!this.props.isMultiple && + isHomePage && + user.group.allowRemoteDownload && + this.props.withFile && + isTorrent(this.props.selected[0].name) && ( + + this.props.openTorrentDownloadDialog() + } + > + + + + + 创建离线下载任务 + + + )} + {!this.props.isMultiple && + isHomePage && + user.group.compress && + this.props.withFile && + isCompressFile(this.props.selected[0].name) && ( + + this.props.openDecompressDialog() + } + > + + + + + 解压缩 + + + )} + + {isHomePage && user.group.compress && ( + + this.props.openCompressDialog() + } + > + + + + + 创建压缩文件 + + + )} + + {!this.props.isMultiple && isHomePage && ( + this.props.openShareDialog()} + > + + + + + 创建分享链接 + + + )} + + {!this.props.isMultiple && isHomePage && ( + + this.props.toggleObjectInfoSidebar(true) + } + > + + + + + 详细信息 + + + )} + + {!this.props.isMultiple && isHomePage && ( + + )} + + {!this.props.isMultiple && isHomePage && ( +
+ + this.props.openRenameDialog() + } + > + + + + + 重命名 + + + {this.props.keywords === "" && ( + + this.props.openCopyDialog() + } + > + + + + + 复制 + + + )} +
+ )} + {isHomePage && ( +
+ {this.props.keywords === "" && ( + + this.props.openMoveDialog() + } + > + + + + + 移动 + + + )} + + + + this.props.openRemoveDialog() + } + > + + + + + 删除 + + +
+ )} +
+ )} +
+
+ ); + } +} + +ContextMenuCompoment.propTypes = { + classes: PropTypes.object.isRequired, + menuType: PropTypes.string.isRequired, +}; + +const ContextMenu = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(withRouter(ContextMenuCompoment))); + +export default ContextMenu; diff --git a/assets/src/component/FileManager/DnD/DragLayer.js b/assets/src/component/FileManager/DnD/DragLayer.js new file mode 100644 index 00000000..fdc1e032 --- /dev/null +++ b/assets/src/component/FileManager/DnD/DragLayer.js @@ -0,0 +1,84 @@ +import React from "react"; +import { useDragLayer } from "react-dnd"; +import Preview from "./Preview"; +import { useSelector } from "react-redux"; +const layerStyles = { + position: "fixed", + pointerEvents: "none", + zIndex: 100, + left: 0, + top: 0, + width: "100%", + height: "100%", +}; + +function getItemStyles( + initialOffset, + currentOffset, + pointerOffset, + viewMethod +) { + if (!initialOffset || !currentOffset) { + return { + display: "none", + }; + } + let { x, y } = currentOffset; + if (viewMethod === "list") { + x += pointerOffset.x - initialOffset.x; + y += pointerOffset.y - initialOffset.y; + } + + const transform = `translate(${x}px, ${y}px)`; + return { + opacity: 0.5, + transform, + WebkitTransform: transform, + }; +} +const CustomDragLayer = (props) => { + const { + itemType, + isDragging, + item, + initialOffset, + currentOffset, + pointerOffset, + } = useDragLayer((monitor) => ({ + item: monitor.getItem(), + itemType: monitor.getItemType(), + initialOffset: monitor.getInitialSourceClientOffset(), + currentOffset: monitor.getSourceClientOffset(), + pointerOffset: monitor.getInitialClientOffset(), + isDragging: monitor.isDragging(), + })); + const viewMethod = useSelector( + (state) => state.viewUpdate.explorerViewMethod + ); + function renderItem() { + switch (itemType) { + case "object": + return ; + default: + return null; + } + } + if (!isDragging) { + return null; + } + return ( +
+
+ {renderItem()} +
+
+ ); +}; +export default CustomDragLayer; diff --git a/assets/src/component/FileManager/DnD/DropWarpper.js b/assets/src/component/FileManager/DnD/DropWarpper.js new file mode 100644 index 00000000..2833ffbd --- /dev/null +++ b/assets/src/component/FileManager/DnD/DropWarpper.js @@ -0,0 +1,49 @@ +import React from "react"; +import { useDrop } from "react-dnd"; +import Folder from "../Folder"; +import classNames from "classnames"; +import TableItem from "../TableRow"; +export default function FolderDropWarpper({ + isListView, + folder, + onIconClick, + contextMenu, + handleClick, + handleDoubleClick, + className, + pref, +}) { + const [{ canDrop, isOver }, drop] = useDrop({ + accept: "object", + drop: () => ({ folder }), + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }), + }); + const isActive = canDrop && isOver; + if (!isListView) { + return ( +
+ +
+ ); + } + return ( + + ); +} diff --git a/assets/src/component/FileManager/DnD/Preview.js b/assets/src/component/FileManager/DnD/Preview.js new file mode 100644 index 00000000..93d03700 --- /dev/null +++ b/assets/src/component/FileManager/DnD/Preview.js @@ -0,0 +1,74 @@ +import React from "react"; +import SmallIcon from "../SmallIcon"; +import FileIcon from "../FileIcon"; +import { useSelector } from "react-redux"; +import { makeStyles } from "@material-ui/core"; +import Folder from "../Folder"; + +const useStyles = makeStyles(() => ({ + dragging: { + width: "200px", + }, + cardDragged: { + position: "absolute", + "transform-origin": "bottom left", + }, +})); + +const diliverIcon = (object, viewMethod, classes) => { + if (object.type === "dir") { + return ( +
+ +
+ ); + } + if (object.type === "file" && viewMethod === "icon") { + return ( +
+ +
+ ); + } + if ( + (object.type === "file" && viewMethod === "smallIcon") || + viewMethod === "list" + ) { + return ( +
+ +
+ ); + } +}; + +const Preview = (props) => { + const selected = useSelector((state) => state.explorer.selected); + const viewMethod = useSelector( + (state) => state.viewUpdate.explorerViewMethod + ); + const classes = useStyles(); + return ( + <> + {selected.length === 0 && + diliverIcon(props.object, viewMethod, classes)} + {selected.length > 0 && ( + <> + {selected.slice(0, 3).map((card, i) => ( +
+ {diliverIcon(card, viewMethod, classes)} +
+ ))} + + )} + + ); +}; +export default Preview; diff --git a/assets/src/component/FileManager/Explorer.js b/assets/src/component/FileManager/Explorer.js new file mode 100644 index 00000000..7a42975b --- /dev/null +++ b/assets/src/component/FileManager/Explorer.js @@ -0,0 +1,515 @@ +import { + CircularProgress, + Grid, + Paper, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Typography, + withStyles, +} from "@material-ui/core"; +import TableSortLabel from "@material-ui/core/TableSortLabel"; +import SadIcon from "@material-ui/icons/SentimentVeryDissatisfied"; +import EmptyIcon from "@material-ui/icons/Unarchive"; +import classNames from "classnames"; +import PropTypes from "prop-types"; +import React, { Component } from "react"; +import { configure, GlobalHotKeys } from "react-hotkeys"; +import { connect } from "react-redux"; +import { withRouter } from "react-router-dom"; +import { + changeContextMenu, + navigateTo, + navigateUp, + openRemoveDialog, + setSelectedTarget, +} from "../../actions/index"; +import explorer from "../../redux/explorer"; +import { isMac } from "../../utils"; +import pathHelper from "../../utils/page"; +import ContextMenu from "./ContextMenu"; +import ImgPreivew from "./ImgPreview"; +import ObjectIcon from "./ObjectIcon"; + +const styles = (theme) => ({ + paper: { + padding: theme.spacing(2), + textAlign: "center", + color: theme.palette.text.secondary, + margin: "10px", + }, + root: { + flexGrow: 1, + padding: "10px", + overflowY: "auto", + height: "calc(100vh - 113px)", + [theme.breakpoints.up("sm")]: { + overflowY: "auto", + height: "calc(100vh - 113px)", + }, + [theme.breakpoints.down("sm")]: { + height: "100%", + }, + }, + rootTable: { + padding: "0px", + backgroundColor: theme.palette.background.paper.white, + [theme.breakpoints.up("sm")]: { + overflowY: "auto", + height: "calc(100vh - 113px)", + }, + [theme.breakpoints.down("sm")]: { + height: "100%", + }, + }, + typeHeader: { + margin: "10px 25px", + color: "#6b6b6b", + fontWeight: "500", + }, + loading: { + justifyContent: "center", + display: "flex", + marginTop: "40px", + }, + errorBox: { + padding: theme.spacing(4), + }, + errorMsg: { + marginTop: "10px", + }, + emptyContainer: { + bottom: "0", + height: "300px", + margin: "50px auto", + width: "300px", + color: theme.palette.text.disabled, + textAlign: "center", + paddingTop: "20px", + }, + emptyIcon: { + fontSize: "160px", + }, + emptyInfoBig: { + fontSize: "25px", + color: theme.palette.text.disabled, + }, + emptyInfoSmall: { + color: theme.palette.text.hint, + }, + hideAuto: { + [theme.breakpoints.down("sm")]: { + display: "none", + }, + }, + flexFix: { + minWidth: 0, + }, + upButton: { + marginLeft: "20px", + marginTop: "10px", + marginBottom: "10px", + }, + clickAway: { + height: "100%", + width: "100%", + }, + rootShare: { + height: "100%", + minHeight: 500, + }, + visuallyHidden: { + border: 0, + clip: "rect(0 0 0 0)", + height: 1, + margin: -1, + overflow: "hidden", + padding: 0, + position: "absolute", + top: 20, + width: 1, + }, +}); + +const mapStateToProps = (state) => { + return { + path: state.navigator.path, + drawerDesktopOpen: state.viewUpdate.open, + viewMethod: state.viewUpdate.explorerViewMethod, + sortMethod: state.viewUpdate.sortMethod, + fileList: state.explorer.fileList, + dirList: state.explorer.dirList, + loading: state.viewUpdate.navigatorLoading, + navigatorError: state.viewUpdate.navigatorError, + navigatorErrorMsg: state.viewUpdate.navigatorErrorMsg, + keywords: state.explorer.keywords, + selected: state.explorer.selected, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + navigateToPath: (path) => { + dispatch(navigateTo(path)); + }, + + changeContextMenu: (type, open) => { + dispatch(changeContextMenu(type, open)); + }, + navigateUp: () => { + dispatch(navigateUp()); + }, + setSelectedTarget: (targets) => { + dispatch(setSelectedTarget(targets)); + }, + openRemoveDialog: () => { + dispatch(openRemoveDialog()); + }, + changeSort: (method) => { + dispatch(explorer.actions.changeSortMethod(method)); + }, + }; +}; + +class ExplorerCompoment extends Component { + constructor() { + super(); + this.keyMap = { + DELETE_FILE: "del", + SELECT_ALL: `${isMac() ? "command" : "ctrl"}+a`, + }; + + this.handlers = { + DELETE_FILE: () => { + if (this.props.selected.length > 0 && !this.props.share) { + this.props.openRemoveDialog(); + } + }, + SELECT_ALL: (e) => { + e.preventDefault(); + if ( + this.props.selected.length >= + this.props.dirList.length + this.props.fileList.length + ) { + this.props.setSelectedTarget([]); + } else { + this.props.setSelectedTarget([ + ...this.props.dirList, + ...this.props.fileList, + ]); + } + }, + }; + + configure({ + ignoreTags: ["input", "select", "textarea"], + }); + } + + contextMenu = (e) => { + e.preventDefault(); + if ( + this.props.keywords === "" && + !pathHelper.isSharePage(this.props.location.pathname) + ) { + if (!this.props.loading) { + this.props.changeContextMenu("empty", true); + } + } + }; + + componentDidUpdate() { + this.away = 0; + } + + ClickAway = (e) => { + const element = e.target; + if (element.dataset.clickaway) { + this.props.setSelectedTarget([]); + } + }; + + render() { + const { classes } = this.props; + const isHomePage = pathHelper.isHomePage(this.props.location.pathname); + + const showView = + !this.props.loading && + (this.props.dirList.length !== 0 || + this.props.fileList.length !== 0); + const listView = ( + + + + + { + this.props.changeSort( + this.props.sortMethod === "namePos" + ? "nameRev" + : "namePos" + ); + }} + > + 名称 + {this.props.sortMethod === "namePos" || + this.props.sortMethod === "nameRev" ? ( + + {this.props.sortMethod === "nameRev" + ? "sorted descending" + : "sorted ascending"} + + ) : null} + + + + { + this.props.changeSort( + this.props.sortMethod === "sizePos" + ? "sizeRes" + : "sizePos" + ); + }} + > + 大小 + {this.props.sortMethod === "sizePos" || + this.props.sortMethod === "sizeRes" ? ( + + {this.props.sortMethod === "sizeRes" + ? "sorted descending" + : "sorted ascending"} + + ) : null} + + + + { + this.props.changeSort( + this.props.sortMethod === "timePos" + ? "timeRev" + : "timePos" + ); + }} + > + 日期 + {this.props.sortMethod === "timePos" || + this.props.sortMethod === "timeRev" ? ( + + {this.props.sortMethod === "sizeRes" + ? "sorted descending" + : "sorted ascending"} + + ) : null} + + + + + + {pathHelper.isMobile() && this.props.path !== "/" && ( + + )} + {this.props.dirList.map((value, index) => ( + + ))} + {this.props.fileList.map((value, index) => ( + + ))} + +
+ ); + + const normalView = ( +
+ {this.props.dirList.length !== 0 && ( + <> + + 文件夹 + + + {this.props.dirList.map((value, index) => ( + + + + ))} + + + )} + {this.props.fileList.length !== 0 && ( + <> + + 文件 + + + {this.props.fileList.map((value, index) => ( + + + + ))} + + + )} +
+ ); + const view = this.props.viewMethod === "list" ? listView : normalView; + return ( +
+ + + + {this.props.navigatorError && ( + + + :( 请求时出现错误 + + + {this.props.navigatorErrorMsg.message} + + + )} + + {this.props.loading && !this.props.navigatorError && ( +
+ +
+ )} + + {this.props.keywords === "" && + isHomePage && + this.props.dirList.length === 0 && + this.props.fileList.length === 0 && + !this.props.loading && + !this.props.navigatorError && ( +
+ +
+ 拖拽文件至此 +
+
+ 或点击右下方“上传文件”按钮添加文件 +
+
+ )} + {((this.props.keywords !== "" && + this.props.dirList.length === 0 && + this.props.fileList.length === 0 && + !this.props.loading && + !this.props.navigatorError) || + (this.props.dirList.length === 0 && + this.props.fileList.length === 0 && + !this.props.loading && + !this.props.navigatorError && + !isHomePage)) && ( +
+ +
+ 什么都没有找到 +
+
+ )} + {showView && view} +
+ ); + } +} + +ExplorerCompoment.propTypes = { + classes: PropTypes.object.isRequired, + path: PropTypes.string.isRequired, +}; + +const Explorer = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(withRouter(ExplorerCompoment))); + +export default Explorer; diff --git a/assets/src/component/FileManager/FileIcon.js b/assets/src/component/FileManager/FileIcon.js new file mode 100644 index 00000000..ad350424 --- /dev/null +++ b/assets/src/component/FileManager/FileIcon.js @@ -0,0 +1,290 @@ +import { + ButtonBase, + Divider, + Tooltip, + Typography, + withStyles, + fade, +} from "@material-ui/core"; +import { lighten } from "@material-ui/core/styles"; +import classNames from "classnames"; +import PropTypes from "prop-types"; +import React, { Component } from "react"; +import ContentLoader from "react-content-loader"; +import { LazyLoadImage } from "react-lazy-load-image-component"; +import { connect } from "react-redux"; +import { withRouter } from "react-router"; +import { baseURL } from "../../middleware/Api"; +import pathHelper from "../../utils/page"; +import TypeIcon from "./TypeIcon"; +import CheckCircleRoundedIcon from "@material-ui/icons/CheckCircleRounded"; +import statusHelper from "../../utils/page"; +import Grow from "@material-ui/core/Grow"; + +const styles = (theme) => ({ + container: { + padding: "7px", + }, + + selected: { + "&:hover": { + border: "1px solid #d0d0d0", + }, + backgroundColor: fade( + theme.palette.primary.main, + theme.palette.type === "dark" ? 0.3 : 0.18 + ), + }, + + notSelected: { + "&:hover": { + backgroundColor: theme.palette.background.default, + border: "1px solid #d0d0d0", + }, + backgroundColor: theme.palette.background.paper, + }, + + button: { + border: "1px solid " + theme.palette.divider, + width: "100%", + borderRadius: "6px", + boxSizing: "border-box", + transition: + "background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms", + alignItems: "initial", + display: "initial", + }, + folderNameSelected: { + color: + theme.palette.type === "dark" ? "#fff" : theme.palette.primary.dark, + fontWeight: "500", + }, + folderNameNotSelected: { + color: theme.palette.text.secondary, + }, + folderName: { + marginTop: "15px", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + overflow: "hidden", + marginRight: "20px", + }, + preview: { + overflow: "hidden", + height: "150px", + width: "100%", + borderRadius: "6px 6px 0 0", + backgroundColor: theme.palette.background.default, + }, + previewIcon: { + overflow: "hidden", + height: "149px", + width: "100%", + borderRadius: "5px 5px 0 0", + backgroundColor: theme.palette.background.paper, + paddingTop: "50px", + }, + iconBig: { + fontSize: 50, + }, + picPreview: { + objectFit: "cover", + width: "100%", + height: "100%", + }, + fileInfo: { + height: "50px", + display: "flex", + }, + icon: { + margin: "10px 10px 10px 16px", + height: "30px", + minWidth: "30px", + backgroundColor: theme.palette.background.paper, + borderRadius: "90%", + paddingTop: "3px", + color: theme.palette.text.secondary, + }, + hide: { + display: "none", + }, + loadingAnimation: { + borderRadius: "6px 6px 0 0", + height: "100%", + width: "100%", + }, + shareFix: { + marginLeft: "20px", + }, + checkIcon: { + color: theme.palette.primary.main, + }, +}); + +const mapStateToProps = (state) => { + return { + path: state.navigator.path, + selected: state.explorer.selected, + }; +}; + +const mapDispatchToProps = () => { + return {}; +}; + +class FileIconCompoment extends Component { + static defaultProps = { + share: false, + }; + + state = { + loading: false, + showPicIcon: false, + }; + + render() { + const { classes } = this.props; + const isSelected = + this.props.selected.findIndex((value) => { + return value === this.props.file; + }) !== -1; + const isSharePage = pathHelper.isSharePage( + this.props.location.pathname + ); + const isMobile = statusHelper.isMobile(); + + return ( +
+ + {this.props.file.pic !== "" && + !this.state.showPicIcon && + this.props.file.pic !== " " && + this.props.file.pic !== "null,null" && ( +
+ + this.setState({ loading: false }) + } + beforeLoad={() => + this.setState({ loading: true }) + } + onError={() => + this.setState({ showPicIcon: true }) + } + /> + + + +
+ )} + {(this.props.file.pic === "" || + this.state.showPicIcon || + this.props.file.pic === " " || + this.props.file.pic === "null,null") && ( +
+ +
+ )} + {(this.props.file.pic === "" || + this.state.showPicIcon || + this.props.file.pic === " " || + this.props.file.pic === "null,null") && } +
+ {!this.props.share && ( +
+ {(!isSelected || !isMobile) && ( + + )} + {isSelected && isMobile && ( + + + + )} +
+ )} + + + {this.props.file.name} + + +
+
+
+ ); + } +} + +FileIconCompoment.propTypes = { + classes: PropTypes.object.isRequired, + file: PropTypes.object.isRequired, +}; + +const FileIcon = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(withRouter(FileIconCompoment))); + +export default FileIcon; diff --git a/assets/src/component/FileManager/FileManager.js b/assets/src/component/FileManager/FileManager.js new file mode 100644 index 00000000..112c7a03 --- /dev/null +++ b/assets/src/component/FileManager/FileManager.js @@ -0,0 +1,81 @@ +import React, { Component } from "react"; +import { DndProvider } from "react-dnd"; +import HTML5Backend from "react-dnd-html5-backend"; +import { connect } from "react-redux"; +import { withRouter } from "react-router-dom"; +import { + closeAllModals, + navigateTo, + setSelectedTarget, + toggleSnackbar, +} from "../../actions"; +import { changeSubTitle } from "../../redux/viewUpdate/action"; +import pathHelper from "../../utils/page"; +import DragLayer from "./DnD/DragLayer"; +import Explorer from "./Explorer"; +import Modals from "./Modals"; +import Navigator from "./Navigator/Navigator"; +import SideDrawer from "./Sidebar/SideDrawer"; + +const mapStateToProps = () => ({}); + +const mapDispatchToProps = (dispatch) => { + return { + changeSubTitle: (text) => { + dispatch(changeSubTitle(text)); + }, + setSelectedTarget: (targets) => { + dispatch(setSelectedTarget(targets)); + }, + toggleSnackbar: (vertical, horizontal, msg, color) => { + dispatch(toggleSnackbar(vertical, horizontal, msg, color)); + }, + closeAllModals: () => { + dispatch(closeAllModals()); + }, + navigateTo: (path) => { + dispatch(navigateTo(path)); + }, + }; +}; + +class FileManager extends Component { + constructor(props) { + super(props); + this.image = React.createRef(); + } + componentWillUnmount() { + this.props.setSelectedTarget([]); + this.props.closeAllModals(); + this.props.navigateTo("/"); + } + + componentDidMount() { + if (pathHelper.isHomePage(this.props.location.pathname)) { + this.props.changeSubTitle(null); + } + } + render() { + return ( +
+ + + + + + + +
+ ); + } +} + +FileManager.propTypes = {}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withRouter(FileManager)); diff --git a/assets/src/component/FileManager/Folder.js b/assets/src/component/FileManager/Folder.js new file mode 100644 index 00000000..f5b26761 --- /dev/null +++ b/assets/src/component/FileManager/Folder.js @@ -0,0 +1,129 @@ +import React from "react"; +import FolderIcon from "@material-ui/icons/Folder"; +import classNames from "classnames"; +import { + ButtonBase, + Typography, + Tooltip, + makeStyles, + fade, +} from "@material-ui/core"; +import { useSelector } from "react-redux"; +import { lighten } from "@material-ui/core/styles"; +import statusHelper from "../../utils/page"; +import TypeIcon from "./TypeIcon"; +import CheckCircleRoundedIcon from "@material-ui/icons/CheckCircleRounded"; +const useStyles = makeStyles((theme) => ({ + container: { + padding: "7px", + }, + + selected: { + "&:hover": { + border: "1px solid #d0d0d0", + }, + backgroundColor: fade( + theme.palette.primary.main, + theme.palette.type === "dark" ? 0.3 : 0.18 + ), + }, + + notSelected: { + "&:hover": { + backgroundColor: theme.palette.background.default, + border: "1px solid #d0d0d0", + }, + backgroundColor: theme.palette.background.paper, + }, + + button: { + height: "50px", + border: "1px solid " + theme.palette.divider, + width: "100%", + borderRadius: "6px", + boxSizing: "border-box", + transition: + "background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms", + display: "flex", + justifyContent: "left", + alignItems: "initial", + }, + icon: { + margin: "10px 10px 10px 16px", + height: "30px", + minWidth: "30px", + backgroundColor: theme.palette.background.paper, + borderRadius: "90%", + paddingTop: "3px", + color: theme.palette.text.secondary, + }, + folderNameSelected: { + color: + theme.palette.type === "dark" ? "#fff" : theme.palette.primary.dark, + fontWeight: "500", + }, + folderNameNotSelected: { + color: theme.palette.text.secondary, + }, + folderName: { + marginTop: "15px", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + overflow: "hidden", + marginRight: "20px", + }, + active: { + boxShadow: "0 0 0 2px " + theme.palette.primary.light, + }, + checkIcon: { + color: theme.palette.primary.main, + }, +})); + +export default function Folder({ folder, isActive, onIconClick }) { + const selected = useSelector((state) => state.explorer.selected); + const classes = useStyles(); + const isMobile = statusHelper.isMobile(); + const isSelected = + selected.findIndex((value) => { + return value === folder; + }) !== -1; + + return ( + +
+ {(!isSelected || !isMobile) && } + {isSelected && isMobile && ( + + )} +
+ + + {folder.name} + + +
+ ); +} diff --git a/assets/src/component/FileManager/ImgPreview.js b/assets/src/component/FileManager/ImgPreview.js new file mode 100644 index 00000000..2f96b8b8 --- /dev/null +++ b/assets/src/component/FileManager/ImgPreview.js @@ -0,0 +1,137 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { baseURL } from "../../middleware/Api"; +import { showImgPreivew } from "../../actions/index"; +import { imgPreviewSuffix } from "../../config"; +import { withStyles } from "@material-ui/core"; +import pathHelper from "../../utils/page"; +import { withRouter } from "react-router"; +import { PhotoSlider } from "react-photo-view"; +import "react-photo-view/dist/index.css"; +import * as explorer from "../../redux/explorer/reducer"; + +const styles = () => ({}); + +const mapStateToProps = (state) => { + return { + first: state.explorer.imgPreview.first, + other: state.explorer.imgPreview.other, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + showImgPreivew: (first) => { + dispatch(showImgPreivew(first)); + }, + }; +}; + +class ImagPreviewComponent extends Component { + state = { + items: [], + photoIndex: 0, + isOpen: false, + }; + + UNSAFE_componentWillReceiveProps = (nextProps) => { + const items = []; + let firstOne = 0; + if (nextProps.first.id !== "") { + if ( + pathHelper.isSharePage(this.props.location.pathname) && + !nextProps.first.path + ) { + const newImg = { + intro: nextProps.first.name, + src: baseURL + "/share/preview/" + nextProps.first.key, + }; + firstOne = 0; + items.push(newImg); + this.setState({ + photoIndex: firstOne, + items: items, + isOpen: true, + }); + return; + } + // eslint-disable-next-line + nextProps.other.map((value) => { + const fileType = value.name.split(".").pop().toLowerCase(); + if (imgPreviewSuffix.indexOf(fileType) !== -1) { + let src = ""; + if (pathHelper.isSharePage(this.props.location.pathname)) { + src = baseURL + "/share/preview/" + value.key; + src = + src + + "?path=" + + encodeURIComponent( + value.path === "/" + ? value.path + value.name + : value.path + "/" + value.name + ); + } else { + src = baseURL + "/file/preview/" + value.id; + } + const newImg = { + intro: value.name, + src: src, + }; + if ( + value.path === nextProps.first.path && + value.name === nextProps.first.name + ) { + firstOne = items.length; + } + items.push(newImg); + } + }); + this.setState({ + photoIndex: firstOne, + items: items, + isOpen: true, + }); + } + }; + + handleClose = () => { + this.props.showImgPreivew(explorer.initState.imgPreview.first); + this.setState({ + isOpen: false, + }); + }; + + render() { + const { photoIndex, isOpen, items } = this.state; + + return ( +
+ {isOpen && ( + this.handleClose()} + index={photoIndex} + onIndexChange={(n) => + this.setState({ + photoIndex: n, + }) + } + /> + )} +
+ ); + } +} + +ImagPreviewComponent.propTypes = { + classes: PropTypes.object.isRequired, +}; + +const ImgPreivew = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(withRouter(ImagPreviewComponent))); + +export default ImgPreivew; diff --git a/assets/src/component/FileManager/ImgPreview_old.js b/assets/src/component/FileManager/ImgPreview_old.js new file mode 100644 index 00000000..de2b0e87 --- /dev/null +++ b/assets/src/component/FileManager/ImgPreview_old.js @@ -0,0 +1,155 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { baseURL } from "../../middleware/Api"; +import { showImgPreivew } from "../../actions/index"; +import { imgPreviewSuffix } from "../../config"; +import { withStyles } from "@material-ui/core"; +import Lightbox from "react-image-lightbox"; +import "react-image-lightbox/style.css"; +import pathHelper from "../../utils/page"; +import { withRouter } from "react-router"; + +const styles = () => ({}); + +const mapStateToProps = (state) => { + return { + first: state.explorer.imgPreview.first, + other: state.explorer.imgPreview.other, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + showImgPreivew: (first) => { + dispatch(showImgPreivew(first)); + }, + }; +}; + +class ImgPreviewCompoment extends Component { + state = { + items: [], + photoIndex: 0, + isOpen: false, + }; + + UNSAFE_componentWillReceiveProps = (nextProps) => { + const items = []; + let firstOne = 0; + if (nextProps.first !== null) { + if ( + pathHelper.isSharePage(this.props.location.pathname) && + !nextProps.first.path + ) { + const newImg = { + title: nextProps.first.name, + src: baseURL + "/share/preview/" + nextProps.first.key, + }; + firstOne = 0; + items.push(newImg); + this.setState({ + photoIndex: firstOne, + items: items, + isOpen: true, + }); + return; + } + // eslint-disable-next-line + nextProps.other.map((value) => { + const fileType = value.name.split(".").pop().toLowerCase(); + if (imgPreviewSuffix.indexOf(fileType) !== -1) { + let src = ""; + if (pathHelper.isSharePage(this.props.location.pathname)) { + src = baseURL + "/share/preview/" + value.key; + src = + src + + "?path=" + + encodeURIComponent( + value.path === "/" + ? value.path + value.name + : value.path + "/" + value.name + ); + } else { + src = baseURL + "/file/preview/" + value.id; + } + const newImg = { + title: value.name, + src: src, + }; + if ( + value.path === nextProps.first.path && + value.name === nextProps.first.name + ) { + firstOne = items.length; + } + items.push(newImg); + } + }); + this.setState({ + photoIndex: firstOne, + items: items, + isOpen: true, + }); + } + }; + + handleClose = () => { + this.props.showImgPreivew(null); + this.setState({ + isOpen: false, + }); + }; + + render() { + const { photoIndex, isOpen, items } = this.state; + + return ( +
+ {isOpen && ( + this.handleClose()} + imageLoadErrorMessage="无法加载此图像" + imageCrossOrigin="anonymous" + imageTitle={items[photoIndex].title} + onMovePrevRequest={() => + this.setState({ + photoIndex: + (photoIndex + items.length - 1) % + items.length, + }) + } + reactModalStyle={{ + overlay: { + zIndex: 10000, + }, + }} + onMoveNextRequest={() => + this.setState({ + photoIndex: (photoIndex + 1) % items.length, + }) + } + /> + )} +
+ ); + } +} + +ImgPreviewCompoment.propTypes = { + classes: PropTypes.object.isRequired, +}; + +const ImgPreivew = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(withRouter(ImgPreviewCompoment))); + +export default ImgPreivew; diff --git a/assets/src/component/FileManager/Modals.js b/assets/src/component/FileManager/Modals.js new file mode 100644 index 00000000..08b2c8d0 --- /dev/null +++ b/assets/src/component/FileManager/Modals.js @@ -0,0 +1,1104 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { + closeAllModals, + toggleSnackbar, + setModalsLoading, + refreshFileList, + refreshStorage, + openLoadingDialog, +} from "../../actions/index"; +import PathSelector from "./PathSelector"; +import API, { baseURL } from "../../middleware/Api"; +import { + withStyles, + Button, + TextField, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + DialogContentText, + CircularProgress, +} from "@material-ui/core"; +import Loading from "../Modals/Loading"; +import CopyDialog from "../Modals/Copy"; +import CreatShare from "../Modals/CreateShare"; +import { withRouter } from "react-router-dom"; +import pathHelper from "../../utils/page"; +import DecompressDialog from "../Modals/Decompress"; +import CompressDialog from "../Modals/Compress"; + +const styles = (theme) => ({ + wrapper: { + margin: theme.spacing(1), + position: "relative", + }, + buttonProgress: { + color: theme.palette.secondary.light, + position: "absolute", + top: "50%", + left: "50%", + marginTop: -12, + marginLeft: -12, + }, + contentFix: { + padding: "10px 24px 0px 24px", + }, +}); + +const mapStateToProps = (state) => { + return { + path: state.navigator.path, + selected: state.explorer.selected, + modalsStatus: state.viewUpdate.modals, + modalsLoading: state.viewUpdate.modalsLoading, + dirList: state.explorer.dirList, + fileList: state.explorer.fileList, + dndSignale: state.explorer.dndSignal, + dndTarget: state.explorer.dndTarget, + dndSource: state.explorer.dndSource, + loading: state.viewUpdate.modals.loading, + loadingText: state.viewUpdate.modals.loadingText, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + closeAllModals: () => { + dispatch(closeAllModals()); + }, + toggleSnackbar: (vertical, horizontal, msg, color) => { + dispatch(toggleSnackbar(vertical, horizontal, msg, color)); + }, + setModalsLoading: (status) => { + dispatch(setModalsLoading(status)); + }, + refreshFileList: () => { + dispatch(refreshFileList()); + }, + refreshStorage: () => { + dispatch(refreshStorage()); + }, + openLoadingDialog: (text) => { + dispatch(openLoadingDialog(text)); + }, + }; +}; + +class ModalsCompoment extends Component { + state = { + newFolderName: "", + newFileName: "", + newName: "", + selectedPath: "", + selectedPathName: "", + secretShare: false, + sharePwd: "", + shareUrl: "", + downloadURL: "", + remoteDownloadPathSelect: false, + source: "", + purchaseCallback: null, + }; + + handleInputChange = (e) => { + this.setState({ + [e.target.id]: e.target.value, + }); + }; + + newNameSuffix = ""; + downloaded = false; + + UNSAFE_componentWillReceiveProps = (nextProps) => { + if (this.props.dndSignale !== nextProps.dndSignale) { + this.dragMove(nextProps.dndSource, nextProps.dndTarget); + return; + } + if (this.props.loading !== nextProps.loading) { + // 打包下载 + if (nextProps.loading === true) { + if (nextProps.loadingText === "打包中...") { + if ( + pathHelper.isSharePage(this.props.location.pathname) && + this.props.share && + this.props.share.score > 0 + ) { + this.scoreHandler(this.archiveDownload); + return; + } + this.archiveDownload(); + } else if (nextProps.loadingText === "获取下载地址...") { + if ( + pathHelper.isSharePage(this.props.location.pathname) && + this.props.share && + this.props.share.score > 0 + ) { + this.scoreHandler(this.Download); + return; + } + this.Download(); + } + } + return; + } + if (this.props.modalsStatus.rename !== nextProps.modalsStatus.rename) { + const name = nextProps.selected[0].name; + this.setState({ + newName: name, + }); + return; + } + if ( + this.props.modalsStatus.getSource !== + nextProps.modalsStatus.getSource && + nextProps.modalsStatus.getSource === true + ) { + API.get("/file/source/" + this.props.selected[0].id) + .then((response) => { + this.setState({ + source: response.data.url, + }); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + }); + } + }; + + scoreHandler = (callback) => { + callback(); + }; + + Download = () => { + let reqURL = ""; + if (this.props.selected[0].key) { + const downloadPath = + this.props.selected[0].path === "/" + ? this.props.selected[0].path + this.props.selected[0].name + : this.props.selected[0].path + + "/" + + this.props.selected[0].name; + reqURL = + "/share/download/" + + this.props.selected[0].key + + "?path=" + + encodeURIComponent(downloadPath); + } else { + reqURL = "/file/download/" + this.props.selected[0].id; + } + + API.put(reqURL) + .then((response) => { + window.location.assign(response.data); + this.onClose(); + this.downloaded = true; + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + this.onClose(); + }); + }; + + archiveDownload = () => { + const dirs = [], + items = []; + this.props.selected.map((value) => { + if (value.type === "dir") { + dirs.push(value.id); + } else { + items.push(value.id); + } + return null; + }); + + let reqURL = "/file/archive"; + const postBody = { + items: items, + dirs: dirs, + }; + if (pathHelper.isSharePage(this.props.location.pathname)) { + reqURL = "/share/archive/" + window.shareInfo.key; + postBody["path"] = this.props.selected[0].path; + } + + API.post(reqURL, postBody) + .then((response) => { + if (response.rawData.code === 0) { + this.onClose(); + window.location.assign(response.data); + } else { + this.props.toggleSnackbar( + "top", + "right", + response.rawData.msg, + "warning" + ); + } + this.onClose(); + this.props.refreshStorage(); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + this.onClose(); + }); + }; + + submitRemove = (e) => { + e.preventDefault(); + this.props.setModalsLoading(true); + const dirs = [], + items = []; + // eslint-disable-next-line + this.props.selected.map((value) => { + if (value.type === "dir") { + dirs.push(value.id); + } else { + items.push(value.id); + } + }); + API.delete("/object", { + data: { + items: items, + dirs: dirs, + }, + }) + .then((response) => { + if (response.rawData.code === 0) { + this.onClose(); + setTimeout(this.props.refreshFileList, 500); + } else { + this.props.toggleSnackbar( + "top", + "right", + response.rawData.msg, + "warning" + ); + } + this.props.setModalsLoading(false); + this.props.refreshStorage(); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + this.props.setModalsLoading(false); + }); + }; + + submitMove = (e) => { + if (e != null) { + e.preventDefault(); + } + this.props.setModalsLoading(true); + const dirs = [], + items = []; + // eslint-disable-next-line + this.props.selected.map((value) => { + if (value.type === "dir") { + dirs.push(value.id); + } else { + items.push(value.id); + } + }); + API.patch("/object", { + action: "move", + src_dir: this.props.selected[0].path, + src: { + dirs: dirs, + items: items, + }, + dst: this.DragSelectedPath + ? this.DragSelectedPath + : this.state.selectedPath === "//" + ? "/" + : this.state.selectedPath, + }) + .then(() => { + this.onClose(); + this.props.refreshFileList(); + this.props.setModalsLoading(false); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + this.props.setModalsLoading(false); + }) + .then(() => { + this.props.closeAllModals(); + }); + }; + + dragMove = (source, target) => { + if (this.props.selected.length === 0) { + this.props.selected[0] = source; + } + let doMove = true; + + // eslint-disable-next-line + this.props.selected.map((value) => { + // 根据ID过滤 + if (value.id === target.id && value.type === target.type) { + doMove = false; + // eslint-disable-next-line + return; + } + // 根据路径过滤 + if ( + value.path === + target.path + (target.path === "/" ? "" : "/") + target.name + ) { + doMove = false; + // eslint-disable-next-line + return; + } + }); + if (doMove) { + this.DragSelectedPath = + target.path === "/" + ? target.path + target.name + : target.path + "/" + target.name; + this.props.openLoadingDialog("处理中..."); + this.submitMove(); + } + }; + + submitRename = (e) => { + e.preventDefault(); + this.props.setModalsLoading(true); + const newName = this.state.newName; + + const src = { + dirs: [], + items: [], + }; + + if (this.props.selected[0].type === "dir") { + src.dirs[0] = this.props.selected[0].id; + } else { + src.items[0] = this.props.selected[0].id; + } + + // 检查重名 + if ( + this.props.dirList.findIndex((value) => { + return value.name === newName; + }) !== -1 || + this.props.fileList.findIndex((value) => { + return value.name === newName; + }) !== -1 + ) { + this.props.toggleSnackbar( + "top", + "right", + "新名称与已有文件重复", + "warning" + ); + this.props.setModalsLoading(false); + } else { + API.post("/object/rename", { + action: "rename", + src: src, + new_name: newName, + }) + .then(() => { + this.onClose(); + this.props.refreshFileList(); + this.props.setModalsLoading(false); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + this.props.setModalsLoading(false); + }); + } + }; + + submitCreateNewFolder = (e) => { + e.preventDefault(); + this.props.setModalsLoading(true); + if ( + this.props.dirList.findIndex((value) => { + return value.name === this.state.newFolderName; + }) !== -1 + ) { + this.props.toggleSnackbar( + "top", + "right", + "文件夹名称重复", + "warning" + ); + this.props.setModalsLoading(false); + } else { + API.put("/directory", { + path: + (this.props.path === "/" ? "" : this.props.path) + + "/" + + this.state.newFolderName, + }) + .then(() => { + this.onClose(); + this.props.refreshFileList(); + this.props.setModalsLoading(false); + }) + .catch((error) => { + this.props.setModalsLoading(false); + + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + }); + } + //this.props.toggleSnackbar(); + }; + + submitCreateNewFile = (e) => { + e.preventDefault(); + this.props.setModalsLoading(true); + if ( + this.props.dirList.findIndex((value) => { + return value.name === this.state.newFileName; + }) !== -1 + ) { + this.props.toggleSnackbar( + "top", + "right", + "文件名称重复", + "warning" + ); + this.props.setModalsLoading(false); + } else { + API.post("/file/create", { + path: + (this.props.path === "/" ? "" : this.props.path) + + "/" + + this.state.newFileName, + }) + .then(() => { + this.onClose(); + this.props.refreshFileList(); + this.props.setModalsLoading(false); + }) + .catch((error) => { + this.props.setModalsLoading(false); + + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + }); + } + //this.props.toggleSnackbar(); + }; + + submitTorrentDownload = (e) => { + e.preventDefault(); + this.props.setModalsLoading(true); + API.post("/aria2/torrent/" + this.props.selected[0].id, { + dst: + this.state.selectedPath === "//" + ? "/" + : this.state.selectedPath, + }) + .then(() => { + this.props.toggleSnackbar( + "top", + "right", + "任务已创建", + "success" + ); + this.onClose(); + this.props.setModalsLoading(false); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + this.props.setModalsLoading(false); + }); + }; + + submitDownload = (e) => { + e.preventDefault(); + this.props.setModalsLoading(true); + API.post("/aria2/url", { + url: this.state.downloadURL, + dst: + this.state.selectedPath === "//" + ? "/" + : this.state.selectedPath, + }) + .then(() => { + this.props.toggleSnackbar( + "top", + "right", + "任务已创建", + "success" + ); + this.onClose(); + this.props.setModalsLoading(false); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + this.props.setModalsLoading(false); + }); + }; + + setMoveTarget = (folder) => { + const path = + folder.path === "/" + ? folder.path + folder.name + : folder.path + "/" + folder.name; + this.setState({ + selectedPath: path, + selectedPathName: folder.name, + }); + }; + + remoteDownloadNext = () => { + this.props.closeAllModals(); + this.setState({ + remoteDownloadPathSelect: true, + }); + }; + + onClose = () => { + this.setState({ + newFolderName: "", + newFileName: "", + newName: "", + selectedPath: "", + selectedPathName: "", + secretShare: false, + sharePwd: "", + downloadURL: "", + shareUrl: "", + remoteDownloadPathSelect: false, + source: "", + }); + this.newNameSuffix = ""; + this.props.closeAllModals(); + }; + + handleChange = (name) => (event) => { + this.setState({ [name]: event.target.checked }); + }; + + render() { + const { classes } = this.props; + + return ( +
+ + + + 获取文件外链 + + + +
+ + +
+ + + +
+ + 新建文件夹 + + +
+ this.handleInputChange(e)} + fullWidth + /> + +
+ + +
+ +
+
+
+ + + 新建文件 + + +
+ this.handleInputChange(e)} + fullWidth + /> + +
+ + +
+ +
+
+
+ + + 重命名 + + + 输入{" "} + + {this.props.selected.length === 1 + ? this.props.selected[0].name + : ""} + {" "} + 的新名称: + +
+ this.handleInputChange(e)} + fullWidth + /> + +
+ + +
+ +
+
+
+ + + + 移动至 + + + {this.state.selectedPath !== "" && ( + + + 移动至{" "} + {this.state.selectedPathName} + + + )} + + +
+ +
+
+
+ + 删除对象 + + + + 确定要删除 + {this.props.selected.length === 1 && ( + {this.props.selected[0].name} + )} + {this.props.selected.length > 1 && ( + + 这{this.props.selected.length}个对象 + + )} + 吗? + + + + +
+ +
+
+
+ + + + + 音频播放 + + + + {this.props.selected.length !== 0 && ( + + + + + + + + + 新建离线下载任务 + + + + + + + + + + + + + + + 选择存储位置 + + + + {this.state.selectedPath !== "" && ( + + + 下载至{" "} + {this.state.selectedPathName} + + + )} + + +
+ +
+
+
+ + + 选择存储位置 + + + + {this.state.selectedPath !== "" && ( + + + 下载至{" "} + {this.state.selectedPathName} + + + )} + + +
+ +
+
+
+ + + +
+ ); + } +} + +ModalsCompoment.propTypes = { + classes: PropTypes.object.isRequired, +}; + +const Modals = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(withRouter(ModalsCompoment))); + +export default Modals; diff --git a/assets/src/component/FileManager/Navigator/DropDown.js b/assets/src/component/FileManager/Navigator/DropDown.js new file mode 100644 index 00000000..d7ae5fae --- /dev/null +++ b/assets/src/component/FileManager/Navigator/DropDown.js @@ -0,0 +1,50 @@ +import React from "react"; +import DropDownItem from "./DropDownItem"; + +export default function DropDown(props) { + let timer; + let first = props.folders.length; + const status = []; + for (let index = 0; index < props.folders.length; index++) { + status[index] = false; + } + + const setActiveStatus = (id, value) => { + status[id] = value; + if (value) { + clearTimeout(timer); + } else { + let shouldClose = true; + status.forEach((element) => { + if (element) { + shouldClose = false; + } + }); + if (shouldClose) { + if (first <= 0) { + timer = setTimeout(() => { + props.onClose(); + }, 100); + } else { + first--; + } + } + } + console.log(status); + }; + + return ( + <> + {props.folders.map((folder, id) => ( + + ))} + + ); +} diff --git a/assets/src/component/FileManager/Navigator/DropDownItem.js b/assets/src/component/FileManager/Navigator/DropDownItem.js new file mode 100644 index 00000000..6df0a109 --- /dev/null +++ b/assets/src/component/FileManager/Navigator/DropDownItem.js @@ -0,0 +1,54 @@ +import React, { useEffect } from "react"; +import { makeStyles } from "@material-ui/core"; +import FolderIcon from "@material-ui/icons/Folder"; +import { MenuItem, ListItemIcon, ListItemText } from "@material-ui/core"; +import { useDrop } from "react-dnd"; +import classNames from "classnames"; + +const useStyles = makeStyles((theme) => ({ + active: { + border: "2px solid " + theme.palette.primary.light, + }, +})); + +export default function DropDownItem(props) { + const [{ canDrop, isOver }, drop] = useDrop({ + accept: "object", + drop: () => { + console.log({ + folder: { + id: -1, + path: props.path, + name: props.folder === "/" ? "" : props.folder, + }, + }); + }, + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }), + }); + + const isActive = canDrop && isOver; + + useEffect(() => { + props.setActiveStatus(props.id, isActive); + // eslint-disable-next-line + }, [isActive]); + + const classes = useStyles(); + return ( + props.navigateTo(e, props.id)} + > + + + + + + ); +} diff --git a/assets/src/component/FileManager/Navigator/Navigator.js b/assets/src/component/FileManager/Navigator/Navigator.js new file mode 100644 index 00000000..72ff930d --- /dev/null +++ b/assets/src/component/FileManager/Navigator/Navigator.js @@ -0,0 +1,499 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { withRouter } from "react-router-dom"; +import RightIcon from "@material-ui/icons/KeyboardArrowRight"; +import ShareIcon from "@material-ui/icons/Share"; +import NewFolderIcon from "@material-ui/icons/CreateNewFolder"; +import RefreshIcon from "@material-ui/icons/Refresh"; +import { + navigateTo, + navigateUp, + setNavigatorError, + setNavigatorLoadingStatus, + refreshFileList, + setSelectedTarget, + openCreateFolderDialog, + openShareDialog, + drawerToggleAction, + openCompressDialog, +} from "../../../actions/index"; +import explorer from "../../../redux/explorer"; +import API from "../../../middleware/Api"; +import { setCookie, setGetParameter, fixUrlHash } from "../../../utils/index"; +import { + withStyles, + Divider, + Menu, + MenuItem, + ListItemIcon, +} from "@material-ui/core"; +import PathButton from "./PathButton"; +import DropDown from "./DropDown"; +import pathHelper from "../../../utils/page"; +import classNames from "classnames"; +import Auth from "../../../middleware/Auth"; +import Avatar from "@material-ui/core/Avatar"; +import { Archive } from "@material-ui/icons"; +import { FilePlus } from "mdi-material-ui"; +import { openCreateFileDialog } from "../../../actions"; +import SubActions from "./SubActions"; + +const mapStateToProps = (state) => { + return { + path: state.navigator.path, + refresh: state.navigator.refresh, + drawerDesktopOpen: state.viewUpdate.open, + viewMethod: state.viewUpdate.explorerViewMethod, + keywords: state.explorer.keywords, + sortMethod: state.viewUpdate.sortMethod, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + navigateToPath: (path) => { + dispatch(navigateTo(path)); + }, + navigateUp: () => { + dispatch(navigateUp()); + }, + setNavigatorError: (status, msg) => { + dispatch(setNavigatorError(status, msg)); + }, + updateFileList: (list) => { + dispatch(explorer.actions.updateFileList(list)); + }, + setNavigatorLoadingStatus: (status) => { + dispatch(setNavigatorLoadingStatus(status)); + }, + refreshFileList: () => { + dispatch(refreshFileList()); + }, + setSelectedTarget: (target) => { + dispatch(setSelectedTarget(target)); + }, + openCreateFolderDialog: () => { + dispatch(openCreateFolderDialog()); + }, + openCreateFileDialog: () => { + dispatch(openCreateFileDialog()); + }, + openShareDialog: () => { + dispatch(openShareDialog()); + }, + handleDesktopToggle: (open) => { + dispatch(drawerToggleAction(open)); + }, + openCompressDialog: () => { + dispatch(openCompressDialog()); + }, + }; +}; + +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +const styles = (theme) => ({ + container: { + [theme.breakpoints.down("xs")]: { + display: "none", + }, + height: "49px", + overflow: "hidden", + backgroundColor: theme.palette.background.paper, + }, + navigatorContainer: { + display: "flex", + justifyContent: "space-between", + }, + nav: { + height: "48px", + padding: "5px 15px", + display: "flex", + }, + optionContainer: { + paddingTop: "6px", + marginRight: "10px", + }, + rightIcon: { + marginTop: "6px", + verticalAlign: "top", + color: "#868686", + }, + expandMore: { + color: "#8d8d8d", + }, + roundBorder: { + borderRadius: "4px 4px 0 0", + }, +}); + +class NavigatorComponent extends Component { + keywords = ""; + currentID = 0; + + state = { + hidden: false, + hiddenFolders: [], + folders: [], + anchorEl: null, + hiddenMode: false, + anchorHidden: null, + }; + + constructor(props) { + super(props); + this.element = React.createRef(); + } + + componentDidMount = () => { + const url = new URL(fixUrlHash(window.location.href)); + const c = url.searchParams.get("path"); + this.renderPath(c === null ? "/" : c); + + if (!this.props.isShare) { + // 如果是在个人文件管理页,首次加载时打开侧边栏 + this.props.handleDesktopToggle(true); + } + + // 后退操作时重新导航 + window.onpopstate = () => { + const url = new URL(fixUrlHash(window.location.href)); + const c = url.searchParams.get("path"); + if (c !== null) { + this.props.navigateToPath(c); + } + }; + }; + + renderPath = (path = null) => { + this.props.setNavigatorError(false, null); + this.setState({ + folders: + path !== null + ? path.substr(1).split("/") + : this.props.path.substr(1).split("/"), + }); + let newPath = path !== null ? path : this.props.path; + const apiURL = this.props.share + ? "/share/list/" + this.props.share.key + : this.keywords === "" + ? "/directory" + : "/file/search/"; + newPath = this.keywords === "" ? newPath : this.keywords; + + API.get(apiURL + encodeURIComponent(newPath)) + .then((response) => { + this.currentID = response.data.parent; + this.props.updateFileList(response.data.objects); + this.props.setNavigatorLoadingStatus(false); + const pathTemp = (path !== null + ? path.substr(1).split("/") + : this.props.path.substr(1).split("/") + ).join(","); + setCookie("path_tmp", encodeURIComponent(pathTemp), 1); + if (this.keywords === "") { + setGetParameter("path", encodeURIComponent(newPath)); + } + }) + .catch((error) => { + this.props.setNavigatorError(true, error); + }); + + this.checkOverFlow(true); + }; + + redresh = (path) => { + this.props.setNavigatorLoadingStatus(true); + this.props.setNavigatorError(false, "error"); + this.renderPath(path); + }; + + UNSAFE_componentWillReceiveProps = (nextProps) => { + if (this.props.keywords !== nextProps.keywords) { + this.keywords = nextProps.keywords; + } + if (this.props.path !== nextProps.path) { + this.renderPath(nextProps.path); + } + if (this.props.refresh !== nextProps.refresh) { + this.redresh(nextProps.path); + } + }; + + componentWillUnmount() { + this.props.updateFileList([]); + } + + componentDidUpdate = (prevProps, prevStates) => { + if (this.state.folders !== prevStates.folders) { + this.checkOverFlow(true); + } + if (this.props.drawerDesktopOpen !== prevProps.drawerDesktopOpen) { + delay(500).then(() => this.checkOverFlow()); + } + }; + + checkOverFlow = (force) => { + if (this.overflowInitLock && !force) { + return; + } + if (this.element.current !== null) { + const hasOverflowingChildren = + this.element.current.offsetHeight < + this.element.current.scrollHeight || + this.element.current.offsetWidth < + this.element.current.scrollWidth; + if (hasOverflowingChildren) { + this.overflowInitLock = true; + this.setState({ hiddenMode: true }); + } + if (!hasOverflowingChildren && this.state.hiddenMode) { + this.setState({ hiddenMode: false }); + } + } + }; + + navigateTo = (event, id) => { + if (id === this.state.folders.length - 1) { + //最后一个路径 + this.setState({ anchorEl: event.currentTarget }); + } else if ( + id === -1 && + this.state.folders.length === 1 && + this.state.folders[0] === "" + ) { + this.props.refreshFileList(); + this.handleClose(); + } else if (id === -1) { + this.props.navigateToPath("/"); + this.handleClose(); + } else { + this.props.navigateToPath( + "/" + this.state.folders.slice(0, id + 1).join("/") + ); + this.handleClose(); + } + }; + + handleClose = () => { + this.setState({ anchorEl: null, anchorHidden: null, anchorSort: null }); + }; + + showHiddenPath = (e) => { + this.setState({ anchorHidden: e.currentTarget }); + }; + + performAction = (e) => { + this.handleClose(); + if (e === "refresh") { + this.redresh(); + return; + } + const presentPath = this.props.path.split("/"); + const newTarget = [ + { + id: this.currentID, + type: "dir", + name: presentPath.pop(), + path: presentPath.length === 1 ? "/" : presentPath.join("/"), + }, + ]; + //this.props.navitateUp(); + switch (e) { + case "share": + this.props.setSelectedTarget(newTarget); + this.props.openShareDialog(); + break; + case "newfolder": + this.props.openCreateFolderDialog(); + break; + case "compress": + this.props.setSelectedTarget(newTarget); + this.props.openCompressDialog(); + break; + case "newFile": + this.props.openCreateFileDialog(); + break; + default: + break; + } + }; + + render() { + const { classes } = this.props; + const isHomePage = pathHelper.isHomePage(this.props.location.pathname); + const user = Auth.GetUser(); + + const presentFolderMenu = ( + + this.performAction("refresh")}> + + + + 刷新 + + {this.props.keywords === "" && isHomePage && ( +
+ + this.performAction("share")}> + + + + 分享 + + {user.group.compress && ( + this.performAction("compress")} + > + + + + 压缩 + + )} + + this.performAction("newfolder")} + > + + + + 创建文件夹 + + this.performAction("newFile")}> + + + + 创建文件 + +
+ )} +
+ ); + + return ( +
+
+
+ + this.navigateTo(e, -1)} + /> + + + {this.state.hiddenMode && ( + + + + + + + {/* */} + + this.navigateTo( + e, + this.state.folders.length - 1 + ) + } + /> + {presentFolderMenu} + + )} + {!this.state.hiddenMode && + this.state.folders.map((folder, id, folders) => ( + + {folder !== "" && ( + + + this.navigateTo(e, id) + } + /> + {id === folders.length - 1 && + presentFolderMenu} + {id !== folders.length - 1 && ( + + )} + + )} + + ))} +
+
+ +
+
+ +
+ ); + } +} + +NavigatorComponent.propTypes = { + classes: PropTypes.object.isRequired, + path: PropTypes.string.isRequired, +}; + +const Navigator = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(withRouter(NavigatorComponent))); + +export default Navigator; diff --git a/assets/src/component/FileManager/Navigator/PathButton.js b/assets/src/component/FileManager/Navigator/PathButton.js new file mode 100644 index 00000000..9998b47a --- /dev/null +++ b/assets/src/component/FileManager/Navigator/PathButton.js @@ -0,0 +1,80 @@ +import React, { useEffect } from "react"; +import ExpandMore from "@material-ui/icons/ExpandMore"; +import { Button } from "@material-ui/core"; +import { makeStyles } from "@material-ui/core"; +import { useDrop } from "react-dnd"; +import classNames from "classnames"; +import MoreIcon from "@material-ui/icons/MoreHoriz"; + +const useStyles = makeStyles((theme) => ({ + expandMore: { + color: "#8d8d8d", + }, + active: { + boxShadow: "0 0 0 2px " + theme.palette.primary.light, + }, + button: { + textTransform: "none", + }, +})); + +export default function PathButton(props) { + const inputRef = React.useRef(null); + + const [{ canDrop, isOver }, drop] = useDrop({ + accept: "object", + drop: () => { + if (props.more) { + inputRef.current.click(); + } else { + return { + folder: { + id: -1, + path: props.path, + name: props.folder === "/" ? "" : props.folder, + }, + }; + } + }, + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }), + }); + + const isActive = canDrop && isOver; + + useEffect(() => { + if (props.more && isActive) { + inputRef.current.click(); + } + // eslint-disable-next-line + }, [isActive]); + + const classes = useStyles(); + return ( + + + + ); +} diff --git a/assets/src/component/FileManager/Navigator/SubActions.js b/assets/src/component/FileManager/Navigator/SubActions.js new file mode 100644 index 00000000..d8357b50 --- /dev/null +++ b/assets/src/component/FileManager/Navigator/SubActions.js @@ -0,0 +1,145 @@ +import React, { useCallback, useState } from "react"; +import { IconButton, makeStyles, Menu, MenuItem } from "@material-ui/core"; +import ViewListIcon from "@material-ui/icons/ViewList"; +import ViewSmallIcon from "@material-ui/icons/ViewComfy"; +import ViewModuleIcon from "@material-ui/icons/ViewModule"; +import TextTotateVerticalIcon from "@material-ui/icons/TextRotateVertical"; +import Avatar from "@material-ui/core/Avatar"; +import { useDispatch, useSelector } from "react-redux"; +import Auth from "../../../middleware/Auth"; +import { changeViewMethod, setShareUserPopover } from "../../../actions"; +import { changeSortMethod } from "../../../redux/explorer/action"; + +const useStyles = makeStyles((theme) => ({ + sideButton: { + padding: "8px", + marginRight: "5px", + }, +})); + +const sortOptions = ["A-Z", "Z-A", "最早", "最新", "最小", "最大"]; + +export default function SubActions({ isSmall, share, inherit }) { + const dispatch = useDispatch(); + const viewMethod = useSelector( + (state) => state.viewUpdate.explorerViewMethod + ); + const OpenLoadingDialog = useCallback( + (method) => dispatch(changeViewMethod(method)), + [dispatch] + ); + const ChangeSortMethod = useCallback( + (method) => dispatch(changeSortMethod(method)), + [dispatch] + ); + const SetShareUserPopover = useCallback( + (e) => dispatch(setShareUserPopover(e)), + [dispatch] + ); + const [anchorSort, setAnchorSort] = useState(null); + const [selectedIndex, setSelectedIndex] = useState(0); + const showSortOptions = (e) => { + setAnchorSort(e.currentTarget); + }; + const handleMenuItemClick = (e, index) => { + setSelectedIndex(index); + const optionsTable = { + 0: "namePos", + 1: "nameRev", + 2: "timePos", + 3: "timeRev", + 4: "sizePos", + 5: "sizeRes", + }; + ChangeSortMethod(optionsTable[index]); + setAnchorSort(null); + }; + + const toggleViewMethod = () => { + const newMethod = + viewMethod === "icon" + ? "list" + : viewMethod === "list" + ? "smallIcon" + : "icon"; + Auth.SetPreference("view_method", newMethod); + OpenLoadingDialog(newMethod); + }; + + const classes = useStyles(); + return ( + <> + {viewMethod === "icon" && ( + + + + )} + {viewMethod === "list" && ( + + + + )} + + {viewMethod === "smallIcon" && ( + + + + )} + + + + + setAnchorSort(null)} + > + {sortOptions.map((option, index) => ( + handleMenuItemClick(event, index)} + > + {option} + + ))} + + {share && ( + SetShareUserPopover(e.currentTarget)} + style={{ padding: 5 }} + > + + + )} + + ); +} diff --git a/assets/src/component/FileManager/ObjectIcon.js b/assets/src/component/FileManager/ObjectIcon.js new file mode 100644 index 00000000..8e287e67 --- /dev/null +++ b/assets/src/component/FileManager/ObjectIcon.js @@ -0,0 +1,261 @@ +import React, { useCallback, useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + changeContextMenu, + setSelectedTarget, + selectFile as selectFileAction, + navigateTo, + showImgPreivew, + openMusicDialog, + toggleSnackbar, + dragAndDrop, + openLoadingDialog, +} from "../../actions/index"; +import statusHelper from "../../utils/page"; +import FileIcon from "./FileIcon"; +import SmallIcon from "./SmallIcon"; +import TableItem from "./TableRow"; +import classNames from "classnames"; +import { isPreviewable } from "../../config"; +import { makeStyles } from "@material-ui/core"; +import { useDrag } from "react-dnd"; +import { getEmptyImage } from "react-dnd-html5-backend"; +import DropWarpper from "./DnD/DropWarpper"; +import { useHistory, useLocation } from "react-router-dom"; +import Auth from "../../middleware/Auth"; +import { pathBack } from "../../utils"; +import { openPreview } from "../../actions"; + +const useStyles = makeStyles(() => ({ + container: { + padding: "7px", + }, + fixFlex: { + minWidth: 0, + }, + dragging: { + opacity: 0.4, + }, +})); + +export default function ObjectIcon(props) { + const path = useSelector((state) => state.navigator.path); + const selected = useSelector((state) => state.explorer.selected); + const viewMethod = useSelector( + (state) => state.viewUpdate.explorerViewMethod + ); + const navigatorPath = useSelector((state) => state.navigator.path); + const location = useLocation(); + + const dispatch = useDispatch(); + const ContextMenu = useCallback( + (type, open) => dispatch(changeContextMenu(type, open)), + [dispatch] + ); + const SetSelectedTarget = useCallback( + (targets) => dispatch(setSelectedTarget(targets)), + [dispatch] + ); + + const NavitateTo = useCallback((targets) => dispatch(navigateTo(targets)), [ + dispatch, + ]); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + const DragAndDrop = useCallback( + (source, target) => dispatch(dragAndDrop(source, target)), + [dispatch] + ); + const OpenLoadingDialog = useCallback( + (text) => dispatch(openLoadingDialog(text)), + [dispatch] + ); + const OpenPreview = useCallback(() => dispatch(openPreview()), [dispatch]); + + const classes = useStyles(); + + const contextMenu = (e) => { + if (props.file.type === "up") { + return; + } + e.preventDefault(); + if ( + selected.findIndex((value) => { + return value === props.file; + }) === -1 + ) { + SetSelectedTarget([props.file]); + } + ContextMenu("file", true); + }; + + const selectFile = (e) => { + dispatch(selectFileAction(props.file, e, props.index)); + }; + const enterFolder = () => { + NavitateTo( + path === "/" ? path + props.file.name : path + "/" + props.file.name + ); + }; + const handleClick = (e) => { + if (props.file.type === "up") { + NavitateTo(pathBack(navigatorPath)); + } + if ( + statusHelper.isMobile() || + statusHelper.isSharePage(location.pathname) + ) { + selectFile(e); + if (props.file.type === "dir" && !e.ctrlKey) { + enterFolder(); + return; + } + } else { + selectFile(e); + } + }; + + const handleDoubleClick = () => { + if (props.file.type === "up") { + return; + } + if (props.file.type === "dir") { + enterFolder(); + return; + } + const isShare = statusHelper.isSharePage(location.pathname); + if (isShare) { + const user = Auth.GetUser(); + if (!Auth.Check() && user && !user.group.shareDownload) { + ToggleSnackbar("top", "right", "请先登录", "warning"); + return; + } + } + if (window.shareInfo && !window.shareInfo.preview) { + OpenLoadingDialog("获取下载地址..."); + return; + } + + OpenPreview(); + }; + + const handleIconClick = (e) => { + if (statusHelper.isMobile()) { + e.stopPropagation(); + e.ctrlKey = true; + selectFile(e); + return false; + } + }; + + const [{ isDragging }, drag, preview] = useDrag({ + item: { + object: props.file, + type: "object", + selected: [...selected], + viewMethod: viewMethod, + }, + end: (item, monitor) => { + const dropResult = monitor.getDropResult(); + if (item && dropResult) { + if (dropResult.folder) { + if ( + item.object.id !== dropResult.folder.id || + item.object.type !== dropResult.folder.type + ) { + DragAndDrop(item.object, dropResult.folder); + } + } + } + }, + canDrag: () => { + return ( + !statusHelper.isMobile() && + statusHelper.isHomePage(location.pathname) + ); + }, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + }); + + useEffect(() => { + preview(getEmptyImage(), { captureDraggingState: true }); + // eslint-disable-next-line + }, []); + + if (viewMethod === "list") { + return ( + <> + {props.file.type === "dir" && ( + + )} + {props.file.type !== "dir" && ( + + )} + + ); + } + + return ( +
+
+ {props.file.type === "dir" && viewMethod !== "list" && ( + + )} + {props.file.type === "file" && viewMethod === "icon" && ( + + )} + {props.file.type === "file" && viewMethod === "smallIcon" && ( + + )} +
+
+ ); +} diff --git a/assets/src/component/FileManager/PathSelector.js b/assets/src/component/FileManager/PathSelector.js new file mode 100644 index 00000000..9aa09f5a --- /dev/null +++ b/assets/src/component/FileManager/PathSelector.js @@ -0,0 +1,190 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import FolderIcon from "@material-ui/icons/Folder"; +import RightIcon from "@material-ui/icons/KeyboardArrowRight"; +import UpIcon from "@material-ui/icons/ArrowUpward"; +import { connect } from "react-redux"; +import classNames from "classnames"; +import { toggleSnackbar } from "../../actions/index"; + +import { + MenuList, + MenuItem, + IconButton, + ListItemIcon, + ListItemText, + withStyles, + ListItemSecondaryAction, +} from "@material-ui/core"; +import API from "../../middleware/Api"; + +const mapStateToProps = (state) => { + return { + keywords: state.explorer.keywords, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + toggleSnackbar: (vertical, horizontal, msg, color) => { + dispatch(toggleSnackbar(vertical, horizontal, msg, color)); + }, + }; +}; + +const styles = (theme) => ({ + iconWhite: { + color: theme.palette.common.white, + }, + selected: { + backgroundColor: theme.palette.primary.main + "!important", + "& $primary, & $icon": { + color: theme.palette.common.white, + }, + }, + primary: {}, + icon: {}, + buttonIcon: {}, + selector: { + minWidth: "300px", + }, + container: { + maxHeight: "330px", + overflowY: " auto", + }, +}); + +class PathSelectorCompoment extends Component { + state = { + presentPath: "/", + dirList: [], + selectedTarget: null, + }; + + componentDidMount = () => { + const toBeLoad = this.props.presentPath; + this.enterFolder(this.props.keywords === "" ? toBeLoad : "/"); + }; + + back = () => { + const paths = this.state.presentPath.split("/"); + paths.pop(); + const toBeLoad = paths.join("/"); + this.enterFolder(toBeLoad === "" ? "/" : toBeLoad); + }; + + enterFolder = (toBeLoad) => { + API.get( + (this.props.api ? this.props.api : "/directory") + + encodeURIComponent(toBeLoad) + ) + .then((response) => { + const dirList = response.data.objects.filter((x) => { + return ( + x.type === "dir" && + this.props.selected.findIndex((value) => { + return ( + value.name === x.name && value.path === x.path + ); + }) === -1 + ); + }); + if (toBeLoad === "/") { + dirList.unshift({ name: "/", path: "" }); + } + this.setState({ + presentPath: toBeLoad, + dirList: dirList, + selectedTarget: null, + }); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "warning" + ); + }); + }; + + handleSelect = (index) => { + this.setState({ selectedTarget: index }); + this.props.onSelect(this.state.dirList[index]); + }; + + render() { + const { classes } = this.props; + + return ( +
+ + {this.state.presentPath !== "/" && ( + + + + + + + )} + {this.state.dirList.map((value, index) => ( + this.handleSelect(index)} + > + + + + + {value.name !== "/" && ( + + + this.enterFolder( + value.path === "/" + ? value.path + value.name + : value.path + + "/" + + value.name + ) + } + > + + + + )} + + ))} + +
+ ); + } +} + +PathSelectorCompoment.propTypes = { + classes: PropTypes.object.isRequired, + presentPath: PropTypes.string.isRequired, + selected: PropTypes.array.isRequired, +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(PathSelectorCompoment)); diff --git a/assets/src/component/FileManager/Sidebar/SideDrawer.js b/assets/src/component/FileManager/Sidebar/SideDrawer.js new file mode 100644 index 00000000..d3924f0f --- /dev/null +++ b/assets/src/component/FileManager/Sidebar/SideDrawer.js @@ -0,0 +1,317 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { makeStyles } from "@material-ui/core"; +import { useDispatch, useSelector } from "react-redux"; +import Drawer from "@material-ui/core/Drawer"; +import Toolbar from "@material-ui/core/Toolbar"; +import { Clear, Folder } from "@material-ui/icons"; +import Divider from "@material-ui/core/Divider"; +import { setSideBar } from "../../../redux/explorer/action"; +import TypeIcon from "../TypeIcon"; +import Typography from "@material-ui/core/Typography"; +import IconButton from "@material-ui/core/IconButton"; +import Grid from "@material-ui/core/Grid"; +import API from "../../../middleware/Api"; +import { navigateTo, toggleSnackbar } from "../../../actions"; +import { filename, sizeToString } from "../../../utils"; +import Link from "@material-ui/core/Link"; +import Tooltip from "@material-ui/core/Tooltip"; +import TimeAgo from "timeago-react"; +import ListLoading from "../../Placeholder/ListLoading"; +import Hidden from "@material-ui/core/Hidden"; +import Dialog from "@material-ui/core/Dialog"; +import Slide from "@material-ui/core/Slide"; +import AppBar from "@material-ui/core/AppBar"; +import { formatLocalTime } from "../../../utils/datetime"; + +const drawerWidth = 350; + +const useStyles = makeStyles((theme) => ({ + drawer: { + width: drawerWidth, + flexShrink: 0, + }, + drawerPaper: { + width: drawerWidth, + boxShadow: + "0px 8px 10px -5px rgb(0 0 0 / 20%), 0px 16px 24px 2px rgb(0 0 0 / 14%), 0px 6px 30px 5px rgb(0 0 0 / 12%)", + }, + drawerContainer: { + overflow: "auto", + }, + header: { + display: "flex", + padding: theme.spacing(3), + placeContent: "space-between", + }, + fileIcon: { width: 33, height: 33 }, + fileIconSVG: { fontSize: 20 }, + folderIcon: { + color: theme.palette.text.secondary, + width: 33, + height: 33, + }, + fileName: { + marginLeft: theme.spacing(2), + marginRight: theme.spacing(2), + wordBreak: "break-all", + flexGrow: 2, + }, + closeIcon: { + placeSelf: "flex-start", + marginTop: 2, + }, + propsContainer: { + padding: theme.spacing(3), + }, + propsLabel: { + color: theme.palette.text.secondary, + padding: theme.spacing(1), + }, + propsTime: { + color: theme.palette.text.disabled, + padding: theme.spacing(1), + }, + propsValue: { + padding: theme.spacing(1), + wordBreak: "break-all", + }, + appBar: { + position: "relative", + }, + title: { + marginLeft: theme.spacing(2), + flex: 1, + }, +})); + +const Transition = React.forwardRef(function Transition(props, ref) { + return ; +}); + +export default function SideDrawer() { + const dispatch = useDispatch(); + const sideBarOpen = useSelector((state) => state.explorer.sideBarOpen); + const selected = useSelector((state) => state.explorer.selected); + const SetSideBar = useCallback((open) => dispatch(setSideBar(open)), [ + dispatch, + ]); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + const NavigateTo = useCallback((k) => dispatch(navigateTo(k)), [dispatch]); + const keywords = useSelector((state) => state.explorer.keywords); + const [target, setTarget] = useState(null); + const [details, setDetails] = useState(null); + const loadProps = (object) => { + API.get( + "/object/property/" + + object.id + + "?trace_root=" + + (keywords !== "" ? "true" : "false") + + "&is_folder=" + + (object.type === "dir").toString() + ) + .then((response) => { + setDetails(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + + useEffect(() => { + setDetails(null); + if (sideBarOpen) { + if (selected.length !== 1) { + SetSideBar(false); + } else { + setTarget(selected[0]); + loadProps(selected[0]); + } + } + }, [selected, sideBarOpen]); + + const classes = useStyles(); + const propsItem = [ + { + label: "大小", + value: (d, t) => + sizeToString(d.size) + + " (" + + d.size.toLocaleString() + + " 字节)", + show: (d) => true, + }, + { + label: "存储策略", + value: (d, t) => d.policy, + show: (d) => d.type === "file", + }, + { + label: "包含目录", + value: (d, t) => d.child_folder_num.toLocaleString() + " " + "个", + show: (d) => d.type === "dir", + }, + { + label: "包含文件", + value: (d, t) => d.child_file_num.toLocaleString() + " " + "个", + show: (d) => d.type === "dir", + }, + { + label: "所在目录", + // eslint-disable-next-line react/display-name + value: (d, t) => { + const path = d.path === "" ? t.path : d.path; + const name = filename(path); + return ( + + NavigateTo(path)} + > + {name === "" ? "根目录" : name} + + + ); + }, + show: (d) => true, + }, + { + label: "修改于", + value: (d, t) => + formatLocalTime(d.updated_at, "YYYY/MM/DD H:mm:ss"), + show: (d) => true, + }, + { + label: "创建于", + value: (d) => formatLocalTime(d.created_at, "YYYY/MM/DD H:mm:ss"), + show: (d) => true, + }, + ]; + const content = ( + + {!details && } + {details && ( + <> + {propsItem.map((item) => { + if (item.show(target)) { + return ( + <> + + {item.label} + + + {item.value(details, target)} + + + ); + } + })} + {target.type === "dir" && ( + + 统计于{" "} + + + )} + + )} + + ); + return ( + <> + + + {target && ( + <> + + + SetSideBar(false)} + aria-label="close" + > + + + + {target.name} + + + + {content} + + )} + + + + + +
+ {target && ( + <> +
+ {target.type === "dir" && ( + + )} + {target.type !== "dir" && ( + + )} +
+ + {target.name} + +
+ SetSideBar(false)} + className={classes.closeIcon} + aria-label="close" + size={"small"} + > + + +
+ + )} + + {content} +
+
+
+ + ); +} diff --git a/assets/src/component/FileManager/SmallIcon.js b/assets/src/component/FileManager/SmallIcon.js new file mode 100644 index 00000000..1bb5350b --- /dev/null +++ b/assets/src/component/FileManager/SmallIcon.js @@ -0,0 +1,166 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import classNames from "classnames"; +import { + withStyles, + ButtonBase, + Typography, + Tooltip, + fade, +} from "@material-ui/core"; +import TypeIcon from "./TypeIcon"; +import { lighten } from "@material-ui/core/styles"; +import CheckCircleRoundedIcon from "@material-ui/icons/CheckCircleRounded"; +import statusHelper from "../../utils/page"; +import Grow from "@material-ui/core/Grow"; +import { Folder } from "@material-ui/icons"; + +const styles = (theme) => ({ + container: { + padding: "7px", + }, + + selected: { + "&:hover": { + border: "1px solid #d0d0d0", + }, + backgroundColor: fade( + theme.palette.primary.main, + theme.palette.type === "dark" ? 0.3 : 0.18 + ), + }, + notSelected: { + "&:hover": { + backgroundColor: theme.palette.background.default, + border: "1px solid #d0d0d0", + }, + backgroundColor: theme.palette.background.paper, + }, + + button: { + height: "50px", + border: "1px solid " + theme.palette.divider, + width: "100%", + borderRadius: "6px", + boxSizing: "border-box", + transition: + "background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms", + display: "flex", + justifyContent: "left", + alignItems: "initial", + }, + icon: { + margin: "10px 10px 10px 16px", + height: "30px", + minWidth: "30px", + backgroundColor: theme.palette.background.paper, + borderRadius: "90%", + paddingTop: "3px", + color: theme.palette.text.secondary, + }, + folderNameSelected: { + color: + theme.palette.type === "dark" ? "#fff" : theme.palette.primary.dark, + fontWeight: "500", + }, + folderNameNotSelected: { + color: theme.palette.text.secondary, + }, + folderName: { + marginTop: "15px", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + overflow: "hidden", + marginRight: "20px", + }, + checkIcon: { + color: theme.palette.primary.main, + }, +}); + +const mapStateToProps = (state) => { + return { + selected: state.explorer.selected, + }; +}; + +const mapDispatchToProps = () => { + return {}; +}; + +class SmallIconCompoment extends Component { + state = {}; + + render() { + const { classes } = this.props; + const isSelected = + this.props.selected.findIndex((value) => { + return value === this.props.file; + }) !== -1; + const isMobile = statusHelper.isMobile(); + + return ( + +
+ {(!isSelected || !isMobile) && ( + <> + {this.props.isFolder && } + {!this.props.isFolder && ( + + )} + + )} + {isSelected && isMobile && ( + + + + )} +
+ + + {this.props.file.name} + + +
+ ); + } +} + +SmallIconCompoment.propTypes = { + classes: PropTypes.object.isRequired, + file: PropTypes.object.isRequired, +}; + +const SmallIcon = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(SmallIconCompoment)); + +export default SmallIcon; diff --git a/assets/src/component/FileManager/TableRow.js b/assets/src/component/FileManager/TableRow.js new file mode 100644 index 00000000..e03c4a83 --- /dev/null +++ b/assets/src/component/FileManager/TableRow.js @@ -0,0 +1,214 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; + +import FolderIcon from "@material-ui/icons/Folder"; +import classNames from "classnames"; +import { sizeToString } from "../../utils/index"; +import { + withStyles, + TableCell, + TableRow, + Typography, + fade, +} from "@material-ui/core"; +import TypeIcon from "./TypeIcon"; +import { lighten } from "@material-ui/core/styles"; +import pathHelper from "../../utils/page"; +import { withRouter } from "react-router"; +import KeyboardReturnIcon from "@material-ui/icons/KeyboardReturn"; +import CheckCircleRoundedIcon from "@material-ui/icons/CheckCircleRounded"; +import statusHelper from "../../utils/page"; +import Grow from "@material-ui/core/Grow"; +import dayjs from "dayjs"; +import { formatLocalTime } from "../../utils/datetime"; + +const styles = (theme) => ({ + selected: { + "&:hover": {}, + backgroundColor: fade(theme.palette.primary.main, 0.18), + }, + + selectedShared: { + "&:hover": {}, + backgroundColor: fade(theme.palette.primary.main, 0.18), + }, + + notSelected: { + "&:hover": { + backgroundColor: theme.palette.background.default, + }, + }, + icon: { + verticalAlign: "middle", + marginRight: "20px", + color: theme.palette.text.secondary, + }, + tableIcon: { + marginRight: "20px", + verticalAlign: "middle", + }, + folderNameSelected: { + color: + theme.palette.type === "dark" ? "#fff" : theme.palette.primary.dark, + fontWeight: "500", + userSelect: "none", + }, + folderNameNotSelected: { + color: theme.palette.text.secondary, + userSelect: "none", + }, + folderName: { + marginRight: "20px", + display: "flex", + alignItems: "center", + }, + hideAuto: { + [theme.breakpoints.down("sm")]: { + display: "none", + }, + }, + tableRow: { + padding: "10px 16px", + }, + checkIcon: { + color: theme.palette.primary.main, + }, + active: { + backgroundColor: fade(theme.palette.primary.main, 0.1), + }, +}); + +const mapStateToProps = (state) => { + return { + selected: state.explorer.selected, + }; +}; + +const mapDispatchToProps = () => { + return {}; +}; + +class TableRowCompoment extends Component { + state = {}; + + render() { + const { classes } = this.props; + const isShare = pathHelper.isSharePage(this.props.location.pathname); + + let icon; + if (this.props.file.type === "dir") { + icon = ; + } else if (this.props.file.type === "up") { + icon = ; + } else { + icon = ( + + ); + } + const isSelected = + this.props.selected.findIndex((value) => { + return value === this.props.file; + }) !== -1; + const isMobile = statusHelper.isMobile(); + + return ( + + + +
+ {(!isSelected || !isMobile) && icon} + {isSelected && isMobile && ( + + + + )} +
+ {this.props.file.name} +
+
+ + + {" "} + {this.props.file.type !== "dir" && + this.props.file.type !== "up" && + sizeToString(this.props.file.size)} + + + + + {" "} + {formatLocalTime( + this.props.file.date, + "YYYY-MM-DD H:mm:ss" + )} + + +
+ ); + } +} + +TableRowCompoment.propTypes = { + classes: PropTypes.object.isRequired, + file: PropTypes.object.isRequired, +}; + +const TableItem = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(withRouter(TableRowCompoment))); + +export default TableItem; diff --git a/assets/src/component/FileManager/TypeIcon.js b/assets/src/component/FileManager/TypeIcon.js new file mode 100644 index 00000000..ce85d7dd --- /dev/null +++ b/assets/src/component/FileManager/TypeIcon.js @@ -0,0 +1,157 @@ +import React from "react"; +import { mediaType } from "../../config"; +import ImageIcon from "@material-ui/icons/PhotoSizeSelectActual"; +import VideoIcon from "@material-ui/icons/Videocam"; +import AudioIcon from "@material-ui/icons/Audiotrack"; +import PdfIcon from "@material-ui/icons/PictureAsPdf"; +import { + Android, + FileExcelBox, + FilePowerpointBox, + FileWordBox, + LanguageC, + LanguageCpp, + LanguageGo, + LanguageJavascript, + LanguagePhp, + LanguagePython, + MagnetOn, + ScriptText, + WindowRestore, + ZipBox, +} from "mdi-material-ui"; +import FileShowIcon from "@material-ui/icons/InsertDriveFile"; +import { lighten } from "@material-ui/core/styles"; +import useTheme from "@material-ui/core/styles/useTheme"; +import { Avatar } from "@material-ui/core"; + +const icons = { + audio: { + color: "#651fff", + icon: AudioIcon, + }, + video: { + color: "#d50000", + icon: VideoIcon, + }, + image: { + color: "#d32f2f", + icon: ImageIcon, + }, + pdf: { + color: "#f44336", + icon: PdfIcon, + }, + word: { + color: "#538ce5", + icon: FileWordBox, + }, + ppt: { + color: "rgb(239, 99, 63)", + icon: FilePowerpointBox, + }, + excel: { + color: "#4caf50", + icon: FileExcelBox, + }, + text: { + color: "#607d8b", + icon: ScriptText, + }, + torrent: { + color: "#5c6bc0", + icon: MagnetOn, + }, + zip: { + color: "#f9a825", + icon: ZipBox, + }, + excute: { + color: "#1a237e", + icon: WindowRestore, + }, + android: { + color: "#8bc34a", + icon: Android, + }, + file: { + color: "#607d8b", + icon: FileShowIcon, + }, + php: { + color: "#777bb3", + icon: LanguagePhp, + }, + go: { + color: "#16b3da", + icon: LanguageGo, + }, + python: { + color: "#3776ab", + icon: LanguagePython, + }, + c: { + color: "#a8b9cc", + icon: LanguageC, + }, + cpp: { + color: "#004482", + icon: LanguageCpp, + }, + js: { + color: "#f4d003", + icon: LanguageJavascript, + }, +}; + +const getColor = (theme, color) => + theme.palette.type === "light" ? color : lighten(color, 0.2); + +let color; + +const TypeIcon = (props) => { + const theme = useTheme(); + + const fileSuffix = props.fileName.split(".").pop().toLowerCase(); + let fileType = "file"; + Object.keys(mediaType).forEach((k) => { + if (mediaType[k].indexOf(fileSuffix) !== -1) { + fileType = k; + } + }); + const IconComponent = icons[fileType].icon; + color = getColor(theme, icons[fileType].color); + if (props.getColorValue) { + props.getColorValue(color); + } + + return ( + <> + {props.isUpload && ( + + + + )} + {!props.isUpload && ( + + )} + + ); +}; + +export default TypeIcon; diff --git a/assets/src/component/Login/Activication.js b/assets/src/component/Login/Activication.js new file mode 100644 index 00000000..07c7997b --- /dev/null +++ b/assets/src/component/Login/Activication.js @@ -0,0 +1,106 @@ +import React, { useCallback, useState, useEffect } from "react"; +import { useDispatch } from "react-redux"; +import { makeStyles } from "@material-ui/core"; +import { toggleSnackbar } from "../../actions/index"; +import { useHistory } from "react-router-dom"; +import API from "../../middleware/Api"; +import { Button, Paper, Avatar, Typography } from "@material-ui/core"; +import EmailIcon from "@material-ui/icons/EmailOutlined"; +import { useLocation } from "react-router"; +const useStyles = makeStyles((theme) => ({ + layout: { + width: "auto", + marginTop: "110px", + marginLeft: theme.spacing(3), + marginRight: theme.spacing(3), + [theme.breakpoints.up("sm")]: { + width: 400, + marginLeft: "auto", + marginRight: "auto", + }, + marginBottom: 110, + }, + paper: { + marginTop: theme.spacing(8), + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing( + 3 + )}px`, + }, + avatar: { + margin: theme.spacing(1), + backgroundColor: theme.palette.secondary.main, + }, + submit: { + marginTop: theme.spacing(3), + }, +})); + +function useQuery() { + return new URLSearchParams(useLocation().search); +} + +function Activation() { + const query = useQuery(); + const location = useLocation(); + + const [success, setSuccess] = useState(false); + const [email, setEmail] = useState(""); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + const history = useHistory(); + + const classes = useStyles(); + + useEffect(() => { + API.get( + "/user/activate/" + query.get("id") + "?sign=" + query.get("sign") + ) + .then((response) => { + setEmail(response.data); + setSuccess(true); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "warning"); + history.push("/login"); + }); + // eslint-disable-next-line + }, [location]); + + return ( +
+ {success && ( + + + + + + 激活成功 + + + 您的账号已被成功激活。 + + + + )} +
+ ); +} + +export default Activation; diff --git a/assets/src/component/Login/LoginForm.js b/assets/src/component/Login/LoginForm.js new file mode 100644 index 00000000..5a0c3bb8 --- /dev/null +++ b/assets/src/component/Login/LoginForm.js @@ -0,0 +1,437 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import LockOutlinedIcon from "@material-ui/icons/LockOutlined"; +import { + Avatar, + Button, + Divider, + FormControl, + Input, + InputLabel, + Link, + makeStyles, + Paper, + Typography, +} from "@material-ui/core"; +import { + applyThemes, + setSessionStatus, + toggleSnackbar, +} from "../../actions/index"; +import { useHistory } from "react-router-dom"; +import API from "../../middleware/Api"; +import Auth from "../../middleware/Auth"; +import { bufferDecode, bufferEncode } from "../../utils/index"; +import { enableUploaderLoad } from "../../middleware/Init"; +import { Fingerprint, VpnKey } from "@material-ui/icons"; +import VpnIcon from "@material-ui/icons/VpnKeyOutlined"; +import { useLocation } from "react-router"; +import { ICPFooter } from "../Common/ICPFooter"; +import { useCaptcha } from "../../hooks/useCaptcha"; + +const useStyles = makeStyles((theme) => ({ + layout: { + width: "auto", + marginTop: "110px", + marginLeft: theme.spacing(3), + marginRight: theme.spacing(3), + [theme.breakpoints.up("sm")]: { + width: 400, + marginLeft: "auto", + marginRight: "auto", + }, + marginBottom: 110, + }, + paper: { + marginTop: theme.spacing(8), + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing( + 3 + )}px`, + }, + avatar: { + margin: theme.spacing(1), + backgroundColor: theme.palette.secondary.main, + }, + form: { + width: "100%", // Fix IE 11 issue. + marginTop: theme.spacing(1), + }, + submit: { + marginTop: theme.spacing(3), + }, + link: { + marginTop: "20px", + display: "flex", + width: "100%", + justifyContent: "space-between", + }, + captchaContainer: { + display: "flex", + marginTop: "10px", + [theme.breakpoints.down("sm")]: { + display: "block", + }, + }, + captchaPlaceholder: { + width: 200, + }, + buttonContainer: { + display: "flex", + }, + authnLink: { + textAlign: "center", + marginTop: 16, + }, +})); + +function useQuery() { + return new URLSearchParams(useLocation().search); +} + +function LoginForm() { + const [email, setEmail] = useState(""); + const [pwd, setPwd] = useState(""); + const [loading, setLoading] = useState(false); + const [useAuthn, setUseAuthn] = useState(false); + const [twoFA, setTwoFA] = useState(false); + const [faCode, setFACode] = useState(""); + + const loginCaptcha = useSelector((state) => state.siteConfig.loginCaptcha); + const title = useSelector((state) => state.siteConfig.title); + const authn = useSelector((state) => state.siteConfig.authn); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + const ApplyThemes = useCallback((theme) => dispatch(applyThemes(theme)), [ + dispatch, + ]); + const SetSessionStatus = useCallback( + (status) => dispatch(setSessionStatus(status)), + [dispatch] + ); + + const history = useHistory(); + const location = useLocation(); + const { + captchaLoading, + isValidate, + validate, + CaptchaRender, + captchaRefreshRef, + captchaParamsRef, + } = useCaptcha(); + const query = useQuery(); + + const classes = useStyles(); + + useEffect(() => { + setEmail(query.get("username")); + }, [location]); + + const afterLogin = (data) => { + Auth.authenticate(data); + + // 设置用户主题色 + if (data["preferred_theme"] !== "") { + ApplyThemes(data["preferred_theme"]); + } + enableUploaderLoad(); + + // 设置登录状态 + SetSessionStatus(true); + + history.push("/home"); + ToggleSnackbar("top", "right", "登录成功", "success"); + + localStorage.removeItem("siteConfigCache"); + }; + + const authnLogin = (e) => { + e.preventDefault(); + if (!navigator.credentials) { + ToggleSnackbar("top", "right", "当前浏览器或环境不支持", "warning"); + + return; + } + + setLoading(true); + + API.get("/user/authn/" + email) + .then((response) => { + const credentialRequestOptions = response.data; + console.log(credentialRequestOptions); + credentialRequestOptions.publicKey.challenge = bufferDecode( + credentialRequestOptions.publicKey.challenge + ); + credentialRequestOptions.publicKey.allowCredentials.forEach( + function (listItem) { + listItem.id = bufferDecode(listItem.id); + } + ); + + return navigator.credentials.get({ + publicKey: credentialRequestOptions.publicKey, + }); + }) + .then((assertion) => { + const authData = assertion.response.authenticatorData; + const clientDataJSON = assertion.response.clientDataJSON; + const rawId = assertion.rawId; + const sig = assertion.response.signature; + const userHandle = assertion.response.userHandle; + + return API.post( + "/user/authn/finish/" + email, + JSON.stringify({ + id: assertion.id, + rawId: bufferEncode(rawId), + type: assertion.type, + response: { + authenticatorData: bufferEncode(authData), + clientDataJSON: bufferEncode(clientDataJSON), + signature: bufferEncode(sig), + userHandle: bufferEncode(userHandle), + }, + }) + ); + }) + .then((response) => { + afterLogin(response.data); + }) + .catch((error) => { + console.log(error); + ToggleSnackbar("top", "right", error.message, "warning"); + }) + .then(() => { + setLoading(false); + }); + }; + + const login = (e) => { + e.preventDefault(); + setLoading(true); + if (!isValidate.current.isValidate && loginCaptcha) { + validate(() => login(e), setLoading); + return; + } + API.post("/user/session", { + userName: email, + Password: pwd, + ...captchaParamsRef.current, + }) + .then((response) => { + setLoading(false); + if (response.rawData.code === 203) { + setTwoFA(true); + } else { + afterLogin(response.data); + } + }) + .catch((error) => { + setLoading(false); + ToggleSnackbar("top", "right", error.message, "warning"); + captchaRefreshRef.current(); + }); + }; + + const twoFALogin = (e) => { + e.preventDefault(); + setLoading(true); + API.post("/user/2fa", { + code: faCode, + }) + .then((response) => { + setLoading(false); + afterLogin(response.data); + }) + .catch((error) => { + setLoading(false); + ToggleSnackbar("top", "right", error.message, "warning"); + }); + }; + + return ( +
+ {!twoFA && ( + <> + + + + + + 登录 {title} + + {!useAuthn && ( +
+ + + 电子邮箱 + + + setEmail(e.target.value) + } + autoComplete + value={email} + autoFocus + /> + + + + 密码 + + setPwd(e.target.value)} + type="password" + id="password" + value={pwd} + autoComplete + /> + + {loginCaptcha && } + + + + )} + {useAuthn && ( +
+ + + 电子邮箱 + + + setEmail(e.target.value) + } + autoComplete + value={email} + autoFocus + /> + + +
+ )} + +
+
+ 忘记密码 +
+
+ 注册账号 +
+
+ + +
+ + {authn && ( +
+ +
+ )} + + )} + {twoFA && ( + + + + + + 二步验证 + +
+ + + 请输入六位二步验证代码 + + + setFACode(event.target.value) + } + autoComplete + value={faCode} + autoFocus + /> + + {" "} +
{" "} + +
+ )} +
+ ); +} + +export default LoginForm; diff --git a/assets/src/component/Login/ReCaptcha.js b/assets/src/component/Login/ReCaptcha.js new file mode 100644 index 00000000..b6c6f838 --- /dev/null +++ b/assets/src/component/Login/ReCaptcha.js @@ -0,0 +1,15 @@ +import ReCAPTCHA from "./ReCaptchaWrapper"; +import makeAsyncScriptLoader from "react-async-script"; + +const callbackName = "onloadcallback"; +const globalName = "grecaptcha"; + +function getURL() { + const hostname = "recaptcha.net"; + return `https://${hostname}/recaptcha/api.js?onload=${callbackName}&render=explicit`; +} + +export default makeAsyncScriptLoader(getURL, { + callbackName, + globalName, +})(ReCAPTCHA); diff --git a/assets/src/component/Login/ReCaptchaWrapper.js b/assets/src/component/Login/ReCaptchaWrapper.js new file mode 100644 index 00000000..7fc46c3f --- /dev/null +++ b/assets/src/component/Login/ReCaptchaWrapper.js @@ -0,0 +1,173 @@ +import React from "react"; +import PropTypes from "prop-types"; + +export default class ReCAPTCHA extends React.Component { + constructor() { + super(); + this.handleExpired = this.handleExpired.bind(this); + this.handleErrored = this.handleErrored.bind(this); + this.handleChange = this.handleChange.bind(this); + this.handleRecaptchaRef = this.handleRecaptchaRef.bind(this); + } + + getValue() { + if (this.props.grecaptcha && this._widgetId !== undefined) { + return this.props.grecaptcha.getResponse(this._widgetId); + } + return null; + } + + getWidgetId() { + if (this.props.grecaptcha && this._widgetId !== undefined) { + return this._widgetId; + } + return null; + } + + execute() { + const { grecaptcha } = this.props; + + if (grecaptcha && this._widgetId !== undefined) { + return grecaptcha.execute(this._widgetId); + } else { + this._executeRequested = true; + } + } + + reset() { + if (this.props.grecaptcha && this._widgetId !== undefined) { + this.props.grecaptcha.reset(this._widgetId); + } + } + + handleExpired() { + if (this.props.onExpired) { + this.props.onExpired(); + } else { + this.handleChange(null); + } + } + + handleErrored() { + if (this.props.onErrored) this.props.onErrored(); + } + + handleChange(token) { + if (this.props.onChange) this.props.onChange(token); + } + + explicitRender() { + if ( + this.props.grecaptcha && + this.props.grecaptcha.render && + this._widgetId === undefined + ) { + const wrapper = document.createElement("div"); + this._widgetId = this.props.grecaptcha.render(wrapper, { + sitekey: this.props.sitekey, + callback: this.handleChange, + theme: this.props.theme, + type: this.props.type, + tabindex: this.props.tabindex, + "expired-callback": this.handleExpired, + "error-callback": this.handleErrored, + size: this.props.size, + stoken: this.props.stoken, + hl: this.props.hl, + badge: this.props.badge, + }); + this.captcha.appendChild(wrapper); + } + if ( + this._executeRequested && + this.props.grecaptcha && + this._widgetId !== undefined + ) { + this._executeRequested = false; + this.execute(); + } + } + + componentDidMount() { + this.explicitRender(); + } + + componentDidUpdate() { + this.explicitRender(); + } + + componentWillUnmount() { + if (this._widgetId !== undefined) { + this.delayOfCaptchaIframeRemoving(); + this.reset(); + } + } + + delayOfCaptchaIframeRemoving() { + const temporaryNode = document.createElement("div"); + document.body.appendChild(temporaryNode); + temporaryNode.style.display = "none"; + + // move of the recaptcha to a temporary node + while (this.captcha.firstChild) { + temporaryNode.appendChild(this.captcha.firstChild); + } + + // delete the temporary node after reset will be done + setTimeout(() => { + document.body.removeChild(temporaryNode); + }, 5000); + } + + handleRecaptchaRef(elem) { + this.captcha = elem; + } + + render() { + // consume properties owned by the reCATPCHA, pass the rest to the div so the user can style it. + /* eslint-disable no-unused-vars */ + /* eslint-disable @typescript-eslint/no-unused-vars */ + const { + sitekey, + onChange, + theme, + type, + tabindex, + onExpired, + onErrored, + size, + stoken, + grecaptcha, + badge, + hl, + ...childProps + } = this.props; + /* eslint-enable no-unused-vars */ + return
; + } +} + +ReCAPTCHA.displayName = "ReCAPTCHA"; +ReCAPTCHA.propTypes = { + sitekey: PropTypes.string.isRequired, + onChange: PropTypes.func, + grecaptcha: PropTypes.object, + theme: PropTypes.oneOf(["dark", "light"]), + type: PropTypes.oneOf(["image", "audio"]), + tabindex: PropTypes.number, + onExpired: PropTypes.func, + onErrored: PropTypes.func, + size: PropTypes.oneOf(["compact", "normal", "invisible"]), + stoken: PropTypes.string, + hl: PropTypes.string, + badge: PropTypes.oneOf(["bottomright", "bottomleft", "inline"]), +}; +ReCAPTCHA.defaultProps = { + // eslint-disable-next-line @typescript-eslint/no-empty-function + onChange: () => {}, + theme: "light", + type: "image", + tabindex: 0, + size: "normal", + badge: "bottomright", +}; diff --git a/assets/src/component/Login/Register.js b/assets/src/component/Login/Register.js new file mode 100644 index 00000000..0fe2984d --- /dev/null +++ b/assets/src/component/Login/Register.js @@ -0,0 +1,254 @@ +import React, { useCallback, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import RegIcon from "@material-ui/icons/AssignmentIndOutlined"; +import { + Avatar, + Button, + Divider, + FormControl, + Input, + InputLabel, + Link, + makeStyles, + Paper, + Typography, +} from "@material-ui/core"; +import { toggleSnackbar } from "../../actions/index"; +import { useHistory } from "react-router-dom"; +import API from "../../middleware/Api"; +import EmailIcon from "@material-ui/icons/EmailOutlined"; +import { useCaptcha } from "../../hooks/useCaptcha"; + +const useStyles = makeStyles((theme) => ({ + layout: { + width: "auto", + marginTop: "110px", + marginLeft: theme.spacing(3), + marginRight: theme.spacing(3), + [theme.breakpoints.up("sm")]: { + width: 400, + marginLeft: "auto", + marginRight: "auto", + }, + marginBottom: 110, + }, + paper: { + marginTop: theme.spacing(8), + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing( + 3 + )}px`, + }, + avatar: { + margin: theme.spacing(1), + backgroundColor: theme.palette.secondary.main, + }, + form: { + width: "100%", // Fix IE 11 issue. + marginTop: theme.spacing(1), + }, + submit: { + marginTop: theme.spacing(3), + }, + link: { + marginTop: "20px", + display: "flex", + width: "100%", + justifyContent: "space-between", + }, + captchaContainer: { + display: "flex", + marginTop: "10px", + [theme.breakpoints.down("sm")]: { + display: "block", + }, + }, + captchaPlaceholder: { + width: 200, + }, + buttonContainer: { + display: "flex", + }, + authnLink: { + textAlign: "center", + marginTop: 16, + }, + avatarSuccess: { + margin: theme.spacing(1), + backgroundColor: theme.palette.primary.main, + }, +})); + +function Register() { + const [input, setInput] = useState({ + email: "", + password: "", + password_repeat: "", + }); + const [loading, setLoading] = useState(false); + const [emailActive, setEmailActive] = useState(false); + + const title = useSelector((state) => state.siteConfig.title); + const regCaptcha = useSelector((state) => state.siteConfig.regCaptcha); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + const history = useHistory(); + + const handleInputChange = (name) => (e) => { + setInput({ + ...input, + [name]: e.target.value, + }); + }; + + const { + captchaLoading, + isValidate, + validate, + CaptchaRender, + captchaRefreshRef, + captchaParamsRef, + } = useCaptcha(); + const classes = useStyles(); + + const register = (e) => { + e.preventDefault(); + + if (input.password !== input.password_repeat) { + ToggleSnackbar("top", "right", "两次密码输入不一致", "warning"); + return; + } + + setLoading(true); + if (!isValidate.current.isValidate && regCaptcha) { + validate(() => register(e), setLoading); + return; + } + API.post("/user", { + userName: input.email, + Password: input.password, + ...captchaParamsRef.current, + }) + .then((response) => { + setLoading(false); + if (response.rawData.code === 203) { + setEmailActive(true); + } else { + history.push("/login?username=" + input.email); + ToggleSnackbar("top", "right", "注册成功", "success"); + } + }) + .catch((error) => { + setLoading(false); + ToggleSnackbar("top", "right", error.message, "warning"); + captchaRefreshRef.current(); + }); + }; + + return ( +
+ <> + {!emailActive && ( + + + + + + 注册 {title} + + +
+ + + 电子邮箱 + + + + + 密码 + + + + + 确认密码 + + + + {regCaptcha && } + + + + + +
+
+ 返回登录 +
+
+ 忘记密码 +
+
+
+ )} + {emailActive && ( + + + + + + 邮件激活 + + + 一封激活邮件已经发送至您的邮箱,请访问邮件中的链接以继续完成注册。 + + + )} + +
+ ); +} + +export default Register; diff --git a/assets/src/component/Login/Reset.js b/assets/src/component/Login/Reset.js new file mode 100644 index 00000000..9c3fc0a6 --- /dev/null +++ b/assets/src/component/Login/Reset.js @@ -0,0 +1,173 @@ +import React, { useCallback, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + Avatar, + Button, + Divider, + FormControl, + Input, + InputLabel, + Link, + makeStyles, + Paper, + Typography, +} from "@material-ui/core"; +import { toggleSnackbar } from "../../actions/index"; +import API from "../../middleware/Api"; +import KeyIcon from "@material-ui/icons/VpnKeyOutlined"; +import { useCaptcha } from "../../hooks/useCaptcha"; + +const useStyles = makeStyles((theme) => ({ + layout: { + width: "auto", + marginTop: "110px", + marginLeft: theme.spacing(3), + marginRight: theme.spacing(3), + [theme.breakpoints.up("sm")]: { + width: 400, + marginLeft: "auto", + marginRight: "auto", + }, + marginBottom: 110, + }, + paper: { + marginTop: theme.spacing(8), + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing( + 3 + )}px`, + }, + avatar: { + margin: theme.spacing(1), + backgroundColor: theme.palette.secondary.main, + }, + submit: { + marginTop: theme.spacing(3), + }, + captchaContainer: { + display: "flex", + marginTop: "10px", + }, + captchaPlaceholder: { + width: 200, + }, + link: { + marginTop: "20px", + display: "flex", + width: "100%", + justifyContent: "space-between", + }, +})); + +function Reset() { + const [input, setInput] = useState({ + email: "", + }); + const [loading, setLoading] = useState(false); + const forgetCaptcha = useSelector( + (state) => state.siteConfig.forgetCaptcha + ); + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + const handleInputChange = (name) => (e) => { + setInput({ + ...input, + [name]: e.target.value, + }); + }; + + const { + captchaLoading, + isValidate, + validate, + CaptchaRender, + captchaRefreshRef, + captchaParamsRef, + } = useCaptcha(); + + const submit = (e) => { + e.preventDefault(); + setLoading(true); + if (!isValidate.current.isValidate && forgetCaptcha) { + validate(() => submit(e), setLoading); + return; + } + API.post("/user/reset", { + userName: input.email, + ...captchaParamsRef.current, + }) + .then(() => { + setLoading(false); + ToggleSnackbar( + "top", + "right", + "密码重置邮件已发送,请注意查收", + "success" + ); + }) + .catch((error) => { + setLoading(false); + ToggleSnackbar("top", "right", error.message, "warning"); + captchaRefreshRef.current(); + }); + }; + + const classes = useStyles(); + + return ( +
+ + + + + + 找回密码 + +
+ + 注册邮箱 + + + {forgetCaptcha && } + {" "} + {" "} + +
+
+ 返回登录 +
+
+ 注册账号 +
+
+
+
+ ); +} + +export default Reset; diff --git a/assets/src/component/Login/ResetForm.js b/assets/src/component/Login/ResetForm.js new file mode 100644 index 00000000..313d0b21 --- /dev/null +++ b/assets/src/component/Login/ResetForm.js @@ -0,0 +1,166 @@ +import React, { useCallback, useState } from "react"; +import { useDispatch } from "react-redux"; +import { makeStyles } from "@material-ui/core"; +import { toggleSnackbar } from "../../actions/index"; +import { useHistory } from "react-router-dom"; +import API from "../../middleware/Api"; +import { + Button, + FormControl, + Divider, + Link, + Input, + InputLabel, + Paper, + Avatar, + Typography, +} from "@material-ui/core"; +import { useLocation } from "react-router"; +import KeyIcon from "@material-ui/icons/VpnKeyOutlined"; +const useStyles = makeStyles((theme) => ({ + layout: { + width: "auto", + marginTop: "110px", + marginLeft: theme.spacing(3), + marginRight: theme.spacing(3), + [theme.breakpoints.up("sm")]: { + width: 400, + marginLeft: "auto", + marginRight: "auto", + }, + marginBottom: 110, + }, + paper: { + marginTop: theme.spacing(8), + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing( + 3 + )}px`, + }, + avatar: { + margin: theme.spacing(1), + backgroundColor: theme.palette.secondary.main, + }, + submit: { + marginTop: theme.spacing(3), + }, + link: { + marginTop: "20px", + display: "flex", + width: "100%", + justifyContent: "space-between", + }, +})); + +function useQuery() { + return new URLSearchParams(useLocation().search); +} + +function ResetForm() { + const query = useQuery(); + const [input, setInput] = useState({ + password: "", + password_repeat: "", + }); + const [loading, setLoading] = useState(false); + const handleInputChange = (name) => (e) => { + setInput({ + ...input, + [name]: e.target.value, + }); + }; + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + const history = useHistory(); + + const submit = (e) => { + e.preventDefault(); + if (input.password !== input.password_repeat) { + ToggleSnackbar("top", "right", "两次密码输入不一致", "warning"); + return; + } + setLoading(true); + API.patch("/user/reset", { + secret: query.get("sign"), + id: query.get("id"), + Password: input.password, + }) + .then(() => { + setLoading(false); + history.push("/login"); + ToggleSnackbar("top", "right", "密码已重设", "success"); + }) + .catch((error) => { + setLoading(false); + ToggleSnackbar("top", "right", error.message, "warning"); + }); + }; + + const classes = useStyles(); + + return ( +
+ + + + + + 找回密码 + +
+ + 新密码 + + + + 重复新密码 + + + {" "} +
{" "} + +
+
+ 返回登录 +
+
+ 注册账号 +
+
+
+
+ ); +} + +export default ResetForm; diff --git a/assets/src/component/Login/ResetPwdForm.js b/assets/src/component/Login/ResetPwdForm.js new file mode 100644 index 00000000..04327a77 --- /dev/null +++ b/assets/src/component/Login/ResetPwdForm.js @@ -0,0 +1,214 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import KeyIcon from "@material-ui/icons/VpnKeyOutlined"; +import { toggleSnackbar } from "../../actions/index"; +import axios from "axios"; + +import { + withStyles, + Button, + FormControl, + Divider, + Link, + Input, + InputLabel, + Paper, + Avatar, + Typography, +} from "@material-ui/core"; + +const styles = (theme) => ({ + layout: { + width: "auto", + marginTop: "110px", + marginLeft: theme.spacing(3), + marginRight: theme.spacing(3), + [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { + width: 400, + marginLeft: "auto", + marginRight: "auto", + }, + }, + paper: { + marginTop: theme.spacing(8), + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing( + 3 + )}px`, + }, + avatar: { + margin: theme.spacing(1), + backgroundColor: theme.palette.secondary.main, + }, + form: { + width: "100%", // Fix IE 11 issue. + marginTop: theme.spacing(1), + }, + submit: { + marginTop: theme.spacing(3), + }, + link: { + marginTop: "10px", + display: "flex", + width: "100%", + justifyContent: "space-between", + }, + captchaContainer: { + display: "flex", + marginTop: "10px", + [theme.breakpoints.down("sm")]: { + display: "block", + }, + }, +}); +const mapStateToProps = () => { + return {}; +}; + +const mapDispatchToProps = (dispatch) => { + return { + toggleSnackbar: (vertical, horizontal, msg, color) => { + dispatch(toggleSnackbar(vertical, horizontal, msg, color)); + }, + }; +}; + +class ResetPwdFormCompoment extends Component { + state = { + pwd: "", + pwdRepeat: "", + loading: false, + }; + + login = (e) => { + e.preventDefault(); + if (this.state.pwdRepeat !== this.state.pwd) { + this.props.toggleSnackbar( + "top", + "right", + "两次密码输入不一致", + "warning" + ); + return; + } + this.setState({ + loading: true, + }); + axios + .post("/Member/Reset", { + pwd: this.state.pwd, + key: window.resetKey, + }) + .then((response) => { + if (response.data.code !== "200") { + this.setState({ + loading: false, + }); + this.props.toggleSnackbar( + "top", + "right", + response.data.message, + "warning" + ); + } else { + this.setState({ + loading: false, + pwd: "", + pwdRepeat: "", + }); + this.props.toggleSnackbar( + "top", + "right", + "密码重设成功", + "success" + ); + } + }) + .catch((error) => { + this.setState({ + loading: false, + }); + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + }); + }; + + handleChange = (name) => (event) => { + this.setState({ [name]: event.target.value }); + }; + + render() { + const { classes } = this.props; + + return ( +
+ + + + + + 找回密码 + +
+ + 新密码 + + + + 重复新密码 + + + {" "} +
{" "} + +
+
+ 返回登录 +
+
+ 注册账号 +
+
+
+
+ ); + } +} + +const ResetPwdForm = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(ResetPwdFormCompoment)); + +export default ResetPwdForm; diff --git a/assets/src/component/Modals/AddTag.js b/assets/src/component/Modals/AddTag.js new file mode 100644 index 00000000..bf655c1f --- /dev/null +++ b/assets/src/component/Modals/AddTag.js @@ -0,0 +1,404 @@ +import React, { useState, useCallback } from "react"; +import { makeStyles, useTheme } from "@material-ui/core"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + CircularProgress, +} from "@material-ui/core"; +import { toggleSnackbar } from "../../actions/index"; +import PathSelector from "../FileManager/PathSelector"; +import { useDispatch } from "react-redux"; +import API from "../../middleware/Api"; +import AppBar from "@material-ui/core/AppBar"; +import Tabs from "@material-ui/core/Tabs"; +import Tab from "@material-ui/core/Tab"; +import TextField from "@material-ui/core/TextField"; +import Typography from "@material-ui/core/Typography"; +import FormLabel from "@material-ui/core/FormLabel"; +import ToggleButtonGroup from "@material-ui/lab/ToggleButtonGroup"; +import ToggleButton from "@material-ui/lab/ToggleButton"; +import { + Circle, + CircleOutline, + Heart, + HeartOutline, + Hexagon, + HexagonOutline, + Hexagram, + HexagramOutline, + Rhombus, + RhombusOutline, + Square, + SquareOutline, + Triangle, +} from "mdi-material-ui"; + +const useStyles = makeStyles((theme) => ({ + contentFix: { + padding: "10px 24px 0px 24px", + }, + wrapper: { + margin: theme.spacing(1), + position: "relative", + }, + buttonProgress: { + color: theme.palette.secondary.light, + position: "absolute", + top: "50%", + left: "50%", + marginTop: -12, + marginLeft: -12, + }, + content: { + padding: 0, + marginTop: 0, + }, + marginTop: { + marginTop: theme.spacing(2), + display: "block", + }, + textField: { + marginTop: theme.spacing(1), + }, + scroll: { + overflowX: "auto", + }, + dialogContent: { + marginTop: theme.spacing(2), + }, + pathSelect: { + marginTop: theme.spacing(2), + display: "flex", + }, +})); + +const icons = { + Circle: , + CircleOutline: , + Heart: , + HeartOutline: , + Hexagon: , + HexagonOutline: , + Hexagram: , + HexagramOutline: , + Rhombus: , + RhombusOutline: , + Square: , + SquareOutline: , + Triangle: , +}; + +export default function AddTag(props) { + const theme = useTheme(); + + const [value, setValue] = React.useState(0); + const [loading, setLoading] = React.useState(false); + const [alignment, setAlignment] = React.useState("Circle"); + const [color, setColor] = React.useState(theme.palette.text.secondary); + const [input, setInput] = React.useState({ + filename: "", + tagName: "", + path: "/", + }); + const [pathSelectDialog, setPathSelectDialog] = React.useState(false); + const [selectedPath, setSelectedPath] = useState(""); + // eslint-disable-next-line + const [selectedPathName, setSelectedPathName] = useState(""); + const setMoveTarget = (folder) => { + const path = + folder.path === "/" + ? folder.path + folder.name + : folder.path + "/" + folder.name; + setSelectedPath(path); + setSelectedPathName(folder.name); + }; + + const handleChange = (event, newValue) => { + setValue(newValue); + }; + + const handleIconChange = (event, newAlignment) => { + if (newAlignment) { + setAlignment(newAlignment); + } + }; + + const handleColorChange = (event, newAlignment) => { + if (newAlignment) { + setColor(newAlignment); + } + }; + + const handleInputChange = (name) => (event) => { + setInput({ + ...input, + [name]: event.target.value, + }); + }; + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const submitNewLink = () => { + setLoading(true); + + API.post("/tag/link", { + path: input.path, + name: input.tagName, + }) + .then((response) => { + setLoading(false); + props.onClose(); + props.onSuccess({ + type: 1, + name: input.tagName, + expression: input.path, + color: theme.palette.text.secondary, + icon: "FolderHeartOutline", + id: response.data, + }); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + + const submitNewTag = () => { + setLoading(true); + + API.post("/tag/filter", { + expression: input.filename, + name: input.tagName, + color: color, + icon: alignment, + }) + .then((response) => { + setLoading(false); + props.onClose(); + props.onSuccess({ + type: 0, + name: input.tagName, + color: color, + icon: alignment, + id: response.data, + }); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setLoading(false); + }); + }; + const submit = () => { + if (value === 0) { + submitNewTag(); + } else { + submitNewLink(); + } + }; + const selectPath = () => { + setInput({ + ...input, + path: selectedPath === "//" ? "/" : selectedPath, + }); + setPathSelectDialog(false); + }; + + const classes = useStyles(); + + return ( + + setPathSelectDialog(false)} + aria-labelledby="form-dialog-title" + > + 选择目录 + + + + + + + + + + + + + + + {value === 0 && ( + + + + + 你可以使用*作为通配符。比如 + *.png + 表示匹配png格式图像。多行规则间会以“或”的关系进行运算。 + + 图标: +
+ + {Object.keys(icons).map((key, index) => ( + + {icons[key]} + + ))} + +
+ 颜色: +
+ + {[ + theme.palette.text.secondary, + "#f44336", + "#e91e63", + "#9c27b0", + "#673ab7", + "#3f51b5", + "#2196f3", + "#03a9f4", + "#00bcd4", + "#009688", + "#4caf50", + "#cddc39", + "#ffeb3b", + "#ffc107", + "#ff9800", + "#ff5722", + "#795548", + "#9e9e9e", + "#607d8b", + ].map((key, index) => ( + + + + ))} + +
+
+ )} + {value === 1 && ( + + +
+ + +
+
+ )} + + +
+ +
+
+
+ ); +} diff --git a/assets/src/component/Modals/Compress.js b/assets/src/component/Modals/Compress.js new file mode 100644 index 00000000..450dcdf5 --- /dev/null +++ b/assets/src/component/Modals/Compress.js @@ -0,0 +1,156 @@ +import React, { useState, useCallback } from "react"; +import { makeStyles } from "@material-ui/core"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + DialogContentText, + CircularProgress, +} from "@material-ui/core"; +import { toggleSnackbar, setModalsLoading } from "../../actions/index"; +import PathSelector from "../FileManager/PathSelector"; +import { useDispatch } from "react-redux"; +import API from "../../middleware/Api"; +import TextField from "@material-ui/core/TextField"; + +const useStyles = makeStyles((theme) => ({ + contentFix: { + padding: "10px 24px 0px 24px", + backgroundColor: theme.palette.background.default, + }, + wrapper: { + margin: theme.spacing(1), + position: "relative", + }, + buttonProgress: { + color: theme.palette.secondary.light, + position: "absolute", + top: "50%", + left: "50%", + marginTop: -12, + marginLeft: -12, + }, +})); + +export default function CompressDialog(props) { + const [selectedPath, setSelectedPath] = useState(""); + const [fileName, setFileName] = useState(""); + // eslint-disable-next-line + const [selectedPathName, setSelectedPathName] = useState(""); + + const dispatch = useDispatch(); + + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const SetModalsLoading = useCallback( + (status) => { + dispatch(setModalsLoading(status)); + }, + [dispatch] + ); + + const setMoveTarget = (folder) => { + const path = + folder.path === "/" + ? folder.path + folder.name + : folder.path + "/" + folder.name; + setSelectedPath(path); + setSelectedPathName(folder.name); + }; + + const submitMove = (e) => { + if (e != null) { + e.preventDefault(); + } + SetModalsLoading(true); + + const dirs = [], + items = []; + // eslint-disable-next-line + props.selected.map((value) => { + if (value.type === "dir") { + dirs.push(value.id); + } else { + items.push(value.id); + } + }); + + API.post("/file/compress", { + src: { + dirs: dirs, + items: items, + }, + name: fileName, + dst: selectedPath === "//" ? "/" : selectedPath, + }) + .then(() => { + props.onClose(); + ToggleSnackbar("top", "right", "压缩任务已创建", "success"); + SetModalsLoading(false); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + SetModalsLoading(false); + }); + }; + + const classes = useStyles(); + + return ( + + 存放到 + + + {selectedPath !== "" && ( + + + setFileName(e.target.value)} + value={fileName} + fullWidth + autoFocus + id="standard-basic" + label="压缩文件名" + /> + + + )} + + +
+ +
+
+
+ ); +} diff --git a/assets/src/component/Modals/Copy.js b/assets/src/component/Modals/Copy.js new file mode 100644 index 00000000..4428e5ee --- /dev/null +++ b/assets/src/component/Modals/Copy.js @@ -0,0 +1,144 @@ +import React, { useState, useCallback } from "react"; +import { makeStyles } from "@material-ui/core"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + DialogContentText, + CircularProgress, +} from "@material-ui/core"; +import { + toggleSnackbar, + setModalsLoading, + refreshFileList, +} from "../../actions/index"; +import PathSelector from "../FileManager/PathSelector"; +import { useDispatch } from "react-redux"; +import API from "../../middleware/Api"; + +const useStyles = makeStyles((theme) => ({ + contentFix: { + padding: "10px 24px 0px 24px", + }, + wrapper: { + margin: theme.spacing(1), + position: "relative", + }, + buttonProgress: { + color: theme.palette.secondary.light, + position: "absolute", + top: "50%", + left: "50%", + marginTop: -12, + marginLeft: -12, + }, +})); + +export default function CopyDialog(props) { + const [selectedPath, setSelectedPath] = useState(""); + const [selectedPathName, setSelectedPathName] = useState(""); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + const SetModalsLoading = useCallback( + (status) => { + dispatch(setModalsLoading(status)); + }, + [dispatch] + ); + const RefreshFileList = useCallback(() => { + dispatch(refreshFileList()); + }, [dispatch]); + + const setMoveTarget = (folder) => { + const path = + folder.path === "/" + ? folder.path + folder.name + : folder.path + "/" + folder.name; + setSelectedPath(path); + setSelectedPathName(folder.name); + }; + + const submitMove = (e) => { + if (e != null) { + e.preventDefault(); + } + SetModalsLoading(true); + const dirs = [], + items = []; + // eslint-disable-next-line + + if (props.selected[0].type === "dir") { + dirs.push(props.selected[0].id); + } else { + items.push(props.selected[0].id); + } + + API.post("/object/copy", { + src_dir: props.selected[0].path, + src: { + dirs: dirs, + items: items, + }, + dst: selectedPath === "//" ? "/" : selectedPath, + }) + .then(() => { + props.onClose(); + RefreshFileList(); + SetModalsLoading(false); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + SetModalsLoading(false); + }); + }; + + const classes = useStyles(); + + return ( + + 复制到 + + + {selectedPath !== "" && ( + + + 复制到 {selectedPathName} + + + )} + + +
+ +
+
+
+ ); +} diff --git a/assets/src/component/Modals/CreateShare.js b/assets/src/component/Modals/CreateShare.js new file mode 100644 index 00000000..d964a865 --- /dev/null +++ b/assets/src/component/Modals/CreateShare.js @@ -0,0 +1,461 @@ +import React, { useCallback } from "react"; +import { + Checkbox, + FormControl, + makeStyles, + TextField, +} from "@material-ui/core"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + CircularProgress, +} from "@material-ui/core"; +import { toggleSnackbar } from "../../actions/index"; +import { useDispatch } from "react-redux"; +import API from "../../middleware/Api"; +import List from "@material-ui/core/List"; +import ListItemText from "@material-ui/core/ListItemText"; +import ListItem from "@material-ui/core/ListItem"; +import ListItemIcon from "@material-ui/core/ListItemIcon"; +import LockIcon from "@material-ui/icons/Lock"; +import TimerIcon from "@material-ui/icons/Timer"; +import CasinoIcon from "@material-ui/icons/Casino"; +import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction"; +import Divider from "@material-ui/core/Divider"; +import MuiExpansionPanel from "@material-ui/core/ExpansionPanel"; +import MuiExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; +import MuiExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails"; +import Typography from "@material-ui/core/Typography"; +import withStyles from "@material-ui/core/styles/withStyles"; +import InputLabel from "@material-ui/core/InputLabel"; +import { Visibility, VisibilityOff } from "@material-ui/icons"; +import IconButton from "@material-ui/core/IconButton"; +import InputAdornment from "@material-ui/core/InputAdornment"; +import OutlinedInput from "@material-ui/core/OutlinedInput"; +import Tooltip from "@material-ui/core/Tooltip"; +import MenuItem from "@material-ui/core/MenuItem"; +import Select from "@material-ui/core/Select"; +import EyeIcon from "@material-ui/icons/RemoveRedEye"; +import ToggleIcon from "material-ui-toggle-icon"; + +const useStyles = makeStyles((theme) => ({ + widthAnimation: {}, + shareUrl: { + minWidth: "400px", + }, + wrapper: { + margin: theme.spacing(1), + position: "relative", + }, + buttonProgress: { + color: theme.palette.secondary.light, + position: "absolute", + top: "50%", + left: "50%", + }, + flexCenter: { + alignItems: "center", + }, + noFlex: { + display: "block", + }, + scoreCalc: { + marginTop: 10, + }, +})); + +const ExpansionPanel = withStyles({ + root: { + border: "0px solid rgba(0, 0, 0, .125)", + boxShadow: "none", + "&:not(:last-child)": { + borderBottom: 0, + }, + "&:before": { + display: "none", + }, + "&$expanded": { + margin: "auto", + }, + }, + expanded: {}, +})(MuiExpansionPanel); + +const ExpansionPanelSummary = withStyles({ + root: { + padding: 0, + "&$expanded": {}, + }, + content: { + margin: 0, + display: "initial", + "&$expanded": { + margin: "0 0", + }, + }, + expanded: {}, +})(MuiExpansionPanelSummary); + +const ExpansionPanelDetails = withStyles((theme) => ({ + root: { + padding: 24, + backgroundColor: theme.palette.background.default, + }, +}))(MuiExpansionPanelDetails); + +export default function CreatShare(props) { + const dispatch = useDispatch(); + const classes = useStyles(); + + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const [expanded, setExpanded] = React.useState(false); + const [shareURL, setShareURL] = React.useState(""); + const [values, setValues] = React.useState({ + password: "", + downloads: 1, + expires: 24 * 3600, + showPassword: false, + }); + const [shareOption, setShareOption] = React.useState({ + password: false, + expire: false, + preview: true, + }); + + const handleChange = (prop) => (event) => { + // 输入密码 + if (prop === "password") { + if (event.target.value === "") { + setShareOption({ ...shareOption, password: false }); + } else { + setShareOption({ ...shareOption, password: true }); + } + } + + setValues({ ...values, [prop]: event.target.value }); + }; + + const handleClickShowPassword = () => { + setValues({ ...values, showPassword: !values.showPassword }); + }; + + const handleMouseDownPassword = (event) => { + event.preventDefault(); + }; + + const randomPassword = () => { + setShareOption({ ...shareOption, password: true }); + setValues({ + ...values, + password: Math.random().toString(36).substr(2).slice(2, 8), + showPassword: true, + }); + }; + + const handleExpand = (panel) => (event, isExpanded) => { + setExpanded(isExpanded ? panel : false); + }; + + const handleCheck = (prop) => () => { + if (!shareOption[prop]) { + handleExpand(prop)(null, true); + } + if (prop === "password" && shareOption[prop]) { + setValues({ + ...values, + password: "", + }); + } + setShareOption({ ...shareOption, [prop]: !shareOption[prop] }); + }; + + const onClose = () => { + props.onClose(); + setTimeout(() => { + setShareURL(""); + }, 500); + }; + + const submitShare = (e) => { + e.preventDefault(); + props.setModalsLoading(true); + const submitFormBody = { + id: props.selected[0].id, + is_dir: props.selected[0].type === "dir", + password: values.password, + downloads: shareOption.expire ? values.downloads : -1, + expire: values.expires, + preview: shareOption.preview, + }; + + API.post("/share", submitFormBody) + .then((response) => { + setShareURL(response.data); + setValues({ + password: "", + downloads: 1, + expires: 24 * 3600, + showPassword: false, + }); + setShareOption({ + password: false, + expire: false, + }); + props.setModalsLoading(false); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + props.setModalsLoading(false); + }); + }; + + const handleFocus = (event) => event.target.select(); + + return ( + + 创建分享链接 + + {shareURL === "" && ( + <> + + + + + + + + + + + + + + + + + + 分享密码 + + + + + + + + + } + offIcon={ + + } + /> + + + } + labelWidth={70} + /> + + + + + + + + + + + + + + + + + + + + 或者 + + + + 后过期 + + + + + + + + + + + + + + + + + 是否允许在分享页面预览文件内容 + + + + + + + )} + {shareURL !== "" && ( + + + + )} + + + + + {shareURL === "" && ( +
+ +
+ )} +
+
+ ); +} diff --git a/assets/src/component/Modals/CreateWebDAVAccount.js b/assets/src/component/Modals/CreateWebDAVAccount.js new file mode 100644 index 00000000..8d644025 --- /dev/null +++ b/assets/src/component/Modals/CreateWebDAVAccount.js @@ -0,0 +1,149 @@ +import React, { useState } from "react"; +import { makeStyles } from "@material-ui/core"; +import { Dialog } from "@material-ui/core"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import DialogActions from "@material-ui/core/DialogActions"; +import Button from "@material-ui/core/Button"; +import TextField from "@material-ui/core/TextField"; +import { FolderOpenOutlined, LabelOutlined } from "@material-ui/icons"; +import PathSelector from "../FileManager/PathSelector"; +const useStyles = makeStyles((theme) => ({ + formGroup: { + display: "flex", + marginTop: theme.spacing(1), + }, + formIcon: { + marginTop: 21, + marginRight: 19, + color: theme.palette.text.secondary, + }, + input: { + width: 250, + }, + dialogContent: { + paddingTop: 24, + paddingRight: 24, + paddingBottom: 8, + paddingLeft: 24, + }, + button: { + marginTop: 8, + }, +})); + +export default function CreateWebDAVAccount(props) { + const [value, setValue] = useState({ + name: "", + path: "/", + }); + const [pathSelectDialog, setPathSelectDialog] = React.useState(false); + const [selectedPath, setSelectedPath] = useState(""); + // eslint-disable-next-line + const [selectedPathName, setSelectedPathName] = useState(""); + const classes = useStyles(); + + const setMoveTarget = (folder) => { + const path = + folder.path === "/" + ? folder.path + folder.name + : folder.path + "/" + folder.name; + setSelectedPath(path); + setSelectedPathName(folder.name); + }; + + const handleInputChange = (name) => (e) => { + setValue({ + ...value, + [name]: e.target.value, + }); + }; + + const selectPath = () => { + setValue({ + ...value, + path: selectedPath === "//" ? "/" : selectedPath, + }); + setPathSelectDialog(false); + }; + + return ( + + setPathSelectDialog(false)} + aria-labelledby="form-dialog-title" + > + 选择目录 + + + + + + + +
+
+
+
+ +
+ + +
+
+
+ +
+
+ +
+ +
+
+
+
+ + + + +
+ ); +} diff --git a/assets/src/component/Modals/Decompress.js b/assets/src/component/Modals/Decompress.js new file mode 100644 index 00000000..68827ee5 --- /dev/null +++ b/assets/src/component/Modals/Decompress.js @@ -0,0 +1,124 @@ +import React, { useState, useCallback } from "react"; +import { makeStyles } from "@material-ui/core"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + DialogContentText, + CircularProgress, +} from "@material-ui/core"; +import { toggleSnackbar, setModalsLoading } from "../../actions/index"; +import PathSelector from "../FileManager/PathSelector"; +import { useDispatch } from "react-redux"; +import API from "../../middleware/Api"; +import { filePath } from "../../utils"; + +const useStyles = makeStyles((theme) => ({ + contentFix: { + padding: "10px 24px 0px 24px", + }, + wrapper: { + margin: theme.spacing(1), + position: "relative", + }, + buttonProgress: { + color: theme.palette.secondary.light, + position: "absolute", + top: "50%", + left: "50%", + marginTop: -12, + marginLeft: -12, + }, +})); + +export default function DecompressDialog(props) { + const [selectedPath, setSelectedPath] = useState(""); + const [selectedPathName, setSelectedPathName] = useState(""); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + const SetModalsLoading = useCallback( + (status) => { + dispatch(setModalsLoading(status)); + }, + [dispatch] + ); + + const setMoveTarget = (folder) => { + const path = + folder.path === "/" + ? folder.path + folder.name + : folder.path + "/" + folder.name; + setSelectedPath(path); + setSelectedPathName(folder.name); + }; + + const submitMove = (e) => { + if (e != null) { + e.preventDefault(); + } + SetModalsLoading(true); + API.post("/file/decompress", { + src: filePath(props.selected[0]), + dst: selectedPath === "//" ? "/" : selectedPath, + }) + .then(() => { + props.onClose(); + ToggleSnackbar("top", "right", "解压缩任务已创建", "success"); + SetModalsLoading(false); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + SetModalsLoading(false); + }); + }; + + const classes = useStyles(); + + return ( + + 解压送至 + + + {selectedPath !== "" && ( + + + 解压送至 {selectedPathName} + + + )} + + +
+ +
+
+
+ ); +} diff --git a/assets/src/component/Modals/Loading.js b/assets/src/component/Modals/Loading.js new file mode 100644 index 00000000..042a8987 --- /dev/null +++ b/assets/src/component/Modals/Loading.js @@ -0,0 +1,39 @@ +import React from "react"; +import { makeStyles } from "@material-ui/core/styles"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import DialogContent from "@material-ui/core/DialogContent"; +import Dialog from "@material-ui/core/Dialog"; +import DialogContentText from "@material-ui/core/DialogContentText"; +import { blue } from "@material-ui/core/colors"; +import { useSelector } from "react-redux"; + +const useStyles = makeStyles({ + avatar: { + backgroundColor: blue[100], + color: blue[600], + }, + loadingContainer: { + display: "flex", + }, + loading: { + marginTop: 10, + marginLeft: 20, + }, +}); + +export default function LoadingDialog() { + const classes = useStyles(); + const open = useSelector((state) => state.viewUpdate.modals.loading); + const text = useSelector((state) => state.viewUpdate.modals.loadingText); + + return ( + + + + +
{text}
+
+
+
+ ); +} diff --git a/assets/src/component/Modals/SelectFile.js b/assets/src/component/Modals/SelectFile.js new file mode 100644 index 00000000..29f0fe53 --- /dev/null +++ b/assets/src/component/Modals/SelectFile.js @@ -0,0 +1,118 @@ +import React, { useState, useEffect } from "react"; +import { makeStyles } from "@material-ui/core"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + CircularProgress, +} from "@material-ui/core"; +import FormGroup from "@material-ui/core/FormGroup"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Checkbox from "@material-ui/core/Checkbox"; +import MenuItem from "@material-ui/core/MenuItem"; + +const useStyles = makeStyles((theme) => ({ + contentFix: { + padding: "10px 24px 0px 24px", + }, + wrapper: { + margin: theme.spacing(1), + position: "relative", + }, + buttonProgress: { + color: theme.palette.secondary.light, + position: "absolute", + top: "50%", + left: "50%", + marginTop: -12, + marginLeft: -12, + }, + content: { + padding: 0, + }, +})); + +export default function SelectFileDialog(props) { + const [files, setFiles] = useState(props.files); + + useEffect(() => { + setFiles(props.files); + }, [props.files]); + + const handleChange = (index) => (event) => { + const filesCopy = [...files]; + // eslint-disable-next-line + filesCopy.map((v, k) => { + if (v.index === index) { + filesCopy[k] = { + ...filesCopy[k], + selected: event.target.checked ? "true" : "false", + }; + } + }); + setFiles(filesCopy); + }; + + const submit = () => { + const index = []; + // eslint-disable-next-line + files.map((v) => { + if (v.selected === "true") { + index.push(parseInt(v.index)); + } + }); + props.onSubmit(index); + }; + + const classes = useStyles(); + + return ( + + 选择要下载的文件 + + {files.map((v, k) => { + return ( + + + + } + label={v.path} + /> + + + ); + })} + + + +
+ +
+
+
+ ); +} diff --git a/assets/src/component/Modals/TimeZone.js b/assets/src/component/Modals/TimeZone.js new file mode 100644 index 00000000..d15fd9f6 --- /dev/null +++ b/assets/src/component/Modals/TimeZone.js @@ -0,0 +1,88 @@ +import React, { useState, useCallback, useEffect } from "react"; +import { FormLabel, makeStyles } from "@material-ui/core"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + DialogContentText, + CircularProgress, +} from "@material-ui/core"; +import { toggleSnackbar, setModalsLoading } from "../../actions/index"; +import PathSelector from "../FileManager/PathSelector"; +import { useDispatch } from "react-redux"; +import API from "../../middleware/Api"; +import TextField from "@material-ui/core/TextField"; +import Select from "@material-ui/core/Select"; +import MenuItem from "@material-ui/core/MenuItem"; +import { + refreshTimeZone, + timeZone, + validateTimeZone, +} from "../../utils/datetime"; +import FormControl from "@material-ui/core/FormControl"; +import Auth from "../../middleware/Auth"; + +const useStyles = makeStyles((theme) => ({})); + +export default function TimeZoneDialog(props) { + const [timeZoneValue, setTimeZoneValue] = useState(timeZone); + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const saveZoneInfo = () => { + if (!validateTimeZone(timeZoneValue)) { + ToggleSnackbar("top", "right", "无效的时区名称", "warning"); + return; + } + Auth.SetPreference("timeZone", timeZoneValue); + refreshTimeZone(); + props.onClose(); + }; + + const classes = useStyles(); + + return ( + + 更改时区 + + + + setTimeZoneValue(e.target.value)} + /> + + + + + +
+ +
+
+
+ ); +} diff --git a/assets/src/component/Navbar/DarkModeSwitcher.js b/assets/src/component/Navbar/DarkModeSwitcher.js new file mode 100644 index 00000000..e2f181f6 --- /dev/null +++ b/assets/src/component/Navbar/DarkModeSwitcher.js @@ -0,0 +1,52 @@ +import React, { useCallback } from "react"; +import { IconButton, makeStyles } from "@material-ui/core"; +import DayIcon from "@material-ui/icons/Brightness7"; +import NightIcon from "@material-ui/icons/Brightness4"; +import { useDispatch, useSelector } from "react-redux"; +import { toggleDaylightMode } from "../../actions"; +import Tooltip from "@material-ui/core/Tooltip"; +import Auth from "../../middleware/Auth"; +import classNames from "classnames"; + +const useStyles = makeStyles(() => ({ + icon: { + color: "rgb(255, 255, 255)", + opacity: "0.54", + }, +})); + +const DarkModeSwitcher = ({ position }) => { + const ThemeType = useSelector( + (state) => state.siteConfig.theme.palette.type + ); + const dispatch = useDispatch(); + const ToggleThemeMode = useCallback(() => dispatch(toggleDaylightMode()), [ + dispatch, + ]); + const isDayLight = (ThemeType && ThemeType === "light") || !ThemeType; + const isDark = ThemeType && ThemeType === "dark"; + const toggleMode = () => { + Auth.SetPreference("theme_mode", isDayLight ? "dark" : "light"); + ToggleThemeMode(); + }; + const classes = useStyles(); + return ( + + + {isDayLight && } + {isDark && } + + + ); +}; + +export default DarkModeSwitcher; diff --git a/assets/src/component/Navbar/FileTags.js b/assets/src/component/Navbar/FileTags.js new file mode 100644 index 00000000..903a9dea --- /dev/null +++ b/assets/src/component/Navbar/FileTags.js @@ -0,0 +1,388 @@ +import React, { useCallback, useState, Suspense } from "react"; +import { + Divider, + List, + ListItem, + ListItemIcon, + ListItemText, + makeStyles, + withStyles, +} from "@material-ui/core"; +import { Clear, KeyboardArrowRight } from "@material-ui/icons"; +import classNames from "classnames"; +import FolderShared from "@material-ui/icons/FolderShared"; +import UploadIcon from "@material-ui/icons/CloudUpload"; +import VideoIcon from "@material-ui/icons/VideoLibraryOutlined"; +import ImageIcon from "@material-ui/icons/CollectionsOutlined"; +import MusicIcon from "@material-ui/icons/LibraryMusicOutlined"; +import DocIcon from "@material-ui/icons/FileCopyOutlined"; +import { useHistory, useLocation } from "react-router"; +import pathHelper from "../../utils/page"; +import MuiExpansionPanel from "@material-ui/core/ExpansionPanel"; +import MuiExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; +import MuiExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails"; +import { navigateTo, searchMyFile, toggleSnackbar } from "../../actions"; +import { useDispatch } from "react-redux"; +import Auth from "../../middleware/Auth"; +import { + Circle, + CircleOutline, + Heart, + HeartOutline, + Hexagon, + HexagonOutline, + Hexagram, + HexagramOutline, + Rhombus, + RhombusOutline, + Square, + SquareOutline, + Triangle, + TriangleOutline, + FolderHeartOutline, + TagPlus, +} from "mdi-material-ui"; +import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction"; +import IconButton from "@material-ui/core/IconButton"; +import API from "../../middleware/Api"; + +const ExpansionPanel = withStyles({ + root: { + maxWidth: "100%", + boxShadow: "none", + "&:not(:last-child)": { + borderBottom: 0, + }, + "&:before": { + display: "none", + }, + "&$expanded": { margin: 0 }, + }, + expanded: {}, +})(MuiExpansionPanel); + +const ExpansionPanelSummary = withStyles({ + root: { + minHeight: 0, + padding: 0, + + "&$expanded": { + minHeight: 0, + }, + }, + content: { + maxWidth: "100%", + margin: 0, + display: "block", + "&$expanded": { + margin: "0", + }, + }, + expanded: {}, +})(MuiExpansionPanelSummary); + +const ExpansionPanelDetails = withStyles((theme) => ({ + root: { + display: "block", + padding: theme.spacing(0), + }, +}))(MuiExpansionPanelDetails); + +const useStyles = makeStyles((theme) => ({ + expand: { + display: "none", + transition: ".15s all ease-in-out", + }, + expanded: { + display: "block", + transform: "rotate(90deg)", + }, + iconFix: { + marginLeft: "16px", + }, + hiddenButton: { + display: "none", + }, + subMenu: { + marginLeft: theme.spacing(2), + }, + overFlow: { + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + }, +})); + +const icons = { + Circle: Circle, + CircleOutline: CircleOutline, + Heart: Heart, + HeartOutline: HeartOutline, + Hexagon: Hexagon, + HexagonOutline: HexagonOutline, + Hexagram: Hexagram, + HexagramOutline: HexagramOutline, + Rhombus: Rhombus, + RhombusOutline: RhombusOutline, + Square: Square, + SquareOutline: SquareOutline, + Triangle: Triangle, + TriangleOutline: TriangleOutline, + FolderHeartOutline: FolderHeartOutline, +}; + +const AddTag = React.lazy(() => import("../Modals/AddTag")); + +export default function FileTag() { + const classes = useStyles(); + + const location = useLocation(); + const history = useHistory(); + + const isHomePage = pathHelper.isHomePage(location.pathname); + + const [tagOpen, setTagOpen] = useState(true); + const [addTagModal, setAddTagModal] = useState(false); + const [tagHover, setTagHover] = useState(null); + const [tags, setTags] = useState( + Auth.GetUser().tags ? Auth.GetUser().tags : [] + ); + + const dispatch = useDispatch(); + const SearchMyFile = useCallback((k) => dispatch(searchMyFile(k)), [ + dispatch, + ]); + const NavigateTo = useCallback((k) => dispatch(navigateTo(k)), [dispatch]); + + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const getIcon = (icon, color) => { + if (icons[icon]) { + const IconComponent = icons[icon]; + return ( + + ); + } + return ; + }; + + const submitSuccess = (tag) => { + const newTags = [...tags, tag]; + setTags(newTags); + const user = Auth.GetUser(); + user.tags = newTags; + Auth.SetUser(user); + }; + + const submitDelete = (id) => { + API.delete("/tag/" + id) + .then(() => { + const newTags = tags.filter((v) => { + return v.id !== id; + }); + setTags(newTags); + const user = Auth.GetUser(); + user.tags = newTags; + Auth.SetUser(user); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + + return ( + <> + + setAddTagModal(false)} + /> + + isHomePage && setTagOpen(!tagOpen)} + > + + + !isHomePage && history.push("/home?path=%2F") + } + > + + + {!(tagOpen && isHomePage) && ( + + )} + + + + + + + + setTagHover(null)}> + + + + + + + + + + + + + {[ + { + key: "视频", + id: "video", + icon: ( + + ), + }, + { + key: "图片", + id: "image", + icon: ( + + ), + }, + { + key: "音频", + id: "audio", + icon: ( + + ), + }, + { + key: "文档", + id: "doc", + icon: ( + + ), + }, + ].map((v) => ( + SearchMyFile(v.id + "/internal")} + > + + {v.icon} + + + + ))} + {tags.map((v) => ( + setTagHover(v.id)} + onClick={() => { + if (v.type === 0) { + SearchMyFile("tag/" + v.id); + } else { + NavigateTo(v.expression); + } + }} + > + + {getIcon( + v.type === 0 + ? v.icon + : "FolderHeartOutline", + v.type === 0 ? v.color : null + )} + + + + {tagHover === v.id && ( + submitDelete(v.id)} + > + + + + + )} + + ))} + + setAddTagModal(true)}> + + + + + + {" "} + + + + + ); +} diff --git a/assets/src/component/Navbar/Navbar.js b/assets/src/component/Navbar/Navbar.js new file mode 100644 index 00000000..94fbe227 --- /dev/null +++ b/assets/src/component/Navbar/Navbar.js @@ -0,0 +1,948 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import classNames from "classnames"; +import { connect } from "react-redux"; +import ShareIcon from "@material-ui/icons/Share"; +import BackIcon from "@material-ui/icons/ArrowBack"; +import OpenIcon from "@material-ui/icons/OpenInNew"; +import DownloadIcon from "@material-ui/icons/CloudDownload"; +import OpenFolderIcon from "@material-ui/icons/FolderOpen"; +import RenameIcon from "@material-ui/icons/BorderColor"; +import MoveIcon from "@material-ui/icons/Input"; +import DeleteIcon from "@material-ui/icons/Delete"; +import SaveIcon from "@material-ui/icons/Save"; +import MenuIcon from "@material-ui/icons/Menu"; +import { isPreviewable } from "../../config"; +import { + drawerToggleAction, + setSelectedTarget, + navigateTo, + openCreateFolderDialog, + changeContextMenu, + searchMyFile, + saveFile, + openMusicDialog, + showImgPreivew, + toggleSnackbar, + openMoveDialog, + openRemoveDialog, + openShareDialog, + openRenameDialog, + openLoadingDialog, + setSessionStatus, + openPreview, +} from "../../actions"; +import { + allowSharePreview, + checkGetParameters, + changeThemeColor, +} from "../../utils"; +import Uploader from "../Upload/Uploader.js"; +import { sizeToString, vhCheck } from "../../utils"; +import pathHelper from "../../utils/page"; +import SezrchBar from "./SearchBar"; +import StorageBar from "./StorageBar"; +import UserAvatar from "./UserAvatar"; +import UserInfo from "./UserInfo"; +import { AccountArrowRight, AccountPlus, LogoutVariant } from "mdi-material-ui"; +import { withRouter } from "react-router-dom"; +import { + AppBar, + Toolbar, + Typography, + withStyles, + withTheme, + Drawer, + SwipeableDrawer, + IconButton, + Hidden, + ListItem, + ListItemIcon, + ListItemText, + List, + Grow, + Tooltip, +} from "@material-ui/core"; +import Auth from "../../middleware/Auth"; +import API from "../../middleware/Api"; +import FileTag from "./FileTags"; +import { Assignment, Devices, MoreHoriz, Settings } from "@material-ui/icons"; +import Divider from "@material-ui/core/Divider"; +import SubActions from "../FileManager/Navigator/SubActions"; + +vhCheck(); +const drawerWidth = 240; +const drawerWidthMobile = 270; + +const mapStateToProps = (state) => { + return { + desktopOpen: state.viewUpdate.open, + selected: state.explorer.selected, + isMultiple: state.explorer.selectProps.isMultiple, + withFolder: state.explorer.selectProps.withFolder, + withFile: state.explorer.selectProps.withFile, + path: state.navigator.path, + keywords: state.explorer.keywords, + title: state.siteConfig.title, + subTitle: state.viewUpdate.subTitle, + loadUploader: state.viewUpdate.loadUploader, + isLogin: state.viewUpdate.isLogin, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + handleDesktopToggle: (open) => { + dispatch(drawerToggleAction(open)); + }, + setSelectedTarget: (targets) => { + dispatch(setSelectedTarget(targets)); + }, + navigateTo: (path) => { + dispatch(navigateTo(path)); + }, + openCreateFolderDialog: () => { + dispatch(openCreateFolderDialog()); + }, + changeContextMenu: (type, open) => { + dispatch(changeContextMenu(type, open)); + }, + searchMyFile: (keywords) => { + dispatch(searchMyFile(keywords)); + }, + saveFile: () => { + dispatch(saveFile()); + }, + openMusicDialog: () => { + dispatch(openMusicDialog()); + }, + showImgPreivew: (first) => { + dispatch(showImgPreivew(first)); + }, + toggleSnackbar: (vertical, horizontal, msg, color) => { + dispatch(toggleSnackbar(vertical, horizontal, msg, color)); + }, + openRenameDialog: () => { + dispatch(openRenameDialog()); + }, + openMoveDialog: () => { + dispatch(openMoveDialog()); + }, + openRemoveDialog: () => { + dispatch(openRemoveDialog()); + }, + openShareDialog: () => { + dispatch(openShareDialog()); + }, + openLoadingDialog: (text) => { + dispatch(openLoadingDialog(text)); + }, + setSessionStatus: () => { + dispatch(setSessionStatus()); + }, + openPreview: () => { + dispatch(openPreview()); + }, + }; +}; + +const styles = (theme) => ({ + appBar: { + marginLeft: drawerWidth, + [theme.breakpoints.down("xs")]: { + marginLeft: drawerWidthMobile, + }, + zIndex: theme.zIndex.drawer + 1, + transition: " background-color 250ms", + }, + + drawer: { + width: 0, + flexShrink: 0, + }, + drawerDesktop: { + width: drawerWidth, + flexShrink: 0, + }, + icon: { + marginRight: theme.spacing(2), + }, + menuButton: { + marginRight: 20, + [theme.breakpoints.up("sm")]: { + display: "none", + }, + }, + menuButtonDesktop: { + marginRight: 20, + [theme.breakpoints.down("sm")]: { + display: "none", + }, + }, + menuIcon: { + marginRight: 20, + }, + toolbar: theme.mixins.toolbar, + drawerPaper: { + width: drawerWidthMobile, + }, + drawerPaperDesktop: { + width: drawerWidth, + }, + upDrawer: { + overflowX: "hidden", + }, + drawerOpen: { + width: drawerWidth, + transition: theme.transitions.create("width", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + }, + drawerClose: { + transition: theme.transitions.create("width", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + overflowX: "hidden", + width: 0, + }, + content: { + flexGrow: 1, + padding: theme.spacing(3), + }, + grow: { + flexGrow: 1, + }, + badge: { + top: 1, + right: -15, + }, + nested: { + paddingLeft: theme.spacing(4), + }, + sectionForFile: { + display: "flex", + }, + extendedIcon: { + marginRight: theme.spacing(1), + }, + addButton: { + marginLeft: "40px", + marginTop: "25px", + marginBottom: "15px", + }, + fabButton: { + borderRadius: "100px", + }, + badgeFix: { + right: "10px", + }, + iconFix: { + marginLeft: "16px", + }, + dividerFix: { + marginTop: "8px", + }, + folderShareIcon: { + verticalAlign: "sub", + marginRight: "5px", + }, + shareInfoContainer: { + display: "flex", + marginTop: "15px", + marginBottom: "20px", + marginLeft: "28px", + textDecoration: "none", + }, + shareAvatar: { + width: "40px", + height: "40px", + }, + stickFooter: { + bottom: "0px", + position: "absolute", + backgroundColor: theme.palette.background.paper, + width: "100%", + }, + ownerInfo: { + marginLeft: "10px", + width: "150px", + }, + minStickDrawer: { + overflowY: "auto", + [theme.breakpoints.up("sm")]: { + height: "calc(var(--vh, 100vh) - 145px)", + }, + + [theme.breakpoints.down("sm")]: { + minHeight: "calc(var(--vh, 100vh) - 360px)", + }, + }, +}); +class NavbarCompoment extends Component { + constructor(props) { + super(props); + this.state = { + mobileOpen: false, + }; + this.UploaderRef = React.createRef(); + } + + UNSAFE_componentWillMount() { + this.unlisten = this.props.history.listen(() => { + this.setState(() => ({ mobileOpen: false })); + }); + } + componentWillUnmount() { + this.unlisten(); + } + + componentDidMount() { + changeThemeColor( + this.props.selected.length <= 1 && + !(!this.props.isMultiple && this.props.withFile) + ? this.props.theme.palette.primary.main + : this.props.theme.palette.background.default + ); + } + + UNSAFE_componentWillReceiveProps = (nextProps) => { + if ( + (this.props.selected.length <= 1 && + !(!this.props.isMultiple && this.props.withFile)) !== + (nextProps.selected.length <= 1 && + !(!nextProps.isMultiple && nextProps.withFile)) + ) { + changeThemeColor( + !( + this.props.selected.length <= 1 && + !(!this.props.isMultiple && this.props.withFile) + ) + ? this.props.theme.palette.type === "dark" + ? this.props.theme.palette.background.default + : this.props.theme.palette.primary.main + : this.props.theme.palette.background.default + ); + } + }; + + handleDrawerToggle = () => { + this.setState((state) => ({ mobileOpen: !state.mobileOpen })); + }; + + loadUploader = () => { + if (pathHelper.isHomePage(this.props.location.pathname)) { + return ( + <> + {this.props.loadUploader && this.props.isLogin && ( + + )} + + ); + } + }; + + openDownload = () => { + if (!allowSharePreview()) { + this.props.toggleSnackbar( + "top", + "right", + "未登录用户无法预览", + "warning" + ); + return; + } + this.props.openLoadingDialog("获取下载地址..."); + }; + + archiveDownload = () => { + this.props.openLoadingDialog("打包中..."); + }; + + signOut = () => { + API.delete("/user/session/") + .then(() => { + this.props.toggleSnackbar( + "top", + "right", + "您已退出登录", + "success" + ); + Auth.signout(); + window.location.reload(); + this.props.setSessionStatus(false); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "warning" + ); + }) + .finally(() => { + this.handleClose(); + }); + }; + + render() { + const { classes } = this.props; + const user = Auth.GetUser(this.props.isLogin); + const isHomePage = pathHelper.isHomePage(this.props.location.pathname); + const isSharePage = pathHelper.isSharePage( + this.props.location.pathname + ); + + const drawer = ( +
+ {pathHelper.isMobile() && } + + {Auth.Check(this.props.isLogin) && ( + <> +
+ + + + this.props.history.push("/shares?") + } + > + + + + + + + this.props.history.push("/aria2?") + } + > + + + + + + {user.group.webdav && ( + + this.props.history.push("/webdav?") + } + > + + + + + + )} + + + this.props.history.push("/tasks?") + } + > + + + + + + +
+ + {pathHelper.isMobile() && ( + <> + + + + this.props.history.push("/setting?") + } + > + + + + + + + + + + + + + + + )} +
+ +
+ + )} + + {!Auth.Check(this.props.isLogin) && ( +
+ this.props.history.push("/login")} + > + + + + + + this.props.history.push("/signup")} + > + + + + + +
+ )} +
+ ); + const iOS = + process.browser && /iPad|iPhone|iPod/.test(navigator.userAgent); + return ( +
+ + + {this.props.selected.length <= 1 && + !( + !this.props.isMultiple && this.props.withFile + ) && ( + + + + )} + {this.props.selected.length <= 1 && + !( + !this.props.isMultiple && this.props.withFile + ) && ( + + this.props.handleDesktopToggle( + !this.props.desktopOpen + ) + } + className={classes.menuButtonDesktop} + > + + + )} + {(this.props.selected.length > 1 || + (!this.props.isMultiple && this.props.withFile)) && + (isHomePage || + pathHelper.isSharePage( + this.props.location.pathname + )) && ( + 1 || + (!this.props.isMultiple && + this.props.withFile) + } + > + + this.props.setSelectedTarget([]) + } + > + + + + )} + {this.props.selected.length <= 1 && + !( + !this.props.isMultiple && this.props.withFile + ) && ( + { + this.props.history.push("/"); + }} + > + {this.props.subTitle + ? this.props.subTitle + : this.props.title} + + )} + + {!this.props.isMultiple && + this.props.withFile && + !pathHelper.isMobile() && ( + + {this.props.selected[0].name}{" "} + {(isHomePage || + pathHelper.isSharePage( + this.props.location.pathname + )) && + "(" + + sizeToString( + this.props.selected[0].size + ) + + ")"} + + )} + + {this.props.selected.length > 1 && + !pathHelper.isMobile() && ( + + {this.props.selected.length}个对象 + + )} + {this.props.selected.length <= 1 && + !( + !this.props.isMultiple && this.props.withFile + ) && } +
+ {(this.props.selected.length > 1 || + (!this.props.isMultiple && this.props.withFile)) && + !isHomePage && + !pathHelper.isSharePage( + this.props.location.pathname + ) && + Auth.Check(this.props.isLogin) && + !checkGetParameters("share") && ( +
+ + + this.props.saveFile() + } + > + + + +
+ )} + {(this.props.selected.length > 1 || + (!this.props.isMultiple && this.props.withFile)) && + (isHomePage || isSharePage) && ( +
+ {!this.props.isMultiple && + this.props.withFile && + isPreviewable( + this.props.selected[0].name + ) && ( + + + + this.props.openPreview() + } + > + + + + + )} + {!this.props.isMultiple && + this.props.withFile && ( + + + + this.openDownload() + } + > + + + + + )} + {(this.props.isMultiple || + this.props.withFolder) && + user.group.allowArchiveDownload && ( + + + + this.archiveDownload() + } + > + + + + + )} + + {!this.props.isMultiple && + this.props.withFolder && ( + + + + this.props.navigateTo( + this.props + .path === + "/" + ? this.props + .path + + this + .props + .selected[0] + .name + : this.props + .path + + "/" + + this + .props + .selected[0] + .name + ) + } + > + + + + + )} + {!this.props.isMultiple && + !pathHelper.isMobile() && + !isSharePage && ( + + + + this.props.openShareDialog() + } + > + + + + + )} + {!this.props.isMultiple && !isSharePage && ( + + + + this.props.openRenameDialog() + } + > + + + + + )} + {!isSharePage && ( +
+ {!pathHelper.isMobile() && ( + + + + this.props.openMoveDialog() + } + > + + + + + )} + + + + + this.props.openRemoveDialog() + } + > + + + + + + {pathHelper.isMobile() && ( + + + + this.props.changeContextMenu( + "file", + true + ) + } + > + + + + + )} +
+ )} +
+ )} + {this.props.selected.length <= 1 && + !( + !this.props.isMultiple && this.props.withFile + ) && } + {this.props.selected.length <= 1 && + !(!this.props.isMultiple && this.props.withFile) && + isHomePage && + pathHelper.isMobile() && } + + + {this.loadUploader()} + + + + this.setState(() => ({ mobileOpen: true })) + } + disableDiscovery={iOS} + ModalProps={{ + keepMounted: true, // Better open performance on mobile. + }} + > + {drawer} + + + + +
+ {drawer} + + +
+ ); + } +} +NavbarCompoment.propTypes = { + classes: PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, +}; + +const Navbar = connect( + mapStateToProps, + mapDispatchToProps +)(withTheme(withStyles(styles)(withRouter(NavbarCompoment)))); + +export default Navbar; diff --git a/assets/src/component/Navbar/SearchBar.js b/assets/src/component/Navbar/SearchBar.js new file mode 100644 index 00000000..975af8bf --- /dev/null +++ b/assets/src/component/Navbar/SearchBar.js @@ -0,0 +1,230 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import SearchIcon from "@material-ui/icons/Search"; +import { fade } from "@material-ui/core/styles/colorManipulator"; +import FileIcon from "@material-ui/icons/InsertDriveFile"; +import ShareIcon from "@material-ui/icons/Share"; +import { connect } from "react-redux"; +import { searchMyFile } from "../../actions"; + +import { + withStyles, + InputBase, + Popper, + Fade, + Paper, + MenuItem, + ListItemIcon, + ListItemText, + Typography, +} from "@material-ui/core"; +import { withRouter } from "react-router"; +import pathHelper from "../../utils/page"; +import { HotKeys, configure } from "react-hotkeys"; + +configure({ + ignoreTags: [], +}); + +const mapStateToProps = () => { + return {}; +}; + +const mapDispatchToProps = (dispatch) => { + return { + searchMyFile: (keywords) => { + dispatch(searchMyFile(keywords)); + }, + }; +}; + +const styles = (theme) => ({ + search: { + [theme.breakpoints.down("sm")]: { + display: "none", + }, + position: "relative", + borderRadius: theme.shape.borderRadius, + backgroundColor: fade(theme.palette.common.white, 0.15), + "&:hover": { + backgroundColor: fade(theme.palette.common.white, 0.25), + }, + marginRight: theme.spacing(2), + marginLeft: 0, + width: "100%", + [theme.breakpoints.up("sm")]: { + marginLeft: theme.spacing(7.2), + width: "auto", + }, + }, + searchIcon: { + width: theme.spacing(9), + height: "100%", + position: "absolute", + pointerEvents: "none", + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + inputRoot: { + color: "inherit", + width: "100%", + }, + inputInput: { + paddingTop: theme.spacing(1), + paddingRight: theme.spacing(1), + paddingBottom: theme.spacing(1), + paddingLeft: theme.spacing(7), + transition: theme.transitions.create("width"), + width: "100%", + [theme.breakpoints.up("md")]: { + width: 200, + "&:focus": { + width: 300, + }, + }, + }, + suggestBox: { + zIndex: "9999", + width: 364, + }, +}); + +const keyMap = { + SEARCH: "enter", +}; + +class SearchBarCompoment extends Component { + constructor(props) { + super(props); + this.state = { + anchorEl: null, + input: "", + }; + } + + handlers = { + SEARCH: (e) => { + if (pathHelper.isHomePage(this.props.location.pathname)) { + this.searchMyFile(); + } else { + this.searchShare(); + } + e.target.blur(); + }, + }; + + handleChange = (event) => { + const { currentTarget } = event; + this.input = event.target.value; + this.setState({ + anchorEl: currentTarget, + input: event.target.value, + }); + }; + + cancelSuggest = () => { + this.setState({ + input: "", + }); + }; + + searchMyFile = () => { + this.props.searchMyFile("keywords/" + this.input); + }; + + searchShare = () => { + this.props.history.push( + "/search?keywords=" + encodeURIComponent(this.input) + ); + }; + + render() { + const { classes } = this.props; + const { anchorEl } = this.state; + const id = this.state.input !== "" ? "simple-popper" : null; + const isHomePage = pathHelper.isHomePage(this.props.location.pathname); + + return ( +
+
+ +
+ + + + + {({ TransitionProps }) => ( + + + {isHomePage && ( + + + + + + 在我的文件中搜索{" "} + + {this.state.input} + + + } + /> + + )} + + + + + + + 在全站分享中搜索{" "} + + {this.state.input} + + + } + /> + + + + )} + +
+ ); + } +} + +SearchBarCompoment.propTypes = { + classes: PropTypes.object.isRequired, +}; + +const SearchBar = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(withRouter(SearchBarCompoment))); + +export default SearchBar; diff --git a/assets/src/component/Navbar/SideDrawer.js b/assets/src/component/Navbar/SideDrawer.js new file mode 100644 index 00000000..5ea6316f --- /dev/null +++ b/assets/src/component/Navbar/SideDrawer.js @@ -0,0 +1,97 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import UploadIcon from "@material-ui/icons/CloudUpload"; +import { + Drawer, + withStyles, + Divider, + ListItem, + ListItemIcon, + ListItemText, + List, + Hidden, +} from "@material-ui/core"; +const drawerWidth = 240; +const styles = (theme) => ({ + drawer: { + [theme.breakpoints.up("sm")]: { + width: drawerWidth, + flexShrink: 0, + }, + }, + drawerPaper: { + width: drawerWidth, + }, + toolbar: theme.mixins.toolbar, +}); +class SideDrawer extends Component { + state = { + mobileOpen: false, + }; + + handleDrawerToggle = () => { + this.setState((state) => ({ mobileOpen: !state.mobileOpen })); + }; + + upload() { + alert(""); + } + + render() { + const { classes } = this.props; + + const drawer = ( +
+
+ + + + + + + + + + +
+ ); + + return ( + + ); + } +} +SideDrawer.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(SideDrawer); diff --git a/assets/src/component/Navbar/StorageBar.js b/assets/src/component/Navbar/StorageBar.js new file mode 100644 index 00000000..575c78f5 --- /dev/null +++ b/assets/src/component/Navbar/StorageBar.js @@ -0,0 +1,192 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import StorageIcon from "@material-ui/icons/Storage"; +import { connect } from "react-redux"; +import API from "../../middleware/Api"; +import { sizeToString } from "../../utils"; +import { toggleSnackbar } from "../../actions"; + +import { + withStyles, + LinearProgress, + Typography, + Divider, + Tooltip, +} from "@material-ui/core"; +import ButtonBase from "@material-ui/core/ButtonBase"; +import { withRouter } from "react-router"; + +const mapStateToProps = (state) => { + return { + refresh: state.viewUpdate.storageRefresh, + isLogin: state.viewUpdate.isLogin, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + toggleSnackbar: (vertical, horizontal, msg, color) => { + dispatch(toggleSnackbar(vertical, horizontal, msg, color)); + }, + }; +}; + +const styles = (theme) => ({ + iconFix: { + marginLeft: "32px", + marginRight: "17px", + color: theme.palette.text.secondary, + marginTop: "2px", + }, + textFix: { + padding: " 0 0 0 16px", + }, + storageContainer: { + display: "flex", + marginTop: "15px", + textAlign: "left", + marginBottom: "11px", + }, + detail: { + width: "100%", + marginRight: "35px", + }, + info: { + width: "131px", + overflow: "hidden", + textOverflow: "ellipsis", + [theme.breakpoints.down("xs")]: { + width: "162px", + }, + marginTop: "5px", + }, + bar: { + marginTop: "5px", + }, + stickFooter: { + backgroundColor: theme.palette.background.paper, + }, +}); + +class StorageBarCompoment extends Component { + state = { + percent: 0, + used: null, + total: null, + showExpand: false, + }; + + firstLoad = true; + + componentDidMount = () => { + if (this.firstLoad && this.props.isLogin) { + this.firstLoad = !this.firstLoad; + this.updateStatus(); + } + }; + + componentWillUnmount() { + this.firstLoad = false; + } + + UNSAFE_componentWillReceiveProps = (nextProps) => { + if ( + (this.props.isLogin && this.props.refresh !== nextProps.refresh) || + (this.props.isLogin !== nextProps.isLogin && nextProps.isLogin) + ) { + this.updateStatus(); + } + }; + + updateStatus = () => { + let percent = 0; + API.get("/user/storage") + .then((response) => { + if (response.data.used / response.data.total >= 1) { + percent = 100; + this.props.toggleSnackbar( + "top", + "right", + "您的已用容量已超过容量配额,请尽快删除多余文件或购买容量", + "warning" + ); + } else { + percent = (response.data.used / response.data.total) * 100; + } + this.setState({ + percent: percent, + used: sizeToString(response.data.used), + total: sizeToString(response.data.total), + }); + }) + // eslint-disable-next-line @typescript-eslint/no-empty-function + .catch(() => {}); + }; + + render() { + const { classes } = this.props; + return ( +
this.setState({ showExpand: true })} + onMouseLeave={() => this.setState({ showExpand: false })} + className={classes.stickFooter} + > + + +
+ +
+ 存储空间{" "} + +
+ + + {this.state.used === null + ? " -- " + : this.state.used} + {" / "} + {this.state.total === null + ? " -- " + : this.state.total} + + +
+
+
+
+
+ ); + } +} + +StorageBarCompoment.propTypes = { + classes: PropTypes.object.isRequired, +}; + +const StorageBar = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(withRouter(StorageBarCompoment))); + +export default StorageBar; diff --git a/assets/src/component/Navbar/UserAvatar.js b/assets/src/component/Navbar/UserAvatar.js new file mode 100644 index 00000000..b9e28b8e --- /dev/null +++ b/assets/src/component/Navbar/UserAvatar.js @@ -0,0 +1,172 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import SettingIcon from "@material-ui/icons/Settings"; +import UserAvatarPopover from "./UserAvatarPopover"; +import { AccountCircle } from "mdi-material-ui"; +import { setUserPopover } from "../../actions"; +import Auth from "../../middleware/Auth"; +import { + withStyles, + Grow, + Avatar, + IconButton, + Tooltip, +} from "@material-ui/core"; +import { withRouter } from "react-router-dom"; +import pathHelper from "../../utils/page"; +import DarkModeSwitcher from "./DarkModeSwitcher"; +import { Home } from "@material-ui/icons"; + +const mapStateToProps = (state) => { + return { + selected: state.explorer.selected, + isMultiple: state.explorer.selectProps.isMultiple, + withFolder: state.explorer.selectProps.withFolder, + withFile: state.explorer.selectProps.withFile, + isLogin: state.viewUpdate.isLogin, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + setUserPopover: (anchor) => { + dispatch(setUserPopover(anchor)); + }, + }; +}; + +const styles = (theme) => ({ + mobileHidden: { + [theme.breakpoints.down("xs")]: { + display: "none", + }, + whiteSpace: "nowrap", + }, + avatar: { + width: "30px", + height: "30px", + }, + header: { + display: "flex", + padding: "20px 20px 20px 20px", + }, + largeAvatar: { + height: "90px", + width: "90px", + }, + info: { + marginLeft: "10px", + width: "139px", + }, + badge: { + marginTop: "10px", + }, + visitorMenu: { + width: 200, + }, +}); + +class UserAvatarCompoment extends Component { + state = { + anchorEl: null, + }; + + showUserInfo = (e) => { + this.props.setUserPopover(e.currentTarget); + }; + + handleClose = () => { + this.setState({ + anchorEl: null, + }); + }; + + openURL = (url) => { + window.location.href = url; + }; + + returnHome = () => { + window.location.href = "/home"; + }; + + render() { + const { classes } = this.props; + const loginCheck = Auth.Check(this.props.isLogin); + const user = Auth.GetUser(this.props.isLogin); + const isAdminPage = pathHelper.isAdminPage( + this.props.location.pathname + ); + + return ( +
+ +
+ {!isAdminPage && ( + <> + + {loginCheck && ( + <> + + + this.props.history.push( + "/setting?" + ) + } + color="inherit" + > + + + + + )} + + )} + {isAdminPage && ( + + + + + + )} + + {!loginCheck && } + {loginCheck && ( + + )} + {" "} +
+
+ +
+ ); + } +} + +UserAvatarCompoment.propTypes = { + classes: PropTypes.object.isRequired, +}; + +const UserAvatar = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(withRouter(UserAvatarCompoment))); + +export default UserAvatar; diff --git a/assets/src/component/Navbar/UserAvatarPopover.js b/assets/src/component/Navbar/UserAvatarPopover.js new file mode 100644 index 00000000..8176559b --- /dev/null +++ b/assets/src/component/Navbar/UserAvatarPopover.js @@ -0,0 +1,250 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { + LogoutVariant, + HomeAccount, + DesktopMacDashboard, + AccountArrowRight, + AccountPlus, +} from "mdi-material-ui"; +import { + setSessionStatus, + setUserPopover, + toggleSnackbar, +} from "../../actions"; +import { withRouter } from "react-router-dom"; +import Auth from "../../middleware/Auth"; +import { + withStyles, + Avatar, + Popover, + Typography, + Chip, + ListItemIcon, + MenuItem, + Divider, +} from "@material-ui/core"; +import API from "../../middleware/Api"; +import pathHelper from "../../utils/page"; + +const mapStateToProps = (state) => { + return { + anchorEl: state.viewUpdate.userPopoverAnchorEl, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + setUserPopover: (anchor) => { + dispatch(setUserPopover(anchor)); + }, + toggleSnackbar: (vertical, horizontal, msg, color) => { + dispatch(toggleSnackbar(vertical, horizontal, msg, color)); + }, + setSessionStatus: (status) => { + dispatch(setSessionStatus(status)); + }, + }; +}; +const styles = () => ({ + avatar: { + width: "30px", + height: "30px", + }, + header: { + display: "flex", + padding: "20px 20px 20px 20px", + }, + largeAvatar: { + height: "90px", + width: "90px", + }, + info: { + marginLeft: "10px", + width: "139px", + }, + badge: { + marginTop: "10px", + }, + visitorMenu: { + width: 200, + }, +}); + +class UserAvatarPopoverCompoment extends Component { + handleClose = () => { + this.props.setUserPopover(null); + }; + + openURL = (url) => { + window.location.href = url; + }; + + sigOut = () => { + API.delete("/user/session/") + .then(() => { + this.props.toggleSnackbar( + "top", + "right", + "您已退出登录", + "success" + ); + Auth.signout(); + window.location.reload(); + this.props.setSessionStatus(false); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "warning" + ); + }) + .then(() => { + this.handleClose(); + }); + }; + + render() { + const { classes } = this.props; + const user = Auth.GetUser(); + const isAdminPage = pathHelper.isAdminPage( + this.props.location.pathname + ); + + return ( + + {!Auth.Check() && ( +
+ + this.props.history.push("/login")} + > + + + + 登录 + + this.props.history.push("/signup")} + > + + + + 注册 + +
+ )} + {Auth.Check() && ( +
+
+
+ +
+
+ {user.nickname} + + {user.user_name} + + +
+
+
+ + {!isAdminPage && ( + { + this.handleClose(); + this.props.history.push( + "/profile/" + user.id + ); + }} + > + + + + 个人主页 + + )} + {user.group.id === 1 && ( + { + this.handleClose(); + this.props.history.push("/admin/home"); + }} + > + + + + 管理面板 + + )} + + + + + + 退出登录 + +
+
+ )} +
+ ); + } +} + +UserAvatarPopoverCompoment.propTypes = { + classes: PropTypes.object.isRequired, +}; + +const UserAvatarPopover = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(withRouter(UserAvatarPopoverCompoment))); + +export default UserAvatarPopover; diff --git a/assets/src/component/Navbar/UserInfo.js b/assets/src/component/Navbar/UserInfo.js new file mode 100644 index 00000000..e2747528 --- /dev/null +++ b/assets/src/component/Navbar/UserInfo.js @@ -0,0 +1,153 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { setUserPopover } from "../../actions"; +import { withStyles, Typography } from "@material-ui/core"; +import Auth from "../../middleware/Auth"; +import DarkModeSwitcher from "./DarkModeSwitcher"; +import Avatar from "@material-ui/core/Avatar"; + +const mapStateToProps = (state) => { + return { + isLogin: state.viewUpdate.isLogin, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + setUserPopover: (anchor) => { + dispatch(setUserPopover(anchor)); + }, + }; +}; + +const styles = (theme) => ({ + userNav: { + height: "170px", + backgroundColor: theme.palette.primary.main, + padding: "20px 20px 2em", + backgroundImage: + "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1600 900'%3E%3Cpolygon fill='" + + theme.palette.primary.light.replace("#", "%23") + + "' points='957 450 539 900 1396 900'/%3E%3Cpolygon fill='" + + theme.palette.primary.dark.replace("#", "%23") + + "' points='957 450 872.9 900 1396 900'/%3E%3Cpolygon fill='" + + theme.palette.secondary.main.replace("#", "%23") + + "' points='-60 900 398 662 816 900'/%3E%3Cpolygon fill='" + + theme.palette.secondary.dark.replace("#", "%23") + + "' points='337 900 398 662 816 900'/%3E%3Cpolygon fill='" + + theme.palette.secondary.light.replace("#", "%23") + + "' points='1203 546 1552 900 876 900'/%3E%3Cpolygon fill='" + + theme.palette.secondary.main.replace("#", "%23") + + "' points='1203 546 1552 900 1162 900'/%3E%3Cpolygon fill='" + + theme.palette.primary.dark.replace("#", "%23") + + "' points='641 695 886 900 367 900'/%3E%3Cpolygon fill='" + + theme.palette.primary.main.replace("#", "%23") + + "' points='587 900 641 695 886 900'/%3E%3Cpolygon fill='" + + theme.palette.secondary.light.replace("#", "%23") + + "' points='1710 900 1401 632 1096 900'/%3E%3Cpolygon fill='" + + theme.palette.secondary.dark.replace("#", "%23") + + "' points='1710 900 1401 632 1365 900'/%3E%3Cpolygon fill='" + + theme.palette.secondary.main.replace("#", "%23") + + "' points='1210 900 971 687 725 900'/%3E%3Cpolygon fill='" + + theme.palette.secondary.dark.replace("#", "%23") + + "' points='943 900 1210 900 971 687'/%3E%3C/svg%3E\")", + backgroundSize: "cover", + }, + avatar: { + display: "block", + width: "70px", + height: "70px", + border: " 2px solid #fff", + borderRadius: "50%", + overflow: "hidden", + boxShadow: + "0 2px 5px 0 rgba(0,0,0,0.16), 0 2px 10px 0 rgba(0,0,0,0.12)", + }, + avatarImg: { + width: "66px", + height: "66px", + }, + nickName: { + color: "#fff", + marginLeft: "10px", + marginTop: "15px", + fontSize: "17px", + }, + flexAvatar: { + display: "flex", + justifyContent: "space-between", + alignItems: "end", + }, + groupName: { + marginLeft: "10px", + color: "#ffffff", + opacity: "0.54", + }, + storageCircle: { + width: "200px", + }, +}); + +class UserInfoCompoment extends Component { + showUserInfo = (e) => { + this.props.setUserPopover(e.currentTarget); + }; + + render() { + const { classes } = this.props; + const isLogin = Auth.Check(this.props.isLogin); + const user = Auth.GetUser(this.props.isLogin); + + return ( +
+
+ {/* eslint-disable-next-line */} + + {isLogin && ( + + )} + {!isLogin && ( + + )} + + +
+
+ + {isLogin ? user.nickname : "未登录"} + + + {isLogin ? user.group.name : "游客"} + +
+
+ ); + } +} + +UserInfoCompoment.propTypes = { + classes: PropTypes.object.isRequired, +}; + +const UserInfo = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(UserInfoCompoment)); + +export default UserInfo; diff --git a/assets/src/component/Placeholder/Captcha.js b/assets/src/component/Placeholder/Captcha.js new file mode 100644 index 00000000..d2666870 --- /dev/null +++ b/assets/src/component/Placeholder/Captcha.js @@ -0,0 +1,20 @@ +import React from "react"; +import ContentLoader from "react-content-loader"; + +const MyLoader = () => ( + + + +); + +function captchaPlacholder() { + return ; +} + +export default captchaPlacholder; diff --git a/assets/src/component/Placeholder/ErrorBoundary.js b/assets/src/component/Placeholder/ErrorBoundary.js new file mode 100644 index 00000000..2311df43 --- /dev/null +++ b/assets/src/component/Placeholder/ErrorBoundary.js @@ -0,0 +1,65 @@ +import React from "react"; +import { withStyles } from "@material-ui/core"; + +const styles = { + h1: { + color: "#a4a4a4", + margin: "5px 0px", + }, + h2: { + margin: "15px 0px", + }, +}; + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false, error: null, errorInfo: null }; + } + + static getDerivedStateFromError() { + // 更新 state 使下一次渲染能够显示降级后的 UI + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) { + this.setState({ + error: error, + errorInfo: errorInfo, + }); + } + + render() { + const { classes } = this.props; + if (this.state.hasError) { + // 你可以自定义降级后的 UI 并渲染 + return ( + <> +

:(

+

+ 页面渲染出现错误,请尝试刷新此页面。 +

+ {this.state.error && + this.state.errorInfo && + this.state.errorInfo.componentStack && ( +
+ 错误详情 +
+                                    {this.state.error.toString()}
+                                
+
+                                    
+                                        {this.state.errorInfo.componentStack}
+                                    
+                                
+
+ )} + + ); + } + + return this.props.children; + } +} + +export default withStyles(styles)(ErrorBoundary); diff --git a/assets/src/component/Placeholder/ListLoading.js b/assets/src/component/Placeholder/ListLoading.js new file mode 100644 index 00000000..bbca6d47 --- /dev/null +++ b/assets/src/component/Placeholder/ListLoading.js @@ -0,0 +1,38 @@ +import React from "react"; +import { BulletList } from "react-content-loader"; +import { makeStyles, useTheme } from "@material-ui/core/styles"; + +const useStyles = makeStyles((theme) => ({ + loader: { + width: "100%", + // padding: 40, + // [theme.breakpoints.down("md")]: { + // width: "100%", + // padding: 10 + // } + }, +})); + +const MyLoader = (props) => ( + +); + +function ListLoading() { + const theme = useTheme(); + const classes = useStyles(); + + return ( +
+ +
+ ); +} + +export default ListLoading; diff --git a/assets/src/component/Placeholder/PageLoading.js b/assets/src/component/Placeholder/PageLoading.js new file mode 100644 index 00000000..411f6622 --- /dev/null +++ b/assets/src/component/Placeholder/PageLoading.js @@ -0,0 +1,44 @@ +import React from "react"; +import { Facebook } from "react-content-loader"; +import { makeStyles, useTheme } from "@material-ui/core/styles"; + +const useStyles = makeStyles((theme) => ({ + loader: { + width: "80%", + [theme.breakpoints.up("md")]: { + width: " 50%", + }, + + marginTop: 30, + }, +})); + +const MyLoader = (props) => { + return ( + + ); +}; + +function PageLoading() { + const theme = useTheme(); + const classes = useStyles(); + + return ( +
+ +
+ ); +} + +export default PageLoading; diff --git a/assets/src/component/Placeholder/TextLoading.js b/assets/src/component/Placeholder/TextLoading.js new file mode 100644 index 00000000..da264ecb --- /dev/null +++ b/assets/src/component/Placeholder/TextLoading.js @@ -0,0 +1,38 @@ +import React from "react"; +import { Code } from "react-content-loader"; +import { makeStyles, useTheme } from "@material-ui/core/styles"; + +const useStyles = makeStyles((theme) => ({ + loader: { + width: "70%", + padding: 40, + [theme.breakpoints.down("md")]: { + width: "100%", + padding: 10, + }, + }, +})); + +const MyLoader = (props) => ( + +); + +function TextLoading() { + const theme = useTheme(); + const classes = useStyles(); + + return ( +
+ +
+ ); +} + +export default TextLoading; diff --git a/assets/src/component/Setting/Authn.js b/assets/src/component/Setting/Authn.js new file mode 100644 index 00000000..fac8d02d --- /dev/null +++ b/assets/src/component/Setting/Authn.js @@ -0,0 +1,209 @@ +import React, { useState, useCallback } from "react"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + List, + ListItem, + ListItemIcon, + ListItemSecondaryAction, + ListItemText, + makeStyles, + Paper, + Typography, +} from "@material-ui/core"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../actions"; +import RightIcon from "@material-ui/icons/KeyboardArrowRight"; +import { Add, Fingerprint, HighlightOff } from "@material-ui/icons"; +import API from "../../middleware/Api"; +import { bufferDecode, bufferEncode } from "../../utils"; + +const useStyles = makeStyles((theme) => ({ + sectionTitle: { + paddingBottom: "10px", + paddingTop: "30px", + }, + rightIcon: { + marginTop: "4px", + marginRight: "10px", + color: theme.palette.text.secondary, + }, + desenList: { + paddingTop: 0, + paddingBottom: 0, + }, + iconFix: { + marginRight: "11px", + marginLeft: "7px", + minWidth: 40, + }, + flexContainer: { + display: "flex", + }, +})); + +export default function Authn(props) { + const [selected, setSelected] = useState(""); + const [confirm, setConfirm] = useState(false); + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const deleteCredential = (id) => { + API.patch("/user/setting/authn", { + id: id, + }) + .then(() => { + ToggleSnackbar("top", "right", "凭证已删除", "success"); + props.remove(id); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }) + .then(() => { + setConfirm(false); + }); + }; + + const classes = useStyles(); + + const addCredential = () => { + if (!navigator.credentials) { + ToggleSnackbar("top", "right", "当前浏览器或环境不支持", "warning"); + + return; + } + API.put("/user/authn", {}) + .then((response) => { + const credentialCreationOptions = response.data; + credentialCreationOptions.publicKey.challenge = bufferDecode( + credentialCreationOptions.publicKey.challenge + ); + credentialCreationOptions.publicKey.user.id = bufferDecode( + credentialCreationOptions.publicKey.user.id + ); + if (credentialCreationOptions.publicKey.excludeCredentials) { + for ( + let i = 0; + i < + credentialCreationOptions.publicKey.excludeCredentials + .length; + i++ + ) { + credentialCreationOptions.publicKey.excludeCredentials[ + i + ].id = bufferDecode( + credentialCreationOptions.publicKey + .excludeCredentials[i].id + ); + } + } + + return navigator.credentials.create({ + publicKey: credentialCreationOptions.publicKey, + }); + }) + .then((credential) => { + const attestationObject = credential.response.attestationObject; + const clientDataJSON = credential.response.clientDataJSON; + const rawId = credential.rawId; + return API.put( + "/user/authn/finish", + JSON.stringify({ + id: credential.id, + rawId: bufferEncode(rawId), + type: credential.type, + response: { + attestationObject: bufferEncode(attestationObject), + clientDataJSON: bufferEncode(clientDataJSON), + }, + }) + ); + }) + .then((response) => { + props.add(response.data); + ToggleSnackbar("top", "right", "验证器已添加", "success"); + return; + }) + .catch((error) => { + console.log(error); + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + + return ( +
+ setConfirm(false)}> + 删除凭证 + 确定要吊销这个凭证吗? + + + + + + + + 外部认证器 + + + + {props.list.map((v) => ( + <> + { + setConfirm(true); + setSelected(v.id); + }} + > + + + + + + deleteCredential(v.id)} + className={classes.flexContainer} + > + + + + + + ))} + addCredential()}> + + + + + + + + + + + +
+ ); +} diff --git a/assets/src/component/Setting/Profile.js b/assets/src/component/Setting/Profile.js new file mode 100644 index 00000000..d77ea313 --- /dev/null +++ b/assets/src/component/Setting/Profile.js @@ -0,0 +1,430 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { toggleSnackbar } from "../../actions"; +import API from "../../middleware/Api"; + +import { + withStyles, + Paper, + Avatar, + Typography, + Tabs, + Tab, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Grid, +} from "@material-ui/core"; +import { withRouter } from "react-router"; +import Pagination from "@material-ui/lab/Pagination"; + +const styles = (theme) => ({ + layout: { + width: "auto", + marginTop: "50px", + marginLeft: theme.spacing(3), + marginRight: theme.spacing(3), + marginBottom: "30px", + [theme.breakpoints.up("sm")]: { + width: 700, + marginLeft: "auto", + marginRight: "auto", + }, + }, + userNav: { + height: "270px", + backgroundColor: theme.palette.primary.main, + padding: "20px 20px 2em", + backgroundImage: + "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1600 900'%3E%3Cpolygon fill='" + + theme.palette.primary.light.replace("#", "%23") + + "' points='957 450 539 900 1396 900'/%3E%3Cpolygon fill='" + + theme.palette.primary.dark.replace("#", "%23") + + "' points='957 450 872.9 900 1396 900'/%3E%3Cpolygon fill='" + + theme.palette.secondary.main.replace("#", "%23") + + "' points='-60 900 398 662 816 900'/%3E%3Cpolygon fill='" + + theme.palette.secondary.dark.replace("#", "%23") + + "' points='337 900 398 662 816 900'/%3E%3Cpolygon fill='" + + theme.palette.secondary.light.replace("#", "%23") + + "' points='1203 546 1552 900 876 900'/%3E%3Cpolygon fill='" + + theme.palette.secondary.main.replace("#", "%23") + + "' points='1203 546 1552 900 1162 900'/%3E%3Cpolygon fill='" + + theme.palette.primary.dark.replace("#", "%23") + + "' points='641 695 886 900 367 900'/%3E%3Cpolygon fill='" + + theme.palette.primary.main.replace("#", "%23") + + "' points='587 900 641 695 886 900'/%3E%3Cpolygon fill='" + + theme.palette.secondary.light.replace("#", "%23") + + "' points='1710 900 1401 632 1096 900'/%3E%3Cpolygon fill='" + + theme.palette.secondary.dark.replace("#", "%23") + + "' points='1710 900 1401 632 1365 900'/%3E%3Cpolygon fill='" + + theme.palette.secondary.main.replace("#", "%23") + + "' points='1210 900 971 687 725 900'/%3E%3Cpolygon fill='" + + theme.palette.secondary.dark.replace("#", "%23") + + "' points='943 900 1210 900 971 687'/%3E%3C/svg%3E\")", + backgroundSize: "cover", + backgroundPosition: "bottom", + }, + avatarContainer: { + height: "80px", + width: "80px", + borderRaidus: "50%", + margin: "auto", + marginTop: "50px", + boxShadow: + "0 2px 5px 0 rgba(0,0,0,0.16), 0 2px 10px 0 rgba(0,0,0,0.12)", + border: "2px solid #fff", + }, + nickName: { + width: "200px", + margin: "auto", + textAlign: "center", + marginTop: "1px", + fontSize: "25px", + color: "#ffffff", + opacity: "0.81", + }, + th: { + minWidth: "106px", + }, + mobileHide: { + [theme.breakpoints.down("md")]: { + display: "none", + }, + }, + tableLink: { + cursor: "pointer", + }, + navigator: { + padding: theme.spacing(2), + }, + pageInfo: { + marginTop: "14px", + marginLeft: "23px", + }, + infoItem: { + paddingLeft: "46px!important", + paddingBottom: "20px!important", + }, + infoContainer: { + marginTop: "30px", + }, + tableContainer: { + overflowX: "auto", + }, +}); +const mapStateToProps = () => { + return {}; +}; + +const mapDispatchToProps = (dispatch) => { + return { + toggleSnackbar: (vertical, horizontal, msg, color) => { + dispatch(toggleSnackbar(vertical, horizontal, msg, color)); + }, + }; +}; + +class ProfileCompoment extends Component { + state = { + listType: 0, + shareList: [], + page: 1, + user: null, + total: 0, + }; + + handleChange = (event, listType) => { + this.setState({ listType }); + if (listType === 1) { + this.loadList(1, "hot"); + } else if (listType === 0) { + this.loadList(1, "default"); + } + }; + + componentDidMount = () => { + this.loadList(1, "default"); + }; + + loadList = (page, order) => { + API.get( + "/user/profile/" + + this.props.match.params.id + + "?page=" + + page + + "&type=" + + order + ) + .then((response) => { + this.setState({ + shareList: response.data.items, + user: response.data.user, + total: response.data.total, + }); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + }); + }; + + loadNext = () => { + this.loadList( + this.state.page + 1, + this.state.listType === 0 ? "default" : "hot" + ); + }; + + loadPrev = () => { + this.loadList( + this.state.page - 1, + this.state.listType === 0 ? "default" : "hot" + ); + }; + + render() { + const { classes } = this.props; + + return ( +
+ {this.state.user === null &&
} + {this.state.user !== null && ( + +
+
+ +
+
+ + {this.state.user.nick} + +
+
+ + + + + + {this.state.listType === 2 && ( +
+ + + + UID + + + {this.state.user.id} + + + + + 昵称 + + + {this.state.user.nick} + + + + + 用户组 + + + {this.state.user.group} + + + + + 分享总数 + + + {this.state.total} + + + + + 注册日期 + + + {this.state.user.date} + + + +
+ )} + {(this.state.listType === 0 || + this.state.listType === 1) && ( +
+
+ + + + 文件名 + + 分享日期 + + + 下载次数 + + + 浏览次数 + + + + + {this.state.shareList.map( + (row, id) => ( + + this.props.history.push( + "/s/" + row.key + ) + } + > + + + {row.source + ? row.source + .name + : "[已失效]"} + + + + {row.create_date} + + + {row.downloads} + + + {row.views} + + + ) + )} + +
+
+ {this.state.shareList.length !== 0 && + this.state.listType === 0 && ( +
+ + this.loadList( + v, + this.state.listType === + 0 + ? "default" + : "hot" + ) + } + color="secondary" + /> +
+ )} +
+ )} +
+ )} +
+ ); + } +} + +const Profile = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(withRouter(ProfileCompoment))); + +export default Profile; diff --git a/assets/src/component/Setting/Tasks.js b/assets/src/component/Setting/Tasks.js new file mode 100644 index 00000000..8f7ba624 --- /dev/null +++ b/assets/src/component/Setting/Tasks.js @@ -0,0 +1,153 @@ +import React, { useState, useCallback, useEffect } from "react"; +import { makeStyles, Typography } from "@material-ui/core"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../actions"; +import Paper from "@material-ui/core/Paper"; +import Table from "@material-ui/core/Table"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import TableCell from "@material-ui/core/TableCell"; +import TableBody from "@material-ui/core/TableBody"; +import API from "../../middleware/Api"; +import { getTaskProgress, getTaskStatus, getTaskType } from "../../config"; +import Pagination from "@material-ui/lab/Pagination"; +import { formatLocalTime } from "../../utils/datetime"; + +const useStyles = makeStyles((theme) => ({ + layout: { + width: "auto", + marginTop: "50px", + marginLeft: theme.spacing(3), + marginRight: theme.spacing(3), + [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { + width: 1100, + marginLeft: "auto", + marginRight: "auto", + }, + marginBottom: "50px", + }, + content: { + marginTop: theme.spacing(4), + overflowX: "auto", + }, + cardContent: { + padding: theme.spacing(2), + }, + tableContainer: { + overflowX: "auto", + }, + create: { + marginTop: theme.spacing(2), + }, + noWrap: { + wordBreak: "keepAll", + }, + footer: { + padding: theme.spacing(2), + }, +})); + +export default function Tasks() { + const [tasks, setTasks] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const loadList = (page) => { + API.get("/user/setting/tasks?page=" + page) + .then((response) => { + setTasks(response.data.tasks); + setTotal(response.data.total); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + + useEffect(() => { + loadList(page); + // eslint-disable-next-line + }, [page]); + + const getError = (error) => { + if (error === "") { + return "-"; + } + try { + const res = JSON.parse(error); + return res.msg; + } catch (e) { + return "未知"; + } + }; + + const classes = useStyles(); + + return ( +
+ + 任务队列 + + + + + + 创建于 + + 任务类型 + + + 状态 + + + 最后进度 + + 错误信息 + + + + {tasks.map((row, id) => ( + + + {formatLocalTime( + row.create_date, + "YYYY-MM-DD H:mm:ss" + )} + + + {getTaskType(row.type)} + + + {getTaskStatus(row.status)} + + + {getTaskProgress(row.type, row.progress)} + + + {getError(row.error)} + + + ))} + +
+
+ setPage(v)} + color="secondary" + /> +
+
+
+ ); +} diff --git a/assets/src/component/Setting/UserSetting.js b/assets/src/component/Setting/UserSetting.js new file mode 100644 index 00000000..f28ba8ae --- /dev/null +++ b/assets/src/component/Setting/UserSetting.js @@ -0,0 +1,1299 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import PhotoIcon from "@material-ui/icons/InsertPhoto"; +import GroupIcon from "@material-ui/icons/Group"; +import DateIcon from "@material-ui/icons/DateRange"; +import EmailIcon from "@material-ui/icons/Email"; +import HomeIcon from "@material-ui/icons/Home"; +import LinkIcon from "@material-ui/icons/Phonelink"; +import InputIcon from "@material-ui/icons/Input"; +import SecurityIcon from "@material-ui/icons/Security"; +import NickIcon from "@material-ui/icons/PermContactCalendar"; +import LockIcon from "@material-ui/icons/Lock"; +import VerifyIcon from "@material-ui/icons/VpnKey"; +import ColorIcon from "@material-ui/icons/Palette"; +import { + applyThemes, + changeViewMethod, + toggleDaylightMode, + toggleSnackbar, +} from "../../actions"; +import axios from "axios"; +import FingerprintIcon from "@material-ui/icons/Fingerprint"; +import ToggleButton from "@material-ui/lab/ToggleButton"; +import ToggleButtonGroup from "@material-ui/lab/ToggleButtonGroup"; +import RightIcon from "@material-ui/icons/KeyboardArrowRight"; +import { + ListItemIcon, + withStyles, + Button, + Divider, + TextField, + Avatar, + Paper, + Typography, + List, + ListItem, + ListItemSecondaryAction, + ListItemText, + ListItemAvatar, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Switch, +} from "@material-ui/core"; +import { blue, green, yellow } from "@material-ui/core/colors"; +import API from "../../middleware/Api"; +import Auth from "../../middleware/Auth"; +import { withRouter } from "react-router"; +import QRCode from "qrcode-react"; +import { Brightness3, ListAlt, PermContactCalendar,Schedule } from "@material-ui/icons"; +import { transformTime } from "../../utils"; +import Authn from "./Authn"; +import { formatLocalTime, timeZone } from "../../utils/datetime"; +import TimeZoneDialog from "../Modals/TimeZone"; + +const styles = (theme) => ({ + layout: { + width: "auto", + marginLeft: theme.spacing(3), + marginRight: theme.spacing(3), + [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { + width: 700, + marginLeft: "auto", + marginRight: "auto", + }, + }, + sectionTitle: { + paddingBottom: "10px", + paddingTop: "30px", + }, + rightIcon: { + marginTop: "4px", + marginRight: "10px", + color: theme.palette.text.secondary, + }, + uploadFromFile: { + backgroundColor: blue[100], + color: blue[600], + }, + userGravatar: { + backgroundColor: yellow[100], + color: yellow[800], + }, + policySelected: { + backgroundColor: green[100], + color: green[800], + }, + infoText: { + marginRight: "17px", + }, + infoTextWithIcon: { + marginRight: "17px", + marginTop: "1px", + }, + rightIconWithText: { + marginTop: "0px", + marginRight: "10px", + color: theme.palette.text.secondary, + }, + iconFix: { + marginRight: "11px", + marginLeft: "7px", + minWidth: 40, + }, + flexContainer: { + display: "flex", + }, + desenList: { + paddingTop: 0, + paddingBottom: 0, + }, + flexContainerResponse: { + display: "flex", + [theme.breakpoints.down("sm")]: { + display: "initial", + }, + }, + desText: { + marginTop: "10px", + }, + secondColor: { + height: "20px", + width: "20px", + backgroundColor: theme.palette.secondary.main, + borderRadius: "50%", + marginRight: "17px", + }, + firstColor: { + height: "20px", + width: "20px", + backgroundColor: theme.palette.primary.main, + borderRadius: "50%", + marginRight: "6px", + }, + themeBlock: { + height: "20px", + width: "20px", + }, + paddingBottom: { + marginBottom: "30px", + }, + paddingText: { + paddingRight: theme.spacing(2), + }, + qrcode: { + width: 128, + marginTop: 16, + marginRight: 16, + }, +}); + +const mapStateToProps = (state) => { + return { + title: state.siteConfig.title, + authn: state.siteConfig.authn, + viewMethod: state.viewUpdate.explorerViewMethod, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + toggleSnackbar: (vertical, horizontal, msg, color) => { + dispatch(toggleSnackbar(vertical, horizontal, msg, color)); + }, + applyThemes: (color) => { + dispatch(applyThemes(color)); + }, + toggleDaylightMode: () => { + dispatch(toggleDaylightMode()); + }, + changeView: (method) => { + dispatch(changeViewMethod(method)); + }, + }; +}; + +class UserSettingCompoment extends Component { + constructor(props) { + super(props); + this.fileInput = React.createRef(); + } + + state = { + avatarModal: false, + nickModal: false, + changePassword: false, + loading: "", + oldPwd: "", + newPwd: "", + webdavPwd: "", + newPwdRepeat: "", + twoFactor: false, + authCode: "", + changeTheme: false, + chosenTheme: null, + showWebDavUrl: false, + showWebDavUserName: false, + changeWebDavPwd: false, + groupBackModal: false, + changePolicy: false, + changeTimeZone: false, + settings: { + uid: 0, + group_expires: 0, + policy: { + current: { + name: "-", + id: "", + }, + options: [], + }, + qq: "", + homepage: true, + two_factor: "", + two_fa_secret: "", + prefer_theme: "", + themes: {}, + authn: [], + }, + }; + + handleClose = () => { + this.setState({ + avatarModal: false, + nickModal: false, + changePassword: false, + loading: "", + twoFactor: false, + changeTheme: false, + showWebDavUrl: false, + showWebDavUserName: false, + changeWebDavPwd: false, + groupBackModal: false, + changePolicy: false, + }); + }; + + componentDidMount() { + this.loadSetting(); + } + + toggleViewMethod = () => { + const newMethod = + this.props.viewMethod === "icon" + ? "list" + : this.props.viewMethod === "list" + ? "smallIcon" + : "icon"; + Auth.SetPreference("view_method", newMethod); + this.props.changeView(newMethod); + }; + + loadSetting = () => { + API.get("/user/setting") + .then((response) => { + const theme = JSON.parse(response.data.themes); + response.data.themes = theme; + this.setState({ + settings: response.data, + }); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + }); + }; + + useGravatar = () => { + this.setState({ + loading: "gravatar", + }); + API.put("/user/setting/avatar") + .then(() => { + this.props.toggleSnackbar( + "top", + "right", + "头像已更新,刷新后生效", + "success" + ); + this.setState({ + loading: "", + }); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + this.setState({ + loading: "", + }); + }); + }; + + changeNick = () => { + this.setState({ + loading: "nick", + }); + API.patch("/user/setting/nick", { + nick: this.state.nick, + }) + .then(() => { + this.props.toggleSnackbar( + "top", + "right", + "昵称已更改,刷新后生效", + "success" + ); + this.setState({ + loading: "", + }); + this.handleClose(); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + this.setState({ + loading: "", + }); + }); + }; + + uploadAvatar = () => { + this.setState({ + loading: "avatar", + }); + const formData = new FormData(); + formData.append("avatar", this.fileInput.current.files[0]); + API.post("/user/setting/avatar", formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }) + .then(() => { + this.props.toggleSnackbar( + "top", + "right", + "头像已更新,刷新后生效", + "success" + ); + this.setState({ + loading: "", + }); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + this.setState({ + loading: "", + }); + }); + }; + + handleToggle = () => { + API.patch("/user/setting/homepage", { + status: !this.state.settings.homepage, + }) + .then(() => { + this.props.toggleSnackbar( + "top", + "right", + "设置已保存", + "success" + ); + this.setState({ + settings: { + ...this.state.settings, + homepage: !this.state.settings.homepage, + }, + }); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + }); + }; + + changhePwd = () => { + if (this.state.newPwd !== this.state.newPwdRepeat) { + this.props.toggleSnackbar( + "top", + "right", + "两次密码输入不一致", + "warning" + ); + return; + } + this.setState({ + loading: "changePassword", + }); + API.patch("/user/setting/password", { + old: this.state.oldPwd, + new: this.state.newPwd, + }) + .then(() => { + this.props.toggleSnackbar( + "top", + "right", + "密码已更新", + "success" + ); + this.setState({ + loading: "", + }); + this.handleClose(); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + this.setState({ + loading: "", + }); + }); + }; + + changeTheme = () => { + this.setState({ + loading: "changeTheme", + }); + API.patch("/user/setting/theme", { + theme: this.state.chosenTheme, + }) + .then(() => { + this.props.toggleSnackbar( + "top", + "right", + "主题配色已更换", + "success" + ); + this.props.applyThemes(this.state.chosenTheme); + this.setState({ + loading: "", + }); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + this.setState({ + loading: "", + }); + }); + }; + + changheWebdavPwd = () => { + this.setState({ + loading: "changheWebdavPwd", + }); + axios + .post("/Member/setWebdavPwd", { + pwd: this.state.webdavPwd, + }) + .then((response) => { + if (response.data.error === "1") { + this.props.toggleSnackbar( + "top", + "right", + response.data.msg, + "error" + ); + this.setState({ + loading: "", + }); + } else { + this.props.toggleSnackbar( + "top", + "right", + response.data.msg, + "success" + ); + this.setState({ + loading: "", + changeWebDavPwd: false, + }); + } + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + this.setState({ + loading: "", + }); + }); + }; + + init2FA = () => { + if (this.state.settings.two_factor) { + this.setState({ twoFactor: true }); + return; + } + API.get("/user/setting/2fa") + .then((response) => { + this.setState({ + two_fa_secret: response.data, + twoFactor: true, + }); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + }); + }; + + twoFactor = () => { + this.setState({ + loading: "twoFactor", + }); + API.patch("/user/setting/2fa", { + code: this.state.authCode, + }) + .then(() => { + this.props.toggleSnackbar( + "top", + "right", + "设定已保存", + "success" + ); + this.setState({ + loading: "", + settings: { + ...this.state.settings, + two_factor: !this.state.settings.two_factor, + }, + }); + this.handleClose(); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + this.setState({ + loading: "", + }); + }); + }; + + handleChange = (name) => (event) => { + this.setState({ [name]: event.target.value }); + }; + + handleAlignment = (event, chosenTheme) => this.setState({ chosenTheme }); + + toggleThemeMode = (current) => { + if (current !== null) { + this.props.toggleDaylightMode(); + Auth.SetPreference("theme_mode", null); + } + }; + + render() { + const { classes } = this.props; + const user = Auth.GetUser(); + const dark = Auth.GetPreference("theme_mode"); + + return ( +
+
+ + 个人资料 + + + + + this.setState({ avatarModal: true }) + } + > + + + + + + + + + + + + + + + + + + {this.state.settings.uid} + + + + + + this.setState({ nickModal: true }) + } + > + + + + + + + this.setState({ nickModal: true }) + } + className={classes.flexContainer} + > + + {user.nickname} + + + + + + + + + + + + + + {user.user_name} + + + + + + + + + + + + + {user.group.name} + + + + + + + + + + + + + {formatLocalTime( + user.created_at, + "YYYY-MM-DD H:mm:ss" + )} + + + + + + + 安全隐私 + + + + + + + + + + + + + + + + this.setState({ changePassword: true }) + } + > + + + + + + + + + + + this.init2FA()}> + + + + + + + + {!this.state.settings.two_factor + ? "未开启" + : "已开启"} + + + + + + + + { + this.setState({ + settings: { + ...this.state.settings, + authn: [ + ...this.state.settings.authn, + credential, + ], + }, + }); + }} + remove={(id) => { + let credentials = [...this.state.settings.authn]; + credentials = credentials.filter((v) => { + return v.id !== id; + }); + this.setState({ + settings: { + ...this.state.settings, + authn: credentials, + }, + }); + }} + /> + + + 个性化 + + + + + this.setState({ changeTheme: true }) + } + > + + + + + + +
+
+
+
+ + this.toggleThemeMode(dark)} + > + + + + + + + + {dark && + (dark === "dark" + ? "偏好开启" + : "偏好关闭")} + {dark === null && "跟随系统"} + + + + + + this.toggleViewMethod()} + > + + + + + + + + {this.props.viewMethod === "icon" && + "大图标"} + {this.props.viewMethod === "list" && + "列表"} + {this.props.viewMethod === + "smallIcon" && "小图标"} + + + + + + + this.setState({ changeTimeZone: true }) + } + button + > + + + + + + + + {timeZone} + + + + +
+
+ {user.group.webdav && ( +
+ + WebDAV + + + + + this.setState({ + showWebDavUrl: true, + }) + } + > + + + + + + + + + + + + this.setState({ + showWebDavUserName: true, + }) + } + > + + + + + + + + + + + + this.props.history.push("/webdav?") + } + > + + + + + + + + + + + +
+ )} +
+
+ this.setState({ changeTimeZone: false })} + open={this.state.changeTimeZone} + /> + + 修改头像 + + + + + + + + + + + + + + + + + + + + + + + + + 修改昵称 + + + + + + + + + + 修改登录密码 + +
+ +
+
+ +
+
+ +
+
+ + + + +
+ + + {this.state.settings.two_factor ? "关闭" : "启用"} + 二步验证 + + +
+ {!this.state.settings.two_factor && ( +
+ +
+
+
+ + + + +
+ + 更改主题配色 + + + {Object.keys(this.state.settings.themes).map( + (value, key) => ( + +
+ + ) + )} + + + + + + +
+ + WebDAV连接地址 + + + + + + + + + WebDAV用户名 + + + + + + + +
+ ); + } +} + +const UserSetting = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(withRouter(UserSettingCompoment))); + +export default UserSetting; diff --git a/assets/src/component/Setting/WebDAV.js b/assets/src/component/Setting/WebDAV.js new file mode 100644 index 00000000..558944ab --- /dev/null +++ b/assets/src/component/Setting/WebDAV.js @@ -0,0 +1,236 @@ +import React, { useState, useCallback, useEffect } from "react"; +import { makeStyles, Typography } from "@material-ui/core"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../actions"; +import Paper from "@material-ui/core/Paper"; +import Tabs from "@material-ui/core/Tabs"; +import Tab from "@material-ui/core/Tab"; +import Button from "@material-ui/core/Button"; +import TableContainer from "@material-ui/core/TableContainer"; +import Table from "@material-ui/core/Table"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import TableCell from "@material-ui/core/TableCell"; +import TableBody from "@material-ui/core/TableBody"; +import Alert from "@material-ui/lab/Alert"; +import Auth from "../../middleware/Auth"; +import API from "../../middleware/Api"; +import IconButton from "@material-ui/core/IconButton"; +import { Delete } from "@material-ui/icons"; +import CreateWebDAVAccount from "../Modals/CreateWebDAVAccount"; +import TimeAgo from "timeago-react"; +import Link from "@material-ui/core/Link"; + +const useStyles = makeStyles((theme) => ({ + layout: { + width: "auto", + marginTop: "50px", + marginLeft: theme.spacing(3), + marginRight: theme.spacing(3), + [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { + width: 1100, + marginLeft: "auto", + marginRight: "auto", + }, + marginBottom: "50px", + }, + content: { + marginTop: theme.spacing(4), + }, + cardContent: { + padding: theme.spacing(2), + }, + tableContainer: { + overflowX: "auto", + }, + create: { + marginTop: theme.spacing(2), + }, + copy: { + marginLeft: 10, + }, +})); + +export default function WebDAV() { + const [tab, setTab] = useState(0); + const [create, setCreate] = useState(false); + const [accounts, setAccounts] = useState([]); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const copyToClipboard = (text) => { + navigator.clipboard.writeText(text); + ToggleSnackbar("top", "center", "已复制到剪切板", "success"); + }; + + const loadList = () => { + API.get("/webdav/accounts") + .then((response) => { + setAccounts(response.data.accounts); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + useEffect(() => { + loadList(); + // eslint-disable-next-line + }, []); + + const deleteAccount = (id) => { + const account = accounts[id]; + API.delete("/webdav/accounts/" + account.ID) + .then(() => { + let accountCopy = [...accounts]; + accountCopy = accountCopy.filter((v, i) => { + return i !== id; + }); + setAccounts(accountCopy); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + + const addAccount = (account) => { + setCreate(false); + API.post("/webdav/accounts", { + path: account.path, + name: account.name, + }) + .then((response) => { + setAccounts([ + { + ID: response.data.id, + Password: response.data.password, + CreatedAt: response.data.created_at, + Name: account.name, + Root: account.path, + }, + ...accounts, + ]); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + + const classes = useStyles(); + const user = Auth.GetUser(); + + return ( +
+ setCreate(false)} + /> + + WebDAV + + + setTab(newValue)} + aria-label="disabled tabs example" + > + + +
+ {tab === 0 && ( +
+ + WebDAV的地址为: + {window.location.origin + "/dav"} + ;登陆用户名统一为:{user.user_name}{" "} + ;密码为所创建账号的密码。 + + + + + + 备注名 + 密码 + + 根目录 + + + 创建日期 + + + 操作 + + + + + {accounts.map((row, id) => ( + + + {row.Name} + + + {row.Password} + + copyToClipboard( + row.Password + ) + } + href={"javascript:void"} + > + 复制 + + + + {row.Root} + + + + + + + deleteAccount(id) + } + > + + + + + ))} + +
+
+ +
+ )} +
+
+
+ ); +} diff --git a/assets/src/component/Share/Creator.js b/assets/src/component/Share/Creator.js new file mode 100644 index 00000000..951396ed --- /dev/null +++ b/assets/src/component/Share/Creator.js @@ -0,0 +1,92 @@ +import React from "react"; +import { makeStyles } from "@material-ui/core/styles"; +import { Avatar, Typography } from "@material-ui/core"; +import { useHistory } from "react-router"; +import Link from "@material-ui/core/Link"; +import { formatLocalTime } from "../../utils/datetime"; + +const useStyles = makeStyles((theme) => ({ + boxHeader: { + textAlign: "center", + padding: 24, + }, + avatar: { + backgroundColor: theme.palette.secondary.main, + margin: "0 auto", + width: 50, + height: 50, + cursor: "pointer", + }, + shareDes: { + marginTop: 12, + }, + shareInfo: { + color: theme.palette.text.disabled, + fontSize: 14, + }, +})); + +export default function Creator(props) { + const classes = useStyles(); + const history = useHistory(); + + const getSecondDes = () => { + if (props.share.expire > 0) { + if (props.share.expire >= 24 * 3600) { + return ( + Math.round(props.share.expire / (24 * 3600)) + " 天后到期" + ); + } + return Math.round(props.share.expire / 3600) + " 小时后到期"; + } + return formatLocalTime(props.share.create_date, "YYYY-MM-DD H:mm:ss"); + }; + + const userProfile = () => { + history.push("/profile/" + props.share.creator.key); + props.onClose && props.onClose(); + }; + + return ( +
+ userProfile()} + /> + + {props.isFolder && ( + <> + 此分享由{" "} + userProfile()} + href={"javascript:void(0)"} + color="inherit" + > + {props.share.creator.nick} + {" "} + 创建 + + )} + {!props.isFolder && ( + <> + {" "} + userProfile()} + href={"javascript:void(0)"} + color="inherit" + > + {props.share.creator.nick} + {" "} + 向您分享了 1 个文件 + + )} + + + {props.share.views} 次浏览 • {props.share.downloads} 次下载 •{" "} + {getSecondDes()} + +
+ ); +} diff --git a/assets/src/component/Share/LockedFile.js b/assets/src/component/Share/LockedFile.js new file mode 100644 index 00000000..c681ce3e --- /dev/null +++ b/assets/src/component/Share/LockedFile.js @@ -0,0 +1,139 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { toggleSnackbar } from "../../actions"; + +import { + withStyles, + Button, + Card, + Divider, + CardHeader, + CardContent, + CardActions, + TextField, + Avatar, +} from "@material-ui/core"; +import { withRouter } from "react-router"; + +const styles = (theme) => ({ + card: { + maxWidth: 400, + margin: "0 auto", + }, + actions: { + display: "flex", + }, + layout: { + width: "auto", + marginTop: "110px", + marginLeft: theme.spacing(3), + marginRight: theme.spacing(3), + [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { + width: 1100, + marginLeft: "auto", + marginRight: "auto", + }, + }, + continue: { + marginLeft: "auto", + marginRight: "10px", + marginRottom: "10px", + }, +}); +const mapStateToProps = () => { + return {}; +}; + +const mapDispatchToProps = (dispatch) => { + return { + toggleSnackbar: (vertical, horizontal, msg, color) => { + dispatch(toggleSnackbar(vertical, horizontal, msg, color)); + }, + }; +}; + +class LockedFileCompoment extends Component { + constructor(props) { + super(props); + const query = new URLSearchParams(this.props.location.search); + this.state = { + pwd: query.get("password"), + }; + } + + handleChange = (name) => (event) => { + this.setState({ [name]: event.target.value }); + }; + + submit = (e) => { + e.preventDefault(); + if (this.state.pwd === "") { + return; + } + this.props.setPassowrd(this.state.pwd); + }; + + render() { + const { classes } = this.props; + + return ( +
+ + + } + title={this.props.share.creator.nick + " 的加密分享"} + subheader={this.props.share.create_date} + /> + + +
+ + +
+ + + +
+
+ ); + } +} + +const LockedFile = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(withRouter(LockedFileCompoment))); + +export default LockedFile; diff --git a/assets/src/component/Share/MyShare.js b/assets/src/component/Share/MyShare.js new file mode 100644 index 00000000..7587d676 --- /dev/null +++ b/assets/src/component/Share/MyShare.js @@ -0,0 +1,510 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { toggleSnackbar } from "../../actions"; +import OpenIcon from "@material-ui/icons/OpenInNew"; +import Pagination from "@material-ui/lab/Pagination"; +import FolderIcon from "@material-ui/icons/Folder"; +import LockIcon from "@material-ui/icons/Lock"; +import UnlockIcon from "@material-ui/icons/LockOpen"; +import EyeIcon from "@material-ui/icons/RemoveRedEye"; +import DeleteIcon from "@material-ui/icons/Delete"; + +import { + withStyles, + Tooltip, + Card, + Avatar, + CardHeader, + CardActions, + Typography, + Grid, + IconButton, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Button, + TextField, +} from "@material-ui/core"; +import API from "../../middleware/Api"; +import TypeIcon from "../FileManager/TypeIcon"; +import Chip from "@material-ui/core/Chip"; +import Divider from "@material-ui/core/Divider"; +import { VisibilityOff, VpnKey } from "@material-ui/icons"; +import Select from "@material-ui/core/Select"; +import MenuItem from "@material-ui/core/MenuItem"; +import FormControl from "@material-ui/core/FormControl"; +import { withRouter } from "react-router-dom"; +import ToggleIcon from "material-ui-toggle-icon"; +import { formatLocalTime } from "../../utils/datetime"; + +const styles = (theme) => ({ + cardContainer: { + padding: theme.spacing(1), + }, + card: { + maxWidth: 400, + margin: "0 auto", + }, + actions: { + display: "flex", + }, + layout: { + width: "auto", + marginTop: "50px", + marginLeft: theme.spacing(3), + marginRight: theme.spacing(3), + [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { + width: 1100, + marginLeft: "auto", + marginRight: "auto", + }, + }, + shareTitle: { + maxWidth: "200px", + }, + avatarFile: { + backgroundColor: theme.palette.primary.light, + }, + avatarFolder: { + backgroundColor: theme.palette.secondary.light, + }, + gird: { + marginTop: "30px", + }, + loadMore: { + textAlign: "right", + marginTop: "20px", + marginBottom: "40px", + }, + badge: { + marginLeft: theme.spacing(1), + height: 17, + }, + orderSelect: { + textAlign: "right", + marginTop: 5, + }, +}); +const mapStateToProps = () => { + return {}; +}; + +const mapDispatchToProps = (dispatch) => { + return { + toggleSnackbar: (vertical, horizontal, msg, color) => { + dispatch(toggleSnackbar(vertical, horizontal, msg, color)); + }, + }; +}; + +class MyShareCompoment extends Component { + state = { + page: 1, + total: 0, + shareList: [], + showPwd: null, + orderBy: "created_at DESC", + }; + + componentDidMount = () => { + this.loadList(1, this.state.orderBy); + }; + + showPwd = (pwd) => { + this.setState({ showPwd: pwd }); + }; + + handleClose = () => { + this.setState({ showPwd: null }); + }; + + removeShare = (id) => { + API.delete("/share/" + id) + .then(() => { + let oldList = this.state.shareList; + oldList = oldList.filter((value) => { + return value.key !== id; + }); + this.setState({ + shareList: oldList, + total: this.state.total - 1, + }); + this.props.toggleSnackbar( + "top", + "right", + "分享已取消", + "success" + ); + if (oldList.length === 0) { + this.loadList(1, this.state.orderBy); + } + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + }); + }; + + changePermission = (id) => { + const newPwd = Math.random().toString(36).substr(2).slice(2, 8); + const oldList = this.state.shareList; + const shareIndex = oldList.findIndex((value) => { + return value.key === id; + }); + API.patch("/share/" + id, { + prop: "password", + value: oldList[shareIndex].password === "" ? newPwd : "", + }) + .then((response) => { + oldList[shareIndex].password = response.data; + this.setState({ + shareList: oldList, + }); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + }); + }; + + changePreviewOption = (id) => { + const oldList = this.state.shareList; + const shareIndex = oldList.findIndex((value) => { + return value.key === id; + }); + API.patch("/share/" + id, { + prop: "preview_enabled", + value: oldList[shareIndex].preview ? "false" : "true", + }) + .then((response) => { + oldList[shareIndex].preview = response.data; + this.setState({ + shareList: oldList, + }); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "error" + ); + }); + }; + + loadList = (page, orderBy) => { + const order = orderBy.split(" "); + API.get( + "/share?page=" + + page + + "&order_by=" + + order[0] + + "&order=" + + order[1] + ) + .then((response) => { + if (response.data.items.length === 0) { + this.props.toggleSnackbar( + "top", + "right", + "没有更多了", + "info" + ); + } + this.setState({ + total: response.data.total, + shareList: response.data.items, + }); + }) + .catch(() => { + this.props.toggleSnackbar("top", "right", "加载失败", "error"); + }); + }; + + handlePageChange = (event, value) => { + this.setState({ + page: value, + }); + this.loadList(value, this.state.orderBy); + }; + + handleOrderChange = (event) => { + this.setState({ + orderBy: event.target.value, + }); + this.loadList(this.state.page, event.target.value); + }; + + isExpired = (share) => { + return share.expire < -1 || share.remain_downloads === 0; + }; + + render() { + const { classes } = this.props; + + return ( +
+ + + + 我的分享 + + + + + + + + + + {this.state.shareList.map((value) => ( + + + + {!value.is_dir && ( + + )}{" "} + {value.is_dir && ( + + + + )} +
+ } + title={ + + + {value.source + ? value.source.name + : "[原始对象不存在]"}{" "} + + + } + subheader={ + + {formatLocalTime( + value.create_date, + "YYYY-MM-DD H:mm:ss" + )} + {this.isExpired(value) && ( + + )} + + } + /> + + + + + this.props.history.push( + "/s/" + + value.key + + (value.password === "" + ? "" + : "?password=" + + value.password) + ) + } + > + + + {" "} + {value.password !== "" && ( + <> + + this.changePermission( + value.key + ) + } + > + + + + + + this.showPwd(value.password) + } + > + + + + + + )} + {value.password === "" && ( + + this.changePermission(value.key) + } + > + + + + + )} + + this.changePreviewOption(value.key) + } + > + + + } + offIcon={ + + } + /> + + + + this.removeShare(value.key) + } + > + + + + + + + + ))} + +
+ +
{" "} + + 分享密码 {" "} + + + {" "} + + {" "} + {" "} + {" "} +
+ ); + } +} + +const MyShare = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(withRouter(MyShareCompoment))); + +export default MyShare; diff --git a/assets/src/component/Share/NotFound.js b/assets/src/component/Share/NotFound.js new file mode 100644 index 00000000..0ce52ae7 --- /dev/null +++ b/assets/src/component/Share/NotFound.js @@ -0,0 +1,32 @@ +import React from "react"; +import SentimentVeryDissatisfiedIcon from "@material-ui/icons/SentimentVeryDissatisfied"; +import { lighten, makeStyles } from "@material-ui/core/styles"; + +const useStyles = makeStyles((theme) => ({ + icon: { + fontSize: "160px", + }, + emptyContainer: { + bottom: "0", + height: "300px", + margin: "50px auto", + width: "300px", + color: lighten(theme.palette.text.disabled, 0.4), + textAlign: "center", + paddingTop: "20px", + }, + emptyInfoBig: { + fontSize: "25px", + color: lighten(theme.palette.text.disabled, 0.4), + }, +})); + +export default function Notice(props) { + const classes = useStyles(); + return ( +
+ +
{props.msg}
+
+ ); +} diff --git a/assets/src/component/Share/ReadMe.js b/assets/src/component/Share/ReadMe.js new file mode 100644 index 00000000..bcfee28d --- /dev/null +++ b/assets/src/component/Share/ReadMe.js @@ -0,0 +1,141 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { makeStyles, useTheme } from "@material-ui/core/styles"; +import { MenuBook } from "@material-ui/icons"; +import { Typography } from "@material-ui/core"; +import Divider from "@material-ui/core/Divider"; +import Paper from "@material-ui/core/Paper"; +import TextLoading from "../Placeholder/TextLoading"; +import API from "../../middleware/Api"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../actions"; +import Editor from "for-editor"; + +const useStyles = makeStyles((theme) => ({ + readMeContainer: { + marginTop: 30, + [theme.breakpoints.down("sm")]: { + marginTop: theme.spacing(2), + }, + }, + readMeHeader: { + padding: "10px 16px", + display: "flex", + color: theme.palette.text.secondary, + }, + readMeIcon: { + marginRight: 8, + }, + content: {}, + "@global": { + //如果嵌套主题,则应该定位[class * =“MuiButton-root”]。 + ".for-container": { + border: "none!important", + }, + ".for-container .for-editor .for-editor-edit": { + height: "0!important", + }, + ".for-container > div:first-child": { + borderTopLeftRadius: "0!important", + borderTopRightRadius: "0!important", + }, + ".for-container .for-editor .for-panel .for-preview": { + backgroundColor: theme.palette.background.paper + "!important", + color: theme.palette.text.primary + "!important", + }, + ".for-container .for-markdown-preview pre": { + backgroundColor: theme.palette.background.default + "!important", + color: + theme.palette.type === "dark" + ? "#fff !important" + : "rgba(0, 0, 0, 0.87);!important", + }, + + ".for-container .for-markdown-preview code": { + backgroundColor: theme.palette.background.default + "!important", + }, + ".for-container .for-markdown-preview a": { + color: + theme.palette.type === "dark" + ? "#67aeff !important" + : "#0366d6 !important", + }, + ".for-container .for-markdown-preview table th": { + backgroundColor: theme.palette.background.default + "!important", + }, + }, +})); + +export default function ReadMe(props) { + const classes = useStyles(); + const theme = useTheme(); + + const [loading, setLoading] = useState(true); + const [content, setContent] = useState(""); + + const dispatch = useDispatch(); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const $vm = React.createRef(); + + useEffect(() => { + setLoading(true); + const previewPath = + props.file.path === "/" + ? props.file.path + props.file.name + : props.file.path + "/" + props.file.name; + API.get( + "/share/readme/" + + props.share.key + + "?path=" + + encodeURIComponent(previewPath) + ) + .then((response) => { + setContent(response.rawData.toString()); + }) + .catch((error) => { + ToggleSnackbar( + "top", + "right", + "无法读取 README 内容," + error.message, + "error" + ); + }) + .then(() => { + setLoading(false); + }); + // eslint-disable-next-line + }, [props.share, props.file]); + + return ( + +
+ + {props.file.name} +
+ + +
+ {loading && } + {!loading && ( + setContent(value)} + preview + toolbar={{}} + /> + )} +
+
+ ); +} diff --git a/assets/src/component/Share/SearchResult.js b/assets/src/component/Share/SearchResult.js new file mode 100644 index 00000000..fa0f1bcd --- /dev/null +++ b/assets/src/component/Share/SearchResult.js @@ -0,0 +1,271 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../actions"; +import OpenIcon from "@material-ui/icons/OpenInNew"; +import Pagination from "@material-ui/lab/Pagination"; +import FolderIcon from "@material-ui/icons/Folder"; + +import { + Tooltip, + Card, + Avatar, + CardHeader, + Typography, + Grid, + IconButton, +} from "@material-ui/core"; +import API from "../../middleware/Api"; +import TypeIcon from "../FileManager/TypeIcon"; +import Select from "@material-ui/core/Select"; +import MenuItem from "@material-ui/core/MenuItem"; +import FormControl from "@material-ui/core/FormControl"; +import { useHistory } from "react-router-dom"; +import { makeStyles } from "@material-ui/core/styles"; +import { useLocation } from "react-router"; +import TimeAgo from "timeago-react"; + +const useStyles = makeStyles((theme) => ({ + cardContainer: { + padding: theme.spacing(1), + }, + card: { + maxWidth: 400, + margin: "0 auto", + }, + actions: { + display: "flex", + }, + layout: { + width: "auto", + marginTop: "50px", + marginLeft: theme.spacing(3), + marginRight: theme.spacing(3), + [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { + width: 1100, + marginLeft: "auto", + marginRight: "auto", + }, + }, + shareTitle: { + maxWidth: "200px", + }, + avatarFile: { + backgroundColor: theme.palette.primary.light, + }, + avatarFolder: { + backgroundColor: theme.palette.secondary.light, + }, + gird: { + marginTop: "30px", + }, + loadMore: { + textAlign: "right", + marginTop: "20px", + marginBottom: "40px", + }, + badge: { + marginLeft: theme.spacing(1), + height: 17, + }, + orderSelect: { + textAlign: "right", + marginTop: 5, + }, +})); + +function useQuery() { + return new URLSearchParams(useLocation().search); +} + +export default function SearchResult() { + const classes = useStyles(); + const dispatch = useDispatch(); + + const query = useQuery(); + const location = useLocation(); + const history = useHistory(); + + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + const [page, setPage] = useState(1); + const [total, setTotal] = useState(0); + const [shareList, setShareList] = useState([]); + const [orderBy, setOrderBy] = useState("created_at DESC"); + + const search = useCallback((keywords, page, orderBy) => { + const order = orderBy.split(" "); + API.get( + "/share/search?page=" + + page + + "&order_by=" + + order[0] + + "&order=" + + order[1] + + "&keywords=" + + encodeURIComponent(keywords) + ) + .then((response) => { + if (response.data.items.length === 0) { + ToggleSnackbar( + "top", + "right", + "找不到符合条件的分享", + "info" + ); + } + setTotal(response.data.total); + setShareList(response.data.items); + }) + .catch(() => { + ToggleSnackbar("top", "right", "加载失败", "error"); + }); + }, []); + + useEffect(() => { + const keywords = query.get("keywords"); + if (keywords) { + search(keywords, page, orderBy); + } else { + ToggleSnackbar("top", "right", "请输入搜索关键词", "warning"); + } + }, [location]); + + const handlePageChange = (event, value) => { + setPage(value); + const keywords = query.get("keywords"); + search(keywords, value, orderBy); + }; + + const handleOrderChange = (event) => { + setOrderBy(event.target.value); + const keywords = query.get("keywords"); + search(keywords, page, event.target.value); + }; + + return ( +
+ + + + 搜索结果 + + + + + + + + + + {shareList.map((value) => ( + + + + {!value.is_dir && ( + + )}{" "} + {value.is_dir && ( + + + + )} +
+ } + action={ + + + history.push("/s/" + value.key) + } + > + + + + } + title={ + + + {value.source + ? value.source.name + : "[原始对象不存在]"}{" "} + + + } + subheader={ + + 分享于{" "} + + + } + /> + + + ))} + +
+ +
{" "} +
+ ); +} diff --git a/assets/src/component/Share/SharePreload.js b/assets/src/component/Share/SharePreload.js new file mode 100644 index 00000000..bf602169 --- /dev/null +++ b/assets/src/component/Share/SharePreload.js @@ -0,0 +1,94 @@ +import React, { Suspense, useCallback, useEffect, useState } from "react"; +import PageLoading from "../Placeholder/PageLoading"; +import { useParams } from "react-router"; +import API from "../../middleware/Api"; +import { toggleSnackbar } from "../../actions"; +import { changeSubTitle } from "../../redux/viewUpdate/action"; +import { useDispatch } from "react-redux"; +import Notice from "./NotFound"; +import LockedFile from "./LockedFile"; +import SharedFile from "./SharedFile"; +import SharedFolder from "./SharedFolder"; + +export default function SharePreload() { + const dispatch = useDispatch(); + const { id } = useParams(); + + const [share, setShare] = useState(undefined); + const [loading, setLoading] = useState(false); + const [password, setPassword] = useState(""); + + const SetSubTitle = useCallback( + (title) => dispatch(changeSubTitle(title)), + [dispatch] + ); + + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + useEffect(() => { + if (share) { + if (share.locked) { + SetSubTitle(share.creator.nick + "的加密分享"); + if (password !== "") { + ToggleSnackbar("top", "right", "密码不正确", "warning"); + } + } else { + SetSubTitle(share.source.name); + } + } else { + SetSubTitle(); + } + }, [share, SetSubTitle, ToggleSnackbar]); + + useEffect(() => { + return () => { + SetSubTitle(); + }; + // eslint-disable-next-line + }, []); + + useEffect(() => { + setLoading(true); + let withPassword = ""; + if (password !== "") { + withPassword = "?password=" + password; + } + API.get("/share/info/" + id + withPassword) + .then((response) => { + setShare(response.data); + setLoading(false); + }) + .catch((error) => { + setLoading(false); + if (error.code === 404) { + setShare(null); + } else { + ToggleSnackbar("top", "right", error.message, "error"); + } + }); + }, [id, password, ToggleSnackbar]); + + return ( + }> + {share === undefined && } + {share === null && } + {share && share.locked && ( + + )} + {share && !share.locked && !share.is_dir && ( + + )} + {share && !share.locked && share.is_dir && ( + + )} + + ); +} diff --git a/assets/src/component/Share/SharedFile.js b/assets/src/component/Share/SharedFile.js new file mode 100644 index 00000000..b2ac118b --- /dev/null +++ b/assets/src/component/Share/SharedFile.js @@ -0,0 +1,301 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { sizeToString, vhCheck } from "../../utils"; +import { + openMusicDialog, + openResaveDialog, + setSelectedTarget, + showImgPreivew, + toggleSnackbar, +} from "../../actions"; +import { isPreviewable } from "../../config"; +import { withStyles, Button, Typography } from "@material-ui/core"; +import Divider from "@material-ui/core/Divider"; +import TypeIcon from "../FileManager/TypeIcon"; +import Auth from "../../middleware/Auth"; +import API from "../../middleware/Api"; +import { withRouter } from "react-router-dom"; +import Creator from "./Creator"; +import pathHelper from "../../utils/page"; + +vhCheck(); +const styles = (theme) => ({ + layout: { + width: "auto", + marginTop: "90px", + marginLeft: theme.spacing(3), + marginRight: theme.spacing(3), + [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { + width: 1100, + marginTop: "90px", + marginLeft: "auto", + marginRight: "auto", + }, + [theme.breakpoints.down("sm")]: { + marginTop: 0, + marginLeft: 0, + marginRight: 0, + }, + justifyContent: "center", + display: "flex", + }, + player: { + borderRadius: "4px", + }, + fileCotainer: { + width: "200px", + margin: "0 auto", + }, + buttonCotainer: { + width: "400px", + margin: "0 auto", + textAlign: "center", + marginTop: "20px", + }, + paper: { + padding: theme.spacing(2), + }, + icon: { + borderRadius: "10%", + marginTop: 2, + }, + + box: { + width: "100%", + maxWidth: 440, + backgroundColor: theme.palette.background.paper, + borderRadius: 12, + boxShadow: "0 8px 16px rgba(29,39,55,.25)", + [theme.breakpoints.down("sm")]: { + height: "calc(var(--vh, 100vh) - 56px)", + borderRadius: 0, + maxWidth: 1000, + }, + display: "flex", + flexDirection: "column", + }, + boxContent: { + padding: 24, + display: "flex", + flex: "1", + }, + fileName: { + marginLeft: 20, + }, + fileSize: { + color: theme.palette.text.disabled, + fontSize: 14, + }, + boxFooter: { + display: "flex", + padding: "20px 16px", + justifyContent: "space-between", + }, + downloadButton: { + marginLeft: 8, + }, +}); +const mapStateToProps = () => { + return {}; +}; + +const mapDispatchToProps = (dispatch) => { + return { + toggleSnackbar: (vertical, horizontal, msg, color) => { + dispatch(toggleSnackbar(vertical, horizontal, msg, color)); + }, + openMusicDialog: () => { + dispatch(openMusicDialog()); + }, + setSelectedTarget: (targets) => { + dispatch(setSelectedTarget(targets)); + }, + showImgPreivew: (first) => { + dispatch(showImgPreivew(first)); + }, + openResave: (key) => { + dispatch(openResaveDialog(key)); + }, + }; +}; + +const Modals = React.lazy(() => import("../FileManager/Modals")); +const ImgPreview = React.lazy(() => import("../FileManager/ImgPreview")); + +class SharedFileCompoment extends Component { + state = { + anchorEl: null, + open: false, + purchaseCallback: null, + loading: false, + }; + + downloaded = false; + + // TODO merge into react thunk + preview = () => { + if (pathHelper.isSharePage(this.props.location.pathname)) { + const user = Auth.GetUser(); + if (!Auth.Check() && user && !user.group.shareDownload) { + this.props.toggleSnackbar( + "top", + "right", + "请先登录", + "warning" + ); + return; + } + } + + switch (isPreviewable(this.props.share.source.name)) { + case "img": + this.props.showImgPreivew({ + key: this.props.share.key, + name: this.props.share.source.name, + }); + return; + case "msDoc": + this.props.history.push( + this.props.share.key + + "/doc?name=" + + encodeURIComponent(this.props.share.source.name) + ); + return; + case "audio": + this.props.setSelectedTarget([ + { + key: this.props.share.key, + type: "share", + }, + ]); + this.props.openMusicDialog(); + return; + case "video": + this.props.history.push( + this.props.share.key + + "/video?name=" + + encodeURIComponent(this.props.share.source.name) + ); + return; + case "edit": + this.props.history.push( + this.props.share.key + + "/text?name=" + + encodeURIComponent(this.props.share.source.name) + ); + return; + case "pdf": + this.props.history.push( + this.props.share.key + + "/pdf?name=" + + encodeURIComponent(this.props.share.source.name) + ); + return; + case "code": + this.props.history.push( + this.props.share.key + + "/code?name=" + + encodeURIComponent(this.props.share.source.name) + ); + return; + default: + this.props.toggleSnackbar( + "top", + "right", + "此文件无法预览", + "warning" + ); + return; + } + }; + + componentWillUnmount() { + this.props.setSelectedTarget([]); + } + + scoreHandle = (callback) => (event) => { + callback(event); + }; + + download = () => { + this.setState({ loading: true }); + API.put("/share/download/" + this.props.share.key) + .then((response) => { + this.downloaded = true; + window.location.assign(response.data); + }) + .catch((error) => { + this.props.toggleSnackbar( + "top", + "right", + error.message, + "warning" + ); + }) + .then(() => { + this.setState({ loading: false }); + }); + }; + + render() { + const { classes } = this.props; + return ( +
+ + +
+ + +
+ +
+ + {this.props.share.source.name} + + + {sizeToString(this.props.share.source.size)} + +
+
+ +
+
+ {this.props.share.preview && ( + + )} +
+
+ +
+
+
+
+ ); + } +} + +const SharedFile = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(withRouter(SharedFileCompoment))); + +export default SharedFile; diff --git a/assets/src/component/Share/SharedFolder.js b/assets/src/component/Share/SharedFolder.js new file mode 100644 index 00000000..98c94991 --- /dev/null +++ b/assets/src/component/Share/SharedFolder.js @@ -0,0 +1,147 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { + openMusicDialog, + openResaveDialog, + setSelectedTarget, + setShareUserPopover, + showImgPreivew, + toggleSnackbar, +} from "../../actions"; +import { withStyles, Typography } from "@material-ui/core"; +import { withRouter } from "react-router-dom"; +import FileManager from "../FileManager/FileManager"; +import Paper from "@material-ui/core/Paper"; +import Popover from "@material-ui/core/Popover"; +import Creator from "./Creator"; +import ClickAwayListener from "@material-ui/core/ClickAwayListener"; +import pathHelper from "../../utils/page"; +const styles = (theme) => ({ + layout: { + width: "auto", + marginTop: 30, + marginBottom: 30, + marginLeft: theme.spacing(3), + marginRight: theme.spacing(3), + [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { + width: 1100, + marginLeft: "auto", + marginRight: "auto", + }, + [theme.breakpoints.down("sm")]: { + marginTop: theme.spacing(2), + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + }, + }, + managerContainer: { + overflowY: "auto", + }, +}); + +const ReadMe = React.lazy(() => import("./ReadMe")); + +const mapStateToProps = (state) => { + return { + anchorEl: state.viewUpdate.shareUserPopoverAnchorEl, + fileList: state.explorer.fileList, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + toggleSnackbar: (vertical, horizontal, msg, color) => { + dispatch(toggleSnackbar(vertical, horizontal, msg, color)); + }, + openMusicDialog: () => { + dispatch(openMusicDialog()); + }, + setSelectedTarget: (targets) => { + dispatch(setSelectedTarget(targets)); + }, + showImgPreivew: (first) => { + dispatch(showImgPreivew(first)); + }, + openResave: (key) => { + dispatch(openResaveDialog(key)); + }, + setShareUserPopover: (e) => { + dispatch(setShareUserPopover(e)); + }, + }; +}; + +class SharedFolderComponent extends Component { + state = {}; + + UNSAFE_componentWillMount() { + window.shareInfo = this.props.share; + } + + componentWillUnmount() { + window.shareInfo = null; + this.props.setSelectedTarget([]); + } + + handleClickAway = () => { + if (!pathHelper.isMobile()) { + this.props.setSelectedTarget([]); + } + }; + + render() { + const { classes } = this.props; + let readmeShowed = false; + const id = this.props.anchorEl !== null ? "simple-popover" : undefined; + + return ( +
+ + + + + + {/* eslint-disable-next-line */} + {this.props.fileList.map((value) => { + if ( + (value.name.toLowerCase() === "readme.md" || + value.name.toLowerCase() === "readme.txt") && + !readmeShowed + ) { + readmeShowed = true; + return ; + } + })} + this.props.setShareUserPopover(null)} + anchorOrigin={{ + vertical: "bottom", + horizontal: "center", + }} + transformOrigin={{ + vertical: "top", + horizontal: "center", + }} + > + + this.props.setShareUserPopover(null)} + share={this.props.share} + /> + + +
+ ); + } +} + +const SharedFolder = connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(withRouter(SharedFolderComponent))); + +export default SharedFolder; diff --git a/assets/src/component/Upload/FileList.js b/assets/src/component/Upload/FileList.js new file mode 100644 index 00000000..2d8477d6 --- /dev/null +++ b/assets/src/component/Upload/FileList.js @@ -0,0 +1,392 @@ +import React, { Component } from "react"; +import CloseIcon from "@material-ui/icons/Close"; +import AddIcon from "@material-ui/icons/AddCircleOutline"; +import DeleteIcon from "@material-ui/icons/Delete"; +import RefreshIcon from "@material-ui/icons/Refresh"; +import { isWidthDown } from "@material-ui/core/withWidth"; +import { darken, lighten } from "@material-ui/core/styles/colorManipulator"; +import { + withStyles, + Dialog, + ListItemText, + ListItem, + List, + Divider, + AppBar, + Toolbar, + IconButton, + Typography, + Slide, + ListItemSecondaryAction, + withWidth, + DialogContent, + Tooltip, +} from "@material-ui/core"; +import TypeIcon from "../FileManager/TypeIcon"; +import { withTheme } from "@material-ui/core/styles"; + +const styles = (theme) => ({ + appBar: { + position: "relative", + }, + flex: { + flex: 1, + }, + progressBar: { + marginTop: 5, + }, + minHight: { + [theme.breakpoints.up("sm")]: { + minWidth: 500, + }, + padding: 0, + }, + dialogContent: { + padding: 0, + }, + successStatus: { + color: "#4caf50", + }, + errorStatus: { + color: "#ff5722", + wordBreak: "break-all", + }, + listAction: { + marginLeft: 20, + marginRight: 20, + }, + delete: { + zIndex: 9, + }, + progressContainer: { + position: "relative", + }, + progressContent: { + position: "relative", + zIndex: 9, + }, + progress: { + transition: "width .4s linear", + zIndex: 1, + height: "100%", + position: "absolute", + left: 0, + top: 0, + }, + fileName: { + wordBreak: "break-all", + }, +}); +class FileList extends Component { + state = { + open: false, + files: [], + }; + + //入队 + enQueue(files) { + this.setState({ + files: [...this.state.files, ...files], + }); + } + + deQueue(file) { + const filesNow = [...this.state.files]; + const fileID = filesNow.findIndex((f) => { + return f.id === file.id; + }); + if (fileID !== -1) { + filesNow.splice(fileID, 1); + this.setState({ + files: filesNow, + open: filesNow.length !== 0, + }); + } + } + + updateStatus(file) { + const filesNow = [...this.state.files]; + const fileID = filesNow.findIndex((f) => { + return f.id === file.id; + }); + if (!file.errMsg || file.ignoreMsg) { + if (filesNow[fileID] && !filesNow[fileID].errMsg) { + filesNow[fileID] = file; + this.setState({ + files: filesNow, + }); + } + } else { + file.ignoreMsg = true; + } + } + + setComplete(file) { + const filesNow = [...this.state.files]; + const fileID = filesNow.findIndex((f) => { + return f.id === file.id; + }); + if (fileID !== -1) { + if (filesNow[fileID].status !== 4) { + filesNow[fileID].status = 5; + this.setState({ + files: filesNow, + }); + } + } + } + + setError(file, errMsg) { + const filesNow = [...this.state.files]; + const fileID = filesNow.findIndex((f) => { + return f.id === file.id; + }); + if (fileID !== -1) { + filesNow[fileID].status = 4; + filesNow[fileID].errMsg = errMsg; + } else { + file.status = 4; + file.errMsg = errMsg; + filesNow.push(file); + } + this.setState({ + files: filesNow, + }); + } + + Transition(props) { + return ; + } + openFileList = () => { + if (!this.state.open) { + this.setState({ open: true }); + } + }; + + cancelUpload = (file) => { + this.props.cancelUpload(file); + // this.deQueue(file); + }; + + handleClose = () => { + this.setState({ open: false }); + }; + + addNewFile = () => { + document.getElementsByClassName("uploadFileForm")[0].click(); + }; + + getProgressBackground = () => { + return this.props.theme.palette.type === "light" + ? lighten(this.props.theme.palette.primary.main, 0.8) + : darken(this.props.theme.palette.background.paper, 0.2); + }; + + render() { + const { classes } = this.props; + const { width } = this.props; + + this.props.inRef({ + openFileList: this.openFileList.bind(this), + enQueue: this.enQueue.bind(this), + updateStatus: this.updateStatus.bind(this), + setComplete: this.setComplete.bind(this), + setError: this.setError.bind(this), + }); + + return ( + + + + + + + + 上传队列 + + + + + + + + + {this.state.files.map((item, i) => ( +
+ {item.status === 2 && ( +
+ )} + + + {item.status === 1 && ( + + {item.name} + + } + secondary={
排队中...
} + /> + )} + {item.status === 2 && ( + + {item.name} + + } + secondary={ +
+ {item.percent <= 99 && ( + <> + {window.plupload + .formatSize( + item.speed + ) + .toUpperCase()} + /s 已上传{" "} + {window.plupload + .formatSize( + item.loaded + ) + .toUpperCase()}{" "} + , 共{" "} + {window.plupload + .formatSize( + item.size + ) + .toUpperCase()}{" "} + - {item.percent}%{" "} + + )} + {item.percent > 99 && ( +
处理中...
+ )} +
+ } + /> + )} + {item.status === 3 && ( + + {item.name} + + } + secondary={item.status} + /> + )} + {item.status === 4 && ( + + {item.name} + + } + secondary={ +
+ {item.errMsg} +
+
+ } + /> + )} + {item.status === 5 && ( + + {item.name} + + } + secondary={ +
+ 已完成 +
+
+ } + /> + )} + + {item.status !== 4 && ( + + this.cancelUpload(item) + } + > + + + )} + {item.status === 4 && ( + + + this.reQueue(item) + } + > + + + + )} + +
+ +
+ ))} + + +
+ ); + } +} +FileList.propTypes = {}; + +export default withStyles(styles)(withWidth()(withTheme(FileList))); diff --git a/assets/src/component/Upload/UploadIcon.js b/assets/src/component/Upload/UploadIcon.js new file mode 100644 index 00000000..ab078b4c --- /dev/null +++ b/assets/src/component/Upload/UploadIcon.js @@ -0,0 +1 @@ +// import React from 'react' diff --git a/assets/src/component/Upload/Uploader.js b/assets/src/component/Upload/Uploader.js new file mode 100644 index 00000000..4f9dc1ec --- /dev/null +++ b/assets/src/component/Upload/Uploader.js @@ -0,0 +1,234 @@ +import React, { Component } from "react"; +import uploaderLoader from "../../loader"; +import { connect } from "react-redux"; +import { refreshFileList, refreshStorage, toggleSnackbar } from "../../actions"; +import FileList from "./FileList.js"; +import Auth from "../../middleware/Auth"; +import UploadButton from "../Dial/Create.js"; +import { basename, pathJoin } from "../../utils"; + +let loaded = false; + +const mapStateToProps = (state) => { + return { + path: state.navigator.path, + keywords: state.explorer.keywords, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + refreshFileList: () => { + dispatch(refreshFileList()); + }, + refreshStorage: () => { + dispatch(refreshStorage()); + }, + toggleSnackbar: (vertical, horizontal, msg, color) => { + dispatch(toggleSnackbar(vertical, horizontal, msg, color)); + }, + }; +}; + +class UploaderComponent extends Component { + constructor(props) { + super(props); + this.state = { + queued: 0, + }; + } + + setRef(val) { + window.fileList = val; + } + + cancelUpload(file) { + this.uploader.removeFile(file); + } + + reQueue = (file) => { + this.uploader.addFile(file.getSource()); + this.props.toggleSnackbar( + "top", + "right", + "文件已经重新加入上传队列", + "info" + ); + }; + + getChunkSize(policyType) { + if (policyType === "qiniu") { + return 4 * 1024 * 1024; + } + if (policyType === "onedrive") { + return 100 * 1024 * 1024; + } + return 0; + } + + fileAdd = (up, files) => { + const path = window.currntPath ? window.currntPath : this.props.path; + if ( + this.props.keywords === "" && + window.location.pathname.toLowerCase().startsWith("/home") + ) { + window.fileList["openFileList"](); + const enqueFiles = files + // 不上传Mac下的布局文件 .DS_Store + .filter((file) => { + const isDsStore = file.name.toLowerCase() === ".ds_store"; + if (isDsStore) { + up.removeFile(file); + } + return !isDsStore; + }) + .map((file) => { + const source = file.getSource(); + if (source.relativePath && source.relativePath !== "") { + file.path = basename( + pathJoin([path, source.relativePath]) + ); + window.pathCache[file.id] = basename( + pathJoin([path, source.relativePath]) + ); + } else { + window.pathCache[file.id] = path; + file.path = path; + } + return file; + }); + window.fileList["enQueue"](enqueFiles); + } else { + window.plupload.each(files, (files) => { + up.removeFile(files); + }); + } + }; + + UNSAFE_componentWillReceiveProps({ isScriptLoaded, isScriptLoadSucceed }) { + if (isScriptLoaded && !this.props.isScriptLoaded) { + // load finished + if (isScriptLoadSucceed) { + if (loaded) { + return; + } + loaded = true; + const user = Auth.GetUser(); + this.uploader = window.Qiniu.uploader({ + runtimes: "html5", + browse_button: ["pickfiles", "pickfolder"], + container: "container", + drop_element: "container", + max_file_size: + user.policy.maxSize === "0.00mb" + ? 0 + : user.policy.maxSize, + dragdrop: true, + chunk_size: this.getChunkSize(user.policy.saveType), + filters: { + mime_types: + user.policy.allowedType === null || + user.policy.allowedType.length === 0 + ? [] + : [ + { + title: "files", + extensions: user.policy.allowedType.join( + "," + ), + }, + ], + }, + // iOS不能多选? + multi_selection: true, + uptoken_url: "/api/v3/file/upload/credential", + uptoken: user.policy.saveType === "local" ? "token" : null, + domain: "s", + max_retries: 0, + get_new_uptoken: true, + auto_start: true, + log_level: 5, + init: { + FilesAdded: this.fileAdd, + + // eslint-disable-next-line @typescript-eslint/no-empty-function + BeforeUpload: function () {}, + QueueChanged: (up) => { + this.setState({ queued: up.total.queued }); + }, + UploadProgress: (up, file) => { + window.fileList["updateStatus"](file); + }, + UploadComplete: (up, file) => { + if (file.length === 0) { + return; + } + console.log( + "UploadComplete", + file[0].status, + file[0] + ); + for (let i = 0; i < file.length; i++) { + if (file[i].status === 5) { + window.fileList["setComplete"](file[i]); + } + } + // 无异步操作的策略,直接刷新 + if ( + user.policy.saveType !== "onedrive" && + user.policy.saveType !== "cos" + ) { + this.props.refreshFileList(); + this.props.refreshStorage(); + } + }, + Fresh: () => { + this.props.refreshFileList(); + this.props.refreshStorage(); + }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + FileUploaded: function () {}, + Error: (up, err, errTip) => { + window.fileList["openFileList"](); + window.fileList["setError"](err.file, errTip); + }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + FilesRemoved: () => {}, + }, + }); + // this.fileList["openFileList"](); + } else this.onError(); + } + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + onError() {} + + openFileList = () => { + window.fileList["openFileList"](); + }; + + render() { + return ( +
+ + {this.props.keywords === "" && ( + + )} +
+ ); + } +} + +const Uploader = connect(mapStateToProps, mapDispatchToProps, null, { + forwardRef: true, +})(uploaderLoader()(UploaderComponent)); + +export default Uploader; diff --git a/assets/src/component/Viewer/Code.js b/assets/src/component/Viewer/Code.js new file mode 100644 index 00000000..66e2588d --- /dev/null +++ b/assets/src/component/Viewer/Code.js @@ -0,0 +1,185 @@ +import React, { Suspense, useCallback, useEffect, useState } from "react"; +import { Paper, useTheme } from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; +import { useLocation, useParams, useRouteMatch } from "react-router"; +import API from "../../middleware/Api"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../actions"; +import { changeSubTitle } from "../../redux/viewUpdate/action"; +import pathHelper from "../../utils/page"; +import SaveButton from "../Dial/Save"; +import { codePreviewSuffix } from "../../config"; +import TextLoading from "../Placeholder/TextLoading"; +import FormControl from "@material-ui/core/FormControl"; +import Select from "@material-ui/core/Select"; +import MenuItem from "@material-ui/core/MenuItem"; +import Divider from "@material-ui/core/Divider"; +const MonacoEditor = React.lazy(() => + import(/* webpackChunkName: "codeEditor" */ "react-monaco-editor") +); + +const useStyles = makeStyles((theme) => ({ + layout: { + width: "auto", + marginTop: "30px", + marginLeft: theme.spacing(3), + marginRight: theme.spacing(3), + [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { + width: 1100, + marginLeft: "auto", + marginRight: "auto", + }, + marginBottom: 50, + }, + editor: { + borderRadius: "4px", + }, + "@global": { + ".overflow-guard": { + borderRadius: "4px!important", + }, + }, + formControl: { + margin: "8px 16px 8px 16px", + }, + toobar: { + textAlign: "right", + }, +})); + +function useQuery() { + return new URLSearchParams(useLocation().search); +} + +export default function CodeViewer() { + const [content, setContent] = useState(""); + const [status, setStatus] = useState(""); + const [loading, setLoading] = useState(true); + const [suffix, setSuffix] = useState("javascript"); + + const math = useRouteMatch(); + const location = useLocation(); + const query = useQuery(); + const { id } = useParams(); + const theme = useTheme(); + + const dispatch = useDispatch(); + const SetSubTitle = useCallback( + (title) => dispatch(changeSubTitle(title)), + [dispatch] + ); + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + useEffect(() => { + if (!pathHelper.isSharePage(location.pathname)) { + const path = query.get("p").split("/"); + const extension = query.get("p").split("."); + setSuffix(codePreviewSuffix[extension.pop()]); + SetSubTitle(path[path.length - 1]); + } else { + const extension = query.get("name").split("."); + setSuffix(codePreviewSuffix[extension.pop()]); + SetSubTitle(query.get("name")); + } + // eslint-disable-next-line + }, [math.params[0], location]); + + useEffect(() => { + let requestURL = "/file/content/" + query.get("id"); + if (pathHelper.isSharePage(location.pathname)) { + requestURL = "/share/content/" + id; + if (query.get("share_path") !== "") { + requestURL += + "?path=" + encodeURIComponent(query.get("share_path")); + } + } + + setLoading(true); + API.get(requestURL, { responseType: "arraybuffer" }) + .then((response) => { + const buffer = new Buffer(response.rawData, "binary"); + const textdata = buffer.toString(); // for string + setContent(textdata); + }) + .catch((error) => { + ToggleSnackbar( + "top", + "right", + "无法读取文件内容," + error.message, + "error" + ); + }) + .then(() => { + setLoading(false); + }); + // eslint-disable-next-line + }, [math.params[0]]); + + const save = () => { + setStatus("loading"); + API.put("/file/update/" + query.get("id"), content) + .then(() => { + setStatus("success"); + setTimeout(() => setStatus(""), 2000); + }) + .catch((error) => { + setStatus(""); + ToggleSnackbar("top", "right", error.message, "error"); + }); + }; + + const classes = useStyles(); + const isSharePage = pathHelper.isSharePage(location.pathname); + return ( +
+ +
+ + + +
+ + {loading && } + {!loading && ( + }> + setContent(value)} + /> + + )} +
+ {!isSharePage && } +
+ ); +} diff --git a/assets/src/component/Viewer/Doc.js b/assets/src/component/Viewer/Doc.js new file mode 100644 index 00000000..4f3b211e --- /dev/null +++ b/assets/src/component/Viewer/Doc.js @@ -0,0 +1,85 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { makeStyles } from "@material-ui/core/styles"; +import { useLocation, useParams, useRouteMatch } from "react-router"; +import API from "../../middleware/Api"; +import { useDispatch } from "react-redux"; +import { toggleSnackbar } from "../../actions"; +import { changeSubTitle } from "../../redux/viewUpdate/action"; +import pathHelper from "../../utils/page"; + +const useStyles = makeStyles(() => ({ + layout: { + width: "auto", + marginTop: "-48px", + }, + + container: { + border: "none", + width: "100%", + height: "calc(100vh - 18px)", + marginBottom: -3, + }, +})); + +function useQuery() { + return new URLSearchParams(useLocation().search); +} + +export default function DocViewer() { + const [url, setURL] = useState(""); + const math = useRouteMatch(); + const location = useLocation(); + const query = useQuery(); + const { id } = useParams(); + + const dispatch = useDispatch(); + + const SetSubTitle = useCallback( + (title) => dispatch(changeSubTitle(title)), + [dispatch] + ); + + const ToggleSnackbar = useCallback( + (vertical, horizontal, msg, color) => + dispatch(toggleSnackbar(vertical, horizontal, msg, color)), + [dispatch] + ); + + useEffect(() => { + if (!pathHelper.isSharePage(location.pathname)) { + const path = query.get("p").split("/"); + SetSubTitle(path[path.length - 1]); + } else { + SetSubTitle(query.get("name")); + } + // eslint-disable-next-line + }, [math.params[0], location]); + + useEffect(() => { + let requestURL = "/file/doc/" + query.get("id"); + if (pathHelper.isSharePage(location.pathname)) { + requestURL = "/share/doc/" + id; + if (query.get("share_path") !== "") { + requestURL += + "?path=" + encodeURIComponent(query.get("share_path")); + } + } + API.get(requestURL) + .then((response) => { + setURL(response.data); + }) + .catch((error) => { + ToggleSnackbar("top", "right", error.message, "error"); + }); + // eslint-disable-next-line + }, [math.params[0], location]); + + const classes = useStyles(); + return ( +
+ {url !== "" && ( +