Online Poll System IN HTML CSS AND JAVASCRIPT WTIH PHP AND MY SQL

Introduction to the Project

The Online Poll System is a comprehensive, full-stack web application designed to create, manage, and participate in polls and surveys. This system provides a robust platform for organizations, businesses, educational institutions, and communities to gather opinions, make data-driven decisions, and engage with their audience effectively. With role-based access control, the application supports three user types: Admin, Poll Creators, and Voters.

The system features real-time result visualization, multiple question types, customizable poll settings, and detailed analytics. Whether you're conducting market research, gathering employee feedback, running student elections, or simply polling your social media followers, this poll system offers all the essential tools needed for effective data collection and analysis.

Key Features

Core Features

  • Poll Creation: Create polls with multiple question types
  • Question Types: Multiple choice, single choice, rating scales, text responses, ranking
  • Real-time Results: Live vote counting and result visualization
  • Poll Settings: Set start/end dates, voting limits, privacy options
  • User Authentication: Secure login and registration system
  • Role-Based Access: Different permissions for admins, creators, and voters
  • Responsive Design: Mobile-friendly interface for all devices

Advanced Features

  • Multiple Question Types:
  • Single choice (radio buttons)
  • Multiple choice (checkboxes)
  • Rating scale (1-5, 1-10, Likert scale)
  • Text response (short answer, long answer)
  • Ranking questions
  • Matrix questions
  • Image-based questions
  • Poll Customization:
  • Custom branding and themes
  • Logo and header images
  • Color schemes
  • Custom confirmation messages
  • Redirect URLs after voting
  • Voting Controls:
  • IP-based voting restrictions
  • Cookie-based vote tracking
  • User account requirements
  • Vote limits per user
  • Password-protected polls
  • Private/public polls
  • Results & Analytics:
  • Real-time charts and graphs
  • Export results (CSV, Excel, PDF)
  • Demographic breakdowns
  • Time-based analysis
  • Response rate tracking
  • Geographic distribution
  • Admin Features:
  • User management
  • Poll moderation
  • System settings
  • Audit logs
  • Spam detection
  • Backup and restore
  • Poll Creator Features:
  • Create unlimited polls
  • Manage polls (edit, close, delete)
  • View detailed results
  • Share poll links
  • Embed polls on websites
  • Email invitations
  • Voter Features:
  • Browse public polls
  • Vote anonymously or with account
  • View results after voting
  • Save favorite polls
  • Comment on polls (optional)
  • Share polls on social media

Technology Stack

  • Frontend: HTML5, CSS3, JavaScript (ES6+), Bootstrap 5
  • Backend: PHP 8.0+ (Core PHP with OOP approach)
  • Database: MySQL 5.7+
  • Additional Libraries:
  • Chart.js for data visualization
  • Bootstrap 5 for responsive UI
  • Font Awesome for icons
  • jQuery for AJAX operations
  • Select2 for enhanced dropdowns
  • DataTables for advanced tables
  • Moment.js for date handling
  • TinyMCE for rich text editing
  • PHPMailer for email notifications
  • PhpSpreadsheet for Excel export
  • TCPDF for PDF generation

Project File Structure

poll-system/
│
├── assets/
│   ├── css/
│   │   ├── style.css
│   │   ├── dashboard.css
│   │   ├── poll.css
│   │   ├── responsive.css
│   │   └── dark-mode.css
│   ├── js/
│   │   ├── main.js
│   │   ├── dashboard.js
│   │   ├── poll.js
│   │   ├── charts.js
│   │   ├── validation.js
│   │   └── tinymce-init.js
│   ├── images/
│   │   ├── polls/
│   │   ├── avatars/
│   │   └── icons/
│   └── plugins/
│       ├── chart.js/
│       ├── datatables/
│       ├── select2/
│       └── tinymce/
│
├── includes/
│   ├── config.php
│   ├── Database.php
│   ├── functions.php
│   ├── auth.php
│   ├── Poll.php
│   ├── Question.php
│   ├── Option.php
│   ├── Vote.php
│   ├── User.php
│   ├── Category.php
│   ├── Notification.php
│   └── helpers/
│       ├── ChartHelper.php
│       ├── ExportHelper.php
│       └── ValidationHelper.php
│
├── admin/
│   ├── dashboard.php
│   ├── manage_users.php
│   ├── user_details.php
│   ├── manage_polls.php
│   ├── poll_details.php
│   ├── manage_categories.php
│   ├── system_settings.php
│   ├── reports.php
│   ├── analytics.php
│   ├── audit_logs.php
│   └── backup.php
│
├── creator/
│   ├── dashboard.php
│   ├── create_poll.php
│   ├── edit_poll.php
│   ├── manage_polls.php
│   ├── poll_results.php
│   ├── poll_analytics.php
│   ├── add_questions.php
│   ├── edit_questions.php
│   ├── share_poll.php
│   ├── invitations.php
│   ├── profile.php
│   └── settings.php
│
├── voter/
│   ├── dashboard.php
│   ├── browse_polls.php
│   ├── poll_details.php
│   ├── vote.php
│   ├── results.php
│   ├── my_votes.php
│   ├── favorites.php
│   ├── profile.php
│   └── settings.php
│
├── api/
│   ├── get_poll.php
│   ├── submit_vote.php
│   ├── get_results.php
│   ├── get_chart_data.php
│   ├── search_polls.php
│   └── track_view.php
│
├── includes/
│   └── templates/
│       ├── email/
│       │   ├── invitation.php
│       │   ├── vote_confirmation.php
│       │   └── results_ready.php
│       └── embed/
│           └── poll_embed.php
│
├── uploads/
│   ├── poll_images/
│   ├── avatars/
│   └── exports/
│
├── vendor/
│
├── index.php
├── login.php
├── register.php
├── forgot_password.php
├── reset_password.php
├── logout.php
├── embed.php
├── .env
├── .gitignore
├── composer.json
└── sql/
└── database.sql

Database Schema

File: sql/database.sql

