$municipality_id]; // Optional: Filters by Status (Default: only approved) $status = $input['status'] ?? 'approved'; if ($status !== 'all') { $sql .= " AND status = :status"; $params[':status'] = $status; } // Optional: Filters by Category if (!empty($input['category'])) { $sql .= " AND category = :cat"; $params[':cat'] = $input['category']; } $sql .= " ORDER BY created_at DESC"; try { // Prepared Statement to prevent SQL Injection $stmt = $pdo->prepare($sql); $stmt->execute($params); // Fetches Results as PHP-Array $rows = $stmt->fetchAll(); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } // Builds GeoJSON FeatureCollection $features = []; foreach ($rows as $row) { $geometry = json_decode($row['geojson']); // Removes raw Geometry Columns from Properties unset($row['geom']); unset($row['geojson']); $features[] = [ 'type' => 'Feature', 'geometry' => $geometry, 'properties' => $row ]; } $featureCollection = [ 'type' => 'FeatureCollection', 'features' => $features ]; // Includes User's Votes for persistent Vote Display // Returns which Contributions the current Browser has voted on $browser_id = $input['browser_id'] ?? ''; if ($browser_id !== '') { $stmt = $pdo->prepare(" SELECT contribution_id, vote_type FROM votes WHERE browser_id = :bid "); $stmt->execute([':bid' => $browser_id]); $user_votes = []; foreach ($stmt->fetchAll() as $v) { $user_votes[$v['contribution_id']] = $v['vote_type']; } $featureCollection['user_votes'] = $user_votes; } json_response($featureCollection); } // --------------------------------------------------------------------- // CREATE: Inserts new Contributions // Required: municipality_id, geom, geom_type, category, title, author_name // Optional: description // --------------------------------------------------------------------- // --------------------------------------------------------------------- // CREATE: Inserts new Contributions with optional Photo Upload // Required: municipality_id, geom, geom_type, category, title, author_name // Optional: description, browser_id, photo (File Upload) // --------------------------------------------------------------------- function handle_create($input) { $pdo = get_db(); // Validates Input $missing = validate_required($input, [ 'municipality_id', 'geom', 'geom_type', 'category', 'title', 'author_name' ]); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } // Validates Geometry Type $valid_geom_types = ['point', 'line', 'polygon']; if (!in_array($input['geom_type'], $valid_geom_types)) { error_response('Invalid Geometry Type. Must be: ' . implode(', ', $valid_geom_types)); } // Validates GeoJSON $geojson = json_decode($input['geom']); if (!$geojson || !isset($geojson->type)) { error_response('Invalid GeoJSON in Geometry Field.'); } // Handles Photo Upload $photo_path = null; if (isset($_FILES['photo']) && $_FILES['photo']['error'] === UPLOAD_ERR_OK) { $photo_path = handle_photo_upload($_FILES['photo']); if (!$photo_path) { error_response('Photo Upload failed. JPG, PNG, GIF and WebP up to 5 MB are allowed.'); } } // Prepared SQL Statement try { $stmt = $pdo->prepare(" INSERT INTO contributions (municipality_id, geom, geom_type, category, title, description, author_name, browser_id, photo_path) VALUES (:mid, ST_SetSRID(ST_GeomFromGeoJSON(:geom), 4326), :geom_type, :category, :title, :description, :author_name, :browser_id, :photo_path) "); $stmt->execute([ ':mid' => $input['municipality_id'], ':geom' => $input['geom'], ':geom_type' => $input['geom_type'], ':category' => $input['category'], ':title' => $input['title'], ':description' => $input['description'] ?? '', ':author_name' => $input['author_name'], ':browser_id' => $input['browser_id'] ?? null, ':photo_path' => $photo_path ]); json_response([ 'message' => 'Contribution created successfully.', 'contribution_id' => (int) $pdo->lastInsertId() ], 201); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // UPDATE: Updates existing Contributions // Required: contribution_id // Optional: category, title, description, status // Provided Fields are updated. Others remain unchanged. // --------------------------------------------------------------------- function handle_update($input) { $pdo = get_db(); // Validates Input $missing = validate_required($input, ['contribution_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } $contribution_id = $input['contribution_id']; // Checks if Contribution exists $stmt = $pdo->prepare("SELECT contribution_id FROM contributions WHERE contribution_id = :id"); $stmt->execute([':id' => $contribution_id]); if (!$stmt->fetch()) { error_response('Contribution not found.', 404); } // Builds dynamic SQL Query to only update sent Fields $updatable_fields = ['category', 'title', 'description', 'status', 'address']; $set_clauses = []; $params = [':id' => $contribution_id]; foreach ($updatable_fields as $field) { if (isset($input[$field]) && $input[$field] !== '') { $set_clauses[] = "$field = :$field"; $params[":$field"] = $input[$field]; } } if (empty($set_clauses)) { error_response('No Fields to update. Provide at least one of: ' . implode(', ', $updatable_fields)); } // Validates Status if (isset($params[':status'])) { $valid_statuses = ['pending', 'approved', 'rejected', 'in_progress', 'done']; if (!in_array($params[':status'], $valid_statuses)) { error_response('Invalid Status. Must be: ' . implode(', ', $valid_statuses)); } } // Builds SQL Statement $sql = "UPDATE contributions SET " . implode(', ', $set_clauses) . " WHERE contribution_id = :id"; // Prepared SQL Statement try { $stmt = $pdo->prepare($sql); $stmt->execute($params); json_response(['message' => 'Contribution updated successfully.']); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // DELETE: Deletes existing Contributions // Required: contribution_id // Associated Votes are deleted automatically // --------------------------------------------------------------------- function handle_delete($input) { $pdo = get_db(); // Validates Input $missing = validate_required($input, ['contribution_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } $contribution_id = $input['contribution_id']; // Checks if Contribution exists $stmt = $pdo->prepare("SELECT contribution_id FROM contributions WHERE contribution_id = :id"); $stmt->execute([':id' => $contribution_id]); if (!$stmt->fetch()) { error_response('Contribution not found.', 404); } // Prepared SQL Statement try { $stmt = $pdo->prepare("DELETE FROM contributions WHERE contribution_id = :id"); $stmt->execute([':id' => $contribution_id]); json_response(['message' => 'Contribution deleted successfully.']); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // VOTE: Likes or Dislikes a Contribution // Required: contribution_id, voter_name, vote_type // Database Trigger automatically updates Likes and Dislikes Count // UNIQUE Constraint prevents duplicate Votes per Voter. // --------------------------------------------------------------------- function handle_vote($input) { $pdo = get_db(); // Validates Input $missing = validate_required($input, ['contribution_id', 'voter_name', 'vote_type']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } // Validates Vote Type $valid_vote_types = ['like', 'dislike']; if (!in_array($input['vote_type'], $valid_vote_types)) { error_response('Invalid vote_type. Must be: ' . implode(', ', $valid_vote_types)); } // Checks if Contribution exists $stmt = $pdo->prepare("SELECT contribution_id FROM contributions WHERE contribution_id = :id"); $stmt->execute([':id' => $input['contribution_id']]); if (!$stmt->fetch()) { error_response('Contribution not found.', 404); } // Prepared SQL Statement try { // Checks if Voter already voted on this Contribution $browser_id = $input['browser_id'] ?? ''; if (empty($browser_id)) { error_response('Browser ID required for Voting.'); } $stmt = $pdo->prepare(" SELECT vote_id, vote_type FROM votes WHERE contribution_id = :cid AND browser_id = :bid "); $stmt->execute([':cid' => $input['contribution_id'], ':bid' => $browser_id]); $existing = $stmt->fetch(); if ($existing) { if ($existing['vote_type'] === $input['vote_type']) { // Same Vote Type — Removes Vote $stmt = $pdo->prepare("DELETE FROM votes WHERE vote_id = :vid"); $stmt->execute([':vid' => $existing['vote_id']]); json_response(['message' => 'Vote removed.', 'action' => 'removed']); } else { // Different Vote Type — Switches Vote $stmt = $pdo->prepare("DELETE FROM votes WHERE vote_id = :vid"); $stmt->execute([':vid' => $existing['vote_id']]); $stmt = $pdo->prepare(" INSERT INTO votes (contribution_id, voter_name, vote_type, browser_id) VALUES (:cid, :voter, :vtype, :bid) "); $stmt->execute([ ':cid' => $input['contribution_id'], ':voter' => $input['voter_name'], ':vtype' => $input['vote_type'], ':bid' => $browser_id ]); json_response(['message' => 'Vote changed.', 'action' => 'changed'], 200); } } else { // No existing Vote — Inserts Vote $stmt = $pdo->prepare(" INSERT INTO votes (contribution_id, voter_name, vote_type, browser_id) VALUES (:cid, :voter, :vtype, :bid) "); $stmt->execute([ ':cid' => $input['contribution_id'], ':voter' => $input['voter_name'], ':vtype' => $input['vote_type'], ':bid' => $browser_id ]); json_response(['message' => 'Vote recorded.', 'action' => 'created'], 201); } } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // ===================================================================== // Action Handlers for News // ===================================================================== // --------------------------------------------------------------------- // CREATE NEWS: Inserts new News Entry // Required: municipality_id, title, content // --------------------------------------------------------------------- function handle_create_news($input) { $pdo = get_db(); $missing = validate_required($input, ['municipality_id', 'title', 'content']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } try { $stmt = $pdo->prepare(" INSERT INTO news (municipality_id, title, content, author_name) VALUES (:mid, :title, :content, :author) "); $stmt->execute([ ':mid' => $input['municipality_id'], ':title' => $input['title'], ':content' => $input['content'], ':author' => $input['author_name'] ?? 'Stadtverwaltung' ]); json_response(['message' => 'News created successfully.', 'news_id' => (int) $pdo->lastInsertId()], 201); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // UPDATE NEWS: Updates existing News Entry // Required: news_id // Optional: title, content // --------------------------------------------------------------------- function handle_update_news($input) { $pdo = get_db(); $missing = validate_required($input, ['news_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } $set = []; $params = [':id' => $input['news_id']]; foreach (['title', 'content', 'author_name'] as $field) { if (isset($input[$field]) && $input[$field] !== '') { $set[] = "$field = :$field"; $params[":$field"] = $input[$field]; } } if (empty($set)) { error_response('No Fields to update.'); } try { $stmt = $pdo->prepare("UPDATE news SET " . implode(', ', $set) . " WHERE news_id = :id"); $stmt->execute($params); json_response(['message' => 'News updated successfully.']); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // DELETE NEWS: Deletes existing News Entry // Required: news_id // --------------------------------------------------------------------- function handle_delete_news($input) { $pdo = get_db(); $missing = validate_required($input, ['news_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } try { $stmt = $pdo->prepare("DELETE FROM news WHERE news_id = :id"); $stmt->execute([':id' => $input['news_id']]); json_response(['message' => 'News deleted successfully.']); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // ===================================================================== // Action Handlers for Photos // ===================================================================== // --------------------------------------------------------------------- // PHOTO UPLOAD: Validates and Saves uploaded Photo Files // Returns relative Path on Success, null on Failure. // Allowed: JPG, PNG, GIF, WebP. with maximum Size of 5 MB. // --------------------------------------------------------------------- function handle_photo_upload($file) { // Validates File Size $max_size = 5 * 1024 * 1024; if ($file['size'] > $max_size) { return null; } // Validates MIME Type $allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = finfo_file($finfo, $file['tmp_name']); finfo_close($finfo); if (!in_array($mime, $allowed_types)) { return null; } // Generates unique Filename $ext = [ 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp' ][$mime]; $filename = uniqid('photo_', true) . '.' . $ext; $upload_dir = __DIR__ . '/../uploads/photos/'; $target_path = $upload_dir . $filename; // Creates Upload Directory if (!is_dir($upload_dir)) { mkdir($upload_dir, 0755, true); } // Moves uploaded File if (move_uploaded_file($file['tmp_name'], $target_path)) { return 'uploads/photos/' . $filename; } return null; } // ===================================================================== // Action Handlers for Comments // ===================================================================== // --------------------------------------------------------------------- // READ COMMENTS: Loads Comments for a Contribution // Returns Comments sorted by Date (newest first) // Required: contribution_id // --------------------------------------------------------------------- function handle_read_comments($input) { $pdo = get_db(); $missing = validate_required($input, ['contribution_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } try { $stmt = $pdo->prepare(" SELECT comment_id, contribution_id, author_name, browser_id, content, status, created_at FROM comments WHERE contribution_id = :cid AND status = 'approved' ORDER BY created_at ASC "); $stmt->execute([':cid' => $input['contribution_id']]); $comments = $stmt->fetchAll(); json_response(['comments' => $comments, 'count' => count($comments)]); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // CREATE COMMENT: Adds Comments to Contributions // Required: contribution_id, author_name, content // Optional: browser_id // --------------------------------------------------------------------- function handle_create_comment($input) { $pdo = get_db(); $missing = validate_required($input, ['contribution_id', 'author_name', 'content']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } // Validates Content Length if (strlen($input['content']) > 1000) { error_response('Comment too long. Maximum 1000 Characters.'); } // Checks if Contribution exists $stmt = $pdo->prepare("SELECT contribution_id FROM contributions WHERE contribution_id = :id"); $stmt->execute([':id' => $input['contribution_id']]); if (!$stmt->fetch()) { error_response('Contribution not found.', 404); } try { $stmt = $pdo->prepare(" INSERT INTO comments (contribution_id, author_name, browser_id, content) VALUES (:cid, :author, :bid, :content) "); $stmt->execute([ ':cid' => $input['contribution_id'], ':author' => $input['author_name'], ':bid' => $input['browser_id'] ?? null, ':content' => $input['content'] ]); json_response([ 'message' => 'Comment created successfully.', 'comment_id' => (int) $pdo->lastInsertId() ], 201); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // DELETE COMMENT: Removes a Comment // Required: comment_id // --------------------------------------------------------------------- function handle_delete_comment($input) { $pdo = get_db(); $missing = validate_required($input, ['comment_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } try { $stmt = $pdo->prepare("DELETE FROM comments WHERE comment_id = :id"); $stmt->execute([':id' => $input['comment_id']]); json_response(['message' => 'Comment deleted successfully.']); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } } // --------------------------------------------------------------------- // UPDATE COMMENT: Changes Comment Status or Content // Required: comment_id // Optional: status, content // --------------------------------------------------------------------- function handle_update_comment($input) { $pdo = get_db(); $missing = validate_required($input, ['comment_id']); if (!empty($missing)) { error_response('Missing Fields: ' . implode(', ', $missing)); } $set = []; $params = [':id' => $input['comment_id']]; // Updates Status if provided if (isset($input['status']) && $input['status'] !== '') { $valid = ['pending', 'approved', 'rejected']; if (!in_array($input['status'], $valid)) { error_response('Invalid Status.'); } $set[] = "status = :status"; $params[':status'] = $input['status']; } // Updates Content if provided if (isset($input['content']) && $input['content'] !== '') { $set[] = "content = :content"; $params[':content'] = $input['content']; } if (empty($set)) { error_response('No Fields to update.'); } try { $stmt = $pdo->prepare("UPDATE comments SET " . implode(', ', $set) . " WHERE comment_id = :id"); $stmt->execute($params); json_response(['message' => 'Comment updated successfully.']); } catch (PDOException $e) { error_response('Database Error: ' . $e->getMessage(), 500); } }