HEX
Server: Apache
System: Linux info 3.0 #1337 SMP Tue Jan 01 00:00:00 CEST 2000 all GNU/Linux
User: u115237990 (7145895)
PHP: 8.3.32
Disabled: NONE
Upload Files
File: /homepages/35/d993672390/htdocs/dermiteseborrheique/admin/blog.php
<?php
define('DS_APP', true);
require_once dirname(__DIR__) . '/includes/init.php';

$user = auth_require();
if (!$user['is_admin'] && !$user['is_author']) {
    flash('error', 'Accès non autorisé.');
    redirect('app/dashboard.php');
}

// ── Actions ───────────────────────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    csrf_verify();
    $action = post('action');

    if ($action === 'save_article') {
        $id         = (int)(post('id') ?: 0);
        $title      = sanitize_string(post('title'), 300);
        $intro      = sanitize_string(post('intro'), 1000);
        $body       = post('body'); // HTML TinyMCE — pas de sanitize strict
        $catId      = (int)post('category_id');
        $status     = in_array(post('status'), ['draft','published','archived']) ? post('status') : 'draft';
        $isFeatured = !empty(post('is_featured')) ? 1 : 0;
        $readTime   = max(1, min(60, (int)post('read_time')));
        $metaTitle  = sanitize_string(post('meta_title'), 300);
        $metaDesc   = sanitize_string(post('meta_desc'), 300);

        if (empty($title) || empty($body) || !$catId) {
            flash('error', 'Titre, contenu et catégorie sont obligatoires.');
            redirect('admin/blog.php?action=edit&id=' . $id);
        }

        // Slug
        $slug = $id
            ? db_fetch("SELECT slug FROM blog_articles WHERE id=?", [$id])['slug']
            : null;

        if (!$slug) {
            $baseSlug = mb_strtolower($title);
            $baseSlug = str_replace(
                ['é','è','ê','à','â','î','ô','ù','û','ç'],
                ['e','e','e','a','a','i','o','u','u','c'],
                $baseSlug
            );
            $baseSlug = preg_replace('/[^a-z0-9\s-]/', '', $baseSlug);
            $baseSlug = preg_replace('/[\s-]+/', '-', trim($baseSlug));
            $baseSlug = mb_substr($baseSlug, 0, 80);
            $slug = $baseSlug;
            // Unicité
            $n = 1;
            while (db_count("SELECT COUNT(*) FROM blog_articles WHERE slug=?", [$slug]) > 0) {
                $slug = $baseSlug . '-' . $n++;
            }
        }

        $publishedAt = ($status === 'published')
            ? ($id ? db_fetch("SELECT published_at FROM blog_articles WHERE id=?",[$id])['published_at'] ?? date('Y-m-d H:i:s') : date('Y-m-d H:i:s'))
            : null;

        // Upload image de couverture
        $coverImage = $id
            ? db_fetch("SELECT cover_image FROM blog_articles WHERE id=?", [$id])['cover_image']
            : null;
        if (!empty($_FILES['cover_image']['tmp_name'])) {
            $mime = mime_content_type($_FILES['cover_image']['tmp_name']);
            if (in_array($mime, ['image/jpeg','image/png','image/webp']) && $_FILES['cover_image']['size'] <= UPLOAD_MAX_SIZE) {
                $ext = match($mime){'image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp',default=>'jpg'};
                $fn  = 'blog_' . uniqid() . '.' . $ext;
                @mkdir(DS_UPLOADS . 'blog/', 0755, true);
                if (move_uploaded_file($_FILES['cover_image']['tmp_name'], DS_UPLOADS . 'blog/' . $fn)) {
                    $coverImage = url('uploads/blog/' . $fn);
                }
            }
        }

        if ($id) {
            db_execute(
                "UPDATE blog_articles SET
                    category_id=?, title=?, intro=?, body=?, cover_image=?,
                    status=?, is_featured=?, read_time=?,
                    meta_title=?, meta_desc=?, published_at=?
                 WHERE id=? AND user_id=?",
                [$catId, $title, $intro, $body, $coverImage,
                 $status, $isFeatured, $readTime,
                 $metaTitle ?: null, $metaDesc ?: null, $publishedAt,
                 $id, $user['id']]
            );
        } else {
            $id = db_insert(
                "INSERT INTO blog_articles
                    (user_id, category_id, slug, title, intro, body, cover_image,
                     status, is_featured, read_time, meta_title, meta_desc, published_at)
                 VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
                [$user['id'], $catId, $slug, $title, $intro, $body, $coverImage,
                 $status, $isFeatured, $readTime,
                 $metaTitle ?: null, $metaDesc ?: null, $publishedAt]
            );
        }

        // Mettre à jour le compteur catégorie
        db_execute(
            "UPDATE blog_categories SET article_count = (
                SELECT COUNT(*) FROM blog_articles WHERE category_id=blog_categories.id AND status='published'
             )"
        );

        // Créer un post social si publication et pas déjà fait
        if ($status === 'published') {
            $article = db_fetch("SELECT * FROM blog_articles WHERE id=?", [$id]);
            if (!$article['social_post_id']) {
                // Accroche courte pour le social (pas le corps complet !)
                $socialBody = "📖 Nouvel article : **{$title}**\n\n"
                    . mb_substr(strip_tags($intro), 0, 200) . "...\n\n"
                    . "🔗 Lire l'article complet → " . url('ressources/' . $slug . '/');

                $postId = db_insert(
                    "INSERT INTO social_posts (user_id, slug, title, body, tags, category_id)
                     VALUES (?, ?, ?, ?, ?, ?)",
                    [
                        $user['id'],
                        \Social::makeSlug("Article: {$title}", 0),
                        "📖 " . mb_substr($title, 0, 100),
                        $socialBody,
                        json_encode(['article', 'ressources']),
                        null
                    ]
                );
                // Fixer le slug avec l'ID
                $postSlug = \Social::makeSlug("Article: {$title}", $postId);
                db_execute("UPDATE social_posts SET slug=? WHERE id=?", [$postSlug, $postId]);
                // Lier
                db_execute("UPDATE blog_articles SET social_post_id=? WHERE id=?", [$postId, $id]);
            }
        }

        flash('success', 'Article sauvegardé.');
        redirect('admin/blog.php?action=edit&id=' . $id);
    }

    if ($action === 'delete') {
        $id = (int)post('id');
        db_execute("DELETE FROM blog_articles WHERE id=? AND user_id=?", [$id, $user['id']]);
        db_execute("UPDATE blog_categories SET article_count = (SELECT COUNT(*) FROM blog_articles WHERE category_id=blog_categories.id AND status='published')");
        flash('success', 'Article supprimé.');
        redirect('admin/blog.php');
    }
}

