options.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. var defaults = {
  2. // storage
  3. origins: {},
  4. header: false,
  5. match: '',
  6. // UI
  7. scheme: 'https',
  8. host: '',
  9. timeout: null,
  10. file: true,
  11. // static
  12. schemes: ['https', 'http', '*'],
  13. encodings: {
  14. 'Unicode': ['UTF-8', 'UTF-16LE'],
  15. 'Arabic': ['ISO-8859-6', 'Windows-1256'],
  16. 'Baltic': ['ISO-8859-4', 'ISO-8859-13', 'Windows-1257'],
  17. 'Celtic': ['ISO-8859-14'],
  18. 'Central European': ['ISO-8859-2', 'Windows-1250'],
  19. 'Chinese Simplified': ['GB18030', 'GBK'],
  20. 'Chinese Traditional': ['BIG5'],
  21. 'Cyrillic': ['ISO-8859-5', 'IBM866', 'KOI8-R', 'KOI8-U', 'Windows-1251'],
  22. 'Greek': ['ISO-8859-7', 'Windows-1253'],
  23. 'Hebrew': ['Windows-1255', 'ISO-8859-8', 'ISO-8859-8-I'],
  24. 'Japanese': ['EUC-JP', 'ISO-2022-JP', 'Shift_JIS'],
  25. 'Korean': ['EUC-KR'],
  26. 'Nordic': ['ISO-8859-10'],
  27. 'Romanian': ['ISO-8859-16'],
  28. 'South European': ['ISO-8859-3'],
  29. 'Thai': ['Windows-874'],
  30. 'Turkish': ['Windows-1254'],
  31. 'Vietnamese': ['Windows-1258'],
  32. 'Western': ['ISO-8859-15', 'Windows-1252', 'Macintosh'],
  33. }
  34. }
  35. var state = Object.assign({}, defaults)
  36. chrome.extension.isAllowedFileSchemeAccess((isAllowedAccess) => {
  37. state.file = /Firefox/.test(navigator.userAgent)
  38. ? true // ff: `Allow access to file URLs` option isn't available
  39. : isAllowedAccess
  40. m.redraw()
  41. })
  42. chrome.runtime.sendMessage({message: 'options'}, (res) => {
  43. state = Object.assign({}, defaults, {file: state.file}, res)
  44. m.redraw()
  45. })
  46. var events = {
  47. file: () => {
  48. chrome.tabs.create({url: `chrome://extensions/?id=${chrome.runtime.id}`})
  49. },
  50. header: (e) => {
  51. state.header = !state.header
  52. chrome.runtime.sendMessage({
  53. message: 'options.header',
  54. header: state.header,
  55. })
  56. },
  57. origin: {
  58. scheme: (e) => {
  59. state.scheme = state.schemes[e.target.selectedIndex]
  60. },
  61. host: (e) => {
  62. state.host = e.target.value.replace(/.*:\/\/([^/]+).*/i, '$1')
  63. },
  64. add: (all) => () => {
  65. if (!all && !state.host) {
  66. return
  67. }
  68. var origin = all ? '*://*' : `${state.scheme}://${state.host}`
  69. chrome.permissions.request({origins: [`${origin}/*`]}, (granted) => {
  70. if (granted) {
  71. chrome.runtime.sendMessage({message: 'origin.add', origin})
  72. state.origins[origin] = {
  73. match: state.match,
  74. csp: false,
  75. encoding: '',
  76. }
  77. state.host = ''
  78. m.redraw()
  79. }
  80. })
  81. },
  82. remove: (origin) => () => {
  83. chrome.permissions.remove({origins: [`${origin}/*`]}, (removed) => {
  84. if (removed) {
  85. chrome.runtime.sendMessage({message: 'origin.remove', origin})
  86. webRequest()
  87. delete state.origins[origin]
  88. m.redraw()
  89. }
  90. })
  91. },
  92. refresh: (origin) => () => {
  93. chrome.permissions.request({origins: [`${origin}/*`]})
  94. },
  95. match: (origin) => (e) => {
  96. state.origins[origin].match = e.target.value
  97. clearTimeout(state.timeout)
  98. state.timeout = setTimeout(() => {
  99. var {match, csp, encoding} = state.origins[origin]
  100. chrome.runtime.sendMessage({
  101. message: 'origin.update',
  102. origin,
  103. options: {match, csp, encoding},
  104. })
  105. }, 750)
  106. },
  107. csp: (origin) => () => {
  108. state.origins[origin].csp = !state.origins[origin].csp
  109. var {match, csp, encoding} = state.origins[origin]
  110. chrome.runtime.sendMessage({
  111. message: 'origin.update',
  112. origin,
  113. options: {match, csp, encoding},
  114. })
  115. webRequest()
  116. },
  117. encoding: (origin) => (e) => {
  118. state.origins[origin].encoding = e.target.value
  119. var {match, csp, encoding} = state.origins[origin]
  120. chrome.runtime.sendMessage({
  121. message: 'origin.update',
  122. origin,
  123. options: {match, csp, encoding},
  124. })
  125. webRequest()
  126. },
  127. },
  128. }
  129. var webRequest = () => {
  130. // ff: webRequest is required permission
  131. if (/Firefox/.test(navigator.userAgent)) {
  132. return
  133. }
  134. var intercept = false
  135. for (var key in state.origins) {
  136. if (state.origins[key].csp || state.origins[key].encoding) {
  137. intercept = true
  138. break
  139. }
  140. }
  141. chrome.permissions[intercept ? 'request' : 'remove']({
  142. permissions: ['webRequest', 'webRequestBlocking']
  143. }, () => {
  144. chrome.runtime.sendMessage({
  145. message: 'options.intercept',
  146. intercept,
  147. })
  148. })
  149. }
  150. var oncreate = {
  151. ripple: (vnode) => {
  152. mdc.ripple.MDCRipple.attachTo(vnode.dom)
  153. },
  154. textfield: (vnode) => {
  155. mdc.textfield.MDCTextField.attachTo(vnode.dom)
  156. }
  157. }
  158. var onupdate = {
  159. header: (vnode) => {
  160. if (vnode.dom.classList.contains('is-checked') !== state.header) {
  161. vnode.dom.classList.toggle('is-checked')
  162. }
  163. },
  164. csp: (origin) => (vnode) => {
  165. if (vnode.dom.classList.contains('is-checked') !== state.origins[origin].csp) {
  166. vnode.dom.classList.toggle('is-checked')
  167. }
  168. }
  169. }
  170. m.mount(document.querySelector('main'), {
  171. view: () =>
  172. m('#options',
  173. // allowed origins
  174. m('.bs-callout m-origins',
  175. // add origin
  176. m('.m-add-origin',
  177. m('h4.mdc-typography--headline5', 'Allowed Origins'),
  178. m('select.mdc-elevation--z2 m-select', {
  179. onchange: events.origin.scheme
  180. },
  181. state.schemes.map((scheme) =>
  182. m('option', {
  183. value: scheme,
  184. selected: scheme === state.scheme
  185. },
  186. scheme + '://'
  187. )
  188. )),
  189. m('.mdc-text-field m-textfield', {
  190. oncreate: oncreate.textfield,
  191. },
  192. m('input.mdc-text-field__input', {
  193. type: 'text',
  194. value: state.host,
  195. onchange: events.origin.host,
  196. placeholder: 'raw.githubusercontent.com'
  197. }),
  198. m('.mdc-line-ripple')
  199. ),
  200. m('button.mdc-button mdc-button--raised m-button', {
  201. oncreate: oncreate.ripple,
  202. onclick: events.origin.add()
  203. },
  204. 'Add'
  205. ),
  206. m('button.mdc-button mdc-button--raised m-button', {
  207. oncreate: oncreate.ripple,
  208. onclick: events.origin.add(true)
  209. },
  210. 'Allow All'
  211. )
  212. ),
  213. // global options
  214. m('.m-global',
  215. (
  216. (
  217. // header detection - ff: disabled
  218. !/Firefox/.test(navigator.userAgent) &&
  219. Object.keys(state.origins).length > 1
  220. )
  221. || null
  222. ) &&
  223. m('label.mdc-switch m-switch', {
  224. onupdate: onupdate.header,
  225. title: 'Toggle header detection'
  226. },
  227. m('input.mdc-switch__native-control', {
  228. type: 'checkbox',
  229. checked: state.header,
  230. onchange: events.header
  231. }),
  232. m('.mdc-switch__background', m('.mdc-switch__knob')),
  233. m('span.mdc-switch-label',
  234. 'Detect ',
  235. m('code', 'text/markdown'),
  236. ' and ',
  237. m('code', 'text/x-markdown'),
  238. ' content type'
  239. )
  240. ),
  241. // file access is disabled
  242. (!state.file || null) &&
  243. m('button.mdc-button mdc-button--raised m-button', {
  244. oncreate: oncreate.ripple,
  245. onclick: events.file
  246. },
  247. 'Allow Access to file:// URLs'
  248. )
  249. ),
  250. // allowed origins
  251. (state.file || Object.keys(state.origins).length > 1 || null) &&
  252. m('ul.m-list',
  253. Object.keys(state.origins).sort().map((origin) =>
  254. (
  255. (
  256. state.file && origin === 'file://' &&
  257. // ff: access to file:// URLs is not allowed
  258. !/Firefox/.test(navigator.userAgent)
  259. )
  260. || origin !== 'file://' || null
  261. ) &&
  262. m('li.mdc-elevation--z2', {
  263. class: state.origins[origin].expanded ? 'm-expanded' : null,
  264. },
  265. m('.m-summary', {
  266. onclick: (e) => state.origins[origin].expanded = !state.origins[origin].expanded
  267. },
  268. m('.m-origin', origin),
  269. m('.m-options',
  270. state.origins[origin].match !== state.match ? m('span', 'match') : null,
  271. state.origins[origin].csp ? m('span', 'csp') : null,
  272. state.origins[origin].encoding ? m('span', 'encoding') : null,
  273. ),
  274. m('i.material-icons', {
  275. class: state.origins[origin].expanded ? 'icon-arrow-up' : 'icon-arrow-down'
  276. })
  277. ),
  278. m('.m-content',
  279. // match
  280. m('.m-option m-match',
  281. m('.m-name', m('span', 'match')),
  282. m('.m-control',
  283. m('.mdc-text-field m-textfield', {
  284. oncreate: oncreate.textfield
  285. },
  286. m('input.mdc-text-field__input', {
  287. type: 'text',
  288. onkeyup: events.origin.match(origin),
  289. value: state.origins[origin].match,
  290. }),
  291. m('.mdc-line-ripple')
  292. )
  293. )
  294. ),
  295. // csp
  296. (origin !== 'file://' || null) &&
  297. m('.m-option m-csp',
  298. m('.m-name', m('span', 'csp')),
  299. m('.m-control',
  300. m('label.mdc-switch m-switch', {
  301. onupdate: onupdate.csp(origin),
  302. },
  303. m('input.mdc-switch__native-control', {
  304. type: 'checkbox',
  305. checked: state.origins[origin].csp,
  306. onchange: events.origin.csp(origin)
  307. }),
  308. m('.mdc-switch__background', m('.mdc-switch__knob')),
  309. m('span.mdc-switch-label',
  310. 'Disable ',
  311. m('code', 'Content Security Policy'),
  312. )
  313. )
  314. )
  315. ),
  316. // encoding
  317. (origin !== 'file://' || null) &&
  318. m('.m-option m-encoding',
  319. m('.m-name', m('span', 'encoding')),
  320. m('.m-control',
  321. m('select.mdc-elevation--z2 m-select', {
  322. onchange: events.origin.encoding(origin),
  323. },
  324. m('option', {
  325. value: '',
  326. selected: state.origins[origin].encoding === ''
  327. },
  328. 'auto'
  329. ),
  330. Object.keys(state.encodings).map((label) =>
  331. m('optgroup', {label}, state.encodings[label].map((encoding) =>
  332. m('option', {
  333. value: encoding,
  334. selected: state.origins[origin].encoding === encoding
  335. },
  336. encoding
  337. )
  338. ))
  339. )
  340. )
  341. )
  342. ),
  343. // refresh/remove
  344. (origin !== 'file://' || null) &&
  345. m('.m-footer',
  346. m('span',
  347. m('button.mdc-button mdc-button--raised m-button', {
  348. oncreate: oncreate.ripple,
  349. onclick: events.origin.refresh(origin)
  350. },
  351. 'Refresh'
  352. ),
  353. m('button.mdc-button mdc-button--raised m-button', {
  354. oncreate: oncreate.ripple,
  355. onclick: events.origin.remove(origin)
  356. },
  357. 'Remove'
  358. )
  359. )
  360. )
  361. )
  362. )
  363. )
  364. )
  365. ),
  366. )
  367. })
  368. // ff: set appropriate footer icon
  369. document.querySelector(
  370. '.icon-' + (/Firefox/.test(navigator.userAgent) ? 'firefox' : 'chrome')
  371. ).classList.remove('icon-hidden')