origins.js 12 KB

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