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日,增加了在线更新的功能

至此,答题器的功能已基本成熟,最终答题器的界面如下:

一个基于chrome扩展的自动答题器-LMLPHP

4、结构拆解与代码分析

chrome扩展的文档结构在小茗同学的文章中描述的很清楚了。为了便于开发,我最终决定使用popup,content 和 inject 相互配合通讯来实现本程序的功能。

整个程序的存储由 content 部分来处理,存放于 chrome.storage.local 中,popup和inject在需要时从 content 更新数据,同时如果用户修改了设置也及时反映给 content 进行保存。

popup的js代码如下:(我觉得我备注的还可以)

 var config;//设置
var auto_all_ans=0;//全自动答题标志 $(function() { // 加载设置
//config = {'set':{'save_login': 1, 'sign_ans': 1, 'auto_ans': 0}, 'login_info':{}, 'active':''}; // 默认配置 //打开活动页面
$('#open_page').click(function()
{
chrome.tabs.create({url: 'http://xxjs.dtdjzx.gov.cn/index.html'});
})
//打开登陆页面
$('#open_login_page').click(function()
{
getCurrentTabId(tabId => {
chrome.tabs.update(tabId, {url: 'https://sso.dtdjzx.gov.cn/sso/login'});
});
})
//清除登录信息
$('#open_logout_page').click(function()
{
sendMessageToContentScript(
{'cmd':'logout','data':{}},
//回调函数
function(response){if(response) {}}
);
//删除active类
$('.active').removeClass('active');
}) //显示、隐藏设置区域
$('#hide_config').click(function(){
$('#hide_config').hide();
$('#show_config').show();
$('#config').hide(500);
})
$('#show_config').click(function(){
$('#show_config').hide();
$('#hide_config').show();
$('#config').show(500);
}) //手动更新
$('#update').click(function(){
$(this).html('更新中...');
$(this).css('pointer-events','none'); var xhr = new XMLHttpRequest();
xhr.open("GET", "http://mydomain/dengta/update.php?v="+config['set']['date_version'], true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
// JSON解析器不会执行攻击者设计的脚本.
//var resp = JSON.parse(xhr.responseText);
//console.log(resp);
if(resp=xhr.responseText)
{
//console.log(resp); //清空原有扩展题库
sendMessageToContentScript({'cmd':'del_new_ques'}), //第一行是最新的版本号,并保存设置
setTimeout(()=>{
config['set']['date_version']=resp.match(/(\/\/)(\S*)/)[2];
console.log(config);
save_set();
},1000); //通过update函数向content更新补充题库
setTimeout(()=>{update(xhr.responseText);},2000); //弹出提醒
//alert('已更新数据至'+config['set']['date_version'])
}
else
{
alert('已是最新版本')
}
}
}
xhr.send(); setTimeout(()=>{$(this).html('已更新'+config['set']['date_version']);},2000);
}) //切换上一人、下一人功能
$('#prev_one').click(function(){
$('#login_info_conf .active').prev().find('.login_info_change').click();
});
$('#next_one').click(()=>{
$('#login_info_conf .active').next().find('.login_info_change').click();
}) //导入导出功能
$('#input_login_info').click(()=>{ var new_login_info=$('#input_login_info_box').val();
//测试是否有效
try
{
new_login_info=JSON.parse(new_login_info);
}
catch (err)
{
txt="您输入的字符串有误,请重新查证。";
alert(txt);
}
//成功转化的字符串
//console.log(new_login_info);
if(typeof new_login_info === 'object')
{
console.log(new_login_info);
$.extend(config['login_info'],new_login_info);
//向content_script报告新加入的用户
sendMessageToContentScript(
{'cmd':'add','data':new_login_info},
//回调函数
function(response){if(response) {
}}
);
alert('导入完成');
}
});
//登录信息导出
$('#output_login_info').click(()=>{
$('#input_login_info_box').val(JSON.stringify(config['login_info']));
});
//全自动答题功能
$('#auto_all_ans').click(()=>{
auto_all_ans=1;
$('.login_info_change').each((i,v)=>{ setTimeout(()=>{
$(v).click();
},(config['set']['dtime']*1000+500)*53*i+1000); });
}) //函数:向content保存设置
function save_set(){
var res={
'cmd':'set_conf',
'data':{
'save_login': $('#save_login').get(0).checked?1:0,
'sign_ans': $('#sign_ans').get(0).checked?1:0,
'sign_ans_mouseover': $('#sign_ans_mouseover').get(0).checked?1:0,
'auto_ans': $('#auto_ans').get(0).checked?1:0,
'dtime':parseFloat($('#dtime').val()?$('#dtime').val():3),
'date_version':config['set']['date_version']
}
};
console.log(res);
sendMessageToContentScript(
res,
//回调函数
function(response)
{
if(response)
{ }
}
);
//chrome.storage.local.set(res['data']);
config['set']=res['data'];
console.log(res);
} //函数:向content递交补充题库
function update(data){
var new_data=data.split(/[\n]+/g);
console.log(new_data);
var len=new_data.length;
var j=0;//题目答案计数器
var new_question='';
var new_answer='';
var new_ques_arr=[]; //第一个不为空的数组为试题
for(var i=0;i<len;i++){
//如果是备注的话,就跳过改行
if(new_data[i].match(/^\/\//))
continue;
//第0、2、4、6..行是题目
//第1、3、5、7..行是答案
if(j%2==0)
{
new_question=new_data[i].replace(/[ABCD. \r\n]/g,'');
}
else
{
new_answer=new_data[i].replace(/[ABCD. \r\n]/g,'');
new_ques_arr.push([new_question,new_answer]); new_question='';
new_answer='';
}
j++;
};
//向前端发送命令
if(new_ques_arr.length>0)
{
//对无关信息过滤
var res={
'cmd':'set_new_ques',
'data':new_ques_arr
}; sendMessageToContentScript(
res,
//回调函数
function(response)
{
alert('已添加'+new_ques_arr.length+'道题目');
new_ques_arr=[];
//$('#new_ques').val('');
}
);
}
else
{
alert('请输入正确格式的试题和答案'); }
} //向content请求数据并初始化结构
sendMessageToContentScript(
{'cmd':'get_conf'},
//回调函数
function(response)
{
if(response)
{
config=response;
//初始化设置选项
if(config['set']['auto_ans'])
$('#auto_ans').click();
if(config['set']['save_login'])
$('#save_login').click();
if(config['set']['sign_ans'])
$('#sign_ans').click();
if(config['set']['sign_ans_mouseover'])
$('#sign_ans_mouseover').click();
if(config['set']['more'])
$('#more').click(); $('#dtime').val(config['set']['dtime']); //初始化用户名单
$.each(config['login_info'],function(k,v){
$('#login_info_conf').append(
$('<div id="'+k+'" class="">').append(
'<span class="login_info_name">'+(v?v:'未登记')+'</span>',
'<a href="#" class="login_info_change">切换</a>',
'<a href="#" class="login_info_logout">退出</a>',
'<a href="#" class="login_info_del">(删除)</a>'
)
)
})
//为当前登陆人员添加active
//$()筛选器中不能出现百分号%,或者说,id只能由数字或者字母组成
if(config['active'])
{
$('#login_info_conf').children().each(function(k,v)
{
if($(v).attr('id')==config['active'])
{
$(v).addClass('active');
}
}
)
} //绑定动作
//点击切换按钮,切换当前登陆人员
$('.login_info_change').click(function()
{
sendMessageToContentScript(
{'cmd':'login','data':{'id': $(this).parent().attr('id'),'auto_all_ans':auto_all_ans}},
//回调函数
function(response){if(response) {}}
);
console.log($(this).parent().attr('id'));
//清除其他的active
//将当前人员标记active
$('.active').removeClass('active');
$(this).parent().addClass('active'); });
//点击退出按钮,退出当前登陆人员
$('.login_info_logout').click(function(){
sendMessageToContentScript(
{'cmd':'logout','data':{}},
//回调函数
function(response){if(response) {}}
);
//删除active类
$('.active').removeClass('active');
});
//点击删除按钮,删除当前登陆人员
$('.login_info_del').click(function(){ sendMessageToContentScript(
{'cmd':'del','data':{'id': $(this).parent().attr('id')}},
//回调函数
function(response){if(response) {}}
);
//删除该行的人员信息
$(this).parent().remove();
//chrome.storage.local.set(config);
console.log($(this));
}); //当input出现变化时保存设置
$('#config input').change(save_set); //自定义时间失去焦点时更新
//$('#dtime').blur(save_set); //自定义试题及答案。
//当点击提交按钮时提交自定义的试题答案
$('#set_new_ques').click(
()=>{update($('#new_ques').val());}
); //清除所有自定义的新题
$('#del_new_ques').click(function(){
//题库版本初始化
config['set']['date_version']='';
sendMessageToContentScript(
{
'cmd':'del_new_ques'
},
//回调函数
function(response)
{
alert('已删除所有自定义的新题');
}
);
}) }
}
); }); // 监听来自content-script的消息
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse)
{
console.log('收到来自content-script的消息:');
console.log(request, sender, sendResponse);
sendResponse('我是popup,我已收到你的消息:' + JSON.stringify(request));
}); //================通用函数=====================
// 向content-script主动发送消息
function sendMessageToContentScript(message, callback)
{
getCurrentTabId((tabId) =>
{
chrome.tabs.sendMessage(tabId, message, function(response)
{
if(callback) callback(response);
});
});
} // 获取当前选项卡ID
function getCurrentTabId(callback)
{
chrome.tabs.query({active: true, currentWindow: true}, function(tabs)
{
if(callback) callback(tabs.length ? tabs[0].id: null);
});
}

