has_cap( self::CAPABILITY ) ) { $role->add_cap( self::CAPABILITY ); } } /** * Cron-Einstiegspunkt, auch fuer den Ausloeser im Backend. * * Der try/catch ist Pflicht: Eine Exception im Cron-Callback bricht den * gesamten WP-Cron-Lauf ab, auch fuer fremde Plugins. * * @param string $trigger Ausloeser des Laufs, nur fuer das Protokoll. * @return bool Ob dieser Prozess den Lauf ausgefuehrt hat. */ public static function run_import( $trigger = 'cron' ): bool { try { return ( new ImportRunner() )->run( is_string( $trigger ) ? $trigger : 'cron' ); } catch ( \Throwable $error ) { EventLog::add( EventLog::TYPE_RUN, sprintf( /* translators: %s: error message */ __( 'Run aborted: %s', 'aivor-content-import' ), $error->getMessage() ), array( 'level' => EventLog::LEVEL_ERROR ) ); return true; } } /** * Registriert den Hauptpunkt „Aivor®", den Unterpunkt und die Reiter-Seiten. * * @return void */ public static function register_menu(): void { // Das zuerst geladene Aivor-Plugin legt den Hauptpunkt an, alle weiteren // haengen sich nur darunter (Konvention siehe PARENT_SLUG). if ( '' === menu_page_url( self::PARENT_SLUG, false ) ) { $parent_hook = add_menu_page( 'Aivor®', 'Aivor®', self::CAPABILITY, self::PARENT_SLUG, '__return_null', // Leer lassen, das Zeichen setzt enqueue_menu_style() per CSS. 'none', 58 ); // Das Menue verlinkt den Hauptpunkt auf seinen ersten Unterpunkt; // wer admin.php?page=aivor direkt aufruft, landet ebenfalls dort. add_action( 'load-' . $parent_hook, array( self::class, 'redirect_to_first_product' ) ); } $settings_hook = add_submenu_page( self::PARENT_SLUG, __( 'Aivor Content Import', 'aivor-content-import' ), __( 'Content Import', 'aivor-content-import' ), self::CAPABILITY, self::MENU_SLUG, array( SettingsPage::class, 'render' ) ); // Beim ersten Unterpunkt setzt WordPress einen Link auf den Hauptpunkt // selbst davor. Der fuehrte auf keine eigene Seite; ohne ihn verlinkt // der Hauptpunkt auf den ersten echten Unterpunkt. remove_submenu_page( self::PARENT_SLUG, self::PARENT_SLUG ); // Content-Jobs und Protokoll sind Reiter; ihr Menueeintrag verschwindet // in hide_tab_pages(). $jobs_hook = add_submenu_page( self::PARENT_SLUG, __( 'Content jobs', 'aivor-content-import' ), __( 'Content jobs', 'aivor-content-import' ), self::CAPABILITY, JobsPage::SLUG, array( JobsPage::class, 'render' ) ); $log_hook = add_submenu_page( self::PARENT_SLUG, __( 'Import log', 'aivor-content-import' ), __( 'Import log', 'aivor-content-import' ), self::CAPABILITY, LogPage::SLUG, array( LogPage::class, 'render' ) ); // Formulare werden in load-* verarbeitet, damit vor jeder Ausgabe // umgeleitet werden kann. if ( $settings_hook ) { add_action( 'load-' . $settings_hook, array( SettingsPage::class, 'handle_post' ) ); } if ( $jobs_hook ) { add_action( 'load-' . $jobs_hook, array( JobsPage::class, 'handle_post' ) ); } if ( $log_hook ) { add_action( 'load-' . $log_hook, array( LogPage::class, 'handle_post' ) ); } } /** * Leitet admin.php?page=aivor auf den ersten Unterpunkt um. * * @return void */ public static function redirect_to_first_product(): void { global $submenu; $first = self::MENU_SLUG; foreach ( (array) ( $submenu[ self::PARENT_SLUG ] ?? array() ) as $item ) { // Nie auf den Hauptpunkt selbst, das waere eine Umleitungsschleife. if ( isset( $item[2] ) && self::PARENT_SLUG !== $item[2] ) { $first = (string) $item[2]; break; } } wp_safe_redirect( add_query_arg( array( 'page' => $first ), admin_url( 'admin.php' ) ) ); exit; } /** * Blendet Content-Jobs und Protokoll im Menue aus. * * Beide bleiben registrierte Seiten unter „Aivor®": Adresse, Rechtepruefung * und Seitentitel funktionieren wie gehabt, erreichbar sind sie ueber die * Reiter. Erst in admin_head, weil WordPress den Seitentitel vorher aus dem * Untermenue bestimmt und das Menue erst danach ausgibt. * * @return void */ public static function hide_tab_pages(): void { remove_submenu_page( self::PARENT_SLUG, JobsPage::SLUG ); remove_submenu_page( self::PARENT_SLUG, LogPage::SLUG ); } /** * Haelt auf den Reiter-Seiten den Hauptpunkt „Aivor®" geoeffnet. * * Ohne den Filter ermittelt WordPress den Hauptpunkt ueber das * Untermenue, aus dem hide_tab_pages() diese Seiten schon entfernt hat. * Dass es trotzdem klappt, hinge daran, dass get_admin_page_title() ihn * vorher zwischenspeichert, und das tut es nicht, wenn $title schon * gesetzt ist. * * @param string|null $parent_file Aktiver Hauptpunkt. * @return string|null */ public static function highlight_parent( $parent_file ) { return self::is_tab_page() ? self::PARENT_SLUG : $parent_file; } /** * Markiert auf den Reiter-Seiten den Unterpunkt „Content Import". * * @param string|null $submenu_file Aktiver Untermenue-Eintrag. * @return string|null */ public static function highlight_menu( $submenu_file ) { if ( self::is_tab_page() ) { return self::MENU_SLUG; } return $submenu_file; } /** * Ob gerade Content-Jobs oder das Protokoll angezeigt wird. * * @return bool */ private static function is_tab_page(): bool { global $plugin_page; return in_array( $plugin_page, array( JobsPage::SLUG, LogPage::SLUG ), true ); } /** * Link „Settings" in der Plugin-Liste. * * @param array $links Vorhandene Links. * @return array */ public static function action_links( $links ) { if ( ! current_user_can( self::CAPABILITY ) ) { return $links; } $settings = sprintf( '%s', esc_url( add_query_arg( array( 'page' => self::MENU_SLUG ), admin_url( 'admin.php' ) ) ), esc_html__( 'Settings', 'aivor-content-import' ) ); return array_merge( array( 'settings' => $settings ), (array) $links ); } /** * Textvorschlag fuer die Datenschutzerklaerung. * * @return void */ public static function add_privacy_policy_content(): void { if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) { return; } $paragraphs = array( __( 'This website uses Aivor Content Import to import articles that are researched and written with the help of AI by Aivor (aivor.ai), a service operated in Germany. Editors review them before they are published.', 'aivor-content-import' ), __( 'The plugin does not collect, store or send any data about visitors of this website. It sets no cookies and loads nothing from Aivor in the browsers of visitors.', 'aivor-content-import' ), __( 'In the background, the website connects to the Aivor API once an hour to fetch new articles and their featured images. It sends the API key, the import job IDs and, for articles from Aivor, their public address, post ID and text, so that Aivor can link related articles with each other.', 'aivor-content-import' ), __( 'Articles from Aivor are marked as AI-generated in their structured data. Depending on the settings, a short notice below the article says so as well.', 'aivor-content-import' ), ); $content = ''; foreach ( $paragraphs as $paragraph ) { $content .= '

' . esc_html( $paragraph ) . '

'; } $content .= '

' . sprintf( /* translators: %s: URL of the Aivor privacy policy */ esc_html__( 'Privacy policy of Aivor: %s', 'aivor-content-import' ), 'https://aivor.ai/datenschutzerklaerung/' ) . '

'; wp_add_privacy_policy_content( __( 'Aivor Content Import', 'aivor-content-import' ), wp_kses_post( $content ) ); } /** * Aktivierung: Capability vergeben, Cron planen, Einstellungen anlegen. * * Wer aktiviert, wird Standard-Autor der importierten Beitraege. * * @return void */ public static function activate(): void { $role = get_role( 'administrator' ); if ( $role instanceof \WP_Role ) { $role->add_cap( self::CAPABILITY ); } Settings::mark_activated(); Settings::adopt_default_author( get_current_user_id() ); if ( ! wp_next_scheduled( AIVOR_CONTENT_CRON_HOOK ) ) { wp_schedule_event( time() + MINUTE_IN_SECONDS, 'hourly', AIVOR_CONTENT_CRON_HOOK ); } } /** * Deaktivierung: alle Cron-Events abraeumen, haengenden Lock loesen. * * Capability und Daten bleiben — sie gehen erst beim Deinstallieren. * * @return void */ public static function deactivate(): void { // wp_unschedule_hook() statt wp_clear_scheduled_hook(): Letzteres // traefe nur Events ohne Argumente, die nachgeschobenen Laeufe blieben. wp_unschedule_hook( AIVOR_CONTENT_CRON_HOOK ); delete_option( Cron\RunLock::OPTION ); } /** * Das Menue-Symbol: das Aivor-Zeichen in Farbe. * * Als Bild-URL zeigte WordPress das Zeichen als img in Originalgroesse und * mit 60 % Deckkraft. Als Hintergrund auf 20 px bleibt es farbig und auch * auf hochaufloesenden Bildschirmen scharf. Ein einfarbiges SVG scheidet * aus: Das Design-System verbietet, das Zeichen nachzuzeichnen oder * umzufaerben. * * @return void */ public static function enqueue_menu_style(): void { if ( ! current_user_can( self::CAPABILITY ) ) { return; } // Familienweit derselbe Handle: Laden mehrere Aivor-Plugins das Symbol, // bindet WordPress es nur einmal ein. wp_enqueue_style( 'aivor-admin-menu', plugins_url( 'assets/admin-menu.css', AIVOR_CONTENT_FILE ), array(), AIVOR_CONTENT_VERSION ); } }