├── .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 |
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 |
69 | All Roles
70 |
71 |
72 |
73 |
74 |
75 |
76 | All Post Types
77 |
78 |
79 |
80 |
81 |
82 |
View Rules
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( '%s
', 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 | [](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(''.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 |
31 |