Plugin Directory

Changeset 3803670


Ignore:
Timestamp:
11/15/2026 06:04:19 PM (less than one hour ago)
Author:
geminilabs
Message:

Release 8.3.8, see readme.txt for the changelog.

Location:
site-reviews
Files:
98 edited
1 copied

Legend:

Unmodified
Added
Removed
  • site-reviews/tags/8.3.8/changelog.txt

    r3777315 r3803670  
    11== Changelog ==
     2
     3= 8.3.8 (2026-11-15) =
     4
     5- Fixed a security issue in review excerpts
     6- Fixed a security issue in the notice shown after restoring a revision
     7- Fixed a security issue in the review type filter
     8- Fixed a security issue in the schema
     9- Fixed captcha checks slowly filling up the database
     10- Fixed forms with more than one Assigned Pages, Categories or Users field not submitting
     11- Fixed hidden form fields changing after a failed submission
     12- Fixed MultilingualPress translations losing their custom field values when the original review is edited
     13- Fixed number fields ignoring decimals in their minimum and maximum
     14- Fixed reviews not saving with version 3 of the SQLite Database Integration plugin
     15- Fixed some assigned pages, categories or users not being saved with a review
     16- Fixed the admin search permission check
     17- Fixed the Install button in the missing plugin notice
     18- Fixed the IP address detection tool permission check
     19- Fixed the review approval permission check
     20- Fixed the validation of submitted reviews
     21- Fixed the verification email permission check
    222
    323= 8.3.7 (2026-10-31) =
  • site-reviews/tags/8.3.8/languages/site-reviews-en_US.po

    r3777315 r3803670  
    116116
    117117#: plugin/Controllers/Api/Version1/RestRenderController.php:103
    118 #: plugin/Controllers/VerificationController.php:96
     118#: plugin/Controllers/VerificationController.php:105
    119119msgid "Thank you, your review has been verified."
    120120msgstr "Thank you, your review has been verified."
    121121
    122122#: plugin/Controllers/Api/Version1/RestRenderController.php:104
    123 #: plugin/Controllers/VerificationController.php:97
     123#: plugin/Controllers/VerificationController.php:106
    124124msgid "Thank you, your review has been verified and is awaiting approval."
    125125msgstr "Thank you, your review has been verified and is awaiting approval."
    … …  
    432432msgstr "Submitting, please wait..."
    433433
    434 #: plugin/Modules/Html/ReviewForm.php:14
     434#: plugin/Modules/Html/ReviewForm.php:15
    435435msgid "Submit Review"
    436436msgstr "Submit Review"
    … …  
    476476msgstr "You have already submitted a review."
    477477
    478 #: plugin/Modules/Validator/SignatureValidator.php:27
     478#: plugin/Modules/Validator/SignatureValidator.php:28
    479479#: views/pages/documentation/support/common-problems-and-solutions.php:89
    480480msgid "This review cannot be submitted, please refresh the page and try again."
    … …  
    24372437msgstr "Enabling this option will persist field values entered into the review form until either the review is submitted or the browser tab or window is closed."
    24382438
    2439 #: plugin/Database.php:237
     2439#: plugin/Database.php:231
    24402440msgctxt "admin-text"
    24412441msgid "(no title)"
    … …  
    34003400
    34013401#: plugin/Commands/SendVerificationEmail.php:71
    3402 #: plugin/Controllers/VerificationController.php:34
     3402#: plugin/Controllers/VerificationController.php:35
    34033403msgctxt "admin-text"
    34043404msgid "Resend Verification Request"
    … …  
    34863486msgstr "Import"
    34873487
    3488 #: plugin/Controllers/AdminController.php:309
    3489 #: plugin/Controllers/AdminController.php:327
    3490 #: plugin/Controllers/AdminController.php:340
     3488#: plugin/Controllers/AdminController.php:313
     3489#: plugin/Controllers/AdminController.php:332
     3490#: plugin/Controllers/AdminController.php:346
    34913491msgctxt "admin-text"
    34923492msgid "Nothing found."
    … …  
    35803580
    35813581#. translators: %s: the review type
    3582 #: plugin/Controllers/EditorController.php:108
     3582#: plugin/Controllers/EditorController.php:109
    35833583#, php-format
    35843584msgctxt "admin-text"
    … …  
    36813681msgstr "Respond"
    36823682
    3683 #: plugin/Controllers/ListTableController.php:272
     3683#: plugin/Controllers/ListTableController.php:270
    36843684msgctxt "admin-text"
    36853685msgid "Sorry, you are not allowed to respond to this review."
    36863686msgstr "Sorry, you are not allowed to respond to this review."
    36873687
    3688 #: plugin/Controllers/ListTableController.php:275
     3688#: plugin/Controllers/ListTableController.php:273
    36893689msgctxt "admin-text"
    36903690msgid "Someone"
    … …  
    36923692
    36933693#. translators: %s: user's name
    3694 #: plugin/Controllers/ListTableController.php:281
     3694#: plugin/Controllers/ListTableController.php:279
    36953695#, php-format
    36963696msgctxt "admin-text"
    … …  
    38213821msgstr "You do not have permission to import settings."
    38223822
    3823 #: plugin/Controllers/ToolsController.php:366
     3823#: plugin/Controllers/ToolsController.php:380
    38243824msgctxt "admin-text"
    38253825msgid "reload the page"
    … …  
    38273827
    38283828#. translators: %s: a link with the text "reload the page"
    3829 #: plugin/Controllers/ToolsController.php:371
     3829#: plugin/Controllers/ToolsController.php:385
    38303830#, php-format
    38313831msgctxt "admin-text"
    … …  
    38333833msgstr "The permissions have been repaired, please %s for them to take effect."
    38343834
    3835 #: plugin/Controllers/ToolsController.php:446
     3835#: plugin/Controllers/ToolsController.php:460
    38363836msgctxt "admin-text"
    38373837msgid "You do not have permission to rollback the plugin."
    … …  
    39213921msgstr "Approved on:"
    39223922
    3923 #: plugin/Controllers/VerificationController.php:35
     3923#: plugin/Controllers/VerificationController.php:36
    39243924msgctxt "admin-text"
    39253925msgid "Send Verification Request"
    … …  
    42244224
    42254225#. translators: %s: the plugin name
    4226 #: plugin/Notices/GatekeeperNotice.php:130
     4226#: plugin/Notices/GatekeeperNotice.php:133
    42274227#, php-format
    42284228msgctxt "admin-text"
    … …  
    42314231
    42324232#. translators: %s: the plugin name
    4233 #: plugin/Notices/GatekeeperNotice.php:146
     4233#: plugin/Notices/GatekeeperNotice.php:149
    42344234#, php-format
    42354235msgctxt "admin-text"
    … …  
    71167116msgstr "Limit result set to verified status."
    71177117
    7118 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:18
     7118#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:19
    71197119msgctxt "admin-text"
    71207120msgid "Cannot create existing review."
    71217121msgstr "Cannot create existing review."
    71227122
    7123 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:22
     7123#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:23
    71247124msgctxt "admin-text"
    71257125msgid "Sorry, you are not allowed to create reviews as this user."
    71267126msgstr "Sorry, you are not allowed to create reviews as this user."
    71277127
    7128 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:26
     7128#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:27
    71297129msgctxt "admin-text"
    71307130msgid "Sorry, you are not allowed to create reviews as this review author."
    71317131msgstr "Sorry, you are not allowed to create reviews as this review author."
    71327132
    7133 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:30
    7134 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:139
     7133#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:31
     7134#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:146
    71357135msgctxt "admin-text"
    71367136msgid "Sorry, you are not allowed to assign the provided terms."
    71377137msgstr "Sorry, you are not allowed to assign the provided terms."
    71387138
    7139 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:45
    7140 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:68
    7141 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:121
     7139#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:46
     7140#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:69
     7141#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:122
    71427142msgctxt "admin-text"
    71437143msgid "Invalid review ID."
    71447144msgstr "Invalid review ID."
    71457145
    7146 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:51
     7146#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:52
    71477147msgctxt "admin-text"
    71487148msgid "Sorry, you are not allowed to delete this review."
    71497149msgstr "Sorry, you are not allowed to delete this review."
    71507150
    7151 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:74
     7151#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:75
    71527152msgctxt "admin-text"
    71537153msgid "Sorry, you are not allowed to view this review."
    71547154msgstr "Sorry, you are not allowed to view this review."
    71557155
    7156 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:81
    7157 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:127
     7156#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:82
     7157#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:128
    71587158msgctxt "admin-text"
    71597159msgid "Sorry, you are not allowed to edit this review."
    71607160msgstr "Sorry, you are not allowed to edit this review."
    71617161
    7162 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:97
     7162#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:98
    71637163msgctxt "admin-text"
    71647164msgid "Sorry, you do not have permission to access reviews."
    71657165msgstr "Sorry, you do not have permission to access reviews."
    71667166
    7167 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:104
     7167#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:105
    71687168msgctxt "admin-text"
    71697169msgid "Sorry, you are not allowed to edit reviews."
    71707170msgstr "Sorry, you are not allowed to edit reviews."
    71717171
    7172 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:133
     7172#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:140
    71737173msgctxt "admin-text"
    71747174msgid "Sorry, you are not allowed to update the review as this user."
    … …  
    75607560
    75617561#. translators: %s: plugin name
    7562 #: plugin/Controllers/ToolsController.php:427
     7562#: plugin/Controllers/ToolsController.php:441
    75637563#, php-format
    75647564msgctxt "Site Reviews (admin-text)"
    … …  
    92359235msgstr ""
    92369236
    9237 #: plugin/Controllers/ToolsController.php:451
     9237#: plugin/Controllers/ToolsController.php:465
    92389238#: views/pages/tools/general/rollback-plugin.php:21
    92399239msgctxt "admin-text"
    … …  
    92549254msgid "%s was not updated because its folder is a git repository."
    92559255msgstr ""
     9256
     9257#: plugin/Controllers/AdminController.php:390
     9258msgctxt "admin-text"
     9259msgid "You do not have permission to search."
     9260msgstr ""
     9261
     9262#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:134
     9263msgctxt "admin-text"
     9264msgid "Sorry, you are not allowed to approve this review."
     9265msgstr ""
     9266
     9267#: plugin/Controllers/ToolsController.php:314
     9268#: plugin/Controllers/ToolsController.php:332
     9269msgctxt "admin-text"
     9270msgid "You do not have permission to change the IP address detection."
     9271msgstr ""
     9272
     9273#: plugin/Controllers/VerificationController.php:52
     9274msgctxt "admin-text"
     9275msgid "You do not have permission to request verification of this review."
     9276msgstr ""
  • site-reviews/tags/8.3.8/languages/site-reviews.pot

    r3777315 r3803670  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: Site Reviews 8.3.7\n"
     5"Project-Id-Version: Site Reviews 8.3.8\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/site-reviews\n"
    77"Last-Translator: Paul Ryley <paul@geminilabs.io>\n"
    … …  
    32683268
    32693269#: plugin/Commands/SendVerificationEmail.php:71
    3270 #: plugin/Controllers/VerificationController.php:34
     3270#: plugin/Controllers/VerificationController.php:35
    32713271msgctxt "admin-text"
    32723272msgid "Resend Verification Request"
    … …  
    33583358msgstr ""
    33593359
    3360 #: plugin/Controllers/AdminController.php:309
    3361 #: plugin/Controllers/AdminController.php:327
    3362 #: plugin/Controllers/AdminController.php:340
     3360#: plugin/Controllers/AdminController.php:313
     3361#: plugin/Controllers/AdminController.php:332
     3362#: plugin/Controllers/AdminController.php:346
    33633363msgctxt "admin-text"
    33643364msgid "Nothing found."
     3365msgstr ""
     3366
     3367#: plugin/Controllers/AdminController.php:390
     3368msgctxt "admin-text"
     3369msgid "You do not have permission to search."
    33653370msgstr ""
    33663371
    … …  
    34943499msgstr ""
    34953500
    3496 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:18
     3501#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:19
    34973502msgctxt "admin-text"
    34983503msgid "Cannot create existing review."
    34993504msgstr ""
    35003505
    3501 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:22
     3506#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:23
    35023507msgctxt "admin-text"
    35033508msgid "Sorry, you are not allowed to create reviews as this user."
    35043509msgstr ""
    35053510
    3506 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:26
     3511#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:27
    35073512msgctxt "admin-text"
    35083513msgid "Sorry, you are not allowed to create reviews as this review author."
    35093514msgstr ""
    35103515
    3511 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:30
    3512 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:139
     3516#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:31
     3517#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:146
    35133518msgctxt "admin-text"
    35143519msgid "Sorry, you are not allowed to assign the provided terms."
    35153520msgstr ""
    35163521
    3517 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:45
    3518 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:68
    3519 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:121
     3522#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:46
     3523#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:69
     3524#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:122
    35203525msgctxt "admin-text"
    35213526msgid "Invalid review ID."
    35223527msgstr ""
    35233528
    3524 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:51
     3529#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:52
    35253530msgctxt "admin-text"
    35263531msgid "Sorry, you are not allowed to delete this review."
    35273532msgstr ""
    35283533
    3529 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:74
     3534#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:75
    35303535msgctxt "admin-text"
    35313536msgid "Sorry, you are not allowed to view this review."
    35323537msgstr ""
    35333538
    3534 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:81
    3535 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:127
     3539#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:82
     3540#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:128
    35363541msgctxt "admin-text"
    35373542msgid "Sorry, you are not allowed to edit this review."
    35383543msgstr ""
    35393544
    3540 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:97
     3545#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:98
    35413546msgctxt "admin-text"
    35423547msgid "Sorry, you do not have permission to access reviews."
    35433548msgstr ""
    35443549
    3545 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:104
     3550#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:105
    35463551msgctxt "admin-text"
    35473552msgid "Sorry, you are not allowed to edit reviews."
    35483553msgstr ""
    35493554
    3550 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:133
     3555#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:134
     3556msgctxt "admin-text"
     3557msgid "Sorry, you are not allowed to approve this review."
     3558msgstr ""
     3559
     3560#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:140
    35513561msgctxt "admin-text"
    35523562msgid "Sorry, you are not allowed to update the review as this user."
    … …  
    35623572
    35633573#: plugin/Controllers/Api/Version1/RestRenderController.php:103
    3564 #: plugin/Controllers/VerificationController.php:96
     3574#: plugin/Controllers/VerificationController.php:105
    35653575msgid "Thank you, your review has been verified."
    35663576msgstr ""
    35673577
    35683578#: plugin/Controllers/Api/Version1/RestRenderController.php:104
    3569 #: plugin/Controllers/VerificationController.php:97
     3579#: plugin/Controllers/VerificationController.php:106
    35703580msgid "Thank you, your review has been verified and is awaiting approval."
    35713581msgstr ""
    … …  
    39233933
    39243934#. translators: %s: the review type
    3925 #: plugin/Controllers/EditorController.php:108
     3935#: plugin/Controllers/EditorController.php:109
    39263936#, php-format
    39273937msgctxt "admin-text"
    … …  
    41924202msgstr ""
    41934203
    4194 #: plugin/Controllers/ListTableController.php:272
     4204#: plugin/Controllers/ListTableController.php:270
    41954205msgctxt "admin-text"
    41964206msgid "Sorry, you are not allowed to respond to this review."
    41974207msgstr ""
    41984208
    4199 #: plugin/Controllers/ListTableController.php:275
     4209#: plugin/Controllers/ListTableController.php:273
    42004210msgctxt "admin-text"
    42014211msgid "Someone"
    … …  
    42034213
    42044214#. translators: %s: user's name
    4205 #: plugin/Controllers/ListTableController.php:281
     4215#: plugin/Controllers/ListTableController.php:279
    42064216#, php-format
    42074217msgctxt "admin-text"
    … …  
    43394349msgstr ""
    43404350
    4341 #: plugin/Controllers/ToolsController.php:366
     4351#: plugin/Controllers/ToolsController.php:314
     4352#: plugin/Controllers/ToolsController.php:332
     4353msgctxt "admin-text"
     4354msgid "You do not have permission to change the IP address detection."
     4355msgstr ""
     4356
     4357#: plugin/Controllers/ToolsController.php:380
    43424358msgctxt "admin-text"
    43434359msgid "reload the page"
    … …  
    43454361
    43464362#. translators: %s: a link with the text "reload the page"
    4347 #: plugin/Controllers/ToolsController.php:371
     4363#: plugin/Controllers/ToolsController.php:385
    43484364#, php-format
    43494365msgctxt "admin-text"
    … …  
    43524368
    43534369#. translators: %s: plugin name
    4354 #: plugin/Controllers/ToolsController.php:427
     4370#: plugin/Controllers/ToolsController.php:441
    43554371#, php-format
    43564372msgctxt "Site Reviews (admin-text)"
    … …  
    43584374msgstr ""
    43594375
    4360 #: plugin/Controllers/ToolsController.php:446
     4376#: plugin/Controllers/ToolsController.php:460
    43614377msgctxt "admin-text"
    43624378msgid "You do not have permission to rollback the plugin."
    43634379msgstr ""
    43644380
    4365 #: plugin/Controllers/ToolsController.php:451
     4381#: plugin/Controllers/ToolsController.php:465
    43664382#: views/pages/tools/general/rollback-plugin.php:21
    43674383msgctxt "admin-text"
    … …  
    44714487msgstr ""
    44724488
    4473 #: plugin/Controllers/VerificationController.php:35
     4489#: plugin/Controllers/VerificationController.php:36
    44744490msgctxt "admin-text"
    44754491msgid "Send Verification Request"
     4492msgstr ""
     4493
     4494#: plugin/Controllers/VerificationController.php:52
     4495msgctxt "admin-text"
     4496msgid "You do not have permission to request verification of this review."
    44764497msgstr ""
    44774498
    … …  
    45074528msgstr ""
    45084529
    4509 #: plugin/Database.php:237
     4530#: plugin/Database.php:231
    45104531msgctxt "admin-text"
    45114532msgid "(no title)"
    … …  
    64976518msgstr ""
    64986519
    6499 #: plugin/Modules/Html/ReviewForm.php:14
     6520#: plugin/Modules/Html/ReviewForm.php:15
    65006521msgid "Submit Review"
    65016522msgstr ""
    … …  
    66296650msgstr ""
    66306651
    6631 #: plugin/Modules/Validator/SignatureValidator.php:27
     6652#: plugin/Modules/Validator/SignatureValidator.php:28
    66326653#: views/pages/documentation/support/common-problems-and-solutions.php:89
    66336654msgid "This review cannot be submitted, please refresh the page and try again."
    … …  
    66606681
    66616682#. translators: %s: the plugin name
    6662 #: plugin/Notices/GatekeeperNotice.php:130
     6683#: plugin/Notices/GatekeeperNotice.php:133
    66636684#, php-format
    66646685msgctxt "admin-text"
    … …  
    66676688
    66686689#. translators: %s: the plugin name
    6669 #: plugin/Notices/GatekeeperNotice.php:146
     6690#: plugin/Notices/GatekeeperNotice.php:149
    66706691#, php-format
    66716692msgctxt "admin-text"
  • site-reviews/tags/8.3.8/plugin/Api.php

    r3769299 r3803670  
    8181            $this->forget($transientKey);
    8282        } else {
    83             $result = get_site_transient($transientKey);
     83            $result = $args['cache'] ? get_site_transient($transientKey) : false;
    8484            if (!empty($result)) {
    8585                return new Response($result);
    … …  
    9797            $response = new Response($result);
    9898            if ($response->successful()) {
    99                 $this->remember($transientKey, $result, $args['expiration']);
     99                if ($args['cache']) {
     100                    $this->remember($transientKey, $result, $args['expiration']);
     101                }
    100102                if ($args['skip_when_down']) {
    101103                    $this->markReachable();
  • site-reviews/tags/8.3.8/plugin/Commands/CreateReview.php

    r3777315 r3803670  
    6060        $this->request = $this->normalize($request); // IP address is set here
    6161        $this->setProperties(); // do this after setting the request
    62         $this->review = new Review($this->toArray(), $init = false);
     62        $this->review = new Review($this->toArray(), false); // don't init
    6363        $this->validation = new Arguments();
    6464        $this->custom = $this->custom();
    … …  
    239239            $request->set('date', '');
    240240            $request->set('date_gmt', '');
     241            $request->set('geolocation', '');
    241242            $request->set('is_pinned', false);
    242243            $request->set('is_verified', false);
    … …  
    244245            $request->set('response_by', 0);
    245246            $request->set('type', 'local');
     247            $request->set('verified', '');
     248            $request->set('verified_on', '');
     249            $request->set('verified_requested', '');
    246250        }
    247251        glsr()->action('review/request', $request);
  • site-reviews/tags/8.3.8/plugin/Commands/FetchPagedReviews.php

    r3683434 r3803670  
    3333        return [
    3434            'max_num_pages' => $this->html->max_num_pages,
    35             'pagination' => $this->html->getPagination($wrap = false),
     35            'pagination' => $this->html->getPagination(false), // don't wrap
    3636            'reviews' => $this->html->getReviews(),
    3737        ];
  • site-reviews/tags/8.3.8/plugin/Controllers/AdminController.php

    r3683434 r3803670  
    262262    public function searchAssignedPostsAjax(Request $request): void
    263263    {
     264        $this->checkSearchPermission();
    264265        $search = glsr(Sanitizer::class)->sanitizeText($request->search);
    265266        $results = glsr(Database::class)->searchAssignedPosts($search)->results();
    … …  
    274275    public function searchAssignedUsersAjax(Request $request): void
    275276    {
     277        $this->checkSearchPermission();
    276278        $search = glsr(Sanitizer::class)->sanitizeText($request->search);
    277279        $results = glsr(Database::class)->searchAssignedUsers($search)->results();
    … …  
    289291    public function searchAuthorsAjax(Request $request): void
    290292    {
     293        $this->checkSearchPermission();
    291294        $search = glsr(Sanitizer::class)->sanitizeText($request->search);
    292295        $results = glsr(Database::class)->searchUsers($search)->results();
    … …  
    304307    public function searchPostsAjax(Request $request): void
    305308    {
     309        $this->checkSearchPermission();
    306310        $search = glsr(Sanitizer::class)->sanitizeText($request->search);
    307311        $results = glsr(Database::class)->searchPosts($search)->render();
    … …  
    317321    public function searchStringsAjax(Request $request): void
    318322    {
     323        $this->checkSearchPermission();
    319324        $search = glsr(Sanitizer::class)->sanitizeText($request->search);
    320325        $exclude = Arr::consolidate($request->exclude);
    … …  
    335340    public function searchUsersAjax(Request $request): void
    336341    {
     342        $this->checkSearchPermission();
    337343        $search = glsr(Sanitizer::class)->sanitizeText($request->search);
    338344        $results = glsr(Database::class)->searchUsers($search)->render();
    … …  
    372378        $this->execute(new ToggleStatus($request))->sendJsonResponse();
    373379    }
     380
     381    /**
     382     * Refuse the request with an error notice unless the user can edit reviews.
     383     */
     384    protected function checkSearchPermission(): void
     385    {
     386        if (glsr()->can('edit_posts')) {
     387            return;
     388        }
     389        glsr(Notice::class)->addError(
     390            _x('You do not have permission to search.', 'admin-text', 'site-reviews')
     391        );
     392        wp_send_json_error([
     393            'notices' => glsr(Notice::class)->get(),
     394        ]);
     395    }
    374396}
  • site-reviews/tags/8.3.8/plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php

    r3583819 r3803670  
    33namespace GeminiLabs\SiteReviews\Controllers\Api\Version1\Permissions;
    44
     5use GeminiLabs\SiteReviews\Defaults\UpdateReviewDefaults;
    56use GeminiLabs\SiteReviews\Helpers\Arr;
    67use GeminiLabs\SiteReviews\Review;
    … …  
    127128            $message = _x('Sorry, you are not allowed to edit this review.', 'admin-text', 'site-reviews');
    128129            return new \WP_Error('rest_cannot_edit', $message, [
     130                'status' => rest_authorization_required_code(),
     131            ]);
     132        }
     133        if ($this->is_approving($request, $review) && !glsr()->can('publish_post', $review->ID)) {
     134            $message = _x('Sorry, you are not allowed to approve this review.', 'admin-text', 'site-reviews');
     135            return new \WP_Error('rest_cannot_publish', $message, [
    129136                'status' => rest_authorization_required_code(),
    130137            ]);
    … …  
    183190        return true;
    184191    }
     192
     193    /**
     194     * Whether the update approves a review that is not approved yet.
     195     */
     196    protected function is_approving(\WP_REST_Request $request, Review $review): bool
     197    {
     198        $values = glsr(UpdateReviewDefaults::class)->restrict($request->get_params()); // the status comes from is_approved
     199        return 'publish' === Arr::get($values, 'status') && !$review->is_approved;
     200    }
    185201}
  • site-reviews/tags/8.3.8/plugin/Controllers/Api/Version1/RestReviewController.php

    r3683434 r3803670  
    291291            ->buildReviewsHtml();
    292292        $response = rest_ensure_response([
    293             'pagination' => $html->getPagination($wrap = false),
     293            'pagination' => $html->getPagination(false), // don't wrap
    294294            'rendered' => $html->getReviews(),
    295295        ]);
  • site-reviews/tags/8.3.8/plugin/Controllers/EditorController.php

    r3642444 r3803670  
    7373        }
    7474        $strings = glsr(UpdatedMessageDefaults::class)->defaults();
    75         $restored = filter_input(\INPUT_GET, 'revision');
    76         if ($revisionTitle = wp_post_revision_title(intval($restored), false)) {
     75        $restored = false;
     76        $revisionId = intval(filter_input(\INPUT_GET, 'revision'));
     77        if ($revisionTitle = wp_post_revision_title($revisionId, false)) {
    7778            $restored = sprintf($strings['restored'], $revisionTitle);
    7879        }
  • site-reviews/tags/8.3.8/plugin/Controllers/ListTableController.php

    r3642444 r3803670  
    206206                unset($filters['type']);
    207207            }
    208             foreach ($filters as $key => &$value) {
    209                 $value = glsr($value)->title();
    210             }
     208            $filters = array_map(fn ($value) => glsr($value)->title(), $filters);
    211209            ksort($filters);
    212210            $setting = 'edit_'.glsr()->post_type.'_filters';
    … …  
    441439                    $clause['clauses'][] = "AND {$assignedTable}.{$column} = {$value}";
    442440                }
    443             } elseif (in_array($key, ['rating', 'terms', 'type'])) {
     441            } elseif (in_array($key, ['rating', 'terms'])) {
    444442                $clause['clauses'][] = "AND {$ratings}.{$key} = '{$value}'";
     443            } elseif ('type' === $key && array_key_exists($value, glsr()->retrieveAs('array', 'review_types'))) {
     444                $clause['clauses'][] = "AND {$ratings}.type = '{$value}'";
    445445            } elseif ('author' === $key && '0' === $value) {
    446446                // Filtering by the "author" URL parameter is automatically done
  • site-reviews/tags/8.3.8/plugin/Controllers/MainController.php

    r3722268 r3803670  
    5252            $table = glsr($classname);
    5353            if ($table instanceof TableRatings) {
    54                 $ratingsTable[$table->name($prefixName = true)] = $table->tablename;
     54                $ratingsTable[$table->name(true)] = $table->tablename; // use the prefixed name
    5555                continue;
    5656            }
    57             $customTables[$table->name($prefixName = true)] = $table->tablename;
     57            $customTables[$table->name(true)] = $table->tablename; // use the prefixed name
    5858        }
    5959        return $customTables + $ratingsTable + $tables;
  • site-reviews/tags/8.3.8/plugin/Controllers/PrivacyController.php

    r3769299 r3803670  
    152152        ", $email, glsr()->post_type, $this->perPage, ($page - 1) * $this->perPage);
    153153        $reviewIds = glsr(Database::class)->dbGetCol($sql);
    154         return empty($reviewIds) ? [] : glsr(Query::class)->reviews([], $reviewIds);
     154        return empty($reviewIds) ? [] : glsr(Query::class)->reviews(['order' => 'asc', 'orderby' => 'id'], $reviewIds);
    155155    }
    156156}
  • site-reviews/tags/8.3.8/plugin/Controllers/ToolsController.php

    r3769299 r3803670  
    310310    public function ipAddressDetection(Request $request): void
    311311    {
     312        if (!glsr()->can(glsr()->getPermission('settings'))) {
     313            glsr(Notice::class)->addError(
     314                _x('You do not have permission to change the IP address detection.', 'admin-text', 'site-reviews')
     315            );
     316            return;
     317        }
    312318        if (wp_validate_boolean($request->get('alt', 0))) {
    313319            $this->execute(new DetectIpAddress());
    … …  
    322328    public function ipAddressDetectionAjax(Request $request): void
    323329    {
     330        if (!glsr()->can(glsr()->getPermission('settings'))) {
     331            glsr(Notice::class)->addError(
     332                _x('You do not have permission to change the IP address detection.', 'admin-text', 'site-reviews')
     333            );
     334            wp_send_json_error([
     335                'notices' => glsr(Notice::class)->get(),
     336            ]);
     337        }
    324338        $command = wp_validate_boolean($request->get('alt', 0))
    325339            ? $this->execute(new DetectIpAddress())
  • site-reviews/tags/8.3.8/plugin/Controllers/VerificationController.php

    r3642444 r3803670  
    1414use GeminiLabs\SiteReviews\Modules\Encryption;
    1515use GeminiLabs\SiteReviews\Modules\Html\Template;
     16use GeminiLabs\SiteReviews\Modules\Notice;
    1617use GeminiLabs\SiteReviews\Request;
    1718use GeminiLabs\SiteReviews\Review;
    … …  
    4748    {
    4849        $review = glsr(ReviewManager::class)->get($request->cast('post_id', 'int'));
     50        if (!$review->isValid() || !glsr()->can('edit_post', $review->ID)) {
     51            glsr(Notice::class)->addError(
     52                _x('You do not have permission to request verification of this review.', 'admin-text', 'site-reviews')
     53            );
     54            wp_send_json_error([
     55                'notices' => glsr(Notice::class)->get(),
     56            ]);
     57        }
    4958        $pathPostId = $review->meta()->cast('_submitted._post_id', 'int');
    5059        $path = Cast::toString(wp_parse_url((string) get_permalink($pathPostId), \PHP_URL_PATH));
  • site-reviews/tags/8.3.8/plugin/Database.php

    r3642444 r3803670  
    3131    public function beginTransaction(string $table): void
    3232    {
    33         if (glsr(Tables::class)->isSqlite()) {
     33        if (glsr(Tables::class)->isSqliteLegacy()) {
    3434            $this->dbQuery("BEGIN TRANSACTION;");
    35         } elseif (glsr(Tables::class)->isInnodb($table)) {
     35        } elseif (glsr(Tables::class)->isSqlite() || glsr(Tables::class)->isInnodb($table)) {
    3636            $this->dbQuery("START TRANSACTION;");
    3737        } elseif (glsr(Tables::class)->isMyisam($table)) {
    … …  
    159159        $fields = glsr(Query::class)->escFieldsForInsert(array_keys($data));
    160160        $values = glsr(Query::class)->escValuesForInsert($data);
    161         if (glsr(Tables::class)->isSqlite()) {
    162             $sql = glsr(Query::class)->sql("INSERT OR IGNORE INTO {$table} {$fields} VALUES {$values}");
    163         } else {
    164             $sql = glsr(Query::class)->sql("INSERT IGNORE INTO {$table} {$fields} VALUES {$values}");
    165         }
     161        $ignore = glsr(Tables::class)->isSqliteLegacy() ? 'OR IGNORE' : 'IGNORE';
     162        $sql = glsr(Query::class)->sql("INSERT {$ignore} INTO {$table} {$fields} VALUES {$values}");
    166163        return $this->dbQuery($sql);
    167164    }
    … …  
    186183        $fields = glsr(Query::class)->escFieldsForInsert($fields);
    187184        $values = implode(',', $data);
    188         if (glsr(Tables::class)->isSqlite()) {
    189             $sql = glsr(Query::class)->sql("INSERT OR IGNORE INTO {$table} {$fields} VALUES {$values}");
    190         } else {
    191             $sql = glsr(Query::class)->sql("INSERT IGNORE INTO {$table} {$fields} VALUES {$values}");
    192         }
     185        $ignore = glsr(Tables::class)->isSqliteLegacy() ? 'OR IGNORE' : 'IGNORE';
     186        $sql = glsr(Query::class)->sql("INSERT {$ignore} INTO {$table} {$fields} VALUES {$values}");
    193187        return $this->dbQuery($sql);
    194188    }
  • site-reviews/tags/8.3.8/plugin/Database/OptionManager.php

    r3769299 r3803670  
    313313        $writes = [];
    314314        $versions = [];
    315         foreach (static::addons() as $slug => $addon) {
     315        foreach (static::addons() as $addon) {
    316316            if ($addon->hostedBy()) {
    317317                continue; // the host claims its whole subtree, hosted values included
    … …  
    382382    {
    383383        $option = get_option($path, $fallback);
    384         return Cast::to($cast, Helper::ifEmpty($option, $fallback, $strict = true));
     384        return Cast::to($cast, Helper::ifEmpty($option, $fallback, true)); // use strict comparison
    385385    }
    386386
    … …  
    393393    protected function compose(array $settings): array
    394394    {
    395         foreach (static::addons() as $slug => $addon) {
     395        foreach (static::addons() as $addon) {
    396396            if ($addon->hostedBy()) {
    397397                continue; // mounted by its host's whole subtree
  • site-reviews/tags/8.3.8/plugin/Database/Query.php

    r3642444 r3803670  
    4444    public function ratings(array $args = []): array
    4545    {
    46         $this->setArgs($args, $unset = ['orderby']);
     46        $this->setArgs($args, ['orderby']); // unset orderby
    4747        $results = glsr(Database::class)->dbGetResults($this->queryRatings(), \ARRAY_A);
    4848        return $this->normalizeRatings($results);
    … …  
    5555            return [];
    5656        }
    57         $this->setArgs($args, $unset = ['orderby']);
     57        $this->setArgs($args, ['orderby']); // unset orderby
    5858        $results = glsr(Database::class)->dbGetResults($this->$method(), \ARRAY_A);
    5959        return $this->normalizeRatingsByAssignedId($results);
    … …  
    125125    public function totalReviews(array $args = [], array $reviews = []): int
    126126    {
    127         $this->setArgs($args, $unset = ['orderby']);
     127        $this->setArgs($args, ['orderby']); // unset orderby
    128128        if (empty($this->sqlLimit()) && !empty($reviews)) {
    129129            return count($reviews);
  • site-reviews/tags/8.3.8/plugin/Database/RatingManager.php

    r3474155 r3803670  
    2222        $metaTable = sprintf('%smeta', $metaGroup);
    2323        $ratings = glsr(Query::class)->ratingsFor($metaTable, $args);
    24         foreach ($ratings as $id => &$group) {
    25             $group = $this->reduce($group, $args);
    26         }
     24        $ratings = array_map(fn ($group) => $this->reduce($group, $args), $ratings);
    2725        return glsr()->filterArray('ratings/grouped', $ratings, $metaGroup, $args);
    2826    }
  • site-reviews/tags/8.3.8/plugin/Database/Sql.php

    r3642444 r3803670  
    284284            $this->join('assigned_posts', $this->joinKeyword()),
    285285            $this->args['assigned_posts'],
    286             $prepare = false
     286            false // don't prepare
    287287        );
    288288    }
    … …  
    304304            $this->join('assigned_terms', $this->joinKeyword()),
    305305            $this->args['assigned_terms'],
    306             $prepare = false
     306            false // don't prepare
    307307        );
    308308    }
    … …  
    313313            $this->join('assigned_users', $this->joinKeyword()),
    314314            $this->args['assigned_users'],
    315             $prepare = false
     315            false // don't prepare
    316316        );
    317317    }
    … …  
    322322            $this->join('posts', 'INNER JOIN'),
    323323            $this->args['content'],
    324             $prepare = false
     324            false // don't prepare
    325325        );
    326326    }
    … …  
    331331            $this->join('posts', 'INNER JOIN'),
    332332            array_filter($this->args['date']),
    333             $prepare = false
     333            false // don't prepare
    334334        );
    335335    }
    … …  
    340340            $this->join('posts', 'INNER JOIN'),
    341341            $this->args['user__in'],
    342             $prepare = false
     342            false // don't prepare
    343343        );
    344344    }
    … …  
    349349            $this->join('posts', 'INNER JOIN'),
    350350            $this->args['user__not_in'],
    351             $prepare = false
     351            false // don't prepare
    352352        );
    353353    }
  • site-reviews/tags/8.3.8/plugin/Database/Tables.php

    r3683434 r3803670  
    124124    {
    125125        return 'sqlite' === $this->engine;
     126    }
     127
     128    /**
     129     * If SQLite Database Integration plugin is < v3.
     130     */
     131    public function isSqliteLegacy(): bool
     132    {
     133        return $this->isSqlite()
     134            && is_a($this->db->dbh, 'WP_SQLite_Translator'); // @phpstan-ignore-line
    126135    }
    127136
  • site-reviews/tags/8.3.8/plugin/Defaults/ApiDefaults.php

    r3769299 r3803670  
    1313    public array $casts = [
    1414        'blocking' => 'bool',
     15        'cache' => 'bool',
    1516        'compress' => 'bool',
    1617        'cookies' => 'array',
    … …  
    5051            'blocking' => true, // Whether the calling code requires the result of the request.
    5152            'body' => null,
     53            'cache' => true, // if the response is cached
    5254            'compress' => false, // Whether to compress the $body when sending the request.
    5355            'cookies' => [],
  • site-reviews/tags/8.3.8/plugin/Helpers/Arr.php

    r3642444 r3803670  
    2525        if (is_object($value)) {
    2626            $values = get_object_vars($value);
    27             $value = Helper::ifEmpty($values, (array) $value, $strict = true);
     27            $value = Helper::ifEmpty($values, (array) $value, true); // use strict comparison
    2828        }
    2929        return is_array($value) ? $value : $fallback;
  • site-reviews/tags/8.3.8/plugin/Helpers/Text.php

    r3777315 r3803670  
    124124        }
    125125        $wordCount = 0;
    126         foreach ($iterator->getPartsIterator() as $part) {
     126        while (\IntlBreakIterator::DONE !== $iterator->next()) {
    127127            if (\IntlBreakIterator::WORD_NONE !== $iterator->getRuleStatus()) {
    128128                ++$wordCount;
    … …  
    245245        }
    246246        $iterator = \IntlRuleBasedBreakIterator::createWordInstance('');
     247        if (!$iterator instanceof \IntlRuleBasedBreakIterator) { // has getRuleStatus()
     248            return null;
     249        }
    247250        $normalizedText = \Normalizer::normalize($text);
    248251        if (empty($normalizedText)) {
    … …  
    258261    {
    259262        $map = [];
     263        $text = str_replace('⍈', '&#x2348;', $text); // a ⍈ in the text is not a placeholder
    260264        $result = preg_replace_callback('|<([a-z]+)[^>]*?>.*?</\\1>|siu', function ($matches) use (&$map) {
    261265            $map[] = $matches[0];
  • site-reviews/tags/8.3.8/plugin/Integrations/MultilingualPress/Defaults/SyncReviewDefaults.php

    r3474155 r3803670  
    33namespace GeminiLabs\SiteReviews\Integrations\MultilingualPress\Defaults;
    44
     5use GeminiLabs\SiteReviews\Defaults\CustomFieldsDefaults;
    56use GeminiLabs\SiteReviews\Defaults\UpdateReviewDefaults;
    67
    … …  
    3940        'author' => 'name',
    4041    ];
     42
     43    /**
     44     * Normalize provided values, this always runs first.
     45     */
     46    protected function normalize(array $values = []): array
     47    {
     48        $customKeys = array_keys(glsr(CustomFieldsDefaults::class)->call('merge', $values));
     49        $this->guarded = array_values(array_unique(array_merge($this->guarded, $customKeys))); // guard all custom keys
     50        return parent::normalize($values);
     51    }
    4152}
  • site-reviews/tags/8.3.8/plugin/Integrations/MultilingualPress/RelationSaveHelper.php

    r3518858 r3803670  
    88use GeminiLabs\SiteReviews\Defaults\RatingDefaults;
    99use GeminiLabs\SiteReviews\Helpers\Arr;
    10 use GeminiLabs\SiteReviews\Integrations\MultilingualPress\Defaults\SyncReviewDefaults;
    1110use GeminiLabs\SiteReviews\Integrations\MultilingualPress\MetaboxFields\AssignedPostsField;
    1211use GeminiLabs\SiteReviews\Integrations\MultilingualPress\MetaboxFields\AssignedUsersField;
    … …  
    201200    }
    202201
     202    /**
     203     * Write the values on the remote review with the assigned posts and users mapped to the remote site.
     204     *
     205     * @param array $data The guarded values (ReviewCopier::syncData) with the source site's assigned ids
     206     */
    203207    public function syncUpdate(array $data, bool $force = false): void
    204208    {
    … …  
    206210            return;
    207211        }
    208         $guarded = glsr(SyncReviewDefaults::class)->merge($data);
     212        // Not merged here as this runs on the remote site.
     213        $values = $data;
     214        unset($values['assigned_posts'], $values['assigned_users']);
    209215        if ($force || $this->canSyncAssignment(AssignedPostsField::FIELD_COPY_ASSIGNED_POSTS)) {
    210216            $sourcePostIds = Arr::consolidate($data['assigned_posts'] ?? []);
    211             $guarded['assigned_posts'] = $this->remoteAssignedPosts($sourcePostIds);
     217            $values['assigned_posts'] = $this->remoteAssignedPosts($sourcePostIds);
    212218        }
    213219        if ($force || $this->canSyncAssignment(AssignedUsersField::FIELD_COPY_ASSIGNED_USERS)) {
    214220            $sourceUserIds = Arr::consolidate($data['assigned_users'] ?? []);
    215             $guarded['assigned_users'] = $this->remoteAssignedUsers($sourceUserIds);
    216         }
    217         $originalSiteId = $this->maybeSwitchSite($this->context->remoteSiteId());
    218         glsr_update_review($this->context->remotePostId(), $guarded);
     221            $values['assigned_users'] = $this->remoteAssignedUsers($sourceUserIds);
     222        }
     223        $originalSiteId = $this->maybeSwitchSite($this->context->remoteSiteId());
     224        glsr_update_review($this->context->remotePostId(), $values);
    219225        $this->syncAssignedTerms();
    220226        $this->maybeRestoreSite($originalSiteId);
  • site-reviews/tags/8.3.8/plugin/Integrations/MultilingualPress/ReviewCopier.php

    r3637056 r3803670  
    33namespace GeminiLabs\SiteReviews\Integrations\MultilingualPress;
    44
     5use GeminiLabs\SiteReviews\Integrations\MultilingualPress\Defaults\SyncReviewDefaults;
     6use GeminiLabs\SiteReviews\Review;
    57use Inpsyde\MultilingualPress\Framework\Api\ContentRelations;
    68use Inpsyde\MultilingualPress\Framework\SwitchSiteTrait;
    … …  
    9193    public function sync(): void
    9294    {
    93         $review = glsr_get_review($this->sourcePostId);
    94         $data = wp_parse_args($review->toArray(), $review->custom()->toArray());
     95        $data = $this->syncData(glsr_get_review($this->sourcePostId));
    9596        $this->run(function ($context) use ($data) {
    9697            $helper = new RelationSaveHelper($context);
    9798            $helper->syncUpdate($data, true);
    9899        });
     100    }
     101
     102    public function syncData(Review $review): array
     103    {
     104        $data = wp_parse_args($review->toArray(), $review->custom()->toArray());
     105        // Guarded on the source site: the site-reviews/defaults/sync-review filter reads the review's form here.
     106        $data = glsr(SyncReviewDefaults::class)->merge($data);
     107        $data['assigned_posts'] = $review->assigned_posts; // the source ids mapped by syncUpdate() on each remote site
     108        $data['assigned_users'] = $review->assigned_users;
     109        return $data;
    99110    }
    100111
  • site-reviews/tags/8.3.8/plugin/Integrations/WPLoyalty/Controller.php

    r3637056 r3803670  
    4444            ]);
    4545            $messages = [$field->after];
    46             foreach ($rewardsList as $action => $rewards) {
    47                 foreach ($rewards as $key => $reward) {
     46            foreach ($rewardsList as $rewards) {
     47                foreach ($rewards as $reward) {
    4848                    $messages[] = $reward['messages'] ?? '';
    4949                }
  • site-reviews/tags/8.3.8/plugin/Integrations/WooCommerce/Controllers/RestApi/ProductReviewsController.php

    r3642444 r3803670  
    226226        $postArgs = $this->prepare_item_for_update($request);
    227227        $postArgs['ID'] = $review->ID;
    228         $updatePost = wp_update_post($postArgs, $wperror = true);
     228        $updatePost = wp_update_post($postArgs, true); // return a WP_Error on failure
    229229        if (is_wp_error($updatePost)) {
    230230            return new \WP_Error('woocommerce_rest_comment_failed_edit', 'Updating review failed.', ['status' => 500]);
  • site-reviews/tags/8.3.8/plugin/Modules/Html/Form.php

    r3683434 r3803670  
    500500    protected function normalizeFieldValue(FieldContract $field): void
    501501    {
     502        if ($this->session->failed && 'hidden' === $field->original_type) {
     503            return; // hidden fields are signed
     504        }
    502505        if ($field->isChoiceField()) {
    503506            $value = Cast::toArray($this->session->values[$field->original_name] ?? []);
    … …  
    511514    }
    512515
     516    /**
     517     * @return array<string, string|array{hidden: string[], visible: int}>
     518     */
     519    protected function signedValues(): array
     520    {
     521        $lists = $values = [];
     522        foreach ($this->hidden() as $field) {
     523            $values[$field->original_name] = $field->value;
     524            if (str_ends_with($field->name, '[]')) { // PHP reads these fields as a list
     525                $lists[$field->original_name]['hidden'][] = Cast::toString($field->value);
     526            }
     527        }
     528        $visibleFields = $this->visible();
     529        foreach (array_keys($lists) as $name) {
     530            $lists[$name]['visible'] = count(
     531                array_filter($visibleFields, fn ($field) => $name === $field->original_name)
     532            );
     533        }
     534        $values = array_merge($values, $lists);
     535        return $this->app()->filterArray("{$this->formName()}/signature/values", $values, $this);
     536    }
     537
    513538    protected function signForm(): void
    514539    {
    515         $hidden = $this->hidden();
    516         $values = [];
    517         foreach ($hidden as $field) {
    518             $values[$field->original_name] = $field->value;
    519         }
    520         $values = $this->app()->filterArray("{$this->formName()}/signature/values", $values, $this);
     540        $values = $this->signedValues();
    521541        $signatureField = $this->field('form_signature', [
    522542            'type' => 'hidden',
    523543            'value' => glsr(Encryption::class)->encrypt(maybe_serialize($values)),
    524544        ]);
    525         $fields = array_values(array_merge($hidden, [$signatureField], $this->visible()));
     545        $fields = array_values(array_merge($this->hidden(), [$signatureField], $this->visible()));
    526546        $this->exchangeArray($fields);
    527547    }
  • site-reviews/tags/8.3.8/plugin/Modules/Html/ReviewForm.php

    r3683434 r3803670  
    66use GeminiLabs\SiteReviews\Helpers\Arr;
    77use GeminiLabs\SiteReviews\Helpers\Cast;
     8use GeminiLabs\SiteReviews\Request;
    89
    910class ReviewForm extends Form
    … …  
    2627    {
    2728        do_action('litespeed_nonce', 'submit-review'); // @litespeedcache
    28         $referer = glsr()->filterString('review-form/referer', wp_get_referer());
     29        $referer = glsr()->filterString('review-form/referer', $this->referer());
    2930        $config = [
    3031            '_action' => 'submit-review',
    … …  
    8889        return $fields;
    8990    }
     91
     92    /**
     93     * The page the visitor came from.
     94     */
     95    protected function referer(): string
     96    {
     97        if (glsr()->session()->cast('form_invalid', 'bool')) {
     98            $submitted = new Request(glsr()->session()->array('form_values'));
     99            if ($referer = Cast::toString($submitted->signedValues()['_referer'] ?? '')) {
     100                return $referer;
     101            }
     102        }
     103        return Cast::toString(wp_get_referer());
     104    }
    90105}
  • site-reviews/tags/8.3.8/plugin/Modules/Html/SettingForm.php

    r3722268 r3803670  
    141141            return; // only a setting field has dependencies
    142142        }
    143         $isHidden = false;
    144143        foreach (Arr::consolidate($field->depends_on) as $path => $expectedValue) {
    145144            $default = Arr::get(glsr()->defaults(), $path);
  • site-reviews/tags/8.3.8/plugin/Modules/Paginate.php

    r3769299 r3803670  
    1919        $parts = explode('?', $base);
    2020        if (wp_is_serving_rest_request() || !NormalizePaginationArgs::pagedRequest()->isEmpty()) {
    21             $parts = [$parts[0]]; // this is an AJAX request
     21            $parts = []; // this is an AJAX request
    2222        }
    2323        if ($parts[1] ?? false) {
  • site-reviews/tags/8.3.8/plugin/Modules/Sanitizers/ArraySanitizer.php

    r2878258 r3803670  
    33namespace GeminiLabs\SiteReviews\Modules\Sanitizers;
    44
     5use GeminiLabs\SiteReviews\Helpers\Arr;
    56use GeminiLabs\SiteReviews\Helpers\Cast;
    67
    … …  
    910    abstract public function run(): array;
    1011
     12    /**
     13     * The values, with a comma-separated string among them read as several values.
     14     */
     15    protected function splitValue(): array
     16    {
     17        $values = [];
     18        foreach ($this->value() as $value) {
     19            $values[] = is_string($value) ? Arr::convertFromString($value) : [$value];
     20        }
     21        return array_merge([], ...$values);
     22    }
     23
    1124    protected function value(): array
    1225    {
  • site-reviews/tags/8.3.8/plugin/Modules/Sanitizers/SanitizeEmails.php

    r2884378 r3803670  
    88    {
    99        $emails = explode(',', $this->value());
    10         foreach ($emails as &$email) {
    11             $email = sanitize_email($email);
    12         }
     10        $emails = array_map(fn ($email) => sanitize_email($email), $emails);
    1311        return implode(',', array_filter($emails));
    1412    }
  • site-reviews/tags/8.3.8/plugin/Modules/Sanitizers/SanitizePostIds.php

    r2878258 r3803670  
    1111    {
    1212        return Arr::uniqueInt(
    13             array_map([Helper::class, 'getPostId'], $this->value())
     13            array_map([Helper::class, 'getPostId'], $this->splitValue())
    1414        );
    1515    }
  • site-reviews/tags/8.3.8/plugin/Modules/Sanitizers/SanitizeTermIds.php

    r2878258 r3803670  
    1111    {
    1212        return Arr::uniqueInt(
    13             array_map([Helper::class, 'getTermTaxonomyId'], $this->value())
     13            array_map([Helper::class, 'getTermTaxonomyId'], $this->splitValue())
    1414        );
    1515    }
  • site-reviews/tags/8.3.8/plugin/Modules/Sanitizers/SanitizeUserIds.php

    r2878258 r3803670  
    1111    {
    1212        return Arr::uniqueInt(
    13             array_map([Helper::class, 'getUserId'], $this->value())
     13            array_map([Helper::class, 'getUserId'], $this->splitValue())
    1414        );
    1515    }
  • site-reviews/tags/8.3.8/plugin/Modules/Schema.php

    r3642444 r3803670  
    114114    {
    115115        if ($schema = $this->retrieve()) {
    116             $json = (string) wp_json_encode($schema, \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES);
     116            $json = (string) wp_json_encode($schema, \JSON_HEX_AMP | \JSON_HEX_TAG | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
    117117            printf('<script type="application/ld+json" class="%s-schema">%s</script>', glsr()->id, $json);
    118118        }
    … …  
    209209            return $this->getSchemaOptionDefault($setting, $fallback);
    210210        }
    211         return Helper::ifEmpty($setting, $fallback, $strict = true);
     211        return Helper::ifEmpty($setting, $fallback, true); // use strict comparison
    212212    }
    213213
  • site-reviews/tags/8.3.8/plugin/Modules/SystemInfo.php

    r3642444 r3803670  
    117117            ];
    118118        }
    119         $engines = glsr(Tables::class)->tableEngines($removePrefix = true);
     119        $engines = glsr(Tables::class)->tableEngines(true); // remove the prefix
    120120        foreach ($engines as $engine => $tables) {
    121121            $engines[$engine] = sprintf('%s (%s)', $engine, implode('|', $tables));
  • site-reviews/tags/8.3.8/plugin/Modules/Validator.php

    r3474155 r3803670  
    172172     * @param mixed $value
    173173     */
    174     protected function getSize(string $attribute, $value): int
     174    protected function getSize(string $attribute, $value): float
    175175    {
    176176        $hasNumeric = $this->hasRule($attribute, $this->numericRules);
    177177        if (is_numeric($value) && $hasNumeric) {
    178             return (int) $value;
     178            return (float) $value;
    179179        }
    180180        if (is_array($value)) {
  • site-reviews/tags/8.3.8/plugin/Modules/Validator/CaptchaValidatorAbstract.php

    r3488530 r3803670  
    8484        return [
    8585            'body' => $body,
    86             'force' => true,
     86            'cache' => false,
    8787        ];
    8888    }
  • site-reviews/tags/8.3.8/plugin/Modules/Validator/FriendlycaptchaV2Validator.php

    r3488530 r3803670  
    6565    {
    6666        return [
    67             'force' => true,
     67            'body' => wp_json_encode($body),
     68            'cache' => false,
    6869            'headers' => [
    6970                'Content-Type' => 'application/json',
    7071                'X-API-Key' => $this->siteSecret(),
    7172            ],
    72             'body' => wp_json_encode($body),
    7373        ];
    7474    }
  • site-reviews/tags/8.3.8/plugin/Modules/Validator/ProcaptchaValidator.php

    r3637056 r3803670  
    5555    {
    5656        return [
    57             'force' => true,
     57            'body' => wp_json_encode($body),
     58            'cache' => false,
    5859            'headers' => ['Content-Type' => 'application/json'],
    59             'body' => wp_json_encode($body),
    6060        ];
    6161    }
  • site-reviews/tags/8.3.8/plugin/Modules/Validator/SignatureValidator.php

    r3683434 r3803670  
    33namespace GeminiLabs\SiteReviews\Modules\Validator;
    44
     5use GeminiLabs\SiteReviews\Helpers\Arr;
    56use GeminiLabs\SiteReviews\Helpers\Cast;
    67
    … …  
    1011    {
    1112        $isValid = true;
    12         $values = $this->request->signedValues(['form_id' => '']);
    13         foreach ($values as $key => $value) {
    14             if (Cast::toString($value) !== $this->request->cast($key, 'string')) {
     13        $signedValues = $this->request->signedValues(['form_id' => '']);
     14        foreach ($signedValues as $key => $signedValue) {
     15            if ($this->hasValueChanged($signedValue, $this->request->get($key))) {
    1516                $isValid = false;
    16                 glsr_log()->debug($values);
     17                glsr_log()->debug($signedValues);
    1718                break;
    1819            }
    1920        }
    20         return glsr()->filterBool('validate/signature', $isValid, $values, $this->request);
     21        return glsr()->filterBool('validate/signature', $isValid, $signedValues, $this->request);
    2122    }
    2223
    … …  
    3031        }
    3132    }
     33
     34    /**
     35     * @param string|array{hidden?: string[], visible?: int} $signedValue
     36     * @param mixed                                          $requestValue
     37     */
     38    protected function hasValueChanged($signedValue, $requestValue): bool
     39    {
     40        if (!is_array($signedValue)) {
     41            return is_array($requestValue) || Cast::toString($signedValue) !== Cast::toString($requestValue);
     42        }
     43        if (!Arr::isIndexedAndFlat($requestValue)) {
     44            return true;
     45        }
     46        $requestValue = array_map([Cast::class, 'toString'], $requestValue);
     47        foreach (Arr::consolidate($signedValue['hidden'] ?? []) as $hidden) {
     48            $index = array_search(Cast::toString($hidden), $requestValue, true);
     49            if (false === $index) {
     50                return true;
     51            }
     52            unset($requestValue[$index]);
     53        }
     54        return count($requestValue) > Cast::toInt($signedValue['visible'] ?? 0);
     55    }
    3256}
  • site-reviews/tags/8.3.8/plugin/Modules/Validator/ValidationRules.php

    r3642444 r3803670  
    162162     * @param mixed $value
    163163     */
    164     abstract protected function getSize(string $attribute, $value): int;
     164    abstract protected function getSize(string $attribute, $value): float;
    165165
    166166    /**
    … …  
    173173        }
    174174        return preg_replace_callback('/(%s)/', function () use (&$parameters) {
    175             foreach ($parameters as $key => $value) {
    176                 return array_shift($parameters);
    177             }
     175            return array_shift($parameters);
    178176        }, $message);
    179177    }
  • site-reviews/tags/8.3.8/plugin/Notices/GatekeeperNotice.php

    r3642444 r3803670  
    121121            return '';
    122122        }
     123        if (!str_starts_with($data['plugin_uri'], 'https://wordpress.org/plugins/')) {
     124            return '';
     125        }
    123126        return $this->pluginAction([
    124127            'action' => 'install-plugin',
    … …  
    126129            'name' => $data['name'],
    127130            'nonce_prefix' => 'install-plugin_',
    128             'plugin' => $data['textdomain'],
     131            'plugin' => dirname($data['plugin']), // wordpress.org names a plugin's folder after its slug
    129132            /* translators: %s: the plugin name */
    130133            'text' => _x('Install %s', 'admin-text', 'site-reviews'),
    … …  
    183186    {
    184187        $links = [];
    185         foreach ($errors as $plugin => $data) {
     188        foreach ($errors as $data) {
    186189            $links[] = sprintf('<span class="plugin-%s"><a href="%s">%s</a></span>',
    187190                $data['textdomain'],
  • site-reviews/tags/8.3.8/readme.txt

    r3777315 r3803670  
    44Tags: reviews, ratings, testimonials, product reviews, business reviews
    55Tested up to: 7.1
    6 Stable tag: 8.3.7
     6Stable tag: 8.3.8
    77License: GPLv3
    88License URI: https://www.gnu.org/licenses/gpl-3.0.html
    … …  
    211211
    212212== Changelog ==
     213
     214= 8.3.8 (2026-11-15) =
     215
     216- Fixed a security issue in review excerpts
     217- Fixed a security issue in the notice shown after restoring a revision
     218- Fixed a security issue in the review type filter
     219- Fixed a security issue in the schema
     220- Fixed captcha checks slowly filling up the database
     221- Fixed forms with more than one Assigned Pages, Categories or Users field not submitting
     222- Fixed hidden form fields changing after a failed submission
     223- Fixed MultilingualPress translations losing their custom field values when the original review is edited
     224- Fixed number fields ignoring decimals in their minimum and maximum
     225- Fixed reviews not saving with version 3 of the SQLite Database Integration plugin
     226- Fixed some assigned pages, categories or users not being saved with a review
     227- Fixed the admin search permission check
     228- Fixed the Install button in the missing plugin notice
     229- Fixed the IP address detection tool permission check
     230- Fixed the review approval permission check
     231- Fixed the validation of submitted reviews
     232- Fixed the verification email permission check
    213233
    214234= 8.3.7 (2026-10-31) =
  • site-reviews/tags/8.3.8/site-reviews.php

    r3777315 r3803670  
    88 * Plugin URI:           https://wordpress.org/plugins/site-reviews
    99 * Description:          Receive and display reviews on your website
    10  * Version:              8.3.7
     10 * Version:              8.3.8
    1111 * Author:               Paul Ryley
    1212 * Author URI:           https://site-reviews.com
  • site-reviews/trunk/changelog.txt

    r3777315 r3803670  
    11== Changelog ==
     2
     3= 8.3.8 (2026-11-15) =
     4
     5- Fixed a security issue in review excerpts
     6- Fixed a security issue in the notice shown after restoring a revision
     7- Fixed a security issue in the review type filter
     8- Fixed a security issue in the schema
     9- Fixed captcha checks slowly filling up the database
     10- Fixed forms with more than one Assigned Pages, Categories or Users field not submitting
     11- Fixed hidden form fields changing after a failed submission
     12- Fixed MultilingualPress translations losing their custom field values when the original review is edited
     13- Fixed number fields ignoring decimals in their minimum and maximum
     14- Fixed reviews not saving with version 3 of the SQLite Database Integration plugin
     15- Fixed some assigned pages, categories or users not being saved with a review
     16- Fixed the admin search permission check
     17- Fixed the Install button in the missing plugin notice
     18- Fixed the IP address detection tool permission check
     19- Fixed the review approval permission check
     20- Fixed the validation of submitted reviews
     21- Fixed the verification email permission check
    222
    323= 8.3.7 (2026-10-31) =
  • site-reviews/trunk/languages/site-reviews-en_US.po

    r3777315 r3803670  
    116116
    117117#: plugin/Controllers/Api/Version1/RestRenderController.php:103
    118 #: plugin/Controllers/VerificationController.php:96
     118#: plugin/Controllers/VerificationController.php:105
    119119msgid "Thank you, your review has been verified."
    120120msgstr "Thank you, your review has been verified."
    121121
    122122#: plugin/Controllers/Api/Version1/RestRenderController.php:104
    123 #: plugin/Controllers/VerificationController.php:97
     123#: plugin/Controllers/VerificationController.php:106
    124124msgid "Thank you, your review has been verified and is awaiting approval."
    125125msgstr "Thank you, your review has been verified and is awaiting approval."
    … …  
    432432msgstr "Submitting, please wait..."
    433433
    434 #: plugin/Modules/Html/ReviewForm.php:14
     434#: plugin/Modules/Html/ReviewForm.php:15
    435435msgid "Submit Review"
    436436msgstr "Submit Review"
    … …  
    476476msgstr "You have already submitted a review."
    477477
    478 #: plugin/Modules/Validator/SignatureValidator.php:27
     478#: plugin/Modules/Validator/SignatureValidator.php:28
    479479#: views/pages/documentation/support/common-problems-and-solutions.php:89
    480480msgid "This review cannot be submitted, please refresh the page and try again."
    … …  
    24372437msgstr "Enabling this option will persist field values entered into the review form until either the review is submitted or the browser tab or window is closed."
    24382438
    2439 #: plugin/Database.php:237
     2439#: plugin/Database.php:231
    24402440msgctxt "admin-text"
    24412441msgid "(no title)"
    … …  
    34003400
    34013401#: plugin/Commands/SendVerificationEmail.php:71
    3402 #: plugin/Controllers/VerificationController.php:34
     3402#: plugin/Controllers/VerificationController.php:35
    34033403msgctxt "admin-text"
    34043404msgid "Resend Verification Request"
    … …  
    34863486msgstr "Import"
    34873487
    3488 #: plugin/Controllers/AdminController.php:309
    3489 #: plugin/Controllers/AdminController.php:327
    3490 #: plugin/Controllers/AdminController.php:340
     3488#: plugin/Controllers/AdminController.php:313
     3489#: plugin/Controllers/AdminController.php:332
     3490#: plugin/Controllers/AdminController.php:346
    34913491msgctxt "admin-text"
    34923492msgid "Nothing found."
    … …  
    35803580
    35813581#. translators: %s: the review type
    3582 #: plugin/Controllers/EditorController.php:108
     3582#: plugin/Controllers/EditorController.php:109
    35833583#, php-format
    35843584msgctxt "admin-text"
    … …  
    36813681msgstr "Respond"
    36823682
    3683 #: plugin/Controllers/ListTableController.php:272
     3683#: plugin/Controllers/ListTableController.php:270
    36843684msgctxt "admin-text"
    36853685msgid "Sorry, you are not allowed to respond to this review."
    36863686msgstr "Sorry, you are not allowed to respond to this review."
    36873687
    3688 #: plugin/Controllers/ListTableController.php:275
     3688#: plugin/Controllers/ListTableController.php:273
    36893689msgctxt "admin-text"
    36903690msgid "Someone"
    … …  
    36923692
    36933693#. translators: %s: user's name
    3694 #: plugin/Controllers/ListTableController.php:281
     3694#: plugin/Controllers/ListTableController.php:279
    36953695#, php-format
    36963696msgctxt "admin-text"
    … …  
    38213821msgstr "You do not have permission to import settings."
    38223822
    3823 #: plugin/Controllers/ToolsController.php:366
     3823#: plugin/Controllers/ToolsController.php:380
    38243824msgctxt "admin-text"
    38253825msgid "reload the page"
    … …  
    38273827
    38283828#. translators: %s: a link with the text "reload the page"
    3829 #: plugin/Controllers/ToolsController.php:371
     3829#: plugin/Controllers/ToolsController.php:385
    38303830#, php-format
    38313831msgctxt "admin-text"
    … …  
    38333833msgstr "The permissions have been repaired, please %s for them to take effect."
    38343834
    3835 #: plugin/Controllers/ToolsController.php:446
     3835#: plugin/Controllers/ToolsController.php:460
    38363836msgctxt "admin-text"
    38373837msgid "You do not have permission to rollback the plugin."
    … …  
    39213921msgstr "Approved on:"
    39223922
    3923 #: plugin/Controllers/VerificationController.php:35
     3923#: plugin/Controllers/VerificationController.php:36
    39243924msgctxt "admin-text"
    39253925msgid "Send Verification Request"
    … …  
    42244224
    42254225#. translators: %s: the plugin name
    4226 #: plugin/Notices/GatekeeperNotice.php:130
     4226#: plugin/Notices/GatekeeperNotice.php:133
    42274227#, php-format
    42284228msgctxt "admin-text"
    … …  
    42314231
    42324232#. translators: %s: the plugin name
    4233 #: plugin/Notices/GatekeeperNotice.php:146
     4233#: plugin/Notices/GatekeeperNotice.php:149
    42344234#, php-format
    42354235msgctxt "admin-text"
    … …  
    71167116msgstr "Limit result set to verified status."
    71177117
    7118 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:18
     7118#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:19
    71197119msgctxt "admin-text"
    71207120msgid "Cannot create existing review."
    71217121msgstr "Cannot create existing review."
    71227122
    7123 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:22
     7123#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:23
    71247124msgctxt "admin-text"
    71257125msgid "Sorry, you are not allowed to create reviews as this user."
    71267126msgstr "Sorry, you are not allowed to create reviews as this user."
    71277127
    7128 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:26
     7128#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:27
    71297129msgctxt "admin-text"
    71307130msgid "Sorry, you are not allowed to create reviews as this review author."
    71317131msgstr "Sorry, you are not allowed to create reviews as this review author."
    71327132
    7133 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:30
    7134 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:139
     7133#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:31
     7134#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:146
    71357135msgctxt "admin-text"
    71367136msgid "Sorry, you are not allowed to assign the provided terms."
    71377137msgstr "Sorry, you are not allowed to assign the provided terms."
    71387138
    7139 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:45
    7140 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:68
    7141 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:121
     7139#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:46
     7140#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:69
     7141#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:122
    71427142msgctxt "admin-text"
    71437143msgid "Invalid review ID."
    71447144msgstr "Invalid review ID."
    71457145
    7146 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:51
     7146#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:52
    71477147msgctxt "admin-text"
    71487148msgid "Sorry, you are not allowed to delete this review."
    71497149msgstr "Sorry, you are not allowed to delete this review."
    71507150
    7151 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:74
     7151#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:75
    71527152msgctxt "admin-text"
    71537153msgid "Sorry, you are not allowed to view this review."
    71547154msgstr "Sorry, you are not allowed to view this review."
    71557155
    7156 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:81
    7157 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:127
     7156#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:82
     7157#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:128
    71587158msgctxt "admin-text"
    71597159msgid "Sorry, you are not allowed to edit this review."
    71607160msgstr "Sorry, you are not allowed to edit this review."
    71617161
    7162 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:97
     7162#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:98
    71637163msgctxt "admin-text"
    71647164msgid "Sorry, you do not have permission to access reviews."
    71657165msgstr "Sorry, you do not have permission to access reviews."
    71667166
    7167 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:104
     7167#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:105
    71687168msgctxt "admin-text"
    71697169msgid "Sorry, you are not allowed to edit reviews."
    71707170msgstr "Sorry, you are not allowed to edit reviews."
    71717171
    7172 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:133
     7172#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:140
    71737173msgctxt "admin-text"
    71747174msgid "Sorry, you are not allowed to update the review as this user."
    … …  
    75607560
    75617561#. translators: %s: plugin name
    7562 #: plugin/Controllers/ToolsController.php:427
     7562#: plugin/Controllers/ToolsController.php:441
    75637563#, php-format
    75647564msgctxt "Site Reviews (admin-text)"
    … …  
    92359235msgstr ""
    92369236
    9237 #: plugin/Controllers/ToolsController.php:451
     9237#: plugin/Controllers/ToolsController.php:465
    92389238#: views/pages/tools/general/rollback-plugin.php:21
    92399239msgctxt "admin-text"
    … …  
    92549254msgid "%s was not updated because its folder is a git repository."
    92559255msgstr ""
     9256
     9257#: plugin/Controllers/AdminController.php:390
     9258msgctxt "admin-text"
     9259msgid "You do not have permission to search."
     9260msgstr ""
     9261
     9262#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:134
     9263msgctxt "admin-text"
     9264msgid "Sorry, you are not allowed to approve this review."
     9265msgstr ""
     9266
     9267#: plugin/Controllers/ToolsController.php:314
     9268#: plugin/Controllers/ToolsController.php:332
     9269msgctxt "admin-text"
     9270msgid "You do not have permission to change the IP address detection."
     9271msgstr ""
     9272
     9273#: plugin/Controllers/VerificationController.php:52
     9274msgctxt "admin-text"
     9275msgid "You do not have permission to request verification of this review."
     9276msgstr ""
  • site-reviews/trunk/languages/site-reviews.pot

    r3777315 r3803670  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: Site Reviews 8.3.7\n"
     5"Project-Id-Version: Site Reviews 8.3.8\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/site-reviews\n"
    77"Last-Translator: Paul Ryley <paul@geminilabs.io>\n"
    … …  
    32683268
    32693269#: plugin/Commands/SendVerificationEmail.php:71
    3270 #: plugin/Controllers/VerificationController.php:34
     3270#: plugin/Controllers/VerificationController.php:35
    32713271msgctxt "admin-text"
    32723272msgid "Resend Verification Request"
    … …  
    33583358msgstr ""
    33593359
    3360 #: plugin/Controllers/AdminController.php:309
    3361 #: plugin/Controllers/AdminController.php:327
    3362 #: plugin/Controllers/AdminController.php:340
     3360#: plugin/Controllers/AdminController.php:313
     3361#: plugin/Controllers/AdminController.php:332
     3362#: plugin/Controllers/AdminController.php:346
    33633363msgctxt "admin-text"
    33643364msgid "Nothing found."
     3365msgstr ""
     3366
     3367#: plugin/Controllers/AdminController.php:390
     3368msgctxt "admin-text"
     3369msgid "You do not have permission to search."
    33653370msgstr ""
    33663371
    … …  
    34943499msgstr ""
    34953500
    3496 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:18
     3501#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:19
    34973502msgctxt "admin-text"
    34983503msgid "Cannot create existing review."
    34993504msgstr ""
    35003505
    3501 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:22
     3506#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:23
    35023507msgctxt "admin-text"
    35033508msgid "Sorry, you are not allowed to create reviews as this user."
    35043509msgstr ""
    35053510
    3506 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:26
     3511#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:27
    35073512msgctxt "admin-text"
    35083513msgid "Sorry, you are not allowed to create reviews as this review author."
    35093514msgstr ""
    35103515
    3511 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:30
    3512 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:139
     3516#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:31
     3517#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:146
    35133518msgctxt "admin-text"
    35143519msgid "Sorry, you are not allowed to assign the provided terms."
    35153520msgstr ""
    35163521
    3517 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:45
    3518 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:68
    3519 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:121
     3522#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:46
     3523#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:69
     3524#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:122
    35203525msgctxt "admin-text"
    35213526msgid "Invalid review ID."
    35223527msgstr ""
    35233528
    3524 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:51
     3529#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:52
    35253530msgctxt "admin-text"
    35263531msgid "Sorry, you are not allowed to delete this review."
    35273532msgstr ""
    35283533
    3529 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:74
     3534#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:75
    35303535msgctxt "admin-text"
    35313536msgid "Sorry, you are not allowed to view this review."
    35323537msgstr ""
    35333538
    3534 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:81
    3535 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:127
     3539#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:82
     3540#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:128
    35363541msgctxt "admin-text"
    35373542msgid "Sorry, you are not allowed to edit this review."
    35383543msgstr ""
    35393544
    3540 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:97
     3545#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:98
    35413546msgctxt "admin-text"
    35423547msgid "Sorry, you do not have permission to access reviews."
    35433548msgstr ""
    35443549
    3545 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:104
     3550#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:105
    35463551msgctxt "admin-text"
    35473552msgid "Sorry, you are not allowed to edit reviews."
    35483553msgstr ""
    35493554
    3550 #: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:133
     3555#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:134
     3556msgctxt "admin-text"
     3557msgid "Sorry, you are not allowed to approve this review."
     3558msgstr ""
     3559
     3560#: plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php:140
    35513561msgctxt "admin-text"
    35523562msgid "Sorry, you are not allowed to update the review as this user."
    … …  
    35623572
    35633573#: plugin/Controllers/Api/Version1/RestRenderController.php:103
    3564 #: plugin/Controllers/VerificationController.php:96
     3574#: plugin/Controllers/VerificationController.php:105
    35653575msgid "Thank you, your review has been verified."
    35663576msgstr ""
    35673577
    35683578#: plugin/Controllers/Api/Version1/RestRenderController.php:104
    3569 #: plugin/Controllers/VerificationController.php:97
     3579#: plugin/Controllers/VerificationController.php:106
    35703580msgid "Thank you, your review has been verified and is awaiting approval."
    35713581msgstr ""
    … …  
    39233933
    39243934#. translators: %s: the review type
    3925 #: plugin/Controllers/EditorController.php:108
     3935#: plugin/Controllers/EditorController.php:109
    39263936#, php-format
    39273937msgctxt "admin-text"
    … …  
    41924202msgstr ""
    41934203
    4194 #: plugin/Controllers/ListTableController.php:272
     4204#: plugin/Controllers/ListTableController.php:270
    41954205msgctxt "admin-text"
    41964206msgid "Sorry, you are not allowed to respond to this review."
    41974207msgstr ""
    41984208
    4199 #: plugin/Controllers/ListTableController.php:275
     4209#: plugin/Controllers/ListTableController.php:273
    42004210msgctxt "admin-text"
    42014211msgid "Someone"
    … …  
    42034213
    42044214#. translators: %s: user's name
    4205 #: plugin/Controllers/ListTableController.php:281
     4215#: plugin/Controllers/ListTableController.php:279
    42064216#, php-format
    42074217msgctxt "admin-text"
    … …  
    43394349msgstr ""
    43404350
    4341 #: plugin/Controllers/ToolsController.php:366
     4351#: plugin/Controllers/ToolsController.php:314
     4352#: plugin/Controllers/ToolsController.php:332
     4353msgctxt "admin-text"
     4354msgid "You do not have permission to change the IP address detection."
     4355msgstr ""
     4356
     4357#: plugin/Controllers/ToolsController.php:380
    43424358msgctxt "admin-text"
    43434359msgid "reload the page"
    … …  
    43454361
    43464362#. translators: %s: a link with the text "reload the page"
    4347 #: plugin/Controllers/ToolsController.php:371
     4363#: plugin/Controllers/ToolsController.php:385
    43484364#, php-format
    43494365msgctxt "admin-text"
    … …  
    43524368
    43534369#. translators: %s: plugin name
    4354 #: plugin/Controllers/ToolsController.php:427
     4370#: plugin/Controllers/ToolsController.php:441
    43554371#, php-format
    43564372msgctxt "Site Reviews (admin-text)"
    … …  
    43584374msgstr ""
    43594375
    4360 #: plugin/Controllers/ToolsController.php:446
     4376#: plugin/Controllers/ToolsController.php:460
    43614377msgctxt "admin-text"
    43624378msgid "You do not have permission to rollback the plugin."
    43634379msgstr ""
    43644380
    4365 #: plugin/Controllers/ToolsController.php:451
     4381#: plugin/Controllers/ToolsController.php:465
    43664382#: views/pages/tools/general/rollback-plugin.php:21
    43674383msgctxt "admin-text"
    … …  
    44714487msgstr ""
    44724488
    4473 #: plugin/Controllers/VerificationController.php:35
     4489#: plugin/Controllers/VerificationController.php:36
    44744490msgctxt "admin-text"
    44754491msgid "Send Verification Request"
     4492msgstr ""
     4493
     4494#: plugin/Controllers/VerificationController.php:52
     4495msgctxt "admin-text"
     4496msgid "You do not have permission to request verification of this review."
    44764497msgstr ""
    44774498
    … …  
    45074528msgstr ""
    45084529
    4509 #: plugin/Database.php:237
     4530#: plugin/Database.php:231
    45104531msgctxt "admin-text"
    45114532msgid "(no title)"
    … …  
    64976518msgstr ""
    64986519
    6499 #: plugin/Modules/Html/ReviewForm.php:14
     6520#: plugin/Modules/Html/ReviewForm.php:15
    65006521msgid "Submit Review"
    65016522msgstr ""
    … …  
    66296650msgstr ""
    66306651
    6631 #: plugin/Modules/Validator/SignatureValidator.php:27
     6652#: plugin/Modules/Validator/SignatureValidator.php:28
    66326653#: views/pages/documentation/support/common-problems-and-solutions.php:89
    66336654msgid "This review cannot be submitted, please refresh the page and try again."
    … …  
    66606681
    66616682#. translators: %s: the plugin name
    6662 #: plugin/Notices/GatekeeperNotice.php:130
     6683#: plugin/Notices/GatekeeperNotice.php:133
    66636684#, php-format
    66646685msgctxt "admin-text"
    … …  
    66676688
    66686689#. translators: %s: the plugin name
    6669 #: plugin/Notices/GatekeeperNotice.php:146
     6690#: plugin/Notices/GatekeeperNotice.php:149
    66706691#, php-format
    66716692msgctxt "admin-text"
  • site-reviews/trunk/plugin/Api.php

    r3769299 r3803670  
    8181            $this->forget($transientKey);
    8282        } else {
    83             $result = get_site_transient($transientKey);
     83            $result = $args['cache'] ? get_site_transient($transientKey) : false;
    8484            if (!empty($result)) {
    8585                return new Response($result);
    … …  
    9797            $response = new Response($result);
    9898            if ($response->successful()) {
    99                 $this->remember($transientKey, $result, $args['expiration']);
     99                if ($args['cache']) {
     100                    $this->remember($transientKey, $result, $args['expiration']);
     101                }
    100102                if ($args['skip_when_down']) {
    101103                    $this->markReachable();
  • site-reviews/trunk/plugin/Commands/CreateReview.php

    r3777315 r3803670  
    6060        $this->request = $this->normalize($request); // IP address is set here
    6161        $this->setProperties(); // do this after setting the request
    62         $this->review = new Review($this->toArray(), $init = false);
     62        $this->review = new Review($this->toArray(), false); // don't init
    6363        $this->validation = new Arguments();
    6464        $this->custom = $this->custom();
    … …  
    239239            $request->set('date', '');
    240240            $request->set('date_gmt', '');
     241            $request->set('geolocation', '');
    241242            $request->set('is_pinned', false);
    242243            $request->set('is_verified', false);
    … …  
    244245            $request->set('response_by', 0);
    245246            $request->set('type', 'local');
     247            $request->set('verified', '');
     248            $request->set('verified_on', '');
     249            $request->set('verified_requested', '');
    246250        }
    247251        glsr()->action('review/request', $request);
  • site-reviews/trunk/plugin/Commands/FetchPagedReviews.php

    r3683434 r3803670  
    3333        return [
    3434            'max_num_pages' => $this->html->max_num_pages,
    35             'pagination' => $this->html->getPagination($wrap = false),
     35            'pagination' => $this->html->getPagination(false), // don't wrap
    3636            'reviews' => $this->html->getReviews(),
    3737        ];
  • site-reviews/trunk/plugin/Controllers/AdminController.php

    r3683434 r3803670  
    262262    public function searchAssignedPostsAjax(Request $request): void
    263263    {
     264        $this->checkSearchPermission();
    264265        $search = glsr(Sanitizer::class)->sanitizeText($request->search);
    265266        $results = glsr(Database::class)->searchAssignedPosts($search)->results();
    … …  
    274275    public function searchAssignedUsersAjax(Request $request): void
    275276    {
     277        $this->checkSearchPermission();
    276278        $search = glsr(Sanitizer::class)->sanitizeText($request->search);
    277279        $results = glsr(Database::class)->searchAssignedUsers($search)->results();
    … …  
    289291    public function searchAuthorsAjax(Request $request): void
    290292    {
     293        $this->checkSearchPermission();
    291294        $search = glsr(Sanitizer::class)->sanitizeText($request->search);
    292295        $results = glsr(Database::class)->searchUsers($search)->results();
    … …  
    304307    public function searchPostsAjax(Request $request): void
    305308    {
     309        $this->checkSearchPermission();
    306310        $search = glsr(Sanitizer::class)->sanitizeText($request->search);
    307311        $results = glsr(Database::class)->searchPosts($search)->render();
    … …  
    317321    public function searchStringsAjax(Request $request): void
    318322    {
     323        $this->checkSearchPermission();
    319324        $search = glsr(Sanitizer::class)->sanitizeText($request->search);
    320325        $exclude = Arr::consolidate($request->exclude);
    … …  
    335340    public function searchUsersAjax(Request $request): void
    336341    {
     342        $this->checkSearchPermission();
    337343        $search = glsr(Sanitizer::class)->sanitizeText($request->search);
    338344        $results = glsr(Database::class)->searchUsers($search)->render();
    … …  
    372378        $this->execute(new ToggleStatus($request))->sendJsonResponse();
    373379    }
     380
     381    /**
     382     * Refuse the request with an error notice unless the user can edit reviews.
     383     */
     384    protected function checkSearchPermission(): void
     385    {
     386        if (glsr()->can('edit_posts')) {
     387            return;
     388        }
     389        glsr(Notice::class)->addError(
     390            _x('You do not have permission to search.', 'admin-text', 'site-reviews')
     391        );
     392        wp_send_json_error([
     393            'notices' => glsr(Notice::class)->get(),
     394        ]);
     395    }
    374396}
  • site-reviews/trunk/plugin/Controllers/Api/Version1/Permissions/ReviewPermissions.php

    r3583819 r3803670  
    33namespace GeminiLabs\SiteReviews\Controllers\Api\Version1\Permissions;
    44
     5use GeminiLabs\SiteReviews\Defaults\UpdateReviewDefaults;
    56use GeminiLabs\SiteReviews\Helpers\Arr;
    67use GeminiLabs\SiteReviews\Review;
    … …  
    127128            $message = _x('Sorry, you are not allowed to edit this review.', 'admin-text', 'site-reviews');
    128129            return new \WP_Error('rest_cannot_edit', $message, [
     130                'status' => rest_authorization_required_code(),
     131            ]);
     132        }
     133        if ($this->is_approving($request, $review) && !glsr()->can('publish_post', $review->ID)) {
     134            $message = _x('Sorry, you are not allowed to approve this review.', 'admin-text', 'site-reviews');
     135            return new \WP_Error('rest_cannot_publish', $message, [
    129136                'status' => rest_authorization_required_code(),
    130137            ]);
    … …  
    183190        return true;
    184191    }
     192
     193    /**
     194     * Whether the update approves a review that is not approved yet.
     195     */
     196    protected function is_approving(\WP_REST_Request $request, Review $review): bool
     197    {
     198        $values = glsr(UpdateReviewDefaults::class)->restrict($request->get_params()); // the status comes from is_approved
     199        return 'publish' === Arr::get($values, 'status') && !$review->is_approved;
     200    }
    185201}
  • site-reviews/trunk/plugin/Controllers/Api/Version1/RestReviewController.php

    r3683434 r3803670  
    291291            ->buildReviewsHtml();
    292292        $response = rest_ensure_response([
    293             'pagination' => $html->getPagination($wrap = false),
     293            'pagination' => $html->getPagination(false), // don't wrap
    294294            'rendered' => $html->getReviews(),
    295295        ]);
  • site-reviews/trunk/plugin/Controllers/EditorController.php

    r3642444 r3803670  
    7373        }
    7474        $strings = glsr(UpdatedMessageDefaults::class)->defaults();
    75         $restored = filter_input(\INPUT_GET, 'revision');
    76         if ($revisionTitle = wp_post_revision_title(intval($restored), false)) {
     75        $restored = false;
     76        $revisionId = intval(filter_input(\INPUT_GET, 'revision'));
     77        if ($revisionTitle = wp_post_revision_title($revisionId, false)) {
    7778            $restored = sprintf($strings['restored'], $revisionTitle);
    7879        }
  • site-reviews/trunk/plugin/Controllers/ListTableController.php

    r3642444 r3803670  
    206206                unset($filters['type']);
    207207            }
    208             foreach ($filters as $key => &$value) {
    209                 $value = glsr($value)->title();
    210             }
     208            $filters = array_map(fn ($value) => glsr($value)->title(), $filters);
    211209            ksort($filters);
    212210            $setting = 'edit_'.glsr()->post_type.'_filters';
    … …  
    441439                    $clause['clauses'][] = "AND {$assignedTable}.{$column} = {$value}";
    442440                }
    443             } elseif (in_array($key, ['rating', 'terms', 'type'])) {
     441            } elseif (in_array($key, ['rating', 'terms'])) {
    444442                $clause['clauses'][] = "AND {$ratings}.{$key} = '{$value}'";
     443            } elseif ('type' === $key && array_key_exists($value, glsr()->retrieveAs('array', 'review_types'))) {
     444                $clause['clauses'][] = "AND {$ratings}.type = '{$value}'";
    445445            } elseif ('author' === $key && '0' === $value) {
    446446                // Filtering by the "author" URL parameter is automatically done
  • site-reviews/trunk/plugin/Controllers/MainController.php

    r3722268 r3803670  
    5252            $table = glsr($classname);
    5353            if ($table instanceof TableRatings) {
    54                 $ratingsTable[$table->name($prefixName = true)] = $table->tablename;
     54                $ratingsTable[$table->name(true)] = $table->tablename; // use the prefixed name
    5555                continue;
    5656            }
    57             $customTables[$table->name($prefixName = true)] = $table->tablename;
     57            $customTables[$table->name(true)] = $table->tablename; // use the prefixed name
    5858        }
    5959        return $customTables + $ratingsTable + $tables;
  • site-reviews/trunk/plugin/Controllers/PrivacyController.php

    r3769299 r3803670  
    152152        ", $email, glsr()->post_type, $this->perPage, ($page - 1) * $this->perPage);
    153153        $reviewIds = glsr(Database::class)->dbGetCol($sql);
    154         return empty($reviewIds) ? [] : glsr(Query::class)->reviews([], $reviewIds);
     154        return empty($reviewIds) ? [] : glsr(Query::class)->reviews(['order' => 'asc', 'orderby' => 'id'], $reviewIds);
    155155    }
    156156}
  • site-reviews/trunk/plugin/Controllers/ToolsController.php

    r3769299 r3803670  
    310310    public function ipAddressDetection(Request $request): void
    311311    {
     312        if (!glsr()->can(glsr()->getPermission('settings'))) {
     313            glsr(Notice::class)->addError(
     314                _x('You do not have permission to change the IP address detection.', 'admin-text', 'site-reviews')
     315            );
     316            return;
     317        }
    312318        if (wp_validate_boolean($request->get('alt', 0))) {
    313319            $this->execute(new DetectIpAddress());
    … …  
    322328    public function ipAddressDetectionAjax(Request $request): void
    323329    {
     330        if (!glsr()->can(glsr()->getPermission('settings'))) {
     331            glsr(Notice::class)->addError(
     332                _x('You do not have permission to change the IP address detection.', 'admin-text', 'site-reviews')
     333            );
     334            wp_send_json_error([
     335                'notices' => glsr(Notice::class)->get(),
     336            ]);
     337        }
    324338        $command = wp_validate_boolean($request->get('alt', 0))
    325339            ? $this->execute(new DetectIpAddress())
  • site-reviews/trunk/plugin/Controllers/VerificationController.php

    r3642444 r3803670  
    1414use GeminiLabs\SiteReviews\Modules\Encryption;
    1515use GeminiLabs\SiteReviews\Modules\Html\Template;
     16use GeminiLabs\SiteReviews\Modules\Notice;
    1617use GeminiLabs\SiteReviews\Request;
    1718use GeminiLabs\SiteReviews\Review;
    … …  
    4748    {
    4849        $review = glsr(ReviewManager::class)->get($request->cast('post_id', 'int'));
     50        if (!$review->isValid() || !glsr()->can('edit_post', $review->ID)) {
     51            glsr(Notice::class)->addError(
     52                _x('You do not have permission to request verification of this review.', 'admin-text', 'site-reviews')
     53            );
     54            wp_send_json_error([
     55                'notices' => glsr(Notice::class)->get(),
     56            ]);
     57        }
    4958        $pathPostId = $review->meta()->cast('_submitted._post_id', 'int');
    5059        $path = Cast::toString(wp_parse_url((string) get_permalink($pathPostId), \PHP_URL_PATH));
  • site-reviews/trunk/plugin/Database.php

    r3642444 r3803670  
    3131    public function beginTransaction(string $table): void
    3232    {
    33         if (glsr(Tables::class)->isSqlite()) {
     33        if (glsr(Tables::class)->isSqliteLegacy()) {
    3434            $this->dbQuery("BEGIN TRANSACTION;");
    35         } elseif (glsr(Tables::class)->isInnodb($table)) {
     35        } elseif (glsr(Tables::class)->isSqlite() || glsr(Tables::class)->isInnodb($table)) {
    3636            $this->dbQuery("START TRANSACTION;");
    3737        } elseif (glsr(Tables::class)->isMyisam($table)) {
    … …  
    159159        $fields = glsr(Query::class)->escFieldsForInsert(array_keys($data));
    160160        $values = glsr(Query::class)->escValuesForInsert($data);
    161         if (glsr(Tables::class)->isSqlite()) {
    162             $sql = glsr(Query::class)->sql("INSERT OR IGNORE INTO {$table} {$fields} VALUES {$values}");
    163         } else {
    164             $sql = glsr(Query::class)->sql("INSERT IGNORE INTO {$table} {$fields} VALUES {$values}");
    165         }
     161        $ignore = glsr(Tables::class)->isSqliteLegacy() ? 'OR IGNORE' : 'IGNORE';
     162        $sql = glsr(Query::class)->sql("INSERT {$ignore} INTO {$table} {$fields} VALUES {$values}");
    166163        return $this->dbQuery($sql);
    167164    }
    … …  
    186183        $fields = glsr(Query::class)->escFieldsForInsert($fields);
    187184        $values = implode(',', $data);
    188         if (glsr(Tables::class)->isSqlite()) {
    189             $sql = glsr(Query::class)->sql("INSERT OR IGNORE INTO {$table} {$fields} VALUES {$values}");
    190         } else {
    191             $sql = glsr(Query::class)->sql("INSERT IGNORE INTO {$table} {$fields} VALUES {$values}");
    192         }
     185        $ignore = glsr(Tables::class)->isSqliteLegacy() ? 'OR IGNORE' : 'IGNORE';
     186        $sql = glsr(Query::class)->sql("INSERT {$ignore} INTO {$table} {$fields} VALUES {$values}");
    193187        return $this->dbQuery($sql);
    194188    }
  • site-reviews/trunk/plugin/Database/OptionManager.php

    r3769299 r3803670  
    313313        $writes = [];
    314314        $versions = [];
    315         foreach (static::addons() as $slug => $addon) {
     315        foreach (static::addons() as $addon) {
    316316            if ($addon->hostedBy()) {
    317317                continue; // the host claims its whole subtree, hosted values included
    … …  
    382382    {
    383383        $option = get_option($path, $fallback);
    384         return Cast::to($cast, Helper::ifEmpty($option, $fallback, $strict = true));
     384        return Cast::to($cast, Helper::ifEmpty($option, $fallback, true)); // use strict comparison
    385385    }
    386386
    … …  
    393393    protected function compose(array $settings): array
    394394    {
    395         foreach (static::addons() as $slug => $addon) {
     395        foreach (static::addons() as $addon) {
    396396            if ($addon->hostedBy()) {
    397397                continue; // mounted by its host's whole subtree
  • site-reviews/trunk/plugin/Database/Query.php

    r3642444 r3803670  
    4444    public function ratings(array $args = []): array
    4545    {
    46         $this->setArgs($args, $unset = ['orderby']);
     46        $this->setArgs($args, ['orderby']); // unset orderby
    4747        $results = glsr(Database::class)->dbGetResults($this->queryRatings(), \ARRAY_A);
    4848        return $this->normalizeRatings($results);
    … …  
    5555            return [];
    5656        }
    57         $this->setArgs($args, $unset = ['orderby']);
     57        $this->setArgs($args, ['orderby']); // unset orderby
    5858        $results = glsr(Database::class)->dbGetResults($this->$method(), \ARRAY_A);
    5959        return $this->normalizeRatingsByAssignedId($results);
    … …  
    125125    public function totalReviews(array $args = [], array $reviews = []): int
    126126    {
    127         $this->setArgs($args, $unset = ['orderby']);
     127        $this->setArgs($args, ['orderby']); // unset orderby
    128128        if (empty($this->sqlLimit()) && !empty($reviews)) {
    129129            return count($reviews);
  • site-reviews/trunk/plugin/Database/RatingManager.php

    r3474155 r3803670  
    2222        $metaTable = sprintf('%smeta', $metaGroup);
    2323        $ratings = glsr(Query::class)->ratingsFor($metaTable, $args);
    24         foreach ($ratings as $id => &$group) {
    25             $group = $this->reduce($group, $args);
    26         }
     24        $ratings = array_map(fn ($group) => $this->reduce($group, $args), $ratings);
    2725        return glsr()->filterArray('ratings/grouped', $ratings, $metaGroup, $args);
    2826    }
  • site-reviews/trunk/plugin/Database/Sql.php

    r3642444 r3803670  
    284284            $this->join('assigned_posts', $this->joinKeyword()),
    285285            $this->args['assigned_posts'],
    286             $prepare = false
     286            false // don't prepare
    287287        );
    288288    }
    … …  
    304304            $this->join('assigned_terms', $this->joinKeyword()),
    305305            $this->args['assigned_terms'],
    306             $prepare = false
     306            false // don't prepare
    307307        );
    308308    }
    … …  
    313313            $this->join('assigned_users', $this->joinKeyword()),
    314314            $this->args['assigned_users'],
    315             $prepare = false
     315            false // don't prepare
    316316        );
    317317    }
    … …  
    322322            $this->join('posts', 'INNER JOIN'),
    323323            $this->args['content'],
    324             $prepare = false
     324            false // don't prepare
    325325        );
    326326    }
    … …  
    331331            $this->join('posts', 'INNER JOIN'),
    332332            array_filter($this->args['date']),
    333             $prepare = false
     333            false // don't prepare
    334334        );
    335335    }
    … …  
    340340            $this->join('posts', 'INNER JOIN'),
    341341            $this->args['user__in'],
    342             $prepare = false
     342            false // don't prepare
    343343        );
    344344    }
    … …  
    349349            $this->join('posts', 'INNER JOIN'),
    350350            $this->args['user__not_in'],
    351             $prepare = false
     351            false // don't prepare
    352352        );
    353353    }
  • site-reviews/trunk/plugin/Database/Tables.php

    r3683434 r3803670  
    124124    {
    125125        return 'sqlite' === $this->engine;
     126    }
     127
     128    /**
     129     * If SQLite Database Integration plugin is < v3.
     130     */
     131    public function isSqliteLegacy(): bool
     132    {
     133        return $this->isSqlite()
     134            && is_a($this->db->dbh, 'WP_SQLite_Translator'); // @phpstan-ignore-line
    126135    }
    127136
  • site-reviews/trunk/plugin/Defaults/ApiDefaults.php

    r3769299 r3803670  
    1313    public array $casts = [
    1414        'blocking' => 'bool',
     15        'cache' => 'bool',
    1516        'compress' => 'bool',
    1617        'cookies' => 'array',
    … …  
    5051            'blocking' => true, // Whether the calling code requires the result of the request.
    5152            'body' => null,
     53            'cache' => true, // if the response is cached
    5254            'compress' => false, // Whether to compress the $body when sending the request.
    5355            'cookies' => [],
  • site-reviews/trunk/plugin/Helpers/Arr.php

    r3642444 r3803670  
    2525        if (is_object($value)) {
    2626            $values = get_object_vars($value);
    27             $value = Helper::ifEmpty($values, (array) $value, $strict = true);
     27            $value = Helper::ifEmpty($values, (array) $value, true); // use strict comparison
    2828        }
    2929        return is_array($value) ? $value : $fallback;
  • site-reviews/trunk/plugin/Helpers/Text.php

    r3777315 r3803670  
    124124        }
    125125        $wordCount = 0;
    126         foreach ($iterator->getPartsIterator() as $part) {
     126        while (\IntlBreakIterator::DONE !== $iterator->next()) {
    127127            if (\IntlBreakIterator::WORD_NONE !== $iterator->getRuleStatus()) {
    128128                ++$wordCount;
    … …  
    245245        }
    246246        $iterator = \IntlRuleBasedBreakIterator::createWordInstance('');
     247        if (!$iterator instanceof \IntlRuleBasedBreakIterator) { // has getRuleStatus()
     248            return null;
     249        }
    247250        $normalizedText = \Normalizer::normalize($text);
    248251        if (empty($normalizedText)) {
    … …  
    258261    {
    259262        $map = [];
     263        $text = str_replace('⍈', '&#x2348;', $text); // a ⍈ in the text is not a placeholder
    260264        $result = preg_replace_callback('|<([a-z]+)[^>]*?>.*?</\\1>|siu', function ($matches) use (&$map) {
    261265            $map[] = $matches[0];
  • site-reviews/trunk/plugin/Integrations/MultilingualPress/Defaults/SyncReviewDefaults.php

    r3474155 r3803670  
    33namespace GeminiLabs\SiteReviews\Integrations\MultilingualPress\Defaults;
    44
     5use GeminiLabs\SiteReviews\Defaults\CustomFieldsDefaults;
    56use GeminiLabs\SiteReviews\Defaults\UpdateReviewDefaults;
    67
    … …  
    3940        'author' => 'name',
    4041    ];
     42
     43    /**
     44     * Normalize provided values, this always runs first.
     45     */
     46    protected function normalize(array $values = []): array
     47    {
     48        $customKeys = array_keys(glsr(CustomFieldsDefaults::class)->call('merge', $values));
     49        $this->guarded = array_values(array_unique(array_merge($this->guarded, $customKeys))); // guard all custom keys
     50        return parent::normalize($values);
     51    }
    4152}
  • site-reviews/trunk/plugin/Integrations/MultilingualPress/RelationSaveHelper.php

    r3518858 r3803670  
    88use GeminiLabs\SiteReviews\Defaults\RatingDefaults;
    99use GeminiLabs\SiteReviews\Helpers\Arr;
    10 use GeminiLabs\SiteReviews\Integrations\MultilingualPress\Defaults\SyncReviewDefaults;
    1110use GeminiLabs\SiteReviews\Integrations\MultilingualPress\MetaboxFields\AssignedPostsField;
    1211use GeminiLabs\SiteReviews\Integrations\MultilingualPress\MetaboxFields\AssignedUsersField;
    … …  
    201200    }
    202201
     202    /**
     203     * Write the values on the remote review with the assigned posts and users mapped to the remote site.
     204     *
     205     * @param array $data The guarded values (ReviewCopier::syncData) with the source site's assigned ids
     206     */
    203207    public function syncUpdate(array $data, bool $force = false): void
    204208    {
    … …  
    206210            return;
    207211        }
    208         $guarded = glsr(SyncReviewDefaults::class)->merge($data);
     212        // Not merged here as this runs on the remote site.
     213        $values = $data;
     214        unset($values['assigned_posts'], $values['assigned_users']);
    209215        if ($force || $this->canSyncAssignment(AssignedPostsField::FIELD_COPY_ASSIGNED_POSTS)) {
    210216            $sourcePostIds = Arr::consolidate($data['assigned_posts'] ?? []);
    211             $guarded['assigned_posts'] = $this->remoteAssignedPosts($sourcePostIds);
     217            $values['assigned_posts'] = $this->remoteAssignedPosts($sourcePostIds);
    212218        }
    213219        if ($force || $this->canSyncAssignment(AssignedUsersField::FIELD_COPY_ASSIGNED_USERS)) {
    214220            $sourceUserIds = Arr::consolidate($data['assigned_users'] ?? []);
    215             $guarded['assigned_users'] = $this->remoteAssignedUsers($sourceUserIds);
    216         }
    217         $originalSiteId = $this->maybeSwitchSite($this->context->remoteSiteId());
    218         glsr_update_review($this->context->remotePostId(), $guarded);
     221            $values['assigned_users'] = $this->remoteAssignedUsers($sourceUserIds);
     222        }
     223        $originalSiteId = $this->maybeSwitchSite($this->context->remoteSiteId());
     224        glsr_update_review($this->context->remotePostId(), $values);
    219225        $this->syncAssignedTerms();
    220226        $this->maybeRestoreSite($originalSiteId);
  • site-reviews/trunk/plugin/Integrations/MultilingualPress/ReviewCopier.php

    r3637056 r3803670  
    33namespace GeminiLabs\SiteReviews\Integrations\MultilingualPress;
    44
     5use GeminiLabs\SiteReviews\Integrations\MultilingualPress\Defaults\SyncReviewDefaults;
     6use GeminiLabs\SiteReviews\Review;
    57use Inpsyde\MultilingualPress\Framework\Api\ContentRelations;
    68use Inpsyde\MultilingualPress\Framework\SwitchSiteTrait;
    … …  
    9193    public function sync(): void
    9294    {
    93         $review = glsr_get_review($this->sourcePostId);
    94         $data = wp_parse_args($review->toArray(), $review->custom()->toArray());
     95        $data = $this->syncData(glsr_get_review($this->sourcePostId));
    9596        $this->run(function ($context) use ($data) {
    9697            $helper = new RelationSaveHelper($context);
    9798            $helper->syncUpdate($data, true);
    9899        });
     100    }
     101
     102    public function syncData(Review $review): array
     103    {
     104        $data = wp_parse_args($review->toArray(), $review->custom()->toArray());
     105        // Guarded on the source site: the site-reviews/defaults/sync-review filter reads the review's form here.
     106        $data = glsr(SyncReviewDefaults::class)->merge($data);
     107        $data['assigned_posts'] = $review->assigned_posts; // the source ids mapped by syncUpdate() on each remote site
     108        $data['assigned_users'] = $review->assigned_users;
     109        return $data;
    99110    }
    100111
  • site-reviews/trunk/plugin/Integrations/WPLoyalty/Controller.php

    r3637056 r3803670  
    4444            ]);
    4545            $messages = [$field->after];
    46             foreach ($rewardsList as $action => $rewards) {
    47                 foreach ($rewards as $key => $reward) {
     46            foreach ($rewardsList as $rewards) {
     47                foreach ($rewards as $reward) {
    4848                    $messages[] = $reward['messages'] ?? '';
    4949                }
  • site-reviews/trunk/plugin/Integrations/WooCommerce/Controllers/RestApi/ProductReviewsController.php

    r3642444 r3803670  
    226226        $postArgs = $this->prepare_item_for_update($request);
    227227        $postArgs['ID'] = $review->ID;
    228         $updatePost = wp_update_post($postArgs, $wperror = true);
     228        $updatePost = wp_update_post($postArgs, true); // return a WP_Error on failure
    229229        if (is_wp_error($updatePost)) {
    230230            return new \WP_Error('woocommerce_rest_comment_failed_edit', 'Updating review failed.', ['status' => 500]);
  • site-reviews/trunk/plugin/Modules/Html/Form.php

    r3683434 r3803670  
    500500    protected function normalizeFieldValue(FieldContract $field): void
    501501    {
     502        if ($this->session->failed && 'hidden' === $field->original_type) {
     503            return; // hidden fields are signed
     504        }
    502505        if ($field->isChoiceField()) {
    503506            $value = Cast::toArray($this->session->values[$field->original_name] ?? []);
    … …  
    511514    }
    512515
     516    /**
     517     * @return array<string, string|array{hidden: string[], visible: int}>
     518     */
     519    protected function signedValues(): array
     520    {
     521        $lists = $values = [];
     522        foreach ($this->hidden() as $field) {
     523            $values[$field->original_name] = $field->value;
     524            if (str_ends_with($field->name, '[]')) { // PHP reads these fields as a list
     525                $lists[$field->original_name]['hidden'][] = Cast::toString($field->value);
     526            }
     527        }
     528        $visibleFields = $this->visible();
     529        foreach (array_keys($lists) as $name) {
     530            $lists[$name]['visible'] = count(
     531                array_filter($visibleFields, fn ($field) => $name === $field->original_name)
     532            );
     533        }
     534        $values = array_merge($values, $lists);
     535        return $this->app()->filterArray("{$this->formName()}/signature/values", $values, $this);
     536    }
     537
    513538    protected function signForm(): void
    514539    {
    515         $hidden = $this->hidden();
    516         $values = [];
    517         foreach ($hidden as $field) {
    518             $values[$field->original_name] = $field->value;
    519         }
    520         $values = $this->app()->filterArray("{$this->formName()}/signature/values", $values, $this);
     540        $values = $this->signedValues();
    521541        $signatureField = $this->field('form_signature', [
    522542            'type' => 'hidden',
    523543            'value' => glsr(Encryption::class)->encrypt(maybe_serialize($values)),
    524544        ]);
    525         $fields = array_values(array_merge($hidden, [$signatureField], $this->visible()));
     545        $fields = array_values(array_merge($this->hidden(), [$signatureField], $this->visible()));
    526546        $this->exchangeArray($fields);
    527547    }
  • site-reviews/trunk/plugin/Modules/Html/ReviewForm.php

    r3683434 r3803670  
    66use GeminiLabs\SiteReviews\Helpers\Arr;
    77use GeminiLabs\SiteReviews\Helpers\Cast;
     8use GeminiLabs\SiteReviews\Request;
    89
    910class ReviewForm extends Form
    … …  
    2627    {
    2728        do_action('litespeed_nonce', 'submit-review'); // @litespeedcache
    28         $referer = glsr()->filterString('review-form/referer', wp_get_referer());
     29        $referer = glsr()->filterString('review-form/referer', $this->referer());
    2930        $config = [
    3031            '_action' => 'submit-review',
    … …  
    8889        return $fields;
    8990    }
     91
     92    /**
     93     * The page the visitor came from.
     94     */
     95    protected function referer(): string
     96    {
     97        if (glsr()->session()->cast('form_invalid', 'bool')) {
     98            $submitted = new Request(glsr()->session()->array('form_values'));
     99            if ($referer = Cast::toString($submitted->signedValues()['_referer'] ?? '')) {
     100                return $referer;
     101            }
     102        }
     103        return Cast::toString(wp_get_referer());
     104    }
    90105}
  • site-reviews/trunk/plugin/Modules/Html/SettingForm.php

    r3722268 r3803670  
    141141            return; // only a setting field has dependencies
    142142        }
    143         $isHidden = false;
    144143        foreach (Arr::consolidate($field->depends_on) as $path => $expectedValue) {
    145144            $default = Arr::get(glsr()->defaults(), $path);
  • site-reviews/trunk/plugin/Modules/Paginate.php

    r3769299 r3803670  
    1919        $parts = explode('?', $base);
    2020        if (wp_is_serving_rest_request() || !NormalizePaginationArgs::pagedRequest()->isEmpty()) {
    21             $parts = [$parts[0]]; // this is an AJAX request
     21            $parts = []; // this is an AJAX request
    2222        }
    2323        if ($parts[1] ?? false) {
  • site-reviews/trunk/plugin/Modules/Sanitizers/ArraySanitizer.php

    r2878258 r3803670  
    33namespace GeminiLabs\SiteReviews\Modules\Sanitizers;
    44
     5use GeminiLabs\SiteReviews\Helpers\Arr;
    56use GeminiLabs\SiteReviews\Helpers\Cast;
    67
    … …  
    910    abstract public function run(): array;
    1011
     12    /**
     13     * The values, with a comma-separated string among them read as several values.
     14     */
     15    protected function splitValue(): array
     16    {
     17        $values = [];
     18        foreach ($this->value() as $value) {
     19            $values[] = is_string($value) ? Arr::convertFromString($value) : [$value];
     20        }
     21        return array_merge([], ...$values);
     22    }
     23
    1124    protected function value(): array
    1225    {
  • site-reviews/trunk/plugin/Modules/Sanitizers/SanitizeEmails.php

    r2884378 r3803670  
    88    {
    99        $emails = explode(',', $this->value());
    10         foreach ($emails as &$email) {
    11             $email = sanitize_email($email);
    12         }
     10        $emails = array_map(fn ($email) => sanitize_email($email), $emails);
    1311        return implode(',', array_filter($emails));
    1412    }
  • site-reviews/trunk/plugin/Modules/Sanitizers/SanitizePostIds.php

    r2878258 r3803670  
    1111    {
    1212        return Arr::uniqueInt(
    13             array_map([Helper::class, 'getPostId'], $this->value())
     13            array_map([Helper::class, 'getPostId'], $this->splitValue())
    1414        );
    1515    }
  • site-reviews/trunk/plugin/Modules/Sanitizers/SanitizeTermIds.php

    r2878258 r3803670  
    1111    {
    1212        return Arr::uniqueInt(
    13             array_map([Helper::class, 'getTermTaxonomyId'], $this->value())
     13            array_map([Helper::class, 'getTermTaxonomyId'], $this->splitValue())
    1414        );
    1515    }
  • site-reviews/trunk/plugin/Modules/Sanitizers/SanitizeUserIds.php

    r2878258 r3803670  
    1111    {
    1212        return Arr::uniqueInt(
    13             array_map([Helper::class, 'getUserId'], $this->value())
     13            array_map([Helper::class, 'getUserId'], $this->splitValue())
    1414        );
    1515    }
  • site-reviews/trunk/plugin/Modules/Schema.php

    r3642444 r3803670  
    114114    {
    115115        if ($schema = $this->retrieve()) {
    116             $json = (string) wp_json_encode($schema, \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES);
     116            $json = (string) wp_json_encode($schema, \JSON_HEX_AMP | \JSON_HEX_TAG | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
    117117            printf('<script type="application/ld+json" class="%s-schema">%s</script>', glsr()->id, $json);
    118118        }
    … …  
    209209            return $this->getSchemaOptionDefault($setting, $fallback);
    210210        }
    211         return Helper::ifEmpty($setting, $fallback, $strict = true);
     211        return Helper::ifEmpty($setting, $fallback, true); // use strict comparison
    212212    }
    213213
  • site-reviews/trunk/plugin/Modules/SystemInfo.php

    r3642444 r3803670  
    117117            ];
    118118        }
    119         $engines = glsr(Tables::class)->tableEngines($removePrefix = true);
     119        $engines = glsr(Tables::class)->tableEngines(true); // remove the prefix
    120120        foreach ($engines as $engine => $tables) {
    121121            $engines[$engine] = sprintf('%s (%s)', $engine, implode('|', $tables));
  • site-reviews/trunk/plugin/Modules/Validator.php

    r3474155 r3803670  
    172172     * @param mixed $value
    173173     */
    174     protected function getSize(string $attribute, $value): int
     174    protected function getSize(string $attribute, $value): float
    175175    {
    176176        $hasNumeric = $this->hasRule($attribute, $this->numericRules);
    177177        if (is_numeric($value) && $hasNumeric) {
    178             return (int) $value;
     178            return (float) $value;
    179179        }
    180180        if (is_array($value)) {
  • site-reviews/trunk/plugin/Modules/Validator/CaptchaValidatorAbstract.php

    r3488530 r3803670  
    8484        return [
    8585            'body' => $body,
    86             'force' => true,
     86            'cache' => false,
    8787        ];
    8888    }
  • site-reviews/trunk/plugin/Modules/Validator/FriendlycaptchaV2Validator.php

    r3488530 r3803670  
    6565    {
    6666        return [
    67             'force' => true,
     67            'body' => wp_json_encode($body),
     68            'cache' => false,
    6869            'headers' => [
    6970                'Content-Type' => 'application/json',
    7071                'X-API-Key' => $this->siteSecret(),
    7172            ],
    72             'body' => wp_json_encode($body),
    7373        ];
    7474    }
  • site-reviews/trunk/plugin/Modules/Validator/ProcaptchaValidator.php

    r3637056 r3803670  
    5555    {
    5656        return [
    57             'force' => true,
     57            'body' => wp_json_encode($body),
     58            'cache' => false,
    5859            'headers' => ['Content-Type' => 'application/json'],
    59             'body' => wp_json_encode($body),
    6060        ];
    6161    }
  • site-reviews/trunk/plugin/Modules/Validator/SignatureValidator.php

    r3683434 r3803670  
    33namespace GeminiLabs\SiteReviews\Modules\Validator;
    44
     5use GeminiLabs\SiteReviews\Helpers\Arr;
    56use GeminiLabs\SiteReviews\Helpers\Cast;
    67
    … …  
    1011    {
    1112        $isValid = true;
    12         $values = $this->request->signedValues(['form_id' => '']);
    13         foreach ($values as $key => $value) {
    14             if (Cast::toString($value) !== $this->request->cast($key, 'string')) {
     13        $signedValues = $this->request->signedValues(['form_id' => '']);
     14        foreach ($signedValues as $key => $signedValue) {
     15            if ($this->hasValueChanged($signedValue, $this->request->get($key))) {
    1516                $isValid = false;
    16                 glsr_log()->debug($values);
     17                glsr_log()->debug($signedValues);
    1718                break;
    1819            }
    1920        }
    20         return glsr()->filterBool('validate/signature', $isValid, $values, $this->request);
     21        return glsr()->filterBool('validate/signature', $isValid, $signedValues, $this->request);
    2122    }
    2223
    … …  
    3031        }
    3132    }
     33
     34    /**
     35     * @param string|array{hidden?: string[], visible?: int} $signedValue
     36     * @param mixed                                          $requestValue
     37     */
     38    protected function hasValueChanged($signedValue, $requestValue): bool
     39    {
     40        if (!is_array($signedValue)) {
     41            return is_array($requestValue) || Cast::toString($signedValue) !== Cast::toString($requestValue);
     42        }
     43        if (!Arr::isIndexedAndFlat($requestValue)) {
     44            return true;
     45        }
     46        $requestValue = array_map([Cast::class, 'toString'], $requestValue);
     47        foreach (Arr::consolidate($signedValue['hidden'] ?? []) as $hidden) {
     48            $index = array_search(Cast::toString($hidden), $requestValue, true);
     49            if (false === $index) {
     50                return true;
     51            }
     52            unset($requestValue[$index]);
     53        }
     54        return count($requestValue) > Cast::toInt($signedValue['visible'] ?? 0);
     55    }
    3256}
  • site-reviews/trunk/plugin/Modules/Validator/ValidationRules.php

    r3642444 r3803670  
    162162     * @param mixed $value
    163163     */
    164     abstract protected function getSize(string $attribute, $value): int;
     164    abstract protected function getSize(string $attribute, $value): float;
    165165
    166166    /**
    … …  
    173173        }
    174174        return preg_replace_callback('/(%s)/', function () use (&$parameters) {
    175             foreach ($parameters as $key => $value) {
    176                 return array_shift($parameters);
    177             }
     175            return array_shift($parameters);
    178176        }, $message);
    179177    }
  • site-reviews/trunk/plugin/Notices/GatekeeperNotice.php

    r3642444 r3803670  
    121121            return '';
    122122        }
     123        if (!str_starts_with($data['plugin_uri'], 'https://wordpress.org/plugins/')) {
     124            return '';
     125        }
    123126        return $this->pluginAction([
    124127            'action' => 'install-plugin',
    … …  
    126129            'name' => $data['name'],
    127130            'nonce_prefix' => 'install-plugin_',
    128             'plugin' => $data['textdomain'],
     131            'plugin' => dirname($data['plugin']), // wordpress.org names a plugin's folder after its slug
    129132            /* translators: %s: the plugin name */
    130133            'text' => _x('Install %s', 'admin-text', 'site-reviews'),
    … …  
    183186    {
    184187        $links = [];
    185         foreach ($errors as $plugin => $data) {
     188        foreach ($errors as $data) {
    186189            $links[] = sprintf('<span class="plugin-%s"><a href="%s">%s</a></span>',
    187190                $data['textdomain'],
  • site-reviews/trunk/readme.txt

    r3777315 r3803670  
    44Tags: reviews, ratings, testimonials, product reviews, business reviews
    55Tested up to: 7.1
    6 Stable tag: 8.3.7
     6Stable tag: 8.3.8
    77License: GPLv3
    88License URI: https://www.gnu.org/licenses/gpl-3.0.html
    … …  
    211211
    212212== Changelog ==
     213
     214= 8.3.8 (2026-11-15) =
     215
     216- Fixed a security issue in review excerpts
     217- Fixed a security issue in the notice shown after restoring a revision
     218- Fixed a security issue in the review type filter
     219- Fixed a security issue in the schema
     220- Fixed captcha checks slowly filling up the database
     221- Fixed forms with more than one Assigned Pages, Categories or Users field not submitting
     222- Fixed hidden form fields changing after a failed submission
     223- Fixed MultilingualPress translations losing their custom field values when the original review is edited
     224- Fixed number fields ignoring decimals in their minimum and maximum
     225- Fixed reviews not saving with version 3 of the SQLite Database Integration plugin
     226- Fixed some assigned pages, categories or users not being saved with a review
     227- Fixed the admin search permission check
     228- Fixed the Install button in the missing plugin notice
     229- Fixed the IP address detection tool permission check
     230- Fixed the review approval permission check
     231- Fixed the validation of submitted reviews
     232- Fixed the verification email permission check
    213233
    214234= 8.3.7 (2026-10-31) =
  • site-reviews/trunk/site-reviews.php

    r3777315 r3803670  
    88 * Plugin URI:           https://wordpress.org/plugins/site-reviews
    99 * Description:          Receive and display reviews on your website
    10  * Version:              8.3.7
     10 * Version:              8.3.8
    1111 * Author:               Paul Ryley
    1212 * Author URI:           https://site-reviews.com
Note: See TracChangeset for help on using the changeset viewer.