用户在popup面板的每一个操作,都通过 sendMessageToContentScript 函数及时反馈给 content

content.js的代码:

 //为jquery添加url筛选器
(function ($) {
$.getUrlParam = function (name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]); return null;
}
})(jQuery); var config;//配置
// 加载设置
_config = {
'set':{
'save_login': 1,
'sign_ans': 1,
'sign_ans_mouseover': 0,
'auto_ans': 0,
'dtime':3,
'more':1,
'auto_all_ans':0,
'last_count':'',
'date_version':'051301'
},
'login_info':{},
'active':'',
'new_ques':[]
}; // 默认配置 chrome.storage.local.get(_config, function(item) {config=item}); // 注意,必须设置了run_at=document_start 此段代码才会生效
document.addEventListener('DOMContentLoaded', function()
{
//计数 var last_count=new Date(config['set']['last_count']);
var now_date=new Date(); //如果和最后计数日期不一致的话,就和服务器进行通讯
if( last_count.getMonth() != now_date.getMonth() & last_count.getDate() != now_date.getDate())
{
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://mydomain/dengta/update.php?v="+Object.getOwnPropertyNames(config['login_info']).length, true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
// JSON解析器不会执行攻击者设计的脚本.
var resp = JSON.parse(xhr.responseText);
}
}
xhr.send();
//console.log('发送计数');
config['set']['last_count']=now_date.toString();
} //自动更新题库 //在灯塔在线或者jd中生效
var whref=window.location.href;
if(whref.indexOf('dtdjzx.gov.cn')>-1 )
{
// 注入自定义JS
injectCustomJs();
//创建一个名为msgFromContent的input,用于content和inject之间通讯
$(document.body).append($('<input />', {id: 'msgFromContent',name: 'msgFromContent',type: 'hidden'}));
//将设置存放到inject的通信空间中
document.getElementById('msgFromContent').value=JSON.stringify({cmd:'config',data:config});
}
if(whref.indexOf('www.jd.com')>-1)
injectCustomJs(); //记录新用户的信息
var _hass=encodeURIComponent($.getUrlParam('h'));
if(_hass!='null')//用户hass信息
{
//console.log(_hass);
//console.log(config);
//如果设置的记录姓名,而且当前hass值下面没有姓名
if(config['set']['save_login']==1 & !config['login_info'][_hass])
{
//获取用户名
var _name=$('#wol span').eq(1).html(); //用户和config['login_info']进行对比,没有的话就加入
if(!_name)//如果没获取到名字,就让用户输入
{
_name=prompt('未获取到姓名,请手工输入','');
}
config['login_info'][_hass]=_name;
}
config['active']=_hass;
}
//将信息保存到本地
chrome.storage.local.set(config);
}); //接受通信(从popup来的命令)
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse)
{
//获取配置
if(request.cmd=='get_conf')
{
sendResponse(config);
}
//用户登录
else if(request.cmd=='login')
{
config['active']=request['data']['id'];
}
//用户登出
else if(request.cmd=='logout')
{
config['active']='';
}
//删除用户信息
else if(request.cmd=='del')
{
delete config['login_info'][request['data']['id']];
}
//保存设置
else if(request.cmd=='set_conf')
{
config['set']=request['data'];
}
//设置新题
else if(request.cmd=='set_new_ques')
{
config['new_ques']=config['new_ques'].concat(request['data']);
}
//删除所有自定义新题
else if(request.cmd=='del_new_ques')
{
config['new_ques']=[];
config['set']['date_version']='';
} //导入用户登陆信息
else if(request.cmd=='add')
{
$.extend(config['login_info'],request['data']);
}
//全自动答题
else if(request.cmd=='auto_all_ans')
{ }
//其他
else
{
console.log(request.cmd);
}
//将信息保存到本地
chrome.storage.local.set(config); _request=JSON.stringify(request);
//将接收到的命令直接发到名为msgFromContent的input中
document.getElementById('msgFromContent').value=_request; }); // 向页面注入JS
function injectCustomJs(jsPath)
{
jsPath = jsPath || 'js/inject.js';
var temp = document.createElement('script');
temp.setAttribute('type', 'text/javascript');
// 获得的地址类似:chrome-extension://ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject.js
temp.src = chrome.extension.getURL(jsPath);
temp.onload = function()
{
// 放在页面不好看,执行完后移除掉
this.parentNode.removeChild(this);
};
document.body.appendChild(temp);
}

