Procore Connect 2.0.0 — rewrite as a standards-compliant WordPress plugin - #1
Conversation
Adds Composer with PHPCS (WordPress-Extra + WordPress-Docs), PHPUnit, a .distignore for release builds, editor config and the GPL-2.0 text. Removes index.php, includes/ and the 1.x stylesheet. They are replaced wholesale in the following commits: the 1.x entry point authenticated against the wrong Procore host and could never obtain a token, so there is nothing in it worth carrying forward. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the 1.x request code, which could not work against the live API. Authentication now posts to login.procore.com, Procore's actual token host. 1.x posted to api.procore.com, a different origin, so no token was ever issued on any install regardless of configuration. The company scope is now sent as the Procore-Company-Id header. 1.x appended it as a company_id query parameter, which most endpoints reject. Also here: - Both grants. Client Credentials for a Developer Managed Service Account, and Authorization Code on behalf of a Procore user. Refreshes are serialised behind a lock because Procore invalidates a refresh token the instant it is exchanged, so two concurrent front-end requests would otherwise lock the site out permanently. - Credentials and tokens encrypted at rest with AES-256-GCM keyed from the site's own salts, in a non-autoloaded option. 1.x kept all three as plaintext in an option read on every page request site-wide. wp-config.php constants take precedence and never reach the database. - Two-layer caching: a fresh entry per endpoint TTL, plus a long-lived stale copy served when Procore is unreachable, so an outage degrades a published page instead of blanking it. Backed by transients, so Redis and Memcached work with no configuration. - Rate-limit handling: records X-Rate-Limit-*, honours Retry-After, retries 429/503 with exponential backoff plus jitter, and opens a circuit breaker after five consecutive failures. - Pagination by following Link: rel="next", refusing any link pointing off the configured API host. 1.x silently returned page one only. - An endpoint allow-list. Nothing outside src/Api/Endpoints.php is callable, which is what later makes [procore_data] and the public REST proxy safe to expose. - Environment resolution for production, both sandboxes and custom regional or federal hosts, with tokens scoped per environment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every 1.x shortcode tag is preserved, and the legacy id, sort_by, sort_order and show_details attributes still work, so pages written for the old plugin render unchanged. Eleven new shortcodes cover RFIs, submittals, punch lists, observations, daily logs, change orders, schedule tasks, vendors, offices, project locations, plus a generic allow-listed reader. Behaviour fixes over 1.x: - Sorting no longer corrupts result sets. array_multisort() over array_column() misaligns rows as soon as any record lacks the sort column; records with no value now sort last in both directions and stay attached to their own row. - Sparse Procore payloads no longer emit PHP warnings. - The stylesheet loads only on pages that actually contain plugin output, rather than on every front-end request. Security and privacy: - register_setting() has a sanitize_callback. 1.x had none, so an arbitrary array could be written to the option. - Email addresses are suppressed by default and need two independent opt-ins to publish; even then they pass through antispambot(). 1.x published every team member's address on a public page by default. - Procore error messages name accounts and permissions, so they reach administrators only; visitors get a neutral notice. - [procore_project_data] reads from a field allow-list rather than any key in the payload. - All cell content goes through Format::cell(), which escapes everything it returns, keeping the escaping guarantee auditable in one place. Markup now lives in templates/ and is overridable from yourtheme/procorewp/. 1.x told users to edit files inside the plugin directory, which lost their work on every update; custom CSS moves to a sanitized admin setting printed via wp_add_inline_style(). uninstall.php removes credentials, tokens, cache and options unless the site opts to keep its data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A tabbed Procore menu: Connection, Display, Cache, Shortcodes, Status and Tools. The connection test replaces 1.x's binary success/failure. It reports six stages separately and then probes every registered endpoint, reporting each as readable, failed or skipped alongside the Procore permission it needs. The common real-world failure is a perfectly valid token whose service account lacks read permission on one tool, and "connection successful" tells you nothing about that. Every admin action verifies a nonce and the manage_options capability. 1.x ran its connection test from an unverified $_POST key. The client secret is never rendered back into the form. The field shows a mask, submitting it blank keeps the stored value, and there is an explicit checkbox to delete it. 1.x echoed the real secret into an input value attribute. The OAuth round trip binds a single-use state value to a transient and refuses any callback that does not match, which is what stops someone grafting their own Procore account onto the site via a crafted URL. The Shortcodes tab is generated from the same registry the shortcodes use, so the reference cannot drift from the code. Status surfaces rate-limit headroom, circuit state, token expiry, cache statistics and a redacted diagnostic log. admin.js is plain ES5, no build step. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One block type with a variation per shortcode, so the inserter is populated without eighteen near-identical block types. It renders server-side by delegating to the same shortcode handler, so markup, escaping and caching are identical in both editing surfaces and there is no second rendering path to keep in sync. The editor script is hand-written ES5 against the global wp.* runtime. No bundler, no node_modules. Shipping a minified bundle without its source is a wordpress.org review flag, makes the plugin unauditable and puts Node between a contributor and a one-line fix. The REST proxy at /wp-json/procorewp/v1/ is off by default and read-only. It serves from the same cache the shortcodes use, so exposing it cannot blow the rate limit, and resolves only against endpoints marked public in the registry, so identity endpoints such as /me and /companies return 404. Credentials never reach the browser, and error detail is restricted to administrators. WP-CLI: test, doctor, projects, cache-clear, cache-warm, reset-token. `test` exits non-zero on failure so it works as a deploy gate, and `doctor` reports whether credentials are set without printing them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
59 tests, 270 assertions, run against JSON fixtures through an injected HTTP transport, so no Procore credentials and no network access are needed. WordPress is shimmed to only the functions the code under test actually calls. Coverage is aimed at the things that broke in 1.x and the things that are easy to regress: the Procore-Company-Id header being present, requests reaching the API host and never the login host, Link-header pagination including refusing off-host links, 429 and 503 backoff, cache hits and the stale fallback, the circuit breaker, encryption round-trips and secret masking, endpoint allow-list refusals, settings sanitization, legacy attribute aliases, and every shortcode's output escaping and email suppression. Two of these tests found real bugs while being written: sort_by() promoted records with no value to the top when reversed, and the class sanitizer needed asserting on what it actually guarantees rather than on a specific output string. Both are fixed in the code they cover. languages/procorewp.pot carries 293 strings with their translator comments. 1.x declared a text domain and contained no translation calls at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
readme.txt for wordpress.org, with headers matching the plugin header exactly, since Plugin Check compares them, and a third-party service disclosure naming the Procore hosts contacted and the data sent. README.md rewritten with a shortcode table mapping each one to the Procore permission it needs, which is the question people actually get stuck on. docs/ is a Jekyll site for GitHub Pages: installation, authentication including a Developer Portal and Permissions Builder walkthrough, shortcode reference, blocks, caching and rate limits, templating, REST, WP-CLI, troubleshooting and an upgrade guide. The upgrade guide leads with the two things a 1.x user must do: activate once, because the main file was renamed from index.php, and re-run the connection test, because 1.x never obtained a token. It also says plainly that the 1.x client secret should be treated as exposed, since it was stored as plaintext in an autoloaded option. CI: PHPCS on WordPress-Extra and WordPress-Docs, php -l across PHP 7.4 to 8.4, PHPUnit on four versions, JS syntax and block.json validation, Plugin Check against the staged distributable rather than the dev tree, a release workflow that fails on any version mismatch between the tag, the plugin header and readme.txt, and Pages deployment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Plugin Check ran against the staged distributable and reported six items. Four are fixed here: - Removed the Update URI header. It is not permitted on plugins hosted at wordpress.org, and GitHub releases do not need it. - Tested up to 7.0, which is the current WordPress release. 6.9 tripped the "not tested against current" error. - Trimmed the 2.0.0 upgrade notice to 237 characters, under the 300 character limit. - Removed the load_plugin_textdomain() call. WordPress has loaded translations just-in-time since 4.6 for any plugin whose text domain matches its directory name. Two remaining findings are about the name "ProcoreWP" containing the restricted term "wp", which wordpress.org disallows in both the plugin name and the slug. That is a project naming decision rather than a code fix, so it is left for review. It does not affect GitHub or self-hosted distribution. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Plugin Check reported "wp" as a restricted term in both the plugin name and the slug. wordpress.org rejects that outright in either position, so "ProcoreWP" could never have been listed in the directory. The plugin is now Procore Connect with the slug procore-connect. The GitHub repository is unchanged and still lives at ibuilder/ProcoreWP; only the distributed plugin identity moved. Renamed throughout: main file, text domain, namespace (ProcoreConnect), PROCORE_CONNECT_ constants, procore_connect_ options, hooks, filters and transients, procore-connect- CSS classes and script handles, the REST namespace, the block name, the theme template override directory, the WP-CLI command and the POT file. Shortcode tags are untouched. They were already procore_ prefixed, so every page written for 1.x still renders. Historical references to 1.x still name it ProcoreWP, because that is what it was released as. Verified after the rename: PHPCS clean across 49 files, 59 tests and 270 assertions passing, php -l and JS syntax clean, and the staged distributable is 50 files at 456 KB with the correct directory name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Update: renamed to Procore ConnectPlugin Check flagged The GitHub repo is unchanged — still Renamed throughout: main file, text domain, namespace ( Shortcode tags are untouched. They were already CI status
The four fixable findings from the earlier run are also resolved: removed the |
Why
Version 1.x could not work against the live Procore API on any install, regardless of configuration. It sent its token request to
api.procore.com/oauth/token; Procore serves authentication fromlogin.procore.com/oauth/token, a different host. No access token was ever issued.Underneath that, it sent the company scope as a
company_idquery parameter where Procore requires theProcore-Company-Idheader, stored the client secret as plaintext in an autoloaded option, ran its connection test from an unverified$_POST, registered its settings with nosanitize_callback, published every team member's email address by default, and cached nothing at all against an API with a documented ten-second spike limit.This is a rewrite. All eighteen shortcode tags from 1.x are preserved, along with the legacy
id,sort_by,sort_orderandshow_detailsattributes, so existing pages render unchanged.What's in it
Correctness
api.procore.com❌login.procore.com✅?company_id=❌Procore-Company-Idheader ✅Link: rel="next"array_multisortmisaligned rowsSecurity
wp-config.phpconstants take precedence and never reach the database.sanitize_callbackon the settings option.Linkheaders pointing off-host are refused.state.New capability
Eleven new shortcodes (RFIs, submittals, punch list, observations, daily logs, change orders, schedule, vendors, offices, project map, generic reader), Gutenberg blocks, an optional read-only REST proxy, WP-CLI commands, and theme template overrides via
yourtheme/procorewp/.The connection test now reports six stages separately and probes every endpoint against the Procore permission it requires — the usual cause of an empty shortcode is a valid token whose service account lacks read access on one specific tool, which "connection successful" never surfaced.
Verification
WordPress-Extra+WordPress-Docs+PHPCompatibilityWP— 49 files, zero errors, zero warningsphp -l— clean; PHP 7.4–8.4 compatiblereadme.txtversion parity checkedTwo of the new tests found real bugs while being written (reverse sort promoted empty records; the class sanitizer needed asserting on its actual guarantee). Both fixed in the commits that cover them.
Still needs a human
Testing against a real Procore account: enter DMSA credentials, run Test connection, confirm the permission probe matches your app manifest, place shortcodes, and exercise the Authorization Code flow.
Procore versions each resource independently and several tools have moved paths between releases. The endpoint registry is filterable via
procorewp_endpointsprecisely so a path can be corrected without forking, and the probe table shows which ones resolve for your account.The main file is renamed
index.php→procorewp.php(required for Plugin Check and wordpress.org). WordPress treats it as a different plugin, so it must be activated once — settings migrate automatically. Because 1.x never authenticated, re-run the connection test afterwards, and rotate the client secret, which 1.x stored as plaintext.Not merged yet
Per the plan, this stays on the branch until you've reviewed it. GitHub Pages is not enabled yet either — say the word and I'll turn it on so
docs/publishes.🤖 Generated with Claude Code