-- Create Database
CREATE DATABASE IF NOT EXISTS `poll_system`;
USE `poll_system`;
-- Users Table
CREATE TABLE `users` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) UNIQUE NOT NULL,
`email` VARCHAR(100) UNIQUE NOT NULL,
`password` VARCHAR(255) NOT NULL,
`first_name` VARCHAR(50) NOT NULL,
`last_name` VARCHAR(50) NOT NULL,
`display_name` VARCHAR(100),
`bio` TEXT,
`profile_picture` VARCHAR(255) DEFAULT 'default.png',
`role` ENUM('admin', 'creator', 'voter') DEFAULT 'voter',
`status` ENUM('active', 'inactive', 'suspended') DEFAULT 'active',
`email_verified` BOOLEAN DEFAULT FALSE,
`verification_token` VARCHAR(255),
`reset_token` VARCHAR(255),
`reset_expires` DATETIME,
`last_login` DATETIME,
`last_ip` VARCHAR(45),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_email` (`email`),
INDEX `idx_role` (`role`),
INDEX `idx_status` (`status`)
);
-- Poll Categories
CREATE TABLE `categories` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`slug` VARCHAR(100) UNIQUE NOT NULL,
`description` TEXT,
`icon` VARCHAR(50) DEFAULT 'fa-folder',
`color` VARCHAR(7) DEFAULT '#6c757d',
`parent_id` INT(11) DEFAULT NULL,
`is_active` BOOLEAN DEFAULT TRUE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`parent_id`) REFERENCES `categories`(`id`),
INDEX `idx_slug` (`slug`)
);
-- Polls Table
CREATE TABLE `polls` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`poll_id` VARCHAR(20) UNIQUE NOT NULL,
`creator_id` INT(11) NOT NULL,
`category_id` INT(11),
`title` VARCHAR(255) NOT NULL,
`slug` VARCHAR(255) UNIQUE NOT NULL,
`description` TEXT,
`featured_image` VARCHAR(255),
`theme_color` VARCHAR(7) DEFAULT '#007bff',
`logo` VARCHAR(255),
`welcome_message` TEXT,
`thank_you_message` TEXT DEFAULT 'Thank you for participating!',
`redirect_url` VARCHAR(255),
-- Poll Settings
`status` ENUM('draft', 'active', 'closed', 'archived') DEFAULT 'draft',
`visibility` ENUM('public', 'private', 'password') DEFAULT 'public',
`password` VARCHAR(255),
`require_login` BOOLEAN DEFAULT FALSE,
`allow_anonymous` BOOLEAN DEFAULT TRUE,
`allow_multiple_votes` BOOLEAN DEFAULT FALSE,
`max_votes_per_user` INT DEFAULT 1,
`voting_interval` INT DEFAULT 0, -- minutes between votes
-- Restrictions
`ip_restriction` BOOLEAN DEFAULT FALSE,
`cookie_restriction` BOOLEAN DEFAULT TRUE,
`country_restriction` TEXT, -- JSON array of country codes
`age_restriction` INT DEFAULT 0, -- minimum age
-- Dates
`start_date` DATETIME,
`end_date` DATETIME,
`timezone` VARCHAR(50) DEFAULT 'UTC',
-- Results Settings
`show_results` ENUM('after_vote', 'after_close', 'never') DEFAULT 'after_vote',
`show_real_time` BOOLEAN DEFAULT TRUE,
`hide_results_until` DATETIME,
-- Statistics
`total_votes` INT DEFAULT 0,
`unique_voters` INT DEFAULT 0,
`views` INT DEFAULT 0,
`completion_rate` DECIMAL(5,2) DEFAULT 0.00,
-- Metadata
`tags` TEXT,
`meta_title` VARCHAR(255),
`meta_description` TEXT,
`meta_keywords` TEXT,
-- Timestamps
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`published_at` DATETIME,
`closed_at` DATETIME,
PRIMARY KEY (`id`),
FOREIGN KEY (`creator_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`category_id`) REFERENCES `categories`(`id`) ON DELETE SET NULL,
INDEX `idx_poll_id` (`poll_id`),
INDEX `idx_slug` (`slug`),
INDEX `idx_status` (`status`),
INDEX `idx_dates` (`start_date`, `end_date`),
INDEX `idx_creator` (`creator_id`),
FULLTEXT INDEX `idx_search` (`title`, `description`, `tags`)
);
-- Questions Table
CREATE TABLE `questions` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`poll_id` INT(11) NOT NULL,
`question_text` TEXT NOT NULL,
`description` TEXT,
`question_type` ENUM(
'single_choice',
'multiple_choice',
'rating',
'likert_scale',
'text_short',
'text_long',
'number',
'date',
'ranking',
'matrix',
'image_choice',
'yes_no'
) NOT NULL DEFAULT 'single_choice',
`required` BOOLEAN DEFAULT TRUE,
`randomize_options` BOOLEAN DEFAULT FALSE,
`show_results` BOOLEAN DEFAULT TRUE,
`order_number` INT NOT NULL,
`settings` JSON, -- For question-specific settings (min, max, steps, etc.)
`image` VARCHAR(255),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`poll_id`) REFERENCES `polls`(`id`) ON DELETE CASCADE,
INDEX `idx_poll_order` (`poll_id`, `order_number`)
);
-- Question Options Table (for choice-based questions)
CREATE TABLE `question_options` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`question_id` INT(11) NOT NULL,
`option_text` VARCHAR(255) NOT NULL,
`option_value` VARCHAR(50),
`description` TEXT,
`image` VARCHAR(255),
`color` VARCHAR(7),
`order_number` INT NOT NULL,
`is_correct` BOOLEAN DEFAULT FALSE, -- For quiz-type polls
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`question_id`) REFERENCES `questions`(`id`) ON DELETE CASCADE,
INDEX `idx_question_order` (`question_id`, `order_number`)
);
-- Matrix Rows (for matrix questions)
CREATE TABLE `matrix_rows` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`question_id` INT(11) NOT NULL,
`row_text` VARCHAR(255) NOT NULL,
`order_number` INT NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`question_id`) REFERENCES `questions`(`id`) ON DELETE CASCADE
);
-- Matrix Columns (for matrix questions)
CREATE TABLE `matrix_columns` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`question_id` INT(11) NOT NULL,
`column_text` VARCHAR(255) NOT NULL,
`column_value` VARCHAR(50),
`order_number` INT NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`question_id`) REFERENCES `questions`(`id`) ON DELETE CASCADE
);
-- Votes Table
CREATE TABLE `votes` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`vote_id` VARCHAR(20) UNIQUE NOT NULL,
`poll_id` INT(11) NOT NULL,
`user_id` INT(11) DEFAULT NULL, -- NULL for anonymous votes
`session_id` VARCHAR(255), -- For anonymous tracking
`ip_address` VARCHAR(45),
`user_agent` TEXT,
`country_code` VARCHAR(2),
`city` VARCHAR(100),
`completed` BOOLEAN DEFAULT FALSE,
`time_taken` INT, -- seconds taken to complete
`started_at` DATETIME,
`submitted_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`poll_id`) REFERENCES `polls`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL,
INDEX `idx_poll` (`poll_id`),
INDEX `idx_user` (`user_id`),
INDEX `idx_session` (`session_id`),
INDEX `idx_ip` (`ip_address`),
INDEX `idx_submitted` (`submitted_at`)
);
-- Vote Answers Table
CREATE TABLE `vote_answers` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`vote_id` INT(11) NOT NULL,
`question_id` INT(11) NOT NULL,
`option_id` INT(11) DEFAULT NULL, -- For choice questions
`matrix_row_id` INT(11) DEFAULT NULL, -- For matrix questions
`matrix_column_id` INT(11) DEFAULT NULL, -- For matrix questions
`answer_text` TEXT, -- For text/number/date responses
`answer_number` DECIMAL(10,2), -- For rating/number responses
`answer_date` DATETIME, -- For date responses
`rank` INT, -- For ranking questions
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`vote_id`) REFERENCES `votes`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`question_id`) REFERENCES `questions`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`option_id`) REFERENCES `question_options`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`matrix_row_id`) REFERENCES `matrix_rows`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`matrix_column_id`) REFERENCES `matrix_columns`(`id`) ON DELETE CASCADE,
UNIQUE KEY `unique_vote_question` (`vote_id`, `question_id`)
);
-- Poll Comments Table
CREATE TABLE `comments` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`poll_id` INT(11) NOT NULL,
`user_id` INT(11),
`comment` TEXT NOT NULL,
`parent_id` INT(11) DEFAULT NULL,
`status` ENUM('pending', 'approved', 'spam', 'deleted') DEFAULT 'pending',
`ip_address` VARCHAR(45),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`poll_id`) REFERENCES `polls`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL,
FOREIGN KEY (`parent_id`) REFERENCES `comments`(`id`) ON DELETE CASCADE,
INDEX `idx_poll` (`poll_id`),
INDEX `idx_status` (`status`)
);
-- Favorites Table
CREATE TABLE `favorites` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`poll_id` INT(11) NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`poll_id`) REFERENCES `polls`(`id`) ON DELETE CASCADE,
UNIQUE KEY `unique_favorite` (`user_id`, `poll_id`)
);
-- Poll Invitations
CREATE TABLE `invitations` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`poll_id` INT(11) NOT NULL,
`email` VARCHAR(100) NOT NULL,
`token` VARCHAR(255) UNIQUE NOT NULL,
`sent_at` DATETIME,
`opened_at` DATETIME,
`voted_at` DATETIME,
`status` ENUM('pending', 'sent', 'opened', 'voted', 'expired') DEFAULT 'pending',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`poll_id`) REFERENCES `polls`(`id`) ON DELETE CASCADE,
INDEX `idx_email` (`email`),
INDEX `idx_token` (`token`)
);
-- Poll Views Tracking
CREATE TABLE `poll_views` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`poll_id` INT(11) NOT NULL,
`user_id` INT(11),
`session_id` VARCHAR(255),
`ip_address` VARCHAR(45),
`user_agent` TEXT,
`referrer` TEXT,
`viewed_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`poll_id`) REFERENCES `polls`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL,
INDEX `idx_poll` (`poll_id`),
INDEX `idx_viewed` (`viewed_at`)
);
-- Notifications Table
CREATE TABLE `notifications` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`type` ENUM('new_vote', 'poll_closed', 'comment', 'invitation', 'system') NOT NULL,
`title` VARCHAR(255) NOT NULL,
`message` TEXT NOT NULL,
`data` JSON,
`is_read` BOOLEAN DEFAULT FALSE,
`read_at` DATETIME,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
INDEX `idx_user_read` (`user_id`, `is_read`)
);
-- Audit Logs
CREATE TABLE `audit_logs` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11),
`action` VARCHAR(100) NOT NULL,
`description` TEXT,
`ip_address` VARCHAR(45),
`user_agent` TEXT,
`data` JSON,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL,
INDEX `idx_user` (`user_id`),
INDEX `idx_action` (`action`),
INDEX `idx_created` (`created_at`)
);
-- System Settings
CREATE TABLE `system_settings` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`setting_key` VARCHAR(100) UNIQUE NOT NULL,
`setting_value` TEXT,
`description` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
-- Insert Default Admin
INSERT INTO `users` (`username`, `email`, `password`, `first_name`, `last_name`, `role`, `email_verified`) 
VALUES ('admin', '[email protected]', '$2y$10$YourHashedPasswordHere', 'System', 'Administrator', 'admin', TRUE);
-- Insert Default Categories
INSERT INTO `categories` (`name`, `slug`, `description`, `icon`, `color`) VALUES
('General', 'general', 'General polls and surveys', 'fa-poll', '#007bff'),
('Business', 'business', 'Market research and business surveys', 'fa-briefcase', '#28a745'),
('Education', 'education', 'Educational polls and feedback', 'fa-graduation-cap', '#17a2b8'),
('Entertainment', 'entertainment', 'Fun polls and entertainment', 'fa-film', '#ffc107'),
('Health', 'health', 'Health and wellness surveys', 'fa-heartbeat', '#dc3545'),
('Politics', 'politics', 'Political polls and elections', 'fa-landmark', '#6f42c1'),
('Sports', 'sports', 'Sports-related polls', 'fa-football-ball', '#fd7e14'),
('Technology', 'technology', 'Tech surveys and feedback', 'fa-laptop', '#20c997'),
('Customer Feedback', 'customer-feedback', 'Customer satisfaction surveys', 'fa-smile', '#e83e8c'),
('Employee Feedback', 'employee-feedback', 'Employee engagement surveys', 'fa-users', '#6610f2');
-- Insert Default System Settings
INSERT INTO `system_settings` (`setting_key`, `setting_value`, `description`) VALUES
('site_name', 'Online Poll System', 'Name of the site'),
('site_description', 'Create and participate in polls', 'Site description'),
('site_keywords', 'polls, surveys, voting, feedback', 'SEO keywords'),
('site_email', '[email protected]', 'Contact email'),
('items_per_page', '20', 'Items per page in listings'),
('allow_registration', '1', 'Allow user registration'),
('require_email_verification', '1', 'Require email verification'),
('default_user_role', 'voter', 'Default role for new users'),
('allow_anonymous_voting', '1', 'Allow anonymous voting'),
('max_polls_per_user', '100', 'Maximum polls per user'),
('enable_comments', '1', 'Enable comments on polls'),
('comment_moderation', '1', 'Require comment moderation'),
('enable_captcha', '1', 'Enable CAPTCHA for forms'),
('timezone', 'America/New_York', 'Default timezone'),
('date_format', 'Y-m-d', 'Date format'),
('time_format', 'H:i', 'Time format'),
('recaptcha_site_key', '', 'Google reCAPTCHA site key'),
('recaptcha_secret_key', '', 'Google reCAPTCHA secret key'),
('mail_driver', 'smtp', 'Mail driver (smtp/sendmail)'),
('mail_host', 'smtp.gmail.com', 'SMTP host'),
('mail_port', '587', 'SMTP port'),
('mail_username', '', 'SMTP username'),
('mail_password', '', 'SMTP password'),
('mail_encryption', 'tls', 'SMTP encryption'),
('mail_from_address', '[email protected]', 'From address'),
('mail_from_name', 'Poll System', 'From name');

Core PHP Classes

Database Class

File: includes/Database.php

<?php
/**
* Database Class
* Handles all database connections and operations using PDO with singleton pattern
*/
class Database {
private static $instance = null;
private $connection;
private $statement;
private $host;
private $dbname;
private $username;
private $password;
/**
* Private constructor for singleton pattern
*/
private function __construct() {
$this->host = DB_HOST;
$this->dbname = DB_NAME;
$this->username = DB_USER;
$this->password = DB_PASS;
try {
$this->connection = new PDO(
"mysql:host={$this->host};dbname={$this->dbname};charset=utf8mb4",
$this->username,
$this->password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
} catch (PDOException $e) {
die("Database connection failed: " . $e->getMessage());
}
}
/**
* Get database instance (Singleton)
*/
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Prepare and execute query with parameters
*/
public function query($sql, $params = []) {
try {
$this->statement = $this->connection->prepare($sql);
$this->statement->execute($params);
return $this->statement;
} catch (PDOException $e) {
$this->logError($e->getMessage(), $sql, $params);
throw new Exception("Database query failed: " . $e->getMessage());
}
}
/**
* Get single row
*/
public function getRow($sql, $params = []) {
$result = $this->query($sql, $params);
return $result->fetch();
}
/**
* Get multiple rows
*/
public function getRows($sql, $params = []) {
$result = $this->query($sql, $params);
return $result->fetchAll();
}
/**
* Get single value
*/
public function getValue($sql, $params = []) {
$result = $this->query($sql, $params);
return $result->fetchColumn();
}
/**
* Insert data and return last insert ID
*/
public function insert($table, $data) {
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
$this->query($sql, $data);
return $this->connection->lastInsertId();
}
/**
* Update data
*/
public function update($table, $data, $where, $whereParams = []) {
$set = [];
foreach (array_keys($data) as $column) {
$set[] = "{$column} = :{$column}";
}
$sql = "UPDATE {$table} SET " . implode(', ', $set) . " WHERE {$where}";
$params = array_merge($data, $whereParams);
return $this->query($sql, $params)->rowCount();
}
/**
* Delete data
*/
public function delete($table, $where, $params = []) {
$sql = "DELETE FROM {$table} WHERE {$where}";
return $this->query($sql, $params)->rowCount();
}
/**
* Begin transaction
*/
public function beginTransaction() {
return $this->connection->beginTransaction();
}
/**
* Commit transaction
*/
public function commit() {
return $this->connection->commit();
}
/**
* Rollback transaction
*/
public function rollback() {
return $this->connection->rollBack();
}
/**
* Get last insert ID
*/
public function lastInsertId() {
return $this->connection->lastInsertId();
}
/**
* Check if table exists
*/
public function tableExists($table) {
$result = $this->getRow("SHOW TABLES LIKE ?", [$table]);
return !empty($result);
}
/**
* Get table columns
*/
public function getColumns($table) {
return $this->getRows("SHOW COLUMNS FROM {$table}");
}
/**
* Escape string
*/
public function escape($string) {
return $this->connection->quote($string);
}
/**
* Log database errors
*/
private function logError($message, $sql, $params) {
$logFile = __DIR__ . '/../logs/database.log';
$logDir = dirname($logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[{$timestamp}] Error: {$message}\n";
$logMessage .= "SQL: {$sql}\n";
$logMessage .= "Params: " . json_encode($params) . "\n";
$logMessage .= "------------------------\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
}
/**
* Prevent cloning of the instance
*/
private function __clone() {}
/**
* Prevent unserializing of the instance
*/
public function __wakeup() {}
}
?>

Configuration File

File: includes/config.php

<?php
/**
* Configuration File
* Loads environment variables and sets up constants
*/
// Start session if not started
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Load environment variables from .env file
function loadEnv($path) {
if (!file_exists($path)) {
return false;
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) {
continue;
}
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
if (!array_key_exists($name, $_ENV)) {
$_ENV[$name] = $value;
putenv(sprintf('%s=%s', $name, $value));
}
}
return true;
}
// Load environment variables
loadEnv(__DIR__ . '/../.env');
// Database Configuration
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
define('DB_NAME', getenv('DB_NAME') ?: 'poll_system');
define('DB_USER', getenv('DB_USER') ?: 'root');
define('DB_PASS', getenv('DB_PASS') ?: '');
// Application Configuration
define('SITE_NAME', getenv('SITE_NAME') ?: 'Online Poll System');
define('SITE_URL', getenv('SITE_URL') ?: 'http://localhost/poll-system');
define('SITE_EMAIL', getenv('SITE_EMAIL') ?: '[email protected]');
define('APP_VERSION', getenv('APP_VERSION') ?: '1.0.0');
define('DEBUG_MODE', getenv('DEBUG_MODE') === 'true');
// Security Configuration
define('SESSION_TIMEOUT', getenv('SESSION_TIMEOUT') ?: 3600); // 1 hour
define('BCRYPT_ROUNDS', 12);
define('CSRF_TOKEN_NAME', 'csrf_token');
define('PEPPER', getenv('PEPPER') ?: 'default-pepper-string-change-this');
// Upload Configuration
define('UPLOAD_DIR', __DIR__ . '/../uploads/');
define('MAX_FILE_SIZE', getenv('MAX_FILE_SIZE') ?: 5 * 1024 * 1024); // 5MB
define('ALLOWED_EXTENSIONS', ['jpg', 'jpeg', 'png', 'gif', 'svg']);
// Pagination
define('ITEMS_PER_PAGE', getenv('ITEMS_PER_PAGE') ?: 20);
// Date/Time Configuration
date_default_timezone_set(getenv('TIMEZONE') ?: 'America/New_York');
define('DATE_FORMAT', 'Y-m-d');
define('TIME_FORMAT', 'H:i');
define('DATETIME_FORMAT', 'Y-m-d H:i:s');
// reCAPTCHA Configuration
define('RECAPTCHA_SITE_KEY', getenv('RECAPTCHA_SITE_KEY') ?: '');
define('RECAPTCHA_SECRET_KEY', getenv('RECAPTCHA_SECRET_KEY') ?: '');
// Mail Configuration
define('MAIL_DRIVER', getenv('MAIL_DRIVER') ?: 'smtp');
define('MAIL_HOST', getenv('MAIL_HOST') ?: 'smtp.gmail.com');
define('MAIL_PORT', getenv('MAIL_PORT') ?: 587);
define('MAIL_USERNAME', getenv('MAIL_USERNAME') ?: '');
define('MAIL_PASSWORD', getenv('MAIL_PASSWORD') ?: '');
define('MAIL_ENCRYPTION', getenv('MAIL_ENCRYPTION') ?: 'tls');
define('MAIL_FROM_ADDRESS', getenv('MAIL_FROM_ADDRESS') ?: '[email protected]');
define('MAIL_FROM_NAME', getenv('MAIL_FROM_NAME') ?: SITE_NAME);
// Error Reporting
if (DEBUG_MODE) {
error_reporting(E_ALL);
ini_set('display_errors', 1);
} else {
error_reporting(0);
ini_set('display_errors', 0);
}
// Include required files
require_once __DIR__ . '/Database.php';
require_once __DIR__ . '/functions.php';
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/Poll.php';
require_once __DIR__ . '/Question.php';
require_once __DIR__ . '/Vote.php';
require_once __DIR__ . '/User.php';
require_once __DIR__ . '/Category.php';
// Initialize database connection
$db = Database::getInstance();
// Load system settings
$settings = $db->getRows("SELECT setting_key, setting_value FROM system_settings");
foreach ($settings as $setting) {
define(strtoupper($setting['setting_key']), $setting['setting_value']);
}
// Set timezone for MySQL
$db->query("SET time_zone = ?", [date('P')]);
// Initialize session with timeout
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > SESSION_TIMEOUT)) {
session_unset();
session_destroy();
}
$_SESSION['last_activity'] = time();
?>

