editorplus.js (7823B)
1 import { select, dispatch } from '@wordpress/data' 2 3 /** 4 * Will check if the given CSSRule contains malicious 3rd party URL to secure against XSS 5 * @param {CSSRule} rule 6 * @return {boolean} isMalicious 7 */ 8 9 function _hasMaliciousURL(rule) { 10 11 let isMalicious = false 12 13 if (!(rule instanceof CSSRule)) return false 14 15 // only allowing airtable API origin 16 let allowedOrigins = [ 'https://dl.airtable.com' ] 17 18 let urlRegex = /[(http(s)?)://(www.)?a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g 19 20 let matchedURLS = rule.cssText.match(urlRegex) ?? [] 21 22 for (const requestURL of matchedURLS) { 23 24 try { 25 let parsedURL = new URL(requestURL) 26 let isNotAllowed = !allowedOrigins.includes(parsedURL.origin) 27 28 if (isNotAllowed) { 29 isMalicious = true 30 break 31 } 32 } catch (e) { 33 34 // verifying if the regex matched a URL, because regex can mess up due to URL in between other strings 35 let isUrl = ['https://', 'http://', '.com'].some(urlPart => requestURL.indexOf(urlPart) !== -1) 36 let isVerifiedOrigin = requestURL.indexOf(allowedOrigins[0]) !== -1 37 38 if (isUrl && !isVerifiedOrigin) { 39 isMalicious = true 40 break 41 } 42 43 } 44 45 } 46 47 return isMalicious 48 } 49 50 /** 51 * Will inject the given css as an stylesheet in the editor 52 * @param {string} css 53 * @return {void} 54 */ 55 56 function injectStyleSheetInEditor(css = window.wp.data.select('core/editor').getEditedPostAttribute('meta')?.extendify_custom_stylesheet ?? '') { 57 if (typeof css !== 'string') return 58 59 css = css.replace(/(.eplus_styles)/g, '') 60 61 let extendifyRoot = document.querySelector('#extendify-root') 62 let styleID = 'extendify-custom-stylesheet' 63 64 if (document.getElementById(styleID)) { 65 // stylesheet already exists 66 document.getElementById(styleID).innerHTML = css 67 } else { 68 let styleElement = document.createElement('style') 69 70 styleElement.id = styleID 71 styleElement.type = 'text/css' 72 73 styleElement.appendChild(document.createTextNode(css)) 74 extendifyRoot.appendChild(styleElement) 75 } 76 } 77 78 /** 79 * Will provide filtered css from the given sheet 80 * @param {CSSStyleSheet} sheet 81 * @param {string[]} prefix 82 * @return {string} css - filtered css 83 */ 84 85 function filterStylesheetWithPrefix(sheet, allowedPrefixes) { 86 let filteredCSS = '' 87 88 let isPrefixed = selector => { 89 return allowedPrefixes.some(allowedPrefix => selector.startsWith(allowedPrefix)) 90 } 91 92 for (const rule of sheet?.cssRules ?? []) { 93 // if it's a media rule we need to also process the nested rule list 94 if (rule instanceof CSSMediaRule) { 95 96 if (_hasMaliciousURL(rule)) continue 97 98 let processedMediaRule = rule?.cssRules ?? [] 99 let rulesToDelete = [] // because deleting them in the loop can disturb the index 100 101 for (const mediaRuleIndex of Object.keys(processedMediaRule)) { 102 let mediaRule = mediaRuleIndex in processedMediaRule 103 ? processedMediaRule[mediaRuleIndex] 104 : {} 105 106 if (!isPrefixed(mediaRule.selectorText)) { 107 rulesToDelete.push(mediaRuleIndex) 108 } 109 } 110 111 for (const mediaRuleIndexToDelete of rulesToDelete) { 112 rule.deleteRule(mediaRuleIndexToDelete) 113 } 114 115 filteredCSS += rule.cssText 116 } 117 118 if (rule instanceof CSSStyleRule) { 119 if (_hasMaliciousURL(rule)) continue 120 121 filteredCSS += isPrefixed(rule.selectorText) 122 ? rule.cssText 123 : '' 124 } 125 } 126 127 return filteredCSS 128 } 129 130 /** 131 * Listener to enable page template 132 */ 133 window._wpLoadBlockEditor && window.addEventListener('extendify-sdk::template-inserted', (event) => { 134 const { template } = event.detail 135 const wpTemplateName = 'editorplus-template.php' 136 137 // check if the instruction has command to enable page 138 if (!template?.fields?.instructions?.includes('enable_page_template')) { 139 return 140 } 141 142 // Get a list of templates from the editor 143 const selector = select('core/editor') 144 const availablePageTemplates = selector.getEditorSettings()?.availableTemplates ?? {} 145 if (!Object.keys(availablePageTemplates).includes(wpTemplateName)) { 146 return 147 } 148 149 // Finally, set the template 150 dispatch('core/editor').editPost({ 151 template: wpTemplateName, 152 }) 153 }) 154 155 /** 156 * Listener to inject stylesheet 157 */ 158 window._wpLoadBlockEditor && window.addEventListener('extendify-sdk::template-inserted', async (event) => { 159 160 // TODO: use better approach which does not use require additional network request 161 162 const { template } = event.detail 163 const stylesheetURL = template?.fields?.stylesheet ?? '' 164 165 if (!stylesheetURL) { 166 return 167 } 168 169 try { 170 let generatedCSS = await (await fetch(stylesheetURL)).text() 171 let appendedCSS = select('core/editor').getEditedPostAttribute('meta')?.extendify_custom_stylesheet ?? '' 172 173 let createdStyleElement = document.createElement('style') 174 let createdStyleID = 'extendify-stylesheet' 175 176 // webkit hack: appending stylesheet to let DOM process rules 177 178 createdStyleElement.id = createdStyleID 179 createdStyleElement.type = 'text/css' 180 createdStyleElement.appendChild(document.createTextNode(generatedCSS)) 181 182 document.querySelector('#extendify-root').appendChild(createdStyleElement) 183 184 let processedStyleSheet = document.getElementById(createdStyleID) 185 186 // disabling the stylesheet 187 processedStyleSheet.sheet.disable = true 188 189 // accessing processed CSSStyleSheet 190 let filteredCSS = filterStylesheetWithPrefix(processedStyleSheet?.sheet, ['.extendify-', '.eplus_styles', '.eplus-', '[class*="extendify-"]', '[class*="extendify"]']) 191 192 // merging existing styles 193 filteredCSS += appendedCSS 194 195 // deleting the generated stylesheet 196 processedStyleSheet.parentNode.removeChild(processedStyleSheet) 197 198 // injecting the stylesheet to style the editor view 199 injectStyleSheetInEditor(filteredCSS) 200 201 // finally, updating the metadata 202 await dispatch('core/editor').editPost({ 203 meta: { 204 extendify_custom_stylesheet: filteredCSS, 205 }, 206 }) 207 208 } catch (error) { 209 console.error(error) 210 } 211 }) 212 213 // loading stylesheet in the editor after page load 214 window._wpLoadBlockEditor && window.wp.domReady(() => { 215 setTimeout(() => injectStyleSheetInEditor(), 0) 216 }) 217 218 // Quick method to hide the title if the template is active 219 let extendifyCurrentPageTemplate 220 window._wpLoadBlockEditor && window.wp.data.subscribe(() => { 221 // Nothing changed 222 if (extendifyCurrentPageTemplate && extendifyCurrentPageTemplate === window.wp.data.select('core/editor').getEditedPostAttribute('template')) { 223 return 224 } 225 const epTemplateSelected = window.wp.data.select('core/editor').getEditedPostAttribute('template') === 'editorplus-template.php' 226 const title = document.querySelector('.edit-post-visual-editor__post-title-wrapper') 227 const wrapper = document.querySelector('.editor-styles-wrapper') 228 229 // Too early 230 if (!title || !wrapper) { 231 return 232 } 233 234 if (epTemplateSelected) { 235 // GB needs to compute the height first 236 Promise.resolve().then(() => title.style.display = 'none') 237 wrapper.style.paddingTop = '0' 238 wrapper.style.backgroundColor = '#ffffff' 239 } else { 240 title.style.removeProperty('display') 241 wrapper.style.removeProperty('padding-top') 242 wrapper.style.removeProperty('background-color') 243 } 244 })