delete( $id ); self::redirect( 'deleted' ); } /** * Gibt die Seite aus. * * @return void */ public static function render(): void { if ( ! current_user_can( Plugin::CAPABILITY ) ) { wp_die( esc_html__( 'You are not allowed to do this.', 'aivor-content-import' ) ); } self::render_notice(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : ''; if ( 'edit' === $action || 'new' === $action ) { self::render_form( $action ); return; } self::render_list(); } /** * Klartext-Beschreibung des Ziels eines Jobs. * * @param Job $job Der Job. * @return string */ public static function describe_target( Job $job ): string { $post_type = get_post_type_object( $job->post_type ); $label = $post_type instanceof \WP_Post_Type ? $post_type->labels->singular_name : $job->post_type; if ( '' === $job->taxonomy || 0 === $job->term_id ) { return $label; } $term = get_term( $job->term_id, $job->taxonomy ); if ( ! $term instanceof \WP_Term ) { return $label . ' — ' . __( '(term no longer exists)', 'aivor-content-import' ); } return $label . ' — ' . $term->name; } /** * Klartext-Beschreibung des Autors eines Jobs. * * @param Job $job Der Job. * @return string */ public static function describe_author( Job $job ): string { if ( 0 === $job->author_id ) { return __( 'Default author', 'aivor-content-import' ); } $user = get_userdata( $job->author_id ); return $user instanceof \WP_User ? $user->display_name : __( '(user deleted)', 'aivor-content-import' ); } /** * Klartext, was nach dem Import mit einem Beitrag passiert. * * @param Job $job Der Job. * @return string */ public static function describe_mode( Job $job ): string { if ( Job::MODE_PENDING === $job->mode ) { return __( 'Submitted for review', 'aivor-content-import' ); } return __( 'Saved as draft', 'aivor-content-import' ); } /** * Nimmt das Formular entgegen. * * @return void */ private static function save(): void { check_admin_referer( self::NONCE_ACTION ); $repository = new JobRepository(); // Das Feld heisst aivor_post_type, nicht post_type: admin.php liest // $_REQUEST['post_type'], sucht die Seite dann unter admin.php?post_type=…, // findet sie nicht und bricht mit „Cannot load" ab. $id = isset( $_POST['job_id'] ) ? absint( wp_unslash( $_POST['job_id'] ) ) : 0; $aivor_job_id = isset( $_POST['aivor_job_id'] ) ? strtolower( trim( sanitize_text_field( wp_unslash( $_POST['aivor_job_id'] ) ) ) ) : ''; $post_type = isset( $_POST['aivor_post_type'] ) ? sanitize_key( wp_unslash( $_POST['aivor_post_type'] ) ) : 'post'; $author_id = isset( $_POST['author_id'] ) ? absint( wp_unslash( $_POST['author_id'] ) ) : 0; $mode = isset( $_POST['mode'] ) ? sanitize_key( wp_unslash( $_POST['mode'] ) ) : Job::MODE_DRAFT; $comments = ! empty( $_POST['enable_comments'] ); $term_field = isset( $_POST['term'] ) ? sanitize_text_field( wp_unslash( $_POST['term'] ) ) : ''; if ( ! self::is_uuid( $aivor_job_id ) ) { self::redirect( 'invalid_uuid', $id, true ); } if ( ! in_array( $post_type, self::selectable_post_types(), true ) ) { self::redirect( 'invalid_post_type', $id, true ); } $taxonomy = ''; $term_id = 0; if ( '' !== $term_field ) { $parts = explode( ':', $term_field, 2 ); if ( 2 === count( $parts ) ) { $taxonomy = sanitize_key( $parts[0] ); $term_id = absint( $parts[1] ); } if ( '' === $taxonomy || 0 === $term_id || ! in_array( $taxonomy, get_object_taxonomies( $post_type ), true ) ) { self::redirect( 'invalid_term', $id, true ); } if ( ! get_term( $term_id, $taxonomy ) instanceof \WP_Term ) { self::redirect( 'invalid_term', $id, true ); } } if ( ! in_array( $mode, Job::modes(), true ) ) { $mode = Job::MODE_DRAFT; } $existing = $id > 0 ? $repository->find( $id ) : null; $base = $existing instanceof Job ? $existing : new Job(); // Eine geaenderte Import-Job-ID gehoert womoeglich zu einer anderen // Site; die zwischengespeicherten Angaben des alten Jobs gelten nicht mehr. $same_remote = $base->aivor_job_id === $aivor_job_id; $job = $base->with( array( 'aivor_job_id' => $aivor_job_id, 'post_type' => $post_type, 'taxonomy' => $taxonomy, 'term_id' => $term_id, 'author_id' => $author_id, 'mode' => $mode, 'enable_comments' => $comments, 'site_hash' => $same_remote ? $base->site_hash : '', 'remote_name' => $same_remote ? $base->remote_name : '', 'remote_site_url' => $same_remote ? $base->remote_site_url : '', 'remote_status' => $same_remote ? $base->remote_status : '', 'error' => $same_remote ? $base->error : '', ) ); $job = $repository->save( $job ); // Bei einer Aktivierung ohne angemeldeten Benutzer (WP-CLI, Installer) // fehlt der Standard-Autor noch; spaetestens hier ist jemand da. Settings::adopt_default_author( get_current_user_id() ); if ( '' === Settings::api_key() ) { self::redirect( 'saved' ); } $notices = array( 'ok' => 'saved_verified', 'not_found' => 'saved_not_found', 'forbidden' => 'saved_forbidden', ); self::redirect( $notices[ ConnectionCheck::check_job( $job ) ] ?? 'saved_unverified' ); } /** * Gibt die Liste aus. * * @return void */ private static function render_list(): void { $table = new JobsListTable(); $table->prepare_items(); $new_url = add_query_arg( array( 'page' => self::SLUG, 'action' => 'new', ), admin_url( 'admin.php' ) ); ?>

display(); ?>
find( $id ) : null; if ( 'edit' === $action && ! $job instanceof Job ) { echo '
'; Tabs::render( self::SLUG ); printf( '

%s

', esc_html__( 'This content job no longer exists.', 'aivor-content-import' ) ); return; } if ( ! $job instanceof Job ) { $job = new Job(); } $list_url = add_query_arg( array( 'page' => self::SLUG ), admin_url( 'admin.php' ) ); ?>

id ? __( 'Add content job', 'aivor-content-import' ) : __( 'Edit content job', 'aivor-content-import' ) ); ?>

error ) : ?>

error ); ?>