Helper Functions

File: includes/functions.php

<?php
/**
* Helper Functions
* Common utility functions used throughout the application
*/
/**
* Sanitize input data
*/
function sanitize($input) {
if (is_array($input)) {
return array_map('sanitize', $input);
}
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
}
/**
* Generate CSRF token
*/
function generateCSRFToken() {
if (!isset($_SESSION[CSRF_TOKEN_NAME])) {
$_SESSION[CSRF_TOKEN_NAME] = bin2hex(random_bytes(32));
}
return $_SESSION[CSRF_TOKEN_NAME];
}
/**
* Verify CSRF token
*/
function verifyCSRFToken($token) {
if (!isset($_SESSION[CSRF_TOKEN_NAME]) || $token !== $_SESSION[CSRF_TOKEN_NAME]) {
return false;
}
return true;
}
/**
* Redirect to URL
*/
function redirect($url) {
header("Location: " . SITE_URL . $url);
exit();
}
/**
* Generate slug from string
*/
function createSlug($string) {
$string = preg_replace('/[^a-zA-Z0-9\s]/', '', $string);
$string = strtolower(trim($string));
$string = preg_replace('/\s+/', '-', $string);
return $string;
}
/**
* Generate unique poll ID
*/
function generatePollId() {
return 'POLL' . date('Y') . strtoupper(uniqid());
}
/**
* Generate unique vote ID
*/
function generateVoteId() {
return 'VOTE' . date('Ymd') . strtoupper(uniqid());
}
/**
* Get user IP address
*/
function getUserIP() {
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
return $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
return $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
return $_SERVER['REMOTE_ADDR'];
}
}
/**
* Get session ID for anonymous tracking
*/
function getSessionId() {
if (!isset($_SESSION['session_id'])) {
$_SESSION['session_id'] = bin2hex(random_bytes(16));
}
return $_SESSION['session_id'];
}
/**
* Check if user has already voted on a poll
*/
function hasVoted($pollId, $userId = null) {
$db = Database::getInstance();
if ($userId) {
// Check by user ID
$count = $db->getValue(
"SELECT COUNT(*) FROM votes WHERE poll_id = ? AND user_id = ?",
[$pollId, $userId]
);
return $count > 0;
} else {
// Check by session or IP
$sessionId = getSessionId();
$ip = getUserIP();
$count = $db->getValue(
"SELECT COUNT(*) FROM votes WHERE poll_id = ? AND (session_id = ? OR ip_address = ?)",
[$pollId, $sessionId, $ip]
);
return $count > 0;
}
}
/**
* Format date
*/
function formatDate($date, $format = null) {
if ($format === null) {
$format = DATE_FORMAT;
}
if ($date instanceof DateTime) {
return $date->format($format);
}
return date($format, strtotime($date));
}
/**
* Format time
*/
function formatTime($time, $format = null) {
if ($format === null) {
$format = TIME_FORMAT;
}
return date($format, strtotime($time));
}
/**
* Get time ago string
*/
function timeAgo($datetime) {
$time = strtotime($datetime);
$now = time();
$diff = $now - $time;
if ($diff < 60) {
return $diff . ' seconds ago';
} elseif ($diff < 3600) {
return floor($diff / 60) . ' minutes ago';
} elseif ($diff < 86400) {
return floor($diff / 3600) . ' hours ago';
} elseif ($diff < 2592000) {
return floor($diff / 86400) . ' days ago';
} elseif ($diff < 31536000) {
return floor($diff / 2592000) . ' months ago';
} else {
return floor($diff / 31536000) . ' years ago';
}
}
/**
* Get poll status
*/
function getPollStatus($poll) {
$now = time();
$start = strtotime($poll['start_date']);
$end = strtotime($poll['end_date']);
if ($poll['status'] === 'draft') {
return ['status' => 'draft', 'label' => 'Draft', 'class' => 'secondary'];
} elseif ($poll['status'] === 'archived') {
return ['status' => 'archived', 'label' => 'Archived', 'class' => 'dark'];
} elseif ($poll['status'] === 'closed') {
return ['status' => 'closed', 'label' => 'Closed', 'class' => 'danger'];
} elseif ($now < $start) {
return ['status' => 'scheduled', 'label' => 'Scheduled', 'class' => 'info'];
} elseif ($now > $end) {
return ['status' => 'ended', 'label' => 'Ended', 'class' => 'warning'];
} else {
return ['status' => 'active', 'label' => 'Active', 'class' => 'success'];
}
}
/**
* Upload file
*/
function uploadFile($file, $targetDir, $allowedTypes = null) {
if ($allowedTypes === null) {
$allowedTypes = ALLOWED_EXTENSIONS;
}
// Check for errors
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'error' => 'Upload failed with error code: ' . $file['error']];
}
// Check file size
if ($file['size'] > MAX_FILE_SIZE) {
return ['success' => false, 'error' => 'File size exceeds limit'];
}
// Check file type
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $allowedTypes)) {
return ['success' => false, 'error' => 'File type not allowed'];
}
// Generate unique filename
$filename = uniqid() . '_' . time() . '.' . $extension;
$targetPath = $targetDir . '/' . $filename;
// Create directory if not exists
if (!is_dir($targetDir)) {
mkdir($targetDir, 0755, true);
}
// Upload file
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
return [
'success' => true,
'filename' => $filename,
'original_name' => $file['name'],
'path' => $targetPath,
'url' => SITE_URL . '/uploads/' . basename($targetDir) . '/' . $filename
];
}
return ['success' => false, 'error' => 'Failed to move uploaded file'];
}
/**
* Delete file
*/
function deleteFile($path) {
if (file_exists($path)) {
return unlink($path);
}
return false;
}
/**
* Generate random string
*/
function generateRandomString($length = 32) {
return bin2hex(random_bytes($length / 2));
}
/**
* Validate email
*/
function validateEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL);
}
/**
* Validate URL
*/
function validateURL($url) {
return filter_var($url, FILTER_VALIDATE_URL);
}
/**
* Truncate text
*/
function truncateText($text, $length = 100, $suffix = '...') {
if (strlen($text) <= $length) {
return $text;
}
return substr($text, 0, $length) . $suffix;
}
/**
* Get percentage
*/
function getPercentage($value, $total) {
if ($total == 0) {
return 0;
}
return round(($value / $total) * 100, 2);
}
/**
* Get random color
*/
function getRandomColor($index = null) {
$colors = [
'#007bff', '#28a745', '#17a2b8', '#ffc107', '#dc3545',
'#6f42c1', '#fd7e14', '#20c997', '#e83e8c', '#6610f2',
'#39cccc', '#ff4136', '#7c4dff', '#ff851b', '#b10dc9'
];
if ($index !== null) {
return $colors[$index % count($colors)];
}
return $colors[array_rand($colors)];
}
/**
* Send email
*/
function sendEmail($to, $subject, $template, $data = []) {
// Load email template
$templateFile = __DIR__ . "/templates/email/{$template}.php";
if (!file_exists($templateFile)) {
logError("Email template not found: {$template}");
return false;
}
// Extract data for template
extract($data);
ob_start();
include $templateFile;
$message = ob_get_clean();
// Headers
$headers = [
'MIME-Version: 1.0',
'Content-type: text/html; charset=utf-8',
'From: ' . MAIL_FROM_NAME . ' <' . MAIL_FROM_ADDRESS . '>',
'Reply-To: ' . MAIL_FROM_ADDRESS,
'X-Mailer: PHP/' . phpversion()
];
return mail($to, $subject, $message, implode("\r\n", $headers));
}
/**
* Log error
*/
function logError($message, $context = []) {
$logFile = __DIR__ . '/../logs/error.log';
$logDir = dirname($logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$timestamp = date('Y-m-d H:i:s');
$contextStr = !empty($context) ? ' ' . json_encode($context) : '';
$logMessage = "[{$timestamp}] {$message}{$contextStr}\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
}
/**
* Log audit trail
*/
function logAudit($userId, $action, $description = '', $data = []) {
$db = Database::getInstance();
$db->insert('audit_logs', [
'user_id' => $userId,
'action' => $action,
'description' => $description,
'ip_address' => getUserIP(),
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'data' => json_encode($data)
]);
}
/**
* Generate pagination
*/
function paginate($currentPage, $totalPages, $url) {
if ($totalPages <= 1) {
return '';
}
$html = '<nav aria-label="Page navigation"><ul class="pagination justify-content-center">';
// Previous button
if ($currentPage > 1) {
$html .= '<li class="page-item"><a class="page-link" href="' . $url . '&page=' . ($currentPage - 1) . '">Previous</a></li>';
} else {
$html .= '<li class="page-item disabled"><span class="page-link">Previous</span></li>';
}
// Page numbers
for ($i = 1; $i <= $totalPages; $i++) {
if ($i == $currentPage) {
$html .= '<li class="page-item active"><span class="page-link">' . $i . '</span></li>';
} else {
$html .= '<li class="page-item"><a class="page-link" href="' . $url . '&page=' . $i . '">' . $i . '</a></li>';
}
}
// Next button
if ($currentPage < $totalPages) {
$html .= '<li class="page-item"><a class="page-link" href="' . $url . '&page=' . ($currentPage + 1) . '">Next</a></li>';
} else {
$html .= '<li class="page-item disabled"><span class="page-link">Next</span></li>';
}
$html .= '</ul></nav>';
return $html;
}
/**
* Verify reCAPTCHA
*/
function verifyRecaptcha($response) {
if (empty(RECAPTCHA_SECRET_KEY)) {
return true; // Skip if not configured
}
$url = 'https://www.google.com/recaptcha/api/siteverify';
$data = [
'secret' => RECAPTCHA_SECRET_KEY,
'response' => $response,
'remoteip' => getUserIP()
];
$options = [
'http' => [
'header'  => "Content-type: application/x-www-form-urlencoded\r\n",
'method'  => 'POST',
'content' => http_build_query($data)
]
];
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
$result = json_decode($result, true);
return $result['success'] ?? false;
}
/**
* Get country from IP
*/
function getCountryFromIP($ip = null) {
if ($ip === null) {
$ip = getUserIP();
}
// Skip for localhost
if ($ip === '127.0.0.1' || $ip === '::1') {
return 'US';
}
// Using free IP geolocation API
$url = "http://ip-api.com/json/{$ip}?fields=countryCode";
$result = @file_get_contents($url);
if ($result) {
$data = json_decode($result, true);
return $data['countryCode'] ?? '';
}
return '';
}
/**
* Get browser language
*/
function getBrowserLanguage() {
$langs = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en';
$lang = substr($langs, 0, 2);
return $lang;
}
/**
* Check if poll is accessible
*/
function canAccessPoll($poll) {
if ($poll['visibility'] === 'private') {
return false;
}
if ($poll['visibility'] === 'password') {
return isset($_SESSION['poll_access'][$poll['id']]);
}
return true;
}
/**
* Set poll access (for password-protected polls)
*/
function setPollAccess($pollId) {
$_SESSION['poll_access'][$pollId] = true;
}
/**
* Clear poll access
*/
function clearPollAccess($pollId) {
unset($_SESSION['poll_access'][$pollId]);
}
?>

Authentication Class

File: includes/auth.php

<?php
/**
* Authentication Class
* Handles user authentication, registration, and session management
*/
class Auth {
private $db;
/**
* Constructor
*/
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Register new user
*/
public function register($data) {
try {
// Check if username or email already exists
$existing = $this->db->getRow(
"SELECT id FROM users WHERE username = ? OR email = ?",
[$data['username'], $data['email']]
);
if ($existing) {
return ['success' => false, 'error' => 'Username or email already exists'];
}
// Hash password with pepper
$pepperedPassword = $data['password'] . PEPPER;
$hashedPassword = password_hash($pepperedPassword, PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
// Generate verification token
$verificationToken = generateRandomString();
// Insert user
$userId = $this->db->insert('users', [
'username' => $data['username'],
'email' => $data['email'],
'password' => $hashedPassword,
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'display_name' => $data['display_name'] ?? $data['first_name'] . ' ' . $data['last_name'],
'role' => $data['role'] ?? DEFAULT_USER_ROLE,
'verification_token' => $verificationToken,
'last_ip' => getUserIP()
]);
if ($userId) {
// Send verification email
if (REQUIRE_EMAIL_VERIFICATION) {
$this->sendVerificationEmail($data['email'], $verificationToken);
} else {
// Auto-verify
$this->db->update('users', ['email_verified' => true], 'id = :id', ['id' => $userId]);
}
logAudit($userId, 'register', 'User registered');
return [
'success' => true,
'user_id' => $userId,
'message' => REQUIRE_EMAIL_VERIFICATION ? 
'Registration successful. Please check your email to verify your account.' :
'Registration successful. You can now login.'
];
}
return ['success' => false, 'error' => 'Registration failed'];
} catch (Exception $e) {
logError('Registration error: ' . $e->getMessage(), $data);
return ['success' => false, 'error' => 'Registration failed: ' . $e->getMessage()];
}
}
/**
* Login user
*/
public function login($username, $password, $remember = false) {
try {
// Get user
$user = $this->db->getRow(
"SELECT * FROM users WHERE (username = ? OR email = ?) AND status = 'active'",
[$username, $username]
);
if (!$user) {
return ['success' => false, 'error' => 'Invalid username or password'];
}
// Check if email verified
if (REQUIRE_EMAIL_VERIFICATION && !$user['email_verified']) {
return ['success' => false, 'error' => 'Please verify your email before logging in'];
}
// Verify password with pepper
$pepperedPassword = $password . PEPPER;
if (!password_verify($pepperedPassword, $user['password'])) {
return ['success' => false, 'error' => 'Invalid username or password'];
}
// Check if password needs rehash
if (password_needs_rehash($user['password'], PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS])) {
$newHash = password_hash($pepperedPassword, PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
$this->db->update('users', ['password' => $newHash], 'id = :id', ['id' => $user['id']]);
}
// Set session
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['user_role'] = $user['role'];
$_SESSION['user_name'] = $user['display_name'] ?? $user['first_name'] . ' ' . $user['last_name'];
$_SESSION['logged_in'] = true;
$_SESSION['login_time'] = time();
// Update last login
$this->db->update(
'users',
[
'last_login' => date('Y-m-d H:i:s'),
'last_ip' => getUserIP()
],
'id = :id',
['id' => $user['id']]
);
// Set remember me cookie
if ($remember) {
$this->setRememberMe($user['id']);
}
logAudit($user['id'], 'login', 'User logged in');
return ['success' => true, 'user' => $user];
} catch (Exception $e) {
logError('Login error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Login failed'];
}
}
/**
* Set remember me cookie
*/
private function setRememberMe($userId) {
$token = generateRandomString(64);
$expires = time() + (86400 * 30); // 30 days
// Store token in database (you would need a remember_tokens table)
// For now, just set cookie
setcookie('remember_token', $token, $expires, '/', '', false, true);
}
/**
* Logout user
*/
public function logout() {
if (isset($_SESSION['user_id'])) {
logAudit($_SESSION['user_id'], 'logout', 'User logged out');
}
// Clear session
$_SESSION = array();
// Clear session cookie
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
// Clear remember me cookie
setcookie('remember_token', '', time() - 3600, '/');
// Destroy session
session_destroy();
}
/**
* Check if user is logged in
*/
public function isLoggedIn() {
return isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true;
}
/**
* Get current user
*/
public function getCurrentUser() {
if (!$this->isLoggedIn()) {
return null;
}
return $this->db->getRow(
"SELECT * FROM users WHERE id = ?",
[$_SESSION['user_id']]
);
}
/**
* Check if user has role
*/
public function hasRole($role) {
if (!$this->isLoggedIn()) {
return false;
}
if (is_array($role)) {
return in_array($_SESSION['user_role'], $role);
}
return $_SESSION['user_role'] === $role;
}
/**
* Require login
*/
public function requireLogin() {
if (!$this->isLoggedIn()) {
$_SESSION['error'] = 'Please login to access this page';
redirect('/login.php');
}
}
/**
* Require role
*/
public function requireRole($role) {
$this->requireLogin();
if (!$this->hasRole($role)) {
$_SESSION['error'] = 'You do not have permission to access this page';
// Redirect based on role
if ($this->hasRole('admin')) {
redirect('/admin/dashboard.php');
} elseif ($this->hasRole('creator')) {
redirect('/creator/dashboard.php');
} else {
redirect('/voter/dashboard.php');
}
}
}
/**
* Verify email
*/
public function verifyEmail($token) {
$user = $this->db->getRow(
"SELECT id FROM users WHERE verification_token = ?",
[$token]
);
if ($user) {
$this->db->update(
'users',
['email_verified' => true, 'verification_token' => null],
'id = :id',
['id' => $user['id']]
);
logAudit($user['id'], 'verify_email', 'Email verified');
return true;
}
return false;
}
/**
* Send verification email
*/
private function sendVerificationEmail($email, $token) {
$subject = "Verify your email - " . SITE_NAME;
$data = [
'verification_link' => SITE_URL . "/verify.php?token=" . $token
];
return sendEmail($email, $subject, 'verification', $data);
}
/**
* Forgot password
*/
public function forgotPassword($email) {
$user = $this->db->getRow(
"SELECT id FROM users WHERE email = ?",
[$email]
);
if ($user) {
$token = generateRandomString();
$expires = date('Y-m-d H:i:s', strtotime('+1 hour'));
$this->db->update(
'users',
['reset_token' => $token, 'reset_expires' => $expires],
'id = :id',
['id' => $user['id']]
);
// Send reset email
$subject = "Password Reset - " . SITE_NAME;
$data = [
'reset_link' => SITE_URL . "/reset_password.php?token=" . $token
];
return sendEmail($email, $subject, 'password_reset', $data);
}
return false;
}
/**
* Reset password
*/
public function resetPassword($token, $password) {
$user = $this->db->getRow(
"SELECT id FROM users WHERE reset_token = ? AND reset_expires > NOW()",
[$token]
);
if ($user) {
$pepperedPassword = $password . PEPPER;
$hashedPassword = password_hash($pepperedPassword, PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
$this->db->update(
'users',
['password' => $hashedPassword, 'reset_token' => null, 'reset_expires' => null],
'id = :id',
['id' => $user['id']]
);
logAudit($user['id'], 'reset_password', 'Password reset');
return true;
}
return false;
}
/**
* Update user profile
*/
public function updateProfile($userId, $data) {
try {
$updateData = [
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'display_name' => $data['display_name'],
'bio' => $data['bio'] ?? null
];
// Handle profile picture upload
if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] == 0) {
$uploadDir = UPLOAD_DIR . 'avatars/';
$result = uploadFile($_FILES['profile_picture'], $uploadDir);
if ($result['success']) {
// Delete old picture
$user = $this->getCurrentUser();
if ($user['profile_picture'] !== 'default.png') {
deleteFile(UPLOAD_DIR . 'avatars/' . $user['profile_picture']);
}
$updateData['profile_picture'] = $result['filename'];
}
}
$this->db->update('users', $updateData, 'id = :id', ['id' => $userId]);
logAudit($userId, 'update_profile', 'Profile updated');
return ['success' => true, 'message' => 'Profile updated successfully'];
} catch (Exception $e) {
logError('Profile update error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to update profile'];
}
}
/**
* Change password
*/
public function changePassword($userId, $currentPassword, $newPassword) {
$user = $this->db->getRow("SELECT password FROM users WHERE id = ?", [$userId]);
$pepperedCurrent = $currentPassword . PEPPER;
if (!password_verify($pepperedCurrent, $user['password'])) {
return ['success' => false, 'error' => 'Current password is incorrect'];
}
$pepperedNew = $newPassword . PEPPER;
$hashedPassword = password_hash($pepperedNew, PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
$this->db->update('users', ['password' => $hashedPassword], 'id = :id', ['id' => $userId]);
logAudit($userId, 'change_password', 'Password changed');
return ['success' => true, 'message' => 'Password changed successfully'];
}
/**
* Get user by ID
*/
public function getUserById($userId) {
return $this->db->getRow("SELECT * FROM users WHERE id = ?", [$userId]);
}
/**
* Get user by email
*/
public function getUserByEmail($email) {
return $this->db->getRow("SELECT * FROM users WHERE email = ?", [$email]);
}
/**
* Get user by username
*/
public function getUserByUsername($username) {
return $this->db->getRow("SELECT * FROM users WHERE username = ?", [$username]);
}
}
// Initialize Auth
$auth = new Auth();
?>

Poll Class

File: includes/Poll.php

<?php
/**
* Poll Class
* Handles all poll-related operations
*/
class Poll {
private $db;
/**
* Constructor
*/
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Create a new poll
*/
public function create($creatorId, $data) {
try {
$this->db->beginTransaction();
// Generate unique poll ID and slug
$pollId = generatePollId();
$slug = createSlug($data['title']);
// Ensure slug is unique
$counter = 1;
$originalSlug = $slug;
while ($this->db->getValue("SELECT id FROM polls WHERE slug = ?", [$slug])) {
$slug = $originalSlug . '-' . $counter;
$counter++;
}
// Prepare poll data
$pollData = [
'poll_id' => $pollId,
'creator_id' => $creatorId,
'category_id' => $data['category_id'] ?? null,
'title' => $data['title'],
'slug' => $slug,
'description' => $data['description'] ?? null,
'theme_color' => $data['theme_color'] ?? '#007bff',
'welcome_message' => $data['welcome_message'] ?? null,
'thank_you_message' => $data['thank_you_message'] ?? 'Thank you for participating!',
'redirect_url' => $data['redirect_url'] ?? null,
// Poll Settings
'status' => $data['status'] ?? 'draft',
'visibility' => $data['visibility'] ?? 'public',
'password' => !empty($data['password']) ? password_hash($data['password'], PASSWORD_DEFAULT) : null,
'require_login' => $data['require_login'] ?? false,
'allow_anonymous' => $data['allow_anonymous'] ?? true,
'allow_multiple_votes' => $data['allow_multiple_votes'] ?? false,
'max_votes_per_user' => $data['max_votes_per_user'] ?? 1,
'voting_interval' => $data['voting_interval'] ?? 0,
// Restrictions
'ip_restriction' => $data['ip_restriction'] ?? false,
'cookie_restriction' => $data['cookie_restriction'] ?? true,
// Dates
'start_date' => $data['start_date'] ?? null,
'end_date' => $data['end_date'] ?? null,
'timezone' => $data['timezone'] ?? 'UTC',
// Results Settings
'show_results' => $data['show_results'] ?? 'after_vote',
'show_real_time' => $data['show_real_time'] ?? true,
// Metadata
'tags' => $data['tags'] ?? null,
'meta_title' => $data['meta_title'] ?? $data['title'],
'meta_description' => $data['meta_description'] ?? substr($data['description'] ?? '', 0, 160),
'meta_keywords' => $data['meta_keywords'] ?? null
];
// Handle featured image upload
if (isset($_FILES['featured_image']) && $_FILES['featured_image']['error'] == 0) {
$uploadDir = UPLOAD_DIR . 'poll_images/';
$result = uploadFile($_FILES['featured_image'], $uploadDir);
if ($result['success']) {
$pollData['featured_image'] = $result['filename'];
}
}
// Handle logo upload
if (isset($_FILES['logo']) && $_FILES['logo']['error'] == 0) {
$uploadDir = UPLOAD_DIR . 'poll_images/';
$result = uploadFile($_FILES['logo'], $uploadDir);
if ($result['success']) {
$pollData['logo'] = $result['filename'];
}
}
// Insert poll
$pollDbId = $this->db->insert('polls', $pollData);
// Set published_at if status is active
if ($data['status'] === 'active') {
$this->db->update('polls', ['published_at' => date('Y-m-d H:i:s')], 'id = :id', ['id' => $pollDbId]);
}
$this->db->commit();
logAudit($creatorId, 'create_poll', 'Created new poll', ['poll_id' => $pollDbId]);
return [
'success' => true,
'poll_id' => $pollDbId,
'poll_code' => $pollId,
'slug' => $slug,
'message' => 'Poll created successfully'
];
} catch (Exception $e) {
$this->db->rollback();
logError('Create poll error: ' . $e->getMessage(), $data);
return ['success' => false, 'error' => 'Failed to create poll: ' . $e->getMessage()];
}
}
/**
* Update poll
*/
public function update($pollId, $data) {
try {
$poll = $this->getPollById($pollId);
if (!$poll) {
return ['success' => false, 'error' => 'Poll not found'];
}
// Prepare update data
$updateData = [
'category_id' => $data['category_id'] ?? $poll['category_id'],
'title' => $data['title'] ?? $poll['title'],
'description' => $data['description'] ?? $poll['description'],
'theme_color' => $data['theme_color'] ?? $poll['theme_color'],
'welcome_message' => $data['welcome_message'] ?? $poll['welcome_message'],
'thank_you_message' => $data['thank_you_message'] ?? $poll['thank_you_message'],
'redirect_url' => $data['redirect_url'] ?? $poll['redirect_url'],
// Poll Settings
'visibility' => $data['visibility'] ?? $poll['visibility'],
'require_login' => $data['require_login'] ?? $poll['require_login'],
'allow_anonymous' => $data['allow_anonymous'] ?? $poll['allow_anonymous'],
'allow_multiple_votes' => $data['allow_multiple_votes'] ?? $poll['allow_multiple_votes'],
'max_votes_per_user' => $data['max_votes_per_user'] ?? $poll['max_votes_per_user'],
'voting_interval' => $data['voting_interval'] ?? $poll['voting_interval'],
// Restrictions
'ip_restriction' => $data['ip_restriction'] ?? $poll['ip_restriction'],
'cookie_restriction' => $data['cookie_restriction'] ?? $poll['cookie_restriction'],
// Dates
'start_date' => $data['start_date'] ?? $poll['start_date'],
'end_date' => $data['end_date'] ?? $poll['end_date'],
'timezone' => $data['timezone'] ?? $poll['timezone'],
// Results Settings
'show_results' => $data['show_results'] ?? $poll['show_results'],
'show_real_time' => $data['show_real_time'] ?? $poll['show_real_time'],
// Metadata
'tags' => $data['tags'] ?? $poll['tags'],
'meta_title' => $data['meta_title'] ?? $poll['meta_title'],
'meta_description' => $data['meta_description'] ?? $poll['meta_description'],
'meta_keywords' => $data['meta_keywords'] ?? $poll['meta_keywords']
];
// Update password if provided
if (!empty($data['password'])) {
$updateData['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
}
// Handle featured image upload
if (isset($_FILES['featured_image']) && $_FILES['featured_image']['error'] == 0) {
$uploadDir = UPLOAD_DIR . 'poll_images/';
$result = uploadFile($_FILES['featured_image'], $uploadDir);
if ($result['success']) {
// Delete old image
if ($poll['featured_image']) {
deleteFile(UPLOAD_DIR . 'poll_images/' . $poll['featured_image']);
}
$updateData['featured_image'] = $result['filename'];
}
}
// Handle logo upload
if (isset($_FILES['logo']) && $_FILES['logo']['error'] == 0) {
$uploadDir = UPLOAD_DIR . 'poll_images/';
$result = uploadFile($_FILES['logo'], $uploadDir);
if ($result['success']) {
// Delete old logo
if ($poll['logo']) {
deleteFile(UPLOAD_DIR . 'poll_images/' . $poll['logo']);
}
$updateData['logo'] = $result['filename'];
}
}
$this->db->update('polls', $updateData, 'id = :id', ['id' => $pollId]);
logAudit($poll['creator_id'], 'update_poll', 'Updated poll', ['poll_id' => $pollId]);
return ['success' => true, 'message' => 'Poll updated successfully'];
} catch (Exception $e) {
logError('Update poll error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to update poll'];
}
}
/**
* Delete poll
*/
public function delete($pollId) {
try {
$poll = $this->getPollById($pollId);
if (!$poll) {
return ['success' => false, 'error' => 'Poll not found'];
}
// Delete images
if ($poll['featured_image']) {
deleteFile(UPLOAD_DIR . 'poll_images/' . $poll['featured_image']);
}
if ($poll['logo']) {
deleteFile(UPLOAD_DIR . 'poll_images/' . $poll['logo']);
}
// Delete poll (cascade will delete related data)
$this->db->delete('polls', 'id = :id', ['id' => $pollId]);
logAudit($poll['creator_id'], 'delete_poll', 'Deleted poll', ['poll_id' => $pollId]);
return ['success' => true, 'message' => 'Poll deleted successfully'];
} catch (Exception $e) {
logError('Delete poll error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to delete poll'];
}
}
/**
* Get poll by ID
*/
public function getPollById($pollId) {
return $this->db->getRow(
"SELECT p.*, u.username as creator_username, u.display_name as creator_name,
c.name as category_name, c.color as category_color
FROM polls p
LEFT JOIN users u ON p.creator_id = u.id
LEFT JOIN categories c ON p.category_id = c.id
WHERE p.id = ?",
[$pollId]
);
}
/**
* Get poll by slug
*/
public function getPollBySlug($slug) {
return $this->db->getRow(
"SELECT p.*, u.username as creator_username, u.display_name as creator_name,
c.name as category_name, c.color as category_color
FROM polls p
LEFT JOIN users u ON p.creator_id = u.id
LEFT JOIN categories c ON p.category_id = c.id
WHERE p.slug = ?",
[$slug]
);
}
/**
* Get poll by poll code
*/
public function getPollByCode($pollCode) {
return $this->db->getRow(
"SELECT p.*, u.username as creator_username, u.display_name as creator_name,
c.name as category_name, c.color as category_color
FROM polls p
LEFT JOIN users u ON p.creator_id = u.id
LEFT JOIN categories c ON p.category_id = c.id
WHERE p.poll_id = ?",
[$pollCode]
);
}
/**
* Get polls by creator
*/
public function getPollsByCreator($creatorId, $status = null, $limit = null) {
$sql = "SELECT p.*, 
(SELECT COUNT(*) FROM votes WHERE poll_id = p.id) as vote_count,
(SELECT COUNT(*) FROM questions WHERE poll_id = p.id) as question_count
FROM polls p
WHERE p.creator_id = :creator_id";
$params = ['creator_id' => $creatorId];
if ($status) {
$sql .= " AND p.status = :status";
$params['status'] = $status;
}
$sql .= " ORDER BY p.created_at DESC";
if ($limit) {
$sql .= " LIMIT :limit";
$params['limit'] = $limit;
}
return $this->db->getRows($sql, $params);
}
/**
* Get public polls
*/
public function getPublicPolls($filters = []) {
$sql = "SELECT p.*, u.display_name as creator_name,
(SELECT COUNT(*) FROM votes WHERE poll_id = p.id) as vote_count,
(SELECT COUNT(*) FROM questions WHERE poll_id = p.id) as question_count
FROM polls p
JOIN users u ON p.creator_id = u.id
WHERE p.visibility = 'public' 
AND p.status = 'active'
AND (p.start_date IS NULL OR p.start_date <= NOW())
AND (p.end_date IS NULL OR p.end_date >= NOW())";
$params = [];
if (!empty($filters['category'])) {
$sql .= " AND p.category_id = :category";
$params['category'] = $filters['category'];
}
if (!empty($filters['search'])) {
$sql .= " AND (p.title LIKE :search OR p.description LIKE :search OR p.tags LIKE :search)";
$params['search'] = '%' . $filters['search'] . '%';
}
if (!empty($filters['sort'])) {
switch ($filters['sort']) {
case 'popular':
$sql .= " ORDER BY vote_count DESC";
break;
case 'newest':
$sql .= " ORDER BY p.created_at DESC";
break;
case 'ending':
$sql .= " ORDER BY p.end_date ASC";
break;
default:
$sql .= " ORDER BY p.created_at DESC";
}
} else {
$sql .= " ORDER BY p.created_at DESC";
}
// Pagination
$page = $filters['page'] ?? 1;
$limit = $filters['limit'] ?? ITEMS_PER_PAGE;
$offset = ($page - 1) * $limit;
$sql .= " LIMIT :limit OFFSET :offset";
$params['limit'] = $limit;
$params['offset'] = $offset;
return $this->db->getRows($sql, $params);
}
/**
* Get poll statistics
*/
public function getStatistics($pollId) {
$stats = [];
// Total votes
$stats['total_votes'] = $this->db->getValue(
"SELECT COUNT(*) FROM votes WHERE poll_id = ?",
[$pollId]
);
// Unique voters
$stats['unique_voters'] = $this->db->getValue(
"SELECT COUNT(DISTINCT COALESCE(user_id, session_id)) FROM votes WHERE poll_id = ?",
[$pollId]
);
// Completion rate
$totalStarts = $this->db->getValue(
"SELECT COUNT(*) FROM poll_views WHERE poll_id = ?",
[$pollId]
);
$stats['completion_rate'] = $totalStarts > 0 ? 
round(($stats['total_votes'] / $totalStarts) * 100, 2) : 0;
// Votes over time (last 7 days)
$stats['votes_over_time'] = $this->db->getRows(
"SELECT DATE(submitted_at) as date, COUNT(*) as count
FROM votes
WHERE poll_id = ? AND submitted_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY DATE(submitted_at)
ORDER BY date ASC",
[$pollId]
);
// Geographic distribution
$stats['geo_distribution'] = $this->db->getRows(
"SELECT country_code, COUNT(*) as count
FROM votes
WHERE poll_id = ? AND country_code IS NOT NULL
GROUP BY country_code
ORDER BY count DESC
LIMIT 10",
[$pollId]
);
return $stats;
}
/**
* Update poll status
*/
public function updateStatus($pollId, $status) {
$poll = $this->getPollById($pollId);
if (!$poll) {
return false;
}
$updateData = ['status' => $status];
if ($status === 'active' && $poll['status'] !== 'active') {
$updateData['published_at'] = date('Y-m-d H:i:s');
} elseif ($status === 'closed') {
$updateData['closed_at'] = date('Y-m-d H:i:s');
}
$this->db->update('polls', $updateData, 'id = :id', ['id' => $pollId]);
logAudit($poll['creator_id'], 'update_poll_status', "Poll status changed to {$status}", ['poll_id' => $pollId]);
return true;
}
/**
* Clone poll
*/
public function clonePoll($pollId, $newCreatorId = null) {
try {
$originalPoll = $this->getPollById($pollId);
if (!$originalPoll) {
return ['success' => false, 'error' => 'Poll not found'];
}
$this->db->beginTransaction();
// Clone poll data
$pollData = $originalPoll;
unset($pollData['id'], $pollData['poll_id'], $pollData['slug'], $pollData['created_at'], $pollData['updated_at']);
$pollData['creator_id'] = $newCreatorId ?? $originalPoll['creator_id'];
$pollData['title'] = $originalPoll['title'] . ' (Copy)';
$pollData['status'] = 'draft';
$pollData['total_votes'] = 0;
$pollData['unique_voters'] = 0;
$pollData['views'] = 0;
// Generate new poll ID and slug
$pollData['poll_id'] = generatePollId();
$pollData['slug'] = createSlug($pollData['title']);
// Ensure slug is unique
$counter = 1;
$originalSlug = $pollData['slug'];
while ($this->db->getValue("SELECT id FROM polls WHERE slug = ?", [$pollData['slug']])) {
$pollData['slug'] = $originalSlug . '-' . $counter;
$counter++;
}
// Insert cloned poll
$newPollId = $this->db->insert('polls', $pollData);
// Clone questions and options
$questions = $this->db->getRows("SELECT * FROM questions WHERE poll_id = ?", [$pollId]);
foreach ($questions as $question) {
$oldQuestionId = $question['id'];
unset($question['id'], $question['created_at'], $question['updated_at']);
$question['poll_id'] = $newPollId;
$newQuestionId = $this->db->insert('questions', $question);
// Clone options
$options = $this->db->getRows("SELECT * FROM question_options WHERE question_id = ?", [$oldQuestionId]);
foreach ($options as $option) {
unset($option['id'], $option['created_at']);
$option['question_id'] = $newQuestionId;
$this->db->insert('question_options', $option);
}
// Clone matrix rows/columns if applicable
if ($question['question_type'] === 'matrix') {
$rows = $this->db->getRows("SELECT * FROM matrix_rows WHERE question_id = ?", [$oldQuestionId]);
foreach ($rows as $row) {
unset($row['id'], $row['created_at']);
$row['question_id'] = $newQuestionId;
$this->db->insert('matrix_rows', $row);
}
$columns = $this->db->getRows("SELECT * FROM matrix_columns WHERE question_id = ?", [$oldQuestionId]);
foreach ($columns as $column) {
unset($column['id'], $column['created_at']);
$column['question_id'] = $newQuestionId;
$this->db->insert('matrix_columns', $column);
}
}
}
$this->db->commit();
logAudit($pollData['creator_id'], 'clone_poll', 'Cloned poll', ['original_poll_id' => $pollId, 'new_poll_id' => $newPollId]);
return ['success' => true, 'poll_id' => $newPollId, 'message' => 'Poll cloned successfully'];
} catch (Exception $e) {
$this->db->rollback();
logError('Clone poll error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to clone poll'];
}
}
/**
* Track poll view
*/
public function trackView($pollId) {
// Insert view record
$this->db->insert('poll_views', [
'poll_id' => $pollId,
'user_id' => $_SESSION['user_id'] ?? null,
'session_id' => getSessionId(),
'ip_address' => getUserIP(),
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
'referrer' => $_SERVER['HTTP_REFERER'] ?? null
]);
// Update view count
$this->db->query(
"UPDATE polls SET views = views + 1 WHERE id = ?",
[$pollId]
);
}
/**
* Get poll results
*/
public function getResults($pollId) {
$results = [];
// Get all questions
$questions = $this->db->getRows(
"SELECT * FROM questions WHERE poll_id = ? ORDER BY order_number ASC",
[$pollId]
);
foreach ($questions as $question) {
$questionResults = [
'id' => $question['id'],
'text' => $question['question_text'],
'type' => $question['question_type'],
'total_answers' => 0,
'options' => []
];
if (in_array($question['question_type'], ['single_choice', 'multiple_choice', 'rating', 'yes_no'])) {
// Get options with vote counts
$options = $this->db->getRows(
"SELECT o.*, COUNT(va.id) as vote_count
FROM question_options o
LEFT JOIN vote_answers va ON o.id = va.option_id AND va.question_id = o.question_id
WHERE o.question_id = ?
GROUP BY o.id
ORDER BY o.order_number ASC",
[$question['id']]
);
$totalVotes = 0;
foreach ($options as $option) {
$totalVotes += $option['vote_count'];
}
foreach ($options as $option) {
$percentage = $totalVotes > 0 ? round(($option['vote_count'] / $totalVotes) * 100, 2) : 0;
$questionResults['options'][] = [
'id' => $option['id'],
'text' => $option['option_text'],
'value' => $option['option_value'],
'votes' => $option['vote_count'],
'percentage' => $percentage,
'color' => $option['color'] ?? getRandomColor()
];
}
$questionResults['total_answers'] = $totalVotes;
} elseif ($question['question_type'] === 'matrix') {
// Get matrix results
$rows = $this->db->getRows(
"SELECT * FROM matrix_rows WHERE question_id = ? ORDER BY order_number ASC",
[$question['id']]
);
$columns = $this->db->getRows(
"SELECT * FROM matrix_columns WHERE question_id = ? ORDER BY order_number ASC",
[$question['id']]
);
foreach ($rows as $row) {
$rowData = [
'row_id' => $row['id'],
'row_text' => $row['row_text'],
'columns' => []
];
foreach ($columns as $column) {
$count = $this->db->getValue(
"SELECT COUNT(*) FROM vote_answers 
WHERE question_id = ? AND matrix_row_id = ? AND matrix_column_id = ?",
[$question['id'], $row['id'], $column['id']]
);
$rowData['columns'][] = [
'column_id' => $column['id'],
'column_text' => $column['column_text'],
'count' => $count
];
}
$questionResults['rows'][] = $rowData;
}
$questionResults['columns'] = $columns;
} elseif (in_array($question['question_type'], ['text_short', 'text_long'])) {
// Get text responses
$responses = $this->db->getRows(
"SELECT va.*, v.submitted_at 
FROM vote_answers va
JOIN votes v ON va.vote_id = v.id
WHERE va.question_id = ?
ORDER BY v.submitted_at DESC",
[$question['id']]
);
$questionResults['responses'] = $responses;
$questionResults['total_answers'] = count($responses);
}
$results[] = $questionResults;
}
return $results;
}
/**
* Export poll results
*/
public function exportResults($pollId, $format = 'csv') {
$poll = $this->getPollById($pollId);
$results = $this->getResults($pollId);
if ($format === 'csv') {
return $this->exportToCSV($poll, $results);
} elseif ($format === 'excel') {
return $this->exportToExcel($poll, $results);
} elseif ($format === 'pdf') {
return $this->exportToPDF($poll, $results);
}
return false;
}
/**
* Export to CSV
*/
private function exportToCSV($poll, $results) {
$filename = 'poll_' . $poll['poll_id'] . '_results.csv';
$filepath = UPLOAD_DIR . 'exports/' . $filename;
$handle = fopen($filepath, 'w');
// Add poll info
fputcsv($handle, ['Poll:', $poll['title']]);
fputcsv($handle, ['Created:', $poll['created_at']]);
fputcsv($handle, ['Total Votes:', $poll['total_votes']]);
fputcsv($handle, []);
// Add results
foreach ($results as $question) {
fputcsv($handle, ['Question:', $question['text']]);
fputcsv($handle, ['Type:', $question['type']]);
if (isset($question['options'])) {
fputcsv($handle, ['Option', 'Votes', 'Percentage']);
foreach ($question['options'] as $option) {
fputcsv($handle, [
$option['text'],
$option['votes'],
$option['percentage'] . '%'
]);
}
} elseif (isset($question['responses'])) {
fputcsv($handle, ['Response', 'Date']);
foreach ($question['responses'] as $response) {
fputcsv($handle, [
$response['answer_text'],
$response['submitted_at']
]);
}
}
fputcsv($handle, []);
}
fclose($handle);
return $filepath;
}
/**
* Export to Excel
*/
private function exportToExcel($poll, $results) {
// This would use PhpSpreadsheet library
// Placeholder for Excel export
return false;
}
/**
* Export to PDF
*/
private function exportToPDF($poll, $results) {
// This would use TCPDF or similar library
// Placeholder for PDF export
return false;
}
}
?>

Frontend Pages

Main Landing Page

File: index.php

<?php
require_once 'includes/config.php';
// Get categories
$categories = $db->getRows(
"SELECT * FROM categories WHERE is_active = 1 ORDER BY name ASC"
);
// Get featured polls
$poll = new Poll();
$featuredPolls = $poll->getPublicPolls(['limit' => 6, 'sort' => 'popular']);
// Get stats
$totalPolls = $db->getValue("SELECT COUNT(*) FROM polls WHERE status = 'active'");
$totalVotes = $db->getValue("SELECT COUNT(*) FROM votes");
$totalUsers = $db->getValue("SELECT COUNT(*) FROM users WHERE status = 'active'");
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo SITE_NAME; ?> - Create and Participate in Polls</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="assets/css/style.css">
<meta name="description" content="Create polls, surveys, and gather opinions easily. Join our community to participate in exciting polls.">
<meta name="keywords" content="polls, surveys, voting, opinions, feedback">
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm fixed-top">
<div class="container">
<a class="navbar-brand" href="index.php">
<i class="fas fa-poll text-primary me-2"></i>
<?php echo SITE_NAME; ?>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link active" href="index.php">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="voter/browse_polls.php">Browse Polls</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#categories">Categories</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#how-it-works">How It Works</a>
</li>
<?php if ($auth->isLoggedIn()): ?>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
<i class="fas fa-user-circle me-1"></i>
<?php echo htmlspecialchars($_SESSION['user_name']); ?>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" href="<?php 
echo $_SESSION['user_role'] == 'admin' ? 'admin/dashboard.php' : 
($_SESSION['user_role'] == 'creator' ? 'creator/dashboard.php' : 'voter/dashboard.php'); 
?>">
<i class="fas fa-tachometer-alt me-2"></i>Dashboard
</a>
</li>
<?php if ($_SESSION['user_role'] == 'creator'): ?>
<li>
<a class="dropdown-item" href="creator/create_poll.php">
<i class="fas fa-plus-circle me-2"></i>Create Poll
</a>
</li>
<?php endif; ?>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item text-danger" href="logout.php">
<i class="fas fa-sign-out-alt me-2"></i>Logout
</a>
</li>
</ul>
</li>
<?php else: ?>
<li class="nav-item">
<a class="nav-link" href="login.php">Login</a>
</li>
<li class="nav-item">
<a class="btn btn-primary ms-2" href="register.php">Sign Up</a>
</li>
<?php endif; ?>
</ul>
</div>
</div>
</nav>
<!-- Hero Section -->
<section class="hero-section bg-primary text-white py-5 mt-5">
<div class="container py-5">
<div class="row align-items-center">
<div class="col-lg-6">
<h1 class="display-4 fw-bold mb-4">Create, Share, and Participate in Polls</h1>
<p class="lead mb-4">Gather opinions, make data-driven decisions, and engage your audience with our easy-to-use poll system.</p>
<div class="d-flex gap-3">
<?php if ($auth->isLoggedIn() && $_SESSION['user_role'] == 'creator'): ?>
<a href="creator/create_poll.php" class="btn btn-light btn-lg">
<i class="fas fa-plus-circle me-2"></i>Create Poll
</a>
<?php elseif ($auth->isLoggedIn()): ?>
<a href="voter/browse_polls.php" class="btn btn-light btn-lg">
<i class="fas fa-search me-2"></i>Browse Polls
</a>
<?php else: ?>
<a href="register.php" class="btn btn-light btn-lg">
<i class="fas fa-user-plus me-2"></i>Get Started Free
</a>
<a href="voter/browse_polls.php" class="btn btn-outline-light btn-lg">
Browse Polls
</a>
<?php endif; ?>
</div>
<div class="mt-4">
<div class="row text-center">
<div class="col-4">
<h3 class="fw-bold"><?php echo number_format($totalPolls); ?></h3>
<small>Active Polls</small>
</div>
<div class="col-4">
<h3 class="fw-bold"><?php echo number_format($totalVotes); ?></h3>
<small>Total Votes</small>
</div>
<div class="col-4">
<h3 class="fw-bold"><?php echo number_format($totalUsers); ?></h3>
<small>Users</small>
</div>
</div>
</div>
</div>
<div class="col-lg-6 text-center">
<img src="assets/images/hero-poll.svg" alt="Poll Illustration" class="img-fluid">
</div>
</div>
</div>
</section>
<!-- Features Section -->
<section class="py-5">
<div class="container">
<h2 class="text-center fw-bold mb-5">Why Choose Our Poll System?</h2>
<div class="row g-4">
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-center p-4">
<div class="feature-icon bg-primary text-white rounded-circle mx-auto mb-3">
<i class="fas fa-pencil-alt"></i>
</div>
<h4>Easy to Create</h4>
<p class="text-muted">Create polls in minutes with our intuitive interface. No coding required.</p>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-center p-4">
<div class="feature-icon bg-primary text-white rounded-circle mx-auto mb-3">
<i class="fas fa-chart-pie"></i>
</div>
<h4>Real-time Results</h4>
<p class="text-muted">Watch results update in real-time with beautiful charts and graphs.</p>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-center p-4">
<div class="feature-icon bg-primary text-white rounded-circle mx-auto mb-3">
<i class="fas fa-share-alt"></i>
</div>
<h4>Easy to Share</h4>
<p class="text-muted">Share polls via social media, email, or embed them on your website.</p>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-center p-4">
<div class="feature-icon bg-primary text-white rounded-circle mx-auto mb-3">
<i class="fas fa-lock"></i>
</div>
<h4>Secure & Private</h4>
<p class="text-muted">Advanced security features to prevent fraud and protect voter privacy.</p>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-center p-4">
<div class="feature-icon bg-primary text-white rounded-circle mx-auto mb-3">
<i class="fas fa-mobile-alt"></i>
</div>
<h4>Mobile Friendly</h4>
<p class="text-muted">Fully responsive design works perfectly on all devices.</p>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-center p-4">
<div class="feature-icon bg-primary text-white rounded-circle mx-auto mb-3">
<i class="fas fa-file-export"></i>
</div>
<h4>Export Results</h4>
<p class="text-muted">Download results in CSV, Excel, or PDF formats for further analysis.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Categories Section -->
<section id="categories" class="py-5 bg-light">
<div class="container">
<h2 class="text-center fw-bold mb-5">Browse by Category</h2>
<div class="row g-4">
<?php foreach ($categories as $category): ?>
<div class="col-lg-3 col-md-4 col-6">
<a href="voter/browse_polls.php?category=<?php echo $category['id']; ?>" class="text-decoration-none">
<div class="card h-100 border-0 shadow-sm text-center p-4 category-card">
<div class="category-icon mb-3">
<i class="fas <?php echo $category['icon']; ?> fa-3x" style="color: <?php echo $category['color']; ?>"></i>
</div>
<h6 class="mb-0"><?php echo htmlspecialchars($category['name']); ?></h6>
</div>
</a>
</div>
<?php endforeach; ?>
</div>
</div>
</section>
<!-- Featured Polls -->
<section class="py-5">
<div class="container">
<h2 class="text-center fw-bold mb-5">Popular Polls</h2>
<div class="row g-4">
<?php foreach ($featuredPolls as $poll): ?>
<div class="col-lg-4 col-md-6">
<div class="card h-100 border-0 shadow-sm poll-card">
<?php if ($poll['featured_image']): ?>
<img src="uploads/poll_images/<?php echo $poll['featured_image']; ?>" 
class="card-img-top" alt="<?php echo htmlspecialchars($poll['title']); ?>"
style="height: 200px; object-fit: cover;">
<?php endif; ?>
<div class="card-body">
<h5 class="card-title"><?php echo htmlspecialchars($poll['title']); ?></h5>
<p class="card-text text-muted small">
<?php echo truncateText($poll['description'] ?? '', 100); ?>
</p>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="badge bg-info">
<i class="fas fa-tag me-1"></i>
<?php echo htmlspecialchars($poll['category_name'] ?? 'Uncategorized'); ?>
</span>
<small class="text-muted">
<i class="fas fa-users me-1"></i>
<?php echo $poll['vote_count']; ?> votes
</small>
</div>
<div class="progress mb-2" style="height: 5px;">
<?php
$status = getPollStatus($poll);
$progress = $poll['end_date'] ? 
min(100, (time() - strtotime($poll['start_date'])) / (strtotime($poll['end_date']) - strtotime($poll['start_date'])) * 100) : 
0;
?>
<div class="progress-bar bg-<?php echo $status['class']; ?>" 
style="width: <?php echo $progress; ?>%"></div>
</div>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
<i class="far fa-clock me-1"></i>
<?php echo timeAgo($poll['created_at']); ?>
</small>
<span class="badge bg-<?php echo $status['class']; ?>">
<?php echo $status['label']; ?>
</span>
</div>
</div>
<div class="card-footer bg-white border-0 pb-3">
<a href="voter/poll_details.php?slug=<?php echo $poll['slug']; ?>" class="btn btn-primary w-100">
View Poll <i class="fas fa-arrow-right ms-2"></i>
</a>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="text-center mt-4">
<a href="voter/browse_polls.php" class="btn btn-outline-primary btn-lg">
View All Polls <i class="fas fa-arrow-right ms-2"></i>
</a>
</div>
</div>
</section>
<!-- How It Works -->
<section id="how-it-works" class="py-5 bg-light">
<div class="container">
<h2 class="text-center fw-bold mb-5">How It Works</h2>
<div class="row">
<div class="col-md-4 mb-4">
<div class="step-card text-center">
<div class="step-number">1</div>
<i class="fas fa-user-plus fa-3x text-primary mb-3"></i>
<h4>Create Account</h4>
<p class="text-muted">Sign up for free as a voter or poll creator.</p>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="step-card text-center">
<div class="step-number">2</div>
<i class="fas fa-poll fa-3x text-primary mb-3"></i>
<h4>Create or Find Polls</h4>
<p class="text-muted">Creators make polls, voters browse and participate.</p>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="step-card text-center">
<div class="step-number">3</div>
<i class="fas fa-chart-line fa-3x text-primary mb-3"></i>
<h4>Analyze Results</h4>
<p class="text-muted">View real-time results and download reports.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Testimonials -->
<section class="py-5">
<div class="container">
<h2 class="text-center fw-bold mb-5">What Our Users Say</h2>
<div class="row">
<div class="col-md-4 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body p-4">
<div class="mb-3 text-warning">
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
</div>
<p class="card-text">"This poll system made it incredibly easy to gather feedback from our customers. The real-time results and export features are fantastic!"</p>
<div class="d-flex align-items-center">
<img src="assets/images/avatars/user1.jpg" class="rounded-circle me-3" width="50" alt="User">
<div>
<h6 class="mb-0">Sarah Johnson</h6>
<small class="text-muted">Marketing Manager</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body p-4">
<div class="mb-3 text-warning">
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
</div>
<p class="card-text">"I use this for my classroom to get student feedback. The interface is intuitive and my students love participating in polls."</p>
<div class="d-flex align-items-center">
<img src="assets/images/avatars/user2.jpg" class="rounded-circle me-3" width="50" alt="User">
<div>
<h6 class="mb-0">Prof. Michael Chen</h6>
<small class="text-muted">University Professor</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body p-4">
<div class="mb-3 text-warning">
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star-half-alt"></i>
</div>
<p class="card-text">"The best free poll system I've found. Great features, easy to embed on my website, and the analytics are comprehensive."</p>
<div class="d-flex align-items-center">
<img src="assets/images/avatars/user3.jpg" class="rounded-circle me-3" width="50" alt="User">
<div>
<h6 class="mb-0">Emily Rodriguez</h6>
<small class="text-muted">Blogger</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Call to Action -->
<section class="py-5 bg-primary text-white">
<div class="container text-center py-4">
<h2 class="fw-bold mb-4">Ready to Start Polling?</h2>
<p class="lead mb-4">Join thousands of users who trust our platform for their polling needs.</p>
<?php if (!$auth->isLoggedIn()): ?>
<a href="register.php" class="btn btn-light btn-lg px-5 me-2">Sign Up Free</a>
<a href="register.php?role=creator" class="btn btn-outline-light btn-lg px-5">Become a Creator</a>
<?php else: ?>
<a href="creator/create_poll.php" class="btn btn-light btn-lg px-5">Create Your First Poll</a>
<?php endif; ?>
</div>
</section>
<!-- Footer -->
<footer class="bg-dark text-white py-5">
<div class="container">
<div class="row">
<div class="col-md-4 mb-4">
<h5><i class="fas fa-poll me-2"></i><?php echo SITE_NAME; ?></h5>
<p class="text-white-50">The easiest way to create and participate in polls. Gather opinions and make better decisions.</p>
<div class="social-links">
<a href="#" class="text-white me-2"><i class="fab fa-facebook fa-lg"></i></a>
<a href="#" class="text-white me-2"><i class="fab fa-twitter fa-lg"></i></a>
<a href="#" class="text-white me-2"><i class="fab fa-instagram fa-lg"></i></a>
<a href="#" class="text-white me-2"><i class="fab fa-linkedin fa-lg"></i></a>
</div>
</div>
<div class="col-md-2 mb-4">
<h6>Quick Links</h6>
<ul class="list-unstyled">
<li><a href="index.php" class="text-white-50">Home</a></li>
<li><a href="voter/browse_polls.php" class="text-white-50">Browse Polls</a></li>
<li><a href="#categories" class="text-white-50">Categories</a></li>
<li><a href="#how-it-works" class="text-white-50">How It Works</a></li>
</ul>
</div>
<div class="col-md-3 mb-4">
<h6>For Creators</h6>
<ul class="list-unstyled">
<li><a href="register.php?role=creator" class="text-white-50">Become a Creator</a></li>
<li><a href="creator/create_poll.php" class="text-white-50">Create Poll</a></li>
<li><a href="creator/manage_polls.php" class="text-white-50">Manage Polls</a></li>
<li><a href="creator/poll_results.php" class="text-white-50">View Results</a></li>
</ul>
</div>
<div class="col-md-3 mb-4">
<h6>Contact Us</h6>
<ul class="list-unstyled text-white-50">
<li><i class="fas fa-envelope me-2"></i><?php echo SITE_EMAIL; ?></li>
<li><i class="fas fa-globe me-2"></i><?php echo SITE_URL; ?></li>
</ul>
</div>
</div>
<hr class="border-secondary">
<div class="row">
<div class="col-12 text-center">
<p class="text-white-50 mb-0">&copy; <?php echo date('Y'); ?> <?php echo SITE_NAME; ?>. All rights reserved.</p>
</div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js"></script>
<style>
.feature-icon {
width: 70px;
height: 70px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
}
.step-card {
position: relative;
padding: 30px 20px;
background: white;
border-radius: 10px;
box-shadow: 0 5px 20px rgba(0,0,0,0.05);
}
.step-number {
position: absolute;
top: -15px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 40px;
background: var(--bs-primary);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.2rem;
}
.category-card {
transition: transform 0.3s ease;
}
.category-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0,0,0,0.1) !important;
}
.poll-card {
transition: transform 0.3s ease;
}
.poll-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(0,0,0,0.15) !important;
}
.navbar {
padding: 1rem 0;
}
.hero-section {
margin-top: 76px;
}
</style>
</body>
</html>

