GitGram — pool.php — GitGram
Hobbes_OS2_Archive / main / v1.12 / pages / pool.php23,583 B↓ Raw
<?php
if (!defined('HOBBES')) { http_response_code(403); exit; }
require_role('editor');

$cats    = categories_load();
$action  = $route_params['action'] ?? 'list';
$item_id = $route_params['id']     ?? '';

// Extensions excluded from bulk pool operations (folder import, approve-all).
// HTML/HTM files are description sidecars from the old Hobbes archive structure;
// PHP files are never safe to auto-approve. Individual imports are unaffected.
define('POOL_BULK_EXCLUDE', ['html', 'htm', 'php']);

// ── Handle approvals ────────────────────────────────────────────────────────
// Approving a web-upload item also saves any metadata edits made inline.
// Category changes move the physical file so stored_name stays in sync.

if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'approve') {
    csrf_check();
    $meta = file_meta_load($item_id);
    if ($meta && empty($meta['approved'])) {
        $user = current_user();

        // Save metadata fields submitted alongside the approval form
        if (isset($_POST['title'])) {
            $old_category = $meta['category'] ?? '';
            $new_category = trim($_POST['category'] ?? $old_category);
            $old_path     = file_physical_path($meta);

            $meta['title']        = trim($_POST['title']        ?? $meta['title']);
            $meta['description']  = trim($_POST['desc']         ?? $meta['description']);
            $meta['version']      = trim($_POST['version']      ?? '');
            $meta['author']       = trim($_POST['author']       ?? '');
            $meta['homepage']     = trim($_POST['homepage']     ?? '');
            $meta['category']     = $new_category;
            $meta['os2_version']  = trim($_POST['os2_version']  ?? '');
            $meta['tags']         = trim($_POST['tags']         ?? '');
            $meta['license']      = trim($_POST['license']      ?? '');
            $meta['requirements'] = trim($_POST['requirements'] ?? '');

            if ($old_category !== $new_category && file_exists($old_path)) {
                $new_cat_path = category_upload_path($new_category);
                $new_dir      = UPLOADS_DIR . '/' . $new_cat_path;
                if (!is_dir($new_dir)) mkdir($new_dir, 0755, true);
                $new_file = $new_dir . '/' . $meta['original_name'];

                if (file_exists($new_file) && realpath($new_file) !== realpath($old_path)) {
                    flash('error', 'A file named "' . h($meta['original_name']) . '" already exists in the target category. Category not changed.');
                    $meta['category'] = $old_category;
                } elseif (@rename($old_path, $new_file)) {
                    $meta['stored_name'] = $new_cat_path . '/' . $meta['original_name'];
                    $old_dir = dirname($old_path);
                    if ($old_dir !== UPLOADS_DIR && is_dir($old_dir) && !glob($old_dir . '/*')) {
                        @rmdir($old_dir);
                    }
                } else {
                    flash('error', 'Could not move the file on disk. Category not changed.');
                    $meta['category'] = $old_category;
                }
            }
        }

        $meta['approved']    = true;
        $meta['approved_by'] = $user['username'];
        $meta['approved_at'] = time();
        file_meta_save($meta);
        search_index_file($meta);
        flash('success', 'File "' . h($meta['original_name']) . '" approved and added to archive.');
    }
    redirect('/pool');
}

if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'reject') {
    csrf_check();
    $meta = file_meta_load($item_id);
    if ($meta && empty($meta['approved'])) {
        $path = file_physical_path($meta);
        if (file_exists($path)) unlink($path);
        // Legacy: clean up the per-file id directory if it exists and is empty
        if (!str_contains($meta['stored_name'], '/')) {
            $dir = UPLOADS_DIR . '/' . $meta['id'];
            if (is_dir($dir)) @rmdir($dir);
        }
        search_remove_file($meta['id']);
        file_meta_delete($meta['id']);
        flash('success', 'File rejected and removed.');
    }
    redirect('/pool');
}

// ── Handle FTP pool import (single file) ────────────────────────────────────