remote_site_url || PostTypeTarget::same_site( $job->remote_site_url, home_url() ) ) { return ''; } return sprintf( /* translators: 1: website address stored in Aivor, 2: address of this website */ __( 'In Aivor, this job belongs to the website %1$s, but this website is %2$s. Correct the website address in Aivor, otherwise the articles are not linked with each other.', 'aivor-content-import' ), $job->remote_site_url, home_url() ); } /** * Gibt die Begriffe des Ziel-Inhaltstyps aus, nach Taxonomie gruppiert. * * @param Job $job Der bearbeitete Job. * @return void */ private static function render_term_options( Job $job ): void { $taxonomies = get_object_taxonomies( $job->post_type, 'objects' ); foreach ( $taxonomies as $taxonomy ) { if ( ! $taxonomy->show_ui ) { continue; } $terms = get_terms( array( 'taxonomy' => $taxonomy->name, 'hide_empty' => false, 'number' => 200, ) ); if ( is_wp_error( $terms ) || array() === $terms ) { continue; } printf( '', esc_attr( $taxonomy->labels->name ) ); foreach ( $terms as $term ) { $value = $taxonomy->name . ':' . $term->term_id; printf( '', esc_attr( $value ), selected( $job->taxonomy . ':' . $job->term_id, $value, false ), esc_html( $term->name ) ); } echo ''; } } /** * Die als Ziel waehlbaren Inhaltstypen. * * @return string[] */ private static function selectable_post_types(): array { $types = get_post_types( array( 'public' => true, 'show_ui' => true, ) ); unset( $types['attachment'] ); return array_values( $types ); } /** * Prueft das UUID-Format. Der Server lehnt alles andere mit 400 ab. * * @param string $value Zu pruefender Wert. * @return bool */ private static function is_uuid( string $value ): bool { return 1 === preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $value ); } /** * Zeigt die Rueckmeldung nach dem Absenden. * * @return void */ private static function render_notice(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nur Anzeige einer Rueckmeldung. $notice = isset( $_GET['aivor_notice'] ) ? sanitize_key( wp_unslash( $_GET['aivor_notice'] ) ) : ''; $messages = array( 'saved' => array( 'success', __( 'Content job saved.', 'aivor-content-import' ) ), 'saved_verified' => array( 'success', __( 'Content job saved and found in Aivor.', 'aivor-content-import' ) ), 'saved_unverified' => array( 'warning', __( 'Content job saved. Aivor could not be asked about it right now; the next run will show whether the import job ID is correct.', 'aivor-content-import' ) ), 'saved_not_found' => array( 'error', __( 'Content job saved, but Aivor does not know this import job ID. Copy it again from the cluster in Aivor.', 'aivor-content-import' ) ), 'saved_forbidden' => array( 'error', __( 'Content job saved, but the import job belongs to a different Aivor project than the API key. Create the API key in the project of the cluster.', 'aivor-content-import' ) ), 'deleted' => array( 'success', __( 'Content job deleted. Posts that were already imported stay.', 'aivor-content-import' ) ), 'invalid_uuid' => array( 'error', __( 'The import job ID must be a UUID. The content job was not saved.', 'aivor-content-import' ) ), 'invalid_post_type' => array( 'error', __( 'This post type is not available. The content job was not saved.', 'aivor-content-import' ) ), 'invalid_term' => array( 'error', __( 'The selected category does not belong to the post type. The content job was not saved.', 'aivor-content-import' ) ), ); if ( ! isset( $messages[ $notice ] ) ) { return; } printf( '

%s

', esc_attr( $messages[ $notice ][0] ), esc_html( $messages[ $notice ][1] ) ); } /** * Leitet nach der Verarbeitung zurueck. * * @param string $notice Schluessel der Rueckmeldung. * @param int $job_id Job, zu dem zurueckgekehrt wird; 0 fuer die Liste. * @param bool $back_to_form Bei $job_id 0 zurueck ins leere Formular statt in die Liste. * @return void */ private static function redirect( string $notice, int $job_id = 0, bool $back_to_form = false ): void { $args = array( 'page' => self::SLUG, 'aivor_notice' => $notice, ); if ( $job_id > 0 ) { $args['action'] = 'edit'; $args['job'] = $job_id; } elseif ( $back_to_form ) { // Ein neuer Job hat keine ID, zu der man zurueckkehren koennte. Ohne // diesen Zweig landet die Fehlermeldung auf der Liste — weit weg von // dem Formular, das sie erklaert. $args['action'] = 'new'; } wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php' ) ) ); exit; } }