http-on-pages

Initiate an XHR request on the page

当前为 2024-05-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name http-on-pages
  3. // @namespace https://github.com/pansong291/
  4. // @version 0.1.7
  5. // @description Initiate an XHR request on the page
  6. // @description:zh 在页面上发起 XHR 请求
  7. // @author paso
  8. // @license Apache-2.0
  9. // @match *://*/*
  10. // @grant none
  11. // @noframes
  12. // @run-at context-menu
  13. // @require https://update.greasyfork.org/scripts/473443/1374764/popup-inject.js
  14. // ==/UserScript==
  15.  
  16. /**
  17. * @typedef {object} Req
  18. * @property {string} method
  19. * @property {string} url
  20. * @property {string} code
  21. * @property {number} timestamp
  22. */
  23. /**
  24. * @typedef {object} ReqsProxy
  25. * @property {(i: number, v: Req) => void} insert
  26. * @property {(i: number) => Req} remove
  27. * @property {Req} selected
  28. */
  29. ;(function () {
  30. 'use strict'
  31. const namespace = 'paso-http-on-pages'
  32. const hint = 'const data = { headers: {}, params: {}, body: void 0, withCredentials: true }'
  33. window.paso.injectPopup({
  34. namespace,
  35. actionName: 'Http Request',
  36. collapse: '70%',
  37. content: `
  38. <div class="tip-box info">${hint}</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. style: `
  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. flex: 1 0 300px;
  82. }
  83. #btn-submit {
  84. width: 100px;
  85. }
  86. #ipt-code {
  87. height: 400px;
  88. }
  89. #error-tip-box {
  90. background: #fdd;
  91. border-left: 6px solid #f66;
  92. border-radius: 4px;
  93. padding: 16px;
  94. }
  95. #error-tip-box:empty {
  96. display: none;
  97. }
  98. </style>`
  99. }).then((result) => {
  100. const { popup } = result.elem
  101. const { createElement } = result.func
  102. popup.classList.add('monospace')
  103. const element = {
  104. ipt_req_sel: popup.querySelector('#ipt-req-sel'),
  105. btn_req_rem: popup.querySelector('#btn-req-rem'),
  106. btn_req_add: popup.querySelector('#btn-req-add'),
  107. ipt_method: popup.querySelector('#ipt-method'),
  108. ipt_url: popup.querySelector('#ipt-url'),
  109. ipt_code: popup.querySelector('#ipt-code'),
  110. btn_submit: popup.querySelector('#btn-submit'),
  111. error_tip: popup.querySelector('#error-tip-box')
  112. }
  113. const method_options = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH']
  114. element.ipt_method.innerHTML = method_options.map(op => `<option value="${op}">${op}</option>`).join('')
  115. /**
  116. * @type {Req[] & ReqsProxy}
  117. */
  118. const reactiveRequests = new Proxy([], {
  119. get(target, prop, receiver) {
  120. if (prop === 'insert') {
  121. return (index, value) => {
  122. checkIndex(index, target.length + 1)
  123. const opt = createElement('option', { value: value.timestamp }, [formatDate(value.timestamp)])
  124. if (target.length === 0 || index === target.length)
  125. element.ipt_req_sel.append(opt)
  126. else
  127. element.ipt_req_sel.children[index].before(opt)
  128. target.splice(index, 0, value)
  129. }
  130. } else if (prop === 'remove') {
  131. return (index) => {
  132. checkIndex(index, target.length)
  133. if (receiver.selected === target[index])
  134. receiver.selected = target[index > 0 ? index - 1 : 1]
  135. element.ipt_req_sel.children[index].remove()
  136. return target.splice(index, 1)[0]
  137. }
  138. } else if (prop === 'push') {
  139. return (value) => receiver.insert(target.length, value)
  140. } else if (prop === 'selected') {
  141. if (!target.selected) {
  142. const v = String(element.ipt_req_sel.value)
  143. target.selected = target.find((r) => String(r.timestamp) === v)
  144. }
  145. }
  146. return target[prop]
  147. },
  148. set(target, prop, newValue, receiver) {
  149. if (prop === 'selected') {
  150. target.selected = newValue
  151. element.ipt_req_sel.value = newValue?.timestamp || ''
  152. element.ipt_method.value = newValue?.method || ''
  153. element.ipt_url.value = newValue?.url || ''
  154. element.ipt_code.value = newValue?.code || ''
  155. }
  156. return true
  157. }
  158. })
  159. /**
  160. * @param {string|number} ts
  161. * @returns {Req}
  162. */
  163. const getReqByTimestamp = (ts) => {
  164. ts = String(ts)
  165. return reactiveRequests.find((r) => String(r.timestamp) === ts)
  166. }
  167.  
  168. const cache = getCache()
  169. if (cache?.requests && Array.isArray(cache.requests)) {
  170. for (const req of cache.requests) {
  171. reactiveRequests.push(createRequestObj(req))
  172. }
  173. }
  174. if (!reactiveRequests.length) reactiveRequests.push(createRequestObj())
  175. reactiveRequests.selected = getReqByTimestamp(cache?.selected) || reactiveRequests[0]
  176.  
  177. element.ipt_req_sel.addEventListener('change', (e) => reactiveRequests.selected = getReqByTimestamp(e.currentTarget.value))
  178. element.btn_req_rem.addEventListener('click', () => {
  179. if (reactiveRequests.length <= 1) return
  180. reactiveRequests.remove(reactiveRequests.indexOf(reactiveRequests.selected))
  181. })
  182. element.btn_req_add.addEventListener('click', () => {
  183. const obj = createRequestObj()
  184. reactiveRequests.push(obj)
  185. reactiveRequests.selected = obj
  186. })
  187. element.ipt_method.addEventListener('change', (e) => reactiveRequests.selected.method = e.currentTarget.value)
  188. element.ipt_url.addEventListener('change', (e) => reactiveRequests.selected.url = e.currentTarget.value)
  189. element.ipt_code.addEventListener('change', (e) => reactiveRequests.selected.code = e.currentTarget.value)
  190. element.btn_submit.addEventListener('click', tryTo(() => {
  191. const selReq = reactiveRequests.selected
  192. if (!selReq.url) throw 'Url is required'
  193. const isGet = selReq.method === 'GET'
  194. const data = {
  195. headers: { 'Content-Type': isGet ? 'application/x-www-form-urlencoded' : 'application/json' },
  196. params: {},
  197. body: void 0,
  198. withCredentials: true
  199. }
  200. const handleData = new Function('data', selReq.code)
  201. handleData.call(data, data)
  202. const xhr = new XMLHttpRequest()
  203. xhr.open(selReq.method, selReq.url + serializeQueryParam(data.params))
  204. xhr.withCredentials = !!data.withCredentials
  205. Object.entries(data.headers).forEach(([n, v]) => xhr.setRequestHeader(n, v))
  206. xhr.send(isGet ? void 0 : typeof data.body === 'string' ? data.body : JSON.stringify(data.body))
  207. saveCache({ requests: reactiveRequests, selected: selReq.timestamp })
  208. element.error_tip.innerText = ''
  209. }, e => element.error_tip.innerText = String(e)))
  210. })
  211.  
  212. /**
  213. * @param {function} fn
  214. * @param {function} [errorCallback]
  215. * @returns {function}
  216. */
  217. function tryTo(fn, errorCallback) {
  218. return function (...args) {
  219. try {
  220. fn.apply(this, args)
  221. } catch (e) {
  222. console.error(e)
  223. errorCallback?.(e)
  224. }
  225. }
  226. }
  227.  
  228. /**
  229. * @param {string | Record<string, string>} [param]
  230. * @param {string} [prefix='?']
  231. * @returns {string}
  232. */
  233. function serializeQueryParam(param, prefix = '?') {
  234. if (!param) return ''
  235. if (typeof param === 'string') return prefix + param
  236. const str = Object.entries(param).map(([k, v]) => k + '=' + encodeURIComponent(String(v))).join('&')
  237. if (str) return prefix + str
  238. return str
  239. }
  240.  
  241. /**
  242. * @param {?Req} [base]
  243. * @returns {Req}
  244. */
  245. function createRequestObj(base) {
  246. return {
  247. method: base?.method || 'GET',
  248. url: base?.url || '',
  249. code: base?.code || '',
  250. timestamp: base?.timestamp || Date.now()
  251. }
  252. }
  253.  
  254. /**
  255. * @param {number} index
  256. * @param {number} length
  257. */
  258. function checkIndex(index, length) {
  259. if (index < 0 || index >= length) throw new RangeError(`Index out of bounds error.\nindex: ${index}\nlength: ${length}`)
  260. }
  261.  
  262. /**
  263. * @param {*} [date]
  264. * @returns {string}
  265. */
  266. function formatDate(date) {
  267. date = new Date(date || null)
  268. const year = formatNumber(date.getFullYear(), 4)
  269. const month = formatNumber(date.getMonth())
  270. const day = formatNumber(date.getDate())
  271. const hour = formatNumber(date.getHours())
  272. const minute = formatNumber(date.getMinutes())
  273. const second = formatNumber(date.getSeconds())
  274. const mill = formatNumber(date.getMilliseconds(), 3)
  275. return `${year}-${month}-${day} ${hour}:${minute}:${second}.${mill}`
  276. }
  277.  
  278. /**
  279. * @param {number} num
  280. * @param {number} [count=2]
  281. * @returns {string}
  282. */
  283. function formatNumber(num, count = 2) {
  284. let str = String(num)
  285. while (str.length < count) {
  286. str = '0' + str
  287. }
  288. return str
  289. }
  290.  
  291. /**
  292. * @param {*} obj
  293. */
  294. function saveCache(obj) {
  295. localStorage.setItem(namespace, JSON.stringify(obj))
  296. }
  297.  
  298. /**
  299. * @returns {{requests: Req[], selected: string} | undefined}
  300. */
  301. function getCache() {
  302. const str = localStorage.getItem(namespace)
  303. try {
  304. if (str) return JSON.parse(str)
  305. } catch (e) {
  306. console.error(e)
  307. }
  308. }
  309. })()