Environment Configuration

File: .env

# Database Configuration
DB_HOST=localhost
DB_NAME=poll_system
DB_USER=root
DB_PASS=
# Application Configuration
SITE_NAME=Online Poll System
SITE_URL=http://localhost/poll-system
[email protected]
APP_VERSION=1.0.0
DEBUG_MODE=true
# Security
SESSION_TIMEOUT=3600
BCRYPT_ROUNDS=12
PEPPER=your-secret-pepper-string-change-this
# Upload Configuration
MAX_FILE_SIZE=5242880  # 5MB in bytes
# Pagination
ITEMS_PER_PAGE=20
# Date/Time
TIMEZONE=America/New_York
DATE_FORMAT=Y-m-d
TIME_FORMAT=H:i
# User Settings
ALLOW_REGISTRATION=true
REQUIRE_EMAIL_VERIFICATION=true
DEFAULT_USER_ROLE=voter
# Poll Settings
ALLOW_ANONYMOUS_VOTING=true
MAX_POLLS_PER_USER=100
ENABLE_COMMENTS=true
COMMENT_MODERATION=true
# reCAPTCHA (optional)
RECAPTCHA_SITE_KEY=
RECAPTCHA_SECRET_KEY=
# Mail Configuration
MAIL_DRIVER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=tls
[email protected]
MAIL_FROM_NAME=Poll System

