' . '' . '' . '' . '' . '' . '' . ''; } /** Erlaubtes Markup für Seitentitel samt Bildmarke (für wp_kses). */ public static function title_kses() { return array( 'h1' => array(), 'span' => array( 'class' => true, ), 'b' => array(), 'svg' => array( 'class' => true, 'width' => true, 'height' => true, 'viewbox' => true, 'aria-hidden' => true, 'focusable' => true, ), 'rect' => array( 'width' => true, 'height' => true, 'rx' => true, 'fill' => true, ), 'g' => array( 'fill' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, ), 'path' => array( 'd' => true, 'stroke' => true, 'stroke-width' => true, 'fill' => true, ), ); } /** Seitentitel ausgeben (escaped). */ public static function the_page_title( $title = '' ) { echo wp_kses( self::page_title( $title ), self::title_kses() ); self::terms_box(); } /** * Zustimmung zu Nutzungsbedingungen und AV-Vertrag (Art. 28 DSGVO). Erscheint auf * allen Sectivio-Seiten, bis die aktuelle Fassung bestätigt ist. Ohne Zustimmung * läuft der Berichtsversand über unseren Mailserver nicht (siehe * ADSEC_Plugin::terms_accepted()). */ private static function terms_box() { if ( ADSEC_Plugin::terms_accepted() || !current_user_can( 'manage_options' ) ) { return; } $de = 0 === strpos( determine_locale(), 'de' ); $terms = ( $de ? 'https://sectivio.com/terms.html' : 'https://sectivio.com/terms.html#en' ); $avv = ( $de ? 'https://sectivio.com/avv.html' : 'https://sectivio.com/avv.html#en' ); $intro = __( 'Sectivio processes data on behalf of your website for sending the monthly report. This requires a data processing agreement under Art. 28 GDPR. Until you confirm, the report is sent from your own server. Everything else keeps working.', 'sectivio' ); ?>

