Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
File Manager
/
wp-content
/
plugins
/
woocommerce
/
src
/
Admin
/
RemoteInboxNotifications
:
RemoteInboxNotificationsDataSourcePoller.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?php /** * Handles polling and storage of specs */ namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\RemoteSpecs\DataSourcePoller; use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\GetRuleProcessor; use WC_Helper; /** * Specs data source poller class. * This handles polling specs from JSON endpoints, and * stores the specs in to the database as an option. */ class RemoteInboxNotificationsDataSourcePoller extends DataSourcePoller { const ID = 'remote_inbox_notifications'; /** * Transient holding the latest read's fetch time and published slugs, or null slugs for an inconclusive read. */ private const PUBLISHED_SNAPSHOT_TRANSIENT = 'woocommerce_admin_remote_inbox_notifications_published_snapshot'; /** * Published identities from this read's canonical response, or null when it is not authoritative. * * @var string[]|null */ private $published_slugs = null; /** * Default data sources array. * * @deprecated since 9.5.0. Use get_data_sources() instead. */ const DATA_SOURCES = array(); /** * Class instance. * * @var RemoteInboxNotificationsDataSourcePoller instance */ protected static $instance = null; /** * Get class instance. */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self( self::ID, self::get_data_sources(), array( 'spec_key' => 'slug', 'transient_expiry' => DAY_IN_SECONDS, ) ); } return self::$instance; } /** * Read the data sources and record the identities published by the canonical WooCommerce.com feed. * * Invalid specs with usable slugs still count as published. An inconclusive response retains only its timestamp to reject older reads. * * @since 11.3.0 * * @return bool Whether any specs were read. */ public function read_specs_from_data_sources() { $fetched_at = time(); $this->published_slugs = null; $result = parent::read_specs_from_data_sources(); $this->save_published_snapshot( $fetched_at, $this->published_slugs ); return $result; } /** * Get the published identities from the latest complete canonical response. * * Inconclusive reads store their fetch time with null slugs to reject older reads, but this method returns null for them. * * @internal * @since 11.3.0 * * @return array{fetched_at: int, slugs: string[]}|null The snapshot, or null when the latest read is inconclusive or absent. */ public function get_published_snapshot(): ?array { $snapshot = get_transient( self::PUBLISHED_SNAPSHOT_TRANSIENT ); if ( ! is_array( $snapshot ) || ! is_int( $snapshot['fetched_at'] ?? null ) || ! is_array( $snapshot['slugs'] ?? null ) ) { return null; } return array( 'fetched_at' => $snapshot['fetched_at'], 'slugs' => array_map( 'strval', array_values( array_filter( $snapshot['slugs'], 'is_scalar' ) ) ), ); } /** * Merge runnable specs and track canonical publication identities independently of validation. * * @since 11.3.0 * * @param array $specs_to_merge_in The specs to merge in to $specs. * @param array $specs The list of specs being merged into. * @param string $url The url of the feed being merged in. * @return void */ protected function merge_specs( $specs_to_merge_in, &$specs, $url ) { parent::merge_specs( $specs_to_merge_in, $specs, $url ); if ( ! in_array( $url, self::get_data_sources(), true ) ) { return; } $this->published_slugs = null; $slugs = array(); foreach ( $specs_to_merge_in as $spec ) { if ( ! isset( $spec->slug ) || ! is_scalar( $spec->slug ) || '' === (string) $spec->slug ) { return; } $slug = (string) $spec->slug; if ( isset( $slugs[ $slug ] ) ) { wc_get_logger()->warning( 'Skipping Store Alert withdrawal cleanup because the canonical feed contains a duplicate slug.', array( 'source' => $url, 'slug' => $slug, ) ); return; } $slugs[ $slug ] = $slug; } // An empty list is inconclusive: WooCommerce.com also serves one when its own notifications query fails. if ( ! empty( $slugs ) ) { $this->published_slugs = array_values( $slugs ); } } /** * Store a read's publication evidence unless a newer read is already stored. * * @param int $fetched_at When the read started. * @param string[]|null $slugs Published slugs, or null for an inconclusive read. */ private function save_published_snapshot( int $fetched_at, ?array $slugs ): void { $stored = get_transient( self::PUBLISHED_SNAPSHOT_TRANSIENT ); if ( is_array( $stored ) && is_int( $stored['fetched_at'] ?? null ) && $stored['fetched_at'] > $fetched_at ) { return; } set_transient( self::PUBLISHED_SNAPSHOT_TRANSIENT, array( 'fetched_at' => $fetched_at, 'slugs' => $slugs, ), $this->args['transient_expiry'] ); } /** * Validate the spec. * * @param object $spec The spec to validate. * @param string $url The url of the feed that provided the spec. * * @return bool The result of the validation. */ protected function validate_spec( $spec, $url ) { $logger = self::get_logger(); $logger_context = array( 'source' => $url ); if ( ! isset( $spec->slug ) || ! is_scalar( $spec->slug ) || '' === (string) $spec->slug ) { $logger->error( 'Spec is invalid because the slug is missing or invalid in feed', $logger_context ); // phpcs:ignore $logger->error( print_r( $spec, true ), $logger_context ); return false; } if ( ! isset( $spec->status ) ) { $logger->error( 'Spec is invalid because the status is missing in feed', $logger_context ); // phpcs:ignore $logger->error( print_r( $spec, true ), $logger_context ); return false; } if ( ! isset( $spec->locales ) || ! is_array( $spec->locales ) ) { $logger->error( 'Spec is invalid because the status is missing or empty in feed', $logger_context ); // phpcs:ignore $logger->error( print_r( $spec, true ), $logger_context ); return false; } if ( null === SpecRunner::get_locale( $spec->locales ) ) { $logger->error( 'Spec is invalid because the locale could not be retrieved in feed', $logger_context ); // phpcs:ignore $logger->error( print_r( $spec, true ), $logger_context ); return false; } if ( ! isset( $spec->type ) ) { $logger->error( 'Spec is invalid because the type is missing in feed', $logger_context ); // phpcs:ignore $logger->error( print_r( $spec, true ), $logger_context ); return false; } if ( isset( $spec->actions ) && is_array( $spec->actions ) ) { foreach ( $spec->actions as $action ) { if ( ! $this->validate_action( $action, $url ) ) { $logger->error( 'Spec is invalid because an action is invalid in feed', $logger_context ); // phpcs:ignore $logger->error( print_r( $spec, true ), $logger_context ); return false; } } } if ( isset( $spec->rules ) && is_array( $spec->rules ) ) { foreach ( $spec->rules as $rule ) { if ( ! isset( $rule->type ) ) { $logger->error( 'Spec is invalid because a rule type is empty in feed', $logger_context ); // phpcs:ignore $logger->error( print_r( $rule, true ), $logger_context ); // phpcs:ignore $logger->error( print_r( $spec, true ), $logger_context ); return false; } $processor = GetRuleProcessor::get_processor( $rule->type ); if ( ! $processor->validate( $rule ) ) { $logger->error( 'Spec is invalid because a rule is invalid in feed', $logger_context ); // phpcs:ignore $logger->error( print_r( $rule, true ), $logger_context ); // phpcs:ignore $logger->error( print_r( $spec, true ), $logger_context ); return false; } } } return true; } /** * Validate the action. * * @param object $action The action to validate. * @param string $url The url of the feed containing the action (for error reporting). * * @return bool The result of the validation. */ private function validate_action( $action, $url ) { $logger = self::get_logger(); $logger_context = array( 'source' => $url ); if ( ! isset( $action->locales ) || ! is_array( $action->locales ) ) { $logger->error( 'Action is invalid because it has empty or missing locales in feed', $logger_context ); // phpcs:ignore $logger->error( print_r( $action, true ), $logger_context ); return false; } if ( null === SpecRunner::get_action_locale( $action->locales ) ) { $logger->error( 'Action is invalid because the locale could not be retrieved in feed', $logger_context ); // phpcs:ignore $logger->error( print_r( $action, true ), $logger_context ); return false; } if ( ! isset( $action->name ) ) { $logger->error( 'Action is invalid because the name is missing in feed', $logger_context ); // phpcs:ignore $logger->error( print_r( $action, true ), $logger_context ); return false; } if ( ! isset( $action->status ) ) { $logger->error( 'Action is invalid because the status is missing in feed', $logger_context ); // phpcs:ignore $logger->error( print_r( $action, true ), $logger_context ); return false; } return true; } /** * Get data sources. * * @return array */ public static function get_data_sources() { return array( WC_Helper::get_woocommerce_com_base_url() . 'wp-json/wccom/inbox-notifications/2.0/notifications.json', ); } }