origins.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. var Origins = () => {
  2. var defaults = {
  3. // storage
  4. origins: {},
  5. match: '',
  6. // UI
  7. host: '',
  8. timeout: null,
  9. file: true,
  10. // chrome
  11. permissions: {},
  12. }
  13. var state = Object.assign({}, defaults)
  14. chrome.extension.isAllowedFileSchemeAccess((isAllowedAccess) => {
  15. state.file = /Firefox/.test(navigator.userAgent)
  16. ? true // ff: `Allow access to file URLs` option isn't available
  17. : isAllowedAccess
  18. m.redraw()
  19. })
  20. chrome.runtime.sendMessage({message: 'options.origins'}, (res) => {
  21. Object.assign(state, {file: state.file}, res)
  22. chrome.permissions.getAll(({origins}) => {
  23. state.permissions = origins.reduce((all, origin) =>
  24. (all[origin.replace(/(.*)\/\*$/, '$1')] = true, all), {})
  25. m.redraw()
  26. })
  27. })
  28. var events = {
  29. file: () => {
  30. chrome.tabs.create({url: `chrome://extensions/?id=${chrome.runtime.id}`})
  31. },
  32. host: (e) => {
  33. state.scheme = e.target.value.replace(/(.*):\/\/.*/i, '$1')
  34. state.domain = e.target.value.replace(/.*:\/\/([^/]+).*/i, '$1')
  35. state.host = e.target.value
  36. },
  37. add: (all) => () => {
  38. if (!all && !state.host && !['https', 'http', '*'].includes(state.scheme)) {
  39. return
  40. }
  41. var origin = all ? '*://*' : `${state.scheme}://${state.domain}`
  42. if (/Firefox/.test(navigator.userAgent) && /:\d{2,4}/.test(origin)) {
  43. origin = origin.replace(/(:\d{2,4})/, '')
  44. }
  45. chrome.permissions.request({origins: [`${origin}/*`]}, (granted) => {
  46. if (granted) {
  47. chrome.runtime.sendMessage({message: 'origin.add', origin})
  48. state.origins[origin] = {
  49. header: true,
  50. path: true,
  51. match: state.match,
  52. }
  53. state.host = ''
  54. state.permissions[origin] = true
  55. m.redraw()
  56. }
  57. })
  58. },
  59. remove: (origin) => () => {
  60. chrome.permissions.remove({origins: [`${origin}/*`]}, (removed) => {
  61. if (removed) {
  62. chrome.runtime.sendMessage({message: 'origin.remove', origin})
  63. delete state.origins[origin]
  64. delete state.permissions[origin]
  65. m.redraw()
  66. }
  67. })
  68. },
  69. refresh: (origin) => () => {
  70. chrome.permissions.request({origins: [`${origin}/*`]}, (granted) => {
  71. if (granted) {
  72. state.permissions[origin] = true
  73. m.redraw()
  74. }
  75. })
  76. },
  77. header: (origin) => () => {
  78. state.origins[origin].header = !state.origins[origin].header
  79. var {header, path, match} = state.origins[origin]
  80. chrome.runtime.sendMessage({
  81. message: 'origin.update',
  82. origin,
  83. options: {header, path, match},
  84. })
  85. },
  86. path: (origin) => () => {
  87. state.origins[origin].path = !state.origins[origin].path
  88. var {header, path, match} = state.origins[origin]
  89. chrome.runtime.sendMessage({
  90. message: 'origin.update',
  91. origin,
  92. options: {header, path, match},
  93. })
  94. },
  95. match: (origin) => (e) => {
  96. state.origins[origin].match = e.target.value
  97. clearTimeout(state.timeout)
  98. state.timeout = setTimeout(() => {
  99. var {header, path, match} = state.origins[origin]
  100. chrome.runtime.sendMessage({
  101. message: 'origin.update',
  102. origin,
  103. options: {header, path, match},
  104. })
  105. }, 750)
  106. },
  107. }
  108. var oncreate = {
  109. ripple: (vnode) => {
  110. mdc.ripple.MDCRipple.attachTo(vnode.dom)
  111. },
  112. textfield: (vnode) => {
  113. mdc.textfield.MDCTextField.attachTo(vnode.dom)
  114. }
  115. }
  116. var onupdate = {
  117. header: (origin) => (vnode) => {
  118. if (vnode.dom.classList.contains('is-checked') !== state.origins[origin].header) {
  119. vnode.dom.classList.toggle('is-checked')
  120. }
  121. },
  122. path: (origin) => (vnode) => {
  123. if (vnode.dom.classList.contains('is-checked') !== state.origins[origin].path) {
  124. vnode.dom.classList.toggle('is-checked')
  125. }
  126. }
  127. }
  128. var render = () =>
  129. m('.m-origins',
  130. // file access
  131. m('.row',
  132. m('.col-xxl-10.col-xl-10.col-lg-9.col-md-8.col-sm-12',
  133. m('h3', 'File Access'),
  134. ),
  135. m('.col-xxl-2.col-xl-2.col-lg-3.col-md-4.col-sm-12',
  136. // file access is disabled
  137. (!state.file || null) &&
  138. m('button.mdc-button mdc-button--raised m-button m-btn-file', {
  139. oncreate: oncreate.ripple,
  140. onclick: events.file
  141. },
  142. 'Allow Access'
  143. )
  144. ),
  145. ),
  146. ...Object.keys(state.origins)
  147. .filter((origin) => origin === 'file://' && state.file)
  148. .map(callout),
  149. // site access
  150. m('.row',
  151. m('.col-xxl-10.col-xl-10.col-lg-10.col-md-9.col-sm-8',
  152. m('h3', 'Site Access'),
  153. ),
  154. (!Object.keys(state.origins).includes('*://*') || null) &&
  155. m('.col-xxl-2.col-xl-2.col-lg-2.col-md-3.col-sm-4',
  156. m('button.mdc-button mdc-button--raised m-button m-btn-all', {
  157. oncreate: oncreate.ripple,
  158. onclick: events.add(true)
  159. },
  160. 'Allow All'
  161. ),
  162. ),
  163. ),
  164. // add origin
  165. m('.bs-callout m-box-add',
  166. m('.row',
  167. m('.col-xxl-11.col-xl-11.col-lg-10.col-md-10.col-sm-12',
  168. m('.mdc-text-field m-textfield', {
  169. oncreate: oncreate.textfield,
  170. },
  171. m('input.mdc-text-field__input', {
  172. type: 'text',
  173. value: state.host,
  174. onchange: events.host,
  175. placeholder: 'Copy/paste URL address here'
  176. }),
  177. m('.mdc-line-ripple')
  178. ),
  179. ),
  180. m('.col-xxl-1.col-xl-1.col-lg-2.col-md-2.col-sm-12',
  181. m('button.mdc-button mdc-button--raised m-button m-btn-add', {
  182. oncreate: oncreate.ripple,
  183. onclick: events.add()
  184. },
  185. 'Add'
  186. ),
  187. )
  188. )
  189. ),
  190. // allowed origins
  191. ...Object.keys(state.origins)
  192. .filter((origin) => origin !== 'file://')
  193. .sort((a, b) => a < b ? 1 : a > b ? -1 : 0)
  194. .map(callout)
  195. )
  196. var callout = (origin) =>
  197. m('.bs-callout', {class: !state.permissions[origin] ? 'm-box-refresh' : undefined},
  198. // origin
  199. m('.row',
  200. m('.col-xxl-8.col-xl-8.col-lg-8.col-md-7.col-sm-12 m-overflow', m('span.m-origin', origin)),
  201. m('.col-xxl-4.col-xl-4.col-lg-4.col-md-5.col-sm-12',
  202. // remove
  203. (origin !== 'file://' || null) &&
  204. m('button.mdc-button mdc-button--raised m-button m-btn-remove', {
  205. oncreate: oncreate.ripple,
  206. onclick: events.remove(origin)
  207. },
  208. 'Remove'
  209. ),
  210. // refresh
  211. (!state.permissions[origin] || null) &&
  212. m('button.mdc-button mdc-button--raised m-button m-btn-refresh', {
  213. oncreate: oncreate.ripple,
  214. onclick: events.refresh(origin)
  215. },
  216. 'Refresh'
  217. )
  218. )
  219. ),
  220. // header detection
  221. m('.row',
  222. m('.col-sm-12',
  223. m('.overflow',
  224. m('label.mdc-switch m-switch', {
  225. onupdate: onupdate.header,
  226. title: 'Toggle Header Detection'
  227. },
  228. m('input.mdc-switch__native-control', {
  229. type: 'checkbox',
  230. checked: state.origins[origin].header,
  231. onchange: events.header(origin)
  232. }),
  233. m('.mdc-switch__background', m('.mdc-switch__knob')),
  234. m('span.mdc-switch-label',
  235. 'Content Type Detection: ',
  236. m('span', 'text/markdown'),
  237. ', ',
  238. m('span', 'text/x-markdown'),
  239. ', ',
  240. m('span', 'text/plain'),
  241. )
  242. ),
  243. )
  244. )
  245. ),
  246. // path matching regexp
  247. m('.row',
  248. m('.col-sm-12',
  249. m('.overflow',
  250. m('label.mdc-switch m-switch', {
  251. onupdate: onupdate.path,
  252. title: 'Toggle Path Detection'
  253. },
  254. m('input.mdc-switch__native-control', {
  255. type: 'checkbox',
  256. checked: state.origins[origin].path,
  257. onchange: events.path(origin)
  258. }),
  259. m('.mdc-switch__background', m('.mdc-switch__knob')),
  260. m('span.mdc-switch-label',
  261. 'Path Matching RegExp: '
  262. )
  263. ),
  264. m('.mdc-text-field m-textfield', {
  265. oncreate: oncreate.textfield
  266. },
  267. m('input.mdc-text-field__input', {
  268. type: 'text',
  269. onkeyup: events.match(origin),
  270. value: state.origins[origin].match,
  271. }),
  272. m('.mdc-line-ripple')
  273. )
  274. )
  275. )
  276. )
  277. )
  278. return {state, render}
  279. }