// ── Listes et édition ─────────────────────────────────────────────────────────
$action    = $_GET['action'] ?? 'list';
$editId    = (int)($_GET['id'] ?? 0);
$editArticle = null;
if ($action === 'edit' && $editId) {
    $editArticle = db_fetch(
        "SELECT * FROM blog_articles WHERE id=? AND (user_id=? OR ?=1)",
        [$editId, $user['id'], $user['is_admin']]
    );
}

$articles = db_fetch_all(
    "SELECT a.*, c.name as cat_name, c.icon as cat_icon
     FROM blog_articles a JOIN blog_categories c ON a.category_id=c.id
     WHERE a.user_id=? OR ?=1
     ORDER BY a.updated_at DESC",
    [$user['id'], $user['is_admin']]
);
$categories = db_fetch_all("SELECT * FROM blog_categories WHERE is_active=1 ORDER BY sort_order");

$pageTitle  = 'Gestion du blog';
$activePage = 'admin';
ob_start();
?>
<style>
.blog-admin{display:grid;grid-template-columns:240px 1fr;gap:20px;align-items:start;}
.blog-admin-nav{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:10px;position:sticky;top:80px;}
.ban-item{display:flex;align-items:center;gap:8px;padding:8px 12px;border-radius:8px;text-decoration:none;color:var(--text-mid);font-size:0.85rem;font-weight:600;transition:all 0.2s;cursor:pointer;}
.ban-item:hover,.ban-item.active{background:var(--cyan-dim);color:var(--cyan);}
.blog-admin-content{min-width:0;}
.articles-table{width:100%;border-collapse:collapse;}
.articles-table th{font-size:0.68rem;font-weight:700;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.08em;padding:8px 12px;text-align:left;border-bottom:1px solid var(--border);}
.articles-table td{font-size:0.82rem;padding:10px 12px;border-bottom:1px solid var(--border);color:var(--text-mid);vertical-align:middle;}
.articles-table tr:last-child td{border:none;}
.articles-table td:first-child{color:var(--text);font-weight:600;}
.status-badge{font-size:0.65rem;font-weight:700;padding:2px 7px;border-radius:100px;}
.s-published{background:var(--emerald-dim);color:var(--emerald);}
.s-draft{background:rgba(255,181,71,0.1);color:var(--amber);}
.s-archived{background:rgba(255,255,255,0.05);color:var(--text-dim);}
.edit-form{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:24px;}
.ef-grid{display:grid;grid-template-columns:1fr 280px;gap:20px;align-items:start;}
.fg{margin-bottom:16px;}
.fl{display:block;font-size:0.7rem;font-weight:700;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.08em;margin-bottom:7px;}
.fi{width:100%;background:var(--surface2);border:1px solid var(--border-bright);border-radius:9px;padding:9px 12px;color:var(--text);font-size:0.88rem;font-family:inherit;transition:border-color 0.2s;}
.fi:focus{outline:none;border-color:rgba(0,212,200,0.4);}
.fta{width:100%;background:var(--surface2);border:1px solid var(--border-bright);border-radius:9px;padding:9px 12px;color:var(--text);font-size:0.88rem;font-family:inherit;resize:vertical;min-height:80px;}
.fta:focus{outline:none;border-color:rgba(0,212,200,0.4);}
.fsel{width:100%;background:var(--surface2);border:1px solid var(--border-bright);border-radius:9px;padding:9px 12px;color:var(--text);font-size:0.88rem;font-family:inherit;cursor:pointer;}
.save-btn{background:var(--cyan);color:var(--bg);border:none;padding:12px 24px;border-radius:9px;font-size:0.9rem;font-weight:800;font-family:inherit;cursor:pointer;transition:all 0.2s;}
.save-btn:hover{background:#fff;}
.meta-card{background:var(--surface2);border:1px solid var(--border);border-radius:10px;padding:16px;}
.tinymce-wrap{border:1px solid var(--border-bright);border-radius:9px;overflow:hidden;min-height:400px;}
#articleBody{min-height:400px;}
</style>

<?php if ($action === 'edit'): ?>
<!-- Éditeur d'article -->
<div class="edit-form">
  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;">
    <div style="font-size:1rem;font-weight:800;"><?= $editId ? 'Modifier l\'article' : 'Nouvel article' ?></div>
    <a href="<?= url('admin/blog.php') ?>" style="font-size:0.82rem;color:var(--text-dim);text-decoration:none;">← Retour à la liste</a>
  </div>

  <form method="POST" enctype="multipart/form-data">
    <?= csrf_field() ?>
    <input type="hidden" name="action" value="save_article">
    <?php if ($editId): ?><input type="hidden" name="id" value="<?= $editId ?>"><?php endif; ?>

    <div class="ef-grid">
      <!-- Colonne principale -->
      <div>
        <div class="fg">
          <label class="fl">Titre *</label>
          <input type="text" name="title" class="fi" value="<?= e($editArticle['title'] ?? post('title')) ?>" required placeholder="Titre de l'article">
        </div>

        <div class="fg">
          <label class="fl">Accroche (chapeau) *</label>
          <textarea name="intro" class="fta" required placeholder="2-3 phrases qui donnent envie de lire. Apparaît dans les cards et en intro de l'article."><?= e($editArticle['intro'] ?? post('intro')) ?></textarea>
        </div>

        <div class="fg">
          <label class="fl">Contenu *</label>
          <!-- TinyMCE sera initialisé ici -->
          <div class="tinymce-wrap">
            <textarea name="body" id="articleBody"><?= $editArticle['body'] ?? '' ?></textarea>
          </div>
        </div>
      </div>

      <!-- Colonne latérale -->
      <div>
        <div class="meta-card" style="margin-bottom:14px;">
          <div style="font-size:0.72rem;font-weight:700;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.08em;margin-bottom:14px;">Publication</div>

          <div class="fg">
            <label class="fl">Statut</label>
            <select name="status" class="fsel">
              <option value="draft"     <?= ($editArticle['status']??'draft')==='draft'?'selected':'' ?>>📝 Brouillon</option>
              <option value="published" <?= ($editArticle['status']??'')==='published'?'selected':'' ?>>✅ Publié</option>
              <option value="archived"  <?= ($editArticle['status']??'')==='archived'?'selected':'' ?>>📦 Archivé</option>
            </select>
          </div>

          <div class="fg">
            <label class="fl">Catégorie *</label>
            <select name="category_id" class="fsel" required>
              <option value="">-- Choisir --</option>
              <?php foreach ($categories as $cat): ?>
                <option value="<?= $cat['id'] ?>" <?= ($editArticle['category_id']??0)==$cat['id']?'selected':'' ?>>
                  <?= $cat['icon'] ?> <?= e($cat['name']) ?>
                </option>
              <?php endforeach; ?>
            </select>
          </div>

          <div class="fg">
            <label class="fl">Temps de lecture (min)</label>
            <input type="number" name="read_time" class="fi" min="1" max="60" value="<?= $editArticle['read_time'] ?? 5 ?>">
          </div>

          <div style="display:flex;align-items:center;gap:8px;padding:10px 0;border-top:1px solid var(--border);">
            <input type="checkbox" name="is_featured" id="isFeatured" <?= ($editArticle['is_featured']??0)?'checked':'' ?>>
            <label for="isFeatured" style="font-size:0.82rem;cursor:pointer;">⭐ Article à la une</label>
          </div>

          <button type="submit" class="save-btn" style="width:100%;margin-top:10px;">💾 Enregistrer</button>

          <?php if ($editId): ?>
          <a href="<?= url('ressources/' . ($editArticle['slug'] ?? '') . '/') ?>" target="_blank"
             style="display:block;text-align:center;margin-top:8px;font-size:0.75rem;color:var(--cyan);text-decoration:none;">
            👁 Voir l'article →
          </a>
          <?php endif; ?>
        </div>

        <div class="meta-card" style="margin-bottom:14px;">
          <div style="font-size:0.72rem;font-weight:700;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.08em;margin-bottom:14px;">Image de couverture</div>
          <?php if (!empty($editArticle['cover_image'])): ?>
            <img src="<?= e($editArticle['cover_image']) ?>" style="width:100%;border-radius:8px;margin-bottom:10px;">
          <?php endif; ?>
          <input type="file" name="cover_image" accept="image/*" style="width:100%;font-size:0.78rem;color:var(--text-mid);">
        </div>

        <div class="meta-card">
          <div style="font-size:0.72rem;font-weight:700;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.08em;margin-bottom:14px;">SEO (optionnel)</div>
          <div class="fg">
            <label class="fl">Meta title</label>
            <input type="text" name="meta_title" class="fi" value="<?= e($editArticle['meta_title'] ?? '') ?>" placeholder="Laissez vide pour utiliser le titre">
          </div>
          <div class="fg">
            <label class="fl">Meta description</label>
            <textarea name="meta_desc" class="fta" style="min-height:60px;" placeholder="Laissez vide pour utiliser l'accroche"><?= e($editArticle['meta_desc'] ?? '') ?></textarea>
          </div>
        </div>
      </div>
    </div>
  </form>
</div>

<!-- TinyMCE -->
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
<script>
tinymce.init({
  selector: '#articleBody',
  height: 500,
  menubar: false,
  language: 'fr_FR',
  skin: document.documentElement.getAttribute('data-theme') === 'light' ? 'oxide' : 'oxide-dark',
  content_css: document.documentElement.getAttribute('data-theme') === 'light' ? 'default' : 'dark',
  plugins: [
    'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
    'preview', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
    'insertdatetime', 'media', 'table', 'help', 'wordcount'
  ],
  toolbar: 'undo redo | blocks | bold italic underline | alignleft aligncenter alignright | bullist numlist | link image | blockquote | code | fullscreen',
  content_style: 'body { font-family: Cabinet Grotesk, sans-serif; font-size: 16px; line-height: 1.8; }',
  block_formats: 'Paragraphe=p; Titre 2=h2; Titre 3=h3; Citation=blockquote; Code=pre',
  // Auto-générer les IDs sur les H2 pour la TOC
  setup: function(editor) {
    editor.on('NodeChange', function() {
      const content = editor.getContent();
      const newContent = content.replace(/<h2(?![^>]*id=)[^>]*>/g, (match) => {
        const text = match.replace(/<[^>]*>/g, '').toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
        return match.replace('<h2', '<h2 id="' + text + '"');
      });
      if (content !== newContent) editor.setContent(newContent);
    });
  }
});
</script>

<?php else: ?>
<!-- Liste des articles -->
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;">
  <div style="font-size:1rem;font-weight:800;">Mes articles (<?= count($articles) ?>)</div>
  <a href="?action=edit" style="background:var(--cyan);color:var(--bg);padding:9px 18px;border-radius:8px;text-decoration:none;font-size:0.82rem;font-weight:800;">+ Nouvel article</a>
</div>

<div style="background:var(--surface);border:1px solid var(--border);border-radius:14px;overflow:hidden;">
  <table class="articles-table">
    <thead>
      <tr>
        <th>Titre</th>
        <th>Catégorie</th>
        <th>Statut</th>
        <th>Vues</th>
        <th>Modifié</th>
        <th>Actions</th>
      </tr>
    </thead>
    <tbody>
      <?php if (empty($articles)): ?>
        <tr><td colspan="6" style="text-align:center;padding:30px;color:var(--text-dim);">Aucun article — <a href="?action=edit" style="color:var(--cyan);">créez le premier →</a></td></tr>
      <?php else: ?>
        <?php foreach ($articles as $a): ?>
        <tr>
          <td><?= e(mb_substr($a['title'], 0, 60)) ?><?= mb_strlen($a['title'])>60?'...':'' ?></td>
          <td><?= $a['cat_icon'] ?> <?= e($a['cat_name']) ?></td>
          <td><span class="status-badge s-<?= $a['status'] ?>"><?= match($a['status']){'published'=>'✅ Publié','draft'=>'📝 Brouillon',default=>'📦 Archivé'} ?></span></td>
          <td><?= number_format($a['views']) ?></td>
          <td style="font-size:0.75rem;"><?= date('d/m/Y', strtotime($a['updated_at'])) ?></td>
          <td style="display:flex;gap:6px;">
            <a href="?action=edit&id=<?= $a['id'] ?>" style="background:var(--surface2);border:1px solid var(--border);color:var(--text-mid);padding:4px 10px;border-radius:6px;font-size:0.75rem;text-decoration:none;">✏️ Éditer</a>
            <?php if ($a['status'] === 'published'): ?>
            <a href="<?= url('ressources/' . $a['slug'] . '/') ?>" target="_blank" style="background:var(--cyan-dim);border:1px solid rgba(0,212,200,0.2);color:var(--cyan);padding:4px 10px;border-radius:6px;font-size:0.75rem;text-decoration:none;">👁 Voir</a>
            <?php endif; ?>
            <form method="POST" style="display:inline;" onsubmit="return confirm('Supprimer cet article ?')">
              <?= csrf_field() ?>
              <input type="hidden" name="action" value="delete">
              <input type="hidden" name="id" value="<?= $a['id'] ?>">
              <button type="submit" style="background:rgba(255,107,138,0.08);border:1px solid rgba(255,107,138,0.2);color:var(--rose);padding:4px 10px;border-radius:6px;font-size:0.75rem;cursor:pointer;font-family:inherit;">🗑</button>
            </form>
          </td>
        </tr>
        <?php endforeach; ?>
      <?php endif; ?>
    </tbody>
  </table>
</div>
<?php endif; ?>

<?php
$content = ob_get_clean();
require DS_VIEWS . 'layouts/app.php';