写一个 Chrome 表单自动化插件(博客评论表单)

132 天前
 lhasa

在刷博客的时候,最麻烦的事情之一就是手动填写各种表单。为了提高我的冲浪体验,诞生了这款表单自动化插件。经过爬虫上百次调教,兼容 95%博客,另外 5%的网站正常人写不出来,autocomplete 小伎俩都上不了台面,各种防止逆向、防调试测试,心累。

项目结构

插件纯绿色,无隐私可言。除 images 外,全部资源和代码文件都经过 Webpack 打包,下面是项目的目录结构以及各部分的说明:

Form-automation-plugin
│  index.html
│  LICENSE
│  manifest.json
│  package-lock.json
│  package.json
│  README.md
│  webpack.config.js
│  
├─dist
│      33a80cb13f78b37acb39.woff2
│      8093dd36a9b1ec918992.ttf
│      8521c461ad88443142d9.woff
│      autoFill.min.js
│      eventHandler.min.js
│      formManager.min.js
│      main.min.css
│
└─src
    │  autoFill.js
    │  eventHandler.js
    │  formManager.js
    │  template.css
    │  template.html
    │
    ├─fonts
    │      iconfont.css
    │      iconfont.ttf
    │      iconfont.woff
    │      iconfont.woff2
    │
    └─images
            Appreciation-code.jpg
            icon128.png
            icon16.png
            icon48.png

webpack.config.js

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    autoFill: './src/autoFill.js',
    eventHandler: './src/eventHandler.js',
    formManager: './src/formManager.js',
  },
  output: {
    filename: '[name].min.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'main.min.css',
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'src', 'template.html'),
      filename: '../index.html',
      inject: 'body',
    }),
  ],
  resolve: {
    extensions: ['.js', '.css'],
  },
};

autoFill.js

// autoFill.js 文件是插件的最重要的核心模块,涉及到了插件的主要输出功能
// 遍历当前页面所有 input ,将 autocomplete 值设置为 on ,监听 Textarea 输入时触发
function handleAutocomplete() {
  const inputs = document.querySelectorAll('input');
  inputs.forEach(input => {
    const autocompleteAttr = input.getAttribute('autocomplete');
    if (autocompleteAttr) {
      input.setAttribute('autocomplete', 'on');
    } else {
      input.setAttribute('autocomplete', 'on');
    }
  });
}

// 这个函数有些臃肿,马上要去骑车,懒得搞了,现在的逻辑已经完善到 9 成了,大多数意外情况都卷了进去,但是一些防逆向防调试,我暂时无法解决,前端菜鸟,还望大哥指点一二
function fillInputFields() {
  chrome.storage.sync.get(["name", "email", "url"], (data) => {
    // console.log(data);

    const hasValidName = data.name !== undefined && data.name !== "";
    const hasValidEmail = data.email !== undefined && data.email !== "";
    const hasValidUrl = data.url !== undefined && data.url !== "";

    // 关键字
    const nameKeywords = [
      "name", "author", "display_name", "full-name", "username", "nick", "displayname", 
      "first-name", "last-name", "full name", "real-name", "given-name", 
      "family-name", "user-name", "pen-name", "alias", "name-field", "displayname"
    ];
    const emailKeywords = [
      "email", "mail", "contact", "emailaddress", "mailaddress", 
      "email-address", "mail-address", "email-addresses", "mail-addresses", 
      "emailaddresses", "mailaddresses", "contactemail", "useremail", 
      "contact-email", "user-mail"
    ];
    const urlKeywords = [
      "url", "link", "website", "homepage", "site", "web", "address", 
      "siteurl", "webaddress", "homepageurl", "profile", "homepage-link"
    ];

    const inputs = document.querySelectorAll("input, textarea");

    inputs.forEach((input) => {
      const typeAttr = input.getAttribute("type")?.toLowerCase() || "";
      const nameAttr = input.getAttribute("name")?.toLowerCase() || "";
      let valueToSet = "";

      // 处理 URL
      if (urlKeywords.some(keyword => nameAttr.includes(keyword))) {
        if (hasValidUrl) {
          valueToSet = data.url;
        }
      }
      // 处理邮箱
      else if (emailKeywords.some(keyword => nameAttr.includes(keyword))) {
        if (hasValidEmail) {
          valueToSet = data.email;
        }
      }
      // 处理名称
      else if (nameKeywords.some(keyword => nameAttr.includes(keyword))) {
        if (hasValidName) {
          valueToSet = data.name;
        }
      }

      // 处理没有 type 属性或者 type 为 text 的情况
      if ((typeAttr === "" || typeAttr === "text") && valueToSet === "") {
        if (nameAttr && nameKeywords.some(keyword => nameAttr.includes(keyword))) {
          if (hasValidName) {
            valueToSet = data.name;
          }
        } else if (nameAttr && urlKeywords.some(keyword => nameAttr.includes(keyword))) {
          if (hasValidUrl) {
            valueToSet = data.url;
          }
        }
      }

      // 处理 type 为 email
      if (typeAttr === "email" && valueToSet === "") {
        if (nameAttr && emailKeywords.some(keyword => nameAttr.includes(keyword))) {
          if (hasValidEmail) {
            valueToSet = data.email;
          }
        }
      }

      // 处理 type 为 url
      if (typeAttr === "url" && valueToSet === "") {
        if (nameAttr && urlKeywords.some(keyword => nameAttr.includes(keyword))) {
          if (hasValidUrl) {
            valueToSet = data.url;
          }
        }
      }

      // 填充输入字段
      if (valueToSet !== "") {
        input.value = valueToSet;
      }
    });
  });
}