File: .gitignore

# Environment variables
.env
# Dependencies
/vendor/
node_modules/
# IDE files
.vscode/
.idea/
*.sublime-*
# OS files
.DS_Store
Thumbs.db
# Logs
/logs/
*.log
# Uploads
/uploads/
!/uploads/.gitkeep
!/uploads/avatars/.gitkeep
!/uploads/poll_images/.gitkeep
!/uploads/exports/.gitkeep
# Cache
/cache/
!/cache/.gitkeep
# Composer
composer.lock
# Temp files
*.tmp
*.temp

File: composer.json

{
"name": "poll-system/application",
"description": "Online Poll System",
"type": "project",
"require": {
"php": ">=7.4",
"ext-pdo": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-curl": "*",
"ext-gd": "*",
"phpmailer/phpmailer": "^6.8",
"phpoffice/phpspreadsheet": "^1.29",
"tecnickcom/tcpdf": "^6.6"
},
"require-dev": {
"phpunit/phpunit": "^9.0"
},
"autoload": {
"psr-4": {
"PollSystem\\": "src/"
}
},
"scripts": {
"test": "phpunit tests",
"post-install-cmd": [
"chmod -R 755 uploads/",
"chmod -R 755 logs/",
"chmod -R 755 cache/"
]
}
}