if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'import') {
    csrf_check();
    $pool_file = POOL_DIR . '/' . safe_filename($_POST['pool_file'] ?? '');
    if (!file_exists($pool_file)) {
        flash('error', 'Pool file not found.');
        redirect('/pool');
    }
    $orig     = safe_filename(basename($pool_file));
    $title    = trim($_POST['title']    ?? '');
    $desc     = trim($_POST['desc']     ?? '');
    $version  = trim($_POST['version']  ?? '');
    $author   = trim($_POST['author']   ?? '');
    $homepage = trim($_POST['homepage'] ?? '');
    $cat_id   = trim($_POST['category'] ?? '');
    $os2_ver  = trim($_POST['os2_version'] ?? '');
    $tags     = trim($_POST['tags']     ?? '');
    $license  = trim($_POST['license']  ?? '');
    $requires = trim($_POST['requirements'] ?? '');
    $approve  = !empty($_POST['approve_now']);

    $errs = [];
    if (!$title)  $errs[] = 'Title required.';
    if (!$desc)   $errs[] = 'Description required.';
    if (!$author) $errs[] = 'Author required.';
    if (!$cat_id) $errs[] = 'Category required.';
    if (!is_allowed_file($orig)) $errs[] = 'File type not allowed.';

    if (empty($errs)) {
        $id       = hobbes_id();
        $cat_path = category_upload_path($cat_id);
        $dest_dir = UPLOADS_DIR . '/' . $cat_path;
        if (!is_dir($dest_dir)) mkdir($dest_dir, 0755, true);
        $dest_file = $dest_dir . '/' . $orig;
        if (file_exists($dest_file)) {
            $errs[] = 'A file named "' . h($orig) . '" already exists in that category.';
        } elseif (!rename($pool_file, $dest_file)) {
            $errs[] = 'Failed to move file from pool.';
        } else {
            @unlink($pool_file . '.meta.json');
            $user = current_user();
            $meta = [
                'id'            => $id,
                'original_name' => $orig,
                'stored_name'   => $cat_path . '/' . $orig,
                'title'         => $title,
                'description'   => $desc,
                'version'       => $version,
                'author'        => $author,
                'homepage'      => $homepage,
                'category'      => $cat_id,
                'os2_version'   => $os2_ver,
                'tags'          => $tags,
                'license'       => $license,
                'requirements'  => $requires,
                'size'          => filesize($dest_file),
                'uploader'      => $user['username'],
                'uploaded'      => time(),
                'approved'      => $approve,
                'approved_by'   => $approve ? $user['username'] : null,
                'approved_at'   => $approve ? time() : null,
                'downloads'     => 0,
                'source'        => 'ftp',
            ];
            file_meta_save($meta);
            if ($approve) search_index_file($meta);
            flash('success', 'Pool file imported' . ($approve ? ' and approved' : ' (pending approval)') . '.');
            redirect('/pool');
        }
    }
    if (!empty($errs)) {
        foreach ($errs as $e) flash('error', $e);
        redirect('/pool');
    }
}

// ── Handle FTP pool folder batch import ─────────────────────────────────────