ADSEC_Plugin::TERMS_VERSION, 'time' => time(), 'user_id' => (int) $user->ID, 'login' => (string) $user->user_login, ); update_option( 'adsec_terms', $rec, false ); $base = ADSEC_AI::service_url(); if ( '' !== $base ) { wp_remote_post( trailingslashit( $base ) . 'v1/terms/accept', array( 'timeout' => 5, 'blocking' => false, 'headers' => array( 'content-type' => 'application/json', ), 'body' => wp_json_encode( array( 'site' => ADSEC_AI::site_id(), 'host' => preg_replace( '#^www\\.#', '', strtolower( (string) wp_parse_url( home_url(), PHP_URL_HOST ) ) ), 'version' => ADSEC_Plugin::TERMS_VERSION, 'accepted' => gmdate( 'c', $rec['time'] ), 'plugin' => ADSEC_VERSION, ) ), ) ); } wp_safe_redirect( $back ); exit; } /** Seitenkopf der Sectivio-Seiten: Bildmarke + Titel (Dashboard: Wortmarke „Sectivio"). */ public static function page_title( $title = '' ) { $text = ( '' === $title ? 'Sectivio' : '' . esc_html( $title ) . '' ); return '

' . self::brand_icon() . $text . '

'; } /** * „Daten zurücksetzen": löscht alle erfassten Mess-/Lerndaten, sodass das * Dashboard bei 0 startet (z. B. nach einer Test-Simulation). Inhalts-Inventar * und Einstellungen bleiben erhalten. */ public function handle_reset_stats() { if ( !current_user_can( 'manage_options' ) || !check_admin_referer( 'adsec_reset_stats' ) ) { wp_die( esc_html__( 'Not allowed.', 'sectivio' ) ); } ADSEC_Database::reset_stats(); wp_safe_redirect( add_query_arg( array( 'page' => 'adsec-settings', 'adsec_reset' => 1, ), admin_url( 'admin.php' ) ) ); exit; } /** Inhaltserkennung: Stand der KI-Verbindung von WordPress. */ private static function render_ai_section( $s ) { ?>

admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'adsec_settings_ajax' ), 'i18n' => array( 'sending' => __( 'Sending report …', 'sectivio' ), 'sendFail' => __( 'The report could not be sent. Please try again later.', 'sectivio' ), 'checking' => __( 'Checking …', 'sectivio' ), 'checkNow' => __( 'Check now', 'sectivio' ), 'scanRun' => $scan_run, 'scanWait' => __( 'Scanning pages … this can take a minute.', 'sectivio' ), 'aiRun' => $ai_run, 'aiWait' => __( 'Detecting sections … this can take a minute.', 'sectivio' ), 'keepOpen' => __( 'Please keep this page open.', 'sectivio' ), 'failed' => __( 'That did not work. Please check your connection and try again.', 'sectivio' ), ), ) ); } /** * Status-Widget auf der WordPress-Startseite des Backends (v0.10.0). * Zeigt den aktuellen Stand auf einen Blick, ohne ins Plugin wechseln zu müssen. */ public function register_dashboard_widget() { if ( !current_user_can( 'manage_options' ) ) { return; } wp_add_dashboard_widget( 'adsec_status_widget', esc_html__( 'Sectivio — Status', 'sectivio' ), array($this, 'render_status_widget') ); } /** * Inhalt des Status-Widgets: in der Sammelphase der Fortschritt (% der * benötigten Sitzungen), sobald optimiert wird die Verbesserungs-Raten. * Eigenständig gestylt (inline), da das Plugin-CSS hier nicht geladen wird. */ public function render_status_widget() { $sum = ADSEC_Dashboard::quick_summary(); $dashboard_url = admin_url( 'admin.php?page=adaptive-sections' ); echo '

' . esc_html( ( $sum['enabled'] ? __( 'Sectivio is measuring', 'sectivio' ) : __( 'Measurement is turned off', 'sectivio' ) ) ) . '

'; echo '

' . esc_html__( 'View results →', 'sectivio' ) . '

'; } public function register_menu() { $dashboard = new ADSEC_Dashboard(); add_menu_page( 'Sectivio', 'Sectivio', 'manage_options', 'adaptive-sections', array($dashboard, 'render'), 'dashicons-randomize', 58 ); add_submenu_page( 'adaptive-sections', __( 'Dashboard', 'sectivio' ), __( 'Dashboard', 'sectivio' ), 'manage_options', 'adaptive-sections', array($dashboard, 'render') ); add_submenu_page( 'adaptive-sections', __( 'Content overview', 'sectivio' ), __( 'Content overview', 'sectivio' ), 'manage_options', 'adsec-inventory', array($this, 'render_inventory_page') ); add_submenu_page( 'adaptive-sections', __( 'Settings', 'sectivio' ), __( 'Settings', 'sectivio' ), 'manage_options', 'adsec-settings', array($this, 'render_settings_page') ); $upgrade = new ADSEC_Upgrade(); add_submenu_page( 'adaptive-sections', __( 'Plan', 'sectivio' ), ( 'free' === ADSEC_Upgrade::current_plan() ? __( 'Upgrade', 'sectivio' ) : __( 'Plan', 'sectivio' ) ), 'manage_options', ADSEC_Upgrade::SLUG, array($upgrade, 'render') ); } /* ---------- Einstellungen ---------- */ public function register_settings() { register_setting( 'adsec', 'adsec_settings', array( 'type' => 'array', 'sanitize_callback' => array($this, 'sanitize_settings'), ) ); } public function sanitize_settings( $input ) { $current = get_option( 'adsec_settings', array() ); $current['enabled'] = !empty( $input['enabled'] ); // Was zählt als Erfolg? Mehrfachauswahl aus den erkannten Conversion-Typen // (Formular/Telefon/E-Mail) plus optionalem eigenem CSS-Selektor. Nur // angehakte Typen werden im Frontend als „goal" getrackt und ausgewertet. $current['goal_types'] = array_values( array_intersect( array( 'form', 'tel', 'mailto', 'selector' ), array_map( 'sanitize_key', (array) ($input['goal_types'] ?? array()) ) ) ); // Eigener Conversion-Selektor: Länge deckeln + in CSS-Selektoren nie gültige // Zeichen (< und Backtick) entfernen. Defense-in-depth — die Ausgabe läuft // ohnehin JSON-kodiert über wp_localize_script (kein direkter HTML-Kontext). $gs_raw = sanitize_text_field( $input['goal_selector'] ?? '' ); $current['goal_selector'] = substr( str_replace( array('<', '`'), '', $gs_raw ), 0, 200 ); // Branche (für die zentrale, branchenweite Auswertung). Auswahl aus der // Taxonomie ODER eigener Freitext, falls keine passt. Die Auswahl hat // Vorrang; nur wenn sie auf „bitte wählen" steht, greift der Freitext // (zu a-z0-9- normalisiert, damit gleiche Eigen-Branchen denselben Pool teilen). $sel = sanitize_key( $input['branche'] ?? '' ); $custom = sanitize_text_field( $input['branche_custom'] ?? '' ); if ( array_key_exists( $sel, ADSEC_Sync::BRANCHEN ) ) { $current['branche'] = $sel; $current['branche_label'] = ''; } elseif ( '' !== $custom && '' !== sanitize_title( $custom ) ) { $current['branche'] = sanitize_title( $custom ); $current['branche_label'] = $custom; } else { $current['branche'] = ''; $current['branche_label'] = ''; } // Anonyme Branchen-Statistik beitragen (Opt-in, Standard aus). $current['share_data'] = !empty( $input['share_data'] ); // 0.52.0: the owner confirms that the privacy policy mentions Sectivio. $current['privacy_done'] = !empty( $input['privacy_done'] ); // Monatlicher E-Mail-Bericht (Opt-in, Standard aus). $current['report_enabled'] = !empty( $input['report_enabled'] ); $report_email = sanitize_email( $input['report_email'] ?? '' ); $current['report_email'] = ( $report_email ? $report_email : get_option( 'admin_email' ) ); return $current; } public function render_settings_page() { $s = get_option( 'adsec_settings', array() ); $inv_link = '' . esc_html__( 'Content overview', 'sectivio' ) . ''; $txt = array( 'main_title' => __( 'Measurement', 'sectivio' ), 'main_label' => __( 'Turn on measurement', 'sectivio' ), 'main_help' => esc_html__( 'Sectivio measures which sections your visitors read and which lead to an enquiry. Your pages stay unchanged.', 'sectivio' ), 'goal_intro' => __( 'Choose which action you would like your visitors to take. Sectivio counts it as an enquiry.', 'sectivio' ), ); ?>

__( 'A visitor submits a form', 'sectivio' ), 'tel' => __( 'A visitor clicks a phone number', 'sectivio' ), 'mailto' => __( 'A visitor clicks an email address', 'sectivio' ), ); foreach ( $goal_labels as $gt => $label ) { $found = in_array( $gt, $detected, true ); $checked = in_array( $gt, $goal_types, true ); if ( !$found && !$checked ) { continue; } ?>
>

✓

render_panel(); } // Dev-Tool, im Release-Build nicht vorhanden. ?>

Bezeichnung, Zustand (ok|err|off), Satz */ private static function connection_check() { $base = ADSEC_AI::service_url(); $rows = array(); $h = ( '' !== $base ? wp_remote_get( trailingslashit( $base ) . 'v1/health', array( 'timeout' => 10, ) ) : null ); $up = $h && !is_wp_error( $h ) && 200 === wp_remote_retrieve_response_code( $h ); $rows[] = array(__( 'Sectivio service', 'sectivio' ), ( $up ? 'ok' : 'err' ), ( $up ? __( 'Your website can reach the Sectivio service.', 'sectivio' ) : __( 'Your website cannot reach the Sectivio service. Please ask your hosting provider whether outgoing connections are allowed.', 'sectivio' ) )); $label = __( 'Detect content automatically', 'sectivio' ); $rows[] = ( ADSEC_AI::wp_ai_available() ? array($label, 'ok', __( 'Ready. Sectivio uses the AI connection set up in WordPress.', 'sectivio' )) : array($label, 'off', __( 'No AI connection is set up in WordPress yet. Sectivio still recognizes most sections on its own.', 'sectivio' )) ); return $rows; } /** Ergebnis der Verbindungsprüfung als Liste. */ private static function connection_rows_html( $check ) { if ( empty( $check['rows'] ) ) { return '

' . esc_html__( 'Not checked yet.', 'sectivio' ) . '

'; } $html = ''; if ( !empty( $check['time'] ) ) { /* translators: %s: human-readable time span, e.g. "5 mins". */ $html .= '

' . esc_html( sprintf( __( 'Last checked %s ago.', 'sectivio' ), human_time_diff( (int) $check['time'] ) ) ) . '

'; } return $html; } /** AJAX: Verbindung prüfen, ohne die Seite neu zu laden. */ public function ajax_sync_test() { if ( !current_user_can( 'manage_options' ) || !check_ajax_referer( 'adsec_settings_ajax', false, false ) ) { wp_send_json_error( 'forbidden', 403 ); } $check = array( 'rows' => self::connection_check(), 'time' => time(), ); set_transient( 'adsec_conn_check', $check, DAY_IN_SECONDS ); wp_send_json_success( array( 'html' => self::connection_rows_html( $check ), ) ); } /** * After a full scan (0.51.0): which page builder the pages use, and a clear * note when Sectivio found no sections or the builder is not supported yet. */ private static function render_scan_summary() { $sum = ADSEC_Inventory::scan_summary(); if ( $sum['builders'] ) { $parts = array(); foreach ( $sum['builders'] as $name => $n ) { /* translators: 1: page builder name, 2: number of pages. */ $parts[] = sprintf( _n( '%1$s (%2$d page)', '%1$s (%2$d pages)', $n, 'sectivio' ), $name, $n ); } echo '

' . esc_html__( 'Recognized page builder:', 'sectivio' ) . ' ' . esc_html( implode( ', ', $parts ) ) . '

'; } if ( $sum['unsupported'] ) { echo '

'; /* translators: %s: page builder names. */ $msg = __( 'Some pages are built with %s. Sectivio does not support this page builder yet and cannot recognize the sections on these pages.', 'sectivio' ); printf( esc_html( $msg ), esc_html( implode( ', ', array_keys( $sum['unsupported'] ) ) ) ); echo '

'; } elseif ( 0 === $sum['sections'] ) { echo '

' . esc_html__( 'Sectivio did not find any sections on your website. Sections are the large blocks of a page, such as a header area, services or references. Pages that consist of one block of text cannot be measured section by section.', 'sectivio' ) . '

'; } } /** AJAX: scan the next few pages (content overview shows the progress). */ public function ajax_scan_step() { if ( !current_user_can( 'manage_options' ) || !check_ajax_referer( 'adsec_settings_ajax', false, false ) ) { wp_send_json_error( 'forbidden', 403 ); } if ( !empty( $_POST['start'] ) ) { update_option( 'adsec_scan_offset', 0, false ); // a click always scans the whole website delete_option( ADSEC_Inventory::REPORT_OPTION ); } wp_send_json_success( ADSEC_Inventory::scan_site( 5 ) ); } /** AJAX: detect the next batch of unclear sections. */ public function ajax_ai_step() { if ( !current_user_can( 'manage_options' ) || !check_ajax_referer( 'adsec_settings_ajax', false, false ) ) { wp_send_json_error( 'forbidden', 403 ); } wp_send_json_success( ADSEC_AI::detect_batch() ); } /** AJAX: Monatsbericht sofort an die angegebene Adresse senden. */ public function ajax_send_report() { if ( !current_user_can( 'manage_options' ) || !check_ajax_referer( 'adsec_settings_ajax', false, false ) ) { wp_send_json_error( 'forbidden', 403 ); } $to = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) ); if ( !$to ) { wp_send_json_success( array( 'ok' => false, 'message' => __( 'Please enter a valid email address.', 'sectivio' ), ) ); } $status = ADSEC_Report::send_now( $to ); // Translator comments sit on their own statements: Freemius' code formatter drops comments inside arrays. /* translators: %s: email address */ $msg_sent = __( 'The report is on its way to %s.', 'sectivio' ); /* translators: %s: email address */ $msg_confirm = __( 'Almost done: we have sent a confirmation to %s. As soon as you click the link in that email, the report will be on its way.', 'sectivio' ); /* translators: %s: email address */ $msg_pending = __( 'The address %s has not been confirmed yet. Please click the link in the confirmation email we sent you, and check your spam folder too.', 'sectivio' ); /* translators: %s: email address */ $msg_stopped = __( 'The monthly report has been unsubscribed for %s. Please use a different address or write to us at office@sectivio.com.', 'sectivio' ); $messages = array( 'sent' => sprintf( $msg_sent, $to ), 'confirm_sent' => sprintf( $msg_confirm, $to ), 'confirm_pending' => sprintf( $msg_pending, $to ), 'stopped' => sprintf( $msg_stopped, $to ), 'error' => __( 'The report could not be sent just now. Please try again in a few minutes.', 'sectivio' ), ); wp_send_json_success( array( 'ok' => in_array( $status, array('sent', 'confirm_sent', 'confirm_pending'), true ), 'message' => $messages[$status] ?? $messages['error'], ) ); } /* ---------- Content-Inventar ---------- */ public function handle_scan() { if ( !current_user_can( 'manage_options' ) || !check_admin_referer( 'adsec_scan' ) ) { wp_die( esc_html__( 'Not allowed.', 'sectivio' ) ); } $result = ADSEC_Inventory::scan_site(); $args = array( 'page' => 'adsec-inventory', 'adsec_scanned' => $result['scanned'], 'adsec_found' => $result['sections'], 'adsec_remaining' => $result['remaining'], ); wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php' ) ) ); exit; } public function handle_save_inventory() { if ( !current_user_can( 'manage_options' ) || !check_admin_referer( 'adsec_save_inventory' ) ) { wp_die( esc_html__( 'Not allowed.', 'sectivio' ) ); } // Seiten-Sperren ZUERST speichern, damit das reusable-Gate in save_inventory // eine frisch gesperrte Seite sofort berücksichtigt. // Große Websites haben Tausende Felder; PHP verwirft alles über max_input_vars // (oft 1.000) stillschweigend. Das Formular schickt die Angaben deshalb als ein // JSON-Feld; ohne JavaScript gilt weiterhin das normale Formular. $payload = ( isset( $_POST['adsec_payload'] ) ? json_decode( sanitize_textarea_field( wp_unslash( $_POST['adsec_payload'] ) ), true ) : null ); if ( is_array( $payload ) ) { $pages = map_deep( (array) ($payload['pages'] ?? array()), 'sanitize_text_field' ); $sections = map_deep( (array) ($payload['sections'] ?? array()), 'sanitize_text_field' ); } else { $pages = ( isset( $_POST['pages'] ) ? map_deep( wp_unslash( (array) $_POST['pages'] ), 'sanitize_text_field' ) : array() ); $sections = ( isset( $_POST['sections'] ) ? map_deep( wp_unslash( (array) $_POST['sections'] ), 'sanitize_text_field' ) : array() ); } ADSEC_Inventory::save_page_overrides( $pages ); ADSEC_Inventory::save_inventory( $sections ); wp_safe_redirect( add_query_arg( array( 'page' => 'adsec-inventory', 'adsec_saved' => 1, ), admin_url( 'admin.php' ) ) ); exit; } public function handle_ai_classify() { if ( !current_user_can( 'manage_options' ) || !check_admin_referer( 'adsec_ai_classify' ) ) { wp_die( esc_html__( 'Not allowed.', 'sectivio' ) ); } $result = ADSEC_AI::detect_batch(); $args = array( 'page' => 'adsec-inventory', 'adsec_ai_done' => (int) $result['done'], 'adsec_ai_remaining' => (int) $result['remaining'], ); if ( null !== $result['error'] ) { $args['adsec_ai_error'] = rawurlencode( mb_substr( $result['error'], 0, 160 ) ); } wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php' ) ) ); exit; } /** * Sitemap über der Inhalts-Übersicht: die Navigation der Website (Hauptmenü mit * Untermenüs) als Sprungliste. Seiten ohne Menüeintrag stehen unter „Weitere Seiten“. */ private static function inventory_sitemap( array $inventory ) { $menu_id = self::main_menu_id(); $items = ( $menu_id ? (array) wp_get_nav_menu_items( $menu_id ) : array() ); $in_menu = array(); $tree = array(); foreach ( $items as $it ) { $tree[(int) $it->menu_item_parent][] = $it; if ( 'post_type' === $it->type ) { $in_menu[(int) $it->object_id] = true; } } $link = function ( $title, $page_id ) use($inventory) { if ( $page_id && isset( $inventory[$page_id] ) ) { return '' . esc_html( $title ) . ''; } return '' . esc_html( $title ) . ''; }; $branch = function ( $parent ) use(&$branch, $tree, $link) { if ( empty( $tree[$parent] ) ) { return ''; } $out = ''; }; $html = ''; } /** Hauptmenü der Website: bevorzugt der Menüort „primary/main/header“, sonst das größte Menü. */ private static function main_menu_id() { $locations = (array) get_nav_menu_locations(); foreach ( array( 'primary', 'main', 'main-menu', 'header', 'header-menu', 'menu-1', 'top' ) as $loc ) { if ( !empty( $locations[$loc] ) ) { return (int) $locations[$loc]; } } $best = 0; $max = 0; foreach ( wp_get_nav_menus() as $m ) { if ( (int) $m->count > $max ) { $max = (int) $m->count; $best = (int) $m->term_id; } } return ( $best ?: (( $locations ? (int) reset( $locations ) : 0 )) ); } /** Knopf „Nach oben“ rechts unten, erscheint erst beim Herunterscrollen. */ private static function back_to_top() { ?>

0 ) { ?>

0 ) { ?>

'adsec-scan-btn', ) ); ?>
'adsec-ai-btn', ) ); ?>
' . esc_html__( 'Settings', 'sectivio' ) . '' ); ?>

$sections ) { $page_excluded = ADSEC_Inventory::is_page_excluded( $page_id ); $page_auto = ADSEC_Inventory::is_legal_page( $page_id ); $exclude_label = __( 'Do not measure this page', 'sectivio' ); ?>

features, true ) ?: array() ); $score = (int) $row->content_score; /* translators: %d: content score. */ $badges = array(sprintf( __( 'Content score %d', 'sectivio' ), $score )); if ( !empty( $features['form'] ) ) { $badges[] = '' . __( 'Form', 'sectivio' ); } if ( !empty( $features['imgs'] ) ) { /* translators: %d: number of images. */ $badges[] = '' . sprintf( _n( '%d image', '%d images', (int) $features['imgs'], 'sectivio' ), (int) $features['imgs'] ); } if ( !empty( $features['buttons'] ) ) { $badges[] = '„' . implode( '“ · „', array_slice( $features['buttons'], 0, 2 ) ) . '“'; } ?>
heading ?: (( $row->ai_label ?: __( '(no heading)', 'sectivio' ) )) ) ); ?> topic_source ) { ?> topic_source ) { ?> excerpt ) { ?>
excerpt, 0, 120 ) ); ?>…