How to Use the Project (Step-by-Step Guide)

Prerequisites

  1. Web Server: XAMPP, WAMP, MAMP, or any PHP-enabled server (PHP 7.4+)
  2. Database: MySQL 5.7+ or MariaDB
  3. Composer: For dependency management
  4. Browser: Modern web browser (Chrome, Firefox, Edge, etc.)

Installation Steps

Step 1: Set Up Local Server

  1. Download and install XAMPP from https://www.apachefriends.org/
  2. Launch XAMPP Control Panel
  3. Start Apache and MySQL services

Step 2: Install Composer Dependencies

  1. Download and install Composer from https://getcomposer.org/
  2. Navigate to your project directory in terminal
  3. Run: composer install

Step 3: Create Project Folder

  1. Navigate to C:\xampp\htdocs\ (Windows) or /Applications/XAMPP/htdocs/ (Mac)
  2. Create a new folder named poll-system
  3. Create all the folders and files as shown in the Project File Structure

Step 4: Set Up Database

  1. Open browser and go to http://localhost/phpmyadmin
  2. Click on "New" to create a new database
  3. Name the database poll_system and select utf8_general_ci
  4. Click on "Import" tab
  5. Click "Choose File" and select the database.sql file from the sql folder
  6. Click "Go" to import the database structure and sample data

