background.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. /**
  2. * FeJson后台运行程序
  3. * @author zhaoxianlie
  4. */
  5. import MSG_TYPE from '../static/js/common.js';
  6. import Settings from '../options/settings.js';
  7. import Menu from './menu.js';
  8. import Awesome from './awesome.js';
  9. import InjectTools from './inject-tools.js';
  10. import Monkey from './monkey.js';
  11. let BgPageInstance = (function () {
  12. let FeJson = {
  13. notifyTimeoutId: -1
  14. };
  15. // 黑名单页面
  16. let blacklist = [
  17. /^https:\/\/chrome\.google\.com/
  18. ];
  19. /**
  20. * 文本格式,可以设置一个图标和标题
  21. * @param {Object} options
  22. * @config {string} type notification的类型,可选值:html、text
  23. * @config {string} icon 图标
  24. * @config {string} title 标题
  25. * @config {string} message 内容
  26. */
  27. let notifyText = function (options) {
  28. let notifyId = 'FeJson-notify-id';
  29. clearTimeout(FeJson.notifyTimeoutId);
  30. if (options.closeImmediately) {
  31. return chrome.notifications.clear(notifyId);
  32. }
  33. if (!options.icon) {
  34. options.icon = "static/img/fe-48.png";
  35. }
  36. if (!options.title) {
  37. options.title = "温馨提示";
  38. }
  39. chrome.notifications.create(notifyId, {
  40. type: 'basic',
  41. title: options.title,
  42. iconUrl: chrome.runtime.getURL(options.icon),
  43. message: options.message
  44. });
  45. FeJson.notifyTimeoutId = setTimeout(() => {
  46. chrome.notifications.clear(notifyId);
  47. }, parseInt(options.autoClose || 3000, 10));
  48. };
  49. // 像页面注入css脚本
  50. let _injectContentCss = function(tabId,toolName,isDevTool){
  51. if(isDevTool){
  52. Awesome.getContentScript(toolName, true)
  53. .then(css => {
  54. InjectTools.inject(tabId, { css })
  55. });
  56. }else{
  57. InjectTools.inject(tabId, {files: [`${toolName}/content-script.css`]});
  58. }
  59. };
  60. // 往当前页面直接注入脚本,不再使用content-script的配置了
  61. let _injectContentScripts = function (tabId) {
  62. // FH工具脚本注入
  63. Awesome.getInstalledTools().then(tools => {
  64. // 注入js
  65. let jsTools = Object.keys(tools)
  66. .filter(tool => !tools[tool]._devTool
  67. && (tools[tool].contentScriptJs || tools[tool].contentScript));
  68. let jsCodes = [];
  69. jsTools.forEach((t, i) => {
  70. let func = `window['${t.replace(/-/g, '')}ContentScript']`;
  71. jsCodes.push(`(()=>{let func=${func};func&&func();})()`);
  72. });
  73. let jsFiles = jsTools.map(tool => `${tool}/content-script.js`);
  74. InjectTools.inject(tabId, {files: jsFiles,js: jsCodes.join(';')});
  75. });
  76. // 其他开发者自定义工具脚本注入======For FH DevTools
  77. Awesome.getInstalledTools().then(tools => {
  78. let list = Object.keys(tools).filter(tool => tools[tool]._devTool);
  79. // 注入js脚本
  80. list.filter(tool => (tools[tool].contentScriptJs || tools[tool].contentScript))
  81. .map(tool => Awesome.getContentScript(tool).then(js => {
  82. InjectTools.inject(tabId, { js });
  83. }));
  84. });
  85. };
  86. /**
  87. * 动态运行工具
  88. * @param configs
  89. * @config tool 工具名称
  90. * @config withContent 默认携带的内容
  91. * @config query 请求参数
  92. * @config noPage 无页面模式
  93. * @constructor
  94. */
  95. chrome.DynamicToolRunner = async function (configs) {
  96. let tool = configs.tool || configs.page;
  97. let withContent = configs.withContent;
  98. let activeTab = null;
  99. let query = configs.query;
  100. // 如果是noPage模式,则表名只完成content-script的工作,直接发送命令即可
  101. if (configs.noPage) {
  102. let toolFunc = tool.replace(/-/g, '');
  103. chrome.tabs.query({active: true, currentWindow: true}, tabs => {
  104. let found = tabs.some(tab => {
  105. if (/^(http(s)?|file):\/\//.test(tab.url) && blacklist.every(reg => !reg.test(tab.url))) {
  106. let codes = `window['${toolFunc}NoPage'] && window['${toolFunc}NoPage'](${JSON.stringify(tab)});`;
  107. InjectTools.inject(tab.id, {js: codes});
  108. return true;
  109. }
  110. return false;
  111. });
  112. if (!found) {
  113. notifyText({
  114. message: '抱歉,此工具无法在当前页面使用!'
  115. });
  116. }
  117. });
  118. return;
  119. }
  120. chrome.tabs.query({currentWindow: true}, function (tabs) {
  121. activeTab = tabs.filter(tab => tab.active)[0];
  122. // 如果是二维码工具,且没有传入内容,则使用当前页面的URL
  123. if (tool === 'qr-code' && !withContent && activeTab) {
  124. withContent = activeTab.url;
  125. }
  126. Settings.getOptions((opts) => {
  127. let isOpened = false;
  128. let tabId;
  129. // 允许在新窗口打开
  130. if (String(opts['FORBID_OPEN_IN_NEW_TAB']) === 'true') {
  131. let reg = new RegExp("^chrome.*\\/" + tool + "\\/index.html" + (query ? "\\?" + query : '') + "$", "i");
  132. for (let i = 0, len = tabs.length; i < len; i++) {
  133. if (reg.test(tabs[i].url)) {
  134. isOpened = true;
  135. tabId = tabs[i].id;
  136. break;
  137. }
  138. }
  139. }
  140. if (!isOpened) {
  141. let url = `/${tool}/index.html` + (query ? "?" + query : '');
  142. chrome.tabs.create({
  143. url,
  144. active: true
  145. }).then(tab => { FeJson[tab.id] = { content: withContent }; });
  146. } else {
  147. chrome.tabs.update(tabId, {highlighted: true}).then(tab => {
  148. FeJson[tab.id] = { content: withContent };
  149. chrome.tabs.reload(tabId);
  150. });
  151. }
  152. });
  153. });
  154. };
  155. /**
  156. * 动态在icon处显示提示
  157. * @param tips
  158. * @private
  159. */
  160. let _animateTips = (tips) => {
  161. setTimeout(() => {
  162. chrome.action.setBadgeText({text: tips});
  163. setTimeout(() => {
  164. chrome.action.setBadgeText({text: ''});
  165. }, 2000);
  166. }, 3000);
  167. };
  168. /**
  169. * 插件图标点击后的默认动作
  170. * @param request
  171. * @param sender
  172. * @param callback
  173. */
  174. let browserActionClickedHandler = function (request, sender, callback) {
  175. chrome.DynamicToolRunner({
  176. tool: MSG_TYPE.JSON_FORMAT
  177. });
  178. };
  179. /**
  180. * 更新browser action的点击动作
  181. * @param action install / upgrade / offload
  182. * @param showTips 是否notify
  183. * @param menuOnly 只管理Menu
  184. * @private
  185. */
  186. let _updateBrowserAction = function (action, showTips, menuOnly) {
  187. if (!menuOnly) {
  188. // 如果有安装过工具,则显示Popup模式
  189. Awesome.getInstalledTools().then(tools => {
  190. if (Object.keys(tools).length > 1) {
  191. chrome.action.setPopup({ popup: '/popup/index.html' });
  192. } else {
  193. // 删除popup page
  194. chrome.action.setPopup({ popup: '' });
  195. // 否则点击图标,直接打开页面
  196. if (!chrome.action.onClicked.hasListener(browserActionClickedHandler)) {
  197. chrome.action.onClicked.addListener(browserActionClickedHandler);
  198. }
  199. }
  200. });
  201. if (action === 'offload') {
  202. _animateTips('-1');
  203. } else if(!!action) {
  204. _animateTips('+1');
  205. }
  206. } else {
  207. // 重绘菜单
  208. Menu.rebuild();
  209. }
  210. if (showTips) {
  211. let actionTxt = '';
  212. switch (action) {
  213. case 'install':
  214. actionTxt = '工具已「安装」成功,并已添加到弹出下拉列表,点击FeHelper图标可正常使用!';
  215. break;
  216. case 'offload':
  217. actionTxt = '工具已「卸载」成功,并已从弹出下拉列表中移除!';
  218. break;
  219. case 'menu-install':
  220. actionTxt = '已将此工具快捷方式加入到「右键菜单」中!';
  221. break;
  222. case 'menu-offload':
  223. actionTxt = '已将此工具快捷方式从「右键菜单」中移除!';
  224. break;
  225. default:
  226. actionTxt = '恭喜,操作成功!';
  227. }
  228. notifyText({
  229. message: actionTxt,
  230. autoClose: 2500
  231. });
  232. }
  233. };
  234. // 捕获当前页面可视区域
  235. let _captureVisibleTab = function (callback) {
  236. chrome.tabs.captureVisibleTab(null, {format: 'png', quality: 100}, uri => {
  237. callback && callback(uri);
  238. });
  239. };
  240. let _addScreenShotByPages = function(params,callback){
  241. chrome.tabs.captureVisibleTab(null, {format: 'png', quality: 100}, uri => {
  242. callback({ params, uri });
  243. });
  244. };
  245. let _showScreenShotResult = function(data){
  246. // 确保截图数据完整有效
  247. if (!data || !data.screenshots || !data.screenshots.length) {
  248. return;
  249. }
  250. chrome.DynamicToolRunner({
  251. tool: 'screenshot',
  252. withContent: data
  253. });
  254. };
  255. let _colorPickerCapture = function(params) {
  256. chrome.tabs.query({active: true, currentWindow: true}, function (tabs) {
  257. chrome.tabs.captureVisibleTab(null, {format: 'png'}, function (dataUrl) {
  258. let js = `window.colorpickerNoPage(${JSON.stringify({
  259. setPickerImage: true,
  260. pickerImage: dataUrl
  261. })})`;
  262. InjectTools.inject(tabs[0].id, { js });
  263. });
  264. });
  265. };
  266. let _codeBeautify = function(params){
  267. if (['javascript', 'css'].includes(params.fileType)) {
  268. Awesome.StorageMgr.get('JS_CSS_PAGE_BEAUTIFY').then(val => {
  269. if(val !== '0') {
  270. let js = `window._codebutifydetect_('${params.fileType}')`;
  271. InjectTools.inject(params.tabId, { js });
  272. }
  273. });
  274. }
  275. };
  276. /**
  277. * 接收来自content_scripts发来的消息
  278. */
  279. let _addExtensionListener = function () {
  280. _updateBrowserAction();
  281. chrome.runtime.onMessage.addListener(function (request, sender, callback) {
  282. // 如果发生了错误,就啥都别干了
  283. if (chrome.runtime.lastError) {
  284. return true;
  285. }
  286. // 动态安装工具或者卸载工具,需要更新browserAction
  287. if (request.type === MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD) {
  288. _updateBrowserAction(request.action, request.showTips, request.menuOnly);
  289. callback && callback();
  290. }
  291. // 截屏
  292. else if (request.type === MSG_TYPE.CAPTURE_VISIBLE_PAGE) {
  293. _captureVisibleTab(callback);
  294. }
  295. // 直接处理content-script.js中的截图请求
  296. else if (request.type === 'fh-screenshot-capture-visible') {
  297. _captureVisibleTab(callback);
  298. }
  299. // 打开动态工具页面
  300. else if (request.type === MSG_TYPE.OPEN_DYNAMIC_TOOL) {
  301. chrome.DynamicToolRunner(request);
  302. callback && callback();
  303. }
  304. // 打开其他页面
  305. else if (request.type === MSG_TYPE.OPEN_PAGE) {
  306. chrome.DynamicToolRunner({
  307. tool: request.page
  308. });
  309. callback && callback();
  310. }
  311. // 任何事件,都可以通过这个钩子来完成
  312. else if (request.type === MSG_TYPE.DYNAMIC_ANY_THING) {
  313. switch(request.thing){
  314. case 'save-options':
  315. notifyText({
  316. message: '配置修改已生效,请继续使用!',
  317. autoClose: 2000
  318. });
  319. break;
  320. case 'trigger-screenshot':
  321. // 处理从popup触发的截图请求
  322. if (request.tabId) {
  323. _triggerScreenshotTool(request.tabId);
  324. } else {
  325. chrome.DynamicToolRunner({
  326. tool: 'screenshot',
  327. noPage: true
  328. });
  329. }
  330. break;
  331. case 'request-jsonformat-options':
  332. Awesome.StorageMgr.get(request.params).then(result => {
  333. Object.keys(result).forEach(key => {
  334. if (['MAX_JSON_KEYS_NUMBER', 'JSON_FORMAT_THEME'].includes(key)) {
  335. result[key] = parseInt(result[key]);
  336. } else {
  337. result[key] = (result[key] !== 'false');
  338. }
  339. });
  340. callback && callback(result);
  341. });
  342. return true; // 这个返回true是非常重要的!!!要不然callback会拿不到结果
  343. case 'save-jsonformat-options':
  344. Awesome.StorageMgr.set(request.params).then(() => {
  345. callback && callback();
  346. });
  347. return true;
  348. case 'toggle-jsonformat-options':
  349. Awesome.StorageMgr.get('JSON_TOOL_BAR_ALWAYS_SHOW').then(result => {
  350. let show = result !== false;
  351. Awesome.StorageMgr.set('JSON_TOOL_BAR_ALWAYS_SHOW',!show).then(() => {
  352. callback && callback(!show);
  353. });
  354. });
  355. return true; // 这个返回true是非常重要的!!!要不然callback会拿不到结果
  356. case 'code-beautify':
  357. _codeBeautify(request.params);
  358. break;
  359. case 'close-beautify':
  360. Awesome.StorageMgr.set('JS_CSS_PAGE_BEAUTIFY',0);
  361. break;
  362. case 'qr-decode':
  363. chrome.DynamicToolRunner({
  364. withContent: request.params.uri,
  365. tool: 'qr-code',
  366. query: `mode=decode`
  367. });
  368. break;
  369. case 'request-page-content':
  370. request.params = FeJson[request.tabId];
  371. delete FeJson[request.tabId];
  372. break;
  373. case 'set-page-timing-data':
  374. chrome.DynamicToolRunner({
  375. tool: 'page-timing',
  376. withContent: request.wpoInfo
  377. });
  378. break;
  379. case 'color-picker-capture':
  380. _colorPickerCapture(request.params);
  381. break;
  382. case 'add-screen-shot-by-pages':
  383. _addScreenShotByPages(request.params,callback);
  384. return true;
  385. case 'page-screenshot-done':
  386. _showScreenShotResult(request.params);
  387. break;
  388. case 'request-monkey-start':
  389. Monkey.start(request.params);
  390. break;
  391. case 'inject-content-css':
  392. _injectContentCss(sender.tab.id,request.tool,!!request.devTool);
  393. break;
  394. case 'open-options-page':
  395. chrome.runtime.openOptionsPage();
  396. break;
  397. }
  398. callback && callback(request.params);
  399. } else {
  400. callback && callback();
  401. }
  402. return true;
  403. });
  404. // 每开一个窗口,都向内容脚本注入一个js,绑定tabId
  405. chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
  406. if (String(changeInfo.status).toLowerCase() === "complete") {
  407. if(/^(http(s)?|file):\/\//.test(tab.url) && blacklist.every(reg => !reg.test(tab.url))){
  408. InjectTools.inject(tabId, { js: `window.__FH_TAB_ID__=${tabId};` });
  409. _injectContentScripts(tabId);
  410. }
  411. }
  412. });
  413. // 安装与更新
  414. chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
  415. switch (reason) {
  416. case 'install':
  417. chrome.runtime.openOptionsPage();
  418. break;
  419. case 'update':
  420. _animateTips('+++1');
  421. if (previousVersion === '2019.12.2415') {
  422. notifyText({
  423. message: '历尽千辛万苦,FeHelper已升级到最新版本,可以到插件设置页去安装旧版功能了!',
  424. autoClose: 5000
  425. });
  426. }
  427. // 从V2020.02.1413版本开始,本地的数据存储大部分迁移至chrome.storage.local
  428. // 这里需要对老版本升级过来的情况进行强制数据迁移
  429. let getAbsNum = num => parseInt(num.split(/\./).map(n => n.padStart(4, '0')).join(''), 10);
  430. // let preVN = getAbsNum(previousVersion);
  431. // let minVN = getAbsNum('2020.02.1413');
  432. // if (preVN < minVN) {
  433. // Awesome.makeStorageUnlimited();
  434. // setTimeout(() => chrome.runtime.reload(), 1000 * 5);
  435. // }
  436. break;
  437. }
  438. });
  439. // 卸载
  440. chrome.runtime.setUninstallURL(chrome.runtime.getManifest().homepage_url);
  441. };
  442. /**
  443. * 检查插件更新
  444. * @private
  445. */
  446. let _checkUpdate = function () {
  447. setTimeout(() => {
  448. chrome.runtime.requestUpdateCheck((status) => {
  449. if (status === "update_available") {
  450. chrome.runtime.reload();
  451. }
  452. });
  453. }, 1000 * 30);
  454. };
  455. /**
  456. * 初始化
  457. */
  458. let _init = function () {
  459. _checkUpdate();
  460. _addExtensionListener();
  461. // 添加截图工具直接命令 - 通过右键菜单触发
  462. chrome.contextMenus.onClicked.addListener((info, tab) => {
  463. if (info.menuItemId === 'fehelper-screenshot-page') {
  464. _triggerScreenshotTool(tab.id);
  465. }
  466. });
  467. // 创建截图工具右键菜单
  468. chrome.contextMenus.create({
  469. id: 'fehelper-screenshot-page',
  470. title: '网页截图',
  471. contexts: ['page']
  472. });
  473. Menu.rebuild();
  474. // 定期清理冗余的垃圾
  475. setTimeout(() => {
  476. Awesome.gcLocalFiles();
  477. }, 1000 * 10);
  478. };
  479. /**
  480. * 触发截图工具的执行
  481. * @param {number} tabId - 标签页ID
  482. */
  483. function _triggerScreenshotTool(tabId) {
  484. // 先尝试直接发送消息给content script
  485. chrome.tabs.sendMessage(tabId, {
  486. type: 'fh-screenshot-start'
  487. }).then(() => {
  488. // 成功触发
  489. }).catch(() => {
  490. // 如果发送消息失败,使用noPage模式
  491. chrome.DynamicToolRunner({
  492. tool: 'screenshot',
  493. noPage: true
  494. });
  495. });
  496. }
  497. return {
  498. pageCapture: _captureVisibleTab,
  499. init: _init
  500. };
  501. })();
  502. BgPageInstance.init();