Plugin Directory

Changeset 3789030


Ignore:
Timestamp:
11/07/2026 06:11:14 AM (9 hours ago)
Author:
klusio
Message:

Release Klusio 1.14.0: read draft text and proposal details inside WordPress; inspect editable pages safely

Location:
klusio-builder-bridge
Files:
4 added
2 edited

Legend:

Unmodified
Added
Removed
  • klusio-builder-bridge/trunk/klusio-builder-bridge.php

    r3789018 r3789030  
    33 * Plugin Name: Klusio
    44 * Description: Lets Klusio update texts on your Elementor and Bricks pages after your approval, with a backup of every change and a restore option. Works as a regular plugin and as a must-use plugin.
    5  * Version: 1.13.0
     5 * Version: 1.14.0
    66 * Requires at least: 5.6
    77 * Requires PHP: 7.2
    … …  
    7272    return;
    7373}
    74 define('KLUSIO_BRIDGE_VERSION', '1.13.0');
     74define('KLUSIO_BRIDGE_VERSION', '1.14.0');
    7575define('KLUSIO_BRIDGE_NAAM', 'klusio-builder-bridge');
    7676define('KLUSIO_BRIDGE_BACKUP_PREFIX', '_klusio_el_backup_');
    … …  
    37773777    if ($request->get_method() === 'GET') {
    37783778        $settings = klusio_bridge_workspace_settings();
    3779         return new WP_REST_Response(array('enabled' => !empty($settings['enabled']), 'schema' => 1, 'account' => (int)(isset($settings['account']) ? $settings['account'] : 0)), 200);
     3779        return new WP_REST_Response(array('enabled' => !empty($settings['enabled']), 'schema' => 1, 'reading' => true, 'account' => (int)(isset($settings['account']) ? $settings['account'] : 0)), 200);
    37803780    }
    37813781    if (!klusio_bridge_workspace_lock()) {
    … …  
    38133813        return new WP_Error('klusio_workspace_disabled', __('Enable the account overview in WordPress first.', 'klusio-builder-bridge'), array('status' => 409));
    38143814    }
    3815     if (strlen($request->get_body()) > 65536) {
     3815    if (strlen($request->get_body()) > 262144) {
    38163816        return new WP_Error('klusio_workspace_size', __('The overview is too large.', 'klusio-builder-bridge'), array('status' => 413));
    38173817    }
    … …  
    38493849            }
    38503850            $v = array('id' => $item['id'],'kind' => $item['kind']);
    3851             foreach (array('title','detail','status','date','page_url','measurement') as $key) {
     3851            foreach (array('title','detail','status','date','page_url','measurement','plan','your_step','verification') as $key) {
    38523852                if (isset($item[$key]) && (!is_string($item[$key]) || strlen($item[$key]) > 2000)) {
    38533853                    return new WP_Error('klusio_workspace_text', __('Invalid overview text.', 'klusio-builder-bridge'), array('status' => 400));
    … …  
    38583858                return new WP_Error('klusio_workspace_status', __('Invalid overview status.', 'klusio-builder-bridge'), array('status' => 400));
    38593859            }
     3860            if (isset($item['reading'])) {
     3861                if ($group !== 'drafts' || $item['kind'] !== 'draft' || !is_string($item['reading']) || strlen($item['reading']) > 18000 || !isset($item['reading_complete']) || !is_bool($item['reading_complete'])) {
     3862                    return new WP_Error('klusio_workspace_reading', __('Invalid draft preview.', 'klusio-builder-bridge'), array('status' => 400));
     3863                }
     3864                $v['reading'] = sanitize_textarea_field($item['reading']);
     3865                $v['reading_complete'] = $item['reading_complete'];
     3866            }
     3867            if (isset($item['local_post_id'])) {
     3868                if ($group !== 'page_drafts' || !is_int($item['local_post_id']) || $item['local_post_id'] < 0) {
     3869                    return new WP_Error('klusio_workspace_local_post', __('Invalid local page reference.', 'klusio-builder-bridge'), array('status' => 400));
     3870                }
     3871                $v['local_post_id'] = $item['local_post_id'];
     3872            }
    38603873            $v['verified_at'] = isset($item['verified_at']) && is_int($item['verified_at']) && $item['verified_at'] > 0 && $item['verified_at'] <= time() + 120 ? $item['verified_at'] : 0;
    38613874            // Page URLs are contextual text only; all clickable account URLs are built locally.
    … …  
    38643877    }
    38653878    $clean['unavailable'] = array();
    3866     foreach (array('growth','drafts','page_drafts','preferences','measurements','page_actions') as $part) {
     3879    foreach (array('growth','drafts','page_drafts','preferences','measurements','page_actions','details') as $part) {
    38673880        if (!empty($d['unavailable']) && is_array($d['unavailable']) && in_array($part, $d['unavailable'], true)) {
    38683881            $clean['unavailable'][] = $part;
    … …  
    39884001    if ($hook !== 'toplevel_page_klusio') { return; }
    39894002    wp_add_inline_style('klusio-workspace', '
     4003.kbw-prose{white-space:pre-wrap;overflow-wrap:anywhere;max-width:72ch;margin:28px auto;font-size:16px;line-height:1.85;color:#253b53}.kbw-back{display:inline-block;margin:0 0 22px;font-weight:600}.kbw-search{margin:0 0 24px}.kbw-search label{display:block;margin-bottom:8px;font-weight:600}.kbw-search>div{display:flex;gap:10px}.kbw-search input{width:100%;max-width:540px;min-width:0;border:1px solid #d9e3ed;border-radius:8px;min-height:44px}.kbw-page-list>li{display:flex;justify-content:space-between;gap:22px;align-items:center}.kbw-page-list h2{font-size:18px;margin:10px 0 4px}.kbw-page-list .kbw-btn{flex-shrink:0}.kbw-outline{padding-left:0;list-style:none}.kbw-outline span{font-size:12px;font-weight:600;color:#31668f;margin-right:8px}.kbw-reading{border-top:4px solid #2175b8}.kbw :focus-visible{outline:2px solid #2175b8;outline-offset:3px}@media(max-width:782px){.kbw-page-list>li{display:block}.kbw-page-list .kbw-btn{margin-top:10px}.kbw-prose{font-size:15px}.kbw-search>div{flex-wrap:wrap}}
    39904004.kbw{max-width:1600px}.kbw-shell{display:grid;grid-template-columns:206px minmax(0,1fr);gap:28px}.kbw-main{min-width:0}.kbw-shell>.kbw-nav{display:flex;flex-direction:column;align-items:stretch;align-self:start;position:sticky;top:52px;border:0;border-radius:12px 12px 12px 0;background:#fff;padding:10px;margin:0;gap:5px;width:auto}.kbw-shell>.kbw-nav a{display:flex;align-items:center;gap:10px;padding:12px;font-size:13px;white-space:normal;border:0}.kbw-shell>.kbw-nav a[aria-current=page]{background:#e9f2fc;box-shadow:none;color:#165a96}.kbw-shell>.kbw-nav .dashicons{font-size:18px;width:18px;height:18px}.kbw-setup{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:24px;padding-left:20px!important}.kbw-setup li{padding:0 0 12px;border:0!important;list-style:decimal}.kbw-fields select{width:100%;max-width:100%;min-height:44px;white-space:normal}.kbw-measurement{padding:12px;background:#edf5fc;border-radius:8px 8px 8px 0}.kbw-card--done li{grid-template-columns:130px 1fr}.kbw-card--done li>a{grid-column:2;grid-row:auto;justify-self:start}.kbw-task-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px;margin:24px 0}.kbw-task-grid .kbw-card{background:#f5f8fc}.kbw-task-grid ul{margin:0}.kbw-task-grid li:first-child{padding-top:4px}
    39914005@media(max-width:1200px){.kbw-shell{grid-template-columns:180px minmax(0,1fr);gap:18px}.kbw-main .kbw-grid,.kbw-task-grid{grid-template-columns:1fr}.kbw-setup{grid-template-columns:1fr;gap:8px}}
    … …  
    40264040    return klusio_bridge_workspace_link($section, $item['id'], '', $item['kind']);
    40274041}
     4042/** A local detail is always looked up in the consented, tenant-bound snapshot. */
     4043function klusio_bridge_workspace_detail_link($item)
     4044{
     4045    return add_query_arg(array('page' => 'klusio', 'view' => 'detail', 'kind' => $item['kind'], 'item' => $item['id']), admin_url('admin.php'));
     4046}
     4047function klusio_bridge_workspace_find_item($data, $kind, $id)
     4048{
     4049    foreach (array('next','doing','done','drafts','page_drafts') as $group) {
     4050        foreach (isset($data[$group]) ? $data[$group] : array() as $item) {
     4051            if ($item['kind'] === $kind && (int)$item['id'] === $id) { return $item; }
     4052        }
     4053    }
     4054    return null;
     4055}
     4056function klusio_bridge_workspace_local_post($id)
     4057{
     4058    if (!is_int($id) || $id < 1) { return null; }
     4059    $post = get_post($id);
     4060    return $post && in_array($post->post_type, array('page','post'), true) && in_array($post->post_status, array('publish','draft','pending','future','private'), true) && current_user_can('edit_post', $post->ID) ? $post : null;
     4061}
     4062function klusio_bridge_workspace_local_buttons($post)
     4063{
     4064    echo '<div class="kbw-buttons"><a class="kbw-btn" href="' . esc_url(get_edit_post_link($post->ID, 'raw')) . '">' . esc_html__('Bewerken in WordPress', 'klusio-builder-bridge') . ' →</a>';
     4065    $preview = $post->post_status === 'publish' ? get_permalink($post) : get_preview_post_link($post);
     4066    if ($preview) {
     4067        echo '<a class="kbw-btn" href="' . esc_url($preview) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Bekijk in de websiteopmaak', 'klusio-builder-bridge') . ' ↗</a>';
     4068    }
     4069    echo '</div>';
     4070}
     4071function klusio_bridge_workspace_detail($data)
     4072{
     4073    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only navigation in the authorized snapshot.
     4074    $kind = isset($_GET['kind']) && is_string($_GET['kind']) ? sanitize_key(wp_unslash($_GET['kind'])) : '';
     4075    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only navigation; an ID never grants access.
     4076    $id = isset($_GET['item']) && is_scalar($_GET['item']) ? absint($_GET['item']) : 0;
     4077    $item = klusio_bridge_workspace_find_item($data, $kind, $id);
     4078    $draft = in_array($kind, array('draft','page_draft'), true);
     4079    echo '<a class="kbw-back" href="' . esc_url(admin_url('admin.php?page=klusio&view=' . ($draft ? 'drafts' : 'growth'))) . '">← ' . esc_html($draft ? __('Terug naar concepten', 'klusio-builder-bridge') : __('Terug naar Groeimotor', 'klusio-builder-bridge')) . '</a>';
     4080    if (!$item) {
     4081        echo '<section class="kbw-card"><h1>' . esc_html__('Dit item staat niet meer in je overzicht', 'klusio-builder-bridge') . '</h1><p>' . esc_html__('Het kan zijn afgerond of vervangen. Kies een actueel item uit de lijst of bekijk de volledige historie in Klusio.', 'klusio-builder-bridge') . '</p></section>';
     4082        return;
     4083    }
     4084    echo '<header class="kbw-viewhead"><span class="kbw-kicker">' . esc_html($draft ? __('Concept beoordelen', 'klusio-builder-bridge') : __('Je groeivoorstel', 'klusio-builder-bridge')) . '</span><h1>' . esc_html($item['title']) . '</h1><p>' . esc_html($item['detail']) . '</p></header>';
     4085    if ($kind === 'draft') {
     4086        echo '<section class="kbw-card kbw-reading"><div class="kbw-head"><h2>' . esc_html__('Lees je concept', 'klusio-builder-bridge') . '</h2><span class="kbw-status">' . esc_html__('Concept bij laatste ontvangst', 'klusio-builder-bridge') . '</span></div><p class="kbw-small">' . esc_html__('Tekstversie uit je Klusio-account. Afbeeldingen en de definitieve opmaak bekijk je bij de publicatiecontrole.', 'klusio-builder-bridge') . '</p>';
     4087        if (!empty($item['reading'])) {
     4088            if (empty($item['reading_complete'])) {
     4089                echo '<p class="kbw-note">' . esc_html__('Dit is het eerste deel van een langer concept. Open het volledige artikel voordat je publiceert.', 'klusio-builder-bridge') . '</p>';
     4090            }
     4091            // Plain text only. Never run shortcodes, blocks, scripts or external embeds.
     4092            echo '<div class="kbw-prose">' . esc_html($item['reading']) . '</div>';
     4093        } else {
     4094            echo '<p>' . esc_html__('De tekst is nog niet in dit overzicht beschikbaar. Open het artikel in Klusio; bij de volgende geslaagde synchronisatie verschijnt de tekst hier.', 'klusio-builder-bridge') . '</p>';
     4095        }
     4096        echo '</section>';
     4097    } elseif ($kind === 'page_draft') {
     4098        $post = !empty($item['local_post_id']) ? klusio_bridge_workspace_local_post($item['local_post_id']) : null;
     4099        echo '<section class="kbw-card"><h2>' . esc_html__('Bekijk je volledige pagina', 'klusio-builder-bridge') . '</h2>';
     4100        if ($post && $post->post_type === 'page') {
     4101            echo '<p>' . esc_html__('Dit is de huidige versie op deze WordPress-website. Eigen aanpassingen blijven behouden; Klusio controleert opnieuw voordat je vanuit je account publiceert.', 'klusio-builder-bridge') . '</p>';
     4102            klusio_bridge_workspace_local_buttons($post);
     4103        } else {
     4104            echo '<p>' . esc_html__('Een bewerkbaar WordPress-concept is hier nog niet bevestigd. Bekijk de pagina-opzet en de verbindingscontrole in Klusio.', 'klusio-builder-bridge') . '</p>';
     4105        }
     4106        echo '</section>';
     4107    } else {
     4108        echo '<div class="kbw-grid">';
     4109        foreach (array('plan' => __('Wat wordt er gedaan?', 'klusio-builder-bridge'), 'your_step' => __('Wat vraagt dit van jou?', 'klusio-builder-bridge'), 'verification' => __('Hoe wordt dit gecontroleerd?', 'klusio-builder-bridge'), 'measurement' => __('Beschikbare resultaatmeting', 'klusio-builder-bridge')) as $key => $label) {
     4110            if (empty($item[$key])) { continue; }
     4111            echo '<section class="kbw-card"><h2>' . esc_html($label) . '</h2><p>' . esc_html($item[$key]) . '</p></section>';
     4112        }
     4113        echo '</div>';
     4114        if (!empty($item['page_url'])) {
     4115            echo '<p class="kbw-small">' . esc_html__('Betrokken pagina:', 'klusio-builder-bridge') . ' ' . esc_html($item['page_url']) . '</p>';
     4116        }
     4117    }
     4118    if (!empty($item['date'])) { echo '<p class="kbw-small">' . esc_html($item['date']) . '</p>'; }
     4119    echo '<section class="kbw-note"><strong>' . esc_html($draft ? __('Klaar voor de volgende stap?', 'klusio-builder-bridge') : __('Beslissen met de actuele stand', 'klusio-builder-bridge')) . '</strong><p>' . esc_html__('Je bekijkt de laatst ontvangen versie. Bij bewerken, akkoord of publicatie controleert Klusio opnieuw je rechten, de huidige inhoud en je instellingen.', 'klusio-builder-bridge') . '</p><div class="kbw-buttons"><a class="kbw-btn kbw-primary" target="_blank" rel="noopener noreferrer" href="' . esc_url(klusio_bridge_workspace_item_link($item)) . '">' . esc_html($draft ? __('Bewerken en publicatie controleren', 'klusio-builder-bridge') : __('Actueel voorstel beoordelen', 'klusio-builder-bridge')) . ' ↗</a></div></section>';
     4120}
     4121/** Extract only known static editorial fields; never run a builder renderer. */
     4122function klusio_bridge_workspace_builder_text($nodes, $builder, &$budget, $depth = 0)
     4123{
     4124    if (!is_array($nodes) || $depth > 24 || $budget <= 0) { return ''; }
     4125    $html = '';
     4126    foreach ($nodes as $node) {
     4127        if (!is_array($node) || --$budget < 0) { break; }
     4128        $s = isset($node['settings']) && is_array($node['settings']) ? $node['settings'] : array();
     4129        $type = $builder === 'Elementor' ? (isset($node['widgetType']) ? $node['widgetType'] : '') : (isset($node['name']) ? $node['name'] : '');
     4130        if (empty($s['__dynamic__']) && empty($s['query']) && empty($node['global'])) {
     4131            $text = '';
     4132            if ($type === 'heading') {
     4133                $value = $builder === 'Elementor' ? (isset($s['title']) ? $s['title'] : '') : (isset($s['text']) ? $s['text'] : '');
     4134                $tag = $builder === 'Elementor' ? (isset($s['header_size']) ? $s['header_size'] : '') : (isset($s['tag']) ? $s['tag'] : '');
     4135                if (is_string($value) && is_string($tag) && preg_match('/^h[1-6]$/D', $tag)) {
     4136                    $text = '<' . $tag . '>' . esc_html(wp_strip_all_tags($value)) . '</' . $tag . '>';
     4137                }
     4138            } elseif (($builder === 'Elementor' && $type === 'text-editor') || ($builder === 'Bricks' && in_array($type, array('text','text-basic'), true))) {
     4139                $value = $builder === 'Elementor' ? (isset($s['editor']) ? $s['editor'] : '') : (isset($s['text']) ? $s['text'] : '');
     4140                if (is_string($value)) { $text = $value; }
     4141            }
     4142            if ($text !== '' && strlen($text) < 65536 && !preg_match('/\{[^}]+\}|\[[a-zA-Z_][^\]]*\]/', $text)) { $html .= $text; }
     4143        }
     4144        if ($builder === 'Elementor' && !empty($node['elements'])) { $html .= klusio_bridge_workspace_builder_text($node['elements'], $builder, $budget, $depth + 1); }
     4145        if (strlen($html) > 262144) { return ''; }
     4146    }
     4147    return $html;
     4148}
     4149/** Read stored editorial HTML only. No rendering, shortcodes, network or writes. */
     4150function klusio_bridge_workspace_page_check($post)
     4151{
     4152    $out = array('source' => 'WordPress', 'partial' => false, 'headings' => array(), 'images' => 0, 'alt_review' => 0, 'vague_links' => 0, 'heading_jumps' => 0, 'empty_title' => trim($post->post_title) === '');
     4153    $html = $post->post_content;
     4154    if (get_post_meta($post->ID, '_elementor_edit_mode', true) === 'builder' || get_post_meta($post->ID, '_bricks_editor_mode', true) === 'bricks' || get_post_meta($post->ID, '_bricks_page_content_2', true)) {
     4155        // post_content may be stale. Read known static fields from builder data;
     4156        // templates, loops, image widgets and unknown components remain unassessed.
     4157        $out['source'] = get_post_meta($post->ID, '_elementor_edit_mode', true) === 'builder' ? 'Elementor' : 'Bricks';
     4158        $out['partial'] = true;
     4159        $raw = get_post_meta($post->ID, $out['source'] === 'Elementor' ? '_elementor_data' : '_bricks_page_content_2', true);
     4160        if (is_string($raw)) { $raw = strlen($raw) <= 2097152 ? json_decode($raw, true, 32) : array(); }
     4161        $budget = 1000;
     4162        $html = klusio_bridge_workspace_builder_text($raw, $out['source'], $budget);
     4163    }
     4164    if (strlen($html) > 262144 || !class_exists('DOMDocument')) { $out['partial'] = true; return $out; }
     4165    $out['partial'] = $out['partial'] || strpos($html, '[') !== false || strpos($html, '<!-- wp:') !== false;
     4166    if (trim($html) === '') { return $out; }
     4167    $doc = new DOMDocument();
     4168    $before = libxml_use_internal_errors(true);
     4169    try {
     4170        $doc->loadHTML('<?xml encoding="UTF-8"><html><body>' . $html . '</body></html>', LIBXML_NONET);
     4171        $xp = new DOMXPath($doc);
     4172        $previous = 0;
     4173        foreach ($xp->query('//h1|//h2|//h3|//h4|//h5|//h6') as $node) {
     4174            $level = (int)substr($node->nodeName, 1);
     4175            if ($previous && $level > $previous + 1) { $out['heading_jumps']++; }
     4176            $previous = $level;
     4177            if (count($out['headings']) < 40) { $out['headings'][] = array('level' => $level, 'text' => wp_html_excerpt(trim($node->textContent), 300, '…')); }
     4178        }
     4179        foreach ($xp->query('//img') as $img) {
     4180            $out['images']++;
     4181            if (trim($img->getAttribute('alt')) === '') { $out['alt_review']++; }
     4182        }
     4183        foreach ($xp->query('//a[@href]') as $a) {
     4184            if (in_array(strtolower(trim($a->textContent)), array('klik hier','lees meer','meer','hier','read more','click here'), true)) { $out['vague_links']++; }
     4185        }
     4186    } finally { libxml_clear_errors(); libxml_use_internal_errors($before); }
     4187    return $out;
     4188}
     4189function klusio_bridge_workspace_pages($data)
     4190{
     4191    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page selection, checked against edit_post below.
     4192    $id = isset($_GET['post']) && is_scalar($_GET['post']) ? absint($_GET['post']) : 0;
     4193    echo '<header class="kbw-viewhead"><span class="kbw-kicker">' . esc_html__('Je eigen WordPress-inhoud', 'klusio-builder-bridge') . '</span><h1>' . esc_html__('Werk gericht aan je pagina’s', 'klusio-builder-bridge') . '</h1><p>' . esc_html__('Bekijk de opgeslagen inhoud, vind aandachtspunten en open meteen de juiste WordPress-bewerker. Bij gekoppelde pagina’s zie je ook het werk van Klusio.', 'klusio-builder-bridge') . '</p></header>';
     4194    if ($id) {
     4195        $post = klusio_bridge_workspace_local_post($id);
     4196        echo '<a class="kbw-back" href="' . esc_url(admin_url('admin.php?page=klusio&view=pages')) . '">← ' . esc_html__('Alle pagina’s', 'klusio-builder-bridge') . '</a>';
     4197        if (!$post) { echo '<section class="kbw-card"><p>' . esc_html__('Deze pagina is niet beschikbaar of je hebt geen bewerkrechten.', 'klusio-builder-bridge') . '</p></section>'; return; }
     4198        $check = klusio_bridge_workspace_page_check($post);
     4199        echo '<section class="kbw-card"><span class="kbw-status">' . esc_html($check['source']) . '</span><h2>' . esc_html(get_the_title($post) ?: __('Zonder titel', 'klusio-builder-bridge')) . '</h2><p class="kbw-small">' . esc_html__('Bron: opgeslagen WordPress-inhoud, gecontroleerd bij het openen.', 'klusio-builder-bridge') . ' ' . esc_html__('Laatst gewijzigd:', 'klusio-builder-bridge') . ' ' . esc_html(get_the_modified_date('j M Y H:i', $post)) . '</p>';
     4200        klusio_bridge_workspace_local_buttons($post);
     4201        echo '</section><div class="kbw-grid"><section class="kbw-card"><h2>' . esc_html__('Dit kun je nu nalopen', 'klusio-builder-bridge') . '</h2>';
     4202        $tips = array();
     4203        if ($check['empty_title']) { $tips[] = __('De WordPress-titel is leeg. Geef de pagina een herkenbare naam.', 'klusio-builder-bridge'); }
     4204        if ($check['heading_jumps']) { $tips[] = __('De opgeslagen koppen slaan een niveau over. Controleer of de indeling logisch is.', 'klusio-builder-bridge'); }
     4205        if ($check['alt_review']) {
     4206            /* translators: %d is the number of stored images with empty alternative text. */
     4207            $tips[] = sprintf(__('%d afbeeldingen hebben een lege alt-tekst. Laat die leeg bij puur decoratieve beelden; beschrijf betekenisvolle beelden in de bewerker.', 'klusio-builder-bridge'), $check['alt_review']);
     4208        }
     4209        if ($check['vague_links']) {
     4210            /* translators: %d is the number of links with generic link text. */
     4211            $tips[] = sprintf(__('%d links hebben een algemene tekst zoals “lees meer”. Controleer of duidelijk is waar de link naartoe gaat.', 'klusio-builder-bridge'), $check['vague_links']);
     4212        }
     4213        if ($tips) { echo '<ul>'; foreach ($tips as $tip) { echo '<li>' . esc_html($tip) . '</li>'; } echo '</ul>'; }
     4214        else { echo '<p>' . esc_html($check['partial'] ? __('Voor deze inhoud is een controle in de echte paginaopmaak nodig.', 'klusio-builder-bridge') : __('In deze beperkte controle zijn geen aandachtspunten gevonden.', 'klusio-builder-bridge')) . '</p>'; }
     4215        echo '<p class="kbw-small">' . esc_html($check['partial'] ? __('Bekende statische tekstvelden worden gecontroleerd. Templates, afbeeldingswidgets, herbruikbare blokken, shortcodes en dynamische inhoud vallen buiten deze controle. Open ook de websiteopmaak.', 'klusio-builder-bridge') : __('Deze controle betreft titel, kopvolgorde, lege alt-teksten en algemene linkteksten in de opgeslagen inhoud. Je thema, bereikbaarheid van links en vindbaarheid vallen hierbuiten.', 'klusio-builder-bridge')) . '</p></section><section class="kbw-card"><h2>' . esc_html__('Koppen in de opgeslagen tekst', 'klusio-builder-bridge') . '</h2><ol class="kbw-outline">';
     4216        foreach ($check['headings'] as $heading) { echo '<li><span>H' . esc_html((string)$heading['level']) . '</span> ' . esc_html($heading['text']) . '</li>'; }
     4217        echo '</ol><p class="kbw-small">' . esc_html__('Een paginatitel uit je thema of builder kan buiten deze tekst staan. Deze lijst is geen beoordeling van de complete openbare pagina.', 'klusio-builder-bridge') . '</p></section></div>';
     4218        $related = array();
     4219        foreach (array('next','doing','done') as $group) {
     4220            foreach (isset($data[$group]) ? $data[$group] : array() as $item) {
     4221                if (rtrim($item['page_url'], '/') === rtrim((string)get_permalink($post), '/')) { $related[] = $item; }
     4222            }
     4223        }
     4224        echo '<section class="kbw-card"><h2>' . esc_html__('Klusio bij deze pagina', 'klusio-builder-bridge') . '</h2>';
     4225        klusio_bridge_workspace_items($related, __('Er staat voor deze pagina geen werk in het ontvangen overzicht.', 'klusio-builder-bridge'));
     4226        echo '<div class="kbw-buttons"><a class="kbw-btn" target="_blank" rel="noopener noreferrer" href="' . esc_url(klusio_bridge_workspace_link($data ? 'improve' : 'setup', 0, klusio_bridge_workspace_page_url($post))) . '">' . esc_html($data ? __('Uitgebreid onderzoeken met Klusio', 'klusio-builder-bridge') : __('Verbind je Klusio-account', 'klusio-builder-bridge')) . ' ↗</a></div></section>';
     4227        return;
     4228    }
     4229    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only bounded search.
     4230    $search = isset($_GET['q']) && is_string($_GET['q']) ? substr(sanitize_text_field(wp_unslash($_GET['q'])), 0, 120) : '';
     4231    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only pagination.
     4232    $page = isset($_GET['pagenum']) && is_scalar($_GET['pagenum']) ? max(1, min(1000, absint($_GET['pagenum']))) : 1;
     4233    $query = new WP_Query(array('post_type' => array('page','post'), 'post_status' => array('publish','draft','pending','future','private'), 'posts_per_page' => 15, 'paged' => $page, 's' => $search, 'orderby' => 'modified', 'order' => 'DESC', 'perm' => 'editable'));
     4234    echo '<form method="get" class="kbw-search"><input type="hidden" name="page" value="klusio"><input type="hidden" name="view" value="pages"><label for="kbw-search">' . esc_html__('Zoek een pagina of bericht', 'klusio-builder-bridge') . '</label><div><input id="kbw-search" type="search" name="q" value="' . esc_attr($search) . '" maxlength="120"><button class="kbw-btn" type="submit">' . esc_html__('Zoeken', 'klusio-builder-bridge') . '</button></div></form><section class="kbw-card"><ul class="kbw-page-list">';
     4235    $shown = 0;
     4236    foreach ($query->posts as $post) {
     4237        if (!current_user_can('edit_post', $post->ID)) { continue; }
     4238        $shown++;
     4239        $status = get_post_status_object($post->post_status);
     4240        echo '<li><div><span class="kbw-status">' . esc_html($status ? $status->label : '') . '</span><h2>' . esc_html(get_the_title($post) ?: __('Zonder titel', 'klusio-builder-bridge')) . '</h2><p class="kbw-small">' . esc_html(get_the_modified_date('j M Y H:i', $post)) . '</p></div><a class="kbw-btn" href="' . esc_url(add_query_arg(array('page' => 'klusio', 'view' => 'pages', 'post' => $post->ID), admin_url('admin.php'))) . '">' . esc_html__('Bekijk aandachtspunten', 'klusio-builder-bridge') . ' →</a></li>';
     4241    }
     4242    echo '</ul>';
     4243    if (!$shown) { echo '<p>' . esc_html__('Geen bewerkbare pagina’s gevonden voor deze selectie.', 'klusio-builder-bridge') . '</p>'; }
     4244    echo '<nav class="kbw-buttons" aria-label="' . esc_attr__('Paginering', 'klusio-builder-bridge') . '">';
     4245    foreach (array(-1 => __('Vorige', 'klusio-builder-bridge'), 1 => __('Volgende', 'klusio-builder-bridge')) as $offset => $label) {
     4246        $next = $page + $offset;
     4247        if ($next < 1 || $next > (int)$query->max_num_pages || $next > 1000) { continue; }
     4248        echo '<a class="kbw-btn" href="' . esc_url(add_query_arg(array('page' => 'klusio','view' => 'pages','q' => $search,'pagenum' => $next), admin_url('admin.php'))) . '">' . esc_html($label) . '</a>';
     4249    }
     4250    echo '</nav></section>';
     4251}
    40284252function klusio_bridge_workspace_items($items, $empty)
    40294253{
    … …  
    40484272            echo '<p class="kbw-small">' . esc_html($item['date']) . '</p>';
    40494273        }
    4050         echo '<a href="' . esc_url(klusio_bridge_workspace_item_link($item)) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Bekijk in Klusio', 'klusio-builder-bridge') . ' <span aria-hidden="true">↗</span></a></li>';
     4274        echo '<a href="' . esc_url(klusio_bridge_workspace_detail_link($item)) . '">' . esc_html__( 'Bekijk details', 'klusio-builder-bridge') . ' <span aria-hidden="true">→</span></a></li>';
    40514275    }echo '</ul>';
    40524276}
    … …  
    40604284    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only allowlisted navigation, not a settings mutation.
    40614285    $view = isset($_GET['view']) && is_string($_GET['view']) ? sanitize_key(wp_unslash($_GET['view'])) : 'overview';
    4062     $tabs = array('overview' => __('Overzicht', 'klusio-builder-bridge'),'growth' => __('Groeimotor', 'klusio-builder-bridge'),'drafts' => __('Concepten', 'klusio-builder-bridge'),'results' => __('Resultaten', 'klusio-builder-bridge'),'settings' => __('Meldingen & rechten', 'klusio-builder-bridge'),'connection' => __('Verbinding', 'klusio-builder-bridge'));
    4063     if (!isset($tabs[$view])) {
     4286    $tabs = array('overview' => __('Overzicht', 'klusio-builder-bridge'),'growth' => __('Groeimotor', 'klusio-builder-bridge'),'drafts' => __('Concepten', 'klusio-builder-bridge'),'pages' => __('Pagina’s', 'klusio-builder-bridge'),'results' => __('Resultaten', 'klusio-builder-bridge'),'settings' => __('Meldingen & rechten', 'klusio-builder-bridge'),'connection' => __('Verbinding', 'klusio-builder-bridge'));
     4287    if (!isset($tabs[$view]) && $view !== 'detail') {
    40644288        $view = 'overview';
    40654289    }
    40664290    $stale = $d && time() - (int)$d['generated_at'] > 7200;
     4291    $active_view = $view;
     4292    if ($view === 'detail') {
     4293        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Navigation highlight only.
     4294        $active_view = isset($_GET['kind']) && is_string($_GET['kind']) && in_array(sanitize_key(wp_unslash($_GET['kind'])), array('draft','page_draft'), true) ? 'drafts' : 'growth';
     4295    }
    40674296    echo '<div class="wrap kbw"><div class="kbw-top"><div class="kbw-brand">' . wp_kses(klusio_bridge_workspace_logo(), array('img' => array('src' => true, 'alt' => true, 'width' => true, 'height' => true)), array('data')) . '<span class="kbw-brand-note">' . esc_html__('In je WordPress-omgeving', 'klusio-builder-bridge') . '</span></div><a class="kbw-btn" target="_blank" rel="noopener noreferrer" href="' . esc_url($d ? klusio_bridge_workspace_link() : 'https://klusio.nl/account/') . '">' . esc_html__('Open je Klusio-account', 'klusio-builder-bridge') . ' ↗</a></div><div class="kbw-shell"><nav class="kbw-nav" aria-label="' . esc_attr__('Klusio-onderdelen', 'klusio-builder-bridge') . '">';
    4068     $tab_icons = array('overview' => 'dashboard', 'growth' => 'chart-area', 'drafts' => 'media-document', 'connection' => 'admin-links', 'settings' => 'admin-settings','results' => 'chart-line');
     4297    $tab_icons = array('overview' => 'dashboard', 'growth' => 'chart-area', 'drafts' => 'media-document', 'connection' => 'admin-links', 'settings' => 'admin-settings','results' => 'chart-line', 'pages' => 'admin-page');
    40694298    foreach ($tabs as $key => $label) {
    4070         echo '<a href="' . esc_url(add_query_arg(array('page' => 'klusio','view' => $key), admin_url('admin.php'))) . '"' . ($view === $key ? ' aria-current="page"' : '') . '><span class="dashicons dashicons-' . esc_attr($tab_icons[$key]) . '" aria-hidden="true"></span>' . esc_html($label) . '</a>';
     4299        echo '<a href="' . esc_url(add_query_arg(array('page' => 'klusio','view' => $key), admin_url('admin.php'))) . '"' . ($active_view === $key ? ' aria-current="page"' : '') . '><span class="dashicons dashicons-' . esc_attr($tab_icons[$key]) . '" aria-hidden="true"></span>' . esc_html($label) . '</a>';
    40714300    }
    40724301    echo '</nav><main class="kbw-main">';
    … …  
    40774306        echo '<div class="kbw-note" role="status"><p>' . esc_html__('Een deel van je account kon niet worden opgehaald. De lijsten hieronder zijn daardoor niet volledig. Open Klusio voor de actuele stand.', 'klusio-builder-bridge') . '</p></div>';
    40784307    }
    4079     if (!$d && $view !== 'connection' && $view !== 'settings') {
     4308    if (!$d && !in_array($view, array('connection','settings','pages'), true)) {
    40804309        klusio_bridge_workspace_onboarding();
    40814310        echo '</main></div></div>';
    40824311        return;
    40834312    }
    4084     if ($view === 'settings') {
     4313    if ($view === 'pages') {
     4314        klusio_bridge_workspace_pages($d);
     4315    } elseif ($view === 'detail') {
     4316        klusio_bridge_workspace_detail($d);
     4317    } elseif ($view === 'settings') {
    40854318        klusio_bridge_workspace_preferences_render($d);
    40864319    } elseif ($view === 'connection') {
    … …  
    40894322        }
    40904323
    4091         echo '<section class="kbw-card"><h1>' . esc_html__('Je website en Klusio', 'klusio-builder-bridge') . '</h1><p>' . esc_html__('Ontvang hier een compacte kopie van je voorstellen, concepttitels en voortgang. Klusio ververst dit via je bestaande WordPress-koppeling. Je website plaatst hierdoor niets automatisch.', 'klusio-builder-bridge') . '</p><p><strong>' . esc_html(site_url()) . '</strong></p>';
     4324        echo '<section class="kbw-card"><h1>' . esc_html__('Je website en Klusio', 'klusio-builder-bridge') . '</h1><p>' . esc_html__('Ontvang hier een kopie van je voorstellen, conceptteksten en voortgang. Klusio ververst dit via je bestaande WordPress-koppeling. Je website plaatst hierdoor niets automatisch.', 'klusio-builder-bridge') . '</p><p><strong>' . esc_html(site_url()) . '</strong></p>';
    40924325        if (current_user_can('manage_options')) {
    40934326            echo '<form class="kbw-fields" method="post" action="' . esc_url(admin_url('admin-post.php')) . '"><input type="hidden" name="action" value="klusio_workspace_settings">';
    … …  
    41394372        echo '<div class="kbw-flow"><span>' . esc_html__('Voorstel bekijken', 'klusio-builder-bridge') . '</span><span aria-hidden="true">→</span><span>' . esc_html__('Concept beoordelen', 'klusio-builder-bridge') . '</span><span aria-hidden="true">→</span><span>' . esc_html__('Resultaat volgen', 'klusio-builder-bridge') . '</span></div></div><div class="kbw-focus"><span class="kbw-kicker">' . esc_html__('Jouw volgende stap', 'klusio-builder-bridge') . '</span>';
    41404373        if ($next) {
    4141             echo '<h2>' . esc_html($next['title']) . '</h2><p>' . esc_html($next['detail']) . '</p><a class="kbw-btn kbw-primary" href="' . esc_url(klusio_bridge_workspace_item_link($next)) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Bekijk het voorstel', 'klusio-builder-bridge') . ' ↗</a>';
     4374            echo '<h2>' . esc_html($next['title']) . '</h2><p>' . esc_html($next['detail']) . '</p><a class="kbw-btn kbw-primary" href="' . esc_url(klusio_bridge_workspace_detail_link($next)) . '">' . esc_html__('Bekijk het voorstel', 'klusio-builder-bridge') . ' →</a>';
    41424375        } else {
    41434376            echo '<h2>' . esc_html($d ? __('Bekijk je Groeimotor', 'klusio-builder-bridge') : __('Verbind je accountoverzicht', 'klusio-builder-bridge')) . '</h2><p>' . esc_html($d ? __('Open Klusio voor alle kansen en de actuele planning.', 'klusio-builder-bridge') : __('Schakel het overzicht in bij Verbinding en werk het daarna bij vanuit je Klusio-account.', 'klusio-builder-bridge')) . '</p><a class="kbw-btn kbw-primary" href="' . esc_url($d ? klusio_bridge_workspace_link('growth') : admin_url('admin.php?page=klusio&view=connection')) . '">' . esc_html($d ? __('Open de Groeimotor', 'klusio-builder-bridge') : __('Verbinding instellen', 'klusio-builder-bridge')) . ' →</a>';
    … …  
    41714404{
    41724405    if (in_array($post->post_type, array('page','post'), true) && current_user_can('edit_post', $post->ID)) {
    4173         $link = klusio_bridge_workspace_data() ? klusio_bridge_workspace_link('improve', 0, klusio_bridge_workspace_page_url($post)) : admin_url('admin.php?page=klusio');
     4406        $link = klusio_bridge_workspace_can_view() ? add_query_arg(array('page' => 'klusio', 'view' => 'pages', 'post' => $post->ID), admin_url('admin.php')) : klusio_bridge_workspace_link('improve', 0, klusio_bridge_workspace_page_url($post));
    41744407        $actions['klusio_improve'] = '<a href="' . esc_url($link) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Verbeter met Klusio', 'klusio-builder-bridge') . '</a>';
    41754408    }
  • klusio-builder-bridge/trunk/readme.txt

    r3789018 r3789030  
    55Tested up to: 7.1
    66Requires PHP: 7.2
    7 Stable tag: 1.13.0
     7Stable tag: 1.14.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    4949The Growth screen includes existing internal-link and conversion actions. Their links open the exact existing account workflow, with proposal review, permissions, backups and verification. Results show live-verified work and available Search Console comparisons after 14, 28 and 56 days. Sources, periods and uncertainty remain visible. Missing measurements are never replaced with invented results.
    5050
     51= Read and work without leaving WordPress =
     52
     53The Pages section searches your editable WordPress pages and posts. A read-only check covers the stored title, heading order, empty image alternatives and generic link labels. It does not render shortcodes, run dynamic blocks, crawl links or claim to audit the complete public page. Elementor and Bricks content is identified as requiring inspection in the real builder. Native edit and preview links respect your WordPress permissions.
     54
     55Draft article text is available after the next successful synchronization with a compatible Klusio account. Text previews are bounded and clearly marked when incomplete; embedded images, scripts and trackers are not loaded. Proposal details show the same execution contract used by the account. Changes, final approvals and publishing continue through the existing checked account workflow. For full page drafts already present in this WordPress, you can open the current local preview and editor directly.
     56
    5157= What the plugin does not do =
    5258
    … …  
    119125
    120126== Changelog ==
     127
     128= 1.14.0 =
     129* Read draft article text and the existing proposal execution plan directly in WordPress.
     130* Search local pages, inspect bounded editorial checks and open native editors and previews without leaving WordPress.
     131* Open mapped full-page drafts in the current website layout, respecting local edit capabilities.
     132* Explicitly distinguish incomplete, dynamic and builder content checks from a full website audit.
     133* Extended previews use the existing opt-in, tenant binding, freshness checks and bounded authenticated synchronization. No new outbound requests or publication permissions.
     134
    121135
    122136= 1.13.0 =
Note: See TracChangeset for help on using the changeset viewer.