Step 5: Configure Environment

  1. Rename .env.example to .env in the project root
  2. Update database credentials:
   DB_HOST=localhost
DB_NAME=poll_system
DB_USER=root
DB_PASS=
  1. Update application URL:
   SITE_URL=http://localhost/poll-system
  1. Set a secure pepper string:
   PEPPER=your-random-secure-string-here
  1. Configure email settings if using email notifications

Step 6: Set Folder Permissions

Create the following folders and ensure they are writable:

  • uploads/avatars/
  • uploads/poll_images/
  • uploads/exports/
  • logs/
  • cache/

On Windows, right-click folders → Properties → Security → give Write permission to Users
On Mac/Linux, run: chmod -R 755 uploads/ logs/ cache/

Step 7: Create Admin Password Hash

  1. Open a PHP file or use an online tool to generate a password hash:
   <?php
$password = 'Admin@123';
$peppered = $password . 'your-pepper-string';
echo password_hash($peppered, PASSWORD_BCRYPT, ['cost' => 12]);
?>
  1. Copy the generated hash
  2. Open phpMyAdmin, go to the users table
  3. Insert or update the admin user with the hash

Step 8: Test the Installation

  1. Open browser and go to http://localhost/poll-system/
  2. You should see the landing page
  3. Test different user types: Admin Login:
  • Username: admin (or email: [email protected])
  • Password: Admin@123 (or the password you set) Creator Registration:
  • Click "Sign Up" and register as a creator
  • Create and manage polls Voter Registration:
  • Register as a regular voter
  • Browse and participate in polls

