balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

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 })