- // ==UserScript==
- // @name Discuz论坛头像上传助手
- // @author 枫谷剑仙
- // @description 突破图片尺寸、GIF帧数限制,无损上传
- // @version 2.0.6
- // @namespace http://www.mapaler.com/
- // @include */home.php?mod=spacecp&ac=avatar*
- // @icon https://gitee.com/ComsenzDiscuz/DiscuzX/raw/master/upload/uc_server/images/noavatar_small.gif
- // @grant unsafeWindow
- // @grant GM_xmlhttpRequest
- // ==/UserScript==
-
- (function(){
- 'use strict';
-
- const avatarform = document.querySelector("#avatarform") ||
- document.querySelector("form[action^=home]"); //以前没有HTML5的老版本,没有#avatarform
- if (!avatarform) return;
-
- let noGM_xmlhttpRequest = false;
- //仿GM_xmlhttpRequest函数v1.4
- if (typeof(GM_xmlhttpRequest) == 'undefined' || typeof(GM_info) == 'undefined')
- {
- noGM_xmlhttpRequest = true;
- window.GM_xmlhttpRequest = function(GM_param) {
- const xhr = new XMLHttpRequest(); //创建XMLHttpRequest对象
- xhr.open(GM_param.method, GM_param.url, true);
- if (GM_param.responseType) xhr.responseType = GM_param.responseType;
- if (GM_param.overrideMimeType) xhr.overrideMimeType(GM_param.overrideMimeType);
- xhr.onreadystatechange = function(e) //设置回调函数
- {
- const _xhr = e.target;
- if (_xhr.readyState === _xhr.DONE) { //请求完成时
- if (_xhr.status === 200 && GM_param.onload) //正确加载时
- {
- GM_param.onload(_xhr);
- }
- if (_xhr.status !== 200 && GM_param.onerror) //发生错误时
- {
- GM_param.onerror(_xhr);
- }
- }
- };
- if (GM_param.onprogress)
- xhr.upload.onprogress = function(e){GM_param.onprogress(e.target)};
- //添加header
- for (let header in GM_param.headers) {
- xhr.setRequestHeader(header, GM_param.headers[header]);
- }
- //发送数据
- xhr.send(GM_param.data ? GM_param.data : null);
- };
- }
-
- const avatarsDefine = [
- {name:'大头像',code:'big',maxWidth:200,maxHeight:250,blob:null},
- {name:'中头像',code:'middle',maxWidth:120,maxHeight:120,blob:null},
- {name:'小头像',code:'small',maxWidth:48,maxHeight:48,blob:null},
- ];
-
- const html5mode = Boolean(avatarform.querySelector('#avatardesigner')); //HTML5模式还是Flash
- const insertPlace = avatarform.parentNode;
-
- // HTML5版本才会有的几个提交按钮
- const ipt_avatarArr = [
- avatarform.querySelector('[name="avatar1"]'),
- avatarform.querySelector('[name="avatar2"]'),
- avatarform.querySelector('[name="avatar3"]'),
- ];
- const ipt_Filedata = avatarform.querySelector('[name="Filedata"]');
- const ipt_confirm = avatarform.querySelector('[name="confirm"]');
- let data = (typeof(unsafeWindow) == 'undefined' ? window : unsafeWindow).data;
- // Flash版本的Flash
- const swf_mycamera = avatarform.querySelector('[name="mycamera"]');
-
- if (!html5mode && !swf_mycamera)
- { //解决垃圾机锋论坛的问题
- const table1 = avatarform.querySelector('table');
- const t1cell = table1.tBodies[0].rows[0].cells[0];
- const avatarSrc = t1cell.querySelector('img').src;
- const fiexdApiUrl = avatarSrc.substring(0,avatarSrc.indexOf('/data/avatar'));
-
- const table2 = avatarform.querySelector('table:nth-of-type(2)');
- const t2cell = table2.tBodies[0].rows[0].cells[0];
- const scriptHTML = t2cell.querySelector('script').innerHTML;
- const regRes = /document\.write\(AC_FL_RunContent\((.+?)\)\);/i.exec(scriptHTML);
- data = regRes[1].split(',').map(str=>str.replace(/^'|'$/g,''));
- const brokenSwfUrl = data[data.indexOf('src')+1];
- const swfUrlParse = new URL(fiexdApiUrl + brokenSwfUrl.substr(brokenSwfUrl.indexOf('/images/camera.swf')));
- swfUrlParse.searchParams.set('ucapi',fiexdApiUrl);
- data[data.indexOf('src')+1] = swfUrlParse.toString();
- }
-
- const swfUrl = new URL(data ? data[data.indexOf('src')+1] : swf_mycamera.src);
- const maxSize = parseInt(swfUrl.searchParams.get('uploadSize') || 2048, 10) * 1024;
-
-
- const styleCss = `.discuz-avatar{
- border: 1px solid #ccc;
- padding: 5px 15px;
- width:auto;
- display:inline-block;
- width: 450px;
- box-sizing: border-box;
- }
- .discuz-avatar h3{
- text-align:center;
- }
- .pic-type-div{
- display:inline-block;
- vertical-align:top;
- margin-right: 15px;
- }
- .pic-type-div:last-of-type{
- margin-right: unset;
- }
- .pic-div{
- border: 1px solid #ccc;
- cursor: pointer;
- position: relative;
- display: table-cell;
- text-align:center;
- vertical-align: middle;
- background: #fff;
- background-image:
- linear-gradient(45deg, #eee 25%, transparent 26%, transparent 74%, #eee 75%),
- linear-gradient(45deg, #eee 25%, transparent 26%, transparent 74%, #eee 75%);
- background-position: 0 0, 10px 10px;
- background-size: 20px 20px;
- }
- .pic-type-big .pic-div{
- width: 200px;
- height: 250px;
- }
- .pic-type-big .pic-img{
- max-width: 200px;
- max-height: 250px;
- }
- .pic-type-middle .pic-div{
- width: 120px;
- height: 120px;
- }
- .pic-type-middle .pic-img{
- max-width: 120px;
- max-height: 120px;
- }
- .pic-type-small .pic-div{
- width: 48px;
- height: 48px;
- }
- .pic-type-small .pic-img{
- max-width: 48px;
- max-height: 48px;
- }
-
- .choose-file{
- display: none;
- }
- .pic-div.nopic::before{
- content:"➕";
- font-size: 2em;
- }
- .pic-tag{
- text-align:center;
- }
- .submit-bar{
- text-align:center;
- }
- /*Flash AJAX状态使用*/
- .status-bar{
- font-size:2em;
- background-repeat: no-repeat;
- background-position: center;
- margin:0px auto;
- display:none;
- text-align: center;
- }
- .status-bar[data-status]{
- display:block;
- }
- @keyframes loading-animate{
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(3600deg);
- }
- }
- .status-bar[data-status="loading"]::before {
- display: inline-block;
- border: 4px SteelBlue dotted;
- border-radius: 50%;
- content:"";
- width: 1em;
- height: 1em;
- animation: loading-animate 50s infinite linear;
- }
- .status-bar[data-status="success"]::before {
- content:"✔️";
- }
- .status-bar[data-status="error"]::before {
- content:"❌";
- }
- .progress-bar{
- padding: 5px;
- text-align: center;
- }`;
-
- const fragment = document.createDocumentFragment();
-
- const ctlDiv = fragment.appendChild(document.createElement('div'));
- ctlDiv.className = 'discuz-avatar';
- const style = ctlDiv.appendChild(document.createElement('style'));
- style.type = 'text/css';
- style.innerHTML = styleCss;
- const caption = ctlDiv.appendChild(document.createElement('h3'));
- caption.appendChild(document.createTextNode(typeof(GM_info) != 'undefined' ?`${GM_info.script.name} ${GM_info.script.version}`:'无脚本扩展,直接执行脚本'));
- caption.appendChild(document.createElement('br'));
- caption.appendChild(document.createTextNode(`${html5mode?'HTML5':'Flash'}模式`));
- const picTable = ctlDiv.appendChild(document.createElement('div'));
- const picImgs = [];
- avatarsDefine.forEach((obj,idx)=>{
- const picTypeDiv = picTable.appendChild(document.createElement('div'));
- picTypeDiv.className = 'pic-type-div pic-type-' + obj.code;
- const picDiv = picTypeDiv.appendChild(document.createElement('div'));
- picDiv.className = 'pic-div nopic';
-
- const pic = new Image();
- picDiv.appendChild(pic);
- pic.className = 'pic-img img-' + obj.code;
- pic.onload = function(){
- if (this.naturalWidth > obj.maxWidth)
- {
- progressDiv.appendChild(document.createElement('br'));
- progressDiv.appendChild(document.createTextNode(`${obj.name}宽度大于 ${obj.maxWidth}px,可能可能上传失败!`));
- }
- if (this.naturalHeight > obj.maxHeight)
- {
- progressDiv.appendChild(document.createElement('br'));
- progressDiv.appendChild(document.createTextNode(`${obj.name}高度大于 ${obj.maxHeight}px,可能可能上传失败!`));
- }
- }
- picImgs.push(pic);
-
- const file = picDiv.appendChild(document.createElement('input'));
- file.type = "file";
- file.className = "choose-file";
- picDiv.onclick = function(){
- file.click();
- }
-
- file.onchange = function(e){
- const file = e.target.files[0];
- const imageType = /image\/.*/i;
- progressDiv.textContent = '';
- if (!imageType.test(file.type)) {
- progressDiv.textContent = `${file.name} 不是有效的图像文件!`;
- pic.src = '';
- picDiv.classList.add('nopic');
- return;
- }
- if (file.size > maxSize) {
- progressDiv.textContent = `${obj.name} ${file.name} 文件大小超出 ${maxSize/1048576}MiB,可能上传失败!`;
- }
- picDiv.classList.remove('nopic');
- if (pic.src.length>0)
- URL.revokeObjectURL(pic.src);
- pic.src = URL.createObjectURL(file);
- obj.blob = file;
- }
-
- const tagDiv = picTypeDiv.appendChild(document.createElement('div'));
- tagDiv.className = 'pic-tag';
- const span1 = tagDiv.appendChild(document.createElement('span'));
- span1.appendChild(document.createTextNode(obj.name));
- tagDiv.appendChild(document.createElement('br'));
- const span2 = tagDiv.appendChild(document.createElement('span'));
- span2.appendChild(document.createTextNode(`${obj.maxWidth}×${obj.maxHeight}`));
-
- });
-
- const statusDiv = ctlDiv.appendChild(document.createElement('div'));
- statusDiv.className = 'status-bar';
- const progressDiv = ctlDiv.appendChild(document.createElement('div'));
- progressDiv.className = 'progress-bar';
- const submitDiv = ctlDiv.appendChild(document.createElement('div'));
- submitDiv.className = 'submit-bar';
- const submit = submitDiv.appendChild(document.createElement('button'));
- submit.className = 'submit-btn';
- submit.innerHTML = '📤提交';
- submit.onclick = function(){
- if (!avatarsDefine.every(obj=>obj.blob))
- {
- progressDiv.textContent = `还未添加 ${avatarsDefine.filter(obj=>!obj.blob).map(obj=>obj.name).join('、')} 图像`;
- return;
- }
- submit.disabled = true;
-
- const fileDataArr = [];
- function readBlobs(blobArr,type,callback)
- {
- if (blobArr.length<1)
- {
- callback(fileDataArr);
- return;
- }
- const file = blobArr.shift();
- const fileReader = new FileReader();
- fileReader.onload = function (e) {
- fileDataArr.push(e.target.result);
- readBlobs(blobArr, type, callback);
- }
- if (type == 'base64')
- fileReader.readAsDataURL(file);
- else //if (type == 'arrayBuffer')
- fileReader.readAsArrayBuffer(file);
- }
- readBlobs(avatarsDefine.map(obj=>obj.blob), html5mode ? 'base64':'arrayBuffer', (html5mode ? sumbitAvatarsHTML5 : sumbitAvatarsFlash));
- }
- ctlDiv.appendChild(document.createElement('hr'));
- const tipsDiv = ctlDiv.appendChild(document.createElement('div'));
- tipsDiv.className = 'tips-bar';
- let quote = null,code = null;
-
- if (!html5mode)
- {
- console.log(new URL(_parseBasePath(swfUrl)).host,location.host,noGM_xmlhttpRequest)
- if (noGM_xmlhttpRequest && new URL(_parseBasePath(swfUrl)).host != location.host)
- {
- quote = submitDiv.appendChild(document.createElement('div'));
- quote.className = 'quote';
- quote.appendChild(document.createTextNode('该站点 UCenter 跨域,目前为直接执行模式无法处理 Flash 跨域问题。请使用脚本扩展,或使用 DZX3.4 的 HTML5 模式。'));
- }
-
- quote = tipsDiv.appendChild(document.createElement('div'));
- quote.className = 'quote';
- quote.appendChild(document.createTextNode('若上传100%后显示'));
- code = quote.appendChild(document.createElement('div'));
- code.className = 'blockcode';
- code.appendChild(document.createTextNode('<?xml version="1.0" ?><root><face success="0"/></root>'));
- quote.appendChild(document.createTextNode('可能是图像像素超出服务器后台限制,或格式不被 PHP 支持。'));
-
- quote = tipsDiv.appendChild(document.createElement('div'));
- quote.className = 'quote';
- quote.appendChild(document.createTextNode('若上传显示'));
- code = quote.appendChild(document.createElement('div'));
- code.className = 'blockcode';
- code.appendChild(document.createTextNode('Access denied for agent changed'));
- quote.appendChild(document.createTextNode('可能是你的活动状态失效了需要刷新,或者是 Discuz 和 UCenter 通信没配好,请直接联系网站管理员。'));
-
- }
-
- quote = tipsDiv.appendChild(document.createElement('div'));
- quote.className = 'quote';
- quote.appendChild(document.createTextNode('PHP 7.1 才支持 WebP 格式,若 WebP 上传失败可能是服务器后端检查时失败。想上传动画还是乖乖用 APNG 或 GIF。'));
-
- //将UI插入
- insertPlace.appendChild(fragment);
-
- //HTML5模式提交
- function sumbitAvatarsHTML5(base64Arr)
- {
- progressDiv.textContent = '已提交,HTML5 模式成功状态请直接参考上方编辑器';
- const dataArr = base64Arr.map(str=>str.substr(str.indexOf(",") + 1)); //拿到3个头像的Base64字符串
- dataArr.forEach((str,idx)=>{
- ipt_avatarArr[idx].value = str;
- });
- ipt_Filedata.value = '';
- ipt_confirm.value = '';
-
- avatarform.action = swfUrl.toString().replace('images/camera.swf?inajax=1', 'index.php?m=user&a=rectavatar&base64=yes'); //来自官方代码: static/avatar/avatar.js?EMK,你敢信?官方代码居然就是字符串替换
- avatarform.target='rectframe';
- avatarform.submit();
- submit.disabled = false;
- }
- //Flash模式提交
- function sumbitAvatarsFlash(arrayBufferArr)
- {
- statusDiv.setAttribute('data-status','loading');
- const dataArr = arrayBufferArr.map(bytes=>{
- const uint8Array = new Uint8Array(bytes);
- const numArray = Array.from(uint8Array);
- const strArray = numArray.map(bit=>`${bit<16?0:''}${bit.toString(16)}`);
- return strArray.join('').toUpperCase();
- });
- const sp = swfUrl.searchParams;
- const loc1 = _parseBasePath(swfUrl);
- const apiUrl = new URL(`${loc1}index.php`);
- apiUrl.protocol = location.protocol; //解决http和https混合内容的问题
- const asp = apiUrl.searchParams;
- asp.set('m','user');
- asp.set('inajax',1);
- asp.set('a','rectavatar');
- asp.set('appid',sp.get('appid'));
- asp.set('input',sp.get('input'));
- asp.set('agent',sp.get('agent'));
- asp.set('avatartype',sp.get('avatartype'));
- const post = new URLSearchParams();
- dataArr.forEach((str,idx)=>{
- post.set(`avatar${idx+1}`,str)
- });
- post.set('urlReaderTS',Date.now());
-
- GM_xmlhttpRequest({
- method: "POST",
- url: apiUrl,
- data: post.toString(),
- headers: {"Content-Type": "application/x-www-form-urlencoded"},
- onload: onloadHandler,
- onerror: onerrorHandler,
- onprogress: uploadOnprogressHandler
- });
- }
-
- //Flash模式的传统方法
- function _parseBasePath(arg1)
- {
- let loc1 = arg1.searchParams.get('ucapi');
- if (loc1.length > 0 && !(loc1.substring((loc1.length - 1)) == "/"))
- {
- loc1 = loc1 + "/";
- }
- if (loc1.length > 0 && !new RegExp("^https?://", "i").test(loc1))
- {
- loc1 = "http://" + loc1;
- }
- return loc1;
- }
-
- function onloadHandler(response) {
- progressDiv.textContent = "100%";
- const xml = response.responseXML;
- console.log(xml)
- if (xml) {
- const success = xml.querySelector('face');
- if (success != null && success.getAttribute("success") == 1) {
- statusDiv.setAttribute('data-status','success');
- } else {
- statusDiv.setAttribute('data-status','error');
- const message = xml.querySelector('message');
- if (message)
- progressDiv.textContent = message.getAttribute('type') + ': ' + message.getAttribute('value');
- else
- progressDiv.textContent = response.responseText;
- }
- } else {
- statusDiv.setAttribute('data-status','error');
- progressDiv.textContent = 'error: no responseXML';
- }
- onloadendHandler();
- }
-
- function onerrorHandler(e) {
- statusDiv.setAttribute('data-status','error');
- onloadendHandler();
- }
-
- function onloadendHandler(e) {
- submit.disabled = false;
- }
-
- function uploadOnprogressHandler(e) {
- if (e.lengthComputable) {
- progressDiv.textContent = (e.loaded / e.total).toLocaleString(undefined,{style:'percent'});
- }
- }
- })();