if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'import_folder') {
    csrf_check();
    set_time_limit(600);
    $folder_name = basename($_POST['pool_folder'] ?? '');
    $folder_path = POOL_DIR . '/' . $folder_name;

    // Security: ensure path resolves inside POOL_DIR
    $real_pool   = realpath(POOL_DIR);
    $real_folder = $folder_name ? realpath($folder_path) : false;
    if (!$real_folder || !str_starts_with($real_folder, $real_pool . DIRECTORY_SEPARATOR) || !is_dir($folder_path)) {
        flash('error', 'Folder not found in pool.');
        redirect('/pool');
    }

    $approve = !empty($_POST['approve_now']);
    $user    = current_user();

    // Load categories fresh, bypassing static cache
    $cats_data = storage_read(CATS_FILE);
    $cats      = $cats_data['categories'] ?? [];

    // Root folder → top-level category
    $root_cat_name = ucwords(str_replace(['-', '_'], ' ', $folder_name));
    $root_cat_id   = category_find_or_create($cats, $root_cat_name, null);

    // Pre-scan folder structure to create all categories upfront, then persist
    // before touching any files. This ensures category IDs are always saved to
    // disk even if the import is interrupted mid-way.
    $all_files = pool_folder_files($folder_path);
    foreach ($all_files as $finfo) {
        $rel_dir = dirname($finfo['rel']);
        if ($rel_dir === '.') continue;
        $parts  = explode('/', str_replace('\\', '/', $rel_dir));
        $par_id = $root_cat_id;
        foreach ($parts as $part) {
            $cat_name = ucwords(str_replace(['-', '_'], ' ', $part));
            $par_id   = category_find_or_create($cats, $cat_name, $par_id);
        }
    }
    categories_save($cats); // persist categories before any files are moved

    $imported  = 0;
    $skipped   = 0;
    $errors    = [];

    foreach ($all_files as $finfo) {
        $filename = $finfo['name'];
        $rel      = $finfo['rel'];   // e.g. "subdir/file.zip" or "file.zip"
        $abs      = $finfo['abs'];

        if (!is_allowed_file($filename) || in_array(get_extension($filename), POOL_BULK_EXCLUDE, true)) {
            $skipped++;
            continue;
        }

        // Determine target category from subdirectory path
        $rel_dir = dirname($rel);
        $cat_id  = $root_cat_id;
        if ($rel_dir !== '.') {
            $parts  = explode('/', str_replace('\\', '/', $rel_dir));
            $par_id = $root_cat_id;
            foreach ($parts as $part) {
                $cat_name = ucwords(str_replace(['-', '_'], ' ', $part));
                $par_id   = category_find_or_create($cats, $cat_name, $par_id);
            }
            $cat_id = $par_id;
        }

        $orig      = safe_filename($filename);
        $cat_path  = category_upload_path_from_cats($cats, $cat_id);
        $dest_dir  = UPLOADS_DIR . '/' . $cat_path;

        if (!is_dir($dest_dir)) mkdir($dest_dir, 0755, true);

        $dest_file = $dest_dir . '/' . $orig;
        if (file_exists($dest_file)) {
            $errors[] = 'Skipped (duplicate): ' . $rel;
            $skipped++;
            continue;
        }
        if (!rename($abs, $dest_file)) {
            $errors[] = 'Failed to move: ' . $rel;
            $skipped++;
            continue;
        }

        // Derive a readable title from filename; fall back to "Unknown"
        $name_part     = pathinfo($filename, PATHINFO_FILENAME);
        $derived_title = ucwords(str_replace(['-', '_', '.'], ' ', $name_part));

        $id   = hobbes_id();
        $meta = [
            'id'            => $id,
            'original_name' => $orig,
            'stored_name'   => $cat_path . '/' . $orig,
            'title'         => $derived_title ?: 'Unknown',
            'description'   => 'Unknown',
            'version'       => 'Unknown',
            'author'        => 'Unknown',
            'homepage'      => '',
            'category'      => $cat_id,
            'os2_version'   => '',
            'tags'          => '',
            'license'       => '',
            'requirements'  => '',
            'size'          => filesize($dest_file),
            'uploader'      => $user['username'],
            'uploaded'      => time(),
            'approved'      => $approve,
            'approved_by'   => $approve ? $user['username'] : null,
            'approved_at'   => $approve ? time() : null,
            'downloads'     => 0,
            'source'        => 'ftp',
        ];
        file_meta_save($meta);
        if ($approve) search_index_file($meta);
        $imported++;
    }

    // Persist the updated (possibly expanded) category tree
    categories_save($cats);

    // Remove the now-empty pool folder
    rmdir_recursive($folder_path);

    $msg = "Folder \"{$folder_name}\" imported: {$imported} file(s) added";
    if ($skipped) $msg .= ", {$skipped} skipped";
    flash('success', $msg . '.');
    foreach (array_slice($errors, 0, 10) as $e) flash('error', $e);
    redirect('/pool');
}

// ── Handle FTP pool folder rejection ────────────────────────────────────────

if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'reject_folder') {
    csrf_check();
    $folder_name = basename($_POST['pool_folder'] ?? '');
    $folder_path = POOL_DIR . '/' . $folder_name;
    $real_pool   = realpath(POOL_DIR);
    $real_folder = $folder_name ? realpath($folder_path) : false;
    if ($real_folder && str_starts_with($real_folder, $real_pool . DIRECTORY_SEPARATOR) && is_dir($folder_path)) {
        rmdir_recursive($folder_path);
        flash('success', 'Folder "' . $folder_name . '" deleted from pool.');
    } else {
        flash('error', 'Folder not found in pool.');
    }
    redirect('/pool');
}

// ── Approve all pending web uploads ─────────────────────────────────────────

if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'approve_all') {
    csrf_check();
    $user  = current_user();
    $now   = time();
    $count = 0;
    // Approve every record in data/files/ that is still pending.
    // This includes both web uploads (source=web) and FTP files that
    // were imported without immediate approval (source=ftp).
    // Unimported FTP pool files and folders have no files/ record yet
    // and are therefore unaffected.
    foreach (file_meta_list(false) as $meta) {
        if (!empty($meta['approved'])) continue;
        if (in_array(get_extension($meta['original_name'] ?? ''), POOL_BULK_EXCLUDE, true)) continue;
        $meta['approved']    = true;
        $meta['approved_by'] = $user['username'];
        $meta['approved_at'] = $now;
        file_meta_save($meta);
        search_index_file($meta);
        $count++;
    }
    flash('success', $count ? "{$count} file(s) approved." : 'Nothing pending approval.');
    redirect('/pool');
}


