$params The request's arguments as received, passed to the * `releasecart_update_check_release` filter. * @return array The assembled payload, after the `releasecart_update_check_response` filter. * * @throws ProductNotFoundException When no product carries the slug, or the product is not * published and the key is not valid for it (controller: generic 404). * @throws LicensePersistenceException When license validation faults with nothing * in the grace store (controller: generic 500, * the client's own 72-hour grace takes over). */ public function check( string $productSlug, string $licenseKey, ?string $siteUrl, ?string $installedVersion, ?DateTimeImmutable $now = null, array $params = array() ): array { $now = $now ?? new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) ); $product = $this->products->findBySlug( $productSlug ); if ( null === $product ) { throw new ProductNotFoundException( 'No product for update-check slug.' ); } $result = $this->validator->validate( $licenseKey, $siteUrl, null, $productSlug, $now ); // A draft or archived product shows its version only to a valid key. if ( ProductStatus::Published !== $product->status() && ! $result->isValid() ) { throw new ProductNotFoundException( 'No published product for update-check slug.' ); } $siteHash = $this->siteHash( $siteUrl ); // Core serves its latest published release unless an extension changes it. $latest = $this->releases->latestPublished( (int) $product->id() ); /** * Filters the release served by update-check. * * Return any release of the product that is not yanked, or null for the no-update * response. `params` is raw request input: it holds the license key and any argument * the client sent. Sanitize what you read and never log the context. * * @since 1.0.0 * * @param Release|null $release The release core would serve (its latest published). * @param array $context Context: `product` (Product), `params` (array, the * request's arguments as received), `site_hash` * (string|null, a stable hash of the site URL), * `installed_version` (string|null), `result` * (ValidationResult). */ $release = apply_filters( 'releasecart_update_check_release', $latest, array( 'product' => $product, 'params' => $params, 'site_hash' => $siteHash, 'installed_version' => $installedVersion, 'result' => $result, ) ); $payload = $this->assemble( $product, $release, $result, $licenseKey, $siteUrl, $installedVersion ); /** * Filters the completed update-check response. * * @since 1.0.0 * * @param array $payload The assembled response payload. * @param array $context Context: `product` (Product), `release` (Release|null, the * served release), `result` (ValidationResult), `site_hash` * (string|null), `installed_version` (string|null). */ return apply_filters( 'releasecart_update_check_response', $payload, array( 'product' => $product, 'release' => $release, 'result' => $result, 'site_hash' => $siteHash, 'installed_version' => $installedVersion, ) ); } // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber /** * Build the raw payload for a resolved product, release, and verdict. * * @param Product $product The catalog product. * @param Release|null $release The release to serve, if any. * @param ValidationResult $result The license verdict. * @param string $licenseKey The presented license key. * @param string|null $siteUrl The presenting site's URL. * @param string|null $installedVersion The client's installed version. * @return array The unfiltered payload. */ private function assemble( Product $product, ?Release $release, ValidationResult $result, string $licenseKey, ?string $siteUrl, ?string $installedVersion ): array { $payload = array( 'name' => $product->name(), 'slug' => $product->updateSlug() ?? $product->slug(), ); if ( null === $release ) { // Keep the license verdict when no release exists. $payload['update_available'] = false; $payload['license'] = $this->licenseBlock( $result ); return $payload; } $payload['new_version'] = $release->version(); $payload['requires'] = $release->requiresWp(); $payload['tested'] = $release->testedUpTo(); $payload['requires_php'] = $release->requiresPhp(); $payload['last_updated'] = null === $release->releasedAt() ? null : $release->releasedAt()->setTimezone( new DateTimeZone( 'UTC' ) )->format( 'Y-m-d\TH:i:s\Z' ); // Offer the update when the installed version is unknown. $payload['update_available'] = null === $installedVersion || '' === $installedVersion ? true : version_compare( $release->version(), $installedVersion, '>' ); $payload['sections'] = array( 'changelog' => $this->renderer->renderCached( (int) $release->id(), $release->changelog() ?? '' ), ); $payload['license'] = $this->licenseBlock( $result ); if ( ! $result->isValid() ) { $payload['upgrade_notice'] = 'expired' === $result->status() ? __( 'Your license has expired. To receive this update, buy a new license and use its key.', 'releasecart' ) : __( 'A valid license is required to receive this update.', 'releasecart' ); return $payload; } $package = $this->packageUrl( $product, $release, $licenseKey, $siteUrl ); if ( null !== $package ) { $payload['package'] = $package; } return $payload; } /** * The two-field license block every response carries. * * @param ValidationResult $result The license verdict. * @return array{valid: bool, status: string} The block. */ private function licenseBlock( ValidationResult $result ): array { return array( 'valid' => $result->isValid(), 'status' => $result->status(), ); } /** * Mint a signed package URL for a valid license. * * @param Product $product The catalog product. * @param Release $release The release being granted. * @param string $licenseKey The presented license key. * @param string|null $siteUrl The presenting site's URL. * @return string|null The signed URL, or null when no grant can be minted. */ private function packageUrl( Product $product, Release $release, string $licenseKey, ?string $siteUrl ): ?string { $siteHash = $this->siteHash( $siteUrl ); if ( null === $siteHash ) { return null; } try { $license = $this->licenses->findByKey( $licenseKey ); } catch ( LicensePersistenceException $e ) { return null; } if ( null === $license || null === $license->id() ) { return null; } return $this->signer->packageDownloadUrl( (int) $product->id(), (int) $release->id(), (int) $license->id(), $siteHash ); } /** * Get a normalized site hash. * * @param string|null $siteUrl The presenting site's URL. * @return string|null The 64-hex site hash, or null. */ private function siteHash( ?string $siteUrl ): ?string { if ( null === $siteUrl || '' === trim( $siteUrl ) ) { return null; } return $this->normalizer->hash( $this->normalizer->normalize( $siteUrl ) ); } }