$patch Patch aus content/patches. * @return PatchResult */ public function apply( array $patch ): PatchResult { $kind = (string) ( $patch['kind'] ?? '' ); $draft_hash = (string) ( $patch['draft_hash'] ?? '' ); $expected_hash = (string) ( $patch['expected_body_hash'] ?? '' ); if ( '' === $draft_hash || '' === $expected_hash ) { return PatchResult::failed( 'patch is missing draft_hash or expected_body_hash' ); } // 1. Beitrag ueber den draft_hash aufloesen. $post_id = DraftMapping::find_post_id( $draft_hash ); if ( 0 === $post_id ) { return PatchResult::failed( 'draft "' . $draft_hash . '" is unknown to this installation' ); } // 2. Ein Draft gehoert genau einem Beitrag. Widersprechen sich Server und // Installation, wird nichts geschrieben — raten ist keine Option. $cms_post_id = (string) ( $patch['cms_post_id'] ?? '' ); if ( '' !== $cms_post_id && (string) $post_id !== $cms_post_id ) { return PatchResult::failed( 'cms_post_id "' . $cms_post_id . '" does not match the stored post id ' . $post_id, $post_id ); } // 3. Ziel verfuegbar? Ein abgeschalteter Post-Type kann zurueckkommen. $post = get_post( $post_id ); if ( ! $post instanceof \WP_Post ) { return PatchResult::failed( 'the imported object no longer exists', $post_id ); } if ( ! post_type_exists( $post->post_type ) ) { return PatchResult::deferred( 'post type "' . $post->post_type . '" is not registered', $post_id ); } $body = PostWriter::read_body( $post_id ); if ( null === $body ) { return PatchResult::failed( 'the imported object no longer exists', $post_id ); } $hash = PostWriter::hash( $body ); // 4. Redelivery-Kurzschluss: nichts zu tun und vor allem nichts zu schreiben. if ( $this->is_already_applied( $patch, $kind, $body ) ) { return PatchResult::applied( $hash, $body, $post_id, false ); } // 5. Hash-Guard: Der Body hat sich seit der Planung geaendert. if ( $hash !== $expected_hash ) { return PatchResult::stale( $hash, $body, 'stored body differs from expected_body_hash', $post_id ); } // 6. Anwenden. switch ( $kind ) { case 'hub': $new_body = $this->build_hub_body( $patch, $body, $post_id ); break; case 'link_resolve': case 'retrofit': $new_body = $this->build_sentence_body( $patch, $body, $hash, $post_id ); break; default: $new_body = PatchResult::failed( 'unknown patch kind "' . $kind . '"', $post_id ); } if ( $new_body instanceof PatchResult ) { return $new_body; } // 7. Ueber den einzigen Schreibpfad speichern; gehasht wird, was danach // in der Datenbank steht. try { $written = $this->writer->update( $post_id, array( 'post_content' => $new_body ), (int) $post->post_author ); } catch ( \Throwable $error ) { $message = $error->getMessage(); return PatchResult::failed( str_starts_with( $message, 'the object' ) ? $message : 'the object could not be updated: ' . $message, $post_id ); } return PatchResult::applied( $written->hash, $written->body, $post_id, true ); } /** * Ob der Body das Ergebnis dieses Patches bereits traegt. * * @param array $patch Patch. * @param string $kind Art des Patches. * @param string $body Aktueller Body. * @return bool */ private function is_already_applied( array $patch, string $kind, string $body ): bool { if ( 'hub' === $kind ) { $inner = $this->hub_inner_html( $patch, $body ); return null !== $inner && (string) ( $patch['inner_html'] ?? '' ) === $inner; } $new_string = (string) ( $patch['new_string'] ?? '' ); $old_string = (string) ( $patch['old_string'] ?? '' ); if ( '' === $new_string || '' === $old_string || ! str_contains( $body, $new_string ) ) { return false; } // new_string kann auch an anderer Stelle stehen; entscheidend ist, ob // daneben noch ein ungepatchtes old_string uebrig ist. return ! str_contains( str_replace( $new_string, '', $body ), $old_string ); } /** * Ersetzt das einzige Vorkommen von old_string. * * Kein Treffer heisst: der Satz wurde umformuliert — veraltet. Mehr als ein * Treffer heisst: welcher gemeint ist, waere geraten — gescheitert. * Niemals unscharf suchen, niemals normalisieren. * * @param array $patch Patch. * @param string $body Aktueller Body. * @param string $hash Hash des aktuellen Bodys. * @param int $post_id Beitrag. * @return string|PatchResult Neuer Body oder das zu meldende Ergebnis. */ private function build_sentence_body( array $patch, string $body, string $hash, int $post_id ) { $old_string = (string) ( $patch['old_string'] ?? '' ); $new_string = (string) ( $patch['new_string'] ?? '' ); if ( '' === $old_string || '' === $new_string ) { return PatchResult::failed( 'patch is missing old_string or new_string', $post_id ); } $occurrences = substr_count( $body, $old_string ); if ( 0 === $occurrences ) { return PatchResult::stale( $hash, $body, 'old_string was not found in the stored body', $post_id ); } if ( $occurrences > 1 ) { return PatchResult::failed( 'old_string occurs ' . $occurrences . ' times, it must be unique', $post_id ); } $position = (int) strpos( $body, $old_string ); return substr_replace( $body, $new_string, $position, strlen( $old_string ) ); } /** * Ersetzt alles zwischen den Hub-Markern; die Marker bleiben stehen. * * @param array $patch Patch. * @param string $body Aktueller Body. * @param int $post_id Beitrag. * @return string|PatchResult Neuer Body oder das zu meldende Ergebnis. */ private function build_hub_body( array $patch, string $body, int $post_id ) { $position = $this->hub_marker_positions( $patch, $body ); if ( null === $position ) { return PatchResult::failed( 'the hub markers are missing or ambiguous in the stored body', $post_id ); } list( $inner_start, $inner_end ) = $position; return substr_replace( $body, (string) ( $patch['inner_html'] ?? '' ), $inner_start, $inner_end - $inner_start ); } /** * Der aktuelle Inhalt zwischen den Hub-Markern. * * @param array $patch Patch. * @param string $body Aktueller Body. * @return string|null Null, wenn die Marker unbrauchbar sind. */ private function hub_inner_html( array $patch, string $body ): ?string { $position = $this->hub_marker_positions( $patch, $body ); if ( null === $position ) { return null; } return substr( $body, $position[0], $position[1] - $position[0] ); } /** * Die Offsets zwischen den Markern. * * Beide Marker muessen genau einmal und in dieser Reihenfolge vorkommen. * * @param array $patch Patch. * @param string $body Aktueller Body. * @return array{0: int, 1: int}|null */ private function hub_marker_positions( array $patch, string $body ): ?array { $start_marker = (string) ( $patch['marker_start'] ?? '' ); $end_marker = (string) ( $patch['marker_end'] ?? '' ); if ( '' === $start_marker || '' === $end_marker ) { return null; } if ( 1 !== substr_count( $body, $start_marker ) || 1 !== substr_count( $body, $end_marker ) ) { return null; } $start = (int) strpos( $body, $start_marker ); $end = (int) strpos( $body, $end_marker ); $inner_start = $start + strlen( $start_marker ); if ( $end < $inner_start ) { return null; } return array( $inner_start, $end ); } }