Safe.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. /* -*- Mode: Javascript; indent-tabs-mode:nil; js-indent-level: 2 -*- */
  2. /* vim: set ts=2 et sw=2 tw=80: */
  3. /*************************************************************
  4. *
  5. * MathJax/extensions/Safe.js
  6. *
  7. * Implements a "Safe" mode that disables features that could be
  8. * misused in a shared environment (such as href's to javascript URL's).
  9. * See the CONFIG variable below for configuration options.
  10. *
  11. * ---------------------------------------------------------------------
  12. *
  13. * Copyright (c) 2013-2018 The MathJax Consortium
  14. *
  15. * Licensed under the Apache License, Version 2.0 (the "License");
  16. * you may not use this file except in compliance with the License.
  17. * You may obtain a copy of the License at
  18. *
  19. * http://www.apache.org/licenses/LICENSE-2.0
  20. *
  21. * Unless required by applicable law or agreed to in writing, software
  22. * distributed under the License is distributed on an "AS IS" BASIS,
  23. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  24. * See the License for the specific language governing permissions and
  25. * limitations under the License.
  26. */
  27. (function (HUB,AJAX) {
  28. var VERSION = "2.7.5";
  29. var CONFIG = MathJax.Hub.CombineConfig("Safe",{
  30. allow: {
  31. //
  32. // Values can be "all", "safe", or "none"
  33. //
  34. URLs: "safe", // safe are in safeProtocols below
  35. classes: "safe", // safe start with MJX-
  36. cssIDs: "safe", // safe start with MJX-
  37. styles: "safe", // safe are in safeStyles below
  38. fontsize: "all", // safe are between sizeMin and sizeMax em's
  39. require: "safe" // safe are in safeRequire below
  40. },
  41. sizeMin: .7, // \scriptsize
  42. sizeMax: 1.44, // \large
  43. lengthMax: 3, // largest padding/border/margin, etc. in em's
  44. safeProtocols: {
  45. http: true,
  46. https: true,
  47. file: true,
  48. javascript: false
  49. },
  50. safeStyles: {
  51. color: true,
  52. backgroundColor: true,
  53. border: true,
  54. cursor: true,
  55. margin: true,
  56. padding: true,
  57. textShadow: true,
  58. fontFamily: true,
  59. fontSize: true,
  60. fontStyle: true,
  61. fontWeight: true,
  62. opacity: true,
  63. outline: true
  64. },
  65. safeRequire: {
  66. action: true,
  67. amscd: true,
  68. amsmath: true,
  69. amssymbols: true,
  70. autobold: false,
  71. "autoload-all": false,
  72. bbox: true,
  73. begingroup: true,
  74. boldsymbol: true,
  75. cancel: true,
  76. color: true,
  77. enclose: true,
  78. extpfeil: true,
  79. HTML: true,
  80. mathchoice: true,
  81. mhchem: true,
  82. newcommand: true,
  83. noErrors: false,
  84. noUndefined: false,
  85. unicode: true,
  86. verb: true
  87. },
  88. //
  89. // CSS styles that have Top/Right/Bottom/Left versions
  90. //
  91. styleParts: {
  92. border: true,
  93. padding: true,
  94. margin: true,
  95. outline: true
  96. },
  97. //
  98. // CSS styles that are lengths needing max/min testing
  99. // A string value means test that style value;
  100. // An array gives [min,max] in em's
  101. // Otherwise use [-lengthMax,lengthMax] from above
  102. //
  103. styleLengths: {
  104. borderTop: "borderTopWidth",
  105. borderRight: "borderRightWidth",
  106. borderBottom: "borderBottomWidth",
  107. borderLeft: "borderLeftWidth",
  108. paddingTop: true,
  109. paddingRight: true,
  110. paddingBottom: true,
  111. paddingLeft: true,
  112. marginTop: true,
  113. marginRight: true,
  114. marginBottom: true,
  115. marginLeft: true,
  116. outlineTop: true,
  117. outlineRight: true,
  118. outlineBottom: true,
  119. outlineLeft: true,
  120. fontSize: [.7,1.44]
  121. }
  122. });
  123. var ALLOW = CONFIG.allow;
  124. if (ALLOW.fontsize !== "all") {CONFIG.safeStyles.fontSize = false}
  125. var SAFE = MathJax.Extension.Safe = {
  126. version: VERSION,
  127. config: CONFIG,
  128. div1: document.createElement("div"), // for CSS processing
  129. div2: document.createElement("div"),
  130. //
  131. // Methods called for MathML attribute processing
  132. //
  133. filter: {
  134. href: "filterURL",
  135. src: "filterURL",
  136. altimg: "filterURL",
  137. "class": "filterClass",
  138. style: "filterStyles",
  139. id: "filterID",
  140. fontsize: "filterFontSize",
  141. mathsize: "filterFontSize",
  142. scriptminsize: "filterFontSize",
  143. scriptsizemultiplier: "filterSizeMultiplier",
  144. scriptlevel: "filterScriptLevel"
  145. },
  146. //
  147. // Filter HREF URL's
  148. //
  149. filterURL: function (url) {
  150. var protocol = (url.match(/^\s*([a-z]+):/i)||[null,""])[1].toLowerCase();
  151. if (ALLOW.URLs === "none" ||
  152. (ALLOW.URLs !== "all" && !CONFIG.safeProtocols[protocol])) {url = null}
  153. return url;
  154. },
  155. //
  156. // Filter class names and css ID's
  157. //
  158. filterClass: function (CLASS) {
  159. if (ALLOW.classes === "none" ||
  160. (ALLOW.classes !== "all" && !CLASS.match(/^MJX-[-a-zA-Z0-9_.]+$/))) {CLASS = null}
  161. return CLASS;
  162. },
  163. filterID: function (id) {
  164. if (ALLOW.cssIDs === "none" ||
  165. (ALLOW.cssIDs !== "all" && !id.match(/^MJX-[-a-zA-Z0-9_.]+$/))) {id = null}
  166. return id;
  167. },
  168. //
  169. // Filter style strings
  170. //
  171. filterStyles: function (styles) {
  172. if (ALLOW.styles === "all") {return styles}
  173. if (ALLOW.styles === "none") {return null}
  174. try {
  175. //
  176. // Set the div1 styles to the given styles, and clear div2
  177. //
  178. var STYLE1 = this.div1.style, STYLE2 = this.div2.style, value;
  179. STYLE1.cssText = styles; STYLE2.cssText = "";
  180. //
  181. // Check each allowed style and transfer OK ones to div2
  182. // If the style has Top/Right/Bottom/Left, look at all four separately
  183. //
  184. for (var name in CONFIG.safeStyles) {if (CONFIG.safeStyles.hasOwnProperty(name)) {
  185. if (CONFIG.styleParts[name]) {
  186. for (var i = 0; i < 4; i++) {
  187. var NAME = name+["Top","Right","Bottom","Left"][i]
  188. value = this.filterStyle(NAME,STYLE1);
  189. if (value) {STYLE2[NAME] = value}
  190. }
  191. } else {
  192. value = this.filterStyle(name,STYLE1);
  193. if (value) {STYLE2[name] = value}
  194. }
  195. }}
  196. //
  197. // Return the div2 style string
  198. //
  199. styles = STYLE2.cssText;
  200. } catch (e) {styles = null}
  201. return styles;
  202. },
  203. //
  204. // Filter an individual name:value style pair
  205. //
  206. filterStyle: function (name,styles) {
  207. var value = styles[name];
  208. if (typeof value !== "string" || value === "") {return null}
  209. if (value.match(/^\s*expression/)) {return null}
  210. if (value.match(/javascript:/)) {return null}
  211. var NAME = name.replace(/Top|Right|Left|Bottom/,"");
  212. if (!CONFIG.safeStyles[name] && !CONFIG.safeStyles[NAME]) {return null}
  213. if (!CONFIG.styleLengths[name]) {return value}
  214. return (this.filterStyleLength(name,value,styles) ? value : null);
  215. },
  216. filterStyleLength: function (name,value,styles) {
  217. if (typeof CONFIG.styleLengths[name] === "string") value = styles[CONFIG.styleLengths[name]];
  218. value = this.length2em(value);
  219. if (value == null) return false;
  220. var mM = [-CONFIG.lengthMax,CONFIG.lengthMax];
  221. if (MathJax.Object.isArray(CONFIG.styleLengths[name])) mM = CONFIG.styleLengths[name];
  222. return (value >= mM[0] && value <= mM[1]);
  223. },
  224. //
  225. // Conversion of units to em's
  226. //
  227. unit2em: {
  228. em: 1,
  229. ex: .5, // assume 1ex = .5em
  230. ch: .5, // assume 1ch = .5em
  231. rem: 1, // assume 1rem = 1em
  232. px: 1/16, // assume 1em = 16px
  233. mm: 96/25.4/16, // 25.4mm = 96px
  234. cm: 96/2.54/16, // 2.54cm = 96px
  235. 'in': 96/16, // 1in = 96px
  236. pt: 96/72/16, // 72pt = 1in
  237. pc: 96/6/16 // 1pc = 12pt
  238. },
  239. length2em: function (value) {
  240. var match = value.match(/(.+)(em|ex|ch|rem|px|mm|cm|in|pt|pc)/);
  241. if (!match) return null;
  242. return parseFloat(match[1])*this.unit2em[match[2]];
  243. },
  244. //
  245. // Filter TeX font size values (in em's)
  246. //
  247. filterSize: function (size) {
  248. if (ALLOW.fontsize === "none") {return null}
  249. if (ALLOW.fontsize !== "all")
  250. {size = Math.min(Math.max(size,CONFIG.sizeMin),CONFIG.sizeMax)}
  251. return size;
  252. },
  253. filterFontSize: function (size) {
  254. return (ALLOW.fontsize === "all" ? size: null);
  255. },
  256. //
  257. // Filter scriptsizemultiplier
  258. //
  259. filterSizeMultiplier: function (size) {
  260. if (ALLOW.fontsize === "none") {size = null}
  261. else if (ALLOW.fontsize !== "all") {size = Math.min(1,Math.max(.6,size)).toString()}
  262. return size;
  263. },
  264. //
  265. // Filter scriptLevel
  266. //
  267. filterScriptLevel: function (level) {
  268. if (ALLOW.fontsize === "none") {level = null}
  269. else if (ALLOW.fontsize !== "all") {level = Math.max(0,level).toString()}
  270. return level;
  271. },
  272. //
  273. // Filter TeX extension names
  274. //
  275. filterRequire: function (name) {
  276. if (ALLOW.require === "none" ||
  277. (ALLOW.require !== "all" && !CONFIG.safeRequire[name.toLowerCase()]))
  278. {name = null}
  279. return name;
  280. }
  281. };
  282. HUB.Register.StartupHook("TeX HTML Ready",function () {
  283. var TEX = MathJax.InputJax.TeX;
  284. TEX.Parse.Augment({
  285. //
  286. // Implements \href{url}{math} with URL filter
  287. //
  288. HREF_attribute: function (name) {
  289. var url = SAFE.filterURL(this.GetArgument(name)),
  290. arg = this.GetArgumentMML(name);
  291. if (url) {arg.With({href:url})}
  292. this.Push(arg);
  293. },
  294. //
  295. // Implements \class{name}{math} with class-name filter
  296. //
  297. CLASS_attribute: function (name) {
  298. var CLASS = SAFE.filterClass(this.GetArgument(name)),
  299. arg = this.GetArgumentMML(name);
  300. if (CLASS) {
  301. if (arg["class"] != null) {CLASS = arg["class"] + " " + CLASS}
  302. arg.With({"class":CLASS});
  303. }
  304. this.Push(arg);
  305. },
  306. //
  307. // Implements \style{style-string}{math} with style filter
  308. //
  309. STYLE_attribute: function (name) {
  310. var style = SAFE.filterStyles(this.GetArgument(name)),
  311. arg = this.GetArgumentMML(name);
  312. if (style) {
  313. if (arg.style != null) {
  314. if (style.charAt(style.length-1) !== ";") {style += ";"}
  315. style = arg.style + " " + style;
  316. }
  317. arg.With({style: style});
  318. }
  319. this.Push(arg);
  320. },
  321. //
  322. // Implements \cssId{id}{math} with ID filter
  323. //
  324. ID_attribute: function (name) {
  325. var ID = SAFE.filterID(this.GetArgument(name)),
  326. arg = this.GetArgumentMML(name);
  327. if (ID) {arg.With({id:ID})}
  328. this.Push(arg);
  329. }
  330. });
  331. });
  332. HUB.Register.StartupHook("TeX Jax Ready",function () {
  333. var TEX = MathJax.InputJax.TeX,
  334. PARSE = TEX.Parse, METHOD = SAFE.filter;
  335. PARSE.Augment({
  336. //
  337. // Implements \require{name} with filtering
  338. //
  339. Require: function (name) {
  340. var file = this.GetArgument(name).replace(/.*\//,"").replace(/[^a-z0-9_.-]/ig,"");
  341. file = SAFE.filterRequire(file);
  342. if (file) {this.Extension(null,file)}
  343. },
  344. //
  345. // Controls \mmlToken attributes
  346. //
  347. MmlFilterAttribute: function (name,value) {
  348. if (METHOD[name]) {value = SAFE[METHOD[name]](value)}
  349. return value;
  350. },
  351. //
  352. // Handles font size macros with filtering
  353. //
  354. SetSize: function (name,size) {
  355. size = SAFE.filterSize(size);
  356. if (size) {
  357. this.stack.env.size = size;
  358. this.Push(TEX.Stack.Item.style().With({styles: {mathsize: size+"em"}}));
  359. }
  360. }
  361. });
  362. });
  363. HUB.Register.StartupHook("TeX bbox Ready",function () {
  364. var TEX = MathJax.InputJax.TeX;
  365. //
  366. // Filter the styles for \bbox
  367. //
  368. TEX.Parse.Augment({
  369. BBoxStyle: function (styles) {return SAFE.filterStyles(styles)},
  370. BBoxPadding: function (pad) {
  371. var styles = SAFE.filterStyles("padding: "+pad);
  372. return (styles ? pad : 0);
  373. }
  374. });
  375. });
  376. HUB.Register.StartupHook("MathML Jax Ready",function () {
  377. var PARSE = MathJax.InputJax.MathML.Parse,
  378. METHOD = SAFE.filter;
  379. //
  380. // Filter MathML attributes
  381. //
  382. PARSE.Augment({
  383. filterAttribute: function (name,value) {
  384. if (METHOD[name]) {value = SAFE[METHOD[name]](value)}
  385. return value;
  386. }
  387. });
  388. });
  389. // MathML input (href, style, fontsize, class, id)
  390. HUB.Startup.signal.Post("Safe Extension Ready");
  391. AJAX.loadComplete("[MathJax]/extensions/Safe.js");
  392. })(MathJax.Hub,MathJax.Ajax);