OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

class-wiki.php

9 KB
<?php

if (!defined('ABSPATH')) {
    exit;
}

/**
 * Wiki-Cache-Manager für den [opennit_wiki]-Shortcode.
 *
 * Lädt Topics und Seiten von der OpenNIT-API, cacht sie als WordPress-Transients
 * und speichert Bilder lokal im Uploads-Verzeichnis (Token-freier Zugriff nach Cache).
 */
class OpenNIT_Wiki {

    const CACHE_TTL    = DAY_IN_SECONDS;   // Standard-TTL: 24h
    const TOPICS_KEY   = 'opennit_wiki_topics';
    const TOPIC_PREFIX = 'opennit_wiki_t_';
    const IMG_SUBDIR   = 'opennit-cache';

    private OpenNIT_ApiClient $client;

    public function __construct() {
        $this->client = new OpenNIT_ApiClient();
    }

    // ----------------------------------------------------------------
    // Öffentliche API
    // ----------------------------------------------------------------

    /**
     * Alle freigegebenen Topics laden (gecacht, Topic-Bilder lokal gespeichert).
     */
    public function get_topics(): array|WP_Error {
        $cached = get_transient(self::TOPICS_KEY);
        if ($cached !== false) {
            return $cached;
        }
        $result = $this->client->get_topics();
        if (!is_wp_error($result)) {
            $result = $this->localize_topic_images($result);
            set_transient(self::TOPICS_KEY, $result, self::CACHE_TTL);
        }
        return $result;
    }

    /**
     * Alle Seiten eines Topics laden (gecacht, Bilder lokal gespeichert).
     */
    public function get_topic_pages(int $id): array|WP_Error {
        $key    = self::TOPIC_PREFIX . $id;
        $cached = get_transient($key);
        if ($cached !== false) {
            return $cached;
        }
        $result = $this->client->get_topic_pages($id);
        if (!is_wp_error($result) && isset($result['pages'])) {
            $result = $this->localize_images($result);
            set_transient($key, $result, self::CACHE_TTL);
        }
        return $result;
    }

    /**
     * Gesamten Wiki-Cache leeren (Transients + Bild-Dateien).
     */
    public static function flush_all(): void {
        global $wpdb;

        $wpdb->query(
            "DELETE FROM {$wpdb->options}
             WHERE option_name LIKE '_transient_opennit\_wiki\_%'
                OR option_name LIKE '_transient_timeout\_opennit\_wiki\_%'"
        );

        $upload    = wp_upload_dir();
        $cache_dir = $upload['basedir'] . '/' . self::IMG_SUBDIR;
        if (is_dir($cache_dir)) {
            foreach (glob($cache_dir . '/*') ?: [] as $file) {
                if (is_file($file)) {
                    unlink($file);
                }
            }
        }
    }

    // ----------------------------------------------------------------
    // Bilder lokal cachen
    // ----------------------------------------------------------------

    /**
     * Topic-Cover-Bilder aus der Topics-Liste lokal cachen.
     */
    private function localize_topic_images(array $data): array {
        if (empty($data['topics'])) {
            return $data;
        }

        $token     = (string)(defined('OPENNIT_API_TOKEN') ? OPENNIT_API_TOKEN : get_option('opennit_api_token', ''));
        $upload    = wp_upload_dir();
        $cache_dir = $upload['basedir'] . '/' . self::IMG_SUBDIR;
        $cache_url = $upload['baseurl'] . '/' . self::IMG_SUBDIR;

        if (!is_dir($cache_dir)) {
            wp_mkdir_p($cache_dir);
            file_put_contents($cache_dir . '/.htaccess', "Options -Indexes\n");
        }

        foreach ($data['topics'] as &$topic) {
            if (empty($topic['image_url'])) {
                continue;
            }
            $local = $this->fetch_and_cache($topic['image_url'], $token, $cache_dir, $cache_url);
            if ($local !== null) {
                $topic['image_url'] = $local;
            }
        }
        unset($topic);

        return $data;
    }