$pool_items = pool_list();
$page_title = 'Approval Pool';
$_page      = 'pool';
include ROOT_DIR . '/templates/header.php';
?>

<div class="panel">
  <div class="panel-title">Approval Pool (<?php echo count($pool_items); ?> items)</div>
  <div class="panel-body">
    <p class="muted" style="margin-bottom:8px;">
      Files uploaded via the web form or FTP appear here for review.
      Editors and Admins can approve, reject, or edit metadata before publishing to the archive.
      FTP folders are batch-imported: sub-folders become sub-categories and required fields
      default to <em>Unknown</em> when no metadata is available.
    </p>

<?php
// Count every item that has a file record (imported or web-uploaded) and
// is still pending — these are exactly what approve_all will act on.
$n_approvable = count(array_filter($pool_items, fn($i) =>
    !empty($i['id']) && !in_array(get_extension($i['name'] ?? ''), POOL_BULK_EXCLUDE, true)
));
?>
<?php if ($n_approvable > 0): ?>
    <div style="display:flex;align-items:center;gap:12px;padding:8px 10px;
                background:#e8ffe8;border:1px solid #006600;margin-bottom:12px;">
      <span><?php echo $n_approvable; ?> file(s) pending approval.</span>
      <form method="post" action="/pool/approve_all" style="margin:0;">
        <?php echo csrf_field(); ?>
        <button type="submit" class="btn btn-primary"
                onclick="return confirm('Approve all <?php echo $n_approvable; ?> pending file(s)?');">
          Approve All
        </button>
      </form>
    </div>
<?php endif; ?>

<?php if (empty($pool_items)): ?>
    <p>No files pending approval.</p>
<?php else: ?>
<?php foreach ($pool_items as $item): ?>
    <div class="panel" style="margin-bottom:14px;">
      <div class="panel-title">
<?php if ($item['source'] === 'ftp_folder'): ?>
        [DIR] <?php echo h($item['name']); ?>
        &mdash; <?php echo (int)$item['file_count']; ?> file(s)
        &mdash; <span class="muted">FTP Folder</span>
        &mdash; <?php echo fmt_datetime($item['mtime']); ?>
<?php else: ?>
        <?php echo h($item['name']); ?>
        &mdash; <?php echo format_size($item['size']); ?>
        &mdash; <span class="muted"><?php echo $item['source'] === 'ftp' ? 'FTP Upload' : 'Web Upload'; ?></span>
        &mdash; <?php echo fmt_datetime($item['mtime']); ?>
<?php endif; ?>
      </div>
      <div class="panel-body">

<?php if ($item['source'] === 'ftp_folder'): ?>
        <p class="muted" style="margin-bottom:8px;">
          Folder from FTP pool. Sub-folders will be created as sub-categories mirroring the
          directory structure. Required fields with no available metadata will be set to
          <strong>Unknown</strong>. Titles are derived from filenames.
        </p>
        <form method="post" action="/pool/import_folder" class="std">
          <?php echo csrf_field(); ?>
          <input type="hidden" name="pool_folder" value="<?php echo h($item['name']); ?>">
          <div style="margin-top:6px;">
            <label style="display:inline;font-weight:normal;">
              <input type="checkbox" name="approve_now" value="1"> Approve all files immediately
            </label>
          </div>
          <div style="margin-top:8px;">
            <button type="submit" class="btn btn-primary">Batch Import Folder</button>
          </div>
        </form>
        <form method="post" action="/pool/reject_folder" style="margin-top:8px;"
              onsubmit="return confirm('Permanently delete this folder and all its contents?');">
          <?php echo csrf_field(); ?>
          <input type="hidden" name="pool_folder" value="<?php echo h($item['name']); ?>">
          <button type="submit" class="btn btn-danger">Reject &amp; Delete Folder</button>
        </form>

