|null Die Lieferung, null wenn nichts bereitliegt. * @throws ApiException Bei Transport- oder Serverfehlern. */ public function fetch_next( string $job_hash ): ?array { return $this->call( self::ACTION_PUBLISH, array( 'hash' => $job_hash ), true ); } /** * Meldet, wo ein importierter Artikel veroeffentlicht wurde. * * @param string $draft_hash Schluessel des Drafts. * @param string $url Oeffentliche https-Adresse. * @param int $post_id Beitrags-ID. * @param string $body_hash sha256 ueber den gespeicherten Body. * @param string $body Der gespeicherte Body. * @param string[] $capabilities Faehigkeiten dieser Installation. * @param int $published_at Zeitpunkt der Veroeffentlichung (GMT), 0 fuer keinen. * @return array * @throws ApiException Bei Transport- oder Serverfehlern. */ public function report_published( string $draft_hash, string $url, int $post_id, string $body_hash, string $body, array $capabilities, int $published_at = 0 ): array { $parameters = array( 'draft_hash' => $draft_hash, 'url' => $url, 'cms_post_id' => (string) $post_id, 'body_hash' => $body_hash, 'capabilities' => array_values( $capabilities ), ); if ( strlen( $body ) <= self::MAX_BODY_BYTES ) { $parameters['body_canonical'] = $body; } if ( $published_at > 0 ) { // Zod prueft mit z.string().datetime() — das nimmt nur die Form mit // „Z" an, kein „+00:00". $parameters['published_at'] = gmdate( 'Y-m-d\TH:i:s\Z', $published_at ); } return (array) $this->call( self::ACTION_PUBLISHED, $parameters ); } /** * Holt die offenen Patches einer Site und meldet die gehaltenen Entwuerfe. * * @param string $job_hash Import-Job-ID; der Server loest daraus die Site auf. Leer, wenn nur die Site bekannt ist. * @param string $site_hash Site-Hash, leer solange keiner bekannt ist. * @param string[] $capabilities Faehigkeiten dieser Installation. * @param string[] $held_drafts Drafts, die hier als nicht oeffentlicher Beitrag liegen. * @return array{patches: array>, unconfirmed_drafts: string[]} * @throws ApiException Bei Transport- oder Serverfehlern. */ public function fetch_patches( string $job_hash, string $site_hash, array $capabilities, array $held_drafts ): array { $parameters = array( 'capabilities' => array_values( $capabilities ), 'limit' => self::PATCH_LIMIT, 'held_drafts' => array_slice( array_values( array_filter( $held_drafts, array( self::class, 'is_uuid' ) ) ), 0, self::MAX_HELD_DRAFTS ), ); if ( self::is_uuid( $job_hash ) ) { $parameters['job_hash'] = $job_hash; } if ( self::is_uuid( $site_hash ) ) { $parameters['site_hash'] = $site_hash; } $result = (array) $this->call( self::ACTION_PATCHES, $parameters ); return array( 'patches' => isset( $result['patches'] ) && is_array( $result['patches'] ) ? array_values( array_filter( $result['patches'], 'is_array' ) ) : array(), 'unconfirmed_drafts' => isset( $result['unconfirmed_drafts'] ) && is_array( $result['unconfirmed_drafts'] ) ? array_values( array_filter( $result['unconfirmed_drafts'], 'is_string' ) ) : array(), ); } /** * Quittiert einen Patch. * * @param string $patch_id ID aus der Lieferung. * @param string $status applied, stale oder failed. * @param string|null $new_body_hash Hash des gespeicherten Bodys. * @param string|null $new_body Der gespeicherte Body. * @param string|null $message Begruendung, ohne Secrets und ohne Personendaten. * @return array * @throws ApiException Bei Transport- oder Serverfehlern. */ public function report_patch_result( string $patch_id, string $status, ?string $new_body_hash, ?string $new_body, ?string $message ): array { $parameters = array( 'patch_id' => $patch_id, 'status' => $status, ); if ( null !== $new_body_hash ) { $parameters['new_body_hash'] = $new_body_hash; } // Der Server plant einen veralteten Patch gegen genau diesen Body neu. // Deshalb lohnt er sich auch dann, wenn nichts geschrieben wurde. if ( null !== $new_body && null !== $new_body_hash && strlen( $new_body ) <= self::MAX_BODY_BYTES ) { $parameters['new_body_canonical'] = $new_body; } if ( null !== $message && '' !== $message ) { $parameters['message'] = mb_substr( $message, 0, 2000 ); } return (array) $this->call( self::ACTION_PATCH_RESULT, $parameters ); } /** * Liest den Stand der angegebenen Jobs, ohne etwas zu veraendern. * * @param string[] $job_hashes Import-Job-IDs. * @return array * @throws ApiException Bei Transport- oder Serverfehlern. */ public function plugin_status( array $job_hashes ): array { return (array) $this->call( self::ACTION_PLUGIN_STATUS, array( 'job_hashes' => array_slice( array_values( $job_hashes ), 0, 50 ) ) ); } /** * Ob der Wert eine UUID ist. Der Server lehnt alles andere ab. * * @param mixed $value Zu pruefender Wert. * @return bool */ public static function is_uuid( $value ): bool { return is_string( $value ) && 1 === preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $value ); } /** * Fuehrt einen Aufruf aus. * * @param string $action Name der Action. * @param array $parameters Nutzdaten. * @param bool $allow_no_result Ob ein 200 mit success:false „nichts da" bedeutet. * @return array|null * @throws ApiException Bei Transport- oder Serverfehlern. */ private function call( string $action, array $parameters, bool $allow_no_result = false ): ?array { if ( '' === $this->api_key ) { throw new ApiException( esc_html__( 'No Aivor API key is set.', 'aivor-content-import' ), 401 ); } $parameters['api_version'] = 'wp-' . AIVOR_CONTENT_VERSION; $parameters['platform'] = 'wordpress'; // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText -- Maschinenwert, den der Server vergleicht. $response = wp_remote_post( $this->base_url . $action, array( 'timeout' => self::TIMEOUT, 'redirection' => 0, 'headers' => array( 'Accept' => 'application/json', 'Content-Type' => 'application/json', 'X-API-KEY' => $this->api_key, ), 'body' => (string) wp_json_encode( $parameters ), 'data_format' => 'body', ) ); if ( is_wp_error( $response ) ) { throw new ApiException( esc_html( sprintf( /* translators: %s: error message of the HTTP request */ __( 'Aivor could not be reached: %s', 'aivor-content-import' ), $response->get_error_message() ) ) ); } $status = (int) wp_remote_retrieve_response_code( $response ); $reply = json_decode( (string) wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $reply ) ) { throw new ApiException( esc_html( sprintf( /* translators: %d: HTTP status code */ __( 'Aivor sent an unreadable answer (HTTP %d). The plugin tries again in the next run.', 'aivor-content-import' ), $status ) ), (int) $status ); } $server_error = self::server_error( $reply ); if ( $status >= 400 ) { throw new ApiException( esc_html( self::describe_error( $action, $status, $server_error ) ), (int) $status, esc_html( $server_error ) ); } $result = isset( $reply['result'] ) && is_array( $reply['result'] ) ? $reply['result'] : array(); if ( true !== ( $reply['success'] ?? false ) ) { if ( $allow_no_result ) { return null; } throw new ApiException( esc_html( self::describe_error( $action, $status, $server_error ) ), (int) $status, esc_html( $server_error ) ); } return $result; } /** * Der Fehlertext aus der Serverantwort. * * @param array $reply Dekodierte Antwort. * @return string */ private static function server_error( array $reply ): string { foreach ( array( 'error', 'message' ) as $key ) { if ( isset( $reply[ $key ] ) && is_string( $reply[ $key ] ) && '' !== $reply[ $key ] ) { return $reply[ $key ]; } } if ( isset( $reply['result']['message'] ) && is_string( $reply['result']['message'] ) ) { return $reply['result']['message']; } return ''; } /** * Verstaendliche Meldung fuer Protokoll und Job-Fehlerfeld. * * @param string $action Name der Action. * @param int $status HTTP-Status. * @param string $server_error Fehlertext des Servers. * @return string */ private static function describe_error( string $action, int $status, string $server_error ): string { $detail = '' !== $server_error ? ' (' . mb_substr( $server_error, 0, 300 ) . ')' : ''; switch ( true ) { case 401 === $status: return __( 'Aivor rejected the API key. Check it under Aivor® → Content Import → Settings.', 'aivor-content-import' ); case 403 === $status && self::ACTION_PUBLISH === $action: return __( 'The API key belongs to a different Aivor project than this import job. Create the key in the project that contains the job.', 'aivor-content-import' ) . $detail; case 403 === $status: return __( 'Aivor refused access: the API key belongs to a different project.', 'aivor-content-import' ) . $detail; case 404 === $status && self::ACTION_PUBLISH === $action: return __( 'Aivor does not know this import job ID. Copy it again from the cluster or job in Aivor.', 'aivor-content-import' ); case 429 === $status: return __( 'Too many requests to Aivor. The plugin tries again in the next run.', 'aivor-content-import' ); case $status >= 500: return sprintf( /* translators: %d: HTTP status code */ __( 'Aivor reported a server error (HTTP %d). The plugin tries again in the next run.', 'aivor-content-import' ), $status ); default: return sprintf( /* translators: 1: HTTP status code, 2: name of the API action */ __( 'Aivor rejected the request (HTTP %1$d, %2$s).', 'aivor-content-import' ), $status, $action ) . $detail; } } }