diff --git a/php/class-connect.php b/php/class-connect.php index d35bb2a89..216e0164c 100644 --- a/php/class-connect.php +++ b/php/class-connect.php @@ -447,7 +447,13 @@ function ( $a ) { public function history( $days = 1 ) { $return = array(); $history = get_option( self::META_KEYS['history'], array() ); - $plan = ! empty( $this->usage['plan'] ) ? $this->usage['plan'] : $this->credentials['cloud_name']; + // Stored option can be a non-array if it was corrupted by a previous + // failed write. Reset to empty array to avoid string-offset fatals. + if ( ! is_array( $history ) ) { + $history = array(); + } + $plan_source = is_array( $this->usage ) && ! empty( $this->usage['plan'] ) ? $this->usage['plan'] : ''; + $plan = '' !== $plan_source ? $plan_source : ( isset( $this->credentials['cloud_name'] ) ? $this->credentials['cloud_name'] : '' ); for ( $i = 1; $i <= $days; $i++ ) { $date = date_i18n( 'd-m-Y', strtotime( '- ' . $i . ' days' ) ); if ( ! isset( $history[ $plan ][ $date ] ) || is_wp_error( $history[ $plan ][ $date ] ) ) { @@ -720,9 +726,14 @@ public function usage_stats( $refresh = false ) { $last_usage = $this->settings->get_setting( 'last_usage' ); // Get users plan. $stats = $this->api->usage(); - if ( ! is_wp_error( $stats ) && ! empty( $stats['media_limits'] ) ) { - $stats['max_image_size'] = $stats['media_limits']['image_max_size_bytes']; - $stats['max_video_size'] = $stats['media_limits']['video_max_size_bytes']; + if ( + ! is_wp_error( $stats ) + && is_array( $stats ) + && isset( $stats['media_limits'] ) + && is_array( $stats['media_limits'] ) + ) { + $stats['max_image_size'] = isset( $stats['media_limits']['image_max_size_bytes'] ) ? $stats['media_limits']['image_max_size_bytes'] : 0; + $stats['max_video_size'] = isset( $stats['media_limits']['video_max_size_bytes'] ) ? $stats['media_limits']['video_max_size_bytes'] : 0; $last_usage->save_value( $stats );// Save the last successful call to prevgent crashing. } else { // Handle error by logging and fetching the last success. diff --git a/php/class-cron.php b/php/class-cron.php index ec0203646..9877c0b0b 100644 --- a/php/class-cron.php +++ b/php/class-cron.php @@ -203,8 +203,15 @@ public function rest_endpoints( $endpoints ) { */ protected function load_schedule() { $this->schedule = get_option( self::CRON_META_KEY, array() ); + // Guard against a corrupted option (e.g. a string was previously + // stored) so the foreach below cannot fatal on PHP 8.x. + if ( ! is_array( $this->schedule ) ) { + $this->schedule = array(); + } foreach ( $this->schedule as &$item ) { - $item['active'] = false; + if ( is_array( $item ) ) { + $item['active'] = false; + } } } diff --git a/php/class-media.php b/php/class-media.php index 2bd521279..0d919ce24 100644 --- a/php/class-media.php +++ b/php/class-media.php @@ -446,15 +446,26 @@ public function is_oversize_media( $attachment_id ) { return $is_oversize[ $attachment_id ]; } - $file_size = $this->get_attachment_file_size( $attachment_id ); - $max_size = ( wp_attachment_is_image( $attachment_id ) ? 'image_max_size_bytes' : 'video_max_size_bytes' ); - $limit = $this->plugin->components['connect']->usage['media_limits'][ $max_size ]; + $file_size = $this->get_attachment_file_size( $attachment_id ); + $max_size_key = ( wp_attachment_is_image( $attachment_id ) ? 'image_max_size_bytes' : 'video_max_size_bytes' ); + $usage = $this->plugin->components['connect']->usage; + $media_limits = is_array( $usage ) && isset( $usage['media_limits'] ) ? $usage['media_limits'] : null; + + // If the limit isn't available yet (e.g. plugin not connected, or API + // returned an unexpected response), treat the asset as not oversize + // rather than fatal-erroring on a string offset. + if ( ! is_array( $media_limits ) || ! isset( $media_limits[ $max_size_key ] ) ) { + $is_oversize[ $attachment_id ] = false; + + return $is_oversize[ $attachment_id ]; + } + + $limit = $media_limits[ $max_size_key ]; $is_oversize[ $attachment_id ] = $file_size > $limit; if ( $is_oversize[ $attachment_id ] ) { - $max_size = ( wp_attachment_is_image( $attachment_id ) ? 'image_max_size_bytes' : 'video_max_size_bytes' ); - $max_size_hr = size_format( $this->plugin->components['connect']->usage['media_limits'][ $max_size ] ); + $max_size_hr = size_format( $limit ); // translators: variable is file size. $message = sprintf( __( 'File size exceeds the maximum of %s. This media asset will be served from WordPress.', 'cloudinary' ), $max_size_hr ); update_post_meta( $attachment_id, Sync::META_KEYS['sync_error'], $message ); diff --git a/php/class-special-offer.php b/php/class-special-offer.php index 075a99fb5..a703d2418 100644 --- a/php/class-special-offer.php +++ b/php/class-special-offer.php @@ -98,6 +98,12 @@ public function filtered_settings( $settings ) { protected function is_special_offer_available() { $last_usage = get_option( Connect::META_KEYS['last_usage'], array( 'plan' => '' ) ); + // `last_usage` can be a string when the usage API has never returned a + // valid response. Only an array with a `plan` key is meaningful here. + if ( ! is_array( $last_usage ) || ! isset( $last_usage['plan'] ) ) { + return false; + } + return 'free' === strtolower( $last_usage['plan'] ); } diff --git a/php/integrations/class-wpml.php b/php/integrations/class-wpml.php index 56199c037..1f576f174 100644 --- a/php/integrations/class-wpml.php +++ b/php/integrations/class-wpml.php @@ -102,6 +102,11 @@ public function wp_generate_attachment_metadata( $metadata, $attachment_id, $con $original_attachment_id = $attachment_id; $data = get_transient( self::TRANSIENT_KEY ); + // Cast to array to avoid string-offset fatals on PHP 8.x when the + // transient holds an unexpected scalar. + if ( ! is_array( $data ) ) { + $data = array(); + } // This is a duplicated attachment. Let's restore the metadata via WPML. if ( ! empty( $data[ $original_attachment_id ] ) ) { diff --git a/php/sync/class-sync-queue.php b/php/sync/class-sync-queue.php index 220b9b7c5..883ff1567 100644 --- a/php/sync/class-sync-queue.php +++ b/php/sync/class-sync-queue.php @@ -699,8 +699,10 @@ public function get_thread_queue( $thread ) { ); wp_cache_delete( $thread_option, 'options' ); $return = get_option( $thread_option ); - if ( empty( $return ) ) { - // Set option to remove notoption and default fro cache. + if ( empty( $return ) || ! is_array( $return ) ) { + // Reset to default when missing or corrupted (e.g. a string was + // previously stored). Without the is_array check, array_merge + // below would fatal on PHP 8.x. $this->set_thread_queue( $thread, $default ); $return = $default; } diff --git a/php/ui/component/class-plan-details.php b/php/ui/component/class-plan-details.php index f4fea528d..ac3f1d0f7 100644 --- a/php/ui/component/class-plan-details.php +++ b/php/ui/component/class-plan-details.php @@ -68,10 +68,20 @@ protected function plan( $struct ) { return $struct; } + // `last_usage` can be a string when the usage API has never returned a + // valid response (e.g. fresh install or API error). Without this + // guard, PHP 8.x fatals on string-offset access below. + if ( ! is_array( $data ) ) { + $data = array(); + } + + $plan_name = isset( $data['plan'] ) ? $data['plan'] : ''; + $requests = isset( $data['requests'] ) ? $data['requests'] : 0; + $struct['element'] = 'div'; $struct['attributes']['class'][] = 'cld-plan'; - $struct['children']['plan'] = $this->make_item( __( 'Plan', 'cloudinary' ), $data['plan'], $this->dir_url . 'css/images/star.svg' ); + $struct['children']['plan'] = $this->make_item( __( 'Plan', 'cloudinary' ), $plan_name, $this->dir_url . 'css/images/star.svg' ); if ( $connection->get_usage_stat( 'credits', 'limit' ) ) { $struct = $this->plan_credit( $struct, $connection ); @@ -79,7 +89,7 @@ protected function plan( $struct ) { $struct = $this->plan_classic( $struct, $connection ); } - $struct['children']['requests'] = $this->make_item( __( 'Total Requests', 'cloudinary' ), number_format_i18n( $data['requests'] ), $this->dir_url . 'css/images/requests.svg' ); + $struct['children']['requests'] = $this->make_item( __( 'Total Requests', 'cloudinary' ), number_format_i18n( $requests ), $this->dir_url . 'css/images/requests.svg' ); $struct['children']['assets'] = $this->make_item( __( 'Optimized assets', 'cloudinary' ), number_format_i18n( Sync_Queue::get_optimized_assets() ), $this->dir_url . 'css/images/image.svg' ); return $struct; diff --git a/ui-definitions/settings-sidebar.php b/ui-definitions/settings-sidebar.php index 5fd2c8ad5..a21b3a4cb 100644 --- a/ui-definitions/settings-sidebar.php +++ b/ui-definitions/settings-sidebar.php @@ -23,10 +23,13 @@ $plugin = get_plugin_instance(); $data = $plugin->settings->get_value( 'last_usage' ); $cloud_name = $plugin->components['connect']->get_cloud_name(); + // `last_usage` may be a string when no successful usage API + // response has been recorded yet; fall back to an empty plan name. + $plan_name = is_array( $data ) && isset( $data['plan'] ) ? $data['plan'] : ''; ob_start(); ?> - +
@