Quelltext des aktuellen Standes
<?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;
}
}