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<len;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                         $(\'<div id="\'+k+\'" class="">\').append(
261                             \'<span class="login_info_name">\'+(v?v:\'未登记\')+\'</span>\',
262                             \'<a  href="#" class="login_info_change">切换</a>\',
263                             \'<a  href="#" class="login_info_logout">退出</a>\',
264                             \'<a  href="#" class="login_info_del">(删除)</a>\'
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($(\'<input />\', {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&nbsp;]/g,"");
204                 for(i=0;i<anslist.length;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&nbsp;]/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余人(一人一块钱我就发了!)

最后,还是感谢小茗同学的教程。

以上!

版权声明:本文为cifang原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/cifang/p/9133868.html