Discuz论坛头像上传助手

突破图片尺寸、GIF帧数限制,无损上传

질문, 리뷰하거나, 이 스크립트를 신고하세요.
  1. // ==UserScript==
  2. // @name Discuz论坛头像上传助手
  3. // @author 枫谷剑仙
  4. // @description 突破图片尺寸、GIF帧数限制,无损上传
  5. // @version 2.0.6
  6. // @namespace http://www.mapaler.com/
  7. // @include */home.php?mod=spacecp&ac=avatar*
  8. // @icon https://gitee.com/ComsenzDiscuz/DiscuzX/raw/master/upload/uc_server/images/noavatar_small.gif
  9. // @grant unsafeWindow
  10. // @grant GM_xmlhttpRequest
  11. // ==/UserScript==
  12.  
  13. (function(){
  14. 'use strict';
  15.  
  16. const avatarform = document.querySelector("#avatarform") ||
  17. document.querySelector("form[action^=home]"); //以前没有HTML5的老版本,没有#avatarform
  18. if (!avatarform) return;
  19.  
  20. let noGM_xmlhttpRequest = false;
  21. //仿GM_xmlhttpRequest函数v1.4
  22. if (typeof(GM_xmlhttpRequest) == 'undefined' || typeof(GM_info) == 'undefined')
  23. {
  24. noGM_xmlhttpRequest = true;
  25. window.GM_xmlhttpRequest = function(GM_param) {
  26. const xhr = new XMLHttpRequest(); //创建XMLHttpRequest对象
  27. xhr.open(GM_param.method, GM_param.url, true);
  28. if (GM_param.responseType) xhr.responseType = GM_param.responseType;
  29. if (GM_param.overrideMimeType) xhr.overrideMimeType(GM_param.overrideMimeType);
  30. xhr.onreadystatechange = function(e) //设置回调函数
  31. {
  32. const _xhr = e.target;
  33. if (_xhr.readyState === _xhr.DONE) { //请求完成时
  34. if (_xhr.status === 200 && GM_param.onload) //正确加载时
  35. {
  36. GM_param.onload(_xhr);
  37. }
  38. if (_xhr.status !== 200 && GM_param.onerror) //发生错误时
  39. {
  40. GM_param.onerror(_xhr);
  41. }
  42. }
  43. };
  44. if (GM_param.onprogress)
  45. xhr.upload.onprogress = function(e){GM_param.onprogress(e.target)};
  46. //添加header
  47. for (let header in GM_param.headers) {
  48. xhr.setRequestHeader(header, GM_param.headers[header]);
  49. }
  50. //发送数据
  51. xhr.send(GM_param.data ? GM_param.data : null);
  52. };
  53. }
  54.  
  55. const avatarsDefine = [
  56. {name:'大头像',code:'big',maxWidth:200,maxHeight:250,blob:null},
  57. {name:'中头像',code:'middle',maxWidth:120,maxHeight:120,blob:null},
  58. {name:'小头像',code:'small',maxWidth:48,maxHeight:48,blob:null},
  59. ];
  60.  
  61. const html5mode = Boolean(avatarform.querySelector('#avatardesigner')); //HTML5模式还是Flash
  62. const insertPlace = avatarform.parentNode;
  63.  
  64. // HTML5版本才会有的几个提交按钮
  65. const ipt_avatarArr = [
  66. avatarform.querySelector('[name="avatar1"]'),
  67. avatarform.querySelector('[name="avatar2"]'),
  68. avatarform.querySelector('[name="avatar3"]'),
  69. ];
  70. const ipt_Filedata = avatarform.querySelector('[name="Filedata"]');
  71. const ipt_confirm = avatarform.querySelector('[name="confirm"]');
  72. let data = (typeof(unsafeWindow) == 'undefined' ? window : unsafeWindow).data;
  73. // Flash版本的Flash
  74. const swf_mycamera = avatarform.querySelector('[name="mycamera"]');
  75.  
  76. if (!html5mode && !swf_mycamera)
  77. { //解决垃圾机锋论坛的问题
  78. const table1 = avatarform.querySelector('table');
  79. const t1cell = table1.tBodies[0].rows[0].cells[0];
  80. const avatarSrc = t1cell.querySelector('img').src;
  81. const fiexdApiUrl = avatarSrc.substring(0,avatarSrc.indexOf('/data/avatar'));
  82.  
  83. const table2 = avatarform.querySelector('table:nth-of-type(2)');
  84. const t2cell = table2.tBodies[0].rows[0].cells[0];
  85. const scriptHTML = t2cell.querySelector('script').innerHTML;
  86. const regRes = /document\.write\(AC_FL_RunContent\((.+?)\)\);/i.exec(scriptHTML);
  87. data = regRes[1].split(',').map(str=>str.replace(/^'|'$/g,''));
  88. const brokenSwfUrl = data[data.indexOf('src')+1];
  89. const swfUrlParse = new URL(fiexdApiUrl + brokenSwfUrl.substr(brokenSwfUrl.indexOf('/images/camera.swf')));
  90. swfUrlParse.searchParams.set('ucapi',fiexdApiUrl);
  91. data[data.indexOf('src')+1] = swfUrlParse.toString();
  92. }
  93.  
  94. const swfUrl = new URL(data ? data[data.indexOf('src')+1] : swf_mycamera.src);
  95. const maxSize = parseInt(swfUrl.searchParams.get('uploadSize') || 2048, 10) * 1024;
  96.  
  97.  
  98. const styleCss = `.discuz-avatar{
  99. border: 1px solid #ccc;
  100. padding: 5px 15px;
  101. width:auto;
  102. display:inline-block;
  103. width: 450px;
  104. box-sizing: border-box;
  105. }
  106. .discuz-avatar h3{
  107. text-align:center;
  108. }
  109. .pic-type-div{
  110. display:inline-block;
  111. vertical-align:top;
  112. margin-right: 15px;
  113. }
  114. .pic-type-div:last-of-type{
  115. margin-right: unset;
  116. }
  117. .pic-div{
  118. border: 1px solid #ccc;
  119. cursor: pointer;
  120. position: relative;
  121. display: table-cell;
  122. text-align:center;
  123. vertical-align: middle;
  124. background: #fff;
  125. background-image:
  126. linear-gradient(45deg, #eee 25%, transparent 26%, transparent 74%, #eee 75%),
  127. linear-gradient(45deg, #eee 25%, transparent 26%, transparent 74%, #eee 75%);
  128. background-position: 0 0, 10px 10px;
  129. background-size: 20px 20px;
  130. }
  131. .pic-type-big .pic-div{
  132. width: 200px;
  133. height: 250px;
  134. }
  135. .pic-type-big .pic-img{
  136. max-width: 200px;
  137. max-height: 250px;
  138. }
  139. .pic-type-middle .pic-div{
  140. width: 120px;
  141. height: 120px;
  142. }
  143. .pic-type-middle .pic-img{
  144. max-width: 120px;
  145. max-height: 120px;
  146. }
  147. .pic-type-small .pic-div{
  148. width: 48px;
  149. height: 48px;
  150. }
  151. .pic-type-small .pic-img{
  152. max-width: 48px;
  153. max-height: 48px;
  154. }
  155.  
  156. .choose-file{
  157. display: none;
  158. }
  159. .pic-div.nopic::before{
  160. content:"➕";
  161. font-size: 2em;
  162. }
  163. .pic-tag{
  164. text-align:center;
  165. }
  166. .submit-bar{
  167. text-align:center;
  168. }
  169. /*Flash AJAX状态使用*/
  170. .status-bar{
  171. font-size:2em;
  172. background-repeat: no-repeat;
  173. background-position: center;
  174. margin:0px auto;
  175. display:none;
  176. text-align: center;
  177. }
  178. .status-bar[data-status]{
  179. display:block;
  180. }
  181. @keyframes loading-animate{
  182. from {
  183. transform: rotate(0deg);
  184. }
  185. to {
  186. transform: rotate(3600deg);
  187. }
  188. }
  189. .status-bar[data-status="loading"]::before {
  190. display: inline-block;
  191. border: 4px SteelBlue dotted;
  192. border-radius: 50%;
  193. content:"";
  194. width: 1em;
  195. height: 1em;
  196. animation: loading-animate 50s infinite linear;
  197. }
  198. .status-bar[data-status="success"]::before {
  199. content:"✔️";
  200. }
  201. .status-bar[data-status="error"]::before {
  202. content:"❌";
  203. }
  204. .progress-bar{
  205. padding: 5px;
  206. text-align: center;
  207. }`;
  208.  
  209. const fragment = document.createDocumentFragment();
  210.  
  211. const ctlDiv = fragment.appendChild(document.createElement('div'));
  212. ctlDiv.className = 'discuz-avatar';
  213. const style = ctlDiv.appendChild(document.createElement('style'));
  214. style.type = 'text/css';
  215. style.innerHTML = styleCss;
  216. const caption = ctlDiv.appendChild(document.createElement('h3'));
  217. caption.appendChild(document.createTextNode(typeof(GM_info) != 'undefined' ?`${GM_info.script.name} ${GM_info.script.version}`:'无脚本扩展,直接执行脚本'));
  218. caption.appendChild(document.createElement('br'));
  219. caption.appendChild(document.createTextNode(`${html5mode?'HTML5':'Flash'}模式`));
  220. const picTable = ctlDiv.appendChild(document.createElement('div'));
  221. const picImgs = [];
  222. avatarsDefine.forEach((obj,idx)=>{
  223. const picTypeDiv = picTable.appendChild(document.createElement('div'));
  224. picTypeDiv.className = 'pic-type-div pic-type-' + obj.code;
  225. const picDiv = picTypeDiv.appendChild(document.createElement('div'));
  226. picDiv.className = 'pic-div nopic';
  227.  
  228. const pic = new Image();
  229. picDiv.appendChild(pic);
  230. pic.className = 'pic-img img-' + obj.code;
  231. pic.onload = function(){
  232. if (this.naturalWidth > obj.maxWidth)
  233. {
  234. progressDiv.appendChild(document.createElement('br'));
  235. progressDiv.appendChild(document.createTextNode(`${obj.name}宽度大于 ${obj.maxWidth}px,可能可能上传失败!`));
  236. }
  237. if (this.naturalHeight > obj.maxHeight)
  238. {
  239. progressDiv.appendChild(document.createElement('br'));
  240. progressDiv.appendChild(document.createTextNode(`${obj.name}高度大于 ${obj.maxHeight}px,可能可能上传失败!`));
  241. }
  242. }
  243. picImgs.push(pic);
  244.  
  245. const file = picDiv.appendChild(document.createElement('input'));
  246. file.type = "file";
  247. file.className = "choose-file";
  248. picDiv.onclick = function(){
  249. file.click();
  250. }
  251.  
  252. file.onchange = function(e){
  253. const file = e.target.files[0];
  254. const imageType = /image\/.*/i;
  255. progressDiv.textContent = '';
  256. if (!imageType.test(file.type)) {
  257. progressDiv.textContent = `${file.name} 不是有效的图像文件!`;
  258. pic.src = '';
  259. picDiv.classList.add('nopic');
  260. return;
  261. }
  262. if (file.size > maxSize) {
  263. progressDiv.textContent = `${obj.name} ${file.name} 文件大小超出 ${maxSize/1048576}MiB,可能上传失败!`;
  264. }
  265. picDiv.classList.remove('nopic');
  266. if (pic.src.length>0)
  267. URL.revokeObjectURL(pic.src);
  268. pic.src = URL.createObjectURL(file);
  269. obj.blob = file;
  270. }
  271.  
  272. const tagDiv = picTypeDiv.appendChild(document.createElement('div'));
  273. tagDiv.className = 'pic-tag';
  274. const span1 = tagDiv.appendChild(document.createElement('span'));
  275. span1.appendChild(document.createTextNode(obj.name));
  276. tagDiv.appendChild(document.createElement('br'));
  277. const span2 = tagDiv.appendChild(document.createElement('span'));
  278. span2.appendChild(document.createTextNode(`${obj.maxWidth${obj.maxHeight}`));
  279.  
  280. });
  281.  
  282. const statusDiv = ctlDiv.appendChild(document.createElement('div'));
  283. statusDiv.className = 'status-bar';
  284. const progressDiv = ctlDiv.appendChild(document.createElement('div'));
  285. progressDiv.className = 'progress-bar';
  286. const submitDiv = ctlDiv.appendChild(document.createElement('div'));
  287. submitDiv.className = 'submit-bar';
  288. const submit = submitDiv.appendChild(document.createElement('button'));
  289. submit.className = 'submit-btn';
  290. submit.innerHTML = '📤提交';
  291. submit.onclick = function(){
  292. if (!avatarsDefine.every(obj=>obj.blob))
  293. {
  294. progressDiv.textContent = `还未添加 ${avatarsDefine.filter(obj=>!obj.blob).map(obj=>obj.name).join('、')} 图像`;
  295. return;
  296. }
  297. submit.disabled = true;
  298.  
  299. const fileDataArr = [];
  300. function readBlobs(blobArr,type,callback)
  301. {
  302. if (blobArr.length<1)
  303. {
  304. callback(fileDataArr);
  305. return;
  306. }
  307. const file = blobArr.shift();
  308. const fileReader = new FileReader();
  309. fileReader.onload = function (e) {
  310. fileDataArr.push(e.target.result);
  311. readBlobs(blobArr, type, callback);
  312. }
  313. if (type == 'base64')
  314. fileReader.readAsDataURL(file);
  315. else //if (type == 'arrayBuffer')
  316. fileReader.readAsArrayBuffer(file);
  317. }
  318. readBlobs(avatarsDefine.map(obj=>obj.blob), html5mode ? 'base64':'arrayBuffer', (html5mode ? sumbitAvatarsHTML5 : sumbitAvatarsFlash));
  319. }
  320. ctlDiv.appendChild(document.createElement('hr'));
  321. const tipsDiv = ctlDiv.appendChild(document.createElement('div'));
  322. tipsDiv.className = 'tips-bar';
  323. let quote = null,code = null;
  324.  
  325. if (!html5mode)
  326. {
  327. console.log(new URL(_parseBasePath(swfUrl)).host,location.host,noGM_xmlhttpRequest)
  328. if (noGM_xmlhttpRequest && new URL(_parseBasePath(swfUrl)).host != location.host)
  329. {
  330. quote = submitDiv.appendChild(document.createElement('div'));
  331. quote.className = 'quote';
  332. quote.appendChild(document.createTextNode('该站点 UCenter 跨域,目前为直接执行模式无法处理 Flash 跨域问题。请使用脚本扩展,或使用 DZX3.4 的 HTML5 模式。'));
  333. }
  334.  
  335. quote = tipsDiv.appendChild(document.createElement('div'));
  336. quote.className = 'quote';
  337. quote.appendChild(document.createTextNode('若上传100%后显示'));
  338. code = quote.appendChild(document.createElement('div'));
  339. code.className = 'blockcode';
  340. code.appendChild(document.createTextNode('<?xml version="1.0" ?><root><face success="0"/></root>'));
  341. quote.appendChild(document.createTextNode('可能是图像像素超出服务器后台限制,或格式不被 PHP 支持。'));
  342.  
  343. quote = tipsDiv.appendChild(document.createElement('div'));
  344. quote.className = 'quote';
  345. quote.appendChild(document.createTextNode('若上传显示'));
  346. code = quote.appendChild(document.createElement('div'));
  347. code.className = 'blockcode';
  348. code.appendChild(document.createTextNode('Access denied for agent changed'));
  349. quote.appendChild(document.createTextNode('可能是你的活动状态失效了需要刷新,或者是 Discuz 和 UCenter 通信没配好,请直接联系网站管理员。'));
  350. }
  351.  
  352. quote = tipsDiv.appendChild(document.createElement('div'));
  353. quote.className = 'quote';
  354. quote.appendChild(document.createTextNode('PHP 7.1 才支持 WebP 格式,若 WebP 上传失败可能是服务器后端检查时失败。想上传动画还是乖乖用 APNG 或 GIF。'));
  355.  
  356. //将UI插入
  357. insertPlace.appendChild(fragment);
  358.  
  359. //HTML5模式提交
  360. function sumbitAvatarsHTML5(base64Arr)
  361. {
  362. progressDiv.textContent = '已提交,HTML5 模式成功状态请直接参考上方编辑器';
  363. const dataArr = base64Arr.map(str=>str.substr(str.indexOf(",") + 1)); //拿到3个头像的Base64字符串
  364. dataArr.forEach((str,idx)=>{
  365. ipt_avatarArr[idx].value = str;
  366. });
  367. ipt_Filedata.value = '';
  368. ipt_confirm.value = '';
  369.  
  370. avatarform.action = swfUrl.toString().replace('images/camera.swf?inajax=1', 'index.php?m=user&a=rectavatar&base64=yes'); //来自官方代码: static/avatar/avatar.js?EMK,你敢信?官方代码居然就是字符串替换
  371. avatarform.target='rectframe';
  372. avatarform.submit();
  373. submit.disabled = false;
  374. }
  375. //Flash模式提交
  376. function sumbitAvatarsFlash(arrayBufferArr)
  377. {
  378. statusDiv.setAttribute('data-status','loading');
  379. const dataArr = arrayBufferArr.map(bytes=>{
  380. const uint8Array = new Uint8Array(bytes);
  381. const numArray = Array.from(uint8Array);
  382. const strArray = numArray.map(bit=>`${bit<16?0:''}${bit.toString(16)}`);
  383. return strArray.join('').toUpperCase();
  384. });
  385. const sp = swfUrl.searchParams;
  386. const loc1 = _parseBasePath(swfUrl);
  387. const apiUrl = new URL(`${loc1}index.php`);
  388. apiUrl.protocol = location.protocol; //解决http和https混合内容的问题
  389. const asp = apiUrl.searchParams;
  390. asp.set('m','user');
  391. asp.set('inajax',1);
  392. asp.set('a','rectavatar');
  393. asp.set('appid',sp.get('appid'));
  394. asp.set('input',sp.get('input'));
  395. asp.set('agent',sp.get('agent'));
  396. asp.set('avatartype',sp.get('avatartype'));
  397. const post = new URLSearchParams();
  398. dataArr.forEach((str,idx)=>{
  399. post.set(`avatar${idx+1}`,str)
  400. });
  401. post.set('urlReaderTS',Date.now());
  402. GM_xmlhttpRequest({
  403. method: "POST",
  404. url: apiUrl,
  405. data: post.toString(),
  406. headers: {"Content-Type": "application/x-www-form-urlencoded"},
  407. onload: onloadHandler,
  408. onerror: onerrorHandler,
  409. onprogress: uploadOnprogressHandler
  410. });
  411. }
  412.  
  413. //Flash模式的传统方法
  414. function _parseBasePath(arg1)
  415. {
  416. let loc1 = arg1.searchParams.get('ucapi');
  417. if (loc1.length > 0 && !(loc1.substring((loc1.length - 1)) == "/"))
  418. {
  419. loc1 = loc1 + "/";
  420. }
  421. if (loc1.length > 0 && !new RegExp("^https?://", "i").test(loc1))
  422. {
  423. loc1 = "http://" + loc1;
  424. }
  425. return loc1;
  426. }
  427.  
  428. function onloadHandler(response) {
  429. progressDiv.textContent = "100%";
  430. const xml = response.responseXML;
  431. console.log(xml)
  432. if (xml) {
  433. const success = xml.querySelector('face');
  434. if (success != null && success.getAttribute("success") == 1) {
  435. statusDiv.setAttribute('data-status','success');
  436. } else {
  437. statusDiv.setAttribute('data-status','error');
  438. const message = xml.querySelector('message');
  439. if (message)
  440. progressDiv.textContent = message.getAttribute('type') + ': ' + message.getAttribute('value');
  441. else
  442. progressDiv.textContent = response.responseText;
  443. }
  444. } else {
  445. statusDiv.setAttribute('data-status','error');
  446. progressDiv.textContent = 'error: no responseXML';
  447. }
  448. onloadendHandler();
  449. }
  450.  
  451. function onerrorHandler(e) {
  452. statusDiv.setAttribute('data-status','error');
  453. onloadendHandler();
  454. }
  455.  
  456. function onloadendHandler(e) {
  457. submit.disabled = false;
  458. }
  459.  
  460. function uploadOnprogressHandler(e) {
  461. if (e.lengthComputable) {
  462. progressDiv.textContent = (e.loaded / e.total).toLocaleString(undefined,{style:'percent'});
  463. }
  464. }
  465. })();