    /**
     * Seiten-Bilder lokal cachen und URLs in content_html ersetzen.
     * Ersetzt sowohl API-Proxy-URLs als auch originale /uploads/-Pfade.
     */
    private function localize_images(array $data): array {
        $token     = (string)(defined('OPENNIT_API_TOKEN') ? OPENNIT_API_TOKEN : get_option('opennit_api_token', ''));
        $upload    = wp_upload_dir();
        $cache_dir = $upload['basedir'] . '/' . self::IMG_SUBDIR;
        $cache_url = $upload['baseurl'] . '/' . self::IMG_SUBDIR;

        if (!is_dir($cache_dir)) {
            wp_mkdir_p($cache_dir);
            file_put_contents($cache_dir . '/.htaccess', "Options -Indexes\n");
        }

        // Topic-Cover-Bild des aktuellen Topic (in topic_pages response)
        if (!empty($data['image_url'])) {
            $local = $this->fetch_and_cache($data['image_url'], $token, $cache_dir, $cache_url);
            if ($local !== null) {
                $data['image_url'] = $local;
            }
        }

        if (empty($data['pages'])) {
            return $data;
        }

        foreach ($data['pages'] as &$page) {
            if (empty($page['images'])) {
                continue;
            }
            foreach ($page['images'] as &$img) {
                $local = $this->fetch_and_cache($img['url'], $token, $cache_dir, $cache_url);
                if ($local !== null) {
                    if (!empty($page['content_html'])) {
                        // Replace the API proxy URL
                        $searches = [htmlspecialchars($img['url']), $img['url']];

                        // Also replace the original /uploads/ path that's likely in content_html
                        if (preg_match('/[?&]path=([^&]+)/i', $img['url'], $m)) {
                            $origPath = '/uploads/' . ltrim(urldecode($m[1]), '/');
                            $searches[] = htmlspecialchars($origPath);
                            $searches[] = $origPath;
                        }

                        $page['content_html'] = str_replace(
                            $searches,
                            array_fill(0, count($searches), $local),
                            $page['content_html']
                        );
                    }
                    $img['url'] = $local;
                }
            }
            unset($img);
        }
        unset($page);

        return $data;
    }

    private function fetch_and_cache(string $url, string $token, string $dir, string $base_url): ?string {
        // Rebuild the fetch URL using the configured API base URL so it matches
        // the same host/port/SSL that all other API calls use, rather than
        // trusting the app.url embedded in the API response.
        $fetch_url = $this->rebase_image_url($url);

        $hash     = md5($url); // keep original URL for hash so cache is stable
        $ext      = $this->guess_ext($url);
        $filename = $hash . ($ext !== 'bin' ? '.' . $ext : '');
        $filepath = $dir . '/' . $filename;

        if (file_exists($filepath)) {
            return $base_url . '/' . $filename;
        }

        $response = wp_remote_get($fetch_url, [
            'timeout' => 20,
            'headers' => ['Authorization' => 'Bearer ' . $token],
        ]);

        if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
            return null;
        }

        $body = wp_remote_retrieve_body($response);
        if (empty($body)) {
            return null;
        }

        if ($ext === 'bin') {
            $ct  = wp_remote_retrieve_header($response, 'content-type');
            $ext = $this->ext_from_mime($ct) ?? 'jpg';
            $filename = $hash . '.' . $ext;
            $filepath = $dir . '/' . $filename;
        }

        if (file_put_contents($filepath, $body) === false) {
            return null;
        }

        return $base_url . '/' . $filename;
    }

    private function rebase_image_url(string $url): string {
        $api_base = rtrim((string)(defined('OPENNIT_API_URL') ? OPENNIT_API_URL : get_option('opennit_api_url', '')), '/');
        if (!$api_base) {
            return $url;
        }
        if (preg_match('/[?&]path=([^&]+)/i', $url, $m)) {
            return $api_base . '/image?path=' . $m[1];
        }
        return $url;
    }

    private function guess_ext(string $url): string {
        if (preg_match('/[?&]path=([^&]+)/i', $url, $m)) {
            $ext = strtolower(pathinfo(urldecode($m[1]), PATHINFO_EXTENSION));
            if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'pdf'], true)) {
                return $ext === 'jpeg' ? 'jpg' : $ext;
            }
        }
        return 'bin';
    }

    private function ext_from_mime(string $ct): ?string {
        $map = [
            'image/jpeg'      => 'jpg',
            'image/png'       => 'png',
            'image/gif'       => 'gif',
            'image/webp'      => 'webp',
            'image/svg+xml'   => 'svg',
            'application/pdf' => 'pdf',
        ];
        $ct = strtolower(trim(explode(';', $ct)[0]));
        return $map[$ct] ?? null;
    }
}