1、寫在前面 首先感謝小茗同學的文章-【乾貨】Chrome插件(擴展)開發全攻略, 基於這篇入門教程和demo,我才能寫出這款 基於chrome擴展的自動答題器。 git地址: https://gitee.com/cifang/lighthouse_answering_machine.git 2、開 ...
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