一个基于chrome扩展的自动答题器
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 ]/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 ]/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余人(一人一块钱我就发了!)
最后,还是感谢小茗同学的教程。
以上!