background.js 24 KB

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