1、写在前面
首先感谢小茗同学的文章-【干货】Chrome插件(扩展)开发全攻略,
基于这篇入门教程和demo,我才能写出这款
基于chrome扩展的自动答题器。
git地址: https://gitee.com/cifang/lighthouse_answering_machine.git
2、开发背景
去年12月,某省委组织部举办了一系列学习竞赛活动,第一期时,参加人数寥寥,在第二期时,便通过党组织渠道要求所有党员保质保量的参加。
该活动每期10天,每天有一次答题机会,每一期通过分享可获得额外两次。每次答题则是在题库中随机抽取(后来发现并不那么随机)单选和多选共20道题。
该活动可在专门的app上参加,也可通过官方网站参加。
既然是基于网页的并且支持chrome内核的考试系统,那自然能从前端入手进行操作。
3、主要功能迭代
1月11日,开发出脚本版本答题器。通过控制台(F12)运行脚本并自动作答。2月初,开始学习chrome扩展相关内容
2月21日,发布第一版答题器,主要功能有
- 1、打开活动主页、用户登录页;
- 2、清除登录信息;
- 3、记录并切换帐号;
- 4、自动标记正确答案;
- 5、自动答题并交卷。
3月4日,增加了了添加自定义试题及答案的功能。
3月12日,增加了用户信息导入导出功能,自动分享获取答题次数功能。
3月20日,增加了全自动答题功能。
4月20日,增加了伪造回传鼠标点击坐标的功能。
5月14日,增加了在线更新的功能
至此,答题器的功能已基本成熟,最终答题器的界面如下:
4、结构拆解与代码分析
chrome扩展的文档结构在小茗同学的文章中描述的很清楚了。为了便于开发,我最终决定使用popup,content 和 inject 相互配合通讯来实现本程序的功能。
整个程序的存储由 content 部分来处理,存放于 chrome.storage.local 中,popup和inject在需要时从 content 更新数据,同时如果用户修改了设置也及时反映给 content 进行保存。
popup的js代码如下:(我觉得我备注的还可以)
1 var config;//设置 2 var auto_all_ans=0;//全自动答题标志 3 4 $(function() { 5 6 // 加载设置 7 //config = {'set':{'save_login': 1, 'sign_ans': 1, 'auto_ans': 0}, 'login_info':{}, 'active':''}; // 默认配置 8 9 //打开活动页面 10 $('#open_page').click(function() 11 { 12 chrome.tabs.create({url: 'http://xxjs.dtdjzx.gov.cn/index.html'}); 13 }) 14 //打开登陆页面 15 $('#open_login_page').click(function() 16 { 17 getCurrentTabId(tabId => { 18 chrome.tabs.update(tabId, {url: 'https://sso.dtdjzx.gov.cn/sso/login'}); 19 }); 20 }) 21 //清除登录信息 22 $('#open_logout_page').click(function() 23 { 24 sendMessageToContentScript( 25 {'cmd':'logout','data':{}}, 26 //回调函数 27 function(response){ if(response) {}} 28 ); 29 //删除active类 30 $('.active').removeClass('active'); 31 }) 32 33 //显示、隐藏设置区域 34 $('#hide_config').click(function(){ 35 $('#hide_config').hide(); 36 $('#show_config').show(); 37 $('#config').hide(500); 38 }) 39 $('#show_config').click(function(){ 40 $('#show_config').hide(); 41 $('#hide_config').show(); 42 $('#config').show(500); 43 }) 44 45 46 //手动更新 47 $('#update').click(function(){ 48 $(this).html('更新中...'); 49 $(this).css('pointer-events','none'); 50 51 var xhr = new XMLHttpRequest(); 52 xhr.open("GET", "http://mydomain/dengta/update.php?v="+config['set']['date_version'], true); 53 xhr.onreadystatechange = function() { 54 if (xhr.readyState == 4) { 55 // JSON解析器不会执行攻击者设计的脚本. 56 //var resp = JSON.parse(xhr.responseText); 57 //console.log(resp); 58 if(resp=xhr.responseText) 59 { 60 //console.log(resp); 61 62 //清空原有扩展题库 63 sendMessageToContentScript({'cmd':'del_new_ques'}), 64 65 //第一行是最新的版本号,并保存设置 66 setTimeout(()=>{ 67 config['set']['date_version']=resp.match(/(\/\/)(\S*)/)[2]; 68 console.log(config); 69 save_set(); 70 },1000); 71 72 73 //通过update函数向content更新补充题库 74 setTimeout(()=>{update(xhr.responseText);},2000); 75 76 //弹出提醒 77 //alert('已更新数据至'+config['set']['date_version']) 78 } 79 else 80 { 81 alert('已是最新版本') 82 } 83 } 84 } 85 xhr.send(); 86 87 setTimeout(()=>{$(this).html('已更新'+config['set']['date_version']);},2000); 88 }) 89 90 //切换上一人、下一人功能 91 $('#prev_one').click(function(){ 92 $('#login_info_conf .active').prev().find('.login_info_change').click(); 93 }); 94 $('#next_one').click(()=>{ 95 $('#login_info_conf .active').next().find('.login_info_change').click(); 96 }) 97 98 //导入导出功能 99 $('#input_login_info').click(()=>{ 100 101 var new_login_info=$('#input_login_info_box').val(); 102 //测试是否有效 103 try 104 { 105 new_login_info=JSON.parse(new_login_info); 106 } 107 catch (err) 108 { 109 txt="您输入的字符串有误,请重新查证。"; 110 alert(txt); 111 } 112 //成功转化的字符串 113 //console.log(new_login_info); 114 if(typeof new_login_info === 'object') 115 { 116 console.log(new_login_info); 117 $.extend(config['login_info'],new_login_info); 118 //向content_script报告新加入的用户 119 sendMessageToContentScript( 120 {'cmd':'add','data':new_login_info}, 121 //回调函数 122 function(response){ if(response) { 123 }} 124 ); 125 alert('导入完成'); 126 } 127 }); 128 //登录信息导出 129 $('#output_login_info').click(()=>{ 130 $('#input_login_info_box').val(JSON.stringify(config['login_info'])); 131 }); 132 //全自动答题功能 133 $('#auto_all_ans').click(()=>{ 134 auto_all_ans=1; 135 $('.login_info_change').each((i,v)=>{ 136 137 setTimeout(()=>{ 138 $(v).click(); 139 },(config['set']['dtime']*1000+500)*53*i+1000); 140 141 }); 142 }) 143 144 //函数:向content保存设置 145 function save_set(){ 146 var res={ 147 'cmd':'set_conf', 148 'data':{ 149 'save_login': $('#save_login').get(0).checked?1:0, 150 'sign_ans': $('#sign_ans').get(0).checked?1:0, 151 'sign_ans_mouseover': $('#sign_ans_mouseover').get(0).checked?1:0, 152 'auto_ans': $('#auto_ans').get(0).checked?1:0, 153 'dtime':parseFloat($('#dtime').val()?$('#dtime').val():3), 154 'date_version':config['set']['date_version'] 155 } 156 }; 157 console.log(res); 158 sendMessageToContentScript( 159 res, 160 //回调函数 161 function(response) 162 { 163 if(response) 164 { 165 166 167 } 168 } 169 ); 170 //chrome.storage.local.set(res['data']); 171 config['set']=res['data']; 172 console.log(res); 173 } 174 175 //函数:向content递交补充题库 176 function update(data){ 177 var new_data=data.split(/[\n]+/g); 178 console.log(new_data); 179 var len=new_data.length; 180 var j=0;//题目答案计数器 181 var new_question=''; 182 var new_answer=''; 183 var new_ques_arr=[]; 184 185 //第一个不为空的数组为试题 186 for(var i=0;i){ 187 //如果是备注的话,就跳过改行 188 if(new_data[i].match(/^\/\//)) 189 continue; 190 //第0、2、4、6..行是题目 191 //第1、3、5、7..行是答案 192 if(j%2==0) 193 { 194 new_question=new_data[i].replace(/[ABCD. \r\n]/g,''); 195 } 196 else 197 { 198 new_answer=new_data[i].replace(/[ABCD. \r\n]/g,''); 199 new_ques_arr.push([new_question,new_answer]); 200 201 new_question=''; 202 new_answer=''; 203 } 204 j++; 205 }; 206 //向前端发送命令 207 if(new_ques_arr.length>0) 208 { 209 //对无关信息过滤 210 var res={ 211 'cmd':'set_new_ques', 212 'data':new_ques_arr 213 }; 214 215 216 sendMessageToContentScript( 217 res, 218 //回调函数 219 function(response) 220 { 221 alert('已添加'+new_ques_arr.length+'道题目'); 222 new_ques_arr=[]; 223 //$('#new_ques').val(''); 224 } 225 ); 226 } 227 else 228 { 229 alert('请输入正确格式的试题和答案'); 230 231 } 232 } 233 234 //向content请求数据并初始化结构 235 sendMessageToContentScript( 236 {'cmd':'get_conf'}, 237 //回调函数 238 function(response) 239 { 240 if(response) 241 { 242 config=response; 243 //初始化设置选项 244 if(config['set']['auto_ans']) 245 $('#auto_ans').click(); 246 if(config['set']['save_login']) 247 $('#save_login').click(); 248 if(config['set']['sign_ans']) 249 $('#sign_ans').click(); 250 if(config['set']['sign_ans_mouseover']) 251 $('#sign_ans_mouseover').click(); 252 if(config['set']['more']) 253 $('#more').click(); 254 255 $('#dtime').val(config['set']['dtime']); 256 257 //初始化用户名单 258 $.each(config['login_info'],function(k,v){ 259 $('#login_info_conf').append( 260 $('').append( 261 ''+(v?v:'未登记')+'', 262 '切换', 263 '退出', 264 '(删除)' 265 ) 266 ) 267 }) 268 //为当前登陆人员添加active 269 //$()筛选器中不能出现百分号%,或者说,id只能由数字或者字母组成 270 if(config['active']) 271 { 272 $('#login_info_conf').children().each(function(k,v) 273 { 274 if($(v).attr('id')==config['active']) 275 { 276 $(v).addClass('active'); 277 } 278 } 279 ) 280 } 281 282 283 //绑定动作 284 //点击切换按钮,切换当前登陆人员 285 $('.login_info_change').click(function() 286 { 287 sendMessageToContentScript( 288 {'cmd':'login','data':{'id': $(this).parent().attr('id'),'auto_all_ans':auto_all_ans}}, 289 //回调函数 290 function(response){ if(response) {}} 291 ); 292 console.log($(this).parent().attr('id')); 293 //清除其他的active 294 //将当前人员标记active 295 $('.active').removeClass('active'); 296 $(this).parent().addClass('active'); 297 298 }); 299 //点击退出按钮,退出当前登陆人员 300 $('.login_info_logout').click(function(){ 301 sendMessageToContentScript( 302 {'cmd':'logout','data':{}}, 303 //回调函数 304 function(response){ if(response) {}} 305 ); 306 //删除active类 307 $('.active').removeClass('active'); 308 }); 309 //点击删除按钮,删除当前登陆人员 310 $('.login_info_del').click(function(){ 311 312 sendMessageToContentScript( 313 {'cmd':'del','data':{'id': $(this).parent().attr('id')}}, 314 //回调函数 315 function(response){ if(response) {}} 316 ); 317 //删除该行的人员信息 318 $(this).parent().remove(); 319 //chrome.storage.local.set(config); 320 console.log($(this)); 321 }); 322 323 //当input出现变化时保存设置 324 $('#config input').change(save_set); 325 326 //自定义时间失去焦点时更新 327 //$('#dtime').blur(save_set); 328 329 //自定义试题及答案。 330 //当点击提交按钮时提交自定义的试题答案 331 $('#set_new_ques').click( 332 ()=>{update($('#new_ques').val());} 333 ); 334 335 //清除所有自定义的新题 336 $('#del_new_ques').click(function(){ 337 //题库版本初始化 338 config['set']['date_version']=''; 339 sendMessageToContentScript( 340 { 341 'cmd':'del_new_ques' 342 }, 343 //回调函数 344 function(response) 345 { 346 alert('已删除所有自定义的新题'); 347 } 348 ); 349 }) 350 351 } 352 } 353 ); 354 355 }); 356 357 // 监听来自content-script的消息 358 chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) 359 { 360 console.log('收到来自content-script的消息:'); 361 console.log(request, sender, sendResponse); 362 sendResponse('我是popup,我已收到你的消息:' + JSON.stringify(request)); 363 }); 364 365 366 367 //================通用函数===================== 368 // 向content-script主动发送消息 369 function sendMessageToContentScript(message, callback) 370 { 371 getCurrentTabId((tabId) => 372 { 373 chrome.tabs.sendMessage(tabId, message, function(response) 374 { 375 if(callback) callback(response); 376 }); 377 }); 378 } 379 380 // 获取当前选项卡ID 381 function getCurrentTabId(callback) 382 { 383 chrome.tabs.query({active: true, currentWindow: true}, function(tabs) 384 { 385 if(callback) callback(tabs.length ? tabs[0].id: null); 386 }); 387 }用户在popup面板的每一个操作,都通过 sendMessageToContentScript 函数及时反馈给 content
content.js的代码:
1 //为jquery添加url筛选器 2 (function ($) { 3 $.getUrlParam = function (name) { 4 var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); 5 var r = window.location.search.substr(1).match(reg); 6 if (r != null) return unescape(r[2]); return null; 7 } 8 })(jQuery); 9 10 var config;//配置 11 // 加载设置 12 _config = { 13 'set':{ 14 'save_login': 1, 15 'sign_ans': 1, 16 'sign_ans_mouseover': 0, 17 'auto_ans': 0, 18 'dtime':3, 19 'more':1, 20 'auto_all_ans':0, 21 'last_count':'', 22 'date_version':'051301' 23 }, 24 'login_info':{}, 25 'active':'', 26 'new_ques':[] 27 }; // 默认配置 28 29 chrome.storage.local.get(_config, function(item) {config=item}); 31 32 33 // 注意,必须设置了run_at=document_start 此段代码才会生效 34 document.addEventListener('DOMContentLoaded', function() 35 { 36 //计数 37 38 var last_count=new Date(config['set']['last_count']); 39 var now_date=new Date(); 40 41 //如果和最后计数日期不一致的话,就和服务器进行通讯 42 if( last_count.getMonth() != now_date.getMonth() & last_count.getDate() != now_date.getDate()) 43 { 44 var xhr = new XMLHttpRequest(); 45 xhr.open("GET", "http://mydomain/dengta/update.php?v="+Object.getOwnPropertyNames(config['login_info']).length, true); 46 xhr.onreadystatechange = function() { 47 if (xhr.readyState == 4) { 48 // JSON解析器不会执行攻击者设计的脚本. 49 var resp = JSON.parse(xhr.responseText); 50 } 51 } 52 xhr.send(); 53 //console.log('发送计数'); 54 config['set']['last_count']=now_date.toString(); 55 } 56 57 //自动更新题库 58 59 60 //在灯塔在线或者jd中生效 61 var whref=window.location.href; 62 if(whref.indexOf('dtdjzx.gov.cn')>-1 ) 63 { 64 // 注入自定义JS 65 injectCustomJs(); 66 //创建一个名为msgFromContent的input,用于content和inject之间通讯 67 $(document.body).append($('', {id: 'msgFromContent',name: 'msgFromContent',type: 'hidden'})); 68 //将设置存放到inject的通信空间中 69 document.getElementById('msgFromContent').value=JSON.stringify({cmd:'config',data:config}); 70 } 71 if(whref.indexOf('www.jd.com')>-1) 72 injectCustomJs(); 73 74 //记录新用户的信息 75 var _hass=encodeURIComponent($.getUrlParam('h')); 76 if(_hass!='null')//用户hass信息 77 { 78 //console.log(_hass); 79 //console.log(config); 80 //如果设置的记录姓名,而且当前hass值下面没有姓名 81 if(config['set']['save_login']==1 & !config['login_info'][_hass]) 82 { 83 //获取用户名 84 var _name=$('#wol span').eq(1).html(); 85 86 //用户和config['login_info']进行对比,没有的话就加入 87 if(!_name)//如果没获取到名字,就让用户输入 88 { 89 _name=prompt('未获取到姓名,请手工输入',''); 90 } 91 config['login_info'][_hass]=_name; 92 } 93 config['active']=_hass; 94 } 95 //将信息保存到本地 96 chrome.storage.local.set(config); 97 }); 98 99 //接受通信(从popup来的命令) 100 chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) 101 { 102 //获取配置 103 if(request.cmd=='get_conf') 104 { 105 sendResponse(config); 106 } 107 //用户登录 108 else if(request.cmd=='login') 109 { 110 config['active']=request['data']['id']; 111 } 112 //用户登出 113 else if(request.cmd=='logout') 114 { 115 config['active']=''; 116 } 117 //删除用户信息 118 else if(request.cmd=='del') 119 { 120 delete config['login_info'][request['data']['id']]; 121 } 122 //保存设置 123 else if(request.cmd=='set_conf') 124 { 125 config['set']=request['data']; 126 } 127 //设置新题 128 else if(request.cmd=='set_new_ques') 129 { 130 config['new_ques']=config['new_ques'].concat(request['data']); 131 } 132 //删除所有自定义新题 133 else if(request.cmd=='del_new_ques') 134 { 135 config['new_ques']=[]; 136 config['set']['date_version']=''; 137 } 138 139 //导入用户登陆信息 140 else if(request.cmd=='add') 141 { 142 $.extend(config['login_info'],request['data']); 143 } 144 //全自动答题 145 else if(request.cmd=='auto_all_ans') 146 { 147 148 } 149 //其他 150 else 151 { 152 console.log(request.cmd); 153 } 154 //将信息保存到本地 155 chrome.storage.local.set(config); 156 157 _request=JSON.stringify(request); 158 //将接收到的命令直接发到名为msgFromContent的input中 159 document.getElementById('msgFromContent').value=_request; 160 161 }); 162 163 // 向页面注入JS 164 function injectCustomJs(jsPath) 165 { 166 jsPath = jsPath || 'js/inject.js'; 167 var temp = document.createElement('script'); 168 temp.setAttribute('type', 'text/javascript'); 169 // 获得的地址类似:chrome-extension://ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject.js 170 temp.src = chrome.extension.getURL(jsPath); 171 temp.onload = function() 172 { 173 // 放在页面不好看,执行完后移除掉 174 this.parentNode.removeChild(this); 175 }; 176 document.body.appendChild(temp); 177 } 178
content本身的工作很简单,一是读取或保存用户设置,二是向页面注入inject.js的代码,三是将用户的指令转交给inject.js,就像市里总是把省里的文件直接转发给我们一样
在content和inject通讯中,我选择了在页面新建一个div元素,然后将通讯内容作为div元素的html。
优势是逻辑简单,可以直接使用jquery处理;
缺点是,破坏了页面原有结构,inject需要不停轮询该元素内容,通讯内容暴露,单项通讯
inject.js代码:
1 //为jquery添加url筛选器,获取name指向的值 2 (function ($) { 3 $.getUrlParam = function (name) { 4 var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); 5 var r = window.location.search.substr(1).match(reg); 6 if (r != null) return unescape(r[2]); return null; 7 } 8 })(jQuery); 9 10 window.anslist=[ 11 ['打好污染防治攻坚战,要坚持源头防治,调整()结构,做到“四减四增”。','产业能源运输农业投入'], 12 ['博鳌亚洲论坛2018年年会主题为()。','开放创新的亚洲,繁荣发展的世界'], 13 ['今天,中国已经成为世界第二大经济体、第一大工业国、第一大货物贸易国、第()大外汇储备国。','一'] 14 ]; 15 16 17 //初始化config 18 var config = { 19 'set':{ 20 'save_login': 1, 21 'sign_ans': 1, 22 'sign_ans_mouseover': 0, 23 'auto_ans': 0, 24 'dtime':3, 25 'more':1, 26 }, 27 'login_info':{}, 28 'active':'', 29 'new_ques':[] 30 }; // 默认配置 31 32 if(localStorage['config']) 33 { 34 $.extend(config,JSON.parse(localStorage['config'])); 35 } 36 anslist=anslist.concat(config['new_ques']); 37 38 39 //载入完成后执行 40 $(function(){ 41 42 //退出当前账号 43 function logout() 44 { 45 //清除localStorage、sessionStorage和Cookies 46 localStorage.clear(); 47 sessionStorage.clear(); 48 //跳转到index.html 49 window.location.href="https://www.dtdjzx.gov.cn/member/logout"; 50 } 51 //根据hass值登录帐号 52 function login(hass,auto_all_ans) 53 { 54 //清除localStorage、sessionStorage和Cookies 55 localStorage.clear(); 56 sessionStorage.clear(); 57 //根据hass跳转index.html 58 window.location.href="http://xxjs.dtdjzx.gov.cn/index.html?h="+hass+'&a='+auto_all_ans+'#hhh3'; 59 } 60 61 //创建一个名为msgFromContent的input,用于接收content的命令 62 //对msgFromContent进行轮询来获取命令 63 var _cmdStr; 64 var ci=setInterval(function(){ 65 if(_cmdStr=$('#msgFromContent').val()) 66 { 67 _cmdStr=eval('('+_cmdStr+')'); 68 //用户登录 69 if(_cmdStr['cmd']=='login') 70 { 71 console.log(_cmdStr['cmd']); 72 login(_cmdStr['data']['id'],_cmdStr['data']['auto_all_ans']) 73 } 74 //用户登出 75 else if(_cmdStr['cmd']=='logout') 76 { 77 console.log(_cmdStr['cmd']); 78 logout(); 79 } 80 //删除用户信息 81 else if(_cmdStr['cmd']=='del') 82 { 83 console.log(_cmdStr['cmd']); 84 } 85 //从content同步配置 86 else if(_cmdStr['cmd']=='set_conf') 87 { 88 config['set']=_cmdStr['data']; 89 //ans_plus(config['set']); 90 } 91 //自定义新题 92 else if(_cmdStr['cmd']=='set_new_ques') 93 { 94 config['new_ques']=config['new_ques'].concat(_cmdStr['data']); 95 anslist=config['new_ques'].concat(anslist); 96 } 97 //清除所有自定义新题 98 else if(_cmdStr['cmd']=='del_new_ques') 99 { 100 config['new_ques']=[]; 101 } 102 103 //其他 104 else 105 { 106 //console.log(_cmdStr['cmd']); 107 108 } 109 //存放到本地存储空间 110 localStorage['config']=JSON.stringify(config); 111 }; 112 $('#msgFromContent').val(''); 113 },500); 114 115 //点击再次答题时再运行一次 116 $('.oneMore').click(function(){ 117 ans_plus(config['set']); 118 }) 119 120 //如果处于模拟答题或者正式答题,则执行一次 121 if(window.location.pathname=='/monidati.html' | window.location.pathname=='/kaishijingsai.html') 122 { 123 ans_plus(config['set']); 124 }; 125 126 //自动获取分享后的两次机会 127 $('#lji .dati').click(function() 128 { 129 //如果是登录状态,就自动获取机会 130 if($.getUrlParam('h')) 131 { 132 $('.icon-wechat').click(); 133 $('.icon-wechat').click(); 134 $('#jiathis_weixin_modal').hide(); 135 } 136 return false; 137 }); 138 setTimeout(()=>$('#lji .dati').click(),500); 139 140 //console.log($('.jtico_weixin')); 141 //$('.jtico_weixin').click(); 142 143 //根据url中a的值判断是否需要自动答题 144 if($('#lji span').eq(0).html()>0) 145 { 146 if($.getUrlParam('a')==1) 147 //将config中的自动答题控制打开, 148 config['set']['auto_ans']=1; 149 //localStorage['config']=JSON.stringify(config); 150 setTimeout(()=>$('#lbuts').click(),1000); 151 } 152 153 }); 154 155 156 157 //根据设置进行答题 158 function ans_plus(conf) 159 { 160 if(!conf['dtime']) 161 conf['dtime']=3; 162 163 //关闭自动作答功能 164 //conf['auto_ans']=0; 165 166 var dtime=parseInt(conf['dtime']*1000+500*Math.random());//做题间隔 167 var err=0;//匹配错误指示器 168 169 //基准x,y坐标,伪造回传数据 170 var posx=Math.floor(800+Math.random()*200); 171 var posy=Math.floor(400+Math.random()*140); 172 173 if(dtime<1200) 174 { 175 dtime=1200; 176 } 177 178 //点击交卷按钮时解锁交卷功能 179 $('.W_jiaoquancol').click(function(){$(this).removeClass('W_jiaoquancol')}); 180 //console.log(dtime); 181 if(conf['auto_ans']==1 | conf['sign_ans']==1 |conf['sign_ans_mouseover']==1) 182 { 183 184 //解锁上一题下一题 185 //setInterval(()=>{$('.W_bgcol').removeClass('W_bgcol');},500); 186 187 jQuery('ul.W_ti_ul li').each( 188 function() 189 { 190 //console.log(dtime); 191 var target=''; 192 var li=jQuery(this); 193 var logtxt=''; 194 195 //题号 196 var questnum=li.find('.w_fz18').eq(0).html(); 197 logtxt=questnum+'.'+logtxt; 198 199 //题目类型,单选题,多选题 200 var questtype=li.find('.w_fz18').eq(1).html(); 201 logtxt=logtxt+'〔'+questtype+'〕'; 202 //题目 203 var quest=li.find('.w_fz18').eq(2).html().replace(/[ \r\n ]/g,""); 204 for(i=0;i) 205 { 206 if(anslist[i][0]==quest) 207 { 208 target=anslist[i][1]; 209 logtxt=logtxt+'题目:'+anslist[i][0]+'%c'; 210 break; 211 } 212 } 213 214 //判断是否匹配,如果不匹配就报错 215 if(target=='') 216 { 217 //alert('匹配试题出现错误,请更新版本或联系作者'); 218 err++; 219 //自动作答的话就点击下一题 220 logtxt=logtxt+'题目:'+quest; 221 console.log("%c"+logtxt,'color:red') 222 if(conf['sign_ans']==1) 223 { 224 setTimeout(()=>{$('.w_btn_tab_down').eq(0).click();},questnum*dtime); 225 } 226 return true; 227 } 228 229 //查找答案 230 li.find('label').each( 231 function() 232 { 233 var label=jQuery(this); 234 var labertxt=label.find('sapn').eq(0).html(); 235 labertxt=labertxt.replace(/[ABCD. \r\n ]/g,''); 236 237 if(questtype=='单选题' & target==labertxt) 238 { 239 logtxt=logtxt+'答案:'+labertxt+';'; 240 //标红答案 241 if(conf['sign_ans']==1) 242 label.find('sapn').eq(0).css('color','red'); 243 //鼠标滑过正确答案时选中 244 if(conf['sign_ans_mouseover']==1) 245 { 246 label.find('sapn').eq(0).mouseover(function(){ 247 $(this).click(); 248 $('.W_bgcol').removeClass('W_bgcol'); 249 $('.W_kuan li').eq(questnum-1).addClass('activess'); 250 if(questnum==20) 251 { 252 $('.W_jiaoquancol').removeClass('W_jiaoquancol'); 253 } 254 }) 255 } 256 //自动作答 257 if(conf['auto_ans']==1) 258 { 259 setTimeout(()=>{ 260 261 label.find('sapn').eq(0).click(); 262 263 //解除上一题下一题和题目序号的锁定 264 $('.W_bgcol').removeClass('W_bgcol'); 265 $('.W_kuan li').eq(questnum-1).addClass('activess'); 266 if(questnum==20) 267 { 268 $('.W_jiaoquancol').removeClass('W_jiaoquancol'); 269 } 270 },(questnum-0.5)*dtime); 271 } 272 return false; 273 } 274 else if(questtype=='多选题' & target.indexOf(labertxt)>-1) 275 { 276 //标红答案 277 logtxt=logtxt+'答案:'+labertxt+';'; 278 //标红答案 279 if(conf['sign_ans']==1) 280 label.find('sapn').eq(0).css('color','red'); 281 //鼠标滑过正确答案时选中 282 if(conf['sign_ans_mouseover']==1) 283 { 284 label.find('sapn').eq(0).mouseover(function(){ 285 $(this).click(); 286 $('.W_bgcol').removeClass('W_bgcol'); 287 $('.W_kuan li').eq(questnum-1).addClass('activess'); 288 if(questnum==20) 289 { 290 $('.W_jiaoquancol').removeClass('W_jiaoquancol'); 291 } 292 }) 293 } 294 if(conf['auto_ans']==1) 295 { 296 //自动作答 297 setTimeout(()=>{ 298 label.find('sapn').eq(0).click(); 299 //解除上一题下一题和题目序号的锁定 300 $('.W_bgcol').removeClass('W_bgcol'); 301 $('.W_kuan li').eq(questnum-1).addClass('activess'); 302 if(questnum==20) 303 { 304 $('.W_jiaoquancol').removeClass('W_jiaoquancol'); 305 } 306 307 },(questnum-0.5)*dtime) 309 310 311 } 312 313 314 } 315 316 } 317 ); 318 //自动作答的话就点击下一题 319 if(conf['auto_ans']==1) 320 { 321 setTimeout(()=>{ 322 $('.w_btn_tab_down').eq(0).click(); 323 if("undefined" != typeof ClickButton) 324 ClickButton({'button':0,'clientX':Math.floor(posx+Math.random()*50),'clientY':Math.floor(posy+Math.random()*15)}); 325 },questnum*dtime); 326 } 327 328 console.log(logtxt,'color:red'); 329 } 330 ); 331 } 332 //如果配有匹配错误,则自动交卷 333 if(conf['auto_ans']==1 & err==0) 334 { 335 setTimeout(()=>{$('.jiaojuan').eq(0).click();},51*dtime); 336 } 337 //if(err>0) 338 //alert('有'+err+'道题目匹配出错,请手动作答'); 339 };inject.js则是根据content上级传过来的指令进行动作。
window.anslist为提前写入到程序中的基础题库,减少在线更新时数据通讯量;
因为只能从content接收指令,所以在inject中也保存了一份用户设置;
其中的ans_plus()函数则是整个答题器的核心,也是我最开始写的脚本部分。
逻辑很简单,
1 遍历所有题目标签 2 { 3 找到题干; 4 在题库中匹配题干; 5 如果未匹配到 6 { 7 就用alert弹出提示 8 错题标记+1 9 } 10 如果匹配到 11 { 12 获取所有选项并进行遍历 13 { 14 如果是单选并且选项等于该题目的答案 15 { 16 选中该选项; 17 continu; 18 } 19 如果是多选并且选项在该题目的答案中 20 { 21 选中该选项; 22 } 23 } 24 } 25 } 26 如果没有错误标记则自动交卷;
以上,就是整个答题器中最重要的popup,content 和 inject 中的js代码。
5、几个功能迭代。
从4月份期,为增加作弊难度,考试系统在每天都会增加几道新题。根据观察,是20道题中,在基础题库中抽取18道,在当日新题中抽取2道。
当时的对策是每天更新一次答题器,为了便于答题,答题器的所有用户每天都需要重新下载更新答题器。(群成员数暴涨)
5月13日,我重写了自定义新题的功能,可以批量添加多个新题。这样每天我只需要更新新题字符串,答题器用户将新题字符串导入答题器即可。
5月14日,在重新学了了小茗同学教程之后,实现了在线更新的功能。自定义新题字符串仅仅使用了两天便被淘汰。
服务器端代码:
1 php 2 3 //当前版本号 4 $_v='060303'; 5 6 //当前新题字符串 7 $date=' 8 十九大报告指出,要建立全面规范透明、标准科学、约束有力的预算制度,全面实施()。 9 绩效管理 10 党组的设立,一般应当由()或者本级党的地方委员会审批。党组不得审批设立党组。 11 党的中央委员会 12 '; 13 14 //客户端版本号 15 $v=$_GET['v']; 16 17 //版本号不一致的话,就反馈更新数据 18 if($v<>$_v) 19 //echo '{"date_varsion":"'.$_v.'","update":"'.$date.'"}'; 20 { 21 echo '//'.$_v; 22 echo $date; 23 24 } 25 ?>
服务器端代码很简单,答题器将当前版本号发送至服务器,如果版本号一致则服务器返回空白页,如果不一致则返回新题数据。
数据的第一行是当前数据版本,后面则是题目/答案。依托于重写的自定义新题功能,自动更新非常顺利的实现了。
4月20日,经确认,考试系统加入了防作弊功能,原理是当鼠标点击“上一题”“下一题”或者题号时执行函数ClickButton,保存当前鼠标坐标,在交卷时同时传给服务器。
一开始我考虑的伪造回传数据,但数据经过了一点简单的计算,实在懒得跟他算计,
然后考虑的伪造下一题按钮的点击事件,但通过脚本触发的点击事件没有鼠标坐标信息,
最后忽然发现,我只要每次题目切换时,伪造一个事件(Event)作为参数传给反作弊的模块即可
var posx=Math.floor(800+Math.random()*200);
var posy=Math.floor(400+Math.random()*140);
ClickButton({'button':0,'clientX':Math.floor(posx+Math.random()*50),'clientY':Math.floor(posy+Math.random()*15)});
6、写在最后
这个答题器功能实用,逻辑清晰,难度不算大,非常适合chrome扩展的学习和练手。
当前,本次竞赛的线上部分已经结束,经历了几个月的学习和使用,我也收获的4个微信群,所有群内用户近2000人。最高安装量6000,最高惠及党员80000余人(一人一块钱我就发了!)
最后,还是感谢小茗同学的教程。
以上!
转载于:https://www.cnblogs.com/cifang/p/9133868.html