Introduction to the Project
The Notes Taking App with Database is a comprehensive, full-stack web application designed to help users capture, organize, and manage their notes efficiently. This system provides a robust platform for personal note-taking, task management, idea organization, and information retention. With user authentication, rich text editing, categorization, and search capabilities, it offers a complete solution for digital note management.
The application features role-based access control with three user types: Admin, Premium Users, and Basic Users. Notes are stored securely in a database with support for rich text formatting, attachments, tags, categories, and sharing options. Whether you're a student taking lecture notes, a professional organizing project ideas, or a writer collecting research, this notes app provides all the essential tools for effective note management.
Key Features
Core Features
- User Authentication: Secure registration and login system
- Note Creation: Create notes with titles and rich text content
- Rich Text Editor: Format text with bold, italic, lists, headings, and more
- Categories: Organize notes by custom categories
- Tags: Add multiple tags to notes for easy filtering
- Search: Full-text search across all notes
- Favorites: Mark important notes as favorites
- Archive: Archive old notes instead of deleting
- Trash: Recover deleted notes from trash
- Responsive Design: Works on desktop, tablet, and mobile
Advanced Features
- Rich Text Editing:
- Bold, italic, underline, strikethrough
- Headings (H1, H2, H3)
- Lists (ordered and unordered)
- Links and images
- Code blocks
- Tables
- Blockquotes
- Horizontal rules
- Text color and highlighting
- Undo/redo functionality
- Organization:
- Nested categories/folders
- Color-coded categories
- Drag-and-drop organization
- Pin important notes
- Custom sorting options
- Grid and list views
- Search & Filter:
- Full-text search
- Filter by category
- Filter by tags
- Filter by date range
- Filter by favorites
- Filter by archive status
- Saved searches
- Collaboration:
- Share notes with other users
- Public/private note visibility
- Comment on shared notes
- Collaborative editing
- Version history
- Note locking
- Export & Import:
- Export notes as PDF
- Export as Markdown
- Export as plain text
- Export as HTML
- Bulk export
- Import from Markdown
- Import from plain text
- Attachments:
- Upload images to notes
- Attach files (PDF, DOC, etc.)
- Image gallery
- File management
- Storage quota tracking
- Reminders:
- Set reminders on notes
- Due dates for tasks
- Email notifications
- Push notifications
- Recurring reminders
- Templates:
- Save note templates
- Quick note creation
- Meeting notes template
- To-do list template
- Journal template
- Premium Features:
- Unlimited storage
- Advanced formatting
- Version history (30 days)
- Collaborative editing
- Priority support
- No ads
- Advanced export options
Admin Features
- User Management: View, edit, suspend, or delete users
- Storage Monitoring: Track storage usage per user
- System Settings: Configure application parameters
- Analytics: View usage statistics
- Backup Management: Database backup and restore
- Logs: View system and error logs
Technology Stack
- Frontend: HTML5, CSS3, JavaScript (ES6+), Bootstrap 5
- Rich Text Editor: TinyMCE or CKEditor 5
- Backend: PHP 8.0+ (Core PHP with OOP approach)
- Database: MySQL 5.7+ or MariaDB
- Additional Libraries:
- 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
- TCPDF for PDF export
- Parsedown for Markdown processing
- Intervention Image for image handling
Project File Structure
notes-app/ │ ├── assets/ │ ├── css/ │ │ ├── style.css │ │ ├── dashboard.css │ │ ├── editor.css │ │ ├── dark-mode.css │ │ └── responsive.css │ ├── js/ │ │ ├── main.js │ │ ├── dashboard.js │ │ ├── editor.js │ │ ├── notes.js │ │ ├── search.js │ │ ├── validation.js │ │ └── tinymce-init.js │ ├── images/ │ │ ├── avatars/ │ │ ├── note-images/ │ │ └── attachments/ │ └── plugins/ │ ├── tinymce/ │ ├── datatables/ │ └── select2/ │ ├── includes/ │ ├── config.php │ ├── Database.php │ ├── functions.php │ ├── auth.php │ ├── Note.php │ ├── Category.php │ ├── Tag.php │ ├── User.php │ ├── Attachment.php │ ├── Share.php │ ├── Reminder.php │ ├── Template.php │ └── helpers/ │ ├── TextHelper.php │ ├── FileHelper.php │ └── ExportHelper.php │ ├── admin/ │ ├── dashboard.php │ ├── manage_users.php │ ├── user_details.php │ ├── system_settings.php │ ├── storage_stats.php │ ├── backup.php │ ├── logs.php │ └── analytics.php │ ├── user/ │ ├── dashboard.php │ ├── notes.php │ ├── create_note.php │ ├── edit_note.php │ ├── view_note.php │ ├── categories.php │ ├── create_category.php │ ├── edit_category.php │ ├── tags.php │ ├── favorites.php │ ├── archive.php │ ├── trash.php │ ├── search.php │ ├── attachments.php │ ├── reminders.php │ ├── templates.php │ ├── shared_with_me.php │ ├── profile.php │ ├── settings.php │ └── upgrade.php │ ├── api/ │ ├── get_notes.php │ ├── save_note.php │ ├── delete_note.php │ ├── restore_note.php │ ├── search_notes.php │ ├── get_categories.php │ ├── get_tags.php │ ├── upload_attachment.php │ ├── share_note.php │ └── set_reminder.php │ ├── uploads/ │ ├── avatars/ │ ├── note-images/ │ └── attachments/ │ ├── documents/ │ └── images/ │ ├── vendor/ │ ├── index.php ├── login.php ├── register.php ├── forgot_password.php ├── reset_password.php ├── logout.php ├── verify.php ├── .env ├── .gitignore ├── composer.json └── sql/ └── database.sql
Database Schema
File: sql/database.sql
-- Create Database CREATE DATABASE IF NOT EXISTS `notes_app`; USE `notes_app`; -- 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', 'premium', 'basic') DEFAULT 'basic', `status` ENUM('active', 'inactive', 'suspended') DEFAULT 'active', -- Account Settings `email_verified` BOOLEAN DEFAULT FALSE, `verification_token` VARCHAR(255), `reset_token` VARCHAR(255), `reset_expires` DATETIME, -- Preferences `theme` ENUM('light', 'dark', 'auto') DEFAULT 'light', `default_view` ENUM('grid', 'list') DEFAULT 'grid', `items_per_page` INT DEFAULT 20, `editor_type` ENUM('simple', 'full') DEFAULT 'full', -- Storage `storage_used` BIGINT DEFAULT 0, `storage_limit` BIGINT DEFAULT 104857600, -- 100 MB for basic, 1GB for premium `notes_count` INT DEFAULT 0, -- Premium Features `premium_until` DATETIME, `last_backup` DATETIME, -- Timestamps `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_username` (`username`), INDEX `idx_role` (`role`), INDEX `idx_status` (`status`) ); -- Categories Table (Folders) CREATE TABLE `categories` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` INT(11) NOT NULL, `name` VARCHAR(100) NOT NULL, `slug` VARCHAR(100) NOT NULL, `description` TEXT, `color` VARCHAR(7) DEFAULT '#6c757d', `icon` VARCHAR(50) DEFAULT 'fa-folder', `parent_id` INT(11) DEFAULT NULL, `sort_order` INT DEFAULT 0, `is_system` BOOLEAN DEFAULT FALSE, `notes_count` INT DEFAULT 0, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, FOREIGN KEY (`parent_id`) REFERENCES `categories`(`id`) ON DELETE CASCADE, UNIQUE KEY `unique_category_per_user` (`user_id`, `slug`), INDEX `idx_user` (`user_id`) ); -- Tags Table CREATE TABLE `tags` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` INT(11) NOT NULL, `name` VARCHAR(50) NOT NULL, `slug` VARCHAR(50) NOT NULL, `color` VARCHAR(7) DEFAULT '#6c757d', `notes_count` INT DEFAULT 0, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, UNIQUE KEY `unique_tag_per_user` (`user_id`, `slug`), INDEX `idx_user` (`user_id`) ); -- Notes Table CREATE TABLE `notes` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` INT(11) NOT NULL, `category_id` INT(11) DEFAULT NULL, `uuid` VARCHAR(36) UNIQUE NOT NULL, `title` VARCHAR(255) NOT NULL, `slug` VARCHAR(255) NOT NULL, `content` LONGTEXT, `content_plain` TEXT, `excerpt` TEXT, -- Formatting `editor_type` ENUM('plain', 'rich', 'markdown') DEFAULT 'rich', -- Status `is_favorite` BOOLEAN DEFAULT FALSE, `is_archived` BOOLEAN DEFAULT FALSE, `is_trashed` BOOLEAN DEFAULT FALSE, `is_public` BOOLEAN DEFAULT FALSE, `is_pinned` BOOLEAN DEFAULT FALSE, `is_protected` BOOLEAN DEFAULT FALSE, `password` VARCHAR(255), -- Metadata `color` VARCHAR(7) DEFAULT NULL, `icon` VARCHAR(50) DEFAULT NULL, `cover_image` VARCHAR(255), -- Statistics `views` INT DEFAULT 0, `word_count` INT DEFAULT 0, `character_count` INT DEFAULT 0, `reading_time` INT DEFAULT 0, -- in minutes -- Versioning `version` INT DEFAULT 1, -- Timestamps `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `deleted_at` DATETIME DEFAULT NULL, PRIMARY KEY (`id`), FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, FOREIGN KEY (`category_id`) REFERENCES `categories`(`id`) ON DELETE SET NULL, UNIQUE KEY `unique_slug_per_user` (`user_id`, `slug`), INDEX `idx_user` (`user_id`), INDEX `idx_category` (`category_id`), INDEX `idx_favorite` (`is_favorite`), INDEX `idx_archived` (`is_archived`), INDEX `idx_trashed` (`is_trashed`), INDEX `idx_public` (`is_public`), INDEX `idx_pinned` (`is_pinned`), INDEX `idx_created` (`created_at`), INDEX `idx_updated` (`updated_at`), FULLTEXT INDEX `idx_search` (`title`, `content`, `content_plain`, `excerpt`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Note Tags Junction Table CREATE TABLE `note_tags` ( `note_id` INT(11) NOT NULL, `tag_id` INT(11) NOT NULL, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`note_id`, `tag_id`), FOREIGN KEY (`note_id`) REFERENCES `notes`(`id`) ON DELETE CASCADE, FOREIGN KEY (`tag_id`) REFERENCES `tags`(`id`) ON DELETE CASCADE ); -- Attachments Table CREATE TABLE `attachments` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `note_id` INT(11) NOT NULL, `user_id` INT(11) NOT NULL, `filename` VARCHAR(255) NOT NULL, `original_filename` VARCHAR(255) NOT NULL, `file_path` VARCHAR(255) NOT NULL, `file_size` INT NOT NULL, `file_type` VARCHAR(100), `mime_type` VARCHAR(100), `width` INT DEFAULT NULL, `height` INT DEFAULT NULL, `is_image` BOOLEAN DEFAULT FALSE, `download_count` INT DEFAULT 0, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), FOREIGN KEY (`note_id`) REFERENCES `notes`(`id`) ON DELETE CASCADE, FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, INDEX `idx_note` (`note_id`), INDEX `idx_user` (`user_id`) ); -- Note Versions Table (for premium users) CREATE TABLE `note_versions` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `note_id` INT(11) NOT NULL, `version` INT NOT NULL, `title` VARCHAR(255) NOT NULL, `content` LONGTEXT, `word_count` INT DEFAULT 0, `character_count` INT DEFAULT 0, `created_by` INT(11), `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), FOREIGN KEY (`note_id`) REFERENCES `notes`(`id`) ON DELETE CASCADE, FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON DELETE SET NULL, INDEX `idx_note` (`note_id`), UNIQUE KEY `unique_note_version` (`note_id`, `version`) ); -- Shared Notes Table CREATE TABLE `shared_notes` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `note_id` INT(11) NOT NULL, `shared_by` INT(11) NOT NULL, `shared_with` INT(11), `share_token` VARCHAR(64) UNIQUE, `email` VARCHAR(100), `permission` ENUM('view', 'comment', 'edit') DEFAULT 'view', `expires_at` DATETIME, `view_count` INT DEFAULT 0, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), FOREIGN KEY (`note_id`) REFERENCES `notes`(`id`) ON DELETE CASCADE, FOREIGN KEY (`shared_by`) REFERENCES `users`(`id`) ON DELETE CASCADE, FOREIGN KEY (`shared_with`) REFERENCES `users`(`id`) ON DELETE CASCADE, INDEX `idx_note` (`note_id`), INDEX `idx_token` (`share_token`), INDEX `idx_email` (`email`) ); -- Comments on Shared Notes CREATE TABLE `comments` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `note_id` INT(11) NOT NULL, `user_id` INT(11) NOT NULL, `parent_id` INT(11) DEFAULT NULL, `content` TEXT NOT NULL, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), FOREIGN KEY (`note_id`) REFERENCES `notes`(`id`) ON DELETE CASCADE, FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, FOREIGN KEY (`parent_id`) REFERENCES `comments`(`id`) ON DELETE CASCADE, INDEX `idx_note` (`note_id`) ); -- Reminders Table CREATE TABLE `reminders` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `note_id` INT(11) NOT NULL, `user_id` INT(11) NOT NULL, `title` VARCHAR(255), `reminder_time` DATETIME NOT NULL, `repeat_type` ENUM('none', 'daily', 'weekly', 'monthly', 'yearly') DEFAULT 'none', `repeat_interval` INT DEFAULT 1, `repeat_end` DATETIME, `is_completed` BOOLEAN DEFAULT FALSE, `completed_at` DATETIME, `notification_sent` BOOLEAN DEFAULT FALSE, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), FOREIGN KEY (`note_id`) REFERENCES `notes`(`id`) ON DELETE CASCADE, FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, INDEX `idx_user` (`user_id`), INDEX `idx_time` (`reminder_time`), INDEX `idx_completed` (`is_completed`) ); -- Templates Table CREATE TABLE `templates` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` INT(11) NOT NULL, `name` VARCHAR(100) NOT NULL, `description` TEXT, `content` LONGTEXT NOT NULL, `category` VARCHAR(50) DEFAULT 'general', `is_system` BOOLEAN DEFAULT FALSE, `usage_count` INT DEFAULT 0, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, INDEX `idx_user` (`user_id`) ); -- System Templates (predefined) INSERT INTO `templates` (`user_id`, `name`, `description`, `content`, `category`, `is_system`) VALUES (1, 'Meeting Notes', 'Template for taking meeting notes', '<h1>Meeting Notes</h1>\n\n<h2>Date:</h2>\n\n<h2>Attendees:</h2>\n\n<h2>Agenda:</h2>\n\n<ol>\n<li></li>\n</ol>\n\n<h2>Discussion Points:</h2>\n\n<h2>Action Items:</h2>\n\n<table>\n<thead>\n<tr>\n<th>Task</th>\n<th>Owner</th>\n<th>Due Date</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td></td>\n<td></td>\n<td></td>\n</tr>\n</tbody>\n</table>\n\n<h2>Next Meeting:</h2>', 'meeting', 1), (1, 'To-Do List', 'Simple to-do list template', '<h1>To-Do List</h1>\n\n<ul>\n<li>[ ] Task 1</li>\n<li>[ ] Task 2</li>\n<li>[ ] Task 3</li>\n</ul>\n\n<h2>Priority:</h2>\n\n<ol>\n<li>High priority task</li>\n<li>Medium priority task</li>\n<li>Low priority task</li>\n</ol>', 'task', 1), (1, 'Journal Entry', 'Daily journal template', '<h1>Journal Entry</h1>\n\n<h2>Date:</h2>\n\n<h2>Mood:</h2>\n\n<h2>Gratitude:</h2>\n\n<ul>\n<li></li>\n</ul>\n\n<h2>Today''s Highlights:</h2>\n\n<h2>Challenges:</h2>\n\n<h2>Tomorrow''s Goals:</h2>', 'journal', 1), (1, 'Project Plan', 'Project planning template', '<h1>Project Plan</h1>\n\n<h2>Project Name:</h2>\n\n<h2>Objective:</h2>\n\n<h2>Timeline:</h2>\n\n<h2>Milestones:</h2>\n\n<ol>\n<li></li>\n</ol>\n\n<h2>Tasks:</h2>\n\n<table>\n<thead>\n<tr>\n<th>Task</th>\n<th>Assigned To</th>\n<th>Deadline</th>\n<th>Status</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td></td>\n<td></td>\n<td></td>\n<td></td>\n</tr>\n</tbody>\n</table>\n\n<h2>Resources:</h2>\n\n<h2>Notes:</h2>', 'project', 1), (1, 'Recipe', 'Recipe template', '<h1>Recipe: </h1>\n\n<h2>Prep Time:</h2>\n\n<h2>Cook Time:</h2>\n\n<h2>Servings:</h2>\n\n<h2>Ingredients:</h2>\n\n<ul>\n<li></li>\n</ul>\n\n<h2>Instructions:</h2>\n\n<ol>\n<li></li>\n</ol>\n\n<h2>Notes:</h2>', 'food', 1); -- User Settings Table CREATE TABLE `user_settings` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` INT(11) NOT NULL, `setting_key` VARCHAR(100) NOT NULL, `setting_value` TEXT, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, UNIQUE KEY `unique_setting` (`user_id`, `setting_key`) ); -- Notifications Table CREATE TABLE `notifications` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` INT(11) NOT NULL, `type` ENUM('reminder', 'share', 'comment', 'system', 'upgrade') 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`) ); -- 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`, `storage_limit`) VALUES ('admin', '[email protected]', '$2y$10$YourHashedPasswordHere', 'System', 'Administrator', 'admin', TRUE, 1073741824); -- 1GB for admin -- Insert Default System Settings INSERT INTO `system_settings` (`setting_key`, `setting_value`, `description`) VALUES ('site_name', 'Notes App', 'Name of the site'), ('site_description', 'Your personal notes taking application', 'Site description'), ('site_keywords', 'notes, notepad, organizer, productivity', 'SEO keywords'), ('site_email', '[email protected]', 'Contact email'), ('allow_registration', '1', 'Allow user registration'), ('require_email_verification', '1', 'Require email verification'), ('default_user_role', 'basic', 'Default role for new users'), ('basic_storage_limit', '104857600', 'Storage limit for basic users (100 MB)'), ('premium_storage_limit', '1073741824', 'Storage limit for premium users (1 GB)'), ('max_file_size', '5242880', 'Maximum file size for uploads (5 MB)'), ('allowed_file_types', 'jpg,jpeg,png,gif,pdf,doc,docx,txt,md', 'Allowed file types'), ('enable_rich_editor', '1', 'Enable rich text editor'), ('enable_markdown', '1', 'Enable Markdown support'), ('enable_attachments', '1', 'Enable file attachments'), ('enable_sharing', '1', 'Enable note sharing'), ('enable_reminders', '1', 'Enable reminders'), ('items_per_page', '20', 'Default items per page'), ('timezone', 'America/New_York', 'Default timezone'), ('date_format', 'Y-m-d', 'Date format'), ('time_format', 'H:i', 'Time format'); 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(); } /** * 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') ?: 'notes_app'); define('DB_USER', getenv('DB_USER') ?: 'root'); define('DB_PASS', getenv('DB_PASS') ?: ''); // Application Configuration define('SITE_NAME', getenv('SITE_NAME') ?: 'Notes App'); define('SITE_URL', getenv('SITE_URL') ?: 'http://localhost/notes-app'); 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', explode(',', getenv('ALLOWED_FILE_TYPES') ?: 'jpg,jpeg,png,gif,pdf,doc,docx,txt,md')); // Storage Limits (in bytes) define('BASIC_STORAGE_LIMIT', getenv('BASIC_STORAGE_LIMIT') ?: 100 * 1024 * 1024); // 100 MB define('PREMIUM_STORAGE_LIMIT', getenv('PREMIUM_STORAGE_LIMIT') ?: 1024 * 1024 * 1024); // 1 GB // 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'); // Feature Flags define('ENABLE_RICH_EDITOR', getenv('ENABLE_RICH_EDITOR') === 'true'); define('ENABLE_MARKDOWN', getenv('ENABLE_MARKDOWN') === 'true'); define('ENABLE_ATTACHMENTS', getenv('ENABLE_ATTACHMENTS') === 'true'); define('ENABLE_SHARING', getenv('ENABLE_SHARING') === 'true'); define('ENABLE_REMINDERS', getenv('ENABLE_REMINDERS') === 'true'); // 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__ . '/Note.php'; require_once __DIR__ . '/Category.php'; require_once __DIR__ . '/Tag.php'; require_once __DIR__ . '/User.php'; require_once __DIR__ . '/Attachment.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 UUID v4 */ function generateUUID() { $data = random_bytes(16); $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100 $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10 return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } /** * Create 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 slug for note */ function generateUniqueSlug($userId, $title, $db, $noteId = null) { $slug = createSlug($title); $originalSlug = $slug; $counter = 1; $query = "SELECT id FROM notes WHERE user_id = :user_id AND slug = :slug"; $params = ['user_id' => $userId, 'slug' => $slug]; if ($noteId) { $query .= " AND id != :note_id"; $params['note_id'] = $noteId; } while ($db->getRow($query, $params)) { $slug = $originalSlug . '-' . $counter; $params['slug'] = $slug; $counter++; } return $slug; } /** * 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']; } } /** * Format file size */ function formatFileSize($bytes, $decimals = 2) { $size = ['B', 'KB', 'MB', 'GB', 'TB']; $factor = floor((strlen($bytes) - 1) / 3); return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . ' ' . $size[$factor]; } /** * Calculate reading time */ function calculateReadingTime($content, $wordsPerMinute = 200) { $wordCount = str_word_count(strip_tags($content)); return ceil($wordCount / $wordsPerMinute); } /** * Get excerpt from content */ function getExcerpt($content, $length = 200) { $text = strip_tags($content); if (strlen($text) <= $length) { return $text; } return substr($text, 0, $length) . '...'; } /** * 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'; } } /** * Check user storage limit */ function checkStorageLimit($userId, $fileSize) { $db = Database::getInstance(); $user = $db->getRow("SELECT storage_used, storage_limit FROM users WHERE id = ?", [$userId]); if ($user['storage_used'] + $fileSize > $user['storage_limit']) { return false; } return true; } /** * Update user storage usage */ function updateStorageUsage($userId, $fileSize, $operation = 'add') { $db = Database::getInstance(); if ($operation === 'add') { $db->query("UPDATE users SET storage_used = storage_used + ? WHERE id = ?", [$fileSize, $userId]); } else { $db->query("UPDATE users SET storage_used = GREATEST(0, storage_used - ?) WHERE id = ?", [$fileSize, $userId]); } } /** * Upload file */ function uploadFile($file, $targetDir, $userId, $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 storage limit if (!checkStorageLimit($userId, $file['size'])) { return ['success' => false, 'error' => 'Storage limit exceeded']; } // 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)) { // Update storage usage updateStorageUsage($userId, $file['size'], 'add'); return [ 'success' => true, 'filename' => $filename, 'original_name' => $file['name'], 'path' => $targetPath, 'size' => $file['size'], 'type' => $file['type'], 'extension' => $extension, 'url' => SITE_URL . '/uploads/' . basename($targetDir) . '/' . $filename ]; } return ['success' => false, 'error' => 'Failed to move uploaded file']; } /** * Delete file */ function deleteFile($path, $userId, $fileSize = null) { if (file_exists($path)) { $size = $fileSize ?: filesize($path); if (unlink($path)) { updateStorageUsage($userId, $size, 'remove'); return true; } } 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); } /** * Send email notification */ 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; } /** * Get user role badge */ function getUserRoleBadge($role) { $badges = [ 'admin' => 'bg-danger', 'premium' => 'bg-warning', 'basic' => 'bg-secondary' ]; $labels = [ 'admin' => 'Admin', 'premium' => 'Premium', 'basic' => 'Basic' ]; $class = $badges[$role] ?? 'bg-secondary'; $label = $labels[$role] ?? ucfirst($role); return '<span class="badge ' . $class . '">' . $label . '</span>'; } /** * Get note status badge */ function getNoteStatusBadge($note) { if ($note['is_trashed']) { return '<span class="badge bg-danger">Trashed</span>'; } if ($note['is_archived']) { return '<span class="badge bg-warning">Archived</span>'; } if ($note['is_favorite']) { return '<span class="badge bg-success">Favorite</span>'; } if ($note['is_pinned']) { return '<span class="badge bg-info">Pinned</span>'; } return '<span class="badge bg-secondary">Active</span>'; } /** * Convert Markdown to HTML */ function markdownToHtml($markdown) { // This is a simplified version. In production, use Parsedown library $html = $markdown; // Headers $html = preg_replace('/^### (.*?)$/m', '<h3>$1</h3>', $html); $html = preg_replace('/^## (.*?)$/m', '<h2>$1</h2>', $html); $html = preg_replace('/^# (.*?)$/m', '<h1>$1</h1>', $html); // Bold and italic $html = preg_replace('/\*\*\*(.*?)\*\*\*/', '<strong><em>$1</em></strong>', $html); $html = preg_replace('/\*\*(.*?)\*\*/', '<strong>$1</strong>', $html); $html = preg_replace('/\*(.*?)\*/', '<em>$1</em>', $html); // Lists $html = preg_replace('/^\- (.*?)$/m', '<li>$1</li>', $html); $html = preg_replace('/(<li>.*<\/li>)/s', '<ul>$1</ul>', $html); return $html; } /** * Send push notification (placeholder) */ function sendPushNotification($userId, $title, $message) { // Implement push notification logic here // This could use Firebase Cloud Messaging or similar return true; } ?> 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(); // Determine storage limit based on role $storageLimit = ($data['role'] ?? 'basic') === 'premium' ? PREMIUM_STORAGE_LIMIT : BASIC_STORAGE_LIMIT; // 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'] ?? 'basic', 'storage_limit' => $storageLimit, 'verification_token' => $verificationToken, 'last_ip' => getUserIP() ]); if ($userId) { // Create default categories for user $this->createDefaultCategories($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()]; } } /** * Create default categories for new user */ private function createDefaultCategories($userId) { $categories = [ ['name' => 'Personal', 'color' => '#28a745', 'icon' => 'fa-user'], ['name' => 'Work', 'color' => '#007bff', 'icon' => 'fa-briefcase'], ['name' => 'Ideas', 'color' => '#ffc107', 'icon' => 'fa-lightbulb'], ['name' => 'Journal', 'color' => '#6f42c1', 'icon' => 'fa-book'], ['name' => 'Tasks', 'color' => '#dc3545', 'icon' => 'fa-tasks'] ]; foreach ($categories as $category) { $category['user_id'] = $userId; $category['slug'] = createSlug($category['name']); $category['is_system'] = true; $this->db->insert('categories', $category); } } /** * 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; } /** * Check if user is premium */ public function isPremium() { if (!$this->isLoggedIn()) { return false; } $user = $this->getCurrentUser(); if ($user['role'] === 'premium' || $user['role'] === 'admin') { // Check if premium hasn't expired if ($user['premium_until'] && strtotime($user['premium_until']) < time()) { // Downgrade to basic $this->downgradeUser($user['id']); return false; } return true; } return false; } /** * Downgrade user from premium to basic */ private function downgradeUser($userId) { $this->db->update( 'users', ['role' => 'basic', 'premium_until' => null], 'id = :id', ['id' => $userId] ); // Update storage limit $this->db->update( 'users', ['storage_limit' => BASIC_STORAGE_LIMIT], 'id = :id', ['id' => $userId] ); } /** * 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('premium')) { redirect('/user/dashboard.php'); } else { redirect('/user/dashboard.php'); } } } /** * Require premium */ public function requirePremium() { $this->requireLogin(); if (!$this->isPremium()) { $_SESSION['error'] = 'This feature requires a premium subscription'; redirect('/user/upgrade.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, 'theme' => $data['theme'] ?? 'light', 'default_view' => $data['default_view'] ?? 'grid', 'items_per_page' => $data['items_per_page'] ?? 20 ]; // Handle profile picture upload if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] == 0) { $uploadDir = UPLOAD_DIR . 'avatars/'; $result = uploadFile($_FILES['profile_picture'], $uploadDir, $userId); if ($result['success']) { // Delete old picture $user = $this->getCurrentUser(); if ($user['profile_picture'] !== 'default.png') { deleteFile(UPLOAD_DIR . 'avatars/' . $user['profile_picture'], $userId); } $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 storage info */ public function getStorageInfo($userId) { $user = $this->getUserById($userId); $used = $user['storage_used']; $limit = $user['storage_limit']; $percentage = $limit > 0 ? round(($used / $limit) * 100, 2) : 0; return [ 'used' => $used, 'limit' => $limit, 'percentage' => $percentage, 'used_formatted' => formatFileSize($used), 'limit_formatted' => formatFileSize($limit), 'is_near_limit' => $percentage > 80, 'is_full' => $percentage >= 100 ]; } } // Initialize Auth $auth = new Auth(); ?> Note Class
File: includes/Note.php
<?php /** * Note Class * Handles all note-related operations */ class Note { private $db; /** * Constructor */ public function __construct() { $this->db = Database::getInstance(); } /** * Create a new note */ public function create($userId, $data) { try { $this->db->beginTransaction(); // Generate UUID and slug $uuid = generateUUID(); $slug = generateUniqueSlug($userId, $data['title'], $this->db); // Calculate word count and reading time $wordCount = str_word_count(strip_tags($data['content'] ?? '')); $readingTime = ceil($wordCount / 200); // Prepare note data $noteData = [ 'user_id' => $userId, 'category_id' => $data['category_id'] ?? null, 'uuid' => $uuid, 'title' => $data['title'], 'slug' => $slug, 'content' => $data['content'] ?? '', 'content_plain' => strip_tags($data['content'] ?? ''), 'excerpt' => getExcerpt($data['content'] ?? ''), 'editor_type' => $data['editor_type'] ?? 'rich', 'color' => $data['color'] ?? null, 'icon' => $data['icon'] ?? null, 'word_count' => $wordCount, 'character_count' => strlen($data['content'] ?? ''), 'reading_time' => $readingTime ]; // Insert note $noteId = $this->db->insert('notes', $noteData); // Handle tags if (!empty($data['tags'])) { $this->addTags($noteId, $userId, $data['tags']); } // Update category notes count if (!empty($data['category_id'])) { $this->db->query( "UPDATE categories SET notes_count = notes_count + 1 WHERE id = ?", [$data['category_id']] ); } // Update user notes count $this->db->query( "UPDATE users SET notes_count = notes_count + 1 WHERE id = ?", [$userId] ); $this->db->commit(); logAudit($userId, 'create_note', 'Created new note', ['note_id' => $noteId]); return [ 'success' => true, 'note_id' => $noteId, 'uuid' => $uuid, 'slug' => $slug, 'message' => 'Note created successfully' ]; } catch (Exception $e) { $this->db->rollback(); logError('Create note error: ' . $e->getMessage(), $data); return ['success' => false, 'error' => 'Failed to create note: ' . $e->getMessage()]; } } /** * Update note */ public function update($noteId, $userId, $data) { try { $note = $this->getNoteById($noteId, $userId); if (!$note) { return ['success' => false, 'error' => 'Note not found']; } $this->db->beginTransaction(); // Generate new slug if title changed $slug = $note['slug']; if ($data['title'] !== $note['title']) { $slug = generateUniqueSlug($userId, $data['title'], $this->db, $noteId); } // Calculate word count and reading time $wordCount = str_word_count(strip_tags($data['content'] ?? '')); $readingTime = ceil($wordCount / 200); // Prepare update data $updateData = [ 'category_id' => $data['category_id'] ?? $note['category_id'], 'title' => $data['title'], 'slug' => $slug, 'content' => $data['content'] ?? $note['content'], 'content_plain' => strip_tags($data['content'] ?? $note['content']), 'excerpt' => getExcerpt($data['content'] ?? $note['content']), 'editor_type' => $data['editor_type'] ?? $note['editor_type'], 'color' => $data['color'] ?? $note['color'], 'icon' => $data['icon'] ?? $note['icon'], 'word_count' => $wordCount, 'character_count' => strlen($data['content'] ?? ''), 'reading_time' => $readingTime, 'version' => $note['version'] + 1 ]; // Save version for premium users $user = (new Auth())->getCurrentUser(); if ($user['role'] === 'premium' || $user['role'] === 'admin') { $this->saveVersion($noteId, $note['title'], $note['content'], $userId); } // Update note $this->db->update('notes', $updateData, 'id = :id', ['id' => $noteId]); // Handle category change if ($note['category_id'] != $updateData['category_id']) { // Decrement old category if ($note['category_id']) { $this->db->query( "UPDATE categories SET notes_count = GREATEST(0, notes_count - 1) WHERE id = ?", [$note['category_id']] ); } // Increment new category if ($updateData['category_id']) { $this->db->query( "UPDATE categories SET notes_count = notes_count + 1 WHERE id = ?", [$updateData['category_id']] ); } } // Handle tags if (isset($data['tags'])) { $this->updateTags($noteId, $userId, $data['tags']); } $this->db->commit(); logAudit($userId, 'update_note', 'Updated note', ['note_id' => $noteId]); return ['success' => true, 'message' => 'Note updated successfully']; } catch (Exception $e) { $this->db->rollback(); logError('Update note error: ' . $e->getMessage()); return ['success' => false, 'error' => 'Failed to update note']; } } /** * Save note version */ private function saveVersion($noteId, $title, $content, $userId) { $version = $this->db->getValue( "SELECT COUNT(*) + 1 FROM note_versions WHERE note_id = ?", [$noteId] ); $this->db->insert('note_versions', [ 'note_id' => $noteId, 'version' => $version, 'title' => $title, 'content' => $content, 'word_count' => str_word_count(strip_tags($content)), 'character_count' => strlen($content), 'created_by' => $userId ]); } /** * Add tags to note */ private function addTags($noteId, $userId, $tags) { $tagIds = []; foreach ($tags as $tagName) { $tagName = trim($tagName); if (empty($tagName)) continue; $slug = createSlug($tagName); // Check if tag exists $tag = $this->db->getRow( "SELECT id FROM tags WHERE user_id = ? AND slug = ?", [$userId, $slug] ); if ($tag) { $tagId = $tag['id']; // Increment tag count $this->db->query( "UPDATE tags SET notes_count = notes_count + 1 WHERE id = ?", [$tagId] ); } else { // Create new tag $tagId = $this->db->insert('tags', [ 'user_id' => $userId, 'name' => $tagName, 'slug' => $slug, 'notes_count' => 1 ]); } $tagIds[] = $tagId; } // Associate tags with note foreach ($tagIds as $tagId) { $this->db->insert('note_tags', [ 'note_id' => $noteId, 'tag_id' => $tagId ]); } } /** * Update note tags */ private function updateTags($noteId, $userId, $newTags) { // Get current tags $currentTags = $this->db->getRows( "SELECT t.id, t.name FROM note_tags nt JOIN tags t ON nt.tag_id = t.id WHERE nt.note_id = ?", [$noteId] ); $currentTagIds = array_column($currentTags, 'id'); $currentTagNames = array_column($currentTags, 'name'); // Tags to remove $tagsToRemove = array_diff($currentTagNames, $newTags); foreach ($tagsToRemove as $tagName) { $tag = $this->db->getRow( "SELECT id FROM tags WHERE user_id = ? AND name = ?", [$userId, $tagName] ); if ($tag) { // Remove association $this->db->delete( 'note_tags', 'note_id = :note_id AND tag_id = :tag_id', ['note_id' => $noteId, 'tag_id' => $tag['id']] ); // Decrement tag count $this->db->query( "UPDATE tags SET notes_count = GREATEST(0, notes_count - 1) WHERE id = ?", [$tag['id']] ); } } // Tags to add $tagsToAdd = array_diff($newTags, $currentTagNames); foreach ($tagsToAdd as $tagName) { $tagName = trim($tagName); if (empty($tagName)) continue; $slug = createSlug($tagName); $tag = $this->db->getRow( "SELECT id FROM tags WHERE user_id = ? AND slug = ?", [$userId, $slug] ); if ($tag) { $tagId = $tag['id']; $this->db->query( "UPDATE tags SET notes_count = notes_count + 1 WHERE id = ?", [$tagId] ); } else { $tagId = $this->db->insert('tags', [ 'user_id' => $userId, 'name' => $tagName, 'slug' => $slug, 'notes_count' => 1 ]); } $this->db->insert('note_tags', [ 'note_id' => $noteId, 'tag_id' => $tagId ]); } } /** * Delete note (soft delete) */ public function delete($noteId, $userId) { try { $note = $this->getNoteById($noteId, $userId); if (!$note) { return ['success' => false, 'error' => 'Note not found']; } // Soft delete $this->db->update( 'notes', [ 'is_trashed' => true, 'deleted_at' => date('Y-m-d H:i:s') ], 'id = :id', ['id' => $noteId] ); logAudit($userId, 'delete_note', 'Moved note to trash', ['note_id' => $noteId]); return ['success' => true, 'message' => 'Note moved to trash']; } catch (Exception $e) { logError('Delete note error: ' . $e->getMessage()); return ['success' => false, 'error' => 'Failed to delete note']; } } /** * Permanently delete note */ public function permanentlyDelete($noteId, $userId) { try { $note = $this->getNoteById($noteId, $userId); if (!$note) { return ['success' => false, 'error' => 'Note not found']; } $this->db->beginTransaction(); // Delete attachments $attachments = $this->db->getRows( "SELECT * FROM attachments WHERE note_id = ?", [$noteId] ); foreach ($attachments as $attachment) { $filePath = UPLOAD_DIR . 'attachments/' . $attachment['file_path']; deleteFile($filePath, $userId, $attachment['file_size']); $this->db->delete('attachments', 'id = :id', ['id' => $attachment['id']]); } // Update category notes count if ($note['category_id']) { $this->db->query( "UPDATE categories SET notes_count = GREATEST(0, notes_count - 1) WHERE id = ?", [$note['category_id']] ); } // Update user notes count $this->db->query( "UPDATE users SET notes_count = GREATEST(0, notes_count - 1) WHERE id = ?", [$userId] ); // Delete note $this->db->delete('notes', 'id = :id', ['id' => $noteId]); $this->db->commit(); logAudit($userId, 'permanent_delete', 'Permanently deleted note', ['note_id' => $noteId]); return ['success' => true, 'message' => 'Note permanently deleted']; } catch (Exception $e) { $this->db->rollback(); logError('Permanent delete error: ' . $e->getMessage()); return ['success' => false, 'error' => 'Failed to delete note']; } } /** * Restore note from trash */ public function restore($noteId, $userId) { try { $this->db->update( 'notes', [ 'is_trashed' => false, 'deleted_at' => null ], 'id = :id', ['id' => $noteId] ); logAudit($userId, 'restore_note', 'Restored note from trash', ['note_id' => $noteId]); return ['success' => true, 'message' => 'Note restored successfully']; } catch (Exception $e) { logError('Restore note error: ' . $e->getMessage()); return ['success' => false, 'error' => 'Failed to restore note']; } } /** * Toggle favorite status */ public function toggleFavorite($noteId, $userId) { $note = $this->getNoteById($noteId, $userId); if (!$note) { return ['success' => false, 'error' => 'Note not found']; } $newStatus = !$note['is_favorite']; $this->db->update( 'notes', ['is_favorite' => $newStatus], 'id = :id', ['id' => $noteId] ); $message = $newStatus ? 'Note added to favorites' : 'Note removed from favorites'; return ['success' => true, 'is_favorite' => $newStatus, 'message' => $message]; } /** * Toggle archive status */ public function toggleArchive($noteId, $userId) { $note = $this->getNoteById($noteId, $userId); if (!$note) { return ['success' => false, 'error' => 'Note not found']; } $newStatus = !$note['is_archived']; $this->db->update( 'notes', ['is_archived' => $newStatus], 'id = :id', ['id' => $noteId] ); $message = $newStatus ? 'Note archived' : 'Note unarchived'; return ['success' => true, 'is_archived' => $newStatus, 'message' => $message]; } /** * Toggle pin status */ public function togglePin($noteId, $userId) { $note = $this->getNoteById($noteId, $userId); if (!$note) { return ['success' => false, 'error' => 'Note not found']; } $newStatus = !$note['is_pinned']; $this->db->update( 'notes', ['is_pinned' => $newStatus], 'id = :id', ['id' => $noteId] ); $message = $newStatus ? 'Note pinned' : 'Note unpinned'; return ['success' => true, 'is_pinned' => $newStatus, 'message' => $message]; } /** * Get note by ID */ public function getNoteById($noteId, $userId = null) { $sql = "SELECT n.*, c.name as category_name, c.color as category_color FROM notes n LEFT JOIN categories c ON n.category_id = c.id WHERE n.id = ?"; $params = [$noteId]; if ($userId) { $sql .= " AND n.user_id = ?"; $params[] = $userId; } return $this->db->getRow($sql, $params); } /** * Get note by UUID */ public function getNoteByUuid($uuid, $userId = null) { $sql = "SELECT n.*, c.name as category_name, c.color as category_color FROM notes n LEFT JOIN categories c ON n.category_id = c.id WHERE n.uuid = ?"; $params = [$uuid]; if ($userId) { $sql .= " AND n.user_id = ?"; $params[] = $userId; } return $this->db->getRow($sql, $params); } /** * Get note by slug */ public function getNoteBySlug($slug, $userId) { return $this->db->getRow( "SELECT n.*, c.name as category_name, c.color as category_color FROM notes n LEFT JOIN categories c ON n.category_id = c.id WHERE n.slug = ? AND n.user_id = ?", [$slug, $userId] ); } /** * Get user notes with filters */ public function getUserNotes($userId, $filters = []) { $sql = "SELECT n.*, c.name as category_name, c.color as category_color, (SELECT GROUP_CONCAT(t.name) FROM note_tags nt JOIN tags t ON nt.tag_id = t.id WHERE nt.note_id = n.id) as tags FROM notes n LEFT JOIN categories c ON n.category_id = c.id WHERE n.user_id = :user_id"; $params = ['user_id' => $userId]; // Apply filters if (!empty($filters['category_id'])) { $sql .= " AND n.category_id = :category_id"; $params['category_id'] = $filters['category_id']; } if (isset($filters['is_favorite'])) { $sql .= " AND n.is_favorite = :is_favorite"; $params['is_favorite'] = $filters['is_favorite']; } if (isset($filters['is_archived'])) { $sql .= " AND n.is_archived = :is_archived"; $params['is_archived'] = $filters['is_archived']; } if (isset($filters['is_trashed'])) { $sql .= " AND n.is_trashed = :is_trashed"; $params['is_trashed'] = $filters['is_trashed']; } else { $sql .= " AND n.is_trashed = 0"; } if (isset($filters['is_pinned'])) { $sql .= " AND n.is_pinned = :is_pinned"; $params['is_pinned'] = $filters['is_pinned']; } if (!empty($filters['tag'])) { $sql .= " AND n.id IN (SELECT note_id FROM note_tags nt JOIN tags t ON nt.tag_id = t.id WHERE t.name = :tag)"; $params['tag'] = $filters['tag']; } if (!empty($filters['search'])) { $sql .= " AND (n.title LIKE :search OR n.content LIKE :search OR n.content_plain LIKE :search)"; $params['search'] = '%' . $filters['search'] . '%'; } // Sorting $sort = $filters['sort'] ?? 'updated_at'; $order = $filters['order'] ?? 'DESC'; $allowedSorts = ['title', 'created_at', 'updated_at', 'views', 'word_count']; if (in_array($sort, $allowedSorts)) { $sql .= " ORDER BY n.{$sort} {$order}"; } else { $sql .= " ORDER BY n.is_pinned DESC, n.updated_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 notes count */ public function getNotesCount($userId, $filters = []) { $sql = "SELECT COUNT(*) FROM notes WHERE user_id = :user_id"; $params = ['user_id' => $userId]; if (!empty($filters['category_id'])) { $sql .= " AND category_id = :category_id"; $params['category_id'] = $filters['category_id']; } if (isset($filters['is_favorite'])) { $sql .= " AND is_favorite = :is_favorite"; $params['is_favorite'] = $filters['is_favorite']; } if (isset($filters['is_archived'])) { $sql .= " AND is_archived = :is_archived"; $params['is_archived'] = $filters['is_archived']; } if (isset($filters['is_trashed'])) { $sql .= " AND is_trashed = :is_trashed"; $params['is_trashed'] = $filters['is_trashed']; } else { $sql .= " AND is_trashed = 0"; } return $this->db->getValue($sql, $params); } /** * Search notes */ public function searchNotes($userId, $query) { return $this->db->getRows( "SELECT n.*, c.name as category_name, c.color as category_color, MATCH(n.title, n.content, n.content_plain) AGAINST(:query) as relevance FROM notes n LEFT JOIN categories c ON n.category_id = c.id WHERE n.user_id = :user_id AND n.is_trashed = 0 AND MATCH(n.title, n.content, n.content_plain) AGAINST(:query IN NATURAL LANGUAGE MODE) ORDER BY relevance DESC", [ 'user_id' => $userId, 'query' => $query ] ); } /** * Get note versions */ public function getVersions($noteId, $userId) { $note = $this->getNoteById($noteId, $userId); if (!$note) { return []; } return $this->db->getRows( "SELECT v.*, u.username as created_by_username FROM note_versions v LEFT JOIN users u ON v.created_by = u.id WHERE v.note_id = ? ORDER BY v.version DESC", [$noteId] ); } /** * Restore version */ public function restoreVersion($noteId, $version, $userId) { try { $version = $this->db->getRow( "SELECT * FROM note_versions WHERE note_id = ? AND version = ?", [$noteId, $version] ); if (!$version) { return ['success' => false, 'error' => 'Version not found']; } $this->db->beginTransaction(); // Save current version $note = $this->getNoteById($noteId, $userId); $this->saveVersion($noteId, $note['title'], $note['content'], $userId); // Restore version $this->db->update( 'notes', [ 'title' => $version['title'], 'content' => $version['content'], 'content_plain' => strip_tags($version['content']), 'word_count' => $version['word_count'], 'character_count' => $version['character_count'] ], 'id = :id', ['id' => $noteId] ); $this->db->commit(); logAudit($userId, 'restore_version', 'Restored note version', ['note_id' => $noteId, 'version' => $version['version']]); return ['success' => true, 'message' => 'Version restored successfully']; } catch (Exception $e) { $this->db->rollback(); logError('Restore version error: ' . $e->getMessage()); return ['success' => false, 'error' => 'Failed to restore version']; } } /** * Get note attachments */ public function getAttachments($noteId, $userId) { $note = $this->getNoteById($noteId, $userId); if (!$note) { return []; } return $this->db->getRows( "SELECT * FROM attachments WHERE note_id = ? ORDER BY created_at DESC", [$noteId] ); } /** * Get note reminders */ public function getReminders($noteId, $userId) { $note = $this->getNoteById($noteId, $userId); if (!$note) { return []; } return $this->db->getRows( "SELECT * FROM reminders WHERE note_id = ? ORDER BY reminder_time ASC", [$noteId] ); } /** * Increment view count */ public function incrementViewCount($noteId) { $this->db->query( "UPDATE notes SET views = views + 1 WHERE id = ?", [$noteId] ); } /** * Duplicate note */ public function duplicate($noteId, $userId) { try { $note = $this->getNoteById($noteId, $userId); if (!$note) { return ['success' => false, 'error' => 'Note not found']; } $this->db->beginTransaction(); // Create duplicate $duplicateData = [ 'title' => $note['title'] . ' (Copy)', 'content' => $note['content'], 'category_id' => $note['category_id'], 'editor_type' => $note['editor_type'], 'color' => $note['color'], 'icon' => $note['icon'] ]; $result = $this->create($userId, $duplicateData); if (!$result['success']) { throw new Exception('Failed to duplicate note'); } // Duplicate tags $tags = $this->db->getRows( "SELECT t.name FROM note_tags nt JOIN tags t ON nt.tag_id = t.id WHERE nt.note_id = ?", [$noteId] ); if (!empty($tags)) { $tagNames = array_column($tags, 'name'); $this->addTags($result['note_id'], $userId, $tagNames); } $this->db->commit(); logAudit($userId, 'duplicate_note', 'Duplicated note', ['original_note_id' => $noteId, 'new_note_id' => $result['note_id']]); return ['success' => true, 'note_id' => $result['note_id'], 'message' => 'Note duplicated successfully']; } catch (Exception $e) { $this->db->rollback(); logError('Duplicate note error: ' . $e->getMessage()); return ['success' => false, 'error' => 'Failed to duplicate note']; } } /** * Export note */ public function exportNote($noteId, $userId, $format = 'html') { $note = $this->getNoteById($noteId, $userId); if (!$note) { return ['success' => false, 'error' => 'Note not found']; } switch ($format) { case 'html': return $this->exportToHtml($note); case 'markdown': return $this->exportToMarkdown($note); case 'text': return $this->exportToText($note); case 'pdf': return $this->exportToPdf($note); default: return ['success' => false, 'error' => 'Invalid export format']; } } /** * Export to HTML */ private function exportToHtml($note) { $html = '<!DOCTYPE html>'; $html .= '<html><head><title>' . htmlspecialchars($note['title']) . '</title>'; $html .= '<meta charset="utf-8"></head><body>'; $html .= '<h1>' . htmlspecialchars($note['title']) . '</h1>'; $html .= '<p><small>Created: ' . $note['created_at'] . ' | Last updated: ' . $note['updated_at'] . '</small></p>'; $html .= '<hr>'; $html .= $note['content']; $html .= '</body></html>'; $filename = 'note_' . $note['id'] . '_' . date('Ymd') . '.html'; $filepath = UPLOAD_DIR . 'exports/' . $filename; file_put_contents($filepath, $html); return [ 'success' => true, 'filename' => $filename, 'filepath' => $filepath, 'url' => SITE_URL . '/uploads/exports/' . $filename ]; } /** * Export to Markdown */ private function exportToMarkdown($note) { $content = '# ' . $note['title'] . "\n\n"; $content .= '*Created: ' . $note['created_at'] . ' | Last updated: ' . $note['updated_at'] . "*\n\n"; $content .= '---' . "\n\n"; // Convert HTML to Markdown (simplified) $markdown = strip_tags($note['content']); $content .= $markdown; $filename = 'note_' . $note['id'] . '_' . date('Ymd') . '.md'; $filepath = UPLOAD_DIR . 'exports/' . $filename; file_put_contents($filepath, $content); return [ 'success' => true, 'filename' => $filename, 'filepath' => $filepath, 'url' => SITE_URL . '/uploads/exports/' . $filename ]; } /** * Export to plain text */ private function exportToText($note) { $content = $note['title'] . "\n"; $content .= str_repeat('=', strlen($note['title'])) . "\n\n"; $content .= 'Created: ' . $note['created_at'] . "\n"; $content .= 'Last updated: ' . $note['updated_at'] . "\n\n"; $content .= strip_tags($note['content']); $filename = 'note_' . $note['id'] . '_' . date('Ymd') . '.txt'; $filepath = UPLOAD_DIR . 'exports/' . $filename; file_put_contents($filepath, $content); return [ 'success' => true, 'filename' => $filename, 'filepath' => $filepath, 'url' => SITE_URL . '/uploads/exports/' . $filename ]; } /** * Export to PDF (placeholder) */ private function exportToPdf($note) { // This would use TCPDF or similar library // Placeholder for PDF export return ['success' => false, 'error' => 'PDF export not implemented']; } } ?> Frontend Pages
Main Landing Page
File: index.php
<?php require_once 'includes/config.php'; // Redirect to dashboard if already logged in if ($auth->isLoggedIn()) { redirect('/user/dashboard.php'); } // Get stats $totalUsers = $db->getValue("SELECT COUNT(*) FROM users WHERE status = 'active'"); $totalNotes = $db->getValue("SELECT COUNT(*) FROM notes WHERE is_trashed = 0"); $totalPublicNotes = $db->getValue("SELECT COUNT(*) FROM notes WHERE is_public = 1 AND is_trashed = 0"); ?> <!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; ?> - Your Personal Notes Taking App</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="A powerful, easy-to-use notes taking application to capture your ideas, organize your thoughts, and boost your productivity."> <meta name="keywords" content="notes, note taking, productivity, organization, ideas"> </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-sticky-note 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="#features">Features</a> </li> <li class="nav-item"> <a class="nav-link" href="#pricing">Pricing</a> </li> <li class="nav-item"> <a class="nav-link" href="#about">About</a> </li> <li class="nav-item"> <a class="btn btn-outline-primary me-2" href="login.php">Login</a> </li> <li class="nav-item"> <a class="btn btn-primary" href="register.php">Sign Up Free</a> </li> </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">Capture Your Ideas, Organize Your Thoughts</h1> <p class="lead mb-4">The most intuitive and powerful notes taking app to help you stay organized and boost your productivity.</p> <div class="d-flex gap-3"> <a href="register.php" class="btn btn-light btn-lg px-4"> <i class="fas fa-user-plus me-2"></i>Get Started Free </a> <a href="#features" class="btn btn-outline-light btn-lg px-4"> Learn More </a> </div> <div class="mt-4"> <div class="row text-center"> <div class="col-4"> <h3 class="fw-bold"><?php echo number_format($totalUsers); ?></h3> <small>Happy Users</small> </div> <div class="col-4"> <h3 class="fw-bold"><?php echo number_format($totalNotes); ?></h3> <small>Notes Created</small> </div> <div class="col-4"> <h3 class="fw-bold"><?php echo number_format($totalPublicNotes); ?></h3> <small>Public Notes</small> </div> </div> </div> </div> <div class="col-lg-6 text-center"> <img src="assets/images/hero-notes.svg" alt="Notes App" class="img-fluid"> </div> </div> </div> </section> <!-- Features Section --> <section id="features" class="py-5"> <div class="container"> <h2 class="text-center fw-bold mb-5">Why Choose Our Notes App?</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>Rich Text Editing</h4> <p class="text-muted">Format your notes with bold, italic, lists, headings, and more using our powerful editor.</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-folder-tree"></i> </div> <h4>Organize with Categories</h4> <p class="text-muted">Keep your notes organized with custom categories, tags, and folders.</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-search"></i> </div> <h4>Powerful Search</h4> <p class="text-muted">Find any note instantly with our full-text search across titles and content.</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-cloud-upload-alt"></i> </div> <h4>Cloud Sync</h4> <p class="text-muted">Access your notes from anywhere, on any device. Your data is always in sync.</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 Sharing</h4> <p class="text-muted">Share your notes with others, collaborate, and get feedback.</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">Your notes are encrypted and secure. We take your privacy seriously.</p> </div> </div> </div> </div> </section> <!-- How It Works --> <section 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 in less than a minute.</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-pen fa-3x text-primary mb-3"></i> <h4>Start Writing</h4> <p class="text-muted">Create notes, organize them into categories, and add tags.</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-tachometer-alt fa-3x text-primary mb-3"></i> <h4>Stay Productive</h4> <p class="text-muted">Access your notes anywhere, set reminders, and never forget an idea.</p> </div> </div> </div> </div> </section> <!-- Pricing Section --> <section id="pricing" class="py-5"> <div class="container"> <h2 class="text-center fw-bold mb-5">Simple, Transparent Pricing</h2> <div class="row justify-content-center"> <div class="col-md-4 mb-4"> <div class="card h-100 border-0 shadow-sm"> <div class="card-header bg-white text-center py-4"> <h4 class="mb-0">Basic</h4> <p class="display-4 fw-bold text-primary">$0</p> <p class="text-muted">Free forever</p> </div> <div class="card-body"> <ul class="list-unstyled"> <li class="mb-3"><i class="fas fa-check text-success me-2"></i>100 MB storage</li> <li class="mb-3"><i class="fas fa-check text-success me-2"></i>Rich text editing</li> <li class="mb-3"><i class="fas fa-check text-success me-2"></i>Categories & tags</li> <li class="mb-3"><i class="fas fa-check text-success me-2"></i>Search notes</li> <li class="mb-3"><i class="fas fa-check text-success me-2"></i>Export to text/HTML</li> <li class="mb-3 text-muted"><i class="fas fa-times text-danger me-2"></i>Version history</li> <li class="mb-3 text-muted"><i class="fas fa-times text-danger me-2"></i>File attachments</li> </ul> </div> <div class="card-footer bg-white border-0 pb-4 text-center"> <a href="register.php" class="btn btn-outline-primary btn-lg">Get Started</a> </div> </div> </div> <div class="col-md-4 mb-4"> <div class="card h-100 border-0 shadow-lg featured"> <div class="card-header bg-primary text-white text-center py-4"> <span class="badge bg-warning position-absolute top-0 end-0 m-3">Popular</span> <h4 class="mb-0">Premium</h4> <p class="display-4 fw-bold">$4.99</p> <p class="mb-0">per month</p> </div> <div class="card-body"> <ul class="list-unstyled"> <li class="mb-3"><i class="fas fa-check text-success me-2"></i>1 GB storage</li> <li class="mb-3"><i class="fas fa-check text-success me-2"></i>All Basic features</li> <li class="mb-3"><i class="fas fa-check text-success me-2"></i>30-day version history</li> <li class="mb-3"><i class="fas fa-check text-success me-2"></i>File attachments</li> <li class="mb-3"><i class="fas fa-check text-success me-2"></i>Export to PDF</li> <li class="mb-3"><i class="fas fa-check text-success me-2"></i>Priority support</li> <li class="mb-3"><i class="fas fa-check text-success me-2"></i>No ads</li> </ul> </div> <div class="card-footer bg-white border-0 pb-4 text-center"> <a href="register.php?plan=premium" class="btn btn-primary btn-lg">Upgrade Now</a> </div> </div> </div> </div> </div> </section> <!-- Testimonials --> <section class="py-5 bg-light"> <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 notes app has completely transformed how I organize my thoughts. The rich editor and category system are perfect for my needs."</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">Writer</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 it for everything - meeting notes, project planning, and personal journaling. The search feature is incredibly fast and accurate."</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">Michael Chen</h6> <small class="text-muted">Product 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-half-alt"></i> </div> <p class="card-text">"The premium features are worth every penny. Version history has saved me multiple times, and the PDF export is perfect for sharing."</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">Researcher</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 Taking Better Notes?</h2> <p class="lead mb-4">Join thousands of happy users who have already organized their digital lives.</p> <a href="register.php" class="btn btn-light btn-lg px-5"> <i class="fas fa-rocket me-2"></i>Get Started Free </a> </div> </section> <!-- Footer --> <footer id="about" 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-sticky-note me-2"></i><?php echo SITE_NAME; ?></h5> <p class="text-white-50">The most intuitive notes taking app to capture your ideas and boost your productivity.</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="#features" class="text-white-50">Features</a></li> <li><a href="#pricing" class="text-white-50">Pricing</a></li> <li><a href="#about" class="text-white-50">About</a></li> </ul> </div> <div class="col-md-3 mb-4"> <h6>Legal</h6> <ul class="list-unstyled"> <li><a href="#" class="text-white-50">Terms of Service</a></li> <li><a href="#" class="text-white-50">Privacy Policy</a></li> <li><a href="#" class="text-white-50">Cookie Policy</a></li> <li><a href="#" class="text-white-50">GDPR</a></li> </ul> </div> <div class="col-md-3 mb-4"> <h6>Contact</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">© <?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; } .navbar { padding: 1rem 0; } .hero-section { margin-top: 76px; } .featured { transform: scale(1.05); z-index: 1; } </style> </body> </html> Environment Configuration
File: .env
# Database Configuration DB_HOST=localhost DB_NAME=notes_app DB_USER=root DB_PASS= # Application Configuration SITE_NAME=Notes App SITE_URL=http://localhost/notes-app [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 ALLOWED_FILE_TYPES=jpg,jpeg,png,gif,pdf,doc,docx,txt,md # Storage Limits (in bytes) BASIC_STORAGE_LIMIT=104857600 # 100 MB PREMIUM_STORAGE_LIMIT=1073741824 # 1 GB # 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=basic # Feature Flags ENABLE_RICH_EDITOR=true ENABLE_MARKDOWN=true ENABLE_ATTACHMENTS=true ENABLE_SHARING=true ENABLE_REMINDERS=true # 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=Notes App
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/note-images/.gitkeep !/uploads/attachments/.gitkeep !/uploads/exports/.gitkeep # Cache /cache/ !/cache/.gitkeep # Composer composer.lock # Temp files *.tmp *.temp
File: composer.json
{ "name": "notes-app/application", "description": "Notes Taking Application", "type": "project", "require": { "php": ">=7.4", "ext-pdo": "*", "ext-json": "*", "ext-mbstring": "*", "ext-curl": "*", "ext-gd": "*", "phpmailer/phpmailer": "^6.8", "tecnickcom/tcpdf": "^6.6", "erusev/parsedown": "^1.7", "intervention/image": "^2.7" }, "require-dev": { "phpunit/phpunit": "^9.0" }, "autoload": { "psr-4": { "NotesApp\\": "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
- Web Server: XAMPP, WAMP, MAMP, or any PHP-enabled server (PHP 7.4+)
- Database: MySQL 5.7+ or MariaDB
- Composer: For dependency management
- Browser: Modern web browser (Chrome, Firefox, Edge, etc.)
Installation Steps
Step 1: Set Up Local Server
- Download and install XAMPP from https://www.apachefriends.org/
- Launch XAMPP Control Panel
- Start Apache and MySQL services
Step 2: Install Composer Dependencies
- Download and install Composer from https://getcomposer.org/
- Navigate to your project directory in terminal
- Run:
composer install
Step 3: Create Project Folder
- Navigate to
C:\xampp\htdocs\(Windows) or/Applications/XAMPP/htdocs/(Mac) - Create a new folder named
notes-app - Create all the folders and files as shown in the Project File Structure
Step 4: Set Up Database
- Open browser and go to
http://localhost/phpmyadmin - Click on "New" to create a new database
- Name the database
notes_appand selectutf8mb4_general_ci - Click on "Import" tab
- Click "Choose File" and select the
database.sqlfile from thesqlfolder - Click "Go" to import the database structure and sample data
Step 5: Configure Environment
- Rename
.env.exampleto.envin the project root - Update database credentials:
DB_HOST=localhost DB_NAME=notes_app DB_USER=root DB_PASS=
- Update application URL:
SITE_URL=http://localhost/notes-app
- Set a secure pepper string:
PEPPER=your-random-secure-string-here-min-32-characters
- Configure email settings if using email notifications
Step 6: Set Folder Permissions
Create the following folders and ensure they are writable:
uploads/avatars/uploads/note-images/uploads/attachments/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
- 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]); ?>
- Copy the generated hash
- Open phpMyAdmin, go to the
userstable - Insert or update the admin user with the hash
Step 8: Test the Installation
- Open browser and go to
http://localhost/notes-app/ - You should see the landing page
- Test different user types: Admin Login:
- Username:
admin(or email:[email protected]) - Password:
Admin@123(or the password you set) Regular User Registration: - Click "Sign Up Free" and register
- Verify email (if enabled)
- Login and start creating notes
System Walkthrough
For Regular Users:
- Dashboard - Overview of your notes and recent activity
- Notes - View all notes with filtering options
- Create Note - Create new notes with rich text editor
- Categories - Manage categories/folders
- Tags - Manage tags for better organization
- Favorites - View favorite notes
- Archive - View archived notes
- Trash - Recover deleted notes
- Search - Search across all notes
- Profile - Update personal information and settings
- Upgrade - Upgrade to premium plan
For Premium Users (additional features):
- Version History - View and restore previous versions
- Attachments - Upload and manage file attachments
- Export Options - Export notes to PDF
- Reminders - Set reminders on notes
For Admins:
- Dashboard - System-wide statistics and overview
- User Management - Manage users, roles, and permissions
- Storage Stats - Monitor storage usage
- System Settings - Configure system parameters
- Backup - Database backup and restore
- Logs - View system and error logs
- Analytics - View usage analytics
Key Features Explained
Rich Text Editor
The application uses TinyMCE or CKEditor for rich text editing with features:
- Text formatting: Bold, italic, underline, strikethrough
- Headings: H1, H2, H3
- Lists: Ordered and unordered
- Links: Insert and edit hyperlinks
- Images: Upload and embed images
- Tables: Create and edit tables
- Code blocks: For code snippets
- Blockquotes: For quotations
- Undo/Redo: Full history support
Organization System
- Categories: Create hierarchical folders to organize notes
- Tags: Add multiple tags to notes for cross-categorization
- Favorites: Mark important notes for quick access
- Pinned: Keep important notes at the top
- Archive: Move old notes out of the way without deleting
- Trash: Recover deleted notes within 30 days
Search Functionality
- Full-text search: Search across titles and content
- Filter by category: Narrow down by category
- Filter by tags: Find notes with specific tags
- Filter by date: Search by creation or update date
- Filter by status: Search in favorites, archive, or trash
Sharing (Premium)
- Share with users: Share notes with other registered users
- Public links: Generate public shareable links
- Permissions: View, comment, or edit permissions
- Expiration: Set expiration dates for shared links
- Password protection: Protect shared notes with password
Version History (Premium)
- Auto-save: Versions saved every 5 minutes
- Manual saves: Save versions manually
- 30-day history: Access versions from last 30 days
- Compare: Compare different versions
- Restore: Restore any previous version
Attachments (Premium)
- File uploads: Attach files to notes
- Image gallery: View all images in a note
- File types: PDF, DOC, DOCX, images, and more
- Storage tracking: Monitor storage usage
- Download: Download attachments
Reminders (Premium)
- Set reminders: Add reminders to notes
- Due dates: Set specific dates and times
- Recurring: Daily, weekly, monthly, yearly
- Email notifications: Get email reminders
- Push notifications: Browser push notifications
Troubleshooting
Common Issues and Solutions
- Database Connection Error
- Check if MySQL is running
- Verify database credentials in
.env - Ensure database
notes_appexists
- 404 Page Not Found
- Check file paths and folder structure
- Verify
SITE_URLin.env - Ensure
.htaccessis properly configured (if using Apache)
- Upload Issues
- Check folder permissions (755 or 777)
- Verify
MAX_FILE_SIZEin.env - Check allowed file extensions
- Email Not Sending
- Configure SMTP settings correctly
- Check spam folder
- Verify PHP mail() function is enabled
- Session/Login Issues
- Clear browser cookies and cache
- Check
SESSION_TIMEOUTin.env - Verify session save path is writable
- Rich Editor Not Loading
- Check TinyMCE/CKEditor paths
- Verify JavaScript console for errors
- Ensure editor files are in
assets/plugins/
- Search Not Working
- Verify MySQL FULLTEXT indexes
- Check if InnoDB engine is used
- Ensure tables are using utf8mb4
Performance Optimizations
- Database Indexing on frequently queried columns
- Query Caching for repeated requests
- Image Optimization for uploads
- Lazy Loading for images and content
- Pagination for large datasets
- Minified CSS and JavaScript for production
- CDN for static assets
- Browser Caching headers
- Gzip Compression for faster loading
Security Best Practices
- Change default admin password immediately after installation
- Use HTTPS in production with SSL certificate
- Regular backups of database and uploads
- Input validation on both client and server side
- SQL injection prevention using prepared statements
- XSS prevention using
htmlspecialchars() - CSRF tokens for all forms
- Password hashing with bcrypt and pepper
- Rate limiting for login attempts
- Session security with proper timeout
- File upload validation and malware scanning
- CSP headers for additional security
Deployment to Production
- Update
.envwith production settings - Set DEBUG_MODE=false
- Configure proper error logging
- Set up SSL certificate for HTTPS
- Configure cron jobs for maintenance tasks:
- Daily backups
- Clean up expired sessions
- Send reminder emails
- Delete old trashed notes
- Set up database backups regularly
- Enable CDN for static assets
- Configure caching (Redis/Memcached)
- Set up monitoring and alerts
- Configure firewall and security rules
Conclusion
The Notes Taking App with Database is a comprehensive, feature-rich application for capturing, organizing, and managing digital notes. With its intuitive interface, powerful rich text editor, robust organization system, and premium features, it provides everything needed for effective note-taking and information management.
This application demonstrates:
- Secure user authentication with password hashing and pepper
- CRUD operations for notes, categories, and tags
- Rich text editing with TinyMCE/CKEditor
- Full-text search capabilities
- File upload and attachment management
- Version history for premium users
- Sharing and collaboration features
- Reminders and notifications
- Responsive design for all devices
- Modular code structure following OOP principles
- Database design with proper relationships and indexing
- Security best practices throughout
- Premium tier with additional features
The system is built to be extensible, allowing easy addition of new features like collaborative editing, mobile apps, API integration, and more. Whether you're a student taking lecture notes, a professional organizing project ideas, a writer collecting research, or a team collaborating on documents, this notes app 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 personal and professional note-taking for years to come.