Changeset 3802095
- Timestamp:
- 11/14/2026 02:34:30 PM (less than one hour ago)
- Location:
- mega-ai
- Files:
-
- 6 edited
- 1 copied
-
tags/2.45.0 (copied) (copied from mega-ai/trunk )
-
tags/2.45.0/custom-fields.php (modified) (2 diffs)
-
tags/2.45.0/mega.php (modified) (4 diffs)
-
tags/2.45.0/readme.txt (modified) (2 diffs)
-
trunk/custom-fields.php (modified) (2 diffs)
-
trunk/mega.php (modified) (4 diffs)
-
trunk/readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
mega-ai/tags/2.45.0/custom-fields.php
r3802069 r3802095 32 32 const SEO_PREFIXES = array('_yoast_wpseo_', 'rank_math_', '_aioseo_', '_seopress_', '_genesis_', '_su_', '_wds_', 33 33 '_mega_', 'wpseo_', 'aioseo_'); 34 /** ACF field types the guarded write accepts: a single text value the sanitizer for its type can judge. */ 35 const WRITABLE_TYPES = array('text', 'textarea', 'wysiwyg'); 36 const LOCK_TTL_SECONDS = 30; 34 37 const SECRET_PATTERN = '/(pass(word|wd)|secret|token|api[_-]?key|nonce|licen[cs]e[_-]?key|private[_-]?key|credential)/i'; 35 38 … … 103 106 } 104 107 108 /** 109 * Guarded write of ONE ACF text, textarea or wysiwyg field of a published page or post (v2.45.0). 110 * 111 * $body: field_name (the ACF field name, as the read lists it), expected_value_sha256 (sha256 of the stored 112 * value the caller read), value (the new text). Refuses, writing nothing, unless: ACF is active; the post is 113 * a published page or post; the field is a writable type; the stored value is a string still hashing to 114 * expected_value_sha256 (under the page's editor lock, so no guarded page edit interleaves); the value is 115 * exactly what the field's own sanitizer would store (text: sanitize_text_field, textarea: 116 * sanitize_textarea_field, wysiwyg: wp_kses_post), at most MAX_VALUE_CHARS characters, never a credential-like 117 * field. After update_field the value is read back and must hash to the value sent, else the answer is a 118 * 500 WRITE_NOT_VERIFIED carrying details.landed. The one operation here that writes. 119 * 120 * @return array|\WP_Error 121 */ 122 public static function write($post_id, $body) { 123 $post_id = (int) $post_id; 124 $body = is_array($body) ? $body : array(); 125 $post = \get_post($post_id); 126 if (!$post) { 127 return MegaPlugin::editor_error_static('POST_NOT_FOUND', 'Post not found: ' . $post_id, 404); 128 } 129 if (!in_array($post->post_type, array('page', 'post'), true) || $post->post_status !== 'publish') { 130 return MegaPlugin::editor_error_static('UNSUPPORTED_POST', 'Only a published page or post is written.', 422); 131 } 132 $name = isset($body['field_name']) && is_string($body['field_name']) ? $body['field_name'] : ''; 133 $expected = isset($body['expected_value_sha256']) && is_string($body['expected_value_sha256']) ? $body['expected_value_sha256'] : ''; 134 $value = isset($body['value']) && is_string($body['value']) ? $body['value'] : null; 135 if ($name === '' || !preg_match('/^[A-Za-z0-9_\-]{1,100}$/', $name)) { 136 return MegaPlugin::editor_error_static('FIELD_REQUIRED', 'field_name must be an ACF field name from a fresh read.', 400); 137 } 138 if (!preg_match('/^[a-f0-9]{64}$/', $expected)) { 139 return MegaPlugin::editor_error_static('CONTENT_REVISION_REQUIRED', 'expected_value_sha256 must be the 64-hex sha256 of the stored value from a fresh read.', 400); 140 } 141 if ($value === null) { 142 return MegaPlugin::editor_error_static('VALUE_REQUIRED', 'value must be a string.', 400); 143 } 144 if (!\function_exists('get_field_object') || !\function_exists('update_field')) { 145 return MegaPlugin::editor_error_static('ACF_NOT_ACTIVE', 'Advanced Custom Fields is not active on this site; nothing was written.', 409); 146 } 147 if (preg_match(self::SECRET_PATTERN, $name)) { 148 return MegaPlugin::editor_error_static('FIELD_UNSUPPORTED', 'This field is never written.', 422); 149 } 150 $object = self::field_object($name, $post_id); 151 if ($object === null) { 152 return MegaPlugin::editor_error_static('FIELD_NOT_FOUND', 'No ACF field named ' . $name . ' on this post.', 404); 153 } 154 $type = (string) $object['type']; 155 if (!in_array($type, self::WRITABLE_TYPES, true) || preg_match(self::SECRET_PATTERN, (string) (isset($object['label']) ? $object['label'] : ''))) { 156 return MegaPlugin::editor_error_static('FIELD_UNSUPPORTED', 'ACF field type ' . $type . ' is not written; only text, textarea and wysiwyg are.', 422); 157 } 158 $canonical = self::sanitize_for($type, $value); 159 if ($canonical !== $value) { 160 return MegaPlugin::editor_error_static('VALUE_NOT_CANONICAL', 'The value would be changed by the field\'s own sanitizer (markup or control characters not allowed in a ' . $type . ' field); nothing was written.', 422); 161 } 162 // update_metadata() unslashes what it is given and ACF's own slashing varies by version, so a backslash cannot be stored faithfully. 163 if (strpos($value, '\\') !== false) { 164 return MegaPlugin::editor_error_static('VALUE_NOT_CANONICAL', 'A value with a backslash cannot be stored faithfully by ACF; nothing was written.', 422); 165 } 166 $chars = preg_match_all('/./us', $value); 167 if ($chars === false || $chars > self::MAX_VALUE_CHARS) { 168 return MegaPlugin::editor_error_static('VALUE_TOO_LARGE', 'The value is over ' . self::MAX_VALUE_CHARS . ' characters.', 413); 169 } 170 $lock = MegaPlugin::acquire_atomic_lock('mega_editor_lock_' . $post_id, self::LOCK_TTL_SECONDS); 171 if ($lock === null) { 172 return MegaPlugin::editor_error_static('LOCK_UNAVAILABLE', 'The page lock could not be taken (database error); nothing was written.', 503); 173 } 174 if ($lock === false) { 175 return MegaPlugin::editor_error_static('CONTENT_WRITE_BUSY', 'Another guarded edit of this page is in progress; read the page again and retry.', 409); 176 } 177 try { 178 // The pre-lock read only chose the field; the compare-and-set judges a read taken while the lock is held. 179 $object = self::field_object($name, $post_id); 180 if ($object === null) { 181 return MegaPlugin::editor_error_static('FIELD_NOT_FOUND', 'The field is gone; read the page again.', 404); 182 } 183 if ($object['type'] !== $type) { 184 return MegaPlugin::editor_error_static('FIELD_UNSUPPORTED', 'The field changed type since it was read.', 422); 185 } 186 $current = $object['value']; 187 if (!is_string($current) || hash('sha256', $current) !== $expected) { 188 return MegaPlugin::editor_error_static('CONTENT_REVISION_MISMATCH', 'The field changed since it was read; read it again.', 409); 189 } 190 $ok = \update_field((string) $object['key'], $value, $post_id); 191 $after = self::field_object($name, $post_id); 192 $after_value = $after !== null && is_string($after['value']) ? $after['value'] : null; 193 $landed = $after_value !== null && hash('sha256', $after_value) === hash('sha256', $value); 194 if (!$landed) { 195 return MegaPlugin::editor_error_static('WRITE_NOT_VERIFIED', 'The field was written but the read-back does not match what was sent.', 500, array( 196 'landed' => $after_value !== null && $after_value !== $current, 'update_field' => (bool) $ok)); 197 } 198 return array( 199 'post_id' => $post_id, 200 'field_name' => $name, 201 'field_key' => (string) $object['key'], 202 'type' => $type, 203 'before_sha256' => $expected, 204 'after_sha256' => hash('sha256', $after_value), 205 'before' => $current, 206 'after' => $after_value, 207 'changed' => $current !== $after_value, 208 'read_back_verified' => true, 209 ); 210 } finally { 211 MegaPlugin::release_atomic_lock('mega_editor_lock_' . $post_id, $lock); 212 } 213 } 214 215 /** The ACF field object (stored, unformatted value) for a field NAME on a post, or null. */ 216 private static function field_object($name, $post_id) { 217 $object = \get_field_object($name, $post_id, false); 218 if (!is_array($object) || !isset($object['key'], $object['type'], $object['name']) || $object['name'] !== $name) return null; 219 return $object; 220 } 221 222 private static function sanitize_for($type, $value) { 223 if ($type === 'wysiwyg') return \wp_kses_post($value); 224 if ($type === 'textarea') return \sanitize_textarea_field($value); 225 return \sanitize_text_field($value); 226 } 227 105 228 private static function acf_entry($source, $name, $object) { 106 229 $key = isset($object['key']) && is_string($object['key']) ? $object['key'] : null; -
mega-ai/tags/2.45.0/mega.php
r3802069 r3802095 3 3 Plugin Name: MEGA AI 4 4 Description: Connect your WordPress website to MEGA for automated SEO optimization and content management. 5 Version: 2.4 4.05 Version: 2.45.0 6 6 Author: MEGA 7 7 Author URI: https://gomega.ai … … 115 115 116 116 private static $namespace = 'mega'; 117 private static $version = '2.4 4.0';117 private static $version = '2.45.0'; 118 118 119 119 // Editor (widget) endpoints. … … 840 840 // options, read only, optional `contains` filter. See custom-fields.php. 841 841 \register_rest_route(self::$namespace . '/v1', '/editor/custom-fields/(?P<id>\d+)', array( 842 'methods' => \WP_REST_Server::READABLE, 843 'callback' => array($this, 'get_editor_custom_fields'), 844 'permission_callback' => array($this, 'check_authenticated_permission') 842 array( 843 'methods' => \WP_REST_Server::READABLE, 844 'callback' => array($this, 'get_editor_custom_fields'), 845 'permission_callback' => array($this, 'check_authenticated_permission') 846 ), 847 array( 848 'methods' => \WP_REST_Server::CREATABLE, 849 'callback' => array($this, 'post_editor_custom_fields'), 850 'permission_callback' => array($this, 'check_authenticated_permission') 851 ), 845 852 )); 846 853 … … 4243 4250 $contains = $request->get_param('contains'); 4244 4251 $result = MegaCustomFields::read((int) $request['id'], is_string($contains) ? $contains : ''); 4252 return \is_wp_error($result) ? $result : new \WP_REST_Response($result); 4253 } 4254 4255 /** 4256 * POST /mega/v1/editor/custom-fields/{id} 4257 * { "field_name": "...", "expected_value_sha256": hex64, "value": "...", "mega_change_log_bypass": true } 4258 * 4259 * Guarded write of one ACF text, textarea or wysiwyg field (2.45.0). See custom-fields.php. 4260 */ 4261 public function post_editor_custom_fields(\WP_REST_Request $request) { 4262 if (!MegaModules::loaded('custom-fields.php')) { 4263 return $this->editor_error('MODULE_UNAVAILABLE', 'The custom-fields module is not loaded on this site.', 503); 4264 } 4265 $body = $request->get_json_params(); 4266 $result = MegaCustomFields::write((int) $request['id'], is_array($body) ? $body : array()); 4245 4267 return \is_wp_error($result) ? $result : new \WP_REST_Response($result); 4246 4268 } -
mega-ai/tags/2.45.0/readme.txt
r3802069 r3802095 4 4 Requires at least: 4.4 5 5 Tested up to: 6.8 6 Stable tag: 2.4 4.06 Stable tag: 2.45.0 7 7 Requires PHP: 7.2 8 8 License: GPLv2 or later … … 149 149 150 150 == Changelog == 151 = 2.45.0 = 152 * Feature: a signed route changes the text of one ACF text, text-area or visual-editor field on a published page or post, only while the stored value is still the one the caller read, under the page's edit lock, and only to a value the field's own sanitizer would store unchanged. The value is read back after the write and the answer says whether it landed. Fields that look like credentials and every other field type are refused. 151 153 = 2.44.0 = 152 154 * Feature: a signed, read-only route lists where a page's text is stored outside its page builder: the page's ACF fields, its public custom fields, the active theme's customizer values and ACF options-page fields, each with where it lives and the key a later edit would name, and an optional search for a phrase. Values that look like credentials are never returned. Nothing is changed. -
mega-ai/trunk/custom-fields.php
r3802069 r3802095 32 32 const SEO_PREFIXES = array('_yoast_wpseo_', 'rank_math_', '_aioseo_', '_seopress_', '_genesis_', '_su_', '_wds_', 33 33 '_mega_', 'wpseo_', 'aioseo_'); 34 /** ACF field types the guarded write accepts: a single text value the sanitizer for its type can judge. */ 35 const WRITABLE_TYPES = array('text', 'textarea', 'wysiwyg'); 36 const LOCK_TTL_SECONDS = 30; 34 37 const SECRET_PATTERN = '/(pass(word|wd)|secret|token|api[_-]?key|nonce|licen[cs]e[_-]?key|private[_-]?key|credential)/i'; 35 38 … … 103 106 } 104 107 108 /** 109 * Guarded write of ONE ACF text, textarea or wysiwyg field of a published page or post (v2.45.0). 110 * 111 * $body: field_name (the ACF field name, as the read lists it), expected_value_sha256 (sha256 of the stored 112 * value the caller read), value (the new text). Refuses, writing nothing, unless: ACF is active; the post is 113 * a published page or post; the field is a writable type; the stored value is a string still hashing to 114 * expected_value_sha256 (under the page's editor lock, so no guarded page edit interleaves); the value is 115 * exactly what the field's own sanitizer would store (text: sanitize_text_field, textarea: 116 * sanitize_textarea_field, wysiwyg: wp_kses_post), at most MAX_VALUE_CHARS characters, never a credential-like 117 * field. After update_field the value is read back and must hash to the value sent, else the answer is a 118 * 500 WRITE_NOT_VERIFIED carrying details.landed. The one operation here that writes. 119 * 120 * @return array|\WP_Error 121 */ 122 public static function write($post_id, $body) { 123 $post_id = (int) $post_id; 124 $body = is_array($body) ? $body : array(); 125 $post = \get_post($post_id); 126 if (!$post) { 127 return MegaPlugin::editor_error_static('POST_NOT_FOUND', 'Post not found: ' . $post_id, 404); 128 } 129 if (!in_array($post->post_type, array('page', 'post'), true) || $post->post_status !== 'publish') { 130 return MegaPlugin::editor_error_static('UNSUPPORTED_POST', 'Only a published page or post is written.', 422); 131 } 132 $name = isset($body['field_name']) && is_string($body['field_name']) ? $body['field_name'] : ''; 133 $expected = isset($body['expected_value_sha256']) && is_string($body['expected_value_sha256']) ? $body['expected_value_sha256'] : ''; 134 $value = isset($body['value']) && is_string($body['value']) ? $body['value'] : null; 135 if ($name === '' || !preg_match('/^[A-Za-z0-9_\-]{1,100}$/', $name)) { 136 return MegaPlugin::editor_error_static('FIELD_REQUIRED', 'field_name must be an ACF field name from a fresh read.', 400); 137 } 138 if (!preg_match('/^[a-f0-9]{64}$/', $expected)) { 139 return MegaPlugin::editor_error_static('CONTENT_REVISION_REQUIRED', 'expected_value_sha256 must be the 64-hex sha256 of the stored value from a fresh read.', 400); 140 } 141 if ($value === null) { 142 return MegaPlugin::editor_error_static('VALUE_REQUIRED', 'value must be a string.', 400); 143 } 144 if (!\function_exists('get_field_object') || !\function_exists('update_field')) { 145 return MegaPlugin::editor_error_static('ACF_NOT_ACTIVE', 'Advanced Custom Fields is not active on this site; nothing was written.', 409); 146 } 147 if (preg_match(self::SECRET_PATTERN, $name)) { 148 return MegaPlugin::editor_error_static('FIELD_UNSUPPORTED', 'This field is never written.', 422); 149 } 150 $object = self::field_object($name, $post_id); 151 if ($object === null) { 152 return MegaPlugin::editor_error_static('FIELD_NOT_FOUND', 'No ACF field named ' . $name . ' on this post.', 404); 153 } 154 $type = (string) $object['type']; 155 if (!in_array($type, self::WRITABLE_TYPES, true) || preg_match(self::SECRET_PATTERN, (string) (isset($object['label']) ? $object['label'] : ''))) { 156 return MegaPlugin::editor_error_static('FIELD_UNSUPPORTED', 'ACF field type ' . $type . ' is not written; only text, textarea and wysiwyg are.', 422); 157 } 158 $canonical = self::sanitize_for($type, $value); 159 if ($canonical !== $value) { 160 return MegaPlugin::editor_error_static('VALUE_NOT_CANONICAL', 'The value would be changed by the field\'s own sanitizer (markup or control characters not allowed in a ' . $type . ' field); nothing was written.', 422); 161 } 162 // update_metadata() unslashes what it is given and ACF's own slashing varies by version, so a backslash cannot be stored faithfully. 163 if (strpos($value, '\\') !== false) { 164 return MegaPlugin::editor_error_static('VALUE_NOT_CANONICAL', 'A value with a backslash cannot be stored faithfully by ACF; nothing was written.', 422); 165 } 166 $chars = preg_match_all('/./us', $value); 167 if ($chars === false || $chars > self::MAX_VALUE_CHARS) { 168 return MegaPlugin::editor_error_static('VALUE_TOO_LARGE', 'The value is over ' . self::MAX_VALUE_CHARS . ' characters.', 413); 169 } 170 $lock = MegaPlugin::acquire_atomic_lock('mega_editor_lock_' . $post_id, self::LOCK_TTL_SECONDS); 171 if ($lock === null) { 172 return MegaPlugin::editor_error_static('LOCK_UNAVAILABLE', 'The page lock could not be taken (database error); nothing was written.', 503); 173 } 174 if ($lock === false) { 175 return MegaPlugin::editor_error_static('CONTENT_WRITE_BUSY', 'Another guarded edit of this page is in progress; read the page again and retry.', 409); 176 } 177 try { 178 // The pre-lock read only chose the field; the compare-and-set judges a read taken while the lock is held. 179 $object = self::field_object($name, $post_id); 180 if ($object === null) { 181 return MegaPlugin::editor_error_static('FIELD_NOT_FOUND', 'The field is gone; read the page again.', 404); 182 } 183 if ($object['type'] !== $type) { 184 return MegaPlugin::editor_error_static('FIELD_UNSUPPORTED', 'The field changed type since it was read.', 422); 185 } 186 $current = $object['value']; 187 if (!is_string($current) || hash('sha256', $current) !== $expected) { 188 return MegaPlugin::editor_error_static('CONTENT_REVISION_MISMATCH', 'The field changed since it was read; read it again.', 409); 189 } 190 $ok = \update_field((string) $object['key'], $value, $post_id); 191 $after = self::field_object($name, $post_id); 192 $after_value = $after !== null && is_string($after['value']) ? $after['value'] : null; 193 $landed = $after_value !== null && hash('sha256', $after_value) === hash('sha256', $value); 194 if (!$landed) { 195 return MegaPlugin::editor_error_static('WRITE_NOT_VERIFIED', 'The field was written but the read-back does not match what was sent.', 500, array( 196 'landed' => $after_value !== null && $after_value !== $current, 'update_field' => (bool) $ok)); 197 } 198 return array( 199 'post_id' => $post_id, 200 'field_name' => $name, 201 'field_key' => (string) $object['key'], 202 'type' => $type, 203 'before_sha256' => $expected, 204 'after_sha256' => hash('sha256', $after_value), 205 'before' => $current, 206 'after' => $after_value, 207 'changed' => $current !== $after_value, 208 'read_back_verified' => true, 209 ); 210 } finally { 211 MegaPlugin::release_atomic_lock('mega_editor_lock_' . $post_id, $lock); 212 } 213 } 214 215 /** The ACF field object (stored, unformatted value) for a field NAME on a post, or null. */ 216 private static function field_object($name, $post_id) { 217 $object = \get_field_object($name, $post_id, false); 218 if (!is_array($object) || !isset($object['key'], $object['type'], $object['name']) || $object['name'] !== $name) return null; 219 return $object; 220 } 221 222 private static function sanitize_for($type, $value) { 223 if ($type === 'wysiwyg') return \wp_kses_post($value); 224 if ($type === 'textarea') return \sanitize_textarea_field($value); 225 return \sanitize_text_field($value); 226 } 227 105 228 private static function acf_entry($source, $name, $object) { 106 229 $key = isset($object['key']) && is_string($object['key']) ? $object['key'] : null; -
mega-ai/trunk/mega.php
r3802069 r3802095 3 3 Plugin Name: MEGA AI 4 4 Description: Connect your WordPress website to MEGA for automated SEO optimization and content management. 5 Version: 2.4 4.05 Version: 2.45.0 6 6 Author: MEGA 7 7 Author URI: https://gomega.ai … … 115 115 116 116 private static $namespace = 'mega'; 117 private static $version = '2.4 4.0';117 private static $version = '2.45.0'; 118 118 119 119 // Editor (widget) endpoints. … … 840 840 // options, read only, optional `contains` filter. See custom-fields.php. 841 841 \register_rest_route(self::$namespace . '/v1', '/editor/custom-fields/(?P<id>\d+)', array( 842 'methods' => \WP_REST_Server::READABLE, 843 'callback' => array($this, 'get_editor_custom_fields'), 844 'permission_callback' => array($this, 'check_authenticated_permission') 842 array( 843 'methods' => \WP_REST_Server::READABLE, 844 'callback' => array($this, 'get_editor_custom_fields'), 845 'permission_callback' => array($this, 'check_authenticated_permission') 846 ), 847 array( 848 'methods' => \WP_REST_Server::CREATABLE, 849 'callback' => array($this, 'post_editor_custom_fields'), 850 'permission_callback' => array($this, 'check_authenticated_permission') 851 ), 845 852 )); 846 853 … … 4243 4250 $contains = $request->get_param('contains'); 4244 4251 $result = MegaCustomFields::read((int) $request['id'], is_string($contains) ? $contains : ''); 4252 return \is_wp_error($result) ? $result : new \WP_REST_Response($result); 4253 } 4254 4255 /** 4256 * POST /mega/v1/editor/custom-fields/{id} 4257 * { "field_name": "...", "expected_value_sha256": hex64, "value": "...", "mega_change_log_bypass": true } 4258 * 4259 * Guarded write of one ACF text, textarea or wysiwyg field (2.45.0). See custom-fields.php. 4260 */ 4261 public function post_editor_custom_fields(\WP_REST_Request $request) { 4262 if (!MegaModules::loaded('custom-fields.php')) { 4263 return $this->editor_error('MODULE_UNAVAILABLE', 'The custom-fields module is not loaded on this site.', 503); 4264 } 4265 $body = $request->get_json_params(); 4266 $result = MegaCustomFields::write((int) $request['id'], is_array($body) ? $body : array()); 4245 4267 return \is_wp_error($result) ? $result : new \WP_REST_Response($result); 4246 4268 } -
mega-ai/trunk/readme.txt
r3802069 r3802095 4 4 Requires at least: 4.4 5 5 Tested up to: 6.8 6 Stable tag: 2.4 4.06 Stable tag: 2.45.0 7 7 Requires PHP: 7.2 8 8 License: GPLv2 or later … … 149 149 150 150 == Changelog == 151 = 2.45.0 = 152 * Feature: a signed route changes the text of one ACF text, text-area or visual-editor field on a published page or post, only while the stored value is still the one the caller read, under the page's edit lock, and only to a value the field's own sanitizer would store unchanged. The value is read back after the write and the answer says whether it landed. Fields that look like credentials and every other field type are refused. 151 153 = 2.44.0 = 152 154 * Feature: a signed, read-only route lists where a page's text is stored outside its page builder: the page's ACF fields, its public custom fields, the active theme's customizer values and ACF options-page fields, each with where it lives and the key a later edit would name, and an optional search for a phrase. Values that look like credentials are never returned. Nothing is changed.
Note:
See TracChangeset
for help on using the changeset viewer.