options.js 13 KB

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