├── .wp-env.json ├── LICENSE ├── README.md ├── SECURITY.md ├── blueprint.json ├── build ├── index.asset.php └── index.js ├── composer.json ├── composer.lock ├── governance-rules.json ├── governance-schema.json ├── governance ├── analytics.php ├── block-locking.php ├── governance-utilities.php ├── init-governance.php ├── nested-governance-processing.php ├── rest │ └── rest-api.php ├── rules-parser.php └── settings │ ├── settings-view.php │ ├── settings.css │ ├── settings.js │ └── settings.php ├── package-lock.json ├── package.json ├── playwright.config.js ├── src ├── block-locking.jsx ├── block-utils.js ├── block-utils.test.js ├── index.js ├── nested-governance-loader.js └── nested-governance-loader.test.js ├── vendor ├── autoload.php ├── composer │ ├── ClassLoader.php │ ├── InstalledVersions.php │ ├── LICENSE │ ├── autoload_classmap.php │ ├── autoload_namespaces.php │ ├── autoload_psr4.php │ ├── autoload_real.php │ ├── autoload_static.php │ ├── installed.json │ ├── installed.php │ └── platform_check.php └── seld │ └── jsonlint │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── bin │ └── jsonlint │ ├── composer.json │ └── src │ └── Seld │ └── JsonLint │ ├── DuplicateKeyException.php │ ├── JsonParser.php │ ├── Lexer.php │ ├── ParsingException.php │ └── Undefined.php ├── vip-governance.php └── webpack.config.js /.wp-env.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ "." ], 3 | "mappings": { 4 | "wp-content/private": "./tests/private" 5 | }, 6 | "config": { 7 | "WPCOM_VIP_PRIVATE_DIR": "/var/www/html/wp-content/private", 8 | "WPCOMVIP_GOVERNANCE_ROOT_PLUGIN_DIR": "/wp-content/plugins/vip-governance-plugin" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | WPVIP and Automattic take security bugs seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. 4 | 5 | To report a security issue, please visit [Automattic's HackerOne](https://hackerone.com/automattic) program. 6 | -------------------------------------------------------------------------------- /blueprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://playground.wordpress.net/blueprint-schema.json", 3 | "landingPage": "/wp-admin/admin.php?page=vip-block-governance", 4 | "preferredVersions": { 5 | "php": "8.0", 6 | "wp": "latest" 7 | }, 8 | "features": { 9 | "networking": true 10 | }, 11 | "login": true, 12 | "steps": [ 13 | { 14 | "step": "installPlugin", 15 | "pluginZipFile": { 16 | "resource": "url", 17 | "url": "https://github-proxy.com/proxy/?repo=Automattic/vip-governance-plugin" 18 | }, 19 | "options": { 20 | "activate": true 21 | } 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /build/index.asset.php: -------------------------------------------------------------------------------- 1 | array('react', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-data', 'wp-hooks', 'wp-i18n', 'wp-notices'), 'version' => '8b428941df826bc15793'); 2 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var e={122:(e,t,o)=>{o.d(t,{G:()=>d});var n=o(609),r=o(715),i=o(427),c=o(491),s=o(143),l=o(619),a=o(418);function d(e){const t=(0,c.createHigherOrderComponent)((t=>o=>{const{name:c,clientId:d}=o,{getBlockParents:u,getBlockName:g}=(0,s.select)(r.store),v=u(d,!0),m=v.some((e=>function(e){return e in p}(e)));if(m)return(0,n.createElement)(t,{...o});const f=v.map((e=>g(e)));let w=(0,a.yw)(c,f,e);if(w=(0,l.applyFilters)("vip_governance__is_block_allowed_for_editing",w,c,f,e),w)return(0,n.createElement)(t,{...o});if(wp?.blockEditor?.useBlockEditingMode){const{useBlockEditingMode:e}=wp.blockEditor;e("disabled")}return function(e){p[e]=!0}(d),(0,n.createElement)(n.Fragment,null,(0,n.createElement)(i.Disabled,null,(0,n.createElement)("div",{style:{opacity:.6,backgroundColor:"#eee",border:"2px dashed #999"}},(0,n.createElement)(t,{...o}))))}),"withDisabledBlocks");(0,l.addFilter)("editor.BlockEdit","wpcomvip-governance/with-disabled-blocks",t)}const p={}},418:(e,t,o)=>{o.d(t,{aV:()=>s,yw:()=>c});var n=o(619),r=o(325);const i={"core/list":["core/list-item"],"core/columns":["core/column"],"core/page-list":["core/page-list-item"],"core/navigation":["core/navigation-link","core/navigation-submenu"],"core/navigation-link":["core/navigation-link","core/navigation-submenu","core/page-list"],"core/quote":["core/paragraph"],"core/media-text":["core/paragraph"],"core/social-links":["core/social-link"],"core/comments-pagination":["core/comments-pagination-previous","core/comments-pagination-numbers","core/comments-pagination-next"]};function c(e,t,o){const c=(0,n.applyFilters)("vip_governance__is_block_allowed_in_hierarchy",!0,e,t,o)||0===t.length?[...o.allowedBlocks]:[];if(t.length>0){if(i[t[0]]&&i[t[0]].includes(e))return!0;if(o.blockSettings){const e=(0,r.W)(t.reverse(),"allowedBlocks",o.blockSettings);e&&e.value&&c.push(...e.value)}}return function(e,t){return t.some((t=>s(e,t)))}(e,c)}function s(e,t){return t.includes("*")?e.match(new RegExp(t.replace("*",".*"))):t===e}},325:(e,t,o)=>{function n(e,t={},o=!1){const r=["allowedBlocks"];for(const[s,l]of Object.entries(e))if(!r.includes(s))if(s.includes("/")||"*"===s)Object.entries(e).forEach((([e,o])=>{r.includes(e)||n(o,t,e)}));else if(!1!==o){var i;const e=c(l,`${s}.`);t[o]={...null!==(i=t[o])&&void 0!==i?i:{},...e}}return t}function r(e,t,o,n={depth:0,value:void 0},c=1){const[s,...l]=e,a=o[s];if(0===l.length){const e=i(a,t);return void 0!==e&&c>=n.depth&&(n.depth=c,n.value=e),n}return void 0!==a&&(n=r(l,t,a,n,c+1)),r(l,t,o,n,c)}function i(e,t,o=void 0){const n=Array.isArray(t)?t:t.replace(/(\[(\d)\])/g,".$2").replace(/^\./,"").split(".");if(!n.length||void 0===n[0])return e;const r=n[0];return"object"==typeof e&&null!==e&&r in e&&void 0!==e[r]?i(e[r],n.slice(1),o):o}function c(e,t=""){const o={};return Object.entries(e).forEach((([e,n])=>{"object"==typeof n&&Boolean(n)&&!Array.isArray(n)?(o[`${t}${e}`]=!0,Object.assign(o,c(n,`${t}${e}.`))):o[`${t}${e}`]=!0})),o}o.d(t,{S:()=>n,W:()=>r})},609:e=>{e.exports=window.React},715:e=>{e.exports=window.wp.blockEditor},427:e=>{e.exports=window.wp.components},491:e=>{e.exports=window.wp.compose},143:e=>{e.exports=window.wp.data},619:e=>{e.exports=window.wp.hooks},723:e=>{e.exports=window.wp.i18n},692:e=>{e.exports=window.wp.notices}},t={};function o(n){var r=t[n];if(void 0!==r)return r.exports;var i=t[n]={exports:{}};return e[n](i,i.exports,o),i.exports}o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var n in t)o.o(t,n)&&!o.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var n=o(715),r=o(143),i=o(619),c=o(723),s=o(692),l=o(122),a=o(418),d=o(325);!function(){if(VIP_GOVERNANCE.error)return void(0,r.dispatch)(s.store).createErrorNotice(VIP_GOVERNANCE.error,{id:"wpcomvip-governance-error",isDismissible:!0,actions:[{label:(0,c.__)("Open governance settings"),url:VIP_GOVERNANCE.urlSettingsPage}]});const e=VIP_GOVERNANCE.governanceRules;(0,i.addFilter)("blockEditor.__unstableCanInsertBlockType","wpcomvip-governance/block-insertion",((t,o,c,{getBlock:s})=>{if(!1===t)return t;let l=[];if(c){const{getBlockParents:e,getBlockName:t}=(0,r.select)(n.store),o=s(c),i=e(c,!0);l=[o.clientId,...i].map((e=>t(e)))}const d=(0,a.yw)(o.name,l,e);return(0,i.applyFilters)("vip_governance__is_block_allowed_for_insertion",d,o.name,l,e)}));const t=VIP_GOVERNANCE.nestedSettings,o=(0,d.S)(t),p={},u={};for(const e in o)-1===e.indexOf("*")?u[e]=o[e]:p[e]=o[e];(0,i.addFilter)("blockEditor.useSetting.before","wpcomvip-governance/nested-block-settings",((e,o,i,c)=>{if(!c)return e;if(void 0!==u[c]&&!0===u[c][o]){const c=[i,...(0,r.select)(n.store).getBlockParents(i,!0)].map((e=>(0,r.select)(n.store).getBlockName(e))).reverse();return({value:e}=(0,d.W)(c,o,t)),e&&e.theme?e.theme:e}if(0!==p.length)for(const s in p)if((0,a.aV)(c,s)&&!0===p[s][o]){const c=[i,...(0,r.select)(n.store).getBlockParents(i,!0)].map((e=>(0,r.select)(n.store).getBlockName(e))).reverse();return-1!==s.indexOf("*")&&(c[c.length-1]=s),({value:e}=(0,d.W)(c,o,t)),e&&e.theme?e.theme:e}return e})),e?.allowedBlocks&&(0,l.G)(e)}()})(); 2 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "automattic/vip-governance", 3 | "description": "A WordPress plugin that adds governance to the block editor", 4 | "type": "wordpress-plugin", 5 | "license": "GPL-2.0-or-later", 6 | "scripts": { 7 | "lint": "phpcs", 8 | "phpcs": "phpcs", 9 | "phpcs-fix": "phpcbf", 10 | "test": "wp-env run tests-cli --env-cwd=wp-content/plugins/vip-governance-plugin ./vendor/bin/phpunit -c phpunit.xml.dist", 11 | "test-multisite": "wp-env run tests-cli --env-cwd=wp-content/plugins/vip-governance-plugin ./vendor/bin/phpunit -c tests/phpunit/multisite.xml" 12 | }, 13 | "require": { 14 | "php": ">=8.0", 15 | "seld/jsonlint": "^1.10" 16 | }, 17 | "require-dev": { 18 | "automattic/vipwpcs": "3.0.0", 19 | "phpcompatibility/phpcompatibility-wp": "2.1.4", 20 | "phpunit/phpunit": "9.6.13", 21 | "yoast/phpunit-polyfills": "2.0.0" 22 | }, 23 | "config": { 24 | "allow-plugins": { 25 | "dealerdirect/phpcodesniffer-composer-installer": true, 26 | "mnsami/composer-custom-directory-installer": false 27 | }, 28 | "sort-packages": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /governance-rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://api.wpvip.com/schemas/plugins/governance.json", 3 | "version": "1.0.0", 4 | "rules": [ 5 | { 6 | "type": "default", 7 | "allowedFeatures": [ "codeEditor", "lockBlocks" ], 8 | "allowedBlocks": [ "*" ] 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /governance-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft-07/schema", 3 | "title": "VIP governance rules JSON schema", 4 | "definitions": { 5 | "allowedFeaturesArray": { 6 | "description": "Array of allowed features for this rule. Supported features are: codeEditor and lockBlocks", 7 | "type": "array", 8 | "items": { 9 | "type": "string", 10 | "enum": [ "codeEditor", "lockBlocks" ] 11 | }, 12 | "uniqueItems": true, 13 | "examples": [ [ "codeEditor", "lockBlocks" ] ] 14 | }, 15 | "allowedBlocksArray": { 16 | "description": "Array of allowed blocks for this rule. Blocks can contain asterisks for pattern matching.", 17 | "type": "array", 18 | "items": [ 19 | { 20 | "type": "string", 21 | "examples": [ "*", "core/*", "core/paragraph", "core/heading" ] 22 | } 23 | ] 24 | }, 25 | "blockSettingsProperties": { 26 | "description": "Block settings for block or nested inner block types", 27 | "type": "object", 28 | "properties": { 29 | "allowedBlocks": { 30 | "$ref": "#/definitions/allowedBlocksArray" 31 | } 32 | }, 33 | "patternProperties": { 34 | "^[a-z][a-z0-9-]*/[a-z][a-z0-9-]*$": { 35 | "$ref": "#/definitions/blockSettingsProperties" 36 | }, 37 | "^[a-z][a-z0-9-]*/[*]$": { 38 | "$ref": "#/definitions/blockSettingsProperties" 39 | } 40 | } 41 | }, 42 | "ruleBlockSettingsProperties": { 43 | "description": "Theme.json block settings for this rule. Allows block nesting and allowedBlocks array.", 44 | "type": "object", 45 | "patternProperties": { 46 | "^[a-z][a-z0-9-]*/[a-z][a-z0-9-]*$": { 47 | "$ref": "#/definitions/blockSettingsProperties" 48 | }, 49 | "^[a-z][a-z0-9-]*/[*]$": { 50 | "$ref": "#/definitions/blockSettingsProperties" 51 | } 52 | }, 53 | "additionalProperties": false, 54 | "examples": [ 55 | { 56 | "core/paragraph": { 57 | "color": { 58 | "gradients": [ 59 | { 60 | "slug": "vertical-red-to-green", 61 | "gradient": "linear-gradient(to bottom,#ff0000 0%,#00FF00 100%)", 62 | "name": "Vertical red to green" 63 | } 64 | ] 65 | } 66 | }, 67 | "core/*": { 68 | "color": { 69 | "text": false 70 | } 71 | }, 72 | "core/quote": { 73 | "allowedBlocks": [ "core/paragraph", "core/heading" ] 74 | }, 75 | "core/media-text": { 76 | "core/heading": { 77 | "typography": { 78 | "customFontSize": false 79 | } 80 | } 81 | } 82 | } 83 | ] 84 | }, 85 | "ruleDefaultProperties": { 86 | "type": "object", 87 | "properties": { 88 | "type": { 89 | "description": "Default rule - applies additively to all users.", 90 | "type": "string", 91 | "enum": [ "default" ] 92 | }, 93 | "allowedFeatures": { 94 | "$ref": "#/definitions/allowedFeaturesArray" 95 | }, 96 | "allowedBlocks": { 97 | "$ref": "#/definitions/allowedBlocksArray" 98 | }, 99 | "blockSettings": { 100 | "$ref": "#/definitions/ruleBlockSettingsProperties" 101 | } 102 | }, 103 | "required": [ "type" ], 104 | "additionalProperties": false 105 | }, 106 | "ruleRoleProperties": { 107 | "type": "object", 108 | "properties": { 109 | "type": { 110 | "description": "Role rule - applies to one or more specific roles.", 111 | "type": "string", 112 | "enum": [ "role" ] 113 | }, 114 | "roles": { 115 | "type": "array", 116 | "items": { 117 | "type": "string" 118 | }, 119 | "examples": [ [ "administrator", "editor" ] ], 120 | "minItems": 1 121 | }, 122 | "allowedFeatures": { 123 | "$ref": "#/definitions/allowedFeaturesArray" 124 | }, 125 | "allowedBlocks": { 126 | "$ref": "#/definitions/allowedBlocksArray" 127 | }, 128 | "blockSettings": { 129 | "$ref": "#/definitions/ruleBlockSettingsProperties" 130 | } 131 | }, 132 | "required": [ "type", "roles" ], 133 | "additionalProperties": false 134 | }, 135 | "rulePostTypeProperties": { 136 | "type": "object", 137 | "properties": { 138 | "type": { 139 | "description": "Post Type rule - applies to one or more specific post types.", 140 | "type": "string", 141 | "enum": [ "postType" ] 142 | }, 143 | "postTypes": { 144 | "type": "array", 145 | "items": { 146 | "type": "string" 147 | }, 148 | "examples": [ [ "page", "post" ] ], 149 | "minItems": 1 150 | }, 151 | "allowedFeatures": { 152 | "$ref": "#/definitions/allowedFeaturesArray" 153 | }, 154 | "allowedBlocks": { 155 | "$ref": "#/definitions/allowedBlocksArray" 156 | }, 157 | "blockSettings": { 158 | "$ref": "#/definitions/ruleBlockSettingsProperties" 159 | } 160 | }, 161 | "required": [ "type", "postTypes" ], 162 | "additionalProperties": false 163 | } 164 | }, 165 | "type": "object", 166 | "default": {}, 167 | "required": [ "rules", "version" ], 168 | "properties": { 169 | "$schema": { 170 | "description": "JSON schema URI for governance JSON", 171 | "type": "string" 172 | }, 173 | "version": { 174 | "description": "Version of governance JSON", 175 | "type": "string", 176 | "enum": [ "1.0.0" ] 177 | }, 178 | "rules": { 179 | "description": "Array of governance rules", 180 | "type": "array", 181 | "default": [], 182 | "items": { 183 | "type": "object", 184 | "oneOf": [ 185 | { "$ref": "#/definitions/ruleDefaultProperties" }, 186 | { "$ref": "#/definitions/ruleRoleProperties" }, 187 | { "$ref": "#/definitions/rulePostTypeProperties" } 188 | ] 189 | } 190 | } 191 | }, 192 | "additionalProperties": false 193 | } 194 | -------------------------------------------------------------------------------- /governance/analytics.php: -------------------------------------------------------------------------------- 1 | get_current_blog_id(), 54 | ]; 55 | 56 | /** 57 | * Filter the governance file path, based on the filter options provided. 58 | * 59 | * Currently supported keys: 60 | * 61 | * site_id: The site ID for the current site. 62 | * 63 | * @param string $governance_file_path Path to the governance file. 64 | * @param array $filter_options Options that can be used as a filter for determining the right file. 65 | */ 66 | $filter_file_path = apply_filters( 'vip_governance__governance_file_path', $governance_file_path, $filter_options ); 67 | 68 | // Make sure the path is normalized. Note that file_exists() is still needed at times. 69 | $filter_file_path = realpath( $filter_file_path ); 70 | 71 | // if the value is false, throw a file not found error right away. 72 | if ( false === $filter_file_path ) { 73 | return new WP_Error( 'governance-file-not-found', __( 'Governance rules could not be found.', 'vip-governance' ) ); 74 | } 75 | 76 | // Make sure the file is a JSON file. 77 | if ( $filter_file_path && $filter_file_path !== $governance_file_path && ! str_ends_with( $filter_file_path, '.json' ) ) { 78 | /* translators: %s: filter file path */ 79 | return new WP_Error( 'governance-file-not-json', sprintf( __( 'Governance rules (%s) must be a JSON file.', 'vip-governance' ), $filter_file_path ) ); 80 | } 81 | 82 | $governance_file_path = $filter_file_path; 83 | 84 | // Make sure the file exists. 85 | if ( ! file_exists( $governance_file_path ) ) { 86 | /* translators: %s: governance file name */ 87 | return new WP_Error( 'governance-file-not-found', sprintf( __( 'Governance rules (%s) could not be found.', 'vip-governance' ), $governance_file_path ) ); 88 | } 89 | 90 | // phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown 91 | $governance_rules_json = file_get_contents( $governance_file_path ); 92 | 93 | if ( false === $governance_rules_json ) { 94 | return new WP_Error( 'governance-file-not-readable', __( 'Governance rules could not be read from specified folder.', 'vip-governance' ) ); 95 | } 96 | 97 | /** 98 | * Filter the governance rules, based on the filter options provided. 99 | * 100 | * Currently supported keys: 101 | * 102 | * site_id: The site ID for the current site. 103 | * 104 | * This filter can be used to either modify the governance rules content before it's parsed, or to generate the content dynamically. 105 | * 106 | * @param string $governance_rules_json Governance rules content. 107 | * @param array $filter_options Options that can be used as a filter for determining the right rules. 108 | */ 109 | $governance_rules_json = apply_filters( 'vip_governance__governance_rules_json', $governance_rules_json, $filter_options ); 110 | 111 | return $governance_rules_json; 112 | } 113 | 114 | /** 115 | * Get the rules using the provided type. 116 | * 117 | * The default rule is the base upon which the other rules are built. Currently, that's postType and role. 118 | * 119 | * @param array $governance_rules Governance rules, not filtered based on the user role. 120 | * @param array $user_roles User roles for the current WP site. 121 | * @param array $post_type Post type for the current post. 122 | * 123 | * @return array Governance rules, filtered by the matching user role or post type. 124 | * 125 | * @access private 126 | */ 127 | public static function get_rules_by_type( $governance_rules, $user_roles = [], $post_type = '' ) { 128 | if ( empty( $governance_rules ) ) { 129 | return []; 130 | } 131 | 132 | // This is the case where its not called by the admin UI, but in factor by the editor. 133 | if ( empty( $user_roles ) && empty( $post_type ) ) { 134 | $current_user = wp_get_current_user(); 135 | $user_roles = $current_user->roles; 136 | $post_type = get_post_type(); 137 | } 138 | 139 | $allowed_features = []; 140 | $allowed_blocks = []; 141 | $block_settings = []; 142 | 143 | // Because PHP doesn't allow passing this in directly. 144 | $type_to_rules_map = RulesParser::TYPE_TO_RULES_MAP; 145 | 146 | // Assumption is that it's been ordered by priority, so it will process those rules first followed by default last. 147 | foreach ( RulesParser::RULE_TYPES as $priority ) { 148 | // look up the rule in $governance_rules where the field type matches priority. 149 | $governance_rules_for_priority = array_filter( $governance_rules, function ( $rule ) use ( $priority, $user_roles, $post_type, $type_to_rules_map ) { 150 | // Its required to have the type, and its corresponding types set unless you are the default rule in which case you only need type set to default. 151 | if ( isset( $rule['type'] ) && $priority === $rule['type'] && ( 'default' === $priority || isset( $rule[ $type_to_rules_map[ $priority ] ] ) ) ) { 152 | if ( 'default' === $priority ) { 153 | return true; 154 | } elseif ( 'role' === $priority ) { 155 | // Only give back true if the roles match the current user. 156 | return array_intersect( $user_roles, $rule['roles'] ); 157 | } elseif ( 'postType' === $priority ) { 158 | // Only give back true if the current post type matches the post types allowed. 159 | return in_array( $post_type, $rule['postTypes'], true ); 160 | } 161 | } 162 | 163 | // Rule should be ignored if it doesn't match the needed criteria for priorities. 164 | return false; 165 | } ); 166 | 167 | if ( ! empty( $governance_rules_for_priority ) ) { 168 | // Re-order the rule so that the 0 index is what's first, otherwise the index is preserved. 169 | $governance_rules_for_priority = array_values( $governance_rules_for_priority ); 170 | 171 | $allowed_blocks = self::get_allowed_blocks_or_features_for_rule_type( 'allowedBlocks', $allowed_blocks, $governance_rules_for_priority[0], $priority ); 172 | $block_settings = self::get_block_settings_for_rule_type( $block_settings, $governance_rules_for_priority[0], $priority ); 173 | $allowed_features = self::get_allowed_blocks_or_features_for_rule_type( 'allowedFeatures', $allowed_features, $governance_rules_for_priority[0], $priority ); 174 | } 175 | } 176 | 177 | // return array of allowed_blocks and block_settings. 178 | return [ 179 | 'allowedBlocks' => $allowed_blocks, 180 | 'blockSettings' => $block_settings, 181 | 'allowedFeatures' => $allowed_features, 182 | ]; 183 | } 184 | 185 | /** 186 | * Get the new allowedBlocks or allowedFeatures based on the rule type 187 | * 188 | * The default rule's allowedBlocks and allowedFeatures is combined with the other rule types. 189 | * For non-default rule types, only one allowedBlocks and allowedFeatures can be picked. It's not combined together. 190 | * 191 | * @param string $allowed_type allowedBlocks or allowedFeatures. 192 | * @param array $allowed_blocks_or_features allowedBlocks or allowedFeatures that have been combined so far. 193 | * @param array $governance_rule current rule being processed. 194 | * @param string $rule_type type of rule being processed. 195 | * @return array allowedBlocks or allowedFeatures that have been combined so far. 196 | */ 197 | private static function get_allowed_blocks_or_features_for_rule_type( $allowed_type, $allowed_blocks_or_features, $governance_rule, $rule_type ) { 198 | if ( isset( $governance_rule[ $allowed_type ] ) ) { 199 | // For the default rule the allowedBlocks and allowedFeatures are combined together. 200 | // Otherwise, there can only be one. 201 | if ( 'default' === $rule_type ) { 202 | return [ ...$allowed_blocks_or_features, ...$governance_rule[ $allowed_type ] ]; 203 | } else { 204 | $allowed_blocks_or_features = $governance_rule[ $allowed_type ]; 205 | } 206 | } 207 | 208 | return $allowed_blocks_or_features; 209 | } 210 | 211 | /** 212 | * Get the new blockSettings based on the rule type 213 | * 214 | * The default rule's blockSettings is combined with the other rule types. 215 | * For non-default rule types, only one blockSettings can be picked. It's not combined together. 216 | * 217 | * @param array $block_settings blockSettings that have been combined so far. 218 | * @param array $governance_rule current rule being processed. 219 | * @param string $rule_type type of rule being processed. 220 | * @return array blockSettings that have been combined so far. 221 | */ 222 | private static function get_block_settings_for_rule_type( $block_settings, $governance_rule, $rule_type ) { 223 | if ( isset( $governance_rule['blockSettings'] ) ) { 224 | // For the default rule the blockSettings are combined together. 225 | // Otherwise, there can only be one. 226 | if ( 'default' === $rule_type ) { 227 | return array_merge_recursive( $block_settings, $governance_rule['blockSettings'] ); 228 | } else { 229 | $block_settings = $governance_rule['blockSettings']; 230 | } 231 | } 232 | 233 | return $block_settings; 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /governance/init-governance.php: -------------------------------------------------------------------------------- 1 | self::$governance_configuration['error'], 75 | 'governanceRules' => self::$governance_configuration['governanceRules'], 76 | 'nestedSettings' => isset( $nested_settings_and_css['settings'] ) ? $nested_settings_and_css['settings'] : [], 77 | 'urlSettingsPage' => menu_page_url( Settings::MENU_SLUG, /* display */ false ), 78 | ]); 79 | } 80 | 81 | /** 82 | * Load the CSS necessary for the block editor UI. 83 | * 84 | * @return void 85 | * 86 | * @access private 87 | */ 88 | public static function load_css() { 89 | if ( ! Settings::is_enabled() ) { 90 | return; 91 | } elseif ( empty( self::$governance_configuration ) ) { 92 | self::$governance_configuration = self::load_governance_configuration(); 93 | } 94 | 95 | $nested_settings_and_css = self::$governance_configuration['nestedSettingsAndCss']; 96 | 97 | // Hack to load the CSS dynamically for the block editor without needing a blank CSS file. 98 | wp_register_style( 'wpcomvip-governance', false, [], WPCOMVIP__GOVERNANCE__PLUGIN_VERSION ); 99 | wp_enqueue_style( 'wpcomvip-governance' ); 100 | wp_add_inline_style( 'wpcomvip-governance', $nested_settings_and_css['css'] ?? '' ); 101 | } 102 | 103 | /** 104 | * Load the governance configuration, based on the user role and ensure the rules are valid. 105 | * 106 | * @return array Governance rules, based on the user role. 107 | */ 108 | private static function load_governance_configuration() { 109 | $governance_error = false; 110 | $governance_rules_for_user = []; 111 | $nested_settings_and_css = []; 112 | 113 | try { 114 | $parsed_governance_rules = GovernanceUtilities::get_parsed_governance_rules(); 115 | 116 | if ( is_wp_error( $parsed_governance_rules ) ) { 117 | $governance_error = __( 'Governance rules could not be loaded.' ); 118 | } else { 119 | $governance_rules_for_user = GovernanceUtilities::get_rules_by_type( $parsed_governance_rules ); 120 | $block_settings_for_user = $governance_rules_for_user['blockSettings']; 121 | $nested_settings_and_css = NestedGovernanceProcessing::get_nested_settings_and_css( $block_settings_for_user ); 122 | BlockLocking::init( $governance_rules_for_user['allowedFeatures'] ); 123 | 124 | $epoch_time = floor( microtime( true ) ); 125 | 126 | if ( ( $epoch_time % 10 ) === 0 ) { 127 | // Sample results. Only send analytics on 10% of configuration loads. 128 | Analytics::record_usage(); 129 | } 130 | } 131 | } catch ( Exception | Error $e ) { 132 | // This is an unexpected exception. Record error for follow-up with WPVIP customers. 133 | Analytics::record_error(); 134 | // ToDo: Log the error to QueryMonitor instead of doing this. 135 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 136 | error_log( $e->getMessage() ); 137 | 138 | $governance_error = __( 'Governance rules could not be loaded due to a plugin error.' ); 139 | } 140 | 141 | return [ 142 | 'error' => $governance_error, 143 | 'governanceRules' => $governance_rules_for_user, 144 | 'nestedSettingsAndCss' => $nested_settings_and_css, 145 | ]; 146 | } 147 | } 148 | 149 | InitGovernance::init(); 150 | -------------------------------------------------------------------------------- /governance/nested-governance-processing.php: -------------------------------------------------------------------------------- 1 | get_all_registered(); 50 | // Get the map of paths to block settings, and their corresponding CSS selectors. 51 | $setting_nodes = static::get_settings_of_blocks( $blocks_registered, $governance_rules ); 52 | 53 | if ( class_exists( 'WP_Theme_JSON_Gutenberg' ) ) { 54 | $presets_metadata = WP_Theme_JSON_Gutenberg::PRESETS_METADATA; 55 | } else { 56 | $presets_metadata = WP_Theme_JSON::PRESETS_METADATA; 57 | } 58 | 59 | // Get the massaged settings and css together for the nested blocks. 60 | self::$nested_settings_and_css = static::get_css_and_theme_settings( $governance_rules, $setting_nodes, $presets_metadata ); 61 | 62 | return self::$nested_settings_and_css; 63 | } 64 | 65 | /** 66 | * Builds the metadata for settings.blocks, whilst ensuring support for nested blocks. This returns in the form of: 67 | * 68 | * [ 69 | * [ 70 | * 'path' => ['path', 'to', 'some', 'node' ], 71 | * 'selector' => 'CSS selector for some node' 72 | * ], 73 | * [ 74 | * 'path' => [ 'path', 'to', 'other', 'node' ], 75 | * 'selector' => 'CSS selector for other node' 76 | * ], 77 | * ] 78 | * 79 | * @param array $blocks_registered List of valid blocks. 80 | * @param array $current_block The current block to break down. 81 | * @param array $nodes The metadata of the nodes that have been built so far. 82 | * @param array $current_selector The current selector of the current block. 83 | * @param array $current_path The current path to the block. 84 | * 85 | * @return array 86 | */ 87 | private static function get_settings_of_blocks( $blocks_registered, $current_block, $nodes = [], $current_selector = null, $current_path = [] ) { 88 | foreach ( $current_block as $block_name => $block ) { 89 | if ( ! self::is_block_supported( $block_name, $blocks_registered ) ) { 90 | continue; 91 | } 92 | 93 | $selector = is_null( $current_selector ) ? null : $current_selector; 94 | 95 | // If the block name ends with /* or is just *, then it's a wildcard rule and we need to use the wp-block selector to match against any block. 96 | if ( str_ends_with( $block_name, '/*' ) || ( '*' === $block_name ) ) { 97 | // Due to the fact that the paragraph block doesn't have a wp-block-paragraph class, we need to add it manually. 98 | // In addition, wp-block is prefixed to the block name so this allows us to target all blocks. 99 | $looked_up_selector = 'p, [class*=wp-block]'; 100 | } elseif ( function_exists( ( 'wp_get_block_css_selector' ) ) ) { 101 | $looked_up_selector = wp_get_block_css_selector( $blocks_registered[ $block_name ] ); 102 | } else { 103 | // ToDo: Once our minimum WordPress version >= 6.3, this can be deleted. 104 | $looked_up_selector = self::get_css_selector_for_block( $block_name, $blocks_registered ); 105 | } 106 | 107 | if ( ! is_null( $looked_up_selector ) ) { 108 | $selector = $selector . ' ' . $looked_up_selector; 109 | } 110 | 111 | $path = empty( $current_path ) ? array( 'settings', 'blocks' ) : $current_path; 112 | array_push( $path, $block_name ); 113 | 114 | $nodes[] = array( 115 | 'path' => $path, 116 | 'selector' => $selector, 117 | ); 118 | 119 | $nodes = static::get_settings_of_blocks( $blocks_registered, $block, $nodes, $selector, $path ); 120 | } 121 | 122 | return $nodes; 123 | } 124 | 125 | /** 126 | * Validates if a block is supported. 127 | * 128 | * Supported blocks are blocks that are registered in the block registry, or blocks that are wildcard blocks. 129 | * 130 | * @param string $block_name The name of the block. 131 | * @param array $blocks_registered The blocks that are registered in the block registry. 132 | * @return boolean True if the block is supported, false otherwise. 133 | */ 134 | private static function is_block_supported( $block_name, $blocks_registered ) { 135 | return array_key_exists( $block_name, $blocks_registered ) || str_ends_with( $block_name, '/*' ) || ( '*' === $block_name ); 136 | } 137 | 138 | /** 139 | * Combine the block metadata with the presets to generate the nested settings and css, that would be used for nested governance 140 | * 141 | * @param [type] $governance_rules Governance rules to be used. 142 | * @param [type] $path_and_selector_of_blocks the map of paths and selectors for each block. 143 | * @param [type] $presets_metadata Preset metadata from Gutenberg/WordPress. 144 | * 145 | * @return array 146 | */ 147 | private static function get_css_and_theme_settings( $governance_rules, $path_and_selector_of_blocks, $presets_metadata ) { 148 | // Expected theme.json path. 149 | $theme_json = array( 150 | 'settings' => array( 151 | 'blocks' => $governance_rules, 152 | ), 153 | ); 154 | 155 | $stylesheet = ''; 156 | 157 | foreach ( $path_and_selector_of_blocks as $path_and_selector_of_block ) { 158 | foreach ( $presets_metadata as $preset_metadata ) { 159 | // Append the path of the property with the path from the block settings. 160 | $path = array_merge( $path_and_selector_of_block['path'], $preset_metadata['path'] ); 161 | // Get the preset value from the theme.json. 162 | $preset = _wp_array_get( $theme_json, $path, null ); 163 | if ( null !== $preset ) { 164 | // If the preset is not already keyed with an origin. 165 | if ( isset( $preset[0] ) || empty( $preset ) ) { 166 | // Add theme as the top level item for each preset value. 167 | _wp_array_set( $theme_json, $path, array( self::DEFAULT_ORIGIN => $preset ) ); 168 | } 169 | } 170 | 171 | if ( null === $path_and_selector_of_block['selector'] ) { 172 | continue; 173 | } 174 | 175 | $setting = _wp_array_get( $theme_json, $path_and_selector_of_block['path'], [] ); 176 | $declarations = []; 177 | $values_by_slug = static::get_settings_values_by_slug( $setting, $preset_metadata ); 178 | foreach ( $values_by_slug as $slug => $value ) { 179 | $declarations[] = array( 180 | 'name' => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ), 181 | 'value' => $value, 182 | ); 183 | } 184 | 185 | $stylesheet .= static::to_ruleset( $path_and_selector_of_block['selector'], $declarations ); 186 | 187 | $slugs = static::get_settings_slugs( $setting, $preset_metadata ); 188 | foreach ( $preset_metadata['classes'] as $class => $property ) { 189 | foreach ( $slugs as $slug ) { 190 | $css_var = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ); 191 | $class_name = static::replace_slug_in_string( $class, $slug ); 192 | 193 | // $selector is often empty, so we can save ourselves the `append_to_selector()` call then. 194 | $new_selector = '' === $path_and_selector_of_block['selector'] ? $class_name : static::append_to_selector( $path_and_selector_of_block['selector'], $class_name ); 195 | $stylesheet .= static::to_ruleset( 196 | $new_selector, 197 | array( 198 | array( 199 | 'name' => $property, 200 | 'value' => 'var(' . $css_var . ') !important', 201 | ), 202 | ) 203 | ); 204 | } 205 | } 206 | } 207 | } 208 | 209 | return array( 210 | 'settings' => $theme_json['settings']['blocks'], 211 | 'css' => $stylesheet, 212 | ); 213 | } 214 | 215 | /** 216 | * Appends a sub-selector to an existing one. 217 | * 218 | * Given the compounded $selector "h1, h2, h3" 219 | * and the $to_append selector ".some-class" the result will be 220 | * "h1.some-class, h2.some-class, h3.some-class". 221 | * 222 | * @since 5.8.0 223 | * @since 6.1.0 Added append position. 224 | * @since 6.3.0 Removed append position parameter. 225 | * 226 | * @param string $selector Original selector. 227 | * @param string $to_append Selector to append. 228 | * 229 | * @return string New selector. 230 | */ 231 | private static function append_to_selector( $selector, $to_append ) { 232 | if ( ! str_contains( $selector, ',' ) ) { 233 | return $selector . $to_append; 234 | } 235 | $new_selectors = []; 236 | $selectors = explode( ',', $selector ); 237 | foreach ( $selectors as $sel ) { 238 | $new_selectors[] = $sel . $to_append; 239 | } 240 | return implode( ',', $new_selectors ); 241 | } 242 | 243 | /** 244 | * Similar to get_settings_values_by_slug, but doesn't compute the value. 245 | * 246 | * @since 5.9.0 247 | * 248 | * @param array $settings Settings to process. 249 | * @param array $preset_metadata One of the PRESETS_METADATA values. 250 | * 251 | * @return array Array of presets where the key and value are both the slug. 252 | */ 253 | private static function get_settings_slugs( $settings, $preset_metadata ) { 254 | $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], [] ); 255 | 256 | $result = []; 257 | if ( isset( $preset_per_origin[ self::DEFAULT_ORIGIN ] ) ) { 258 | foreach ( $preset_per_origin[ self::DEFAULT_ORIGIN ] as $preset ) { 259 | $slug = _wp_to_kebab_case( $preset['slug'] ); 260 | 261 | $result[ $slug ] = $slug; 262 | } 263 | } 264 | 265 | return $result; 266 | } 267 | 268 | /** 269 | * Given a selector and a declaration list, 270 | * creates the corresponding ruleset. 271 | * 272 | * @since 5.8.0 273 | * 274 | * @param string $selector CSS selector. 275 | * @param array $declarations List of declarations. 276 | * 277 | * @return string Resulting CSS ruleset. 278 | */ 279 | private static function to_ruleset( $selector, $declarations ) { 280 | if ( empty( $declarations ) ) { 281 | return ''; 282 | } 283 | 284 | $declaration_block = array_reduce( 285 | $declarations, 286 | static function ( $carry, $element ) { 287 | return $carry .= $element['name'] . ': ' . $element['value'] . ';'; }, 288 | '' 289 | ); 290 | 291 | return $selector . '{' . $declaration_block . '}'; 292 | } 293 | 294 | /** 295 | * Gets preset values keyed by slugs based on settings and metadata. 296 | * 297 | * 298 | * $settings = array( 299 | * 'typography' => array( 300 | * 'fontFamilies' => array( 301 | * array( 302 | * 'slug' => 'sansSerif', 303 | * 'fontFamily' => '"Helvetica Neue", sans-serif', 304 | * ), 305 | * array( 306 | * 'slug' => 'serif', 307 | * 'colors' => 'Georgia, serif', 308 | * ) 309 | * ), 310 | * ), 311 | * ); 312 | * $meta = array( 313 | * 'path' => array( 'typography', 'fontFamilies' ), 314 | * 'value_key' => 'fontFamily', 315 | * ); 316 | * $values_by_slug = get_settings_values_by_slug(); 317 | * // $values_by_slug === array( 318 | * // 'sans-serif' => '"Helvetica Neue", sans-serif', 319 | * // 'serif' => 'Georgia, serif', 320 | * // ); 321 | * 322 | * 323 | * @since 5.9.0 324 | * 325 | * @param array $settings Settings to process. 326 | * @param array $preset_metadata One of the PRESETS_METADATA values. 327 | * 328 | * @return array Array of presets where each key is a slug and each value is the preset value. 329 | */ 330 | private static function get_settings_values_by_slug( $settings, $preset_metadata ) { 331 | $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], [] ); 332 | 333 | $result = []; 334 | 335 | if ( isset( $preset_per_origin[ self::DEFAULT_ORIGIN ] ) ) { 336 | foreach ( $preset_per_origin[ self::DEFAULT_ORIGIN ] as $preset ) { 337 | $slug = _wp_to_kebab_case( $preset['slug'] ); 338 | 339 | $value = ''; 340 | if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) { 341 | $value_key = $preset_metadata['value_key']; 342 | $value = $preset[ $value_key ]; 343 | } elseif ( 344 | isset( $preset_metadata['value_func'] ) && 345 | is_callable( $preset_metadata['value_func'] ) 346 | ) { 347 | $value_func = $preset_metadata['value_func']; 348 | $value = call_user_func( $value_func, $preset ); 349 | } else { 350 | // If we don't have a value, then don't add it to the result. 351 | continue; 352 | } 353 | 354 | $result[ $slug ] = $value; 355 | } 356 | } 357 | 358 | return $result; 359 | } 360 | 361 | /** 362 | * Transforms a slug into a CSS Custom Property. 363 | * 364 | * @since 5.9.0 365 | * 366 | * @param string $input String to replace. 367 | * @param string $slug Slug value to use to generate the custom property. 368 | * 369 | * @return string CSS Custom Property. Something along the lines of `--wp--preset--color--black`. 370 | */ 371 | private static function replace_slug_in_string( $input, $slug ) { 372 | return strtr( $input, array( '$slug' => $slug ) ); 373 | } 374 | 375 | /** 376 | * Get the CSS selector for a block using the block name 377 | * 378 | * This method is only used for WordPress versions below 6.3. After 6.3, we have a built in 379 | * way of accessing this selector. This will be deprecated once 6.3 is available for a 380 | * majority of VIP sites. 381 | * 382 | * @param string $block_name Name of the block. 383 | * @param array $blocks_registered Blocks that are allowed via the block registry. 384 | * 385 | * @return string the css selector for the block. 386 | */ 387 | private static function get_css_selector_for_block( $block_name, $blocks_registered ) { 388 | $block = $blocks_registered[ $block_name ]; 389 | if ( 390 | isset( $block->supports['__experimentalSelector'] ) && 391 | is_string( $block->supports['__experimentalSelector'] ) 392 | ) { 393 | return $block->supports['__experimentalSelector']; 394 | } else { 395 | return '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) ); 396 | } 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /governance/rest/rest-api.php: -------------------------------------------------------------------------------- 1 | 'GET', 39 | 'permission_callback' => [ __CLASS__, 'permission_callback' ], 40 | 'callback' => [ __CLASS__, 'get_governance_rules_for_rule_type' ], 41 | 'args' => [ 42 | 'role' => [ 43 | 'validate_callback' => function ( $param ) { 44 | $all_roles = array_keys( wp_roles()->roles ); 45 | $roles = [ strval( $param ) ]; 46 | return array_intersect( $all_roles, $roles ); 47 | }, 48 | 'sanitize_callback' => function ( $param ) { 49 | return strval( $param ); 50 | }, 51 | ], 52 | 'postType' => [ 53 | 'validate_callback' => function ( $param ) { 54 | $post_types = [ strval( $param ) ]; 55 | return array_intersect( get_post_types(), $post_types ); 56 | }, 57 | 'sanitize_callback' => function ( $param ) { 58 | return strval( $param ); 59 | }, 60 | ], 61 | ], 62 | ] ); 63 | } 64 | 65 | /** 66 | * Restrict the users that can access this rest API to be who can manage options only. 67 | * 68 | * @return bool True, if they are allow or false otherwise. 69 | * 70 | * @access private 71 | */ 72 | public static function permission_callback() { 73 | return current_user_can( 'manage_options' ); 74 | } 75 | 76 | /** 77 | * Get the governance rules specifically for a role. 78 | * 79 | * @param array $params Rest parameters. 80 | * 81 | * @return array Response containing the rules. 82 | * 83 | * @access private 84 | */ 85 | public static function get_governance_rules_for_rule_type( $params ) { 86 | $role = isset( $params['role'] ) ? [ $params['role'] ] : []; 87 | $post_type = $params['postType'] ?? ''; 88 | 89 | try { 90 | $parsed_governance_rules = GovernanceUtilities::get_parsed_governance_rules(); 91 | 92 | if ( is_wp_error( $parsed_governance_rules ) ) { 93 | return new WP_Error( 'vip-governance-rules-error', 'Error: Governance rules could not be loaded.', [ 'status' => 400 ] ); 94 | } else { 95 | return GovernanceUtilities::get_rules_by_type( $parsed_governance_rules, $role, $post_type ); 96 | } 97 | } catch ( Exception | Error $e ) { 98 | return new WP_Error( 'vip-governance-rules-error', 'Error: Governance rules could not be loaded due to a plugin error.', [ 'status' => 500 ] ); 99 | } 100 | } 101 | } 102 | 103 | RestApi::init(); 104 | -------------------------------------------------------------------------------- /governance/rules-parser.php: -------------------------------------------------------------------------------- 1 | 'roles', 22 | 'postType' => 'postTypes', 23 | ]; 24 | // Keep this order this way, as it's used for determing the priority of rules in governance-utilities. 25 | public const RULE_TYPES = [ 'postType', 'role', 'default' ]; 26 | private const RULE_KEYS_GENERAL = [ 'allowedFeatures', 'allowedBlocks', 'blockSettings' ]; 27 | 28 | /** 29 | * Parses and validates governance rules. 30 | * 31 | * @param string $rules_content Contents of rules file. 32 | * 33 | * @return array|WP_Error 34 | * 35 | * @access private 36 | */ 37 | public static function parse( $rules_content ) { 38 | if ( empty( $rules_content ) ) { 39 | // Allow an empty file to be valid for no rules. 40 | return []; 41 | } 42 | 43 | // Parse JSON from rules file. 44 | $rules_parsed = self::parse_rules_from_json( $rules_content ); 45 | 46 | if ( is_wp_error( $rules_parsed ) ) { 47 | return $rules_parsed; 48 | } elseif ( empty( $rules_parsed ) ) { 49 | // Allow an empty object to be valid for no rules. 50 | return []; 51 | } 52 | 53 | // Validate governance rule logic. 54 | $rule_validation_result = self::validate_rule_logic( $rules_parsed ); 55 | 56 | if ( is_wp_error( $rule_validation_result ) ) { 57 | return $rule_validation_result; 58 | } 59 | 60 | return $rules_parsed['rules']; 61 | } 62 | 63 | /** 64 | * Given a JSON string, return an array of structured rules, or a WP_Error if parsing fails. 65 | * 66 | * @param string $rules_content Contents of rules file. 67 | * 68 | * @return array|WP_Error 69 | */ 70 | private static function parse_rules_from_json( $rules_content ) { 71 | $rules_parsed = json_decode( $rules_content, true ); 72 | 73 | if ( null === $rules_parsed && JSON_ERROR_NONE !== json_last_error() ) { 74 | // PHP's JSON parsing failed. Use JsonParser to get a more detailed error. 75 | $parser = new JsonParser(); 76 | $result = $parser->lint( $rules_content, JsonParser::DETECT_KEY_CONFLICTS | JsonParser::PARSE_TO_ASSOC ); 77 | 78 | if ( $result instanceof ParsingException ) { 79 | /* translators: %s: Technical data - JSON parsing error */ 80 | $error_message = sprintf( __( 'There was an error parsing JSON: %s', 'vip-governance' ), $result->getMessage() ); 81 | return new WP_Error( 'parsing-error-from-json', $error_message, $result->getDetails() ); 82 | } else { 83 | // If the parser failed to return an error, return default PHP error message. 84 | 85 | /* translators: %s: Technical data - JSON parsing error */ 86 | $error_message = sprintf( __( 'There was an error decoding JSON: %s', 'vip-governance' ), json_last_error_msg() ); 87 | return new WP_Error( 'parsing-error-generic', $error_message ); 88 | } 89 | } 90 | 91 | if ( empty( $rules_parsed ) ) { 92 | // If parsed rules contain an empty object, treat this as a valid form of no rules. 93 | return []; 94 | } 95 | 96 | return $rules_parsed; 97 | } 98 | 99 | 100 | /** 101 | * Evaluate parsed rules for logic errors, like multiple default rules or missing required keys. 102 | * Returns true if validation succeeds, or a WP_Error indicating a logic error. 103 | * 104 | * @param array $rules_parsed Parsed contents of a governance rules file. 105 | * 106 | * @return true|WP_Error 107 | */ 108 | private static function validate_rule_logic( $rules_parsed ) { 109 | if ( ! isset( $rules_parsed['version'] ) || WPCOMVIP__GOVERNANCE__RULES_SCHEMA_VERSION !== $rules_parsed['version'] ) { 110 | /* translators: %s: Latest schema version, e.g. 0.2.0 */ 111 | $error_message = sprintf( __( 'Governance JSON should have a root-level "version" key set to "%s".', 'vip-governance' ), WPCOMVIP__GOVERNANCE__RULES_SCHEMA_VERSION ); 112 | return new WP_Error( 'logic-missing-version', $error_message ); 113 | } elseif ( ! isset( $rules_parsed['rules'] ) ) { 114 | // If parsed rules contain values but no 'rules' key, return an error. 115 | return new WP_Error( 'logic-missing-rules', __( 'Governance JSON should have a root-level "rules" key.', 'vip-governance' ) ); 116 | } elseif ( ! is_array( $rules_parsed['rules'] ) ) { 117 | return new WP_Error( 'logic-non-array-rules', __( 'Governance JSON "rules" key should be an array.', 'vip-governance' ) ); 118 | } 119 | 120 | $rules = $rules_parsed['rules']; 121 | $default_rule_index = null; 122 | 123 | foreach ( $rules as $rule_index => $rule ) { 124 | $rule_type = $rule['type'] ?? null; 125 | $rule_ordinal = self::format_number_with_ordinal( $rule_index + 1 ); 126 | 127 | if ( null === $rule_type || ! in_array( $rule_type, self::RULE_TYPES ) ) { 128 | $rule_types = self::format_array_to_keys( self::RULE_TYPES ); 129 | /* translators: 1: Ordinal number of rule, e.g. 1st 2: Comma-separated list of rule types */ 130 | $error_message = sprintf( __( '%1$s rule should have a "type" key set to one of these values: %2$s.', 'vip-governance' ), $rule_ordinal, $rule_types ); 131 | return new WP_Error( 'logic-incorrect-rule-type', $error_message ); 132 | } 133 | 134 | if ( 'default' === $rule_type ) { 135 | if ( null === $default_rule_index ) { 136 | $verify_rule_result = self::verify_default_rule( $rule ); 137 | $default_rule_index = $rule_index; 138 | } else { 139 | // There's already a default rule defined, bubble an error. 140 | 141 | /* translators: 1: Ordinal number of rule, e.g. 1st */ 142 | $error_message = sprintf( __( 'Only one default rule is allowed, but the %s rule already contains a default rule.', 'vip-governance' ), self::format_number_with_ordinal( $default_rule_index + 1 ) ); 143 | $verify_rule_result = new WP_Error( 'logic-rule-default-multiple', $error_message ); 144 | } 145 | } else { 146 | $verify_rule_result = self::verify_type_rule( $rule ); 147 | } 148 | 149 | if ( is_wp_error( $verify_rule_result ) ) { 150 | // Add rule index to error message. 151 | /* translators: 1: Ordinal number of rule, e.g. 1st 2: Error message for failed rule */ 152 | $error_message = sprintf( __( 'Error parsing %1$s rule: %2$s', 'vip-governance' ), $rule_ordinal, $verify_rule_result->get_error_message() ); 153 | return new WP_Error( $verify_rule_result->get_error_code(), $error_message ); 154 | } 155 | } 156 | 157 | return true; 158 | } 159 | 160 | /** 161 | * Format the number with ordinal suffix, without the PHP number formatter. That doesn't work on all systems. 162 | * 163 | * Taken from https://stackoverflow.com/a/3110033. 164 | * 165 | * @param int $number Number to format. 166 | * @return string Formatted number with ordinal suffix. 167 | */ 168 | private static function format_number_with_ordinal( $number ) { 169 | $ends = array( 'th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th' ); 170 | if ( ( $number % 100 ) >= 11 && ( $number % 100 ) <= 13 ) { 171 | return $number . 'th'; 172 | } else { 173 | return $number . $ends[ $number % 10 ]; 174 | } 175 | } 176 | 177 | /** 178 | * Returns true if the given 'default'-type rule is valid, or a WP_Error otherwise. 179 | * 180 | * @param array $rule Parsed rule. 181 | * 182 | * @return true|WP_Error 183 | */ 184 | private static function verify_default_rule( $rule ) { 185 | if ( count( $rule ) === 1 ) { 186 | $rule_keys = self::format_array_to_keys( self::RULE_KEYS_GENERAL ); 187 | 188 | /* translators: %s: Comma-separate list of valid rule keys */ 189 | $error_message = sprintf( __( 'This default rule is empty. Add additional keys (%s) to make it functional.', 'vip-governance' ), $rule_keys ); 190 | return new WP_Error( 'logic-rule-empty', $error_message ); 191 | } 192 | 193 | foreach ( self::TYPE_TO_RULES_MAP as $type => $types ) { 194 | if ( isset( $rule[ $types ] ) ) { 195 | /* translators: %s: Comma-separate list of valid rule keys */ 196 | $error_message = sprintf( __( '"default"-type rule should not contain "%1$s" key. Default rules apply to all %2$s.', 'vip-governance' ), $types, $type ); 197 | return new WP_Error( 'logic-rule-default-type', $error_message ); 198 | } 199 | } 200 | 201 | return true; 202 | } 203 | 204 | /** 205 | * Returns true if the given type rule is valid, or a WP_Error otherwise. 206 | * 207 | * @param array $rule Parsed rule. 208 | * 209 | * @return true|WP_Error 210 | */ 211 | private static function verify_type_rule( $rule ) { 212 | $type_to_be_checked = self::TYPE_TO_RULES_MAP[ $rule['type'] ]; 213 | 214 | if ( ! isset( $rule[ $type_to_be_checked ] ) || ! is_array( $rule[ $type_to_be_checked ] ) || empty( $rule[ $type_to_be_checked ] ) ) { 215 | $rule_keys = self::format_array_to_keys( self::RULE_KEYS_GENERAL ); 216 | 217 | /* translators: %s: Comma-separate list of valid rule keys */ 218 | $error_message = sprintf( __( '"%1$s"-type rules require a "%2$s" key containing an array of applicable "%3$s".', 'vip-governance' ), $rule['type'], $type_to_be_checked, $type_to_be_checked ); 219 | return new WP_Error( 'logic-rule-type-missing-valid-types', $error_message ); 220 | } 221 | 222 | if ( count( $rule ) === 2 ) { 223 | $rule_keys = self::format_array_to_keys( self::RULE_KEYS_GENERAL ); 224 | 225 | /* translators: %s: Comma-separate list of valid rule keys */ 226 | $error_message = sprintf( __( 'This rule doesn\'t apply any settings to the given type. Add additional keys (%s) to make it functional.', 'vip-governance' ), $rule_keys ); 227 | return new WP_Error( 'logic-rule-empty', $error_message ); 228 | } 229 | 230 | return true; 231 | } 232 | 233 | /** 234 | * Format an array into a quoted, comma-separated list of keys for display. 235 | * e.g. [ 'default', 'role' ] => '"default", "role"'. 236 | * 237 | * @param array $input_array Parsed rule. 238 | * 239 | * @return string Comma-separated list of quoted keys. 240 | */ 241 | private static function format_array_to_keys( $input_array ) { 242 | return implode( ', ', array_map( function ( $item ) { 243 | return sprintf( '"%s"', $item ); 244 | }, $input_array ) ); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /governance/settings/settings-view.php: -------------------------------------------------------------------------------- 1 | %s', esc_html( $line ) ); 15 | }, explode( "\n", trim( $governance_rules_json ) ))) : false; 16 | 17 | ?> 18 | 19 |
20 |

21 | 22 |
23 | 28 |
29 | 30 |
31 | 32 | 33 |

34 | 35 |
36 |
37 | 38 |

39 |
40 | 41 |

42 | 43 |
44 | 45 | 46 |
47 | 48 |

49 | 50 |
51 | 52 |
53 | 54 | 55 | 56 |
57 |
58 | 59 |
60 | 61 |
62 | 63 | 64 |
65 |
66 |

67 |

Rules for a type like roles work by combining the type's governance rules with default rules. Use this tool to view rules using the role/post type and debug permissions issues.

68 | 74 | 75 | 81 | 82 | 83 | 84 | 85 | 86 |
87 | 88 | 89 |
90 | 91 |

92 |

93 | 97 |

98 |
99 | -------------------------------------------------------------------------------- /governance/settings/settings.css: -------------------------------------------------------------------------------- 1 | .governance-rules pre, .combined-governance-rules pre { 2 | background: rgba(0, 0, 0, .07); 3 | padding: 1rem 2rem 1rem 1rem; 4 | margin: 0; 5 | overflow-x: auto; 6 | } 7 | 8 | .governance-rules pre code { 9 | background: none; 10 | } 11 | 12 | .governance-rules-validation { 13 | margin-bottom: 1rem; 14 | } 15 | 16 | .governance-rules.with-errors .governance-rules-validation { 17 | border-left: 2px solid #d63638; 18 | padding-left: 1.25rem; 19 | } 20 | 21 | .governance-rules-validation pre { 22 | white-space: pre-wrap; 23 | display: inline-block; 24 | } 25 | 26 | .governance-rules-json summary, .combined-governance-rules summary { 27 | cursor: pointer; 28 | padding: 0.5rem 0 1rem 0; 29 | } 30 | 31 | .validation-errors { 32 | font-size: 1rem; 33 | font-weight: 700; 34 | } 35 | 36 | .vip-governance-query-spinner span { 37 | float: none; 38 | margin-top: 0; 39 | } 40 | 41 | pre { 42 | counter-reset: line; 43 | tab-size: 4; 44 | } 45 | 46 | code { 47 | counter-increment: line; 48 | } 49 | 50 | code:before { 51 | content: counter(line); 52 | width: 2rem; 53 | display: inline-block; 54 | color: #777; 55 | } 56 | -------------------------------------------------------------------------------- /governance/settings/settings.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // eslint-disable-next-line no-unused-vars 3 | ( () => { 4 | function showRulesForRuleType() { 5 | const roleSelector = document.getElementById( 'user-role-selector' ); 6 | const rolePicked = roleSelector?.value ?? null; 7 | 8 | const postTypeSelector = document.getElementById( 'post-type-selector' ); 9 | const postTypePicked = postTypeSelector?.value ?? null; 10 | 11 | if ( ( rolePicked || postTypePicked ) && window.wp && window.wp.apiRequest ) { 12 | dataToBeSent = {}; 13 | if ( rolePicked ) { 14 | dataToBeSent.role = rolePicked; 15 | } 16 | 17 | if ( postTypePicked ) { 18 | dataToBeSent.postType = postTypePicked; 19 | } 20 | 21 | document.querySelector( '.vip-governance-query-spinner' ).classList.add( 'is-active' ); 22 | window.wp 23 | .apiRequest( { 24 | path: `/vip-governance/v1/rules`, 25 | data: dataToBeSent, 26 | } ) 27 | .done( rules => { 28 | document.getElementById( 'json' ).textContent = JSON.stringify( rules, undefined, 4 ); 29 | document.getElementById( 'json' ).hidden = false; 30 | } ) 31 | .fail( error => { 32 | document.getElementById( 'json' ).textContent = error.responseJSON.message; 33 | document.getElementById( 'json' ).hidden = false; 34 | } ) 35 | .complete( () => { 36 | document.querySelector( '.vip-governance-query-spinner' ).classList.remove( 'is-active' ); 37 | } ); 38 | } 39 | } 40 | 41 | const roleSelector = document.getElementById( 'user-role-selector' ); 42 | 43 | if ( roleSelector ) { 44 | // Reset to the default value on refresh 45 | roleSelector.value = ''; 46 | 47 | roleSelector.addEventListener( 'change', () => { 48 | const postTypePicked = document.getElementById( 'post-type-selector' )?.value ?? null; 49 | if ( roleSelector.value ) { 50 | document.getElementById( 'view-rules-button' ).style.display = 'inline'; 51 | } else if ( ! postTypePicked ) { 52 | document.getElementById( 'view-rules-button' ).style.display = 'none'; 53 | document.getElementById( 'json' ).hidden = true; 54 | } 55 | } ); 56 | } 57 | 58 | const postTypeSelector = document.getElementById( 'post-type-selector' ); 59 | 60 | if ( postTypeSelector ) { 61 | // Reset to the default value on refresh 62 | postTypeSelector.value = ''; 63 | 64 | postTypeSelector.addEventListener( 'change', () => { 65 | const rolePicked = document.getElementById( 'user-role-selector' )?.value ?? null; 66 | if ( postTypeSelector.value ) { 67 | document.getElementById( 'view-rules-button' ).style.display = 'inline'; 68 | } else if ( ! rolePicked ) { 69 | document.getElementById( 'view-rules-button' ).style.display = 'none'; 70 | document.getElementById( 'json' ).hidden = true; 71 | } 72 | } ); 73 | } 74 | 75 | const viewButton = document.getElementById( 'view-rules-button' ); 76 | 77 | if ( viewButton ) { 78 | viewButton.style.display = 'none'; 79 | 80 | viewButton.addEventListener( 'click', showRulesForRuleType ); 81 | } 82 | } )(); 83 | -------------------------------------------------------------------------------- /governance/settings/settings.php: -------------------------------------------------------------------------------- 1 | self::OPTIONS_KEY_IS_ENABLED, 48 | ] ); 49 | } 50 | 51 | /** 52 | * Register the menu in wp-admin 53 | * 54 | * @return void 55 | * 56 | * @access private 57 | */ 58 | public static function register_menu() { 59 | $hook = add_menu_page( 'VIP Block Governance', 'VIP Block Governance', 'manage_options', self::MENU_SLUG, [ __CLASS__, 'render' ], 'dashicons-groups' ); 60 | add_action( 'load-' . $hook, [ __CLASS__, 'enqueue_scripts' ] ); 61 | add_action( 'load-' . $hook, [ __CLASS__, 'enqueue_resources' ] ); 62 | } 63 | 64 | /** 65 | * Load the CSS for the settings page. 66 | * 67 | * @return void 68 | * 69 | * @access private 70 | */ 71 | public static function enqueue_resources() { 72 | wp_enqueue_style( 73 | 'wpcomvip-governance-settings', 74 | plugins_url( '/governance/settings/settings.css', WPCOMVIP_GOVERNANCE_ROOT_PLUGIN_FILE ), 75 | /* dependencies */ [], 76 | WPCOMVIP__GOVERNANCE__PLUGIN_VERSION 77 | ); 78 | } 79 | 80 | /** 81 | * Load the JS for the settings page. 82 | * 83 | * @return void 84 | * 85 | * @access private 86 | */ 87 | public static function enqueue_scripts() { 88 | wp_enqueue_script( 89 | 'wpcomvip-governance-settings', 90 | plugins_url( '/governance/settings/settings.js', WPCOMVIP_GOVERNANCE_ROOT_PLUGIN_FILE ), 91 | /* dependencies */ [ 'wp-api' ], 92 | WPCOMVIP__GOVERNANCE__PLUGIN_VERSION, 93 | /* in footer */ true 94 | ); 95 | } 96 | 97 | /** 98 | * Render the view for the setting page, which includes form to enable/disable the plugin 99 | * and to show the rules along with its errors. 100 | * 101 | * @return void 102 | * 103 | * @access private 104 | */ 105 | public static function render() { 106 | // Remove the default rule type from the list of rule types allowed. 107 | $rule_types_available = RulesParser::RULE_TYPES; 108 | $post_types_available = get_post_types(); 109 | $user_roles_available = array_keys( wp_roles()->roles ); 110 | $governance_rules_json = GovernanceUtilities::get_governance_rules_json(); 111 | $governance_error = false; 112 | if ( is_wp_error( $governance_rules_json ) ) { 113 | $governance_error = $governance_rules_json->get_error_message(); 114 | $governance_rules_json = false; 115 | } else { 116 | $governance_rules = GovernanceUtilities::get_parsed_governance_rules(); 117 | 118 | if ( is_wp_error( $governance_rules ) ) { 119 | $governance_error = $governance_rules->get_error_message(); 120 | $governance_rules = []; 121 | } 122 | } 123 | 124 | include __DIR__ . '/settings-view.php'; 125 | } 126 | 127 | /** 128 | * Setting used to enable/disable the plugin. 129 | * 130 | * @return void 131 | * 132 | * @access private 133 | */ 134 | public static function render_is_enabled() { 135 | $options = get_option( self::OPTIONS_KEY ); 136 | $is_enabled = $options[ self::OPTIONS_KEY_IS_ENABLED ] ?? true; 137 | 138 | printf( '', esc_attr( self::OPTIONS_KEY_IS_ENABLED ), esc_attr( self::OPTIONS_KEY ), checked( $is_enabled, true, false ) ); 139 | printf( '', esc_attr( self::OPTIONS_KEY_IS_ENABLED ), esc_html__( 'Enable block editor governance rules for all users.' ) ); 140 | } 141 | 142 | /** 143 | * Ensure that the options are valid. 144 | * 145 | * @param array $options Options that have been submitted. 146 | * 147 | * @return boolean True, if its valid or false otherwise. 148 | */ 149 | public static function validate_options( $options ) { 150 | $options[ self::OPTIONS_KEY_IS_ENABLED ] = 'yes' === $options[ self::OPTIONS_KEY_IS_ENABLED ]; 151 | 152 | return $options; 153 | } 154 | 155 | /** 156 | * Check if the plugin is to be enabled or not. 157 | * 158 | * @return boolean True, if it is or false otherwise. 159 | * 160 | * @access private 161 | */ 162 | public static function is_enabled() { 163 | $options = get_option( self::OPTIONS_KEY ); 164 | return $options[ self::OPTIONS_KEY_IS_ENABLED ] ?? true; 165 | } 166 | } 167 | 168 | Settings::init(); 169 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "block-editor-governance", 3 | "version": "1.0.11", 4 | "description": "This is a plugin adding additional governance capabilities to the block editor.", 5 | "author": "VIP Bistro", 6 | "main": "build/index.js", 7 | "devDependencies": { 8 | "@automattic/eslint-plugin-wpvip": "^0.13.0", 9 | "@playwright/test": "^1.44.1", 10 | "@wordpress/block-editor": "^14.0.0", 11 | "@wordpress/components": "^28.0.0", 12 | "@wordpress/compose": "^7.2.0", 13 | "@wordpress/data": "^10.2.0", 14 | "@wordpress/e2e-test-utils-playwright": "^1.7.0", 15 | "@wordpress/hooks": "^4.7.0", 16 | "@wordpress/i18n": "^5.0.0", 17 | "@wordpress/notices": "^5.5.0", 18 | "@wordpress/scripts": "^27.9.0", 19 | "babel-jest": "^29.7.0", 20 | "eslint": "^8.56.0", 21 | "husky": "^9.0.11", 22 | "lint-staged": "^15.2.5", 23 | "phplint": "^2.0.5", 24 | "prettier": "npm:wp-prettier@2.8.5", 25 | "webpack": "^5.91.0" 26 | }, 27 | "scripts": { 28 | "build": "webpack --mode production", 29 | "dev": "webpack --watch", 30 | "lint:js": "eslint . --ext .js", 31 | "phplint": "npx phplint '**/*.php' '!vendor/**' '!node_modules/**' '!build' > /dev/null", 32 | "phpcs": "vendor/bin/phpcs --cache", 33 | "phpcs:fix": "vendor/bin/phpcbf", 34 | "lint": "npm run phplint && npm run phpcs && npm run lint:js", 35 | "prepare": "husky install", 36 | "test": "npm run test:unit", 37 | "test:unit": "npm run test:php && npm run test:js", 38 | "test:e2e": "npx playwright test", 39 | "test:js": "wp-scripts test-unit-js" 40 | }, 41 | "jest": { 42 | "testMatch": [ 43 | "/src/**/*.test.js" 44 | ], 45 | "preset": "@wordpress/jest-preset-default", 46 | "testPathIgnorePatterns": [ 47 | "/.git/", 48 | "/node_modules/", 49 | "/governance/", 50 | "/tests/" 51 | ], 52 | "transform": { 53 | "^.+\\.[jt]sx?$": "@wordpress/scripts/config/babel-transform.js" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /playwright.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { defineConfig, devices } from '@playwright/test'; 5 | import path from 'path'; 6 | 7 | const STORAGE_STATE_PATH = 8 | process.env.STORAGE_STATE_PATH || 9 | path.join( process.cwd(), 'artifacts/storage-states/admin.json' ); 10 | 11 | const config = defineConfig( { 12 | forbidOnly: Boolean( process.env.CI ), 13 | workers: 1, 14 | retries: process.env.CI ? 2 : 0, 15 | timeout: parseInt( process.env.TIMEOUT || '', 10 ) || 100_000, // Defaults to 100 seconds. 16 | // Don't report slow test "files", as we will be running our tests in serial. 17 | reportSlowTests: null, 18 | globalSetup: require.resolve( './tests/e2e/globalSetup.js' ), 19 | testDir: 'tests/e2e', 20 | outputDir: path.join( process.cwd(), 'artifacts/test-results' ), 21 | snapshotPathTemplate: '{testDir}/{testFileDir}/__snapshots__/{arg}-{projectName}{ext}', 22 | use: { 23 | baseURL: process.env.WP_BASE_URL || 'http://localhost:8889', 24 | headless: true, 25 | viewport: { 26 | width: 960, 27 | height: 700, 28 | }, 29 | ignoreHTTPSErrors: true, 30 | locale: 'en-US', 31 | contextOptions: { 32 | reducedMotion: 'reduce', 33 | strictSelectors: true, 34 | }, 35 | storageState: STORAGE_STATE_PATH, 36 | actionTimeout: 10_000, // 10 seconds. 37 | trace: 'retain-on-failure', 38 | screenshot: 'only-on-failure', 39 | video: 'on-first-retry', 40 | }, 41 | projects: [ 42 | { 43 | name: 'chromium', 44 | use: { ...devices[ 'Desktop Chrome' ] }, 45 | grepInvert: /-chromium/, 46 | }, 47 | ], 48 | } ); 49 | 50 | export default config; 51 | -------------------------------------------------------------------------------- /src/block-locking.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { store as blockEditorStore } from '@wordpress/block-editor'; 5 | import { Disabled } from '@wordpress/components'; 6 | import { createHigherOrderComponent } from '@wordpress/compose'; 7 | import { select } from '@wordpress/data'; 8 | import { addFilter, applyFilters } from '@wordpress/hooks'; 9 | 10 | /** 11 | * Internal dependencies 12 | */ 13 | import { isBlockAllowedInHierarchy } from './block-utils'; 14 | 15 | export function setupBlockLocking( governanceRules ) { 16 | const withDisabledBlocks = createHigherOrderComponent( BlockEdit => { 17 | return props => { 18 | const { name: blockName, clientId } = props; 19 | 20 | const { getBlockParents, getBlockName } = select( blockEditorStore ); 21 | const parentClientIds = getBlockParents(clientId, true); 22 | 23 | const isParentLocked = parentClientIds.some( parentClientId => isBlockLocked(parentClientId) ); 24 | 25 | if ( isParentLocked ) { 26 | // To avoid layout issues, only disable the outermost locked block 27 | return ; 28 | } 29 | 30 | const parentBlockNames = parentClientIds.map( parentClientId => 31 | getBlockName( parentClientId ) 32 | ); 33 | 34 | let isAllowed = isBlockAllowedInHierarchy( blockName, parentBlockNames, governanceRules ); 35 | 36 | /** 37 | * Change what blocks are allowed to be edited in the block editor. 38 | * 39 | * @param {bool} isAllowed Whether or not the block will be allowed. 40 | * @param {string} blockName The name of the block to be edited. 41 | * @param {string[]} parentBlockNames An array of zero or more parent block names, 42 | * starting with the most recent parent ancestor. 43 | * @param {Object} governanceRules An object containing the full set of governance 44 | * rules for the current user. 45 | */ 46 | isAllowed = applyFilters( 47 | 'vip_governance__is_block_allowed_for_editing', 48 | isAllowed, 49 | blockName, 50 | parentBlockNames, 51 | governanceRules 52 | ); 53 | 54 | if ( isAllowed ) { 55 | return ; 56 | } else { 57 | 58 | // Only available on WP 6.4 and above, so this guards against that. 59 | if ( wp?.blockEditor?.useBlockEditingMode ) { 60 | const { useBlockEditingMode } = wp.blockEditor; 61 | useBlockEditingMode( 'disabled' ); 62 | } 63 | 64 | // Mark block as locked so that children can detect they're within an existing locked block 65 | setBlockLocked( clientId ); 66 | 67 | return <> 68 | 69 |
70 | 71 |
72 |
73 | ; 74 | } 75 | }; 76 | }, 'withDisabledBlocks' ); 77 | 78 | addFilter( 'editor.BlockEdit', 'wpcomvip-governance/with-disabled-blocks', withDisabledBlocks ); 79 | } 80 | 81 | /** 82 | * In-memory map of block clientIds that have been marked as locked. 83 | * 84 | * This replaces using props.setAttributes() to set lock status, as this caused an 85 | * "unsaved changes" warning to appear in the editor when block locking was in use. 86 | */ 87 | const lockedBlockMap = {}; 88 | 89 | /** 90 | * Marks a block as locked via the block's clientId. 91 | * 92 | * @param {string} clientId Block clientId in editor 93 | * @returns {void} 94 | */ 95 | function setBlockLocked( clientId ) { 96 | lockedBlockMap[clientId] = true; 97 | } 98 | 99 | /** 100 | * Returns true if a block has previously been marked as locked, false otherwise. 101 | * 102 | * @param {string} clientId Block clientId in editor 103 | * @returns {boolean} 104 | */ 105 | function isBlockLocked( clientId ) { 106 | return clientId in lockedBlockMap; 107 | } 108 | -------------------------------------------------------------------------------- /src/block-utils.js: -------------------------------------------------------------------------------- 1 | import { applyFilters } from '@wordpress/hooks'; 2 | 3 | import { getNestedSetting } from './nested-governance-loader'; 4 | 5 | // The list of default core blocks that should be allowed to be inserted, in order to make life easier. 6 | const DEFAULT_CORE_BLOCK_LIST = { 7 | 'core/list': [ 'core/list-item' ], 8 | 'core/columns': [ 'core/column' ], 9 | 'core/page-list': [ 'core/page-list-item' ], 10 | 'core/navigation': [ 'core/navigation-link', 'core/navigation-submenu' ], 11 | 'core/navigation-link': [ 'core/navigation-link', 'core/navigation-submenu', 'core/page-list' ], 12 | 'core/quote': [ 'core/paragraph' ], 13 | 'core/media-text': [ 'core/paragraph' ], 14 | 'core/social-links': [ 'core/social-link' ], 15 | 'core/comments-pagination': [ 16 | 'core/comments-pagination-previous', 17 | 'core/comments-pagination-numbers', 18 | 'core/comments-pagination-next', 19 | ], 20 | }; 21 | 22 | /** 23 | * Given a block name, a parent list and a set of governance rules, determine if 24 | * the block can be inserted. 25 | * 26 | * By default, will return if the block is allowed to be inserted at the root level 27 | * per the user's rules. If a parent block contains a rule for allowedBlocks, 28 | * the function will return if the block is allowed as a child of that parent. 29 | * 30 | * Rules declared in allowedBlocks will override root level rules when the block 31 | * is currently a child of the parent with allowedBlocks. 32 | * 33 | * @param {string} blockName The current block's name. 34 | * @param {string[]} parentBlockNames A list of zero or more parent block names, 35 | * starting with the most recent parent ancestor. 36 | * @param {Object} governanceRules An object containing the full set of governance 37 | * rules for the current user. 38 | * @returns True if the block is allowed in set of parent blocks, or false otherwise. 39 | */ 40 | export function isBlockAllowedInHierarchy( blockName, parentBlockNames, governanceRules ) { 41 | // Filter to decide if the mode should be cascading or restrictive, where true is cascading and false is restrictive. 42 | const isInCascadingMode = applyFilters( 43 | 'vip_governance__is_block_allowed_in_hierarchy', 44 | true, 45 | blockName, 46 | parentBlockNames, 47 | governanceRules 48 | ); 49 | 50 | // Build the blocks that are allowed using the root level blocks for cascading mode or if no parent has been past, or empty otherwise. 51 | const blocksAllowedToBeInserted = 52 | isInCascadingMode || parentBlockNames.length === 0 ? [ ...governanceRules.allowedBlocks ] : []; 53 | 54 | // Only execute this if we are determining the block under a parent. 55 | if ( parentBlockNames.length > 0 ) { 56 | // Shortcircuit the parent-child hierarchy for some core blocks 57 | if ( 58 | DEFAULT_CORE_BLOCK_LIST[ parentBlockNames[ 0 ] ] && 59 | DEFAULT_CORE_BLOCK_LIST[ parentBlockNames[ 0 ] ].includes( blockName ) 60 | ) { 61 | return true; 62 | } 63 | 64 | // Only do a search if there are block settings to search through. 65 | if ( governanceRules.blockSettings ) { 66 | // Get the child block's parent block settings at whatever depth its located at. 67 | const nestedSetting = getNestedSetting( 68 | parentBlockNames.reverse(), 69 | 'allowedBlocks', 70 | governanceRules.blockSettings 71 | ); 72 | 73 | // If we found the allowedBlocks for the parent block, add that to the array of blocks that can be inserted. 74 | if ( nestedSetting && nestedSetting.value ) { 75 | blocksAllowedToBeInserted.push( ...nestedSetting.value ); 76 | } 77 | } 78 | } 79 | 80 | // Check if the block is allowed using the array of blocks that can be inserted. 81 | return isBlockAllowedByBlockWildcards( blockName, blocksAllowedToBeInserted ); 82 | } 83 | 84 | /** 85 | * Matches a block name to a list of block wildcard rules. 86 | * For wildcard rules, see doesBlockNameMatchBlockWildcard(). 87 | * 88 | * @param {string} blockName 89 | * @param {string[]} rules 90 | * @returns True if the block name matches any of the rules, false otherwise. 91 | */ 92 | export function isBlockAllowedByBlockWildcards( blockName, rules ) { 93 | return rules.some( rule => doesBlockNameMatchBlockWildcard( blockName, rule ) ); 94 | } 95 | 96 | /** 97 | * Matches a rule to a block name, with the following cases being possible: 98 | * 99 | * 1. ['*'] - matches all blocks 100 | * 2. '*' can be located somewhere else alongside a string, e.g. 'core/*' - matches all core blocks 101 | * 3. ['core/paragraph'] - matches only the core/paragraph block 102 | * 103 | * @param {string} blockName 104 | * @param {string} rule 105 | * @returns True if the block name matches the rule, or false otherwise 106 | */ 107 | export function doesBlockNameMatchBlockWildcard( blockName, rule ) { 108 | if ( rule.includes( '*' ) ) { 109 | // eslint-disable-next-line security/detect-non-literal-regexp 110 | return blockName.match( new RegExp( rule.replace( '*', '.*' ) ) ); 111 | } 112 | 113 | return rule === blockName; 114 | } 115 | -------------------------------------------------------------------------------- /src/block-utils.test.js: -------------------------------------------------------------------------------- 1 | import { applyFilters } from '@wordpress/hooks'; 2 | 3 | import { 4 | doesBlockNameMatchBlockWildcard, 5 | isBlockAllowedByBlockWildcards, 6 | isBlockAllowedInHierarchy, 7 | } from './block-utils'; 8 | 9 | jest.mock( '@wordpress/hooks', () => ( { 10 | applyFilters: jest.fn(), 11 | } ) ); 12 | 13 | describe( 'blockUtils', () => { 14 | describe( 'isBlockAllowedInHierarchy', () => { 15 | describe( 'cascading mode', () => { 16 | beforeEach( () => { 17 | applyFilters.mockImplementation( () => true ); 18 | } ); 19 | 20 | it( 'should return true if the child block is a special core block', () => { 21 | const blockName = 'core/list-item'; 22 | const parentBlockNames = [ 'core/list', 'core/media-text' ]; 23 | const governanceRules = { 24 | allowedBlocks: [ 'core/group', 'core/paragraph' ], 25 | blockSettings: { 26 | 'core/media-text': { 27 | allowedBlocks: [ 'core/heading' ], 28 | }, 29 | }, 30 | }; 31 | 32 | const result = isBlockAllowedInHierarchy( blockName, parentBlockNames, governanceRules ); 33 | 34 | expect( result ).toBe( true ); 35 | } ); 36 | 37 | it( 'should return true if the child block is allowed in the hierarchy', () => { 38 | const blockName = 'core/heading'; 39 | const parentBlockNames = [ 'core/media-text' ]; 40 | const governanceRules = { 41 | allowedBlocks: [ 'core/group', 'core/paragraph' ], 42 | blockSettings: { 43 | 'core/media-text': { 44 | allowedBlocks: [ 'core/heading' ], 45 | }, 46 | }, 47 | }; 48 | 49 | const result = isBlockAllowedInHierarchy( blockName, parentBlockNames, governanceRules ); 50 | 51 | expect( result ).toBe( true ); 52 | } ); 53 | 54 | it( 'should return false if the child block is not allowed in the hierarchy', () => { 55 | const blockName = 'core/heading'; 56 | const parentBlockNames = [ 'core/media-text' ]; 57 | const governanceRules = { 58 | allowedBlocks: [ 'core/group', 'core/paragraph' ], 59 | blockSettings: { 60 | 'core/media-text': { 61 | allowedBlocks: [ 'core/image' ], 62 | }, 63 | }, 64 | }; 65 | 66 | const result = isBlockAllowedInHierarchy( blockName, parentBlockNames, governanceRules ); 67 | 68 | expect( result ).toBe( false ); 69 | } ); 70 | 71 | it( 'should return false if the child block is not allowed in the hierarchy per root rules', () => { 72 | const blockName = 'core/heading'; 73 | const parentBlockNames = [ 'core/media-text' ]; 74 | const governanceRules = { 75 | allowedBlocks: [ 'core/group', 'core/paragraph' ], 76 | blockSettings: { 77 | 'core/media-text': { 78 | color: { 79 | text: true, 80 | }, 81 | }, 82 | }, 83 | }; 84 | 85 | const result = isBlockAllowedInHierarchy( blockName, parentBlockNames, governanceRules ); 86 | 87 | expect( result ).toBe( false ); 88 | } ); 89 | 90 | it( 'should return true if the child block is allowed in the hierarchy with no blockSettings', () => { 91 | const blockName = 'core/heading'; 92 | const parentBlockNames = [ 'core/media-text' ]; 93 | const governanceRules = { 94 | allowedBlocks: [ 'core/heading', 'core/paragraph' ], 95 | }; 96 | 97 | const result = isBlockAllowedInHierarchy( blockName, parentBlockNames, governanceRules ); 98 | 99 | expect( result ).toBe( true ); 100 | } ); 101 | 102 | it( 'should return false if the child block is not allowed in the hierarchy with no blockSettings', () => { 103 | const blockName = 'core/heading'; 104 | const parentBlockNames = [ 'core/media-text' ]; 105 | const governanceRules = { 106 | allowedBlocks: [ 'core/paragraph' ], 107 | }; 108 | 109 | const result = isBlockAllowedInHierarchy( blockName, parentBlockNames, governanceRules ); 110 | 111 | expect( result ).toBe( false ); 112 | } ); 113 | 114 | it( 'should return true if the root block is allowed in the hierarchy', () => { 115 | const blockName = 'core/heading'; 116 | const parentBlockNames = []; 117 | const governanceRules = { 118 | allowedBlocks: [ 'core/heading', 'core/paragraph' ], 119 | }; 120 | 121 | const result = isBlockAllowedInHierarchy( blockName, parentBlockNames, governanceRules ); 122 | 123 | expect( result ).toBe( true ); 124 | } ); 125 | 126 | it( 'should return false if the root block is not allowed in the hierarchy', () => { 127 | const blockName = 'core/heading'; 128 | const parentBlockNames = []; 129 | const governanceRules = { 130 | allowedBlocks: [ 'core/paragraph' ], 131 | }; 132 | 133 | const result = isBlockAllowedInHierarchy( blockName, parentBlockNames, governanceRules ); 134 | 135 | expect( result ).toBe( false ); 136 | } ); 137 | } ); 138 | 139 | describe( 'restrictive mode', () => { 140 | beforeEach( () => { 141 | applyFilters.mockImplementation( () => false ); 142 | } ); 143 | 144 | it( 'should return true if the child block is a special core block', () => { 145 | const blockName = 'core/list-item'; 146 | const parentBlockNames = [ 'core/list', 'core/media-text' ]; 147 | const governanceRules = { 148 | allowedBlocks: [ 'core/group', 'core/paragraph' ], 149 | blockSettings: { 150 | 'core/media-text': { 151 | allowedBlocks: [ 'core/heading' ], 152 | }, 153 | }, 154 | }; 155 | 156 | const result = isBlockAllowedInHierarchy( blockName, parentBlockNames, governanceRules ); 157 | 158 | expect( result ).toBe( true ); 159 | } ); 160 | 161 | it( 'should return true if the child block is allowed in the hierarchy', () => { 162 | const blockName = 'core/heading'; 163 | const parentBlockNames = [ 'core/media-text' ]; 164 | const governanceRules = { 165 | allowedBlocks: [ 'core/group', 'core/paragraph' ], 166 | blockSettings: { 167 | 'core/media-text': { 168 | allowedBlocks: [ 'core/heading' ], 169 | }, 170 | }, 171 | }; 172 | 173 | const result = isBlockAllowedInHierarchy( blockName, parentBlockNames, governanceRules ); 174 | 175 | expect( result ).toBe( true ); 176 | } ); 177 | 178 | it( 'should return false if the child block is not allowed in the hierarchy', () => { 179 | const blockName = 'core/heading'; 180 | const parentBlockNames = [ 'core/media-text' ]; 181 | const governanceRules = { 182 | allowedBlocks: [ 'core/group', 'core/paragraph' ], 183 | blockSettings: { 184 | 'core/media-text': { 185 | allowedBlocks: [ 'core/image' ], 186 | }, 187 | }, 188 | }; 189 | 190 | const result = isBlockAllowedInHierarchy( blockName, parentBlockNames, governanceRules ); 191 | 192 | expect( result ).toBe( false ); 193 | } ); 194 | 195 | it( 'should return false if the child block is allowed in the hierarchy with no blockSettings', () => { 196 | const blockName = 'core/heading'; 197 | const parentBlockNames = [ 'core/media-text' ]; 198 | const governanceRules = { 199 | allowedBlocks: [ 'core/heading', 'core/paragraph' ], 200 | }; 201 | 202 | const result = isBlockAllowedInHierarchy( blockName, parentBlockNames, governanceRules ); 203 | 204 | expect( result ).toBe( false ); 205 | } ); 206 | 207 | it( 'should return false if the child block is not allowed in the hierarchy with no blockSettings', () => { 208 | const blockName = 'core/heading'; 209 | const parentBlockNames = [ 'core/media-text' ]; 210 | const governanceRules = { 211 | allowedBlocks: [ 'core/paragraph' ], 212 | }; 213 | 214 | const result = isBlockAllowedInHierarchy( blockName, parentBlockNames, governanceRules ); 215 | 216 | expect( result ).toBe( false ); 217 | } ); 218 | 219 | it( 'should return true if the root block is allowed in the hierarchy', () => { 220 | const blockName = 'core/heading'; 221 | const parentBlockNames = []; 222 | const governanceRules = { 223 | allowedBlocks: [ 'core/heading', 'core/paragraph' ], 224 | }; 225 | 226 | const result = isBlockAllowedInHierarchy( blockName, parentBlockNames, governanceRules ); 227 | 228 | expect( result ).toBe( true ); 229 | } ); 230 | 231 | it( 'should return false if the root block is not allowed in the hierarchy', () => { 232 | const blockName = 'core/heading'; 233 | const parentBlockNames = []; 234 | const governanceRules = { 235 | allowedBlocks: [ 'core/paragraph' ], 236 | }; 237 | 238 | const result = isBlockAllowedInHierarchy( blockName, parentBlockNames, governanceRules ); 239 | 240 | expect( result ).toBe( false ); 241 | } ); 242 | } ); 243 | } ); 244 | 245 | describe( 'isBlockAllowedByBlockWildcards', () => { 246 | it( 'should return true if the block name matches any of the rules', () => { 247 | const blockName = 'core/heading'; 248 | const rules = [ 'core/heading', 'core/paragraph' ]; 249 | 250 | const result = isBlockAllowedByBlockWildcards( blockName, rules ); 251 | 252 | expect( result ).toBe( true ); 253 | } ); 254 | 255 | it( 'should return false if the block name does not match any rules', () => { 256 | const blockName = 'core/heading'; 257 | const rules = [ 'core/paragraph' ]; 258 | 259 | const result = isBlockAllowedByBlockWildcards( blockName, rules ); 260 | 261 | expect( result ).toBe( false ); 262 | } ); 263 | } ); 264 | 265 | describe( 'doesBlockNameMatchBlockWildcard', () => { 266 | it( 'should not be null if the block name matches any of the wildcard rules', () => { 267 | const blockName = 'core/heading'; 268 | const rules = 'core/*'; 269 | 270 | const result = doesBlockNameMatchBlockWildcard( blockName, rules ); 271 | 272 | expect( result ).toBeTruthy(); 273 | } ); 274 | 275 | it( 'should be null if the block name does not match any of the wildcard rules', () => { 276 | const blockName = 'custom/heading'; 277 | const rules = 'core/*'; 278 | 279 | const result = doesBlockNameMatchBlockWildcard( blockName, rules ); 280 | 281 | expect( result ).toBeFalsy(); 282 | } ); 283 | 284 | it( 'should return true if the block name matches any of the rules', () => { 285 | const blockName = 'core/heading'; 286 | const rules = 'core/heading'; 287 | 288 | const result = doesBlockNameMatchBlockWildcard( blockName, rules ); 289 | 290 | expect( result ).toBeTruthy(); 291 | } ); 292 | 293 | it( 'should return false if the block name does not match any rules', () => { 294 | const blockName = 'core/heading'; 295 | const rules = 'core/paragraph'; 296 | 297 | const result = doesBlockNameMatchBlockWildcard( blockName, rules ); 298 | 299 | expect( result ).toBeFalsy(); 300 | } ); 301 | } ); 302 | } ); 303 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { store as blockEditorStore } from '@wordpress/block-editor'; 2 | import { dispatch, select } from '@wordpress/data'; 3 | import { addFilter, applyFilters } from '@wordpress/hooks'; 4 | import { __ } from '@wordpress/i18n'; 5 | import { store as noticeStore } from '@wordpress/notices'; 6 | 7 | import { setupBlockLocking } from './block-locking'; 8 | import { doesBlockNameMatchBlockWildcard, isBlockAllowedInHierarchy } from './block-utils'; 9 | import { getNestedSetting, getNestedSettingPaths } from './nested-governance-loader'; 10 | 11 | function setup() { 12 | if ( VIP_GOVERNANCE.error ) { 13 | dispatch( noticeStore ).createErrorNotice( VIP_GOVERNANCE.error, { 14 | id: 'wpcomvip-governance-error', 15 | isDismissible: true, 16 | actions: [ 17 | { 18 | label: __( 'Open governance settings' ), 19 | url: VIP_GOVERNANCE.urlSettingsPage, 20 | }, 21 | ], 22 | } ); 23 | 24 | return; 25 | } 26 | 27 | const governanceRules = VIP_GOVERNANCE.governanceRules; 28 | 29 | addFilter( 30 | 'blockEditor.__unstableCanInsertBlockType', 31 | `wpcomvip-governance/block-insertion`, 32 | ( canInsert, blockType, rootClientId, { getBlock } ) => { 33 | if ( canInsert === false ) { 34 | return canInsert; 35 | } 36 | 37 | let parentBlockNames = []; 38 | 39 | if ( rootClientId ) { 40 | // This block has parents. Build a list of parentBlockNames 41 | const { getBlockParents, getBlockName } = select( blockEditorStore ); 42 | const parentBlock = getBlock( rootClientId ); 43 | const ancestorClientIds = getBlockParents( rootClientId, true ); 44 | 45 | parentBlockNames = [ parentBlock.clientId, ...ancestorClientIds ].map( parentClientId => 46 | getBlockName( parentClientId ) 47 | ); 48 | } 49 | 50 | const isAllowed = isBlockAllowedInHierarchy( 51 | blockType.name, 52 | parentBlockNames, 53 | governanceRules 54 | ); 55 | 56 | /** 57 | * Change what blocks are allowed to be inserted in the block editor. 58 | * 59 | * @param {bool} isAllowed Whether or not the block will be allowed. 60 | * @param {string} blockName The name of the block to be inserted. 61 | * @param {string[]} parentBlockNames An array of zero or more parent block names, 62 | * starting with the most recent parent ancestor. 63 | * @param {Object} governanceRules An object containing the full set of governance 64 | * rules for the current user. 65 | */ 66 | return applyFilters( 67 | 'vip_governance__is_block_allowed_for_insertion', 68 | isAllowed, 69 | blockType.name, 70 | parentBlockNames, 71 | governanceRules 72 | ); 73 | } 74 | ); 75 | 76 | const nestedSettings = VIP_GOVERNANCE.nestedSettings; 77 | const nestedSettingPaths = getNestedSettingPaths( nestedSettings ); 78 | 79 | const nestedWildcardPaths = {}; 80 | const nestedNonWildcardPaths = {}; 81 | 82 | for ( const blockName in nestedSettingPaths ) { 83 | if ( blockName.indexOf( '*' ) === -1 ) { 84 | // eslint-disable-next-line security/detect-object-injection 85 | nestedNonWildcardPaths[ blockName ] = nestedSettingPaths[ blockName ]; 86 | } else { 87 | // eslint-disable-next-line security/detect-object-injection 88 | nestedWildcardPaths[ blockName ] = nestedSettingPaths[ blockName ]; 89 | } 90 | } 91 | 92 | addFilter( 93 | 'blockEditor.useSetting.before', 94 | `wpcomvip-governance/nested-block-settings`, 95 | ( result, path, clientId, blockName ) => { 96 | if ( ! blockName ) { 97 | return result; 98 | } 99 | 100 | // Test if the blockName is in the nestedNonWildcardPaths. 101 | if ( 102 | // eslint-disable-next-line security/detect-object-injection 103 | nestedNonWildcardPaths[ blockName ] !== undefined && 104 | // eslint-disable-next-line security/detect-object-injection 105 | nestedNonWildcardPaths[ blockName ][ path ] === true 106 | ) { 107 | const blockNamePath = [ 108 | clientId, 109 | ...select( blockEditorStore ).getBlockParents( clientId, /* ascending */ true ), 110 | ] 111 | .map( candidateId => select( blockEditorStore ).getBlockName( candidateId ) ) 112 | .reverse(); 113 | ( { value: result } = getNestedSetting( blockNamePath, path, nestedSettings ) ); 114 | 115 | // This is necessary because the nestedSettingPaths are flattened, so a child's path could match the parent's path. 116 | return result && result.theme ? result.theme : result; 117 | // Test if the blockName is in the nestedWildcardPaths. 118 | } else if ( nestedWildcardPaths.length !== 0 ) { 119 | for ( const nestedBlockName in nestedWildcardPaths ) { 120 | if ( 121 | doesBlockNameMatchBlockWildcard( blockName, nestedBlockName ) && 122 | // eslint-disable-next-line security/detect-object-injection 123 | nestedWildcardPaths[ nestedBlockName ][ path ] === true 124 | ) { 125 | const blockNamePath = [ 126 | clientId, 127 | ...select( blockEditorStore ).getBlockParents( clientId, /* ascending */ true ), 128 | ] 129 | .map( candidateId => select( blockEditorStore ).getBlockName( candidateId ) ) 130 | .reverse(); 131 | 132 | // Replace the original block name with the matched wildcard block name, for easier lookup. 133 | // This will be at the end of the blockNamePath array. 134 | if ( nestedBlockName.indexOf( '*' ) !== -1 ) { 135 | blockNamePath[ blockNamePath.length - 1 ] = nestedBlockName; 136 | } 137 | 138 | ( { value: result } = getNestedSetting( blockNamePath, path, nestedSettings ) ); 139 | 140 | // This is necessary because the nestedSettingPaths are flattened, so a child's path could match the parent's path. 141 | return result && result.theme ? result.theme : result; 142 | } 143 | } 144 | } 145 | 146 | return result; 147 | } 148 | ); 149 | 150 | // Block locking 151 | if ( governanceRules?.allowedBlocks ) { 152 | setupBlockLocking( governanceRules ); 153 | } 154 | } 155 | 156 | setup(); 157 | -------------------------------------------------------------------------------- /src/nested-governance-loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Find the list of nestedPaths that can be found in the block settings, so that 3 | * it's faster to find out if a deeper nested setting exists or not. 4 | * 5 | * @param {Object} nestedSettings the nestedSettings found from the governance rules. 6 | * @param {Object} nestedMetadata the nestedMetadata object that's to be populated with the paths. 7 | * @param {String} currentBlock the current nested block name being processed. 8 | * @returns {Object} Map of the block name along with the nested paths that can be found inside. 9 | */ 10 | export function getNestedSettingPaths( nestedSettings, nestedMetadata = {}, currentBlock = false ) { 11 | const SETTINGS_TO_SKIP = [ 'allowedBlocks' ]; 12 | for ( const [ settingKey, settingValue ] of Object.entries( nestedSettings ) ) { 13 | if ( SETTINGS_TO_SKIP.includes( settingKey ) ) { 14 | continue; 15 | } 16 | 17 | // A nested block would be in the form of blockName/childBlockName or blockName/* or * 18 | const isNestedBlock = settingKey.includes( '/' ) || settingKey === '*'; 19 | 20 | if ( isNestedBlock ) { 21 | // This setting contains another block, look at the child for metadata 22 | Object.entries( nestedSettings ).forEach( ( [ blockName, blockNestedSettings ] ) => { 23 | if ( ! SETTINGS_TO_SKIP.includes( blockName ) ) { 24 | getNestedSettingPaths( blockNestedSettings, nestedMetadata, blockName ); 25 | } 26 | } ); 27 | } else if ( currentBlock !== false ) { 28 | // This is a leaf block, add setting paths to nestedMetadata 29 | const settingPaths = flattenSettingPaths( settingValue, `${ settingKey }.` ); 30 | 31 | // eslint-disable-next-line security/detect-object-injection 32 | nestedMetadata[ currentBlock ] = { 33 | // eslint-disable-next-line security/detect-object-injection 34 | ...( nestedMetadata[ currentBlock ] ?? {} ), 35 | ...settingPaths, 36 | }; 37 | } 38 | } 39 | 40 | return nestedMetadata; 41 | } 42 | 43 | /** 44 | * Find block settings nested in other block settings. 45 | * 46 | * Given an array of blocks names from the top level of the editor to the 47 | * current block (`blockNamePath`), return the value for the deepest-nested 48 | * settings value that applies to the current block. 49 | * 50 | * If two setting values share the same nesting depth, use the last one that 51 | * occurs in settings (like CSS). 52 | * 53 | * @param {string[]} blockNamePath Block names representing the path to the 54 | * current block from the top level of the 55 | * block editor. 56 | * @param {string} normalizedPath Path to the setting being retrieved. 57 | * @param {Object} settings Object containing all block settings. 58 | * @param {Object} result Optional. Object with keys `depth` and 59 | * `value` used to track current most-nested 60 | * setting. 61 | * @param {number} depth Optional. The current recursion depth used 62 | * to calculate the most-nested setting. 63 | * @return {Object} Object with keys `depth` and `value`. 64 | * Destructure the `value` key for the result. 65 | */ 66 | export function getNestedSetting( 67 | blockNamePath, 68 | normalizedPath, 69 | settings, 70 | result = { depth: 0, value: undefined }, 71 | depth = 1 72 | ) { 73 | const [ currentBlockName, ...remainingBlockNames ] = blockNamePath; 74 | // eslint-disable-next-line security/detect-object-injection 75 | const blockSettings = settings[ currentBlockName ]; 76 | 77 | if ( remainingBlockNames.length === 0 ) { 78 | const settingValue = deepGet( blockSettings, normalizedPath ); 79 | 80 | if ( settingValue !== undefined && depth >= result.depth ) { 81 | result.depth = depth; 82 | result.value = settingValue; 83 | } 84 | 85 | return result; 86 | } else if ( blockSettings !== undefined ) { 87 | // Recurse into the parent block's settings 88 | result = getNestedSetting( 89 | remainingBlockNames, 90 | normalizedPath, 91 | blockSettings, 92 | result, 93 | depth + 1 94 | ); 95 | } 96 | 97 | // Continue down the array of blocks 98 | return getNestedSetting( remainingBlockNames, normalizedPath, settings, result, depth ); 99 | } 100 | 101 | /** 102 | * Port of lodash's get function from https://gist.github.com/andrewchilds/30a7fb18981d413260c7a36428ed13da?permalink_comment_id=4433741#gistcomment-4433741 103 | * @param {Object} value The value to query. 104 | * @param {String} query The query to run. 105 | * @param {Object} defaultVal The default value to return if the query doesn't exist. 106 | * @returns 107 | */ 108 | function deepGet( value, query, defaultVal = undefined ) { 109 | const splitQuery = Array.isArray( query ) 110 | ? query 111 | : query 112 | .replace( /(\[(\d)\])/g, '.$2' ) 113 | .replace( /^\./, '' ) 114 | .split( '.' ); 115 | 116 | if ( ! splitQuery.length || splitQuery[ 0 ] === undefined ) return value; 117 | 118 | const key = splitQuery[ 0 ]; 119 | 120 | if ( 121 | typeof value !== 'object' || 122 | value === null || 123 | ! ( key in value ) || 124 | // eslint-disable-next-line security/detect-object-injection 125 | value[ key ] === undefined 126 | ) { 127 | return defaultVal; 128 | } 129 | 130 | // eslint-disable-next-line security/detect-object-injection 131 | return deepGet( value[ key ], splitQuery.slice( 1 ), defaultVal ); 132 | } 133 | 134 | /** 135 | * Flatten a nested object into a map of paths. 136 | * @param {Object} settings The settings value that is to be flattened. 137 | * @param {String} prefix The key for the settings value. 138 | * @returns {Object} the flattened settings object. 139 | */ 140 | function flattenSettingPaths( settings, prefix = '' ) { 141 | const result = {}; 142 | 143 | Object.entries( settings ).forEach( ( [ key, value ] ) => { 144 | const isRegularObject = 145 | typeof value === 'object' && Boolean( value ) && ! Array.isArray( value ); 146 | 147 | if ( isRegularObject ) { 148 | result[ `${ prefix }${ key }` ] = true; 149 | Object.assign( result, flattenSettingPaths( value, `${ prefix }${ key }.` ) ); 150 | } else { 151 | result[ `${ prefix }${ key }` ] = true; 152 | } 153 | } ); 154 | 155 | return result; 156 | } 157 | -------------------------------------------------------------------------------- /src/nested-governance-loader.test.js: -------------------------------------------------------------------------------- 1 | import { getNestedSettingPaths, getNestedSetting } from './nested-governance-loader'; 2 | 3 | describe( 'getNestedSettingPaths', () => { 4 | describe( 'getNestedSettingPaths', () => { 5 | it( 'should return the nested setting paths given the nested settings', () => { 6 | const result = getNestedSettingPaths( getTestNestedSettings() ); 7 | expect( result ).toEqual( { 8 | 'core/heading': { 9 | 'color.palette': true, 10 | 'color.palette.theme': true, 11 | }, 12 | 'core/paragraph': { 13 | 'color.palette': true, 14 | 'color.palette.theme': true, 15 | }, 16 | } ); 17 | } ); 18 | } ); 19 | 20 | describe( 'getNestedSetting', () => { 21 | it( 'should return the nested setting found at the top level', () => { 22 | const result = getNestedSetting( 23 | [ 'core/heading' ], 24 | 'color.palette.theme', 25 | getTestNestedSettings() 26 | ); 27 | expect( result ).toEqual( { 28 | depth: 1, 29 | value: [ 30 | { 31 | color: '#FFFFF', 32 | name: 'Primary', 33 | slug: 'primary', 34 | }, 35 | ], 36 | } ); 37 | } ); 38 | 39 | it( 'should return the nested setting found deep inside a nested block', () => { 40 | const result = getNestedSetting( 41 | [ 'core/media-text', 'core/quote', 'core/paragraph' ], 42 | 'color.palette.theme', 43 | getTestNestedSettings() 44 | ); 45 | expect( result ).toEqual( { 46 | depth: 3, 47 | value: [ 48 | { 49 | color: '#F00001', 50 | name: 'Tertiary', 51 | slug: 'tertiary', 52 | }, 53 | ], 54 | } ); 55 | } ); 56 | } ); 57 | } ); 58 | 59 | function getTestNestedSettings() { 60 | return { 61 | 'core/heading': { 62 | color: { 63 | palette: { 64 | theme: [ 65 | { 66 | color: '#FFFFF', 67 | name: 'Primary', 68 | slug: 'primary', 69 | }, 70 | ], 71 | }, 72 | }, 73 | }, 74 | 'core/quote': { 75 | allowedBlocks: [ 'core/paragraph' ], 76 | 'core/paragraph': { 77 | color: { 78 | palette: { 79 | theme: [ 80 | { 81 | color: '#F00000', 82 | name: 'Secondary', 83 | slug: 'secondary', 84 | }, 85 | ], 86 | }, 87 | }, 88 | }, 89 | }, 90 | 'core/media-text': { 91 | allowedBlocks: [ 'core/heading', 'core/paragraph' ], 92 | 'core/heading': { 93 | color: { 94 | palette: { 95 | theme: [ 96 | { 97 | color: '#FFFFF', 98 | name: 'Primary', 99 | slug: 'primary', 100 | }, 101 | ], 102 | }, 103 | }, 104 | }, 105 | 'core/quote': { 106 | allowedBlocks: [ 'core/paragraph' ], 107 | 'core/paragraph': { 108 | color: { 109 | palette: { 110 | theme: [ 111 | { 112 | color: '#F00001', 113 | name: 'Tertiary', 114 | slug: 'tertiary', 115 | }, 116 | ], 117 | }, 118 | }, 119 | }, 120 | }, 121 | }, 122 | }; 123 | } 124 | -------------------------------------------------------------------------------- /vendor/autoload.php: -------------------------------------------------------------------------------- 1 | 7 | * Jordi Boggiano 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace Composer\Autoload; 14 | 15 | /** 16 | * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. 17 | * 18 | * $loader = new \Composer\Autoload\ClassLoader(); 19 | * 20 | * // register classes with namespaces 21 | * $loader->add('Symfony\Component', __DIR__.'/component'); 22 | * $loader->add('Symfony', __DIR__.'/framework'); 23 | * 24 | * // activate the autoloader 25 | * $loader->register(); 26 | * 27 | * // to enable searching the include path (eg. for PEAR packages) 28 | * $loader->setUseIncludePath(true); 29 | * 30 | * In this example, if you try to use a class in the Symfony\Component 31 | * namespace or one of its children (Symfony\Component\Console for instance), 32 | * the autoloader will first look for the class under the component/ 33 | * directory, and it will then fallback to the framework/ directory if not 34 | * found before giving up. 35 | * 36 | * This class is loosely based on the Symfony UniversalClassLoader. 37 | * 38 | * @author Fabien Potencier 39 | * @author Jordi Boggiano 40 | * @see https://www.php-fig.org/psr/psr-0/ 41 | * @see https://www.php-fig.org/psr/psr-4/ 42 | */ 43 | class ClassLoader 44 | { 45 | /** @var \Closure(string):void */ 46 | private static $includeFile; 47 | 48 | /** @var string|null */ 49 | private $vendorDir; 50 | 51 | // PSR-4 52 | /** 53 | * @var array> 54 | */ 55 | private $prefixLengthsPsr4 = array(); 56 | /** 57 | * @var array> 58 | */ 59 | private $prefixDirsPsr4 = array(); 60 | /** 61 | * @var list 62 | */ 63 | private $fallbackDirsPsr4 = array(); 64 | 65 | // PSR-0 66 | /** 67 | * List of PSR-0 prefixes 68 | * 69 | * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) 70 | * 71 | * @var array>> 72 | */ 73 | private $prefixesPsr0 = array(); 74 | /** 75 | * @var list 76 | */ 77 | private $fallbackDirsPsr0 = array(); 78 | 79 | /** @var bool */ 80 | private $useIncludePath = false; 81 | 82 | /** 83 | * @var array 84 | */ 85 | private $classMap = array(); 86 | 87 | /** @var bool */ 88 | private $classMapAuthoritative = false; 89 | 90 | /** 91 | * @var array 92 | */ 93 | private $missingClasses = array(); 94 | 95 | /** @var string|null */ 96 | private $apcuPrefix; 97 | 98 | /** 99 | * @var array 100 | */ 101 | private static $registeredLoaders = array(); 102 | 103 | /** 104 | * @param string|null $vendorDir 105 | */ 106 | public function __construct($vendorDir = null) 107 | { 108 | $this->vendorDir = $vendorDir; 109 | self::initializeIncludeClosure(); 110 | } 111 | 112 | /** 113 | * @return array> 114 | */ 115 | public function getPrefixes() 116 | { 117 | if (!empty($this->prefixesPsr0)) { 118 | return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); 119 | } 120 | 121 | return array(); 122 | } 123 | 124 | /** 125 | * @return array> 126 | */ 127 | public function getPrefixesPsr4() 128 | { 129 | return $this->prefixDirsPsr4; 130 | } 131 | 132 | /** 133 | * @return list 134 | */ 135 | public function getFallbackDirs() 136 | { 137 | return $this->fallbackDirsPsr0; 138 | } 139 | 140 | /** 141 | * @return list 142 | */ 143 | public function getFallbackDirsPsr4() 144 | { 145 | return $this->fallbackDirsPsr4; 146 | } 147 | 148 | /** 149 | * @return array Array of classname => path 150 | */ 151 | public function getClassMap() 152 | { 153 | return $this->classMap; 154 | } 155 | 156 | /** 157 | * @param array $classMap Class to filename map 158 | * 159 | * @return void 160 | */ 161 | public function addClassMap(array $classMap) 162 | { 163 | if ($this->classMap) { 164 | $this->classMap = array_merge($this->classMap, $classMap); 165 | } else { 166 | $this->classMap = $classMap; 167 | } 168 | } 169 | 170 | /** 171 | * Registers a set of PSR-0 directories for a given prefix, either 172 | * appending or prepending to the ones previously set for this prefix. 173 | * 174 | * @param string $prefix The prefix 175 | * @param list|string $paths The PSR-0 root directories 176 | * @param bool $prepend Whether to prepend the directories 177 | * 178 | * @return void 179 | */ 180 | public function add($prefix, $paths, $prepend = false) 181 | { 182 | $paths = (array) $paths; 183 | if (!$prefix) { 184 | if ($prepend) { 185 | $this->fallbackDirsPsr0 = array_merge( 186 | $paths, 187 | $this->fallbackDirsPsr0 188 | ); 189 | } else { 190 | $this->fallbackDirsPsr0 = array_merge( 191 | $this->fallbackDirsPsr0, 192 | $paths 193 | ); 194 | } 195 | 196 | return; 197 | } 198 | 199 | $first = $prefix[0]; 200 | if (!isset($this->prefixesPsr0[$first][$prefix])) { 201 | $this->prefixesPsr0[$first][$prefix] = $paths; 202 | 203 | return; 204 | } 205 | if ($prepend) { 206 | $this->prefixesPsr0[$first][$prefix] = array_merge( 207 | $paths, 208 | $this->prefixesPsr0[$first][$prefix] 209 | ); 210 | } else { 211 | $this->prefixesPsr0[$first][$prefix] = array_merge( 212 | $this->prefixesPsr0[$first][$prefix], 213 | $paths 214 | ); 215 | } 216 | } 217 | 218 | /** 219 | * Registers a set of PSR-4 directories for a given namespace, either 220 | * appending or prepending to the ones previously set for this namespace. 221 | * 222 | * @param string $prefix The prefix/namespace, with trailing '\\' 223 | * @param list|string $paths The PSR-4 base directories 224 | * @param bool $prepend Whether to prepend the directories 225 | * 226 | * @throws \InvalidArgumentException 227 | * 228 | * @return void 229 | */ 230 | public function addPsr4($prefix, $paths, $prepend = false) 231 | { 232 | $paths = (array) $paths; 233 | if (!$prefix) { 234 | // Register directories for the root namespace. 235 | if ($prepend) { 236 | $this->fallbackDirsPsr4 = array_merge( 237 | $paths, 238 | $this->fallbackDirsPsr4 239 | ); 240 | } else { 241 | $this->fallbackDirsPsr4 = array_merge( 242 | $this->fallbackDirsPsr4, 243 | $paths 244 | ); 245 | } 246 | } elseif (!isset($this->prefixDirsPsr4[$prefix])) { 247 | // Register directories for a new namespace. 248 | $length = strlen($prefix); 249 | if ('\\' !== $prefix[$length - 1]) { 250 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 251 | } 252 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 253 | $this->prefixDirsPsr4[$prefix] = $paths; 254 | } elseif ($prepend) { 255 | // Prepend directories for an already registered namespace. 256 | $this->prefixDirsPsr4[$prefix] = array_merge( 257 | $paths, 258 | $this->prefixDirsPsr4[$prefix] 259 | ); 260 | } else { 261 | // Append directories for an already registered namespace. 262 | $this->prefixDirsPsr4[$prefix] = array_merge( 263 | $this->prefixDirsPsr4[$prefix], 264 | $paths 265 | ); 266 | } 267 | } 268 | 269 | /** 270 | * Registers a set of PSR-0 directories for a given prefix, 271 | * replacing any others previously set for this prefix. 272 | * 273 | * @param string $prefix The prefix 274 | * @param list|string $paths The PSR-0 base directories 275 | * 276 | * @return void 277 | */ 278 | public function set($prefix, $paths) 279 | { 280 | if (!$prefix) { 281 | $this->fallbackDirsPsr0 = (array) $paths; 282 | } else { 283 | $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; 284 | } 285 | } 286 | 287 | /** 288 | * Registers a set of PSR-4 directories for a given namespace, 289 | * replacing any others previously set for this namespace. 290 | * 291 | * @param string $prefix The prefix/namespace, with trailing '\\' 292 | * @param list|string $paths The PSR-4 base directories 293 | * 294 | * @throws \InvalidArgumentException 295 | * 296 | * @return void 297 | */ 298 | public function setPsr4($prefix, $paths) 299 | { 300 | if (!$prefix) { 301 | $this->fallbackDirsPsr4 = (array) $paths; 302 | } else { 303 | $length = strlen($prefix); 304 | if ('\\' !== $prefix[$length - 1]) { 305 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 306 | } 307 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 308 | $this->prefixDirsPsr4[$prefix] = (array) $paths; 309 | } 310 | } 311 | 312 | /** 313 | * Turns on searching the include path for class files. 314 | * 315 | * @param bool $useIncludePath 316 | * 317 | * @return void 318 | */ 319 | public function setUseIncludePath($useIncludePath) 320 | { 321 | $this->useIncludePath = $useIncludePath; 322 | } 323 | 324 | /** 325 | * Can be used to check if the autoloader uses the include path to check 326 | * for classes. 327 | * 328 | * @return bool 329 | */ 330 | public function getUseIncludePath() 331 | { 332 | return $this->useIncludePath; 333 | } 334 | 335 | /** 336 | * Turns off searching the prefix and fallback directories for classes 337 | * that have not been registered with the class map. 338 | * 339 | * @param bool $classMapAuthoritative 340 | * 341 | * @return void 342 | */ 343 | public function setClassMapAuthoritative($classMapAuthoritative) 344 | { 345 | $this->classMapAuthoritative = $classMapAuthoritative; 346 | } 347 | 348 | /** 349 | * Should class lookup fail if not found in the current class map? 350 | * 351 | * @return bool 352 | */ 353 | public function isClassMapAuthoritative() 354 | { 355 | return $this->classMapAuthoritative; 356 | } 357 | 358 | /** 359 | * APCu prefix to use to cache found/not-found classes, if the extension is enabled. 360 | * 361 | * @param string|null $apcuPrefix 362 | * 363 | * @return void 364 | */ 365 | public function setApcuPrefix($apcuPrefix) 366 | { 367 | $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; 368 | } 369 | 370 | /** 371 | * The APCu prefix in use, or null if APCu caching is not enabled. 372 | * 373 | * @return string|null 374 | */ 375 | public function getApcuPrefix() 376 | { 377 | return $this->apcuPrefix; 378 | } 379 | 380 | /** 381 | * Registers this instance as an autoloader. 382 | * 383 | * @param bool $prepend Whether to prepend the autoloader or not 384 | * 385 | * @return void 386 | */ 387 | public function register($prepend = false) 388 | { 389 | spl_autoload_register(array($this, 'loadClass'), true, $prepend); 390 | 391 | if (null === $this->vendorDir) { 392 | return; 393 | } 394 | 395 | if ($prepend) { 396 | self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; 397 | } else { 398 | unset(self::$registeredLoaders[$this->vendorDir]); 399 | self::$registeredLoaders[$this->vendorDir] = $this; 400 | } 401 | } 402 | 403 | /** 404 | * Unregisters this instance as an autoloader. 405 | * 406 | * @return void 407 | */ 408 | public function unregister() 409 | { 410 | spl_autoload_unregister(array($this, 'loadClass')); 411 | 412 | if (null !== $this->vendorDir) { 413 | unset(self::$registeredLoaders[$this->vendorDir]); 414 | } 415 | } 416 | 417 | /** 418 | * Loads the given class or interface. 419 | * 420 | * @param string $class The name of the class 421 | * @return true|null True if loaded, null otherwise 422 | */ 423 | public function loadClass($class) 424 | { 425 | if ($file = $this->findFile($class)) { 426 | $includeFile = self::$includeFile; 427 | $includeFile($file); 428 | 429 | return true; 430 | } 431 | 432 | return null; 433 | } 434 | 435 | /** 436 | * Finds the path to the file where the class is defined. 437 | * 438 | * @param string $class The name of the class 439 | * 440 | * @return string|false The path if found, false otherwise 441 | */ 442 | public function findFile($class) 443 | { 444 | // class map lookup 445 | if (isset($this->classMap[$class])) { 446 | return $this->classMap[$class]; 447 | } 448 | if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { 449 | return false; 450 | } 451 | if (null !== $this->apcuPrefix) { 452 | $file = apcu_fetch($this->apcuPrefix.$class, $hit); 453 | if ($hit) { 454 | return $file; 455 | } 456 | } 457 | 458 | $file = $this->findFileWithExtension($class, '.php'); 459 | 460 | // Search for Hack files if we are running on HHVM 461 | if (false === $file && defined('HHVM_VERSION')) { 462 | $file = $this->findFileWithExtension($class, '.hh'); 463 | } 464 | 465 | if (null !== $this->apcuPrefix) { 466 | apcu_add($this->apcuPrefix.$class, $file); 467 | } 468 | 469 | if (false === $file) { 470 | // Remember that this class does not exist. 471 | $this->missingClasses[$class] = true; 472 | } 473 | 474 | return $file; 475 | } 476 | 477 | /** 478 | * Returns the currently registered loaders keyed by their corresponding vendor directories. 479 | * 480 | * @return array 481 | */ 482 | public static function getRegisteredLoaders() 483 | { 484 | return self::$registeredLoaders; 485 | } 486 | 487 | /** 488 | * @param string $class 489 | * @param string $ext 490 | * @return string|false 491 | */ 492 | private function findFileWithExtension($class, $ext) 493 | { 494 | // PSR-4 lookup 495 | $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; 496 | 497 | $first = $class[0]; 498 | if (isset($this->prefixLengthsPsr4[$first])) { 499 | $subPath = $class; 500 | while (false !== $lastPos = strrpos($subPath, '\\')) { 501 | $subPath = substr($subPath, 0, $lastPos); 502 | $search = $subPath . '\\'; 503 | if (isset($this->prefixDirsPsr4[$search])) { 504 | $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); 505 | foreach ($this->prefixDirsPsr4[$search] as $dir) { 506 | if (file_exists($file = $dir . $pathEnd)) { 507 | return $file; 508 | } 509 | } 510 | } 511 | } 512 | } 513 | 514 | // PSR-4 fallback dirs 515 | foreach ($this->fallbackDirsPsr4 as $dir) { 516 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { 517 | return $file; 518 | } 519 | } 520 | 521 | // PSR-0 lookup 522 | if (false !== $pos = strrpos($class, '\\')) { 523 | // namespaced class name 524 | $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) 525 | . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); 526 | } else { 527 | // PEAR-like class name 528 | $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; 529 | } 530 | 531 | if (isset($this->prefixesPsr0[$first])) { 532 | foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { 533 | if (0 === strpos($class, $prefix)) { 534 | foreach ($dirs as $dir) { 535 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 536 | return $file; 537 | } 538 | } 539 | } 540 | } 541 | } 542 | 543 | // PSR-0 fallback dirs 544 | foreach ($this->fallbackDirsPsr0 as $dir) { 545 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 546 | return $file; 547 | } 548 | } 549 | 550 | // PSR-0 include paths. 551 | if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { 552 | return $file; 553 | } 554 | 555 | return false; 556 | } 557 | 558 | /** 559 | * @return void 560 | */ 561 | private static function initializeIncludeClosure() 562 | { 563 | if (self::$includeFile !== null) { 564 | return; 565 | } 566 | 567 | /** 568 | * Scope isolated include. 569 | * 570 | * Prevents access to $this/self from included files. 571 | * 572 | * @param string $file 573 | * @return void 574 | */ 575 | self::$includeFile = \Closure::bind(static function($file) { 576 | include $file; 577 | }, null, null); 578 | } 579 | } 580 | -------------------------------------------------------------------------------- /vendor/composer/InstalledVersions.php: -------------------------------------------------------------------------------- 1 | 7 | * Jordi Boggiano 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace Composer; 14 | 15 | use Composer\Autoload\ClassLoader; 16 | use Composer\Semver\VersionParser; 17 | 18 | /** 19 | * This class is copied in every Composer installed project and available to all 20 | * 21 | * See also https://getcomposer.org/doc/07-runtime.md#installed-versions 22 | * 23 | * To require its presence, you can require `composer-runtime-api ^2.0` 24 | * 25 | * @final 26 | */ 27 | class InstalledVersions 28 | { 29 | /** 30 | * @var mixed[]|null 31 | * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null 32 | */ 33 | private static $installed; 34 | 35 | /** 36 | * @var bool|null 37 | */ 38 | private static $canGetVendors; 39 | 40 | /** 41 | * @var array[] 42 | * @psalm-var array}> 43 | */ 44 | private static $installedByVendor = array(); 45 | 46 | /** 47 | * Returns a list of all package names which are present, either by being installed, replaced or provided 48 | * 49 | * @return string[] 50 | * @psalm-return list 51 | */ 52 | public static function getInstalledPackages() 53 | { 54 | $packages = array(); 55 | foreach (self::getInstalled() as $installed) { 56 | $packages[] = array_keys($installed['versions']); 57 | } 58 | 59 | if (1 === \count($packages)) { 60 | return $packages[0]; 61 | } 62 | 63 | return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); 64 | } 65 | 66 | /** 67 | * Returns a list of all package names with a specific type e.g. 'library' 68 | * 69 | * @param string $type 70 | * @return string[] 71 | * @psalm-return list 72 | */ 73 | public static function getInstalledPackagesByType($type) 74 | { 75 | $packagesByType = array(); 76 | 77 | foreach (self::getInstalled() as $installed) { 78 | foreach ($installed['versions'] as $name => $package) { 79 | if (isset($package['type']) && $package['type'] === $type) { 80 | $packagesByType[] = $name; 81 | } 82 | } 83 | } 84 | 85 | return $packagesByType; 86 | } 87 | 88 | /** 89 | * Checks whether the given package is installed 90 | * 91 | * This also returns true if the package name is provided or replaced by another package 92 | * 93 | * @param string $packageName 94 | * @param bool $includeDevRequirements 95 | * @return bool 96 | */ 97 | public static function isInstalled($packageName, $includeDevRequirements = true) 98 | { 99 | foreach (self::getInstalled() as $installed) { 100 | if (isset($installed['versions'][$packageName])) { 101 | return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; 102 | } 103 | } 104 | 105 | return false; 106 | } 107 | 108 | /** 109 | * Checks whether the given package satisfies a version constraint 110 | * 111 | * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: 112 | * 113 | * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') 114 | * 115 | * @param VersionParser $parser Install composer/semver to have access to this class and functionality 116 | * @param string $packageName 117 | * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package 118 | * @return bool 119 | */ 120 | public static function satisfies(VersionParser $parser, $packageName, $constraint) 121 | { 122 | $constraint = $parser->parseConstraints((string) $constraint); 123 | $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); 124 | 125 | return $provided->matches($constraint); 126 | } 127 | 128 | /** 129 | * Returns a version constraint representing all the range(s) which are installed for a given package 130 | * 131 | * It is easier to use this via isInstalled() with the $constraint argument if you need to check 132 | * whether a given version of a package is installed, and not just whether it exists 133 | * 134 | * @param string $packageName 135 | * @return string Version constraint usable with composer/semver 136 | */ 137 | public static function getVersionRanges($packageName) 138 | { 139 | foreach (self::getInstalled() as $installed) { 140 | if (!isset($installed['versions'][$packageName])) { 141 | continue; 142 | } 143 | 144 | $ranges = array(); 145 | if (isset($installed['versions'][$packageName]['pretty_version'])) { 146 | $ranges[] = $installed['versions'][$packageName]['pretty_version']; 147 | } 148 | if (array_key_exists('aliases', $installed['versions'][$packageName])) { 149 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); 150 | } 151 | if (array_key_exists('replaced', $installed['versions'][$packageName])) { 152 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); 153 | } 154 | if (array_key_exists('provided', $installed['versions'][$packageName])) { 155 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); 156 | } 157 | 158 | return implode(' || ', $ranges); 159 | } 160 | 161 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 162 | } 163 | 164 | /** 165 | * @param string $packageName 166 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present 167 | */ 168 | public static function getVersion($packageName) 169 | { 170 | foreach (self::getInstalled() as $installed) { 171 | if (!isset($installed['versions'][$packageName])) { 172 | continue; 173 | } 174 | 175 | if (!isset($installed['versions'][$packageName]['version'])) { 176 | return null; 177 | } 178 | 179 | return $installed['versions'][$packageName]['version']; 180 | } 181 | 182 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 183 | } 184 | 185 | /** 186 | * @param string $packageName 187 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present 188 | */ 189 | public static function getPrettyVersion($packageName) 190 | { 191 | foreach (self::getInstalled() as $installed) { 192 | if (!isset($installed['versions'][$packageName])) { 193 | continue; 194 | } 195 | 196 | if (!isset($installed['versions'][$packageName]['pretty_version'])) { 197 | return null; 198 | } 199 | 200 | return $installed['versions'][$packageName]['pretty_version']; 201 | } 202 | 203 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 204 | } 205 | 206 | /** 207 | * @param string $packageName 208 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference 209 | */ 210 | public static function getReference($packageName) 211 | { 212 | foreach (self::getInstalled() as $installed) { 213 | if (!isset($installed['versions'][$packageName])) { 214 | continue; 215 | } 216 | 217 | if (!isset($installed['versions'][$packageName]['reference'])) { 218 | return null; 219 | } 220 | 221 | return $installed['versions'][$packageName]['reference']; 222 | } 223 | 224 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 225 | } 226 | 227 | /** 228 | * @param string $packageName 229 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. 230 | */ 231 | public static function getInstallPath($packageName) 232 | { 233 | foreach (self::getInstalled() as $installed) { 234 | if (!isset($installed['versions'][$packageName])) { 235 | continue; 236 | } 237 | 238 | return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; 239 | } 240 | 241 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 242 | } 243 | 244 | /** 245 | * @return array 246 | * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} 247 | */ 248 | public static function getRootPackage() 249 | { 250 | $installed = self::getInstalled(); 251 | 252 | return $installed[0]['root']; 253 | } 254 | 255 | /** 256 | * Returns the raw installed.php data for custom implementations 257 | * 258 | * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. 259 | * @return array[] 260 | * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} 261 | */ 262 | public static function getRawData() 263 | { 264 | @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); 265 | 266 | if (null === self::$installed) { 267 | // only require the installed.php file if this file is loaded from its dumped location, 268 | // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 269 | if (substr(__DIR__, -8, 1) !== 'C') { 270 | self::$installed = include __DIR__ . '/installed.php'; 271 | } else { 272 | self::$installed = array(); 273 | } 274 | } 275 | 276 | return self::$installed; 277 | } 278 | 279 | /** 280 | * Returns the raw data of all installed.php which are currently loaded for custom implementations 281 | * 282 | * @return array[] 283 | * @psalm-return list}> 284 | */ 285 | public static function getAllRawData() 286 | { 287 | return self::getInstalled(); 288 | } 289 | 290 | /** 291 | * Lets you reload the static array from another file 292 | * 293 | * This is only useful for complex integrations in which a project needs to use 294 | * this class but then also needs to execute another project's autoloader in process, 295 | * and wants to ensure both projects have access to their version of installed.php. 296 | * 297 | * A typical case would be PHPUnit, where it would need to make sure it reads all 298 | * the data it needs from this class, then call reload() with 299 | * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure 300 | * the project in which it runs can then also use this class safely, without 301 | * interference between PHPUnit's dependencies and the project's dependencies. 302 | * 303 | * @param array[] $data A vendor/composer/installed.php data set 304 | * @return void 305 | * 306 | * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data 307 | */ 308 | public static function reload($data) 309 | { 310 | self::$installed = $data; 311 | self::$installedByVendor = array(); 312 | } 313 | 314 | /** 315 | * @return array[] 316 | * @psalm-return list}> 317 | */ 318 | private static function getInstalled() 319 | { 320 | if (null === self::$canGetVendors) { 321 | self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); 322 | } 323 | 324 | $installed = array(); 325 | 326 | if (self::$canGetVendors) { 327 | foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { 328 | if (isset(self::$installedByVendor[$vendorDir])) { 329 | $installed[] = self::$installedByVendor[$vendorDir]; 330 | } elseif (is_file($vendorDir.'/composer/installed.php')) { 331 | /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ 332 | $required = require $vendorDir.'/composer/installed.php'; 333 | $installed[] = self::$installedByVendor[$vendorDir] = $required; 334 | if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { 335 | self::$installed = $installed[count($installed) - 1]; 336 | } 337 | } 338 | } 339 | } 340 | 341 | if (null === self::$installed) { 342 | // only require the installed.php file if this file is loaded from its dumped location, 343 | // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 344 | if (substr(__DIR__, -8, 1) !== 'C') { 345 | /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ 346 | $required = require __DIR__ . '/installed.php'; 347 | self::$installed = $required; 348 | } else { 349 | self::$installed = array(); 350 | } 351 | } 352 | 353 | if (self::$installed !== array()) { 354 | $installed[] = self::$installed; 355 | } 356 | 357 | return $installed; 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /vendor/composer/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) Nils Adermann, Jordi Boggiano 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /vendor/composer/autoload_classmap.php: -------------------------------------------------------------------------------- 1 | $vendorDir . '/composer/InstalledVersions.php', 10 | ); 11 | -------------------------------------------------------------------------------- /vendor/composer/autoload_namespaces.php: -------------------------------------------------------------------------------- 1 | array($vendorDir . '/seld/jsonlint/src/Seld/JsonLint'), 10 | ); 11 | -------------------------------------------------------------------------------- /vendor/composer/autoload_real.php: -------------------------------------------------------------------------------- 1 | register(true); 35 | 36 | return $loader; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /vendor/composer/autoload_static.php: -------------------------------------------------------------------------------- 1 | 11 | array ( 12 | 'Seld\\JsonLint\\' => 14, 13 | ), 14 | ); 15 | 16 | public static $prefixDirsPsr4 = array ( 17 | 'Seld\\JsonLint\\' => 18 | array ( 19 | 0 => __DIR__ . '/..' . '/seld/jsonlint/src/Seld/JsonLint', 20 | ), 21 | ); 22 | 23 | public static $classMap = array ( 24 | 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 25 | ); 26 | 27 | public static function getInitializer(ClassLoader $loader) 28 | { 29 | return \Closure::bind(function () use ($loader) { 30 | $loader->prefixLengthsPsr4 = ComposerStaticInitdd8012b317c6b32d326fb106782551b7::$prefixLengthsPsr4; 31 | $loader->prefixDirsPsr4 = ComposerStaticInitdd8012b317c6b32d326fb106782551b7::$prefixDirsPsr4; 32 | $loader->classMap = ComposerStaticInitdd8012b317c6b32d326fb106782551b7::$classMap; 33 | 34 | }, null, ClassLoader::class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /vendor/composer/installed.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | { 4 | "name": "seld/jsonlint", 5 | "version": "1.10.2", 6 | "version_normalized": "1.10.2.0", 7 | "source": { 8 | "type": "git", 9 | "url": "https://github.com/Seldaek/jsonlint.git", 10 | "reference": "9bb7db07b5d66d90f6ebf542f09fc67d800e5259" 11 | }, 12 | "dist": { 13 | "type": "zip", 14 | "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9bb7db07b5d66d90f6ebf542f09fc67d800e5259", 15 | "reference": "9bb7db07b5d66d90f6ebf542f09fc67d800e5259", 16 | "shasum": "" 17 | }, 18 | "require": { 19 | "php": "^5.3 || ^7.0 || ^8.0" 20 | }, 21 | "require-dev": { 22 | "phpstan/phpstan": "^1.5", 23 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" 24 | }, 25 | "time": "2024-02-07T12:57:50+00:00", 26 | "bin": [ 27 | "bin/jsonlint" 28 | ], 29 | "type": "library", 30 | "installation-source": "dist", 31 | "autoload": { 32 | "psr-4": { 33 | "Seld\\JsonLint\\": "src/Seld/JsonLint/" 34 | } 35 | }, 36 | "notification-url": "https://packagist.org/downloads/", 37 | "license": [ 38 | "MIT" 39 | ], 40 | "authors": [ 41 | { 42 | "name": "Jordi Boggiano", 43 | "email": "j.boggiano@seld.be", 44 | "homepage": "https://seld.be" 45 | } 46 | ], 47 | "description": "JSON Linter", 48 | "keywords": [ 49 | "json", 50 | "linter", 51 | "parser", 52 | "validator" 53 | ], 54 | "support": { 55 | "issues": "https://github.com/Seldaek/jsonlint/issues", 56 | "source": "https://github.com/Seldaek/jsonlint/tree/1.10.2" 57 | }, 58 | "funding": [ 59 | { 60 | "url": "https://github.com/Seldaek", 61 | "type": "github" 62 | }, 63 | { 64 | "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", 65 | "type": "tidelift" 66 | } 67 | ], 68 | "install-path": "../seld/jsonlint" 69 | } 70 | ], 71 | "dev": false, 72 | "dev-package-names": [] 73 | } 74 | -------------------------------------------------------------------------------- /vendor/composer/installed.php: -------------------------------------------------------------------------------- 1 | array( 3 | 'name' => 'automattic/vip-governance', 4 | 'pretty_version' => 'dev-trunk', 5 | 'version' => 'dev-trunk', 6 | 'reference' => '206d5d44ac106c19bd88abdee391f7cf92db638d', 7 | 'type' => 'wordpress-plugin', 8 | 'install_path' => __DIR__ . '/../../', 9 | 'aliases' => array(), 10 | 'dev' => false, 11 | ), 12 | 'versions' => array( 13 | 'automattic/vip-governance' => array( 14 | 'pretty_version' => 'dev-trunk', 15 | 'version' => 'dev-trunk', 16 | 'reference' => '206d5d44ac106c19bd88abdee391f7cf92db638d', 17 | 'type' => 'wordpress-plugin', 18 | 'install_path' => __DIR__ . '/../../', 19 | 'aliases' => array(), 20 | 'dev_requirement' => false, 21 | ), 22 | 'seld/jsonlint' => array( 23 | 'pretty_version' => '1.10.2', 24 | 'version' => '1.10.2.0', 25 | 'reference' => '9bb7db07b5d66d90f6ebf542f09fc67d800e5259', 26 | 'type' => 'library', 27 | 'install_path' => __DIR__ . '/../seld/jsonlint', 28 | 'aliases' => array(), 29 | 'dev_requirement' => false, 30 | ), 31 | ), 32 | ); 33 | -------------------------------------------------------------------------------- /vendor/composer/platform_check.php: -------------------------------------------------------------------------------- 1 | = 80000)) { 8 | $issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.0". You are running ' . PHP_VERSION . '.'; 9 | } 10 | 11 | if ($issues) { 12 | if (!headers_sent()) { 13 | header('HTTP/1.1 500 Internal Server Error'); 14 | } 15 | if (!ini_get('display_errors')) { 16 | if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { 17 | fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); 18 | } elseif (!headers_sent()) { 19 | echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; 20 | } 21 | } 22 | trigger_error( 23 | 'Composer detected issues in your platform: ' . implode(' ', $issues), 24 | E_USER_ERROR 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /vendor/seld/jsonlint/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | You can find newer changelog entries in [GitHub releases](https://github.com/Seldaek/jsonlint/releases) 2 | 3 | ### 1.10.0 (2023-05-11) 4 | 5 | * Added ALLOW_COMMENTS flag to parse while allowing (and ignoring) inline `//` and multiline `/* */` comments in the JSON document (#81) 6 | 7 | ### 1.9.0 (2022-04-01) 8 | 9 | * Internal cleanups and type fixes 10 | 11 | ### 1.8.1 (2020-08-13) 12 | 13 | * Added type annotations 14 | 15 | ### 1.8.0 (2020-04-30) 16 | 17 | * Improved lexer performance 18 | * Added (tentative) support for PHP 8 19 | * Fixed wording of error reporting for invalid strings when the error happened after the 20th character 20 | 21 | ### 1.7.2 (2019-10-24) 22 | 23 | * Fixed issue decoding some unicode escaped characters (for " and ') 24 | 25 | ### 1.7.1 (2018-01-24) 26 | 27 | * Fixed PHP 5.3 compatibility in bin/jsonlint 28 | 29 | ### 1.7.0 (2018-01-03) 30 | 31 | * Added ability to lint multiple files at once using the jsonlint binary 32 | 33 | ### 1.6.2 (2017-11-30) 34 | 35 | * No meaningful public changes 36 | 37 | ### 1.6.1 (2017-06-18) 38 | 39 | * Fixed parsing of `0` as invalid 40 | 41 | ### 1.6.0 (2017-03-06) 42 | 43 | * Added $flags arg to JsonParser::lint() to take the same flag as parse() did 44 | * Fixed backtracking performance issues on long strings with a lot of escaped characters 45 | 46 | ### 1.5.0 (2016-11-14) 47 | 48 | * Added support for PHP 7.1 (which converts `{"":""}` to an object property called `""` and not `"_empty_"` like 7.0 and below). 49 | 50 | ### 1.4.0 (2015-11-21) 51 | 52 | * Added a DuplicateKeyException allowing for more specific error detection and handling 53 | 54 | ### 1.3.1 (2015-01-04) 55 | 56 | * Fixed segfault when parsing large JSON strings 57 | 58 | ### 1.3.0 (2014-09-05) 59 | 60 | * Added parsing to an associative array via JsonParser::PARSE_TO_ASSOC 61 | * Fixed a warning when rendering parse errors on empty lines 62 | 63 | ### 1.2.0 (2014-07-20) 64 | 65 | * Added support for linting multiple files at once in bin/jsonlint 66 | * Added a -q/--quiet flag to suppress the output 67 | * Fixed error output being on STDOUT instead of STDERR 68 | * Fixed parameter parsing 69 | 70 | ### 1.1.2 (2013-11-04) 71 | 72 | * Fixed handling of Unicode BOMs to give a better failure hint 73 | 74 | ### 1.1.1 (2013-02-12) 75 | 76 | * Fixed handling of empty keys in objects in certain cases 77 | 78 | ### 1.1.0 (2012-12-13) 79 | 80 | * Added optional parsing of duplicate keys into key.2, key.3, etc via JsonParser::ALLOW_DUPLICATE_KEYS 81 | * Improved error reporting for common mistakes 82 | 83 | ### 1.0.1 (2012-04-03) 84 | 85 | * Added optional detection and error reporting for duplicate keys via JsonParser::DETECT_KEY_CONFLICTS 86 | * Added ability to pipe content through stdin into bin/jsonlint 87 | 88 | ### 1.0.0 (2012-03-12) 89 | 90 | * Initial release 91 | -------------------------------------------------------------------------------- /vendor/seld/jsonlint/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Jordi Boggiano 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /vendor/seld/jsonlint/README.md: -------------------------------------------------------------------------------- 1 | JSON Lint 2 | ========= 3 | 4 | [![Build Status](https://github.com/Seldaek/jsonlint/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/Seldaek/jsonlint/actions/workflows/continuous-integration.yml) 5 | 6 | Usage 7 | ----- 8 | 9 | ```php 10 | use Seld\JsonLint\JsonParser; 11 | 12 | $parser = new JsonParser(); 13 | 14 | // returns null if it's valid json, or a ParsingException object. 15 | $parser->lint($json); 16 | 17 | // Call getMessage() on the exception object to get 18 | // a well formatted error message error like this 19 | 20 | // Parse error on line 2: 21 | // ... "key": "value" "numbers": [1, 2, 3] 22 | // ----------------------^ 23 | // Expected one of: 'EOF', '}', ':', ',', ']' 24 | 25 | // Call getDetails() on the exception to get more info. 26 | 27 | // returns parsed json, like json_decode() does, but slower, throws 28 | // exceptions on failure. 29 | $parser->parse($json); 30 | ``` 31 | 32 | You can also pass additional flags to `JsonParser::lint/parse` that tweak the functionality: 33 | 34 | - `JsonParser::DETECT_KEY_CONFLICTS` throws an exception on duplicate keys. 35 | - `JsonParser::ALLOW_DUPLICATE_KEYS` collects duplicate keys. e.g. if you have two `foo` keys they will end up as `foo` and `foo.2`. 36 | - `JsonParser::PARSE_TO_ASSOC` parses to associative arrays instead of stdClass objects. 37 | - `JsonParser::ALLOW_COMMENTS` parses while allowing (and ignoring) inline `//` and multiline `/* */` comments in the JSON document. 38 | 39 | Example: 40 | 41 | ```php 42 | $parser = new JsonParser; 43 | try { 44 | $parser->parse(file_get_contents($jsonFile), JsonParser::DETECT_KEY_CONFLICTS); 45 | } catch (DuplicateKeyException $e) { 46 | $details = $e->getDetails(); 47 | echo 'Key '.$details['key'].' is a duplicate in '.$jsonFile.' at line '.$details['line']; 48 | } 49 | ``` 50 | 51 | > **Note:** This library is meant to parse JSON while providing good error messages on failure. There is no way it can be as fast as php native `json_decode()`. 52 | > 53 | > It is recommended to parse with `json_decode`, and when it fails parse again with seld/jsonlint to get a proper error message back to the user. See for example [how Composer uses this library](https://github.com/composer/composer/blob/56edd53046fd697d32b2fd2fbaf45af5d7951671/src/Composer/Json/JsonFile.php#L283-L318): 54 | 55 | 56 | Installation 57 | ------------ 58 | 59 | For a quick install with Composer use: 60 | 61 | ```bash 62 | composer require seld/jsonlint 63 | ``` 64 | 65 | JSON Lint can easily be used within another app if you have a 66 | [PSR-4](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md) 67 | autoloader, or it can be installed through [Composer](https://getcomposer.org/) 68 | for use as a CLI util. 69 | Once installed via Composer you can run the following command to lint a json file or URL: 70 | 71 | $ bin/jsonlint file.json 72 | 73 | Requirements 74 | ------------ 75 | 76 | - PHP 5.3+ 77 | - [optional] PHPUnit 3.5+ to execute the test suite (phpunit --version) 78 | 79 | Submitting bugs and feature requests 80 | ------------------------------------ 81 | 82 | Bugs and feature request are tracked on [GitHub](https://github.com/Seldaek/jsonlint/issues) 83 | 84 | Author 85 | ------ 86 | 87 | Jordi Boggiano - - 88 | 89 | License 90 | ------- 91 | 92 | JSON Lint is licensed under the MIT License - see the LICENSE file for details 93 | 94 | Acknowledgements 95 | ---------------- 96 | 97 | This library is a port of the JavaScript [jsonlint](https://github.com/zaach/jsonlint) library. 98 | -------------------------------------------------------------------------------- /vendor/seld/jsonlint/bin/jsonlint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | function includeIfExists($file) 14 | { 15 | if (file_exists($file)) { 16 | return include $file; 17 | } 18 | } 19 | 20 | if (!includeIfExists(__DIR__.'/../vendor/autoload.php') && !includeIfExists(__DIR__.'/../../../autoload.php')) { 21 | $msg = 'You must set up the project dependencies, run the following commands:'.PHP_EOL. 22 | 'curl -sS https://getcomposer.org/installer | php'.PHP_EOL. 23 | 'php composer.phar install'.PHP_EOL; 24 | fwrite(STDERR, $msg); 25 | exit(1); 26 | } 27 | 28 | use Seld\JsonLint\JsonParser; 29 | 30 | $files = array(); 31 | $quiet = false; 32 | 33 | if (isset($_SERVER['argc']) && $_SERVER['argc'] > 1) { 34 | for ($i = 1; $i < $_SERVER['argc']; $i++) { 35 | $arg = $_SERVER['argv'][$i]; 36 | if ($arg == '-q' || $arg == '--quiet') { 37 | $quiet = true; 38 | } else { 39 | if ($arg == '-h' || $arg == '--help') { 40 | showUsage($_SERVER['argv'][0]); 41 | } else { 42 | $files[] = $arg; 43 | } 44 | } 45 | } 46 | } 47 | 48 | if (!empty($files)) { 49 | // file linting 50 | $exitCode = 0; 51 | foreach ($files as $file) { 52 | $result = lintFile($file, $quiet); 53 | if ($result === false) { 54 | $exitCode = 1; 55 | } 56 | } 57 | exit($exitCode); 58 | } else { 59 | //stdin linting 60 | if ($contents = file_get_contents('php://stdin')) { 61 | lint($contents, $quiet); 62 | } else { 63 | fwrite(STDERR, 'No file name or json input given' . PHP_EOL); 64 | exit(1); 65 | } 66 | } 67 | 68 | // stdin lint function 69 | function lint($content, $quiet = false) 70 | { 71 | $parser = new JsonParser(); 72 | if ($err = $parser->lint($content)) { 73 | fwrite(STDERR, $err->getMessage() . ' (stdin)' . PHP_EOL); 74 | exit(1); 75 | } 76 | if (!$quiet) { 77 | echo 'Valid JSON (stdin)' . PHP_EOL; 78 | exit(0); 79 | } 80 | } 81 | 82 | // file lint function 83 | function lintFile($file, $quiet = false) 84 | { 85 | if (!preg_match('{^https?://}i', $file)) { 86 | if (!file_exists($file)) { 87 | fwrite(STDERR, 'File not found: ' . $file . PHP_EOL); 88 | return false; 89 | } 90 | if (!is_readable($file)) { 91 | fwrite(STDERR, 'File not readable: ' . $file . PHP_EOL); 92 | return false; 93 | } 94 | } 95 | 96 | $content = file_get_contents($file); 97 | $parser = new JsonParser(); 98 | if ($err = $parser->lint($content)) { 99 | fwrite(STDERR, $file . ': ' . $err->getMessage() . PHP_EOL); 100 | return false; 101 | } 102 | if (!$quiet) { 103 | echo 'Valid JSON (' . $file . ')' . PHP_EOL; 104 | } 105 | return true; 106 | } 107 | 108 | // usage text function 109 | function showUsage($programPath) 110 | { 111 | echo 'Usage: '.$programPath.' file [options]'.PHP_EOL; 112 | echo PHP_EOL; 113 | echo 'Options:'.PHP_EOL; 114 | echo ' -q, --quiet Cause jsonlint to be quiet when no errors are found'.PHP_EOL; 115 | echo ' -h, --help Show this message'.PHP_EOL; 116 | exit(0); 117 | } 118 | -------------------------------------------------------------------------------- /vendor/seld/jsonlint/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seld/jsonlint", 3 | "description": "JSON Linter", 4 | "keywords": ["json", "parser", "linter", "validator"], 5 | "type": "library", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Jordi Boggiano", 10 | "email": "j.boggiano@seld.be", 11 | "homepage": "https://seld.be" 12 | } 13 | ], 14 | "require": { 15 | "php": "^5.3 || ^7.0 || ^8.0" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13", 19 | "phpstan/phpstan": "^1.5" 20 | }, 21 | "autoload": { 22 | "psr-4": { "Seld\\JsonLint\\": "src/Seld/JsonLint/" } 23 | }, 24 | "bin": ["bin/jsonlint"], 25 | "scripts": { 26 | "test": "vendor/bin/phpunit", 27 | "phpstan": "vendor/bin/phpstan analyse" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /vendor/seld/jsonlint/src/Seld/JsonLint/DuplicateKeyException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Seld\JsonLint; 13 | 14 | class DuplicateKeyException extends ParsingException 15 | { 16 | /** 17 | * @var array{key: string, line: int} 18 | */ 19 | protected $details; 20 | 21 | /** 22 | * @param string $message 23 | * @param string $key 24 | * @phpstan-param array{line: int} $details 25 | */ 26 | public function __construct($message, $key, array $details) 27 | { 28 | $details['key'] = $key; 29 | parent::__construct($message, $details); 30 | } 31 | 32 | /** 33 | * @return string 34 | */ 35 | public function getKey() 36 | { 37 | return $this->details['key']; 38 | } 39 | 40 | /** 41 | * @phpstan-return array{key: string, line: int} 42 | */ 43 | public function getDetails() 44 | { 45 | return $this->details; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /vendor/seld/jsonlint/src/Seld/JsonLint/JsonParser.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Seld\JsonLint; 13 | use stdClass; 14 | 15 | /** 16 | * Parser class 17 | * 18 | * Example: 19 | * 20 | * $parser = new JsonParser(); 21 | * // returns null if it's valid json, or an error object 22 | * $parser->lint($json); 23 | * // returns parsed json, like json_decode does, but slower, throws exceptions on failure. 24 | * $parser->parse($json); 25 | * 26 | * Ported from https://github.com/zaach/jsonlint 27 | */ 28 | class JsonParser 29 | { 30 | const DETECT_KEY_CONFLICTS = 1; 31 | const ALLOW_DUPLICATE_KEYS = 2; 32 | const PARSE_TO_ASSOC = 4; 33 | const ALLOW_COMMENTS = 8; 34 | 35 | /** @var Lexer */ 36 | private $lexer; 37 | 38 | /** 39 | * @var int 40 | * @phpstan-var int-mask-of 41 | */ 42 | private $flags; 43 | /** @var list */ 44 | private $stack; 45 | /** @var list|int|bool|float|string|null> */ 46 | private $vstack; // semantic value stack 47 | /** @var list */ 48 | private $lstack; // location stack 49 | 50 | /** 51 | * @phpstan-var array 52 | */ 53 | private $symbols = array( 54 | 'error' => 2, 55 | 'JSONString' => 3, 56 | 'STRING' => 4, 57 | 'JSONNumber' => 5, 58 | 'NUMBER' => 6, 59 | 'JSONNullLiteral' => 7, 60 | 'NULL' => 8, 61 | 'JSONBooleanLiteral' => 9, 62 | 'TRUE' => 10, 63 | 'FALSE' => 11, 64 | 'JSONText' => 12, 65 | 'JSONValue' => 13, 66 | 'EOF' => 14, 67 | 'JSONObject' => 15, 68 | 'JSONArray' => 16, 69 | '{' => 17, 70 | '}' => 18, 71 | 'JSONMemberList' => 19, 72 | 'JSONMember' => 20, 73 | ':' => 21, 74 | ',' => 22, 75 | '[' => 23, 76 | ']' => 24, 77 | 'JSONElementList' => 25, 78 | '$accept' => 0, 79 | '$end' => 1, 80 | ); 81 | 82 | /** 83 | * @phpstan-var array 84 | * @const 85 | */ 86 | private $terminals_ = array( 87 | 2 => "error", 88 | 4 => "STRING", 89 | 6 => "NUMBER", 90 | 8 => "NULL", 91 | 10 => "TRUE", 92 | 11 => "FALSE", 93 | 14 => "EOF", 94 | 17 => "{", 95 | 18 => "}", 96 | 21 => ":", 97 | 22 => ",", 98 | 23 => "[", 99 | 24 => "]", 100 | ); 101 | 102 | /** 103 | * @phpstan-var array, array{int, int}> 104 | * @const 105 | */ 106 | private $productions_ = array( 107 | 1 => array(3, 1), 108 | 2 => array(5, 1), 109 | 3 => array(7, 1), 110 | 4 => array(9, 1), 111 | 5 => array(9, 1), 112 | 6 => array(12, 2), 113 | 7 => array(13, 1), 114 | 8 => array(13, 1), 115 | 9 => array(13, 1), 116 | 10 => array(13, 1), 117 | 11 => array(13, 1), 118 | 12 => array(13, 1), 119 | 13 => array(15, 2), 120 | 14 => array(15, 3), 121 | 15 => array(20, 3), 122 | 16 => array(19, 1), 123 | 17 => array(19, 3), 124 | 18 => array(16, 2), 125 | 19 => array(16, 3), 126 | 20 => array(25, 1), 127 | 21 => array(25, 3) 128 | ); 129 | 130 | /** 131 | * @var array, array|int>> List of stateID=>symbolID=>actionIDs|actionID 132 | * @const 133 | */ 134 | private $table = array( 135 | 0 => array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 12 => 1, 13 => 2, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), 136 | 1 => array( 1 => array(3)), 137 | 2 => array( 14 => array(1,16)), 138 | 3 => array( 14 => array(2,7), 18 => array(2,7), 22 => array(2,7), 24 => array(2,7)), 139 | 4 => array( 14 => array(2,8), 18 => array(2,8), 22 => array(2,8), 24 => array(2,8)), 140 | 5 => array( 14 => array(2,9), 18 => array(2,9), 22 => array(2,9), 24 => array(2,9)), 141 | 6 => array( 14 => array(2,10), 18 => array(2,10), 22 => array(2,10), 24 => array(2,10)), 142 | 7 => array( 14 => array(2,11), 18 => array(2,11), 22 => array(2,11), 24 => array(2,11)), 143 | 8 => array( 14 => array(2,12), 18 => array(2,12), 22 => array(2,12), 24 => array(2,12)), 144 | 9 => array( 14 => array(2,3), 18 => array(2,3), 22 => array(2,3), 24 => array(2,3)), 145 | 10 => array( 14 => array(2,4), 18 => array(2,4), 22 => array(2,4), 24 => array(2,4)), 146 | 11 => array( 14 => array(2,5), 18 => array(2,5), 22 => array(2,5), 24 => array(2,5)), 147 | 12 => array( 14 => array(2,1), 18 => array(2,1), 21 => array(2,1), 22 => array(2,1), 24 => array(2,1)), 148 | 13 => array( 14 => array(2,2), 18 => array(2,2), 22 => array(2,2), 24 => array(2,2)), 149 | 14 => array( 3 => 20, 4 => array(1,12), 18 => array(1,17), 19 => 18, 20 => 19 ), 150 | 15 => array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 23, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15), 24 => array(1,21), 25 => 22 ), 151 | 16 => array( 1 => array(2,6)), 152 | 17 => array( 14 => array(2,13), 18 => array(2,13), 22 => array(2,13), 24 => array(2,13)), 153 | 18 => array( 18 => array(1,24), 22 => array(1,25)), 154 | 19 => array( 18 => array(2,16), 22 => array(2,16)), 155 | 20 => array( 21 => array(1,26)), 156 | 21 => array( 14 => array(2,18), 18 => array(2,18), 22 => array(2,18), 24 => array(2,18)), 157 | 22 => array( 22 => array(1,28), 24 => array(1,27)), 158 | 23 => array( 22 => array(2,20), 24 => array(2,20)), 159 | 24 => array( 14 => array(2,14), 18 => array(2,14), 22 => array(2,14), 24 => array(2,14)), 160 | 25 => array( 3 => 20, 4 => array(1,12), 20 => 29 ), 161 | 26 => array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 30, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), 162 | 27 => array( 14 => array(2,19), 18 => array(2,19), 22 => array(2,19), 24 => array(2,19)), 163 | 28 => array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 31, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), 164 | 29 => array( 18 => array(2,17), 22 => array(2,17)), 165 | 30 => array( 18 => array(2,15), 22 => array(2,15)), 166 | 31 => array( 22 => array(2,21), 24 => array(2,21)), 167 | ); 168 | 169 | /** 170 | * @var array{16: array{2, 6}} 171 | * @const 172 | */ 173 | private $defaultActions = array( 174 | 16 => array(2, 6) 175 | ); 176 | 177 | /** 178 | * @param string $input JSON string 179 | * @param int $flags Bitmask of parse/lint options (see constants of this class) 180 | * @return null|ParsingException null if no error is found, a ParsingException containing all details otherwise 181 | * 182 | * @phpstan-param int-mask-of $flags 183 | */ 184 | public function lint($input, $flags = 0) 185 | { 186 | try { 187 | $this->parse($input, $flags); 188 | } catch (ParsingException $e) { 189 | return $e; 190 | } 191 | return null; 192 | } 193 | 194 | /** 195 | * @param string $input JSON string 196 | * @param int $flags Bitmask of parse/lint options (see constants of this class) 197 | * @return mixed 198 | * @throws ParsingException 199 | * 200 | * @phpstan-param int-mask-of $flags 201 | */ 202 | public function parse($input, $flags = 0) 203 | { 204 | $this->failOnBOM($input); 205 | 206 | $this->flags = $flags; 207 | 208 | $this->stack = array(0); 209 | $this->vstack = array(null); 210 | $this->lstack = array(); 211 | 212 | $yytext = ''; 213 | $yylineno = 0; 214 | $yyleng = 0; 215 | /** @var int<0,3> */ 216 | $recovering = 0; 217 | 218 | $this->lexer = new Lexer($flags); 219 | $this->lexer->setInput($input); 220 | 221 | $yyloc = $this->lexer->yylloc; 222 | $this->lstack[] = $yyloc; 223 | 224 | $symbol = null; 225 | $preErrorSymbol = null; 226 | $action = null; 227 | $a = null; 228 | $r = null; 229 | $p = null; 230 | $len = null; 231 | $newState = null; 232 | $expected = null; 233 | /** @var string|null */ 234 | $errStr = null; 235 | 236 | while (true) { 237 | // retrieve state number from top of stack 238 | $state = $this->stack[\count($this->stack)-1]; 239 | 240 | // use default actions if available 241 | if (isset($this->defaultActions[$state])) { 242 | $action = $this->defaultActions[$state]; 243 | } else { 244 | if ($symbol === null) { 245 | $symbol = $this->lexer->lex(); 246 | } 247 | // read action for current state and first input 248 | /** @var array|false */ 249 | $action = isset($this->table[$state][$symbol]) ? $this->table[$state][$symbol] : false; 250 | } 251 | 252 | // handle parse error 253 | if (!$action || !$action[0]) { 254 | assert(isset($symbol)); 255 | if (!$recovering) { 256 | // Report error 257 | $expected = array(); 258 | foreach ($this->table[$state] as $p => $ignore) { 259 | if (isset($this->terminals_[$p]) && $p > 2) { 260 | $expected[] = "'" . $this->terminals_[$p] . "'"; 261 | } 262 | } 263 | 264 | $message = null; 265 | if (\in_array("'STRING'", $expected) && \in_array(substr($this->lexer->match, 0, 1), array('"', "'"))) { 266 | $message = "Invalid string"; 267 | if ("'" === substr($this->lexer->match, 0, 1)) { 268 | $message .= ", it appears you used single quotes instead of double quotes"; 269 | } elseif (preg_match('{".+?(\\\\[^"bfnrt/\\\\u](...)?)}', $this->lexer->getFullUpcomingInput(), $match)) { 270 | $message .= ", it appears you have an unescaped backslash at: ".$match[1]; 271 | } elseif (preg_match('{"(?:[^"]+|\\\\")*$}m', $this->lexer->getFullUpcomingInput())) { 272 | $message .= ", it appears you forgot to terminate a string, or attempted to write a multiline string which is invalid"; 273 | } 274 | } 275 | 276 | $errStr = 'Parse error on line ' . ($yylineno+1) . ":\n"; 277 | $errStr .= $this->lexer->showPosition() . "\n"; 278 | if ($message) { 279 | $errStr .= $message; 280 | } else { 281 | $errStr .= (\count($expected) > 1) ? "Expected one of: " : "Expected: "; 282 | $errStr .= implode(', ', $expected); 283 | } 284 | 285 | if (',' === substr(trim($this->lexer->getPastInput()), -1)) { 286 | $errStr .= " - It appears you have an extra trailing comma"; 287 | } 288 | 289 | $this->parseError($errStr, array( 290 | 'text' => $this->lexer->match, 291 | 'token' => isset($this->terminals_[$symbol]) ? $this->terminals_[$symbol] : $symbol, 292 | 'line' => $this->lexer->yylineno, 293 | 'loc' => $yyloc, 294 | 'expected' => $expected, 295 | )); 296 | } 297 | 298 | // just recovered from another error 299 | if ($recovering == 3) { 300 | if ($symbol === Lexer::EOF) { 301 | throw new ParsingException($errStr ?: 'Parsing halted.'); 302 | } 303 | 304 | // discard current lookahead and grab another 305 | $yyleng = $this->lexer->yyleng; 306 | $yytext = $this->lexer->yytext; 307 | $yylineno = $this->lexer->yylineno; 308 | $yyloc = $this->lexer->yylloc; 309 | $symbol = $this->lexer->lex(); 310 | } 311 | 312 | // try to recover from error 313 | while (true) { 314 | // check for error recovery rule in this state 315 | if (\array_key_exists(Lexer::T_ERROR, $this->table[$state])) { 316 | break; 317 | } 318 | if ($state == 0) { 319 | throw new ParsingException($errStr ?: 'Parsing halted.'); 320 | } 321 | $this->popStack(1); 322 | $state = $this->stack[\count($this->stack)-1]; 323 | } 324 | 325 | $preErrorSymbol = $symbol; // save the lookahead token 326 | $symbol = Lexer::T_ERROR; // insert generic error symbol as new lookahead 327 | $state = $this->stack[\count($this->stack)-1]; 328 | /** @var array|false */ 329 | $action = isset($this->table[$state][Lexer::T_ERROR]) ? $this->table[$state][Lexer::T_ERROR] : false; 330 | if ($action === false) { 331 | throw new \LogicException('No table value found for '.$state.' => '.Lexer::T_ERROR); 332 | } 333 | $recovering = 3; // allow 3 real symbols to be shifted before reporting a new error 334 | } 335 | 336 | // this shouldn't happen, unless resolve defaults are off 337 | if (\is_array($action[0]) && \count($action) > 1) { // @phpstan-ignore-line 338 | throw new ParsingException('Parse Error: multiple actions possible at state: ' . $state . ', token: ' . $symbol); 339 | } 340 | 341 | switch ($action[0]) { 342 | case 1: // shift 343 | assert(isset($symbol)); 344 | $this->stack[] = $symbol; 345 | $this->vstack[] = $this->lexer->yytext; 346 | $this->lstack[] = $this->lexer->yylloc; 347 | $this->stack[] = $action[1]; // push state 348 | $symbol = null; 349 | if (!$preErrorSymbol) { // normal execution/no error 350 | $yyleng = $this->lexer->yyleng; 351 | $yytext = $this->lexer->yytext; 352 | $yylineno = $this->lexer->yylineno; 353 | $yyloc = $this->lexer->yylloc; 354 | if ($recovering > 0) { 355 | $recovering--; 356 | } 357 | } else { // error just occurred, resume old lookahead from before error 358 | $symbol = $preErrorSymbol; 359 | $preErrorSymbol = null; 360 | } 361 | break; 362 | 363 | case 2: // reduce 364 | $len = $this->productions_[$action[1]][1]; 365 | 366 | // perform semantic action 367 | $currentToken = $this->vstack[\count($this->vstack) - $len]; // default to $$ = $1 368 | // default location, uses first token for firsts, last for lasts 369 | $position = array( // _$ = store 370 | 'first_line' => $this->lstack[\count($this->lstack) - ($len ?: 1)]['first_line'], 371 | 'last_line' => $this->lstack[\count($this->lstack) - 1]['last_line'], 372 | 'first_column' => $this->lstack[\count($this->lstack) - ($len ?: 1)]['first_column'], 373 | 'last_column' => $this->lstack[\count($this->lstack) - 1]['last_column'], 374 | ); 375 | list($newToken, $actionResult) = $this->performAction($currentToken, $yytext, $yyleng, $yylineno, $action[1]); 376 | 377 | if (!$actionResult instanceof Undefined) { 378 | return $actionResult; 379 | } 380 | 381 | if ($len) { 382 | $this->popStack($len); 383 | } 384 | 385 | $this->stack[] = $this->productions_[$action[1]][0]; // push nonterminal (reduce) 386 | $this->vstack[] = $newToken; 387 | $this->lstack[] = $position; 388 | /** @var int */ 389 | $newState = $this->table[$this->stack[\count($this->stack)-2]][$this->stack[\count($this->stack)-1]]; 390 | $this->stack[] = $newState; 391 | break; 392 | 393 | case 3: // accept 394 | 395 | return true; 396 | } 397 | } 398 | } 399 | 400 | /** 401 | * @param string $str 402 | * @param array{text: string, token: string|int, line: int, loc: array{first_line: int, first_column: int, last_line: int, last_column: int}, expected: string[]}|null $hash 403 | * @return never 404 | */ 405 | protected function parseError($str, $hash = null) 406 | { 407 | throw new ParsingException($str, $hash ?: array()); 408 | } 409 | 410 | /** 411 | * @param stdClass|array|int|bool|float|string|null $currentToken 412 | * @param string $yytext 413 | * @param int $yyleng 414 | * @param int $yylineno 415 | * @param int $yystate 416 | * @return array{stdClass|array|int|bool|float|string|null, stdClass|array|int|bool|float|string|null|Undefined} 417 | */ 418 | private function performAction($currentToken, $yytext, $yyleng, $yylineno, $yystate) 419 | { 420 | $token = $currentToken; 421 | 422 | $len = \count($this->vstack) - 1; 423 | switch ($yystate) { 424 | case 1: 425 | $yytext = preg_replace_callback('{(?:\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4})}', array($this, 'stringInterpolation'), $yytext); 426 | $token = $yytext; 427 | break; 428 | case 2: 429 | if (strpos($yytext, 'e') !== false || strpos($yytext, 'E') !== false) { 430 | $token = \floatval($yytext); 431 | } else { 432 | $token = strpos($yytext, '.') === false ? \intval($yytext) : \floatval($yytext); 433 | } 434 | break; 435 | case 3: 436 | $token = null; 437 | break; 438 | case 4: 439 | $token = true; 440 | break; 441 | case 5: 442 | $token = false; 443 | break; 444 | case 6: 445 | $token = $this->vstack[$len-1]; 446 | 447 | return array($token, $token); 448 | case 13: 449 | if ($this->flags & self::PARSE_TO_ASSOC) { 450 | $token = array(); 451 | } else { 452 | $token = new stdClass; 453 | } 454 | break; 455 | case 14: 456 | $token = $this->vstack[$len-1]; 457 | break; 458 | case 15: 459 | $token = array($this->vstack[$len-2], $this->vstack[$len]); 460 | break; 461 | case 16: 462 | assert(\is_array($this->vstack[$len])); 463 | if (PHP_VERSION_ID < 70100) { 464 | $property = $this->vstack[$len][0] === '' ? '_empty_' : $this->vstack[$len][0]; 465 | } else { 466 | $property = $this->vstack[$len][0]; 467 | } 468 | if ($this->flags & self::PARSE_TO_ASSOC) { 469 | $token = array(); 470 | $token[$property] = $this->vstack[$len][1]; 471 | } else { 472 | $token = new stdClass; 473 | $token->$property = $this->vstack[$len][1]; 474 | } 475 | break; 476 | case 17: 477 | assert(\is_array($this->vstack[$len])); 478 | if ($this->flags & self::PARSE_TO_ASSOC) { 479 | assert(\is_array($this->vstack[$len-2])); 480 | $token =& $this->vstack[$len-2]; 481 | $key = $this->vstack[$len][0]; 482 | if (($this->flags & self::DETECT_KEY_CONFLICTS) && isset($this->vstack[$len-2][$key])) { 483 | $errStr = 'Parse error on line ' . ($yylineno+1) . ":\n"; 484 | $errStr .= $this->lexer->showPosition() . "\n"; 485 | $errStr .= "Duplicate key: ".$this->vstack[$len][0]; 486 | throw new DuplicateKeyException($errStr, $this->vstack[$len][0], array('line' => $yylineno+1)); 487 | } elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($this->vstack[$len-2][$key])) { 488 | $duplicateCount = 1; 489 | do { 490 | $duplicateKey = $key . '.' . $duplicateCount++; 491 | } while (isset($this->vstack[$len-2][$duplicateKey])); 492 | $key = $duplicateKey; 493 | } 494 | $this->vstack[$len-2][$key] = $this->vstack[$len][1]; 495 | } else { 496 | assert($this->vstack[$len-2] instanceof stdClass); 497 | $token = $this->vstack[$len-2]; 498 | if (PHP_VERSION_ID < 70100) { 499 | $key = $this->vstack[$len][0] === '' ? '_empty_' : $this->vstack[$len][0]; 500 | } else { 501 | $key = $this->vstack[$len][0]; 502 | } 503 | if (($this->flags & self::DETECT_KEY_CONFLICTS) && isset($this->vstack[$len-2]->{$key})) { 504 | $errStr = 'Parse error on line ' . ($yylineno+1) . ":\n"; 505 | $errStr .= $this->lexer->showPosition() . "\n"; 506 | $errStr .= "Duplicate key: ".$this->vstack[$len][0]; 507 | throw new DuplicateKeyException($errStr, $this->vstack[$len][0], array('line' => $yylineno+1)); 508 | } elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($this->vstack[$len-2]->{$key})) { 509 | $duplicateCount = 1; 510 | do { 511 | $duplicateKey = $key . '.' . $duplicateCount++; 512 | } while (isset($this->vstack[$len-2]->$duplicateKey)); 513 | $key = $duplicateKey; 514 | } 515 | $this->vstack[$len-2]->$key = $this->vstack[$len][1]; 516 | } 517 | break; 518 | case 18: 519 | $token = array(); 520 | break; 521 | case 19: 522 | $token = $this->vstack[$len-1]; 523 | break; 524 | case 20: 525 | $token = array($this->vstack[$len]); 526 | break; 527 | case 21: 528 | assert(\is_array($this->vstack[$len-2])); 529 | $this->vstack[$len-2][] = $this->vstack[$len]; 530 | $token = $this->vstack[$len-2]; 531 | break; 532 | } 533 | 534 | return array($token, new Undefined()); 535 | } 536 | 537 | /** 538 | * @param string $match 539 | * @return string 540 | */ 541 | private function stringInterpolation($match) 542 | { 543 | switch ($match[0]) { 544 | case '\\\\': 545 | return '\\'; 546 | case '\"': 547 | return '"'; 548 | case '\b': 549 | return \chr(8); 550 | case '\f': 551 | return \chr(12); 552 | case '\n': 553 | return "\n"; 554 | case '\r': 555 | return "\r"; 556 | case '\t': 557 | return "\t"; 558 | case '\/': 559 | return "/"; 560 | default: 561 | return html_entity_decode('&#x'.ltrim(substr($match[0], 2), '0').';', ENT_QUOTES, 'UTF-8'); 562 | } 563 | } 564 | 565 | /** 566 | * @param int $n 567 | * @return void 568 | */ 569 | private function popStack($n) 570 | { 571 | $this->stack = \array_slice($this->stack, 0, - (2 * $n)); 572 | $this->vstack = \array_slice($this->vstack, 0, - $n); 573 | $this->lstack = \array_slice($this->lstack, 0, - $n); 574 | } 575 | 576 | /** 577 | * @param string $input 578 | * @return void 579 | */ 580 | private function failOnBOM($input) 581 | { 582 | // UTF-8 ByteOrderMark sequence 583 | $bom = "\xEF\xBB\xBF"; 584 | 585 | if (substr($input, 0, 3) === $bom) { 586 | $this->parseError("BOM detected, make sure your input does not include a Unicode Byte-Order-Mark"); 587 | } 588 | } 589 | } 590 | -------------------------------------------------------------------------------- /vendor/seld/jsonlint/src/Seld/JsonLint/Lexer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Seld\JsonLint; 13 | 14 | /** 15 | * Lexer class 16 | * 17 | * Ported from https://github.com/zaach/jsonlint 18 | */ 19 | class Lexer 20 | { 21 | /** @internal */ 22 | const EOF = 1; 23 | /** @internal */ 24 | const T_INVALID = -1; 25 | const T_SKIP_WHITESPACE = 0; 26 | const T_ERROR = 2; 27 | /** @internal */ 28 | const T_BREAK_LINE = 3; 29 | /** @internal */ 30 | const T_COMMENT = 30; 31 | /** @internal */ 32 | const T_OPEN_COMMENT = 31; 33 | /** @internal */ 34 | const T_CLOSE_COMMENT = 32; 35 | 36 | /** 37 | * @phpstan-var array, string> 38 | * @const 39 | */ 40 | private $rules = array( 41 | 0 => '/\G\s*\n\r?/', 42 | 1 => '/\G\s+/', 43 | 2 => '/\G-?([0-9]|[1-9][0-9]+)(\.[0-9]+)?([eE][+-]?[0-9]+)?\b/', 44 | 3 => '{\G"(?>\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4}|[^\0-\x1f\\\\"]++)*+"}', 45 | 4 => '/\G\{/', 46 | 5 => '/\G\}/', 47 | 6 => '/\G\[/', 48 | 7 => '/\G\]/', 49 | 8 => '/\G,/', 50 | 9 => '/\G:/', 51 | 10 => '/\Gtrue\b/', 52 | 11 => '/\Gfalse\b/', 53 | 12 => '/\Gnull\b/', 54 | 13 => '/\G$/', 55 | 14 => '/\G\/\//', 56 | 15 => '/\G\/\*/', 57 | 16 => '/\G\*\//', 58 | 17 => '/\G./', 59 | ); 60 | 61 | /** @var string */ 62 | private $input; 63 | /** @var bool */ 64 | private $more; 65 | /** @var bool */ 66 | private $done; 67 | /** @var 0|positive-int */ 68 | private $offset; 69 | /** @var int */ 70 | private $flags; 71 | 72 | /** @var string */ 73 | public $match; 74 | /** @var 0|positive-int */ 75 | public $yylineno; 76 | /** @var 0|positive-int */ 77 | public $yyleng; 78 | /** @var string */ 79 | public $yytext; 80 | /** @var array{first_line: 0|positive-int, first_column: 0|positive-int, last_line: 0|positive-int, last_column: 0|positive-int} */ 81 | public $yylloc; 82 | 83 | /** 84 | * @param int $flags 85 | */ 86 | public function __construct($flags = 0) 87 | { 88 | $this->flags = $flags; 89 | } 90 | 91 | /** 92 | * @return 0|1|4|6|8|10|11|14|17|18|21|22|23|24|30|-1 93 | */ 94 | public function lex() 95 | { 96 | while (true) { 97 | $symbol = $this->next(); 98 | switch ($symbol) { 99 | case self::T_SKIP_WHITESPACE: 100 | case self::T_BREAK_LINE: 101 | break; 102 | case self::T_COMMENT: 103 | case self::T_OPEN_COMMENT: 104 | if (!($this->flags & JsonParser::ALLOW_COMMENTS)) { 105 | $this->parseError('Lexical error on line ' . ($this->yylineno+1) . ". Comments are not allowed.\n" . $this->showPosition()); 106 | } 107 | $this->skipUntil($symbol === self::T_COMMENT ? self::T_BREAK_LINE : self::T_CLOSE_COMMENT); 108 | if ($this->done) { 109 | // last symbol '/\G$/' before EOF 110 | return 14; 111 | } 112 | break; 113 | case self::T_CLOSE_COMMENT: 114 | $this->parseError('Lexical error on line ' . ($this->yylineno+1) . ". Unexpected token.\n" . $this->showPosition()); 115 | default: 116 | return $symbol; 117 | } 118 | } 119 | } 120 | 121 | /** 122 | * @param string $input 123 | * @return $this 124 | */ 125 | public function setInput($input) 126 | { 127 | $this->input = $input; 128 | $this->more = false; 129 | $this->done = false; 130 | $this->offset = 0; 131 | $this->yylineno = $this->yyleng = 0; 132 | $this->yytext = $this->match = ''; 133 | $this->yylloc = array('first_line' => 1, 'first_column' => 0, 'last_line' => 1, 'last_column' => 0); 134 | 135 | return $this; 136 | } 137 | 138 | /** 139 | * @return string 140 | */ 141 | public function showPosition() 142 | { 143 | if ($this->yylineno === 0 && $this->offset === 1 && $this->match !== '{') { 144 | return $this->match.'...' . "\n^"; 145 | } 146 | 147 | $pre = str_replace("\n", '', $this->getPastInput()); 148 | $c = str_repeat('-', max(0, \strlen($pre) - 1)); // new Array(pre.length + 1).join("-"); 149 | 150 | return $pre . str_replace("\n", '', $this->getUpcomingInput()) . "\n" . $c . "^"; 151 | } 152 | 153 | /** 154 | * @return string 155 | */ 156 | public function getPastInput() 157 | { 158 | $pastLength = $this->offset - \strlen($this->match); 159 | 160 | return ($pastLength > 20 ? '...' : '') . substr($this->input, max(0, $pastLength - 20), min(20, $pastLength)); 161 | } 162 | 163 | /** 164 | * @return string 165 | */ 166 | public function getUpcomingInput() 167 | { 168 | $next = $this->match; 169 | if (\strlen($next) < 20) { 170 | $next .= substr($this->input, $this->offset, 20 - \strlen($next)); 171 | } 172 | 173 | return substr($next, 0, 20) . (\strlen($next) > 20 ? '...' : ''); 174 | } 175 | 176 | /** 177 | * @return string 178 | */ 179 | public function getFullUpcomingInput() 180 | { 181 | $next = $this->match; 182 | if (substr($next, 0, 1) === '"' && substr_count($next, '"') === 1) { 183 | $len = \strlen($this->input); 184 | if ($len === $this->offset) { 185 | $strEnd = $len; 186 | } else { 187 | $strEnd = min(strpos($this->input, '"', $this->offset + 1) ?: $len, strpos($this->input, "\n", $this->offset + 1) ?: $len); 188 | } 189 | $next .= substr($this->input, $this->offset, $strEnd - $this->offset); 190 | } elseif (\strlen($next) < 20) { 191 | $next .= substr($this->input, $this->offset, 20 - \strlen($next)); 192 | } 193 | 194 | return $next; 195 | } 196 | 197 | /** 198 | * @param string $str 199 | * @return never 200 | */ 201 | protected function parseError($str) 202 | { 203 | throw new ParsingException($str); 204 | } 205 | 206 | /** 207 | * @param int $token 208 | * @return void 209 | */ 210 | private function skipUntil($token) 211 | { 212 | $symbol = $this->next(); 213 | while ($symbol !== $token && false === $this->done) { 214 | $symbol = $this->next(); 215 | } 216 | } 217 | 218 | /** 219 | * @return 0|1|3|4|6|8|10|11|14|17|18|21|22|23|24|30|31|32|-1 220 | */ 221 | private function next() 222 | { 223 | if ($this->done) { 224 | return self::EOF; 225 | } 226 | if ($this->offset === \strlen($this->input)) { 227 | $this->done = true; 228 | } 229 | 230 | $token = null; 231 | $match = null; 232 | $col = null; 233 | $lines = null; 234 | 235 | if (!$this->more) { 236 | $this->yytext = ''; 237 | $this->match = ''; 238 | } 239 | 240 | $rulesLen = count($this->rules); 241 | 242 | for ($i=0; $i < $rulesLen; $i++) { 243 | if (preg_match($this->rules[$i], $this->input, $match, 0, $this->offset)) { 244 | $lines = explode("\n", $match[0]); 245 | array_shift($lines); 246 | $lineCount = \count($lines); 247 | $this->yylineno += $lineCount; 248 | $this->yylloc = array( 249 | 'first_line' => $this->yylloc['last_line'], 250 | 'last_line' => $this->yylineno+1, 251 | 'first_column' => $this->yylloc['last_column'], 252 | 'last_column' => $lineCount > 0 ? \strlen($lines[$lineCount - 1]) : $this->yylloc['last_column'] + \strlen($match[0]), 253 | ); 254 | $this->yytext .= $match[0]; 255 | $this->match .= $match[0]; 256 | $this->yyleng = \strlen($this->yytext); 257 | $this->more = false; 258 | $this->offset += \strlen($match[0]); 259 | return $this->performAction($i); 260 | } 261 | } 262 | 263 | if ($this->offset === \strlen($this->input)) { 264 | return self::EOF; 265 | } 266 | 267 | $this->parseError( 268 | 'Lexical error on line ' . ($this->yylineno+1) . ". Unrecognized text.\n" . $this->showPosition() 269 | ); 270 | } 271 | 272 | /** 273 | * @param int $rule 274 | * @return 0|3|4|6|8|10|11|14|17|18|21|22|23|24|30|31|32|-1 275 | */ 276 | private function performAction($rule) 277 | { 278 | switch ($rule) { 279 | case 0:/* skip break line */ 280 | return self::T_BREAK_LINE; 281 | case 1:/* skip whitespace */ 282 | return self::T_SKIP_WHITESPACE; 283 | case 2: 284 | return 6; 285 | case 3: 286 | $this->yytext = substr($this->yytext, 1, $this->yyleng-2); 287 | return 4; 288 | case 4: 289 | return 17; 290 | case 5: 291 | return 18; 292 | case 6: 293 | return 23; 294 | case 7: 295 | return 24; 296 | case 8: 297 | return 22; 298 | case 9: 299 | return 21; 300 | case 10: 301 | return 10; 302 | case 11: 303 | return 11; 304 | case 12: 305 | return 8; 306 | case 13: 307 | return 14; 308 | case 14: 309 | return self::T_COMMENT; 310 | case 15: 311 | return self::T_OPEN_COMMENT; 312 | case 16: 313 | return self::T_CLOSE_COMMENT; 314 | case 17: 315 | return self::T_INVALID; 316 | default: 317 | throw new \LogicException('Unsupported rule '.$rule); 318 | } 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /vendor/seld/jsonlint/src/Seld/JsonLint/ParsingException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Seld\JsonLint; 13 | 14 | class ParsingException extends \Exception 15 | { 16 | /** 17 | * @var array{text?: string, token?: string|int, line?: int, loc?: array{first_line: int, first_column: int, last_line: int, last_column: int}, expected?: string[]} 18 | */ 19 | protected $details; 20 | 21 | /** 22 | * @param string $message 23 | * @phpstan-param array{text?: string, token?: string|int, line?: int, loc?: array{first_line: int, first_column: int, last_line: int, last_column: int}, expected?: string[]} $details 24 | */ 25 | public function __construct($message, $details = array()) 26 | { 27 | $this->details = $details; 28 | parent::__construct($message); 29 | } 30 | 31 | /** 32 | * @phpstan-return array{text?: string, token?: string|int, line?: int, loc?: array{first_line: int, first_column: int, last_line: int, last_column: int}, expected?: string[]} 33 | */ 34 | public function getDetails() 35 | { 36 | return $this->details; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /vendor/seld/jsonlint/src/Seld/JsonLint/Undefined.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Seld\JsonLint; 13 | 14 | class Undefined 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /vip-governance.php: -------------------------------------------------------------------------------- 1 | 28 |
29 |

30 |
31 |