Google Calendar Sync only works while the access token has not expired #5322

Open
opened 2026-02-20 16:59:37 -05:00 by deekerman · 3 comments
Owner

Originally created by @Guiferreira2000 on GitHub (May 13, 2025).

Issue

https://community.suitecrm.com/t/google-calendar-sync-only-works-once/91880/7

I’m experiencing the following critical issue with Google Calendar sync in SuiteCRM 8.8 on PHP 8.3, and I want to clarify that this is not related to scheduler misconfiguration — the scheduled sync is running perfectly every minute.

The core issue:
The OAuth access token is being refreshed successfully and appears to be saved using setPreference() and savePreferencesToDB(). But despite this, every subsequent run treats the token as expired, resulting in the same cycle:

Scheduler runs and loads the access token from user preferences
isAccessTokenExpired() returns true
A new token is fetched and logged as successfully saved
On the next run, it repeats — never reusing the token just saved
What I’ve discovered:
This isn’t just a logic bug — it appears to be tied to SuiteCRM’s session behavior, particularly in CLI (cron) context:

Either the token isn’t actually saved to the DB despite the success log, or
The session data ($_SESSION[...]) used to track preference state is reset or not properly restored when cron runs again
So even though setPreference() sets the value and savePreferencesToDB() reports success, the system still loads an outdated or blank token on the next run. This suggests the refreshed token isn’t truly being persisted or is being lost between runs — possibly because SuiteCRM stores preferences in session and relies on session state even in CLI mode.

Why this matters:
This did not happen in SuiteCRM 8.3 under the same environment. It only started after upgrading to 8.8. So this looks like a regression, possibly due to session changes or stricter behavior introduced in 8.8 or triggered by PHP 8.3.

Possible Fix

The OAuth logic needs to force-load preferences from the database explicitly during cron execution and not rely on session state to persist values between runs.

Steps to Reproduce the Issue

1.Authorize syncronization with google calendar
2. Syncronization will work will access token is active. (Aprox. 1 hour)
3. After 1 hour the cronjob in getGoogleClient in googlesyncbase.php will try to update the access token using the stored refreshtoken but either the googlesync or global content in session will be cleaned out. Depending on how is set the following line in googlesyncbase.php

                $this->workingUser->setPreference('GoogleApiToken', base64_encode(json_encode($client->getAccessToken())), 'GoogleSync');
...

Context

No response

Version

SuiteCRM 8.8

What browser are you currently using?

Chrome

Browser Version

No response

Environment Information

MySQL, php 8.3

Operating System and Version

Debian 12

Originally created by @Guiferreira2000 on GitHub (May 13, 2025). ### Issue https://community.suitecrm.com/t/google-calendar-sync-only-works-once/91880/7 I’m experiencing the following critical issue with Google Calendar sync in SuiteCRM 8.8 on PHP 8.3, and I want to clarify that this is not related to scheduler misconfiguration — the scheduled sync is running perfectly every minute. The core issue: The OAuth access token is being refreshed successfully and appears to be saved using setPreference() and savePreferencesToDB(). But despite this, every subsequent run treats the token as expired, resulting in the same cycle: Scheduler runs and loads the access token from user preferences isAccessTokenExpired() returns true A new token is fetched and logged as successfully saved On the next run, it repeats — never reusing the token just saved What I’ve discovered: This isn’t just a logic bug — it appears to be tied to SuiteCRM’s session behavior, particularly in CLI (cron) context: Either the token isn’t actually saved to the DB despite the success log, or The session data ($_SESSION[...]) used to track preference state is reset or not properly restored when cron runs again So even though setPreference() sets the value and savePreferencesToDB() reports success, the system still loads an outdated or blank token on the next run. This suggests the refreshed token isn’t truly being persisted or is being lost between runs — possibly because SuiteCRM stores preferences in session and relies on session state even in CLI mode. Why this matters: This did not happen in SuiteCRM 8.3 under the same environment. It only started after upgrading to 8.8. So this looks like a regression, possibly due to session changes or stricter behavior introduced in 8.8 or triggered by PHP 8.3. ### Possible Fix The OAuth logic needs to force-load preferences from the database explicitly during cron execution and not rely on session state to persist values between runs. ### Steps to Reproduce the Issue ```bash 1.Authorize syncronization with google calendar 2. Syncronization will work will access token is active. (Aprox. 1 hour) 3. After 1 hour the cronjob in getGoogleClient in googlesyncbase.php will try to update the access token using the stored refreshtoken but either the googlesync or global content in session will be cleaned out. Depending on how is set the following line in googlesyncbase.php $this->workingUser->setPreference('GoogleApiToken', base64_encode(json_encode($client->getAccessToken())), 'GoogleSync'); ... ``` ### Context _No response_ ### Version SuiteCRM 8.8 ### What browser are you currently using? Chrome ### Browser Version _No response_ ### Environment Information MySQL, php 8.3 ### Operating System and Version Debian 12
Author
Owner

