From 563e3430e08735cdfbd230392e8cf3b9008b2909 Mon Sep 17 00:00:00 2001 From: "Matthew M. Emma" Date: Fri, 7 Aug 2026 17:21:44 -0700 Subject: [PATCH] Fix four defects found by real-WordPress integration testing Installed the v2.0.0 release ZIP into a fresh WordPress 7.0.3 with a mocked Procore API and exercised every surface. Four defects surfaced that the unit suite structurally cannot catch, because it shims the WordPress functions involved. Pagination was sent to endpoints that have none. Every request carried page=1, including /projects/{id} and /me. Beyond being meaningless on the wire, it varied the cache key for what was the same request. Pagination now applies only where the registry marks an endpoint paginated, and an explicitly requested page is still honoured. The REST proxy ignored the configured default company. The route declares company_id as an integer defaulting to 0, and the client treated a supplied zero as a deliberate choice rather than an omission, so the site default never applied and every proxied request failed with a missing-company error. A zero identifier now falls back to the configured default, which also makes the proxy behave like shortcodes. Translations loaded before init. Shortcode definitions carry translated titles and were built on plugins_loaded, so WordPress 6.7+ emitted a _load_textdomain_just_in_time notice and silently dropped translations. Registration is deferred to init, where add_shortcode belongs anyway. WP-CLI subcommands did not match the documentation. They registered as cache_clear, cache_warm and reset_token because WP-CLI derives names from methods and does not convert underscores. Added @subcommand annotations so the documented hyphenated forms work. Regression tests added for the two API-layer defects. The other two are registration-time behaviours that only a real WordPress exhibits. Verified after the fixes, in the live install: all 24 shortcode invocations render, no PHP notices or warnings, the token request goes to login.procore.com, single-record URLs carry no pagination, caching suppresses the second HTTP call, a simulated outage serves stale cache, the REST proxy returns records, all six admin tabs render without leaking the secret or token, the 1.x migration re-encrypts the secret and discards the stale token, and uninstall removes all 10 options. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 29 +++ languages/procore-connect.pot | 194 +++++++++--------- procore-connect.php | 4 +- readme.txt | 14 +- src/Api/Client.php | 24 ++- src/Cli/Commands.php | 6 + src/Frontend/Shortcodes/AbstractShortcode.php | 19 +- src/Frontend/Shortcodes/Registrar.php | 16 +- tests/unit/ClientTest.php | 81 ++++++++ 9 files changed, 275 insertions(+), 112 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb69693..03b0d3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,34 @@ All notable changes to Procore Connect are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.0.1] — 2026-08-08 + +Four defects found by running the plugin inside a real WordPress install +against a mocked Procore API — a surface the unit tests, which shim WordPress +functions, cannot reach. + +### Fixed + +- **Pagination parameters were sent to single-record endpoints.** Every request + carried `page=1`, including `/projects/{id}` and `/me`, which have no pages. + It was meaningless noise on the wire and it varied the cache key for what was + the same request. Pagination is now applied only to endpoints the registry + marks as paginated. +- **The REST proxy ignored the configured default company.** The route always + sends `company_id` as an integer defaulting to `0`, and the client treated a + supplied zero as a deliberate choice rather than an omission, so the site + default never applied and every request failed with a missing-company error. + A zero identifier now falls back to the configured default. +- **Translations were loaded before `init`.** Shortcode definitions carry + translated titles and were built on `plugins_loaded`, which triggers + WordPress 6.7's `_load_textdomain_just_in_time` notice and silently drops + translations. Shortcode registration is now deferred to `init`. +- **WP-CLI subcommands did not match the documentation.** They were registered + as `cache_clear`, `cache_warm` and `reset_token`; the documentation + advertised the hyphenated forms. Added `@subcommand` annotations so + `wp procore-connect cache-clear`, `cache-warm` and `reset-token` work as + documented. + ## [2.0.0] — 2026-08-07 A complete rewrite. Version 1.x could not authenticate against the live Procore API, so @@ -124,5 +152,6 @@ every install was non-functional regardless of configuration. - Initial release. +[2.0.1]: https://github.com/ibuilder/ProcoreWP/releases/tag/v2.0.1 [2.0.0]: https://github.com/ibuilder/ProcoreWP/releases/tag/v2.0.0 [1.0.0]: https://github.com/ibuilder/ProcoreWP/releases/tag/v1.0.0 diff --git a/languages/procore-connect.pot b/languages/procore-connect.pot index f3383b8..077639d 100644 --- a/languages/procore-connect.pot +++ b/languages/procore-connect.pot @@ -65,11 +65,11 @@ msgstr "" msgid "A service account keeps working when staff change and is installed by a Procore company administrator from the App Marketplace. A user connection inherits one person's permissions and breaks if their access is revoked." msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:234 +#: src/Frontend/Shortcodes/Registrar.php:248 msgid "A single allow-listed field from a project record." msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:67 +#: src/Frontend/Shortcodes/Registrar.php:81 msgid "A table of Procore projects for a company." msgstr "" @@ -91,8 +91,8 @@ msgstr "" msgid "Added inline after the plugin stylesheet. This survives plugin updates — editing files inside the plugin directory does not." msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:112 -#: src/Frontend/Shortcodes/Registrar.php:488 +#: src/Frontend/Shortcodes/Registrar.php:126 +#: src/Frontend/Shortcodes/Registrar.php:502 msgid "Address" msgstr "" @@ -160,11 +160,11 @@ msgstr "" msgid "Change order packages" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:388 +#: src/Frontend/Shortcodes/Registrar.php:402 msgid "Change order packages raised on a project." msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:387 +#: src/Frontend/Shortcodes/Registrar.php:401 msgid "Change orders" msgstr "" @@ -196,12 +196,12 @@ msgstr "" msgid "Companies" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:450 +#: src/Frontend/Shortcodes/Registrar.php:464 msgid "Companies in the Procore company directory." msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:171 -#: src/Frontend/Shortcodes/Registrar.php:454 +#: src/Frontend/Shortcodes/Registrar.php:185 +#: src/Frontend/Shortcodes/Registrar.php:468 msgid "Company" msgstr "" @@ -232,11 +232,11 @@ msgstr "" msgid "Company offices" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:437 +#: src/Frontend/Shortcodes/Registrar.php:451 msgid "Complete" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:142 +#: src/Frontend/Shortcodes/Registrar.php:156 msgid "Completion date" msgstr "" @@ -317,11 +317,11 @@ msgstr "" msgid "Daily construction report logs" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:364 +#: src/Frontend/Shortcodes/Registrar.php:378 msgid "Daily construction report logs for a project." msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:363 +#: src/Frontend/Shortcodes/Registrar.php:377 msgid "Daily logs" msgstr "" @@ -329,7 +329,7 @@ msgstr "" msgid "Database transients" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:368 +#: src/Frontend/Shortcodes/Registrar.php:382 msgid "Date" msgstr "" @@ -356,13 +356,13 @@ msgstr "" msgid "Depends on the endpoint attribute" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:201 -#: src/Frontend/Shortcodes/Registrar.php:220 -#: src/Frontend/Shortcodes/Registrar.php:314 +#: src/Frontend/Shortcodes/Registrar.php:215 +#: src/Frontend/Shortcodes/Registrar.php:234 +#: src/Frontend/Shortcodes/Registrar.php:328 msgid "Description" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:108 +#: src/Frontend/Shortcodes/Registrar.php:122 msgid "Detail panel for a single Procore project." msgstr "" @@ -394,7 +394,7 @@ msgstr "" msgid "Drawing areas" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:192 +#: src/Frontend/Shortcodes/Registrar.php:206 msgid "Drawing areas published for a project." msgstr "" @@ -402,7 +402,7 @@ msgstr "" msgid "Drawing revisions" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:191 +#: src/Frontend/Shortcodes/Registrar.php:205 msgid "Drawings" msgstr "" @@ -411,10 +411,10 @@ msgstr "" msgid "Drawings: Read Only" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:264 -#: src/Frontend/Shortcodes/Registrar.php:294 -#: src/Frontend/Shortcodes/Registrar.php:324 -#: src/Frontend/Shortcodes/Registrar.php:354 +#: src/Frontend/Shortcodes/Registrar.php:278 +#: src/Frontend/Shortcodes/Registrar.php:308 +#: src/Frontend/Shortcodes/Registrar.php:338 +#: src/Frontend/Shortcodes/Registrar.php:368 msgid "Due" msgstr "" @@ -422,7 +422,7 @@ msgstr "" msgid "Each environment needs its own credentials; tokens are not shared between them." msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:176 +#: src/Frontend/Shortcodes/Registrar.php:190 msgid "Email" msgstr "" @@ -459,7 +459,7 @@ msgstr "" msgid "Expose cached Procore data at /wp-json/procore-connect/v1/" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:432 +#: src/Frontend/Shortcodes/Registrar.php:446 msgid "Finish" msgstr "" @@ -467,7 +467,7 @@ msgstr "" msgid "For the strongest protection, define PROCORE_CONNECT_CLIENT_SECRET in wp-config.php instead of storing it here." msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:79 +#: src/Frontend/Shortcodes/Registrar.php:93 msgid "ID" msgstr "" @@ -475,7 +475,7 @@ msgstr "" msgid "Inactive" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:309 +#: src/Frontend/Shortcodes/Registrar.php:323 msgid "Item" msgstr "" @@ -507,10 +507,10 @@ msgstr "" msgid "Loading…" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:94 -#: src/Frontend/Shortcodes/Registrar.php:117 -#: src/Frontend/Shortcodes/Registrar.php:459 -#: src/Frontend/Shortcodes/Registrar.php:493 +#: src/Frontend/Shortcodes/Registrar.php:108 +#: src/Frontend/Shortcodes/Registrar.php:131 +#: src/Frontend/Shortcodes/Registrar.php:473 +#: src/Frontend/Shortcodes/Registrar.php:507 msgid "Location" msgstr "" @@ -534,8 +534,8 @@ msgstr "" msgid "Monthly Sandbox" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:161 -#: src/Frontend/Shortcodes/Registrar.php:196 +#: src/Frontend/Shortcodes/Registrar.php:175 +#: src/Frontend/Shortcodes/Registrar.php:210 msgid "Name" msgstr "" @@ -555,7 +555,7 @@ msgstr "" msgid "No Client Secret is set." msgstr "" -#: src/Api/Client.php:182 +#: src/Api/Client.php:196 msgid "No Procore company ID is available. Set a default company in Procore → Connection, or pass company_id." msgstr "" @@ -599,7 +599,7 @@ msgstr "" msgid "Not set" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:373 +#: src/Frontend/Shortcodes/Registrar.php:387 msgid "Notes" msgstr "" @@ -607,12 +607,12 @@ msgstr "" msgid "Nothing recorded. Turn on diagnostics under Tools to start collecting events." msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:89 -#: src/Frontend/Shortcodes/Registrar.php:215 -#: src/Frontend/Shortcodes/Registrar.php:249 -#: src/Frontend/Shortcodes/Registrar.php:279 -#: src/Frontend/Shortcodes/Registrar.php:339 -#: src/Frontend/Shortcodes/Registrar.php:393 +#: src/Frontend/Shortcodes/Registrar.php:103 +#: src/Frontend/Shortcodes/Registrar.php:229 +#: src/Frontend/Shortcodes/Registrar.php:263 +#: src/Frontend/Shortcodes/Registrar.php:293 +#: src/Frontend/Shortcodes/Registrar.php:353 +#: src/Frontend/Shortcodes/Registrar.php:407 msgid "Number" msgstr "" @@ -620,12 +620,12 @@ msgstr "" msgid "Object cache" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:344 +#: src/Frontend/Shortcodes/Registrar.php:358 msgid "Observation" msgstr "" #: src/Api/Endpoints.php:221 -#: src/Frontend/Shortcodes/Registrar.php:333 +#: src/Frontend/Shortcodes/Registrar.php:347 msgid "Observations" msgstr "" @@ -642,15 +642,15 @@ msgstr "" msgid "Off by default, so deleting the plugin removes its credentials, tokens and cache." msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:483 +#: src/Frontend/Shortcodes/Registrar.php:497 msgid "Office" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:478 +#: src/Frontend/Shortcodes/Registrar.php:492 msgid "Offices" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:479 +#: src/Frontend/Shortcodes/Registrar.php:493 msgid "Offices registered against the Procore company." msgstr "" @@ -675,7 +675,7 @@ msgstr "" msgid "Optional. Used whenever a project-scoped shortcode omits project_id." msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:304 +#: src/Frontend/Shortcodes/Registrar.php:318 msgid "Outstanding punch list items for a project." msgstr "" @@ -687,8 +687,8 @@ msgstr "" msgid "Persistent" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:464 -#: src/Frontend/Shortcodes/Registrar.php:498 +#: src/Frontend/Shortcodes/Registrar.php:478 +#: src/Frontend/Shortcodes/Registrar.php:512 msgid "Phone" msgstr "" @@ -696,7 +696,7 @@ msgstr "" msgid "Plugin version" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:122 +#: src/Frontend/Shortcodes/Registrar.php:136 msgid "Postcode" msgstr "" @@ -745,7 +745,7 @@ msgstr "" msgid "Procore has not reported a rate limit yet." msgstr "" -#: src/Api/Client.php:415 +#: src/Api/Client.php:433 msgid "Procore is rate limiting this site. Cached data will be shown until the limit resets." msgstr "" @@ -757,7 +757,7 @@ msgstr "" msgid "Procore recommends keeping this at or below 2000." msgstr "" -#: src/Api/Client.php:394 +#: src/Api/Client.php:412 msgid "Procore rejected the access token." msgstr "" @@ -770,15 +770,15 @@ msgstr "" msgid "Procore requests are paused after repeated failures and will resume in %s. Cached data is being served in the meantime." msgstr "" -#: src/Api/Client.php:225 +#: src/Api/Client.php:243 msgid "Procore requests are paused after repeated failures. They will resume automatically." msgstr "" -#: src/Api/Client.php:467 +#: src/Api/Client.php:485 msgid "Procore returned HTTP %d." msgstr "" -#: src/Api/Client.php:482 +#: src/Api/Client.php:500 msgid "Procore returned a response that could not be decoded." msgstr "" @@ -800,7 +800,7 @@ msgstr "" msgid "Production" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:84 +#: src/Frontend/Shortcodes/Registrar.php:98 msgid "Project" msgstr "" @@ -822,16 +822,16 @@ msgstr "" msgid "Project information is temporarily unavailable." msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:507 +#: src/Frontend/Shortcodes/Registrar.php:521 msgid "Project locations" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:127 +#: src/Frontend/Shortcodes/Registrar.php:141 msgid "Project number" msgstr "" #: src/Api/Endpoints.php:131 -#: src/Frontend/Shortcodes/Registrar.php:156 +#: src/Frontend/Shortcodes/Registrar.php:170 msgid "Project team" msgstr "" @@ -840,7 +840,7 @@ msgid "Project vendors" msgstr "" #: src/Api/Endpoints.php:110 -#: src/Frontend/Shortcodes/Registrar.php:66 +#: src/Frontend/Shortcodes/Registrar.php:80 msgid "Projects" msgstr "" @@ -849,7 +849,7 @@ msgstr "" msgid "Projects are readable for company %d." msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:508 +#: src/Frontend/Shortcodes/Registrar.php:522 msgid "Projects that have coordinates, as an accessible location list with geo microdata." msgstr "" @@ -858,11 +858,11 @@ msgid "Punch List: Read Only" msgstr "" #: src/Api/Endpoints.php:210 -#: src/Frontend/Shortcodes/Registrar.php:303 +#: src/Frontend/Shortcodes/Registrar.php:317 msgid "Punch list" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:334 +#: src/Frontend/Shortcodes/Registrar.php:348 msgid "Quality and safety observations recorded on a project." msgstr "" @@ -875,7 +875,7 @@ msgid "REST proxy" msgstr "" #: src/Api/Endpoints.php:190 -#: src/Frontend/Shortcodes/Registrar.php:243 +#: src/Frontend/Shortcodes/Registrar.php:257 msgid "RFIs" msgstr "" @@ -928,13 +928,13 @@ msgstr "" #. translators: %d: number of cache entries removed. #: src/Admin/Ajax.php:77 -#: src/Cli/Commands.php:101 +#: src/Cli/Commands.php:103 msgid "Removed %d cached response." msgid_plural "Removed %d cached responses." msgstr[0] "" msgstr[1] "" -#: src/Frontend/Shortcodes/Registrar.php:517 +#: src/Frontend/Shortcodes/Registrar.php:531 msgid "Render any endpoint published in the Procore endpoint registry." msgstr "" @@ -942,7 +942,7 @@ msgstr "" msgid "Request timeout" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:244 +#: src/Frontend/Shortcodes/Registrar.php:258 msgid "Requests for information raised on a project." msgstr "" @@ -950,11 +950,11 @@ msgstr "" msgid "Response cache" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:225 +#: src/Frontend/Shortcodes/Registrar.php:239 msgid "Revision" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:166 +#: src/Frontend/Shortcodes/Registrar.php:180 msgid "Role" msgstr "" @@ -966,7 +966,7 @@ msgstr "" msgid "Runs through authentication, company access and every endpoint the shortcodes use, so you can see exactly which Procore tool permissions your credentials have." msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:417 +#: src/Frontend/Shortcodes/Registrar.php:431 msgid "Schedule" msgstr "" @@ -974,7 +974,7 @@ msgstr "" msgid "Schedule tasks" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:418 +#: src/Frontend/Shortcodes/Registrar.php:432 msgid "Schedule tasks and milestones for a project." msgstr "" @@ -1018,11 +1018,11 @@ msgstr "" msgid "Specification sections" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:211 +#: src/Frontend/Shortcodes/Registrar.php:225 msgid "Specification sections published for a project." msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:210 +#: src/Frontend/Shortcodes/Registrar.php:224 msgid "Specifications" msgstr "" @@ -1030,27 +1030,27 @@ msgstr "" msgid "Specifications: Read Only" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:132 +#: src/Frontend/Shortcodes/Registrar.php:146 msgid "Stage" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:427 +#: src/Frontend/Shortcodes/Registrar.php:441 msgid "Start" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:137 +#: src/Frontend/Shortcodes/Registrar.php:151 msgid "Start date" msgstr "" #: src/Admin/SettingsPage.php:147 -#: src/Frontend/Shortcodes/Registrar.php:99 -#: src/Frontend/Shortcodes/Registrar.php:147 -#: src/Frontend/Shortcodes/Registrar.php:259 -#: src/Frontend/Shortcodes/Registrar.php:289 -#: src/Frontend/Shortcodes/Registrar.php:319 -#: src/Frontend/Shortcodes/Registrar.php:349 -#: src/Frontend/Shortcodes/Registrar.php:378 -#: src/Frontend/Shortcodes/Registrar.php:403 +#: src/Frontend/Shortcodes/Registrar.php:113 +#: src/Frontend/Shortcodes/Registrar.php:161 +#: src/Frontend/Shortcodes/Registrar.php:273 +#: src/Frontend/Shortcodes/Registrar.php:303 +#: src/Frontend/Shortcodes/Registrar.php:333 +#: src/Frontend/Shortcodes/Registrar.php:363 +#: src/Frontend/Shortcodes/Registrar.php:392 +#: src/Frontend/Shortcodes/Registrar.php:417 msgid "Status" msgstr "" @@ -1062,16 +1062,16 @@ msgstr "" msgid "Stylesheet" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:254 +#: src/Frontend/Shortcodes/Registrar.php:268 msgid "Subject" msgstr "" #: src/Api/Endpoints.php:200 -#: src/Frontend/Shortcodes/Registrar.php:273 +#: src/Frontend/Shortcodes/Registrar.php:287 msgid "Submittals" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:274 +#: src/Frontend/Shortcodes/Registrar.php:288 msgid "Submittals tracked on a project." msgstr "" @@ -1079,11 +1079,11 @@ msgstr "" msgid "Submittals: Read Only" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:422 +#: src/Frontend/Shortcodes/Registrar.php:436 msgid "Task" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:157 +#: src/Frontend/Shortcodes/Registrar.php:171 msgid "Team members assigned to a project. Email addresses are hidden unless explicitly enabled." msgstr "" @@ -1115,7 +1115,7 @@ msgstr "" msgid "The Procore connection has expired. Open Procore → Connection and reconnect." msgstr "" -#: src/Api/Client.php:425 +#: src/Api/Client.php:443 msgid "The Procore request could not be completed." msgstr "" @@ -1138,7 +1138,7 @@ msgstr "" msgid "The field attribute is required." msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:185 +#: src/Frontend/Shortcodes/Registrar.php:199 msgid "The logo or featured image for a project." msgstr "" @@ -1166,8 +1166,8 @@ msgstr "" msgid "This site is not connected to Procore. Open Procore → Connection and choose \"Connect to Procore\"." msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:284 -#: src/Frontend/Shortcodes/Registrar.php:398 +#: src/Frontend/Shortcodes/Registrar.php:298 +#: src/Frontend/Shortcodes/Registrar.php:412 msgid "Title" msgstr "" @@ -1209,11 +1209,11 @@ msgstr "" msgid "Users who can edit posts" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:408 +#: src/Frontend/Shortcodes/Registrar.php:422 msgid "Value" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:449 +#: src/Frontend/Shortcodes/Registrar.php:463 msgid "Vendors" msgstr "" @@ -1226,7 +1226,7 @@ msgstr "" msgid "View %s on a map" msgstr "" -#: src/Frontend/Shortcodes/Registrar.php:469 +#: src/Frontend/Shortcodes/Registrar.php:483 msgid "Website" msgstr "" diff --git a/procore-connect.php b/procore-connect.php index 6dd6149..dbdcd40 100644 --- a/procore-connect.php +++ b/procore-connect.php @@ -3,7 +3,7 @@ * Plugin Name: Procore Connect * Plugin URI: https://github.com/ibuilder/ProcoreWP * Description: Connect WordPress to the Procore construction management platform. Display projects, teams, drawings, RFIs and more with shortcodes, blocks and a cached REST proxy. - * Version: 2.0.0 + * Version: 2.0.1 * Requires at least: 6.5 * Requires PHP: 7.4 * Author: ibuilder @@ -22,7 +22,7 @@ defined( 'ABSPATH' ) || exit; -const VERSION = '2.0.0'; +const VERSION = '2.0.1'; define( 'PROCORE_CONNECT_VERSION', VERSION ); define( 'PROCORE_CONNECT_FILE', __FILE__ ); diff --git a/readme.txt b/readme.txt index d9b4e50..bef0866 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: procore, construction, project management, shortcode, api Requires at least: 6.5 Tested up to: 7.0 Requires PHP: 7.4 -Stable tag: 2.0.0 +Stable tag: 2.0.1 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -136,6 +136,15 @@ Yes. Caching goes through the transient API, so a persistent object cache such a == Changelog == += 2.0.1 = + +Fixes found by running the plugin inside a real WordPress install. + +* Fixed: pagination parameters were sent to single-record endpoints such as `/projects/{id}`, which have no pages. This was noise on the wire and varied the cache key for identical requests. +* Fixed: the REST proxy ignored the configured default company, so every request failed with a missing-company error unless company_id was passed explicitly. +* Fixed: translations were loaded before `init`, which triggers a `_load_textdomain_just_in_time` notice on WordPress 6.7+ and silently drops translations. +* Fixed: WP-CLI subcommands were registered with underscores while the documentation advertised hyphens. `wp procore-connect cache-clear`, `cache-warm` and `reset-token` now work as documented. + = 2.0.0 = A complete rewrite. See the upgrade notice below before updating. @@ -167,5 +176,8 @@ A complete rewrite. See the upgrade notice below before updating. == Upgrade Notice == += 2.0.1 = +Bug fixes from real-world integration testing: pagination on single-record endpoints, the REST proxy ignoring the default company, translations loading too early, and WP-CLI subcommand names. No action required. + = 2.0.0 = Rewrite. Fixes authentication, adds caching and encrypts credentials. Settings migrate and all 1.x shortcodes still work, but the main file was renamed: activate the plugin once, re-run the connection test, and rotate your client secret. diff --git a/src/Api/Client.php b/src/Api/Client.php index b0e9660..8bcc36c 100644 --- a/src/Api/Client.php +++ b/src/Api/Client.php @@ -173,8 +173,22 @@ public function fetch( string $slug, array $params = array(), array $options = a ); } - $company = isset( $options['company_id'] ) ? absint( $options['company_id'] ) : Settings::default_company_id(); - $project = isset( $options['project_id'] ) ? absint( $options['project_id'] ) : 0; + /* + * A zero is treated as "not supplied" rather than as an explicit + * choice, so callers that always pass the key — the REST proxy sends + * an integer parameter that defaults to 0 — still inherit the site + * defaults instead of failing with a missing-context error. + */ + $company = absint( $options['company_id'] ?? 0 ); + $project = absint( $options['project_id'] ?? 0 ); + + if ( $company <= 0 ) { + $company = Settings::default_company_id(); + } + + if ( $project <= 0 ) { + $project = Settings::default_project_id(); + } if ( Endpoints::SCOPE_NONE !== $definition['scope'] && $company <= 0 ) { return new \WP_Error( @@ -193,7 +207,11 @@ public function fetch( string $slug, array $params = array(), array $options = a if ( ! empty( $definition['paginated'] ) ) { $query['per_page'] = min( 2000, max( 1, (int) ( $options['per_page'] ?? Settings::get( 'per_page', 100 ) ) ) ); - $query['page'] = max( 1, (int) ( $query['page'] ?? 1 ) ); + $query['page'] = max( 1, (int) ( $query['page'] ?? $options['page'] ?? 1 ) ); + } else { + // A single-record resource has no pages; sending them is meaningless + // noise that also varies the cache key for identical requests. + unset( $query['page'], $query['per_page'] ); } $ttl = isset( $options['ttl'] ) ? (int) $options['ttl'] : (int) $definition['ttl']; diff --git a/src/Cli/Commands.php b/src/Cli/Commands.php index d335c87..d8edbd7 100644 --- a/src/Cli/Commands.php +++ b/src/Cli/Commands.php @@ -87,6 +87,8 @@ public function test( array $args, array $assoc_args ): void { * wp procore-connect cache-clear * wp procore-connect cache-clear --group=rfis * + * @subcommand cache-clear + * * @param array $args Positional arguments. * @param array $assoc_args Associative arguments. * @return void @@ -111,6 +113,8 @@ public function cache_clear( array $args, array $assoc_args ): void { * * wp procore-connect cache-warm * + * @subcommand cache-warm + * * @param array $args Positional arguments. * @param array $assoc_args Associative arguments. * @return void @@ -208,6 +212,8 @@ public function projects( array $args, array $assoc_args ): void { * * wp procore-connect reset-token * + * @subcommand reset-token + * * @param array $args Positional arguments. * @param array $assoc_args Associative arguments. * @return void diff --git a/src/Frontend/Shortcodes/AbstractShortcode.php b/src/Frontend/Shortcodes/AbstractShortcode.php index 4e322c5..b9b9a9d 100644 --- a/src/Frontend/Shortcodes/AbstractShortcode.php +++ b/src/Frontend/Shortcodes/AbstractShortcode.php @@ -222,17 +222,19 @@ protected function resolve( array $atts ) { /** * Query parameters sent with the request. * + * Pagination is deliberately not added here. It is passed through + * `fetch_options()` so the client can drop it for endpoints the registry + * marks as unpaginated — sending `page` to a single-record resource such as + * `/projects/{id}` is meaningless and needlessly varies the cache key. + * + * The parameter is retained because this is an extension point: subclasses + * override it to add endpoint-specific filters derived from the attributes. + * * @param array $atts Sanitized attributes. * @return array Query parameters. */ - protected function query_args( array $atts ): array { - $args = array(); - - if ( ! empty( $atts['page'] ) ) { - $args['page'] = (int) $atts['page']; - } - - return $args; + protected function query_args( array $atts ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Extension point for subclasses. + return array(); } /** @@ -246,6 +248,7 @@ protected function fetch_options( array $atts ): array { 'company_id' => $this->company_id( $atts ), 'project_id' => $this->project_id( $atts ), 'all' => ! empty( $atts['all'] ), + 'page' => max( 1, (int) $atts['page'] ), ); if ( '' !== $atts['cache'] && $atts['cache'] > 0 ) { diff --git a/src/Frontend/Shortcodes/Registrar.php b/src/Frontend/Shortcodes/Registrar.php index 7d8587a..496b56a 100644 --- a/src/Frontend/Shortcodes/Registrar.php +++ b/src/Frontend/Shortcodes/Registrar.php @@ -32,11 +32,25 @@ final class Registrar { private static $definitions = null; /** - * Register every shortcode with WordPress. + * Hook shortcode registration. + * + * Deferred to `init` because the definitions carry translated titles and + * descriptions. Building them on `plugins_loaded` would ask WordPress to + * load the text domain before `init`, which since 6.7 emits a + * `_load_textdomain_just_in_time` notice and silently drops translations. * * @return void */ public function register(): void { + add_action( 'init', array( $this, 'register_shortcodes' ) ); + } + + /** + * Register every shortcode with WordPress. + * + * @return void + */ + public function register_shortcodes(): void { foreach ( self::definitions() as $tag => $definition ) { $handler = $definition['handler']; $instance = new $handler( $definition ); diff --git a/tests/unit/ClientTest.php b/tests/unit/ClientTest.php index 96703d6..9d5c632 100644 --- a/tests/unit/ClientTest.php +++ b/tests/unit/ClientTest.php @@ -65,6 +65,57 @@ public function test_sets_timeout_and_user_agent(): void { $this->assertStringContainsString( 'Procore Connect/2.0.0', $calls[0]['args']['user-agent'] ); } + /** + * A single-record endpoint must not receive pagination parameters. + * + * `page` and `per_page` are meaningless on `/projects/{id}`, and including + * them also varies the cache key for what is the same request. + * + * @return void + */ + public function test_unpaginated_endpoints_get_no_pagination_params(): void { + $calls = array(); + $client = $this->client_returning( array( $this->response( array( 'id' => 123 ) ) ), $calls ); + + $client->fetch( + 'project', + array( 'page' => 3 ), + array( + 'company_id' => 1, + 'project_id' => 123, + 'per_page' => 50, + 'page' => 3, + ) + ); + + $this->assertSame( 'https://api.procore.com/rest/v1.0/projects/123', $calls[0]['url'] ); + $this->assertStringNotContainsString( 'page=', $calls[0]['url'] ); + } + + /** + * A paginated endpoint must still honour an explicitly requested page. + * + * @return void + */ + public function test_paginated_endpoints_honour_the_requested_page(): void { + $calls = array(); + $client = $this->client_returning( array( $this->response( array() ) ), $calls ); + + $client->fetch( + 'rfis', + array(), + array( + 'company_id' => 1, + 'project_id' => 123, + 'page' => 4, + 'per_page' => 25, + ) + ); + + $this->assertStringContainsString( 'page=4', $calls[0]['url'] ); + $this->assertStringContainsString( 'per_page=25', $calls[0]['url'] ); + } + /** * `all` must follow the Link header rather than guessing page numbers. * @@ -238,6 +289,36 @@ public function test_returns_an_error_when_no_fallback_exists(): void { $this->assertSame( 'Forbidden', $result->get_error_message() ); } + /** + * A zero identifier must fall back to the site default, not fail. + * + * The REST proxy always sends `company_id` and `project_id` as integers + * that default to 0, so treating a supplied zero as an explicit choice + * made the proxy ignore the configured defaults entirely. + * + * @return void + */ + public function test_zero_identifiers_fall_back_to_site_defaults(): void { + Settings::set( 'default_company_id', 4242 ); + Settings::set( 'default_project_id', 77 ); + + $calls = array(); + $client = $this->client_returning( array( $this->response( array() ) ), $calls ); + + $result = $client->fetch( + 'rfis', + array(), + array( + 'company_id' => 0, + 'project_id' => 0, + ) + ); + + $this->assertFalse( is_wp_error( $result ) ); + $this->assertSame( '4242', $calls[0]['args']['headers']['Procore-Company-Id'] ); + $this->assertStringContainsString( '/projects/77/rfis', $calls[0]['url'] ); + } + /** * A company-scoped endpoint must refuse to run without a company. *