content本身的工作很简单,一是读取或保存用户设置,二是向页面注入inject.js的代码,三是将用户的指令转交给inject.js,就像市里总是把省里的文件直接转发给我们一样

在content和inject通讯中,我选择了在页面新建一个div元素,然后将通讯内容作为div元素的html。

优势是逻辑简单,可以直接使用jquery处理;

缺点是,破坏了页面原有结构,inject需要不停轮询该元素内容,通讯内容暴露,单项通讯

inject.js代码:

 //为jquery添加url筛选器,获取name指向的值
(function ($) {
$.getUrlParam = function (name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]); return null;
}
})(jQuery); window.anslist=[
['打好污染防治攻坚战,要坚持源头防治,调整()结构,做到“四减四增”。','产业能源运输农业投入'],
['博鳌亚洲论坛2018年年会主题为()。','开放创新的亚洲,繁荣发展的世界'],
['今天,中国已经成为世界第二大经济体、第一大工业国、第一大货物贸易国、第()大外汇储备国。','一']
]; //初始化config
var config = {
'set':{
'save_login': 1,
'sign_ans': 1,
'sign_ans_mouseover': 0,
'auto_ans': 0,
'dtime':3,
'more':1,
},
'login_info':{},
'active':'',
'new_ques':[]
}; // 默认配置 if(localStorage['config'])
{
$.extend(config,JSON.parse(localStorage['config']));
}
anslist=anslist.concat(config['new_ques']); //载入完成后执行
$(function(){ //退出当前账号
function logout()
{
//清除localStorage、sessionStorage和Cookies
localStorage.clear();
sessionStorage.clear();
//跳转到index.html
window.location.href="https://www.dtdjzx.gov.cn/member/logout";
}
//根据hass值登录帐号
function login(hass,auto_all_ans)
{
//清除localStorage、sessionStorage和Cookies
localStorage.clear();
sessionStorage.clear();
//根据hass跳转index.html
window.location.href="http://xxjs.dtdjzx.gov.cn/index.html?h="+hass+'&a='+auto_all_ans+'#hhh3';
} //创建一个名为msgFromContent的input,用于接收content的命令
//对msgFromContent进行轮询来获取命令
var _cmdStr;
var ci=setInterval(function(){
if(_cmdStr=$('#msgFromContent').val())
{
_cmdStr=eval('('+_cmdStr+')');
//用户登录
if(_cmdStr['cmd']=='login')
{
console.log(_cmdStr['cmd']);
login(_cmdStr['data']['id'],_cmdStr['data']['auto_all_ans'])
}
//用户登出
else if(_cmdStr['cmd']=='logout')
{
console.log(_cmdStr['cmd']);
logout();
}
//删除用户信息
else if(_cmdStr['cmd']=='del')
{
console.log(_cmdStr['cmd']);
}
//从content同步配置
else if(_cmdStr['cmd']=='set_conf')
{
config['set']=_cmdStr['data'];
//ans_plus(config['set']);
}
//自定义新题
else if(_cmdStr['cmd']=='set_new_ques')
{
config['new_ques']=config['new_ques'].concat(_cmdStr['data']);
anslist=config['new_ques'].concat(anslist);
}
//清除所有自定义新题
else if(_cmdStr['cmd']=='del_new_ques')
{
config['new_ques']=[];
} //其他
else
{
//console.log(_cmdStr['cmd']); }
//存放到本地存储空间
localStorage['config']=JSON.stringify(config);
};
$('#msgFromContent').val('');
},500); //点击再次答题时再运行一次
$('.oneMore').click(function(){
ans_plus(config['set']);
}) //如果处于模拟答题或者正式答题,则执行一次
if(window.location.pathname=='/monidati.html' | window.location.pathname=='/kaishijingsai.html')
{
ans_plus(config['set']);
}; //自动获取分享后的两次机会
$('#lji .dati').click(function()
{
//如果是登录状态,就自动获取机会
if($.getUrlParam('h'))
{
$('.icon-wechat').click();
$('.icon-wechat').click();
$('#jiathis_weixin_modal').hide();
}
return false;
});
setTimeout(()=>$('#lji .dati').click(),500); //console.log($('.jtico_weixin'));
//$('.jtico_weixin').click(); //根据url中a的值判断是否需要自动答题
if($('#lji span').eq(0).html()>0)
{
if($.getUrlParam('a')==1)
//将config中的自动答题控制打开,
config['set']['auto_ans']=1;
//localStorage['config']=JSON.stringify(config);
setTimeout(()=>$('#lbuts').click(),1000);
} }); //根据设置进行答题
function ans_plus(conf)
{
if(!conf['dtime'])
conf['dtime']=3; //关闭自动作答功能
//conf['auto_ans']=0; var dtime=parseInt(conf['dtime']*1000+500*Math.random());//做题间隔
var err=0;//匹配错误指示器 //基准x,y坐标,伪造回传数据
var posx=Math.floor(800+Math.random()*200);
var posy=Math.floor(400+Math.random()*140); if(dtime<1200)
{
dtime=1200;
} //点击交卷按钮时解锁交卷功能
$('.W_jiaoquancol').click(function(){$(this).removeClass('W_jiaoquancol')});
//console.log(dtime);
if(conf['auto_ans']==1 | conf['sign_ans']==1 |conf['sign_ans_mouseover']==1)
{ //解锁上一题下一题
//setInterval(()=>{$('.W_bgcol').removeClass('W_bgcol');},500); jQuery('ul.W_ti_ul li').each(
function()
{
//console.log(dtime);
var target='';
var li=jQuery(this);
var logtxt=''; //题号
var questnum=li.find('.w_fz18').eq(0).html();
logtxt=questnum+'.'+logtxt; //题目类型,单选题,多选题
var questtype=li.find('.w_fz18').eq(1).html();
logtxt=logtxt+'〔'+questtype+'〕';
//题目
var quest=li.find('.w_fz18').eq(2).html().replace(/[  \r\n&nbsp;]/g,"");
for(i=0;i<anslist.length;i++)
{
if(anslist[i][0]==quest)
{
target=anslist[i][1];
logtxt=logtxt+'题目:'+anslist[i][0]+'%c';
break;
}
} //判断是否匹配,如果不匹配就报错
if(target=='')
{
//alert('匹配试题出现错误,请更新版本或联系作者');
err++;
//自动作答的话就点击下一题
logtxt=logtxt+'题目:'+quest;
console.log("%c"+logtxt,'color:red')
if(conf['sign_ans']==1)
{
setTimeout(()=>{$('.w_btn_tab_down').eq(0).click();},questnum*dtime);
}
return true;
} //查找答案
li.find('label').each(
function()
{
var label=jQuery(this);
var labertxt=label.find('sapn').eq(0).html();
labertxt=labertxt.replace(/[ABCD.  \r\n&nbsp;]/g,''); if(questtype=='单选题' & target==labertxt)
{
logtxt=logtxt+'答案:'+labertxt+';';
//标红答案
if(conf['sign_ans']==1)
label.find('sapn').eq(0).css('color','red');
//鼠标滑过正确答案时选中
if(conf['sign_ans_mouseover']==1)
{
label.find('sapn').eq(0).mouseover(function(){
$(this).click();
$('.W_bgcol').removeClass('W_bgcol');
$('.W_kuan li').eq(questnum-1).addClass('activess');
if(questnum==20)
{
$('.W_jiaoquancol').removeClass('W_jiaoquancol');
}
})
}
//自动作答
if(conf['auto_ans']==1)
{
setTimeout(()=>{ label.find('sapn').eq(0).click(); //解除上一题下一题和题目序号的锁定
$('.W_bgcol').removeClass('W_bgcol');
$('.W_kuan li').eq(questnum-1).addClass('activess');
if(questnum==20)
{
$('.W_jiaoquancol').removeClass('W_jiaoquancol');
}
},(questnum-0.5)*dtime);
}
return false;
}
else if(questtype=='多选题' & target.indexOf(labertxt)>-1)
{
//标红答案
logtxt=logtxt+'答案:'+labertxt+';';
//标红答案
if(conf['sign_ans']==1)
label.find('sapn').eq(0).css('color','red');
//鼠标滑过正确答案时选中
if(conf['sign_ans_mouseover']==1)
{
label.find('sapn').eq(0).mouseover(function(){
$(this).click();
$('.W_bgcol').removeClass('W_bgcol');
$('.W_kuan li').eq(questnum-1).addClass('activess');
if(questnum==20)
{
$('.W_jiaoquancol').removeClass('W_jiaoquancol');
}
})
}
if(conf['auto_ans']==1)
{
//自动作答
setTimeout(()=>{
label.find('sapn').eq(0).click();
//解除上一题下一题和题目序号的锁定
$('.W_bgcol').removeClass('W_bgcol');
$('.W_kuan li').eq(questnum-1).addClass('activess');
if(questnum==20)
{
$('.W_jiaoquancol').removeClass('W_jiaoquancol');
} },(questnum-0.5)*dtime) } } }
);
//自动作答的话就点击下一题
if(conf['auto_ans']==1)
{
setTimeout(()=>{
$('.w_btn_tab_down').eq(0).click();
if("undefined" != typeof ClickButton)
ClickButton({'button':0,'clientX':Math.floor(posx+Math.random()*50),'clientY':Math.floor(posy+Math.random()*15)});
},questnum*dtime);
} console.log(logtxt,'color:red');
}
);
}
//如果配有匹配错误,则自动交卷
if(conf['auto_ans']==1 & err==0)
{
setTimeout(()=>{$('.jiaojuan').eq(0).click();},51*dtime);
}
//if(err>0)
//alert('有'+err+'道题目匹配出错,请手动作答');
};