System Walkthrough

For Voters:

  1. Browse Polls - Browse public polls by category or search
  2. View Poll Details - See poll information, questions, and status
  3. Vote - Answer questions and submit your vote
  4. View Results - See real-time results after voting
  5. Save Favorites - Bookmark polls for later
  6. My Votes - View your voting history
  7. Profile - Update personal information

For Poll Creators:

  1. Dashboard - Overview of your polls and statistics
  2. Create Poll - Create new polls with multiple question types
  3. Add Questions - Add questions with various types:
  • Single choice
  • Multiple choice
  • Rating scale
  • Text response
  • Matrix questions
  • Yes/No
  1. Manage Polls - Edit, close, or delete polls
  2. View Results - See detailed results with charts
  3. Export Data - Download results in CSV, Excel, or PDF
  4. Share Poll - Get shareable links and embed codes
  5. Send Invitations - Email invitations to specific participants

For Admins:

  1. Dashboard - System-wide statistics and overview
  2. User Management - Manage users, roles, and permissions
  3. Poll Management - Moderate polls and content
  4. Category Management - Manage poll categories
  5. System Settings - Configure system parameters
  6. Reports - Generate system reports
  7. Audit Logs - View system activity logs

Key Features Explained

Question Types

  1. Single Choice - Radio buttons, one answer allowed
  2. Multiple Choice - Checkboxes, multiple answers allowed
  3. Rating Scale - 1-5, 1-10, or custom scale
  4. Likert Scale - Strongly Agree to Strongly Disagree
  5. Text Response - Short answer or long answer text
  6. Number - Numeric input with validation
  7. Date - Date picker
  8. Ranking - Rank options in order
  9. Matrix - Grid of rows and columns
  10. Image Choice - Select from images
  11. Yes/No - Binary choice

Poll Settings

  • Visibility: Public, Private, Password-protected
  • Voting Restrictions: Login required, IP restriction, cookie tracking
  • Date Range: Set start and end dates
  • Multiple Votes: Allow or restrict multiple votes
  • Results Display: Show results after vote, after close, or never
  • Real-time Updates: Enable/disable real-time results

Security Features

  • CSRF Protection on all forms
  • XSS Prevention with output escaping
  • SQL Injection Prevention with prepared statements
  • Password Hashing with bcrypt and pepper
  • Session Management with timeout
  • Rate Limiting for API endpoints
  • reCAPTCHA for forms (optional)

Troubleshooting

Common Issues and Solutions

  1. Database Connection Error
  • Check if MySQL is running
  • Verify database credentials in .env
  • Ensure database poll_system exists
  1. 404 Page Not Found
  • Check file paths and folder structure
  • Verify SITE_URL in .env
  • Ensure .htaccess is properly configured (if using Apache)
  1. Upload Issues
  • Check folder permissions (755 or 777)
  • Verify MAX_FILE_SIZE in .env
  • Check allowed file extensions
  1. Email Not Sending
  • Configure SMTP settings correctly
  • Check spam folder
  • Verify PHP mail() function is enabled
  1. Session/Login Issues
  • Clear browser cookies and cache
  • Check SESSION_TIMEOUT in .env
  • Verify session save path is writable
  1. Charts Not Displaying
  • Check browser console for JavaScript errors
  • Ensure Chart.js is properly loaded
  • Verify data is being passed correctly

Performance Optimizations

  1. Database Indexing on frequently queried columns
  2. Query Caching for repeated requests
  3. Image Optimization for poll images
  4. Lazy Loading for images and content
  5. Pagination for large datasets
  6. Minified CSS and JavaScript for production
  7. CDN for static assets
  8. Browser Caching headers

Deployment to Production

  1. Update .env with production settings
  2. Set DEBUG_MODE=false
  3. Configure proper error logging
  4. Set up SSL certificate for HTTPS
  5. Configure cron jobs for maintenance tasks
  6. Set up database backups regularly
  7. Enable CDN for static assets
  8. Configure caching (Redis/Memcached)
  9. Set up monitoring and alerts

Conclusion

The Online Poll System is a comprehensive, feature-rich application for creating, managing, and participating in polls and surveys. With its intuitive interface, multiple question types, real-time results, and robust security features, it provides everything needed for effective data collection and analysis.

This application demonstrates:

  • Secure user authentication with password hashing and pepper
  • CRUD operations for polls, questions, and votes
  • Multiple question types with flexible options
  • Real-time data visualization with Chart.js
  • File upload for images and attachments
  • Export functionality for results
  • Responsive design for all devices
  • Modular code structure following OOP principles
  • Database design with proper relationships and indexing
  • Security best practices throughout

The system is built to be extensible, allowing easy addition of new question types, export formats, and integration with external services. Whether you're conducting market research, gathering employee feedback, running educational surveys, or simply having fun with friends, this poll system provides a solid foundation that can be customized to meet specific needs.

With proper deployment, regular backups, and security updates, this application can serve as a reliable tool for gathering and analyzing opinions in any context.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper