Plugin Directory

Changeset 3802095


Ignore:
Timestamp:
11/14/2026 02:34:30 PM (less than one hour ago)
Author:
joegomega
Message:

Update to version 2.45.0 from GitHub

Location:
mega-ai
Files:
6 edited
1 copied

Legend:

Unmodified
Added
Removed
  • mega-ai/tags/2.45.0/custom-fields.php

    r3802069 r3802095  
    3232    const SEO_PREFIXES = array('_yoast_wpseo_', 'rank_math_', '_aioseo_', '_seopress_', '_genesis_', '_su_', '_wds_',
    3333        '_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;
    3437    const SECRET_PATTERN = '/(pass(word|wd)|secret|token|api[_-]?key|nonce|licen[cs]e[_-]?key|private[_-]?key|credential)/i';
    3538
    … …  
    103106    }
    104107
     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
    105228    private static function acf_entry($source, $name, $object) {
    106229        $key = isset($object['key']) && is_string($object['key']) ? $object['key'] : null;
  • mega-ai/tags/2.45.0/mega.php

    r3802069 r3802095  
    33Plugin Name: MEGA AI
    44Description: Connect your WordPress website to MEGA for automated SEO optimization and content management.
    5 Version: 2.44.0
     5Version: 2.45.0
    66Author: MEGA
    77Author URI: https://gomega.ai
    … …  
    115115
    116116    private static $namespace = 'mega';
    117     private static $version = '2.44.0';
     117    private static $version = '2.45.0';
    118118
    119119    // Editor (widget) endpoints.
    … …  
    840840        // options, read only, optional `contains` filter. See custom-fields.php.
    841841        \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            ),
    845852        ));
    846853
    … …  
    42434250        $contains = $request->get_param('contains');
    42444251        $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());
    42454267        return \is_wp_error($result) ? $result : new \WP_REST_Response($result);
    42464268    }
  • mega-ai/tags/2.45.0/readme.txt

    r3802069 r3802095  
    44Requires at least: 4.4
    55Tested up to: 6.8
    6 Stable tag: 2.44.0
     6Stable tag: 2.45.0
    77Requires PHP: 7.2
    88License: GPLv2 or later
    … …  
    149149
    150150== 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.
    151153= 2.44.0 =
    152154* 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  
    3232    const SEO_PREFIXES = array('_yoast_wpseo_', 'rank_math_', '_aioseo_', '_seopress_', '_genesis_', '_su_', '_wds_',
    3333        '_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;
    3437    const SECRET_PATTERN = '/(pass(word|wd)|secret|token|api[_-]?key|nonce|licen[cs]e[_-]?key|private[_-]?key|credential)/i';
    3538
    … …  
    103106    }
    104107
     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
    105228    private static function acf_entry($source, $name, $object) {
    106229        $key = isset($object['key']) && is_string($object['key']) ? $object['key'] : null;
  • mega-ai/trunk/mega.php

    r3802069 r3802095  
    33Plugin Name: MEGA AI
    44Description: Connect your WordPress website to MEGA for automated SEO optimization and content management.
    5 Version: 2.44.0
     5Version: 2.45.0
    66Author: MEGA
    77Author URI: https://gomega.ai
    … …  
    115115
    116116    private static $namespace = 'mega';
    117     private static $version = '2.44.0';
     117    private static $version = '2.45.0';
    118118
    119119    // Editor (widget) endpoints.
    … …  
    840840        // options, read only, optional `contains` filter. See custom-fields.php.
    841841        \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            ),
    845852        ));
    846853
    … …  
    42434250        $contains = $request->get_param('contains');
    42444251        $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());
    42454267        return \is_wp_error($result) ? $result : new \WP_REST_Response($result);
    42464268    }
  • mega-ai/trunk/readme.txt

    r3802069 r3802095  
    44Requires at least: 4.4
    55Tested up to: 6.8
    6 Stable tag: 2.44.0
     6Stable tag: 2.45.0
    77Requires PHP: 7.2
    88License: GPLv2 or later
    … …  
    149149
    150150== 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.
    151153= 2.44.0 =
    152154* 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.