http-on-pages

Initiate an XHR request on the page

  1. // ==UserScript==
  2. // @name http-on-pages
  3. // @namespace https://github.com/pansong291/
  4. // @version 0.1.10
  5. // @description Initiate an XHR request on the page
  6. // @description:zh 在页面上发起 XHR 请求
  7. // @author paso
  8. // @license Apache-2.0
  9. // @match *://*/*
  10. // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201024%201024%22%20fill%3D%22black%22%3E%3Cpath%20d%3D%22M474.937%20387.054c0-20.275%2016.638-36.403%2036.913-36.403h53.035L362.632%20148.402%20160.377%20350.65h53.297c20.275%200%2036.652%2016.128%2036.652%2036.403v259.959c0%2020.276-16.377%2036.914-36.652%2036.914-20.021%200-36.919-16.638-36.919-36.914V423.967H71.732c-20.015%200-36.137-17.159-36.137-36.914%200-9.883%203.644-19.244%2010.138-25.999l291.42-291.16c13.781-13.521%2037.435-13.521%2051.217%200l291.421%20291.161c13.776%2014.552%2013.776%2037.956%200%2051.987-7.282%206.766-16.898%2010.925-25.999%2010.925H548.247v223.045c0%2020.276-16.377%2036.914-36.398%2036.914h-53.291l202.509%20202.26%20201.994-202.26H809.77c-20.276%200-36.398-16.638-36.398-36.914V387.054c0-20.275%2016.122-36.403%2036.398-36.403s36.914%2016.128%2036.914%2036.403v223.567h104.768c9.617%200%2018.717%203.116%2026.254%2010.393a37.359%2037.359%200%200%201%200%2051.999L687.328%20962.609l-0.261%201.043c-14.558%2014.563-37.957%2014.563-51.993%200l-291.16-290.639c-6.239-6.755-10.659-16.117-10.659-26%200-20.275%2016.898-36.392%2036.397-36.392h105.285V387.054z%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E
  11. // @grant none
  12. // @noframes
  13. // @run-at context-menu
  14. // @require https://update.gf.qytechs.cn/scripts/473443/1374764/popup-inject.js
  15. // ==/UserScript==
  16.  
  17. /**
  18. * @typedef {object} ReqObj
  19. * @property {string} method
  20. * @property {string} url
  21. * @property {string} code
  22. * @property {number} timestamp
  23. */
  24. /**
  25. * @typedef {object} ProxiedReqExtension
  26. * @property {(i: number, v: ReqObj) => void} insert
  27. * @property {(i: number) => ReqObj} remove
  28. * @property {ReqObj} selected
  29. */
  30. /**
  31. * @typedef {ReqObj[] & ProxiedReqExtension} ProxiedReqArray
  32. */
  33. ;(function () {
  34. 'use strict'
  35. const namespace = 'paso-http-on-pages'
  36. const injectHint = 'const data = { headers: {}, params: {}, body: void 0, withCredentials: true }'
  37. const injectHtml = `
  38. <div class="tip-box info">${injectHint}</div>
  39. <div class="flex gap-4">
  40. <select id="ipt-req-sel" class="input"></select>
  41. <button id="btn-req-rem" type="button" class="button square">
  42. <svg width="16" height="16" fill="currentcolor">
  43. <path d="M2 7h12v2H2Z"></path>
  44. </svg>
  45. </button>
  46. <button id="btn-req-add" type="button" class="button square">
  47. <svg width="16" height="16" fill="currentcolor">
  48. <path d="M2 7H7V2H9V7H14V9H9V14H7V9H2Z"></path>
  49. </svg>
  50. </button>
  51. </div>
  52. <div class="flex gap-4">
  53. <select id="ipt-method" class="input"></select>
  54. <input type="text" id="ipt-url" class="input" autocomplete="off">
  55. <button type="button" id="btn-submit" class="button">Submit</button>
  56. </div>
  57. <textarea id="ipt-code" class="input" spellcheck="false"></textarea>
  58. <div id="error-tip-box"></div>`
  59. const injectStyle = `
  60. <style>
  61. .popup {
  62. gap: 4px;
  63. }
  64. .gap-4 {
  65. gap: 4px;
  66. }
  67. .tip-box.info {
  68. background: #d3dff7;
  69. border-left: 6px solid #3d7fff;
  70. border-radius: 4px;
  71. padding: 16px;
  72. }
  73. .button.square {
  74. width: 32px;
  75. padding: 0;
  76. }
  77. #ipt-method {
  78. width: 90px;
  79. }
  80. #ipt-url {
  81. width: 300px;
  82. flex-grow: 1;
  83. }
  84. #btn-submit {
  85. width: 100px;
  86. }
  87. #ipt-code {
  88. height: 400px;
  89. }
  90. #error-tip-box {
  91. background: #fdd;
  92. border-left: 6px solid #f66;
  93. border-radius: 4px;
  94. padding: 16px;
  95. }
  96. #error-tip-box:empty {
  97. display: none;
  98. }
  99. </style>`
  100. window.paso.injectPopup({
  101. namespace,
  102. actionName: 'Http Request',
  103. collapse: '70%',
  104. content: injectHtml,
  105. style: injectStyle
  106. }).then((result) => {
  107. const { popup } = result.elem
  108. const { createElement } = result.func
  109. popup.classList.add('monospace')
  110. const element = {
  111. ipt_req_sel: popup.querySelector('#ipt-req-sel'),
  112. btn_req_rem: popup.querySelector('#btn-req-rem'),
  113. btn_req_add: popup.querySelector('#btn-req-add'),
  114. ipt_method: popup.querySelector('#ipt-method'),
  115. ipt_url: popup.querySelector('#ipt-url'),
  116. ipt_code: popup.querySelector('#ipt-code'),
  117. btn_submit: popup.querySelector('#btn-submit'),
  118. error_tip: popup.querySelector('#error-tip-box')
  119. }
  120. const method_options = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH']
  121. element.ipt_method.innerHTML = method_options.map(op => `<option value="${op}">${op}</option>`).join('')
  122. /**
  123. * @type {ProxiedReqArray}
  124. */
  125. const reactiveRequests = new Proxy([], {
  126. get(target, prop, receiver) {
  127. if (prop === 'insert') {
  128. return (index, value) => {
  129. checkIndex(index, target.length + 1)
  130. const opt = createElement('option', { value: value.timestamp }, [formatDate(value.timestamp)])
  131. if (target.length === 0 || index === target.length)
  132. element.ipt_req_sel.append(opt)
  133. else
  134. element.ipt_req_sel.children[index].before(opt)
  135. target.splice(index, 0, value)
  136. }
  137. } else if (prop === 'remove') {
  138. return (index) => {
  139. checkIndex(index, target.length)
  140. if (receiver.selected === target[index])
  141. receiver.selected = target[index > 0 ? index - 1 : 1]
  142. element.ipt_req_sel.children[index].remove()
  143. return target.splice(index, 1)[0]
  144. }
  145. } else if (prop === 'push') {
  146. return (value) => receiver.insert(target.length, value)
  147. } else if (prop === 'selected') {
  148. if (!target.selected) {
  149. const v = String(element.ipt_req_sel.value)
  150. target.selected = target.find((r) => String(r.timestamp) === v)
  151. }
  152. }
  153. return target[prop]
  154. },
  155. set(target, prop, newValue, receiver) {
  156. if (prop === 'selected') {
  157. target.selected = newValue
  158. element.ipt_req_sel.value = newValue?.timestamp || ''
  159. element.ipt_method.value = newValue?.method || ''
  160. element.ipt_url.value = newValue?.url || ''
  161. element.ipt_code.value = newValue?.code || ''
  162. }
  163. return true
  164. }
  165. })
  166. /**
  167. * @param {string|number} ts
  168. * @returns {ReqObj}
  169. */
  170. const getReqByTimestamp = (ts) => {
  171. ts = String(ts)
  172. return reactiveRequests.find((r) => String(r.timestamp) === ts)
  173. }
  174.  
  175. const cache = getCache()
  176. if (cache?.requests && Array.isArray(cache.requests)) {
  177. for (const req of cache.requests) {
  178. reactiveRequests.push(createRequestObj(req))
  179. }
  180. }
  181. if (!reactiveRequests.length) reactiveRequests.push(createRequestObj())
  182. reactiveRequests.selected = getReqByTimestamp(cache?.selected) || reactiveRequests[0]
  183.  
  184. element.ipt_req_sel.addEventListener('change', (e) => reactiveRequests.selected = getReqByTimestamp(e.currentTarget.value))
  185. element.btn_req_rem.addEventListener('click', () => {
  186. if (reactiveRequests.length <= 1) return
  187. reactiveRequests.remove(reactiveRequests.indexOf(reactiveRequests.selected))
  188. })
  189. element.btn_req_add.addEventListener('click', () => {
  190. const obj = createRequestObj()
  191. reactiveRequests.push(obj)
  192. reactiveRequests.selected = obj
  193. })
  194. element.ipt_method.addEventListener('change', (e) => reactiveRequests.selected.method = e.currentTarget.value)
  195. element.ipt_url.addEventListener('change', (e) => reactiveRequests.selected.url = e.currentTarget.value)
  196. element.ipt_code.addEventListener('change', (e) => reactiveRequests.selected.code = e.currentTarget.value)
  197. element.btn_submit.addEventListener('click', tryTo(() => {
  198. const selReq = reactiveRequests.selected
  199. if (!selReq.url) throw 'Url is required'
  200. const isGet = selReq.method === 'GET'
  201. // 预备数据
  202. const data = {
  203. headers: { 'Content-Type': isGet ? 'application/x-www-form-urlencoded' : 'application/json' },
  204. params: {},
  205. body: void 0,
  206. withCredentials: true
  207. }
  208. // 处理数据
  209. const handleData = new Function('data', selReq.code)
  210. handleData.call(data, data)
  211. const isForm = data.body instanceof FormData
  212. if (isForm) {
  213. // 使用表单时不填充 Content-Type
  214. delete data.headers['Content-Type']
  215. }
  216.  
  217. const xhr = new XMLHttpRequest()
  218. // 链接
  219. xhr.open(selReq.method, selReq.url + serializeQueryParam(data.params))
  220. // 使用凭证
  221. xhr.withCredentials = !!data.withCredentials
  222. // 请求头
  223. Object.entries(data.headers).forEach(([n, v]) => {
  224. if (v !== null && v !== undefined) xhr.setRequestHeader(n, v)
  225. })
  226. // 请求体
  227. xhr.send(isGet ? void 0 : isForm ? data.body : serializeHttpBody(data.body))
  228. saveCache({ requests: reactiveRequests, selected: selReq.timestamp })
  229. element.error_tip.innerText = ''
  230. }, e => element.error_tip.innerText = String(e)))
  231. })
  232.  
  233. /**
  234. * @param {function} fn
  235. * @param {function} [errorCallback]
  236. * @returns {function}
  237. */
  238. function tryTo(fn, errorCallback) {
  239. return function (...args) {
  240. try {
  241. fn.apply(this, args)
  242. } catch (e) {
  243. console.error(e)
  244. errorCallback?.(e)
  245. }
  246. }
  247. }
  248.  
  249. /**
  250. * @param {string | Record<string, string>} [param]
  251. * @param {string} [prefix='?']
  252. * @returns {string}
  253. */
  254. function serializeQueryParam(param, prefix = '?') {
  255. if (!param) return ''
  256. if (typeof param === 'string') return prefix + param
  257. const str = Object.entries(flatten(param)).flatMap(([k, v]) => {
  258. if (v === null || v === void 0) return []
  259. return [k + '=' + encodeURIComponent(String(v))]
  260. }).join('&')
  261. if (str) return prefix + str
  262. return str
  263. }
  264.  
  265. /**
  266. * @param {*} obj
  267. * @param {string} [name='']
  268. * @returns {Record<string, *>}
  269. */
  270. function flatten(obj, name = '') {
  271. const result = {}
  272. if (!obj || typeof obj !== 'object') {
  273. if (!name) return [obj]
  274. result[name] = obj
  275. } else {
  276. const isArr = Array.isArray(obj)
  277. Object.entries(obj).forEach(([k, v]) => {
  278. Object.entries(flatten(v, !name ? k : isArr ? `${name}[${k}]` : `${name}.${k}`)).forEach(([k2, v2]) => {
  279. result[k2] = v2
  280. })
  281. })
  282. }
  283. return result
  284. }
  285.  
  286. /**
  287. * @param {?ReqObj} [base]
  288. * @returns {ReqObj}
  289. */
  290. function createRequestObj(base) {
  291. return {
  292. method: base?.method || 'GET',
  293. url: base?.url || '',
  294. code: base?.code || '',
  295. timestamp: base?.timestamp || Date.now()
  296. }
  297. }
  298.  
  299. /**
  300. * @param {number} index
  301. * @param {number} length
  302. */
  303. function checkIndex(index, length) {
  304. if (index < 0 || index >= length) throw new RangeError(`Index out of bounds error.\nindex: ${index}\nlength: ${length}`)
  305. }
  306.  
  307. /**
  308. * @param {*} [date]
  309. * @returns {string}
  310. */
  311. function formatDate(date) {
  312. date = new Date(date || null)
  313. const year = formatNumber(date.getFullYear(), 4)
  314. const month = formatNumber(date.getMonth())
  315. const day = formatNumber(date.getDate())
  316. const hour = formatNumber(date.getHours())
  317. const minute = formatNumber(date.getMinutes())
  318. const second = formatNumber(date.getSeconds())
  319. const mill = formatNumber(date.getMilliseconds(), 3)
  320. return `${year}-${month}-${day} ${hour}:${minute}:${second}.${mill}`
  321. }
  322.  
  323. /**
  324. * @param {number} num
  325. * @param {number} [count=2]
  326. * @returns {string}
  327. */
  328. function formatNumber(num, count = 2) {
  329. return String(num).padStart(count, '0')
  330. }
  331.  
  332. /**
  333. * @param {*} obj
  334. * @returns {string}
  335. */
  336. function serializeHttpBody(obj) {
  337. if (typeof obj === 'string') return obj
  338. return JSON.stringify(obj)
  339. }
  340.  
  341. /**
  342. * @param {*} obj
  343. */
  344. function saveCache(obj) {
  345. localStorage.setItem(namespace, JSON.stringify(obj))
  346. }
  347.  
  348. /**
  349. * @returns {{requests: ReqObj[], selected: string} | undefined}
  350. */
  351. function getCache() {
  352. const str = localStorage.getItem(namespace)
  353. try {
  354. if (str) return JSON.parse(str)
  355. } catch (e) {
  356. console.error(e)
  357. }
  358. }
  359. })()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址