\n\nDetails: " (see split()). * * @package Tidelist */ namespace Tidelist\Errors; use Tidelist\Channel\ChannelInterface; use Tidelist\Channel\Exception\ApiException; use Tidelist\Channel\Exception\AuthException; use Tidelist\Channel\Exception\BlockedException; use Tidelist\Channel\Exception\ChannelException; use Tidelist\Channel\Exception\TransportException; use Tidelist\Support\Text; /** * Error explainer. */ final class ErrorExplainer { const DETAILS_MARK = "\n\nDetails: "; /** * Catalogue entries: match, what, fix. * * @var array */ private $catalogue; /** * Marketplace name. * * @var string */ private $label; /** * Constructor. * * @param array $catalogue Known cases. * @param string $label Marketplace name. */ public function __construct( array $catalogue, string $label ) { $this->catalogue = $catalogue; $this->label = $label; } /** * Explainer for a channel. * * @param ChannelInterface $channel Channel. */ public static function for_channel( ChannelInterface $channel ): self { return new self( $channel->error_catalogue(), $channel->label() ); } /** * Readable text for an exception, ready to store. * * @param ChannelException $e Exception. */ public function explain_exception( ChannelException $e ): string { $status = $e instanceof ApiException ? $e->status() : 0; $raw = $e->getMessage(); if ( $e instanceof ApiException ) { foreach ( $e->validation() as $item ) { $raw .= "\n" . rtrim( $item['field'] . ': ' . $item['title'] ); } } $known = $this->lookup( $status, $raw ); if ( null !== $known ) { return self::compose( $known['what'] . ' ' . $known['fix'], $raw ); } return self::compose( $this->generic( $e ), $raw ); } /** * Readable text for a marketplace's own message (for example a moderation reason). A message the catalogue does * not know is returned unchanged, because it is already the marketplace's explanation. * * @param string $raw The marketplace's words. */ public function explain_message( string $raw ): string { $raw = trim( $raw ); if ( '' === $raw ) { return ''; } $known = $this->lookup( 0, $raw ); return null === $known ? $raw : self::compose( $known['what'] . ' ' . $known['fix'], $raw ); } /** * Splits stored text into the readable part and the details. * * @param string $stored Stored text. * @return array{0:string, 1:string} Readable text, raw details ('' when there are none). */ public static function split( string $stored ): array { $at = strpos( $stored, self::DETAILS_MARK ); if ( false === $at ) { return array( trim( $stored ), '' ); } return array( trim( substr( $stored, 0, $at ) ), trim( substr( $stored, $at + strlen( self::DETAILS_MARK ) ) ) ); } /** * The first catalogue entry that fits. * * @param int $status HTTP status (0 = none). * @param string $raw Raw text. * @return array{match:string, what:string, fix:string}|null */ private function lookup( int $status, string $raw ): ?array { foreach ( $this->catalogue as $entry ) { $match = (string) $entry['match']; if ( 0 === strpos( $match, 'status:' ) ) { if ( $status > 0 && (int) substr( $match, 7 ) === $status ) { return $entry; } } elseif ( 1 === @preg_match( $match, $raw ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- a bad expression in a catalogue must not break the screen. return $entry; } } return null; } /** * A sentence for a case the catalogue does not know. * * @param ChannelException $e Exception. */ private function generic( ChannelException $e ): string { if ( $e instanceof BlockedException ) { return $e->is_paused() ? __( 'Tidelist is paused, so nothing was sent.', 'tidelist' ) : Text::channel( __( 'The request was held back to stay within the limits of {channel}. It is tried again later.', 'tidelist' ), $this->label ); } if ( $e instanceof AuthException ) { return Text::channel( __( 'Tidelist is not connected to {channel} (or the connection expired). Reconnect the account on the Connect screen.', 'tidelist' ), $this->label ); } if ( $e instanceof TransportException ) { return Text::channel( __( '{channel} could not be reached. Tidelist tries again later.', 'tidelist' ), $this->label ); } return Text::channel( __( '{channel} did not accept this request. Open the details; if you cannot tell what to change, send them to support.', 'tidelist' ), $this->label ); } /** * Readable text plus details. * * @param string $readable Sentence(s). * @param string $raw Raw text. */ private static function compose( string $readable, string $raw ): string { $raw = trim( $raw ); return '' === $raw ? trim( $readable ) : trim( $readable ) . self::DETAILS_MARK . $raw; } }