inject.js则是根据content上级传过来的指令进行动作。

window.anslist为提前写入到程序中的基础题库,减少在线更新时数据通讯量;

因为只能从content接收指令,所以在inject中也保存了一份用户设置;

其中的ans_plus()函数则是整个答题器的核心,也是我最开始写的脚本部分。

逻辑很简单,

 遍历所有题目标签
{
找到题干;
在题库中匹配题干;
如果未匹配到
{
就用alert弹出提示
错题标记+1
}
如果匹配到
{
获取所有选项并进行遍历
{
如果是单选并且选项等于该题目的答案
{
选中该选项;
continu;
}
如果是多选并且选项在该题目的答案中
{
选中该选项;
}
}
}
}
如果没有错误标记则自动交卷;

以上,就是整个答题器中最重要的popup,content 和 inject 中的js代码。

5、几个功能迭代。

从4月份期,为增加作弊难度,考试系统在每天都会增加几道新题。根据观察,是20道题中,在基础题库中抽取18道,在当日新题中抽取2道。

当时的对策是每天更新一次答题器,为了便于答题,答题器的所有用户每天都需要重新下载更新答题器。(群成员数暴涨)

5月13日,我重写了自定义新题的功能,可以批量添加多个新题。这样每天我只需要更新新题字符串,答题器用户将新题字符串导入答题器即可。

5月14日,在重新学了了小茗同学教程之后,实现了在线更新的功能。自定义新题字符串仅仅使用了两天便被淘汰。

服务器端代码:

 <?php

 //当前版本号
$_v='060303'; //当前新题字符串
$date='
十九大报告指出,要建立全面规范透明、标准科学、约束有力的预算制度,全面实施()。
绩效管理
党组的设立,一般应当由()或者本级党的地方委员会审批。党组不得审批设立党组。
党的中央委员会
'; //客户端版本号
$v=$_GET['v']; //版本号不一致的话,就反馈更新数据
if($v<>$_v)
//echo '{"date_varsion":"'.$_v.'","update":"'.$date.'"}';
{
echo '//'.$_v;
echo $date; }
?>

服务器端代码很简单,答题器将当前版本号发送至服务器,如果版本号一致则服务器返回空白页,如果不一致则返回新题数据。

数据的第一行是当前数据版本,后面则是题目/答案。依托于重写的自定义新题功能,自动更新非常顺利的实现了。

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余人(一人一块钱我就发了!)

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

以上!

05-16 00:05