@SuiteBot commented on GitHub (May 13, 2025):

This issue has been mentioned on SuiteCRM. There might be relevant details there:

https://community.suitecrm.com/t/google-calendar-sync-only-works-once/91880/10

@SuiteBot commented on GitHub (May 13, 2025): This issue has been mentioned on **SuiteCRM**. There might be relevant details there: https://community.suitecrm.com/t/google-calendar-sync-only-works-once/91880/10
Author
Owner

@techsurrusr commented on GitHub (May 14, 2025):

Confirm issue on my side as well with a scratch installation of v8.8.0: https://community.suitecrm.com/t/google-calendar-integration-sync-issues/99313/1

@techsurrusr commented on GitHub (May 14, 2025): Confirm issue on my side as well with a scratch installation of v8.8.0: https://community.suitecrm.com/t/google-calendar-integration-sync-issues/99313/1
Author
Owner

@Guiferreira2000 commented on GitHub (May 20, 2025):

Solution for Sessions for Google sync users

Issue 1

The proposed fix suggested on https://github.com/salesagility/SuiteCRM/issues/10637 still won’t work, because in reloadPreferences() in the modules/UserPreferences/UserPreference.php that inner if

        if ($row) {
            if (!$GLOBALS['current_user']->id || $GLOBALS['current_user']->user_name === $user->user_name){
                $_SESSION[$user->user_name . '_PREFERENCES'][$category] = unserialize(base64_decode($row['contents']));
            }
            $user->user_preferences[$category] = unserialize(base64_decode($row['contents']));
            return true;
        } else {
            if (!$GLOBALS['current_user']->id || $GLOBALS['current_user']->user_name === $user->user_name){
                $_SESSION[$user->user_name . '_PREFERENCES'][$category] = array();
            }
            $user->user_preferences[$category] = array();
        }

will never be true for your Google‐Sync user when run under cron, since current_user is always the admin. As a result, the sync user’s session bucket never gets populated—instead it falls through to the “else” and gets reset to an empty array. That change was introduced in commit ea756b13f0 (“Add check for current user on session preferences”) and has broken Google‐Sync users

Fix

Or remove the condition completely

