Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ 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.3] — 2026-08-08

### Fixed

- **Repairs a client secret damaged by 2.0.0 or 2.0.1.** Those versions added an
encryption layer to the stored secret on every settings write, so an install
upgrading from either can hold cipher text that a single decrypt cannot recover —
the site simply fails to authenticate, with nothing to explain why. 2.0.2 stopped
the damage but did not undo it.

The upgrade routine now unwraps the value back to plain text and re-stores it
correctly, and reports what it did in an admin notice. If the value cannot be
recovered it is cleared and the administrator is asked to enter it again, rather
than being left with a silent authentication failure.

Added `Encryption::decrypt_deep()` and `Encryption::depth()`, plus four tests that
reproduce a three-layer secret and assert it is restored.

## [2.0.2] — 2026-08-08

### Fixed
Expand Down Expand Up @@ -188,6 +206,7 @@ every install was non-functional regardless of configuration.

- Initial release.

[2.0.3]: https://github.com/ibuilder/ProcoreWP/releases/tag/v2.0.3
[2.0.2]: https://github.com/ibuilder/ProcoreWP/releases/tag/v2.0.2
[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
Expand Down
27 changes: 18 additions & 9 deletions languages/procore-connect.pot
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ msgstr ""
msgid "Connected as %1$s (%2$s)."
msgstr ""

#: src/Admin/Notices.php:68
#: src/Admin/Notices.php:69
msgid "Connected to Procore."
msgstr ""

Expand Down Expand Up @@ -430,7 +430,7 @@ msgstr ""
msgid "Encrypted (AES-256-GCM)"
msgstr ""

#: src/Admin/Notices.php:77
#: src/Admin/Notices.php:78
msgid "Enter a Client ID and Client Secret before connecting."
msgstr ""

Expand Down Expand Up @@ -663,7 +663,7 @@ msgstr ""
msgid "Open — resumes in %s"
msgstr ""

#: src/Admin/Notices.php:123
#: src/Admin/Notices.php:158
msgid "OpenSSL is not available on this server, so your Procore Client Secret cannot be encrypted in the database. Define PROCORE_CONNECT_CLIENT_SECRET in wp-config.php instead."
msgstr ""

Expand Down Expand Up @@ -724,15 +724,24 @@ msgid "Procore Connect (visible to administrators only): %s"
msgstr ""

#. translators: %s: settings screen URL.
#: src/Admin/Notices.php:102
#: src/Admin/Notices.php:139
msgid "Procore Connect could not recover your stored Client Secret. Versions 2.0.0 and 2.0.1 re-encrypted it on every settings save until it became unreadable, so it has been cleared. Please <a href=\"%s\">enter it again</a>; this cannot happen on 2.0.2 or later."
msgstr ""

#. translators: %s: settings screen URL.
#: src/Admin/Notices.php:103
msgid "Procore Connect imported your settings from version 1.x. Version 1.x authenticated against the wrong Procore host, so you need to <a href=\"%s\">re-run the connection test</a> before shortcodes will return data."
msgstr ""

#. translators: 1: environment label, 2: API host.
#: src/Admin/Notices.php:141
#: src/Admin/Notices.php:176
msgid "Procore Connect is pointed at %1$s (%2$s). Front-end shortcodes are showing sandbox data."
msgstr ""

#: src/Admin/Notices.php:129
msgid "Procore Connect repaired your stored Client Secret. Versions 2.0.0 and 2.0.1 re-encrypted it on every settings save, which eventually made it unreadable. Nothing further is needed, though it is worth running Procore → Connection → Test connection to confirm."
msgstr ""

#: src/Api/Auth/AbstractAuth.php:101
msgid "Procore did not return an access token."
msgstr ""
Expand Down Expand Up @@ -766,7 +775,7 @@ msgid "Procore rejected the authentication request (HTTP %d)."
msgstr ""

#. translators: %s: human readable time difference.
#: src/Admin/Notices.php:164
#: src/Admin/Notices.php:199
msgid "Procore requests are paused after repeated failures and will resume in %s. Cached data is being served in the meantime."
msgstr ""

Expand Down Expand Up @@ -998,7 +1007,7 @@ msgstr ""
msgid "Service Account (Client Credentials) — recommended"
msgstr ""

#: src/Plugin.php:158
#: src/Plugin.php:174
msgid "Settings"
msgstr ""

Expand Down Expand Up @@ -1099,15 +1108,15 @@ msgstr ""
msgid "The Procore Connect REST proxy is disabled."
msgstr ""

#: src/Admin/Notices.php:76
#: src/Admin/Notices.php:77
msgid "The Procore authorization request was declined."
msgstr ""

#: src/Api/Auth/AuthorizationCode.php:149
msgid "The Procore authorization response could not be verified. Please start the connection again."
msgstr ""

#: src/Admin/Notices.php:78
#: src/Admin/Notices.php:79
msgid "The Procore connection could not be completed."
msgstr ""

Expand Down
4 changes: 2 additions & 2 deletions procore-connect.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.2
* Version: 2.0.3
* Requires at least: 6.5
* Requires PHP: 7.4
* Author: ibuilder
Expand All @@ -22,7 +22,7 @@

defined( 'ABSPATH' ) || exit;

const VERSION = '2.0.2';
const VERSION = '2.0.3';

define( 'PROCORE_CONNECT_VERSION', VERSION );
define( 'PROCORE_CONNECT_FILE', __FILE__ );
Expand Down
9 changes: 8 additions & 1 deletion readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.2
Stable tag: 2.0.3
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Expand Down Expand Up @@ -127,6 +127,10 @@ Yes. Caching goes through the transient API, so a persistent object cache such a

== Changelog ==

= 2.0.3 =

* **Repairs credentials damaged by 2.0.0 or 2.0.1.** Those versions re-encrypted the stored Client Secret on every settings save until it could no longer be read, leaving the site unable to authenticate with no explanation. 2.0.2 stopped the damage; 2.0.3 undoes it. The upgrade unwraps the value, re-stores it correctly, and tells you what it did. If it cannot be recovered it is cleared and you are asked to enter it again.

= 2.0.2 =

* **Fixed a credential-corrupting bug.** WordPress runs a registered setting's sanitize callback on every update to that option, including the plugin's own internal writes. Because the sanitizer encrypted the client secret unconditionally, each write re-encrypted the stored value until it could no longer be decrypted and the site silently lost its Procore connection. Encryption is now idempotent. If your connection stopped working after saving settings, re-enter the Client Secret once on 2.0.2 and it will stay valid.
Expand Down Expand Up @@ -174,6 +178,9 @@ A complete rewrite. See the upgrade notice below before updating.

== Upgrade Notice ==

= 2.0.3 =
Repairs a Client Secret damaged by 2.0.0 or 2.0.1, which re-encrypted it on every save until it stopped working. Upgrading restores it automatically and reports the result. Recommended for anyone who ran 2.0.0 or 2.0.1.

= 2.0.2 =
Important fix: repeated settings saves could re-encrypt the stored Client Secret until it became unrecoverable, silently breaking the Procore connection. Update, then re-enter your Client Secret once if the connection had stopped working.

Expand Down
35 changes: 35 additions & 0 deletions src/Admin/Notices.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public function render(): void {

$this->oauth_result();
$this->migration_notice();
$this->secret_repair_notice();

if ( ! $this->on_plugin_screen() ) {
return;
Expand Down Expand Up @@ -108,6 +109,40 @@ private function migration_notice(): void {
delete_option( 'procore_connect_migrated_from' );
}

/**
* Report the outcome of repairing a secret damaged by 2.0.0 or 2.0.1.
*
* @return void
*/
private function secret_repair_notice(): void {
$state = (string) get_option( 'procore_connect_secret_repair', '' );

if ( '' === $state ) {
return;
}

delete_option( 'procore_connect_secret_repair' );

if ( 'repaired' === $state ) {
$this->notice(
'success',
__( 'Procore Connect repaired your stored Client Secret. Versions 2.0.0 and 2.0.1 re-encrypted it on every settings save, which eventually made it unreadable. Nothing further is needed, though it is worth running Procore → Connection → Test connection to confirm.', 'procore-connect' )
);

return;
}

$this->notice(
'error',
sprintf(
/* translators: %s: settings screen URL. */
__( 'Procore Connect could not recover your stored Client Secret. Versions 2.0.0 and 2.0.1 re-encrypted it on every settings save until it became unreadable, so it has been cleared. Please <a href="%s">enter it again</a>; this cannot happen on 2.0.2 or later.', 'procore-connect' ),
esc_url( admin_url( 'admin.php?page=procore-connect' ) )
),
true
);
}

/**
* Warn when credentials cannot be encrypted at rest.
*
Expand Down
42 changes: 42 additions & 0 deletions src/Admin/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,48 @@ public static function sanitize( $input ): array {
return (array) apply_filters( 'procore_connect_sanitize_settings', $clean, $input );
}

/**
* Repair a client secret that earlier versions encrypted more than once.
*
* Versions 2.0.0 and 2.0.1 ran the settings sanitizer on every write to the
* option, including internal ones, so the stored secret gained a layer of
* encryption each time. Those installs cannot authenticate and give no clue
* why. Peel the value back to plain text and re-store it correctly.
*
* @return string One of `ok` (nothing to do), `repaired`, or `unrecoverable`.
*/
public static function repair_client_secret(): string {
$stored = (string) self::get( 'client_secret', '' );

if ( '' === $stored ) {
return 'ok';
}

if ( Encryption::depth( $stored ) <= 1 ) {
return 'ok';
}

$plain = Encryption::decrypt_deep( $stored );

if ( '' === $plain ) {
// Nothing usable is left; clear it so the admin is prompted rather
// than left staring at an unexplained authentication failure.
$all = self::all();
$all['client_secret'] = '';
update_option( self::OPTION, $all, false );
self::flush();

return 'unrecoverable';
}

$all = self::all();
$all['client_secret'] = Encryption::encrypt( $plain );
update_option( self::OPTION, $all, false );
self::flush();

return 'repaired';
}

/**
* Import settings saved by ProcoreWP 1.x.
*
Expand Down
16 changes: 16 additions & 0 deletions src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use ProcoreConnect\Admin\SettingsPage;
use ProcoreConnect\Admin\Settings;
use ProcoreConnect\Api\Cache;
use ProcoreConnect\Api\TokenStore;
use ProcoreConnect\Blocks\Registrar as BlockRegistrar;
use ProcoreConnect\Cli\Commands;
use ProcoreConnect\Frontend\Assets;
Expand Down Expand Up @@ -120,6 +121,21 @@ public function maybe_upgrade(): void {
Cache::flush();
}

/*
* 2.0.0 and 2.0.1 re-encrypted the client secret on every settings
* write, so an install upgrading from either may hold a secret it can
* no longer decrypt. Repair it here rather than leaving the site with
* an unexplained authentication failure.
*/
if ( '' !== $installed && version_compare( $installed, '2.0.2', '<' ) ) {
$repair = Settings::repair_client_secret();

if ( 'ok' !== $repair ) {
update_option( 'procore_connect_secret_repair', $repair, false );
TokenStore::clear();
}
}

update_option( self::VERSION_OPTION, PROCORE_CONNECT_VERSION, false );
}

Expand Down
64 changes: 64 additions & 0 deletions src/Support/Encryption.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,70 @@ public static function decrypt( string $value ): string {
return false === $plain ? '' : $plain;
}

/**
* Peel every layer of encryption off a value.
*
* Versions 2.0.0 and 2.0.1 re-encrypted the stored client secret on each
* settings write, leaving multi-layered cipher text that a single
* `decrypt()` cannot recover. This unwraps repeatedly until the result is
* no longer cipher text, so an affected install can be repaired in place
* rather than forcing the operator to find their credentials again.
*
* @param string $value Stored value, possibly encrypted more than once.
* @param int $layers Safety ceiling on unwrapping passes.
* @return string Plain text value, or an empty string when unrecoverable.
*/
public static function decrypt_deep( string $value, int $layers = 12 ): string {
$current = $value;

for ( $i = 0; $i < $layers; $i++ ) {
if ( ! self::is_encrypted( $current ) ) {
return $current;
}

$next = self::decrypt( $current );

// A layer that will not unwrap means the value is unrecoverable.
if ( '' === $next || $next === $current ) {
return '';
}

$current = $next;
}

// Still cipher text after the ceiling: treat as unrecoverable.
return self::is_encrypted( $current ) ? '' : $current;
}

/**
* How many times a value has been encrypted.
*
* @param string $value Stored value.
* @param int $layers Safety ceiling on counting passes.
* @return int Number of encryption layers; 0 for plain text.
*/
public static function depth( string $value, int $layers = 12 ): int {
$current = $value;
$depth = 0;

for ( $i = 0; $i < $layers; $i++ ) {
if ( ! self::is_encrypted( $current ) ) {
break;
}

$next = self::decrypt( $current );

if ( '' === $next || $next === $current ) {
return $depth + 1;
}

$current = $next;
++$depth;
}

return $depth;
}

/**
* Mask a secret for display, revealing only the final four characters.
*
Expand Down
Loading
Loading