<?php elseif (!empty($item['id'])): ?>
        <?php
            $meta     = $item['meta'];
            $sub_bc   = category_breadcrumb($cats, $meta['category'] ?? '');
            $sub_path = $sub_bc ? implode(' › ', array_column($sub_bc, 'name')) : '—';
        ?>
        <p class="muted" style="margin-bottom:10px;font-size:0.85rem;">
          Submitted by <strong><?php echo h($meta['uploader'] ?? ''); ?></strong>
          to <strong><?php echo h($sub_path); ?></strong>
        </p>
        <form method="post" action="/pool/approve/<?php echo h($item['id']); ?>" class="std">
          <?php echo csrf_field(); ?>
          <label>Title *
            <input type="text" name="title" value="<?php echo h($meta['title'] ?? ''); ?>" required maxlength="200">
          </label>
          <label>Description *
            <textarea name="desc" rows="4"><?php echo h($meta['description'] ?? ''); ?></textarea>
          </label>
          <label>Author *
            <input type="text" name="author" value="<?php echo h($meta['author'] ?? ''); ?>" maxlength="200">
          </label>
          <label>Version
            <input type="text" name="version" value="<?php echo h($meta['version'] ?? ''); ?>" maxlength="50">
          </label>
          <label>Homepage
            <input type="url" name="homepage" value="<?php echo h($meta['homepage'] ?? ''); ?>" maxlength="500">
          </label>
          <label>Category
            <select name="category">
              <option value="">-- Uncategorized --</option>
              <?php render_cat_options(build_category_tree($cats), $meta['category'] ?? ''); ?>
            </select>
          </label>
          <label>OS/2 Version
            <input type="text" name="os2_version" value="<?php echo h($meta['os2_version'] ?? ''); ?>" maxlength="100">
          </label>
          <label>License
            <input type="text" name="license" value="<?php echo h($meta['license'] ?? ''); ?>" maxlength="100">
          </label>
          <label>Requirements
            <input type="text" name="requirements" value="<?php echo h($meta['requirements'] ?? ''); ?>" maxlength="300">
          </label>
          <label>Tags
            <input type="text" name="tags" value="<?php echo h($meta['tags'] ?? ''); ?>" maxlength="300">
          </label>
          <div class="row" style="gap:8px;margin-top:4px;">
            <button type="submit" class="btn btn-primary">Approve</button>
          </div>
        </form>
        <form method="post" action="/pool/reject/<?php echo h($item['id']); ?>"
              style="margin-top:8px;"
              onsubmit="return confirm('Permanently reject and delete &quot;<?php echo addslashes($item['name']); ?>&quot;?');">
          <?php echo csrf_field(); ?>
          <button type="submit" class="btn btn-danger">Reject &amp; Delete</button>
        </form>

<?php else: // ftp single file ?>
        <p class="muted" style="margin-bottom:8px;">File from FTP pool. Please fill in the required information below before importing.</p>
        <form method="post" action="/pool/import" class="std">
          <?php echo csrf_field(); ?>
          <input type="hidden" name="pool_file" value="<?php echo h(basename($item['pool_file'])); ?>">
          <label>Title * <input type="text" name="title" value="<?php echo h($item['meta']['title'] ?? ''); ?>" required></label>
          <label>Description * <textarea name="desc" rows="3"><?php echo h($item['meta']['desc'] ?? ''); ?></textarea></label>
          <label>Version <input type="text" name="version" value="<?php echo h($item['meta']['version'] ?? ''); ?>"></label>
          <label>Author * <input type="text" name="author" value="<?php echo h($item['meta']['author'] ?? ''); ?>" required></label>
          <label>Homepage <input type="url" name="homepage" value="<?php echo h($item['meta']['homepage'] ?? ''); ?>"></label>
          <label>Category *
            <select name="category" required>
              <option value="">-- Select --</option>
              <?php render_cat_options(build_category_tree($cats), $_POST['category'] ?? ''); ?>
            </select>
          </label>
          <label>OS/2 Version <input type="text" name="os2_version" value="<?php echo h($item['meta']['os2_version'] ?? ''); ?>"></label>
          <label>License <input type="text" name="license" value=""></label>
          <label>Requirements <input type="text" name="requirements" value=""></label>
          <label>Tags <input type="text" name="tags" value="<?php echo h($item['meta']['tags'] ?? ''); ?>"></label>
          <div style="margin-top:10px;">
            <label style="display:inline;font-weight:normal;">
              <input type="checkbox" name="approve_now" value="1"> Approve immediately
            </label>
          </div>
          <div style="margin-top:10px;">
            <button type="submit" class="btn btn-primary">Import from Pool</button>
          </div>
        </form>
<?php endif; ?>

      </div>
    </div>
<?php endforeach; ?>
<?php endif; ?>
  </div>
</div>

<?php include ROOT_DIR . '/templates/footer.php'; ?>
Ready
GitGram