if (!$GLOBALS['current_user']->id || $GLOBALS['current_user']->user_name === $user->user_name){

Or add an extra condition like

if (PHP_SAPI === 'cli' || $GLOBALS['current_user']->user_name === $user->user_name){

Whenever PHP is invoked from the command‐line (for example by your cron job), its “Server API” string (PHP_SAPI) is set to cli. In contrast, when PHP runs as an Apache module it might be apache2handler, or under PHP‐FPM it’s fpm‐cgi, etc.

This will always be true in the cron context, because you’re literally calling php -f cron.php (the CLI binary), not going through the web server. That guarantees your session‐reload logic fires under cron even though current_user is still “admin.

Issue 2

On the other hand, the function getGoogleClient in the file include/GoogleSync/GoogleSyncBase.php needs also a small fix for code optimization so it does not refresh the Google API token every minute. To fix this, pass the “no-session” flag (0) as the third argument and the category as the fourth:

By changing from

                $this->workingUser->setPreference('GoogleApiToken', base64_encode(json_encode($client->getAccessToken())), 'GoogleSync');

to

$this->workingUser->setPreference('GoogleApiToken', base64_encode(json_encode($client->getAccessToken())), 0,'GoogleSync');

you are making sure that the generated GoogleApiToken is actually saved under the user preference GoogleSync and not Global

If you make this change the token is saved under the GoogleSync category, so subsequent runs will load the existing token and only refresh when it’s actually expired.

The current behaviour means that the function getGoogleClient always perceives the GoogleApiToken as expired and forces the token to be reloaded because it cannot find it in the GoogleSync preference.

@Guiferreira2000 commented on GitHub (May 20, 2025): # Solution for Sessions for Google sync users ## Issue 1 The proposed fix suggested on https://github.com/salesagility/SuiteCRM/issues/10637 still won’t work, because in reloadPreferences() in the _modules/UserPreferences/UserPreference.php_ that inner if ```sh if ($row) { if (!$GLOBALS['current_user']->id || $GLOBALS['current_user']->user_name === $user->user_name){ $_SESSION[$user->user_name . '_PREFERENCES'][$category] = unserialize(base64_decode($row['contents'])); } $user->user_preferences[$category] = unserialize(base64_decode($row['contents'])); return true; } else { if (!$GLOBALS['current_user']->id || $GLOBALS['current_user']->user_name === $user->user_name){ $_SESSION[$user->user_name . '_PREFERENCES'][$category] = array(); } $user->user_preferences[$category] = array(); } ``` will never be true for your Google‐Sync user when run under cron, since current_user is always the admin. As a result, the sync user’s session bucket never gets populated—instead it falls through to the “else” and gets reset to an empty array. That change was introduced in commit ea756b13f0ae606997c0ccb4382e1a72902a9207 (“Add check for current user on session preferences”) and has broken Google‐Sync users ## Fix Or remove the condition completely ```sh if (!$GLOBALS['current_user']->id || $GLOBALS['current_user']->user_name === $user->user_name){ ``` Or add an extra condition like ```sh if (PHP_SAPI === 'cli' || $GLOBALS['current_user']->user_name === $user->user_name){ ``` Whenever PHP is invoked from the command‐line (for example by your cron job), its “Server API” string (PHP_SAPI) is set to cli. In contrast, when PHP runs as an Apache module it might be apache2handler, or under PHP‐FPM it’s fpm‐cgi, etc. This will always be true in the cron context, because you’re literally calling php -f cron.php (the CLI binary), not going through the web server. That guarantees your session‐reload logic fires under cron even though current_user is still “admin. ## Issue 2 On the other hand, the function getGoogleClient in the file include/GoogleSync/GoogleSyncBase.php needs also a small fix for code optimization so it does not refresh the Google API token every minute. To fix this, pass the “no-session” flag (0) as the third argument and the category as the fourth: By changing from ```sh $this->workingUser->setPreference('GoogleApiToken', base64_encode(json_encode($client->getAccessToken())), 'GoogleSync'); ``` to ```sh $this->workingUser->setPreference('GoogleApiToken', base64_encode(json_encode($client->getAccessToken())), 0,'GoogleSync'); ``` you are making sure that the generated GoogleApiToken is actually saved under the user preference _GoogleSync_ and not _Global_ If you make this change the token is saved under the GoogleSync category, so subsequent runs will load the existing token and only refresh when it’s actually expired. The current behaviour means that the function getGoogleClient always perceives the GoogleApiToken as expired and forces the token to be reloaded because it cannot find it in the _GoogleSync_ preference.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/SuiteCRM-SuiteCRM#5322
No description provided.