function clearInputFields() {
  const inputs = document.querySelectorAll("input");
  inputs.forEach((input) => {
    const typeAttr = input.getAttribute("type")?.toLowerCase();
    if (typeAttr === "text" || typeAttr === "email") {
      input.value = "";
    }
  });
}

// 监听 textarea 标签的输入事件
document.addEventListener("input", (event) => {
  if (event.target.tagName.toLowerCase() === "textarea") {
    handleAutocomplete();
    fillInputFields();
  }
});

formManager.js

该文件负责向 Chrome 本地存储和修改,就 CURD ,没啥含量

import './fonts/iconfont.css';
import './template.css';

document.getElementById("save").addEventListener("click", () => {
  const saveButton = document.getElementById("save");
  if (saveButton.textContent === "更改") {
    unlockInputFields();
    changeButtonText("保存");
    return;
  }

  const name = document.getElementById("name").value.trim();
  const email = document.getElementById("email").value.trim();
  const url = document.getElementById("url").value.trim();

  // 验证邮箱格式的正则表达式
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (name === "" || email === "") {
    alert("请填写必填字段:姓名和邮箱!");
    return;
  }

  if (!emailPattern.test(email)) {
    alert("请输入有效的邮箱地址!");
    return;
  }

  // 从 Chrome 存储中读取当前的值
  chrome.storage.sync.get(["name", "email", "url"], (data) => {
    const isNameAndEmailChanged = name !== data.name || email !== data.email;
    const isUrlChanged = url !== data.url;

    if (isNameAndEmailChanged || isUrlChanged) {
      chrome.storage.sync.set({ name, email, url }, () => {
        lockInputFields();
        changeButtonText("更改");
      });
    } else {
      lockInputFields();
      changeButtonText("更改");
    }
  });
});

// 页面加载完成时执行
document.addEventListener("DOMContentLoaded", () => {
  chrome.storage.sync.get(["name", "email", "url"], (data) => {
    document.getElementById("name").value = data.name || "";
    document.getElementById("email").value = data.email || "";
    document.getElementById("url").value = data.url || "";

    if (data.name || data.email || data.url) {
      lockInputFields();
      changeButtonText("更改");
    }
  });

  const menuItems = document.querySelectorAll('.dl-menu li a');
  const tabContents = document.querySelectorAll('.tab-content');

  menuItems.forEach(menuItem => {
    menuItem.addEventListener('click', (event) => {
      event.preventDefault();
      tabContents.forEach(tab => tab.classList.remove('active'));
      const targetId = menuItem.getAttribute('href').substring(1);
      document.getElementById(targetId).classList.add('active');
      menuItems.forEach(item => item.parentElement.classList.remove('active'));
      menuItem.parentElement.classList.add('active');
    });
  });
});

// 锁定输入框
function lockInputFields() {
  document.getElementById("name").setAttribute("disabled", "true");
  document.getElementById("email").setAttribute("disabled", "true");
  document.getElementById("url").setAttribute("disabled", "true");
}

// 解锁输入框
function unlockInputFields() {
  document.getElementById("name").removeAttribute("disabled");
  document.getElementById("email").removeAttribute("disabled");
  document.getElementById("url").removeAttribute("disabled");
}

// 更改按钮文本
function changeButtonText(text) {
  document.getElementById("save").textContent = text;
}

使用方法

git clone 到本地,浏览器打开:chrome://extensions/,加载已解压的扩展程序

由于我没有注册 Chrome 应用商店开发者,目前只能本地运行,过几天上线应用商店,Tampermonkey 等骑车回来再做

Github

Form-automation-plugin:https://github.com/achuanya/Form-automation-plugin

博客原文: https://lhasa.icu/FormAutomationPlugin.html

1200 次点击
所在节点    程序员
0 条回复

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1063214

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX