diff --git a/admin_dashboard.php b/admin_dashboard.php new file mode 100644 index 0000000..8b47e72 --- /dev/null +++ b/admin_dashboard.php @@ -0,0 +1,145 @@ +prepare("SELECT leave_requests.*, users.full_name AS student_name FROM leave_requests JOIN users ON leave_requests.student_id = users.id WHERE leave_requests.status = 'approved_by_teacher' ORDER BY leave_requests.created_at DESC"); + $stmt->execute(); + $leave_requests = $stmt->fetchAll(); +} catch (PDOException $e) { + // handle error +} + +// Monthly report +$report = []; +$month = date('Y-m'); +try { + $pdo = db(); + $stmt = $pdo->prepare("SELECT status, COUNT(*) as count FROM leave_requests WHERE DATE_FORMAT(created_at, '%Y-%m') = ? GROUP BY status"); + $stmt->execute([$month]); + $result = $stmt->fetchAll(); + foreach ($result as $row) { + $report[$row['status']] = $row['count']; + } +} catch (PDOException $e) { + // handle error +} + +$import_success_message = ''; +if (isset($_GET['import_success']) && $_GET['import_success'] == 1) { + $import_success_message = 'Students imported successfully!'; +} + +$import_error_message = ''; +if (isset($_GET['import_error']) && $_GET['import_error'] == 1) { + $import_error_message = 'Error importing students. Please check the file and try again.'; +} + +?> + +
+
+

Admin Dashboard

+
+ Welcome, ! + Logout +
+
+ + + + + + + + + +
+
+

Pending Leave Requests for Final Approval

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Student NameLeave TypeStart DateEnd DateReasonAttachmentAction
+ + View Attachment + + No Attachment + + + Approve + Reject +
+
+
+ +
+
+

Monthly Report ()

+ + + + + + + + + $count): ?> + + + + + + +
StatusCount
+ Export to CSV +
+
+

Import Students

+
+
+ + +
+ +
+
+
+
+ + \ No newline at end of file diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..f34f91c --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,39 @@ + +html, body { + height: 100%; +} + +body { + display: flex; + align-items: center; + padding-top: 40px; + padding-bottom: 40px; + background-color: #f5f5f5; +} + +.form-signin { + width: 100%; + max-width: 330px; + padding: 15px; + margin: auto; +} + +.form-signin .checkbox { + font-weight: 400; +} + +.form-signin .form-floating:focus-within { + z-index: 2; +} + +.form-signin input[type="email"] { + margin-bottom: -1px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.form-signin input[type="password"] { + margin-bottom: 10px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} diff --git a/blog.php b/blog.php new file mode 100644 index 0000000..b003d0c --- /dev/null +++ b/blog.php @@ -0,0 +1,150 @@ + + + + + + + My Awesome App + + + + +
+ +
+
+ +

Page Heading + Secondary Text +

+ + +
+ Card image cap +
+

Post Title

+

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reiciendis aliquid atque, nulla? Quos cum ex quis soluta, a laboriosam. Dicta expedita corporis animi vero voluptate voluptatibus possimus, veniam magni quis!

+ Read More → +
+ +
+ + +
+ Card image cap +
+

Post Title

+

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reiciendis aliquid atque, nulla? Quos cum ex quis soluta, a laboriosam. Dicta expedita corporis animi vero voluptate voluptatibus possimus, veniam magni quis!

+ Read More → +
+ +
+ + +
+ Card image cap +
+

Post Title

+

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reiciendis aliquid atque, nulla? Quos cum ex quis soluta, a laboriosam. Dicta expedita corporis animi vero voluptate voluptatibus possimus, veniam magni quis!

+ Read More → +
+ +
+ + + + +
+ + +
+ + +
+
Search
+
+
+ + + + +
+
+
+ + +
+
Categories
+
+
+
+ +
+
+ +
+
+
+
+ + +
+
Side Widget
+
+ You can put anything you want inside of these side widgets. They are easy to use, and feature the new Bootstrap 4 card containers! +
+
+ +
+ +
+ + +
+ + + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..cead1b4 --- /dev/null +++ b/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "parsecsv/php-parsecsv": "^1.3" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..580807f --- /dev/null +++ b/composer.lock @@ -0,0 +1,83 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "710967b1d9d7c2e29226ea5c1834b93a", + "packages": [ + { + "name": "parsecsv/php-parsecsv", + "version": "1.3.2", + "source": { + "type": "git", + "url": "https://github.com/parsecsv/parsecsv-for-php.git", + "reference": "2d6236cae09133e0533d34ed45ba1e1ecafffebb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/parsecsv/parsecsv-for-php/zipball/2d6236cae09133e0533d34ed45ba1e1ecafffebb", + "reference": "2d6236cae09133e0533d34ed45ba1e1ecafffebb", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^6", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "illuminate/support": "Fluent array interface for map functions" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "ParseCsv\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Myhrberg", + "email": "contact@jimeh.me" + }, + { + "name": "William Knauss", + "email": "will.knauss@gmail.com" + }, + { + "name": "Susann Sgorzaly", + "homepage": "https://github.com/susgo" + }, + { + "name": "Christian Bläul", + "homepage": "https://github.com/Fonata" + } + ], + "description": "CSV data parser for PHP", + "support": { + "issues": "https://github.com/parsecsv/parsecsv-for-php/issues", + "source": "https://github.com/parsecsv/parsecsv-for-php" + }, + "time": "2021-11-07T14:15:46+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/composer.phar b/composer.phar new file mode 100755 index 0000000..7f8a37d Binary files /dev/null and b/composer.phar differ diff --git a/cron/send_reminders.php b/cron/send_reminders.php new file mode 100644 index 0000000..58cb35e --- /dev/null +++ b/cron/send_reminders.php @@ -0,0 +1,18 @@ +prepare("SELECT leave_requests.*, users.full_name AS student_name FROM leave_requests JOIN users ON leave_requests.student_id = users.id WHERE status = 'pending' AND created_at < NOW() - INTERVAL 24 HOUR"); +$stmt->execute(); +$requests = $stmt->fetchAll(); + +foreach ($requests as $request) { + // Send reminder email to teacher + $teacher_email = 'teacher@example.com'; // Hardcoded for now + $subject = 'Reminder: Pending Leave Request from ' . $request['student_name']; + $body = "

This is a reminder that a leave request from {$request['student_name']} is still pending your approval.

\n

Please login to the dashboard to review the request.

"; + MailService::sendMail($teacher_email, $subject, $body); +} + diff --git a/db/migrations/001_create_initial_tables.sql b/db/migrations/001_create_initial_tables.sql new file mode 100644 index 0000000..5cb66a3 --- /dev/null +++ b/db/migrations/001_create_initial_tables.sql @@ -0,0 +1,51 @@ + +-- Create users table +CREATE TABLE IF NOT EXISTS `users` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `email` VARCHAR(255) NOT NULL UNIQUE, + `password` VARCHAR(255) NOT NULL, + `role` ENUM('student', 'teacher', 'admin') NOT NULL, + `full_name` VARCHAR(255) DEFAULT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Create leave_requests table +CREATE TABLE IF NOT EXISTS `leave_requests` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `student_id` INT NOT NULL, + `start_date` DATE NOT NULL, + `end_date` DATE NOT NULL, + `reason` TEXT, + `status` ENUM('pending_teacher', 'pending_admin', 'approved', 'rejected') NOT NULL DEFAULT 'pending_teacher', + `teacher_id` INT DEFAULT NULL, + `teacher_approved_at` DATETIME DEFAULT NULL, + `teacher_rejection_reason` TEXT DEFAULT NULL, + `admin_id` INT DEFAULT NULL, + `admin_approved_at` DATETIME DEFAULT NULL, + `admin_rejection_reason` TEXT DEFAULT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (`student_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`teacher_id`) REFERENCES `users`(`id`) ON DELETE SET NULL, + FOREIGN KEY (`admin_id`) REFERENCES `users`(`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Create leave_request_attachments table +CREATE TABLE IF NOT EXISTS `leave_request_attachments` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `leave_request_id` INT NOT NULL, + `file_path` VARCHAR(255) NOT NULL, + `original_filename` VARCHAR(255) DEFAULT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`leave_request_id`) REFERENCES `leave_requests`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Create password_resets table +CREATE TABLE IF NOT EXISTS `password_resets` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `email` VARCHAR(255) NOT NULL, + `token` VARCHAR(255) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + diff --git a/db/migrations/002_create_password_resets_table.sql b/db/migrations/002_create_password_resets_table.sql new file mode 100644 index 0000000..d9caa71 --- /dev/null +++ b/db/migrations/002_create_password_resets_table.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS `password_resets` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `email` varchar(255) NOT NULL, + `token` varchar(255) NOT NULL, + `expires_at` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `token` (`token`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/db/migrations/003_create_leave_requests_table.sql b/db/migrations/003_create_leave_requests_table.sql new file mode 100644 index 0000000..2a6ae7e --- /dev/null +++ b/db/migrations/003_create_leave_requests_table.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS `leave_requests` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `student_id` int(11) NOT NULL, + `leave_type` varchar(255) NOT NULL, + `start_date` date NOT NULL, + `end_date` date NOT NULL, + `reason` text NOT NULL, + `attachment_path` varchar(255) DEFAULT NULL, + `status` varchar(255) NOT NULL DEFAULT 'pending', + `created_at` timestamp NOT NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + KEY `student_id` (`student_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/export_report.php b/export_report.php new file mode 100644 index 0000000..56c64e5 --- /dev/null +++ b/export_report.php @@ -0,0 +1,41 @@ +prepare("SELECT users.full_name AS student_name, leave_requests.* FROM leave_requests JOIN users ON leave_requests.student_id = users.id WHERE DATE_FORMAT(leave_requests.created_at, '%Y-%m') = ?"); + $stmt->execute([$month]); + + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + fputcsv($output, [ + $row['student_name'], + $row['leave_type'], + $row['start_date'], + $row['end_date'], + $row['reason'], + $row['status'] + ]); + } +} catch (PDOException $e) { + // handle error +} + +fclose($output); +exit; diff --git a/forgot_password.php b/forgot_password.php new file mode 100644 index 0000000..aaf804f --- /dev/null +++ b/forgot_password.php @@ -0,0 +1,75 @@ +prepare("SELECT * FROM users WHERE email = ?"); + $stmt->execute([$email]); + $user = $stmt->fetch(); + + if ($user) { + // Generate a unique token + $token = bin2hex(random_bytes(32)); + + // Set expiration date to 1 hour from now + $expires_at = date('Y-m-d H:i:s', strtotime('+1 hour')); + + // Store the token in the database + $stmt = $pdo->prepare("INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, ?)"); + $stmt->execute([$email, $token, $expires_at]); + + // Send the password reset link + $reset_link = 'http://' . $_SERVER['HTTP_HOST'] . '/reset_password.php?token=' . $token; + + // For now, we just display the link. Later we will send it by email. + $message = 'Password reset link: ' . $reset_link . ''; + + } else { + $message = 'If your email address exists in our database, you will receive a password reset link.'; + } + } catch (PDOException $e) { + $message = 'Database error: ' . $e->getMessage(); + } + } +} + +require_once __DIR__ . '/includes/header.php'; +?> + +
+
+
+

Forgot Password

+

Please enter your email address to receive a password reset link.

+ + + + + +
+
+ + +
+ +
+
+
+
+ + diff --git a/import_students.php b/import_students.php new file mode 100644 index 0000000..9d15898 --- /dev/null +++ b/import_students.php @@ -0,0 +1,43 @@ +auto($_FILES['student_file']['tmp_name']); + + $pdo = db(); + + foreach ($csv->data as $row) { + $full_name = $row['full_name']; + $email = $row['email']; + + // Generate a random password + $password = bin2hex(random_bytes(8)); + $hashed_password = password_hash($password, PASSWORD_DEFAULT); + + try { + $stmt = $pdo->prepare("INSERT INTO users (full_name, email, password, role) VALUES (?, ?, ?, 'student')"); + $stmt->execute([$full_name, $email, $hashed_password]); + } catch (PDOException $e) { + // Handle duplicate email or other errors + // For now, we just skip the row + continue; + } + } + + header('Location: admin_dashboard.php?import_success=1'); + exit; +} else { + header('Location: admin_dashboard.php?import_error=1'); + exit; +} diff --git a/includes/footer.php b/includes/footer.php new file mode 100644 index 0000000..1d62d0e --- /dev/null +++ b/includes/footer.php @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/includes/header.php b/includes/header.php new file mode 100644 index 0000000..99928c7 --- /dev/null +++ b/includes/header.php @@ -0,0 +1,10 @@ + + + + + + Login - School Leave System + + + + \ No newline at end of file diff --git a/index.php b/index.php index 7205f3d..8ea8e81 100644 --- a/index.php +++ b/index.php @@ -4,147 +4,92 @@ declare(strict_types=1); @error_reporting(E_ALL); @date_default_timezone_set('UTC'); -$phpVersion = PHP_VERSION; -$now = date('Y-m-d H:i:s'); +require_once __DIR__ . '/db/config.php'; + +session_start(); + +// Redirect if already logged in +if (isset($_SESSION['user_role'])) { + header('Location: ' . $_SESSION['user_role'] . '_dashboard.php'); + exit; +} + +$error_message = ''; +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $email = $_POST['email'] ?? ''; + $password = $_POST['password'] ?? ''; + $role = $_POST['role'] ?? ''; + + if (empty($email) || empty($password) || empty($role)) { + $error_message = 'Please provide email, password, and select a role.'; + } else { + try { + $pdo = db(); + $stmt = $pdo->prepare("SELECT * FROM users WHERE email = ? AND role = ?"); + $stmt->execute([$email, $role]); + $user = $stmt->fetch(); + + if ($user && password_verify($password, $user['password'])) { + // Password is correct, set session variables + $_SESSION['user_id'] = $user['id']; + $_SESSION['user_role'] = $user['role']; + $_SESSION['user_email'] = $user['email']; + $_SESSION['user_full_name'] = $user['full_name']; + + // Redirect to the respective dashboard + header('Location: ' . $user['role'] . '_dashboard.php'); + exit; + } else { + $error_message = 'Invalid email, password, or role.'; + } + } catch (PDOException $e) { + $error_message = 'Database error: ' . $e->getMessage(); + } + } +} + +require_once __DIR__ . '/includes/header.php'; ?> - - - - - - New Style - - - - - - - - - - - - - - - - - - - - - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… + +
+
+

School Leave System

+

Please sign in to continue

+ + + + + +
+ +
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

-
+
+ + +
+
+ + +
+ +
+ +
+ +

+ Forgot password? +

+

© . All rights reserved.

+
- - - + \ No newline at end of file diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..85facf7 --- /dev/null +++ b/logout.php @@ -0,0 +1,5 @@ +prepare("SELECT * FROM password_resets WHERE token = ? AND expires_at > NOW()"); + $stmt->execute([$token]); + $reset_request = $stmt->fetch(); + + if ($reset_request) { + $show_form = true; + } else { + $message = 'Invalid or expired password reset token.'; + } + } catch (PDOException $e) { + $message = 'Database error: ' . $e->getMessage(); + } +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $password = $_POST['password'] ?? ''; + $password_confirm = $_POST['password_confirm'] ?? ''; + + if (empty($password) || empty($password_confirm)) { + $message = 'Please enter and confirm your new password.'; + } elseif ($password !== $password_confirm) { + $message = 'Passwords do not match.'; + } else { + try { + $pdo = db(); + $stmt = $pdo->prepare("SELECT * FROM password_resets WHERE token = ? AND expires_at > NOW()"); + $stmt->execute([$token]); + $reset_request = $stmt->fetch(); + + if ($reset_request) { + $email = $reset_request['email']; + $hashed_password = password_hash($password, PASSWORD_DEFAULT); + + // Update user's password + $stmt = $pdo->prepare("UPDATE users SET password = ? WHERE email = ?"); + $stmt->execute([$hashed_password, $email]); + + // Delete the reset token + $stmt = $pdo->prepare("DELETE FROM password_resets WHERE token = ?"); + $stmt->execute([$token]); + + $message = 'Your password has been reset successfully. You can now login with your new password.'; + $show_form = false; + } else { + $message = 'Invalid or expired password reset token.'; + } + } catch (PDOException $e) { + $message = 'Database error: ' . $e->getMessage(); + } + } +} + +require_once __DIR__ . '/includes/header.php'; +?> + +
+
+
+

Reset Password

+ + + + + + +
+
+ + +
+
+ + +
+ +
+ +
+
+
+ + diff --git a/student_dashboard.php b/student_dashboard.php new file mode 100644 index 0000000..308ddb1 --- /dev/null +++ b/student_dashboard.php @@ -0,0 +1,103 @@ +prepare("SELECT * FROM leave_requests WHERE student_id = ? ORDER BY created_at DESC"); + $stmt->execute([$_SESSION['user_id']]); + $leave_requests = $stmt->fetchAll(); +} catch (PDOException $e) { + // handle error +} + +?> + +
+
+

Student Dashboard

+
+ Welcome, ! + Logout +
+
+ + + + + +
+
+

Submit Leave Request

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+

My Leave Requests

+ + + + + + + + + + + + + + + + + + + +
Leave TypeStart DateEnd DateStatus
+
+
+
+ + \ No newline at end of file diff --git a/submit_leave_request.php b/submit_leave_request.php new file mode 100644 index 0000000..097352c --- /dev/null +++ b/submit_leave_request.php @@ -0,0 +1,52 @@ +prepare("INSERT INTO leave_requests (student_id, leave_type, start_date, end_date, reason, attachment_path) VALUES (?, ?, ?, ?, ?, ?)"); + $stmt->execute([$student_id, $leave_type, $start_date, $end_date, $reason, $attachment_path]); + + // Send email to teacher + $teacher_email = 'teacher@example.com'; // Hardcoded for now + $subject = 'New Leave Request from ' . $_SESSION['user_full_name']; + $body = "

A new leave request has been submitted by {" . $_SESSION['user_full_name'] . "}.

\n

Leave Type: {" . $leave_type . "}

\n

Start Date: {" . $start_date . "}

\n

End Date: {" . $end_date . "}

\n

Reason: {" . $reason . "}

\n

Please login to the dashboard to approve or reject this request.

"; + MailService::sendMail($teacher_email, $subject, $body); + + header('Location: student_dashboard.php?success=1'); + exit; + } catch (PDOException $e) { + die('Database error: ' . $e->getMessage()); + } +} \ No newline at end of file diff --git a/teacher_dashboard.php b/teacher_dashboard.php new file mode 100644 index 0000000..3e85860 --- /dev/null +++ b/teacher_dashboard.php @@ -0,0 +1,75 @@ +prepare("SELECT leave_requests.*, users.full_name AS student_name FROM leave_requests JOIN users ON leave_requests.student_id = users.id WHERE leave_requests.status = 'pending' ORDER BY leave_requests.created_at DESC"); + $stmt->execute(); + $leave_requests = $stmt->fetchAll(); +} catch (PDOException $e) { + // handle error +} + +?> + +
+
+

Teacher Dashboard

+
+ Welcome, ! + Logout +
+
+ +
+
+

Pending Leave Requests

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Student NameLeave TypeStart DateEnd DateReasonAttachmentAction
+ + View Attachment + + No Attachment + + + Approve + Reject +
+
+
+
+ + diff --git a/update_leave_status.php b/update_leave_status.php new file mode 100644 index 0000000..6eb3f73 --- /dev/null +++ b/update_leave_status.php @@ -0,0 +1,85 @@ +prepare("UPDATE leave_requests SET status = ? WHERE id = ?"); + $stmt->execute([$new_status, $leave_request_id]); + + // Get student email + $stmt = $pdo->prepare("SELECT users.email, users.full_name FROM leave_requests JOIN users ON leave_requests.student_id = users.id WHERE leave_requests.id = ?"); + $stmt->execute([$leave_request_id]); + $student = $stmt->fetch(); + + if ($student) { + $student_email = $student['email']; + $student_name = $student['full_name']; + + if ($new_status === 'approved_by_teacher') { + // Notify admin + $admin_email = 'admin@example.com'; // Hardcoded for now + $subject = 'Leave Request Approved by Teacher'; + $body = "

The leave request for {$student_name} has been approved by the teacher and is waiting for your final approval.

Please login to the dashboard to review the request.

"; + MailService::sendMail($admin_email, $subject, $body); + + // Notify student + $subject_student = 'Your Leave Request has been updated'; + $body_student = "

Your leave request has been approved by your teacher and is now pending final approval from the admin.

"; + MailService::sendMail($student_email, $subject_student, $body_student); + + } elseif ($new_status === 'rejected_by_teacher') { + // Notify student + $subject_student = 'Your Leave Request has been updated'; + $body_student = "

Your leave request has been rejected by your teacher.

"; + MailService::sendMail($student_email, $subject_student, $body_student); + + } elseif ($new_status === 'approved_by_admin') { + // Notify student + $subject_student = 'Your Leave Request has been approved'; + $body_student = "

Your leave request has been approved by the admin.

"; + MailService::sendMail($student_email, $subject_student, $body_student); + + } elseif ($new_status === 'rejected_by_admin') { + // Notify student + $subject_student = 'Your Leave Request has been rejected'; + $body_student = "

Your leave request has been rejected by the admin.

"; + MailService::sendMail($student_email, $subject_student, $body_student); + } + } + + if ($_SESSION['user_role'] === 'teacher') { + header('Location: teacher_dashboard.php'); + } elseif ($_SESSION['user_role'] === 'admin') { + header('Location: admin_dashboard.php'); + } + exit; + } catch (PDOException $e) { + die('Database error: ' . $e->getMessage()); + } +} diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 0000000..6a04776 --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,22 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var string|null */ + private $vendorDir; + + // PSR-4 + /** + * @var array> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var list + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array>> + */ + private $prefixesPsr0 = array(); + /** + * @var list + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var array + */ + private $missingClasses = array(); + + /** @var string|null */ + private $apcuPrefix; + + /** + * @var array + */ + private static $registeredLoaders = array(); + + /** + * @param string|null $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return array> + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return list + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return list + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return array Array of classname => path + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + $includeFile = self::$includeFile; + $includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders keyed by their corresponding vendor directories. + * + * @return array + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } + + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } +} diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php new file mode 100644 index 0000000..2052022 --- /dev/null +++ b/vendor/composer/InstalledVersions.php @@ -0,0 +1,396 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to + * @internal + */ + private static $selfDir = null; + + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool + */ + private static $installedIsLocalDir; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + + // when using reload, we disable the duplicate protection to ensure that self::$installed data is + // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not, + // so we have to assume it does not, and that may result in duplicate data being returned when listing + // all installed packages for example + self::$installedIsLocalDir = false; + } + + /** + * @return string + */ + private static function getSelfDir() + { + if (self::$selfDir === null) { + self::$selfDir = strtr(__DIR__, '\\', '/'); + } + + return self::$selfDir; + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + $copiedLocalDir = false; + + if (self::$canGetVendors) { + $selfDir = self::getSelfDir(); + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + $vendorDir = strtr($vendorDir, '\\', '/'); + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + self::$installedByVendor[$vendorDir] = $required; + $installed[] = $required; + if (self::$installed === null && $vendorDir.'/composer' === $selfDir) { + self::$installed = $required; + self::$installedIsLocalDir = true; + } + } + if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) { + $copiedLocalDir = true; + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array() && !$copiedLocalDir) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 0000000..f27399a --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..0fb0a2c --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,10 @@ + $vendorDir . '/composer/InstalledVersions.php', +); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..15a2ff3 --- /dev/null +++ b/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($vendorDir . '/parsecsv/php-parsecsv/src'), +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 0000000..abdaa0e --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,38 @@ +register(true); + + return $loader; + } +} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php new file mode 100644 index 0000000..ceea6e5 --- /dev/null +++ b/vendor/composer/autoload_static.php @@ -0,0 +1,36 @@ + + array ( + 'ParseCsv\\' => 9, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'ParseCsv\\' => + array ( + 0 => __DIR__ . '/..' . '/parsecsv/php-parsecsv/src', + ), + ); + + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitf6815287adc097f2e9e30b4ea978d3c7::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitf6815287adc097f2e9e30b4ea978d3c7::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInitf6815287adc097f2e9e30b4ea978d3c7::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 0000000..fbc9db9 --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,73 @@ +{ + "packages": [ + { + "name": "parsecsv/php-parsecsv", + "version": "1.3.2", + "version_normalized": "1.3.2.0", + "source": { + "type": "git", + "url": "https://github.com/parsecsv/parsecsv-for-php.git", + "reference": "2d6236cae09133e0533d34ed45ba1e1ecafffebb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/parsecsv/parsecsv-for-php/zipball/2d6236cae09133e0533d34ed45ba1e1ecafffebb", + "reference": "2d6236cae09133e0533d34ed45ba1e1ecafffebb", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^6", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "illuminate/support": "Fluent array interface for map functions" + }, + "time": "2021-11-07T14:15:46+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "ParseCsv\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Myhrberg", + "email": "contact@jimeh.me" + }, + { + "name": "William Knauss", + "email": "will.knauss@gmail.com" + }, + { + "name": "Susann Sgorzaly", + "homepage": "https://github.com/susgo" + }, + { + "name": "Christian Bläul", + "homepage": "https://github.com/Fonata" + } + ], + "description": "CSV data parser for PHP", + "support": { + "issues": "https://github.com/parsecsv/parsecsv-for-php/issues", + "source": "https://github.com/parsecsv/parsecsv-for-php" + }, + "install-path": "../parsecsv/php-parsecsv" + } + ], + "dev": true, + "dev-package-names": [] +} diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php new file mode 100644 index 0000000..2af8ffc --- /dev/null +++ b/vendor/composer/installed.php @@ -0,0 +1,32 @@ + array( + 'name' => '__root__', + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => '2a30fbdcdbc8f6f2a59d2d0d87a5d6b9e8549fd6', + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev' => true, + ), + 'versions' => array( + '__root__' => array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => '2a30fbdcdbc8f6f2a59d2d0d87a5d6b9e8549fd6', + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'parsecsv/php-parsecsv' => array( + 'pretty_version' => '1.3.2', + 'version' => '1.3.2.0', + 'reference' => '2d6236cae09133e0533d34ed45ba1e1ecafffebb', + 'type' => 'library', + 'install_path' => __DIR__ . '/../parsecsv/php-parsecsv', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +); diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php new file mode 100644 index 0000000..103a1ff --- /dev/null +++ b/vendor/composer/platform_check.php @@ -0,0 +1,25 @@ += 50500)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 5.5.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + throw new \RuntimeException( + 'Composer detected issues in your platform: ' . implode(' ', $issues) + ); +} diff --git a/vendor/parsecsv/php-parsecsv/.github/workflows/ci.yml b/vendor/parsecsv/php-parsecsv/.github/workflows/ci.yml new file mode 100644 index 0000000..dfc6dbb --- /dev/null +++ b/vendor/parsecsv/php-parsecsv/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +--- +name: CI +on: + push: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php_version: + - "7.4" + - "7.3" + - "7.2" + - "7.1" + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php_version }} + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + - name: Cache composer dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install dependencies + run: composer update + - name: Validate dependencies + run: composer validate + - name: Run tests + run: vendor/bin/phpunit --configuration tests/phpunit.xml diff --git a/vendor/parsecsv/php-parsecsv/License.txt b/vendor/parsecsv/php-parsecsv/License.txt new file mode 100644 index 0000000..84efc1c --- /dev/null +++ b/vendor/parsecsv/php-parsecsv/License.txt @@ -0,0 +1,21 @@ +(The MIT license) + +Copyright (c) 2014 Jim Myhrberg. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/parsecsv/php-parsecsv/README.md b/vendor/parsecsv/php-parsecsv/README.md new file mode 100644 index 0000000..bc47d9f --- /dev/null +++ b/vendor/parsecsv/php-parsecsv/README.md @@ -0,0 +1,246 @@ +# ParseCsv +[![Financial Contributors on Open Collective](https://opencollective.com/parsecsv/all/badge.svg?label=financial+contributors)](https://opencollective.com/parsecsv) + +ParseCsv is an easy-to-use PHP class that reads and writes CSV data properly. It +fully conforms to the specifications outlined on the on the +[Wikipedia article][CSV] (and thus RFC 4180). It has many advanced features which help make your +life easier when dealing with CSV data. + +You may not need a library at all: before using ParseCsv, please make sure if PHP's own `str_getcsv()`, ``fgetcsv()`` or `fputcsv()` meets your needs. + +This library was originally created in early 2007 by [jimeh](https://github.com/jimeh) due to the lack of built-in +and third-party support for handling CSV data in PHP. + +[csv]: http://en.wikipedia.org/wiki/Comma-separated_values + +## Features + +* ParseCsv is a complete and fully featured CSV solution for PHP +* Supports enclosed values, enclosed commas, double quotes and new lines. +* Automatic delimiter character detection. +* Sort data by specific fields/columns. +* Easy data manipulation. +* Basic SQL-like _conditions_, _offset_ and _limit_ options for filtering + data. +* Error detection for incorrectly formatted input. It attempts to be + intelligent, but can not be trusted 100% due to the structure of CSV, and + how different programs like Excel for example outputs CSV data. +* Support for character encoding conversion using PHP's + `iconv()` and `mb_convert_encoding()` functions. +* Supports PHP 5.5 and higher. + It certainly works with PHP 7.2 and all versions in between. + +## Installation + +Installation is easy using Composer. Just run the following on the +command line: +``` +composer require parsecsv/php-parsecsv +``` + +If you don't use a framework such as Drupal, Laravel, Symfony, Yii etc., +you may have to manually include Composer's autoloader file in your PHP +script: +```php +require_once __DIR__ . '/vendor/autoload.php'; +``` + +#### Without composer +Not recommended, but technically possible: you can also clone the +repository or extract the +[ZIP](https://github.com/parsecsv/parsecsv-for-php/archive/master.zip). +To use ParseCSV, you then have to add a `require 'parsecsv.lib.php';` line. + +## Example Usage + +**Parse a tab-delimited CSV file with encoding conversion** + +```php +$csv = new \ParseCsv\Csv(); +$csv->encoding('UTF-16', 'UTF-8'); +$csv->delimiter = "\t"; +$csv->parseFile('data.tsv'); +print_r($csv->data); +``` + +**Auto-detect field delimiter character** + +```php +$csv = new \ParseCsv\Csv(); +$csv->auto('data.csv'); +print_r($csv->data); +``` + +**Parse data with offset** +* ignoring the first X (e.g. two) rows +```php +$csv = new \ParseCsv\Csv(); +$csv->offset = 2; +$csv->parseFile('data.csv'); +print_r($csv->data); +``` + +**Limit the number of returned data rows** +```php +$csv = new \ParseCsv\Csv(); +$csv->limit = 5; +$csv->parseFile('data.csv'); +print_r($csv->data); +``` + +**Get total number of data rows without parsing whole data** +* Excluding heading line if present (see $csv->header property) +```php +$csv = new \ParseCsv\Csv(); +$csv->loadFile('data.csv'); +$count = $csv->getTotalDataRowCount(); +print_r($count); +``` + +**Get most common data type for each column** + +```php +$csv = new \ParseCsv\Csv('data.csv'); +$csv->getDatatypes(); +print_r($csv->data_types); +``` + +**Modify data in a CSV file** + +Change data values: +```php +$csv = new \ParseCsv\Csv(); +$csv->sort_by = 'id'; +$csv->parseFile('data.csv'); +# "4" is the value of the "id" column of the CSV row +$csv->data[4] = array('firstname' => 'John', 'lastname' => 'Doe', 'email' => 'john@doe.com'); +$csv->save(); +``` + +Enclose each data value by quotes: +```php +$csv = new \ParseCsv\Csv(); +$csv->parseFile('data.csv'); +$csv->enclose_all = true; +$csv->save(); +``` + +**Replace field names or set ones if missing** + +```php +$csv = new \ParseCsv\Csv(); +$csv->fields = ['id', 'name', 'category']; +$csv->parseFile('data.csv'); +``` + +**Add row/entry to end of CSV file** + +_Only recommended when you know the exact structure of the file._ + +```php +$csv = new \ParseCsv\Csv(); +$csv->save('data.csv', array(array('1986', 'Home', 'Nowhere', '')), /* append */ true); +``` + +**Convert 2D array to CSV data and send headers to browser to treat output as +a file and download it** + +Your web app users would call this an export. + +```php +$csv = new \ParseCsv\Csv(); +$csv->linefeed = "\n"; +$header = array('field 1', 'field 2'); +$csv->output('movies.csv', $data_array, $header, ','); +``` + +For more complex examples, see the ``tests`` and `examples` directories. + +## Test coverage + +All tests are located in the `tests` directory. To execute tests, run the following commands: + +````bash +composer install +composer run test +```` + +When pushing code to GitHub, tests will be executed using GitHub Actions. The relevant configuration is in the +file `.github/workflows/ci.yml`. To run the `test` action locally, you can execute the following command: + +````bash +make local-ci +```` + +## Security + +If you discover any security related issues, please email ParseCsv@blaeul.de instead of using GitHub issues. + +## Credits + +* ParseCsv is based on the concept of [Ming Hong Ng][ming]'s [CsvFileParser][] + class. + +[ming]: http://minghong.blogspot.com/ +[CsvFileParser]: http://minghong.blogspot.com/2006/07/csv-parser-for-php.html + + +## Contributors + +### Code Contributors + +This project exists thanks to all the people who contribute. + +Please find a complete list on the project's [contributors][] page. + +[contributors]: https://github.com/parsecsv/parsecsv-for-php/graphs/contributors + + +### Financial Contributors + +Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/parsecsv/contribute)] + +#### Individuals + + + +#### Organizations + +Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/parsecsv/contribute)] + + + + + + + + + + + + +## License + +(The MIT license) + +Copyright (c) 2014 Jim Myhrberg. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +[![Build Status](https://travis-ci.org/parsecsv/parsecsv-for-php.svg?branch=master)](https://travis-ci.org/parsecsv/parsecsv-for-php) diff --git a/vendor/parsecsv/php-parsecsv/composer.json b/vendor/parsecsv/php-parsecsv/composer.json new file mode 100644 index 0000000..cbbaca0 --- /dev/null +++ b/vendor/parsecsv/php-parsecsv/composer.json @@ -0,0 +1,57 @@ +{ + "name": "parsecsv/php-parsecsv", + "description": "CSV data parser for PHP", + "license": "MIT", + "authors": [ + { + "name": "Jim Myhrberg", + "email": "contact@jimeh.me" + }, + { + "name": "William Knauss", + "email": "will.knauss@gmail.com" + }, + { + "name": "Susann Sgorzaly", + "homepage": "https://github.com/susgo" + }, + { + "name": "Christian Bläul", + "homepage": "https://github.com/Fonata" + } + ], + "autoload": { + "psr-4": { + "ParseCsv\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "ParseCsv\\tests\\": "tests" + } + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^6", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "illuminate/support": "Fluent array interface for map functions" + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "scripts": { + "test": [ + "vendor/bin/phpunit -c tests tests --disallow-test-output --coverage-clover coverage_clover.xml --whitelist src" + ] + }, + "support": { + "issues": "https://github.com/parsecsv/parsecsv-for-php/issues", + "source": "https://github.com/parsecsv/parsecsv-for-php" + } +} diff --git a/vendor/parsecsv/php-parsecsv/parsecsv.lib.php b/vendor/parsecsv/php-parsecsv/parsecsv.lib.php new file mode 100644 index 0000000..265ff22 --- /dev/null +++ b/vendor/parsecsv/php-parsecsv/parsecsv.lib.php @@ -0,0 +1,24 @@ +var_name = 'value'; + */ + + /** + * Header row: + * Use first line/entry as field names + * + * @var bool + */ + public $heading = true; + + /** + * Override field names + * + * @var array + */ + public $fields = array(); + + /** + * Sort CSV by this field + * + * @var string|null + */ + public $sort_by = null; + + /** + * Reverse the sort direction + * + * @var bool + */ + public $sort_reverse = false; + + /** + * Sort behavior passed to sort methods + * + * regular = SORT_REGULAR + * numeric = SORT_NUMERIC + * string = SORT_STRING + * + * @var string|null + */ + public $sort_type = SortEnum::SORT_TYPE_REGULAR; + + /** + * Field delimiter character + * + * @var string + */ + public $delimiter = ','; + + /** + * Enclosure character + * + * This is useful for cell values that are either multi-line + * or contain the field delimiter character. + * + * @var string + */ + public $enclosure = '"'; + + /** + * Force enclosing all columns. + * + * If false, only cells that are either multi-line or + * contain the field delimiter character are enclosed + * in the $enclosure char. + * + * @var bool + */ + public $enclose_all = false; + + /** + * Basic SQL-Like conditions for row matching + * + * @var string|null + */ + public $conditions = null; + + /** + * Number of rows to ignore from beginning of data. If present, the heading + * row is also counted (if $this->heading == true). In other words, + * $offset == 1 and $offset == 0 have the same meaning in that situation. + * + * @var int|null + */ + public $offset = null; + + /** + * Limits the number of returned rows to the specified amount + * + * @var int|null + */ + public $limit = null; + + /** + * Number of rows to analyze when attempting to auto-detect delimiter + * + * @var int + */ + public $auto_depth = 15; + + /** + * Characters that should be ignored when attempting to auto-detect delimiter + * + * @var string + */ + public $auto_non_chars = "a-zA-Z0-9\n\r"; + + /** + * preferred delimiter characters, only used when all filtering method + * returns multiple possible delimiters (happens very rarely) + * + * @var string + */ + public $auto_preferred = ",;\t.:|"; + + /** + * Should we convert the CSV character encoding? + * Used for both parse and unparse operations. + * + * @var bool + */ + public $convert_encoding = false; + + /** + * Set the input encoding + * + * @var string + */ + public $input_encoding = 'ISO-8859-1'; + + /** + * Set the output encoding + * + * @var string + */ + public $output_encoding = 'ISO-8859-1'; + + /** + * Whether to use mb_convert_encoding() instead of iconv(). + * + * The former is platform-independent whereas the latter is the traditional + * default go-to solution. + * + * @var bool (if false, iconv() is used) + */ + public $use_mb_convert_encoding = false; + + /** + * Line feed characters used by unparse, save, and output methods + * Popular choices are "\r\n" and "\n". + * + * @var string + */ + public $linefeed = "\r"; + + /** + * Sets the output delimiter used by the output method + * + * @var string + */ + public $output_delimiter = ','; + + /** + * Sets the output filename + * + * @var string + */ + public $output_filename = 'data.csv'; + + /** + * keep raw file data in memory after successful parsing (useful for debugging) + * + * @var bool + */ + public $keep_file_data = false; + + /** + * Internal variables + */ + + /** + * File + * Current Filename + * + * @var string + */ + public $file; + + /** + * File Data + * Current file data + * + * @var string + */ + public $file_data; + + /** + * Error + * Contains the error code if one occurred + * + * 0 = No errors found. Everything should be fine :) + * 1 = Hopefully correctable syntax error was found. + * 2 = Enclosure character (double quote by default) + * was found in non-enclosed field. This means + * the file is either corrupt, or does not + * standard CSV formatting. Please validate + * the parsed data yourself. + * + * @var int + */ + public $error = 0; + + /** + * Detailed error information + * + * @var array + */ + public $error_info = array(); + + /** + * $titles has 4 distinct tasks: + * 1. After reading in CSV data, $titles will contain the column headers + * present in the data. + * + * 2. It defines which fields from the $data array to write e.g. when + * calling unparse(), and in which order. This lets you skip columns you + * don't want in your output, but are present in $data. + * See examples/save_to_file_without_header_row.php. + * + * 3. It lets you rename columns. See StreamTest::testWriteStream for an + * example. + * + * 4. When writing data and $header is true, then $titles is also used for + * the first row. + * + * @var array + */ + public $titles = array(); + + /** + * Two-dimensional array of CSV data. + * The first dimension are the line numbers. Each line is represented as an array with field names as keys. + * + * @var array + */ + public $data = array(); + + use DatatypeTrait; + + /** + * Class constructor + * + * @param string|null $data The CSV string or a direct file path. + * + * WARNING: Supplying file paths here is + * deprecated. Use parseFile() instead. + * + * @param int|null $offset Number of rows to ignore from the + * beginning of the data + * @param int|null $limit Limits the number of returned rows + * to specified amount + * @param string|null $conditions Basic SQL-like conditions for row + * matching + * @param null|true $keep_file_data Keep raw file data in memory after + * successful parsing + * (useful for debugging) + */ + public function __construct($data = null, $offset = null, $limit = null, $conditions = null, $keep_file_data = null) { + $this->init($offset, $limit, $conditions, $keep_file_data); + + if (!empty($data)) { + $this->parse($data); + } + } + + /** + * @param int|null $offset Number of rows to ignore from the + * beginning of the data + * @param int|null $limit Limits the number of returned rows + * to specified amount + * @param string|null $conditions Basic SQL-like conditions for row + * matching + * @param null|true $keep_file_data Keep raw file data in memory after + * successful parsing + * (useful for debugging) + */ + public function init($offset = null, $limit = null, $conditions = null, $keep_file_data = null) { + if (!is_null($offset)) { + $this->offset = $offset; + } + + if (!is_null($limit)) { + $this->limit = $limit; + } + + if (!is_null($conditions)) { + $this->conditions = $conditions; + } + + if (!is_null($keep_file_data)) { + $this->keep_file_data = $keep_file_data; + } + } + + // ============================================== + // ----- [ Main Functions ] --------------------- + // ============================================== + + /** + * Parse a CSV file or string + * + * @param string|null $dataString The CSV string or a direct file path + * WARNING: Supplying file paths here is + * deprecated and will trigger an + * E_USER_DEPRECATED error. + * @param int|null $offset Number of rows to ignore from the + * beginning of the data + * @param int|null $limit Limits the number of returned rows to + * specified amount + * @param string|null $conditions Basic SQL-like conditions for row + * matching + * + * @return bool True on success + */ + public function parse($dataString = null, $offset = null, $limit = null, $conditions = null) { + if (is_null($dataString)) { + $this->data = $this->parseFile(); + return $this->data !== false; + } + + if (empty($dataString)) { + return false; + } + + $this->init($offset, $limit, $conditions); + + if (strlen($dataString) <= PHP_MAXPATHLEN && is_readable($dataString)) { + $this->file = $dataString; + $this->data = $this->parseFile(); + trigger_error( + 'Supplying file paths to parse() will no longer ' . + 'be supported in a future version of ParseCsv. ' . + 'Use ->parseFile() instead.', + E_USER_DEPRECATED + ); + } else { + $this->file = null; + $this->file_data = &$dataString; + $this->data = $this->_parse_string(); + } + + return $this->data !== false; + } + + /** + * Save changes, or write a new file and/or data. + * + * @param string $file File location to save to + * @param array $data 2D array of data + * @param bool $append Append current data to end of target CSV, if file + * exists + * @param array $fields Field names. Sets the header. If it is not set + * $this->titles would be used instead. + * + * @return bool + * True on success + */ + public function save($file = '', $data = array(), $append = FileProcessingModeEnum::MODE_FILE_OVERWRITE, $fields = array()) { + if (empty($file)) { + $file = &$this->file; + } + + $mode = FileProcessingModeEnum::getAppendMode($append); + $is_php = preg_match('/\.php$/i', $file) ? true : false; + + return $this->_wfile($file, $this->unparse($data, $fields, $append, $is_php), $mode); + } + + /** + * Generate a CSV-based string for output. + * + * Useful for exports in web applications. + * + * @param string|null $filename If a filename is specified here or in the + * object, headers and data will be output + * directly to browser as a downloadable + * file. This file doesn't have to exist on + * the server; the parameter only affects + * how the download is called to the + * browser. + * @param array[] $data 2D array with data + * @param array $fields Field names + * @param string|null $delimiter character used to separate data + * + * @return string The resulting CSV string + */ + public function output($filename = null, $data = array(), $fields = array(), $delimiter = null) { + if (empty($filename)) { + $filename = $this->output_filename; + } + + if ($delimiter === null) { + $delimiter = $this->output_delimiter; + } + + $flat_string = $this->unparse($data, $fields, null, null, $delimiter); + + if (!is_null($filename)) { + $mime = $delimiter === "\t" ? + 'text/tab-separated-values' : + 'application/csv'; + header('Content-type: ' . $mime); + header('Content-Length: ' . strlen($flat_string)); + header('Cache-Control: no-cache, must-revalidate'); + header('Pragma: no-cache'); + header('Expires: 0'); + header('Content-Disposition: attachment; filename="' . $filename . '"; modification-date="' . date('r') . '";'); + + echo $flat_string; + } + + return $flat_string; + } + + /** + * Convert character encoding + * + * Specify the encoding to use for the next parsing or unparsing. + * Calling this function will not change the data held in the object immediately. + * + * @param string|null $input Input character encoding + * If the value null is passed, the existing input encoding remains set (default: ISO-8859-1). + * @param string|null $output Output character encoding, uses default if left blank + * If the value null is passed, the existing input encoding remains set (default: ISO-8859-1). + * + * @return void + */ + public function encoding($input = null, $output = null) { + $this->convert_encoding = true; + if (!is_null($input)) { + $this->input_encoding = $input; + } + + if (!is_null($output)) { + $this->output_encoding = $output; + } + } + + /** + * Auto-detect delimiter: Find delimiter by analyzing a specific number of + * rows to determine most probable delimiter character + * + * @param string|null $file Local CSV file + * Supplying CSV data (file content) here is deprecated. + * For CSV data, please use autoDetectionForDataString(). + * Support for CSV data will be removed in v2.0.0. + * @param bool $parse True/false parse file directly + * @param int|null $search_depth Number of rows to analyze + * @param string|null $preferred Preferred delimiter characters + * @param string|null $enclosure Enclosure character, default is double quote ("). + * + * @return string|false The detected field delimiter + */ + public function auto($file = null, $parse = true, $search_depth = null, $preferred = null, $enclosure = null) { + if (is_null($file)) { + $file = $this->file; + } + + if (empty($search_depth)) { + $search_depth = $this->auto_depth; + } + + if (is_null($enclosure)) { + $enclosure = $this->enclosure; + } else { + $this->enclosure = $enclosure; + } + + if (is_null($preferred)) { + $preferred = $this->auto_preferred; + } + + if (empty($this->file_data)) { + if ($this->_check_data($file)) { + $data = &$this->file_data; + } else { + return false; + } + } else { + $data = &$this->file_data; + } + + $this->autoDetectionForDataString($data, $parse, $search_depth, $preferred, $enclosure); + + return $this->delimiter; + } + + public function autoDetectionForDataString($data, $parse = true, $search_depth = null, $preferred = null, $enclosure = null) { + $this->file_data = &$data; + if (!$this->_detect_and_remove_sep_row_from_data($data)) { + $this->_guess_delimiter($search_depth, $preferred, $enclosure, $data); + } + + // parse data + if ($parse) { + $this->data = $this->_parse_string(); + } + + return $this->delimiter; + } + + /** + * Get total number of data rows (exclusive heading line if present) in CSV + * without parsing the whole data string. + * + * @return bool|int + */ + public function getTotalDataRowCount() { + if (empty($this->file_data)) { + return false; + } + + $data = $this->file_data; + + $this->_detect_and_remove_sep_row_from_data($data); + + $pattern = sprintf('/%1$s[^%1$s]*%1$s/i', $this->enclosure); + preg_match_all($pattern, $data, $matches); + + /** @var array[] $matches */ + foreach ($matches[0] as $match) { + if (empty($match) || (strpos($match, $this->enclosure) === false)) { + continue; + } + + $replace = str_replace(["\r", "\n"], '', $match); + $data = str_replace($match, $replace, $data); + } + + $headingRow = $this->heading ? 1 : 0; + + return substr_count($data, "\r") + + substr_count($data, "\n") + - substr_count($data, "\r\n") + - $headingRow; + } + + // ============================================== + // ----- [ Core Functions ] --------------------- + // ============================================== + + /** + * Read file to string and call _parse_string() + * + * @param string|null $file Path to a CSV file. + * If configured in files such as php.ini, + * the path may also contain a protocol: + * https://example.org/some/file.csv + * + * @return array|false + */ + public function parseFile($file = null) { + if (is_null($file)) { + $file = $this->file; + } + + /** + * @see self::keep_file_data + * Usually, _parse_string will clean this + * Instead of leaving stale data for the next parseFile call behind. + */ + if (empty($this->file_data) && !$this->loadFile($file)) { + return false; + } + + if (empty($this->file_data)) { + return false; + } + return $this->data = $this->_parse_string(); + } + + /** + * Internal function to parse CSV strings to arrays. + * + * If you need BOM detection or character encoding conversion, please call + * $csv->load_data($your_data_string) first, followed by a call to + * $csv->parse($csv->file_data). + * + * To detect field separators, please use auto() instead. + * + * @param string|null $data CSV data + * + * @return array|false + * 2D array with CSV data, or false on failure + */ + protected function _parse_string($data = null) { + if (empty($data)) { + if ($this->_check_data()) { + $data = &$this->file_data; + } else { + return false; + } + } + + $white_spaces = str_replace($this->delimiter, '', " \t\x0B\0"); + + $rows = array(); + $row = array(); + $row_count = 0; + $current = ''; + $head = !empty($this->fields) ? $this->fields : array(); + $col = 0; + $enclosed = false; + $was_enclosed = false; + $strlen = strlen($data); + + // force the parser to process end of data as a character (false) when + // data does not end with a line feed or carriage return character. + $lch = $data[$strlen - 1]; + if ($lch != "\n" && $lch != "\r") { + $data .= "\n"; + $strlen++; + } + + // walk through each character + for ($i = 0; $i < $strlen; $i++) { + $ch = isset($data[$i]) ? $data[$i] : false; + $nch = isset($data[$i + 1]) ? $data[$i + 1] : false; + + // open/close quotes, and inline quotes + if ($ch == $this->enclosure) { + if (!$enclosed) { + if (ltrim($current, $white_spaces) == '') { + $enclosed = true; + $was_enclosed = true; + } else { + $this->error = 2; + $error_row = count($rows) + 1; + $error_col = $col + 1; + $index = $error_row . '-' . $error_col; + if (!isset($this->error_info[$index])) { + $this->error_info[$index] = array( + 'type' => 2, + 'info' => 'Syntax error found on row ' . $error_row . '. Non-enclosed fields can not contain double-quotes.', + 'row' => $error_row, + 'field' => $error_col, + 'field_name' => !empty($head[$col]) ? $head[$col] : null, + ); + } + + $current .= $ch; + } + } elseif ($nch == $this->enclosure) { + $current .= $ch; + $i++; + } elseif ($nch != $this->delimiter && $nch != "\r" && $nch != "\n") { + $x = $i + 1; + while (isset($data[$x]) && ltrim($data[$x], $white_spaces) == '') { + $x++; + } + if ($data[$x] == $this->delimiter) { + $enclosed = false; + $i = $x; + } else { + if ($this->error < 1) { + $this->error = 1; + } + + $error_row = count($rows) + 1; + $error_col = $col + 1; + $index = $error_row . '-' . $error_col; + if (!isset($this->error_info[$index])) { + $this->error_info[$index] = array( + 'type' => 1, + 'info' => + 'Syntax error found on row ' . (count($rows) + 1) . '. ' . + 'A single double-quote was found within an enclosed string. ' . + 'Enclosed double-quotes must be escaped with a second double-quote.', + 'row' => count($rows) + 1, + 'field' => $col + 1, + 'field_name' => !empty($head[$col]) ? $head[$col] : null, + ); + } + + $current .= $ch; + $enclosed = false; + } + } else { + $enclosed = false; + } + // end of field/row/csv + } elseif ((in_array($ch, [$this->delimiter, "\n", "\r", false], true)) && !$enclosed) { + $key = !empty($head[$col]) ? $head[$col] : $col; + $row[$key] = $was_enclosed ? $current : trim($current); + $current = ''; + $was_enclosed = false; + $col++; + + // end of row + if (in_array($ch, ["\n", "\r", false], true)) { + if ($this->_validate_offset($row_count) && $this->_validate_row_conditions($row, $this->conditions)) { + if ($this->heading && empty($head)) { + $head = $row; + } elseif (empty($this->fields) || (!empty($this->fields) && (($this->heading && $row_count > 0) || !$this->heading))) { + if (!empty($this->sort_by) && !empty($row[$this->sort_by])) { + $sort_field = $row[$this->sort_by]; + if (isset($rows[$sort_field])) { + $rows[$sort_field . '_0'] = &$rows[$sort_field]; + unset($rows[$sort_field]); + $sn = 1; + while (isset($rows[$sort_field . '_' . $sn])) { + $sn++; + } + $rows[$sort_field . '_' . $sn] = $row; + } else { + $rows[$sort_field] = $row; + } + + } else { + $rows[] = $row; + } + } + } + + $row = array(); + $col = 0; + $row_count++; + + if ($this->sort_by === null && $this->limit !== null && count($rows) == $this->limit) { + $i = $strlen; + } + + if ($ch == "\r" && $nch == "\n") { + $i++; + } + } + + // append character to current field + } else { + $current .= $ch; + } + } + + $this->titles = $head; + if (!empty($this->sort_by)) { + $sort_type = SortEnum::getSorting($this->sort_type); + $this->sort_reverse ? krsort($rows, $sort_type) : ksort($rows, $sort_type); + + if ($this->offset !== null || $this->limit !== null) { + $rows = array_slice($rows, ($this->offset === null ? 0 : $this->offset), $this->limit, true); + } + } + + if (!$this->keep_file_data) { + $this->file_data = null; + } + + return $rows; + } + + /** + * Create CSV data string from array + * + * @param array[] $data 2D array with data + * @param array $fields field names + * @param bool $append if true, field names will not be output + * @param bool $is_php if a php die() call should be put on the + * first line of the file, this is later + * ignored when read. + * @param string|null $delimiter field delimiter to use + * + * @return string CSV data + */ + public function unparse($data = array(), $fields = array(), $append = FileProcessingModeEnum::MODE_FILE_OVERWRITE, $is_php = false, $delimiter = null) { + if (!is_array($data) || empty($data)) { + $data = &$this->data; + } else { + /** @noinspection ReferenceMismatchInspection */ + $this->data = $data; + } + + if (!is_array($fields) || empty($fields)) { + $fields = &$this->titles; + } + + if ($delimiter === null) { + $delimiter = $this->delimiter; + } + + $string = $is_php ? "" . $this->linefeed : ''; + $entry = array(); + + // create heading + /** @noinspection ReferenceMismatchInspection */ + $fieldOrder = $this->_validate_fields_for_unparse($fields); + if (!$fieldOrder && !empty($data)) { + $column_count = count($data[0]); + $columns = range(0, $column_count - 1, 1); + $fieldOrder = array_combine($columns, $columns); + } + + if ($this->heading && !$append && !empty($fields)) { + foreach ($fieldOrder as $column_name) { + $entry[] = $this->_enclose_value($column_name, $delimiter); + } + + $string .= implode($delimiter, $entry) . $this->linefeed; + $entry = array(); + } + + // create data + foreach ($data as $key => $row) { + foreach (array_keys($fieldOrder) as $index) { + $cell_value = $row[$index]; + $entry[] = $this->_enclose_value($cell_value, $delimiter); + } + + $string .= implode($delimiter, $entry) . $this->linefeed; + $entry = array(); + } + + if ($this->convert_encoding) { + /** @noinspection PhpComposerExtensionStubsInspection + * + * If you receive an error at the following 3 lines, you must enable + * the following PHP extension: + * + * - if $use_mb_convert_encoding is true: mbstring + * - if $use_mb_convert_encoding is false: iconv + */ + $string = $this->use_mb_convert_encoding ? + mb_convert_encoding($string, $this->output_encoding, $this->input_encoding) : + iconv($this->input_encoding, $this->output_encoding, $string); + } + + return $string; + } + + /** + * @param array $fields + * + * @return array|false + */ + private function _validate_fields_for_unparse(array $fields) { + if (empty($fields)) { + $fields = $this->titles; + } + + if (empty($fields)) { + return array(); + } + + // this is needed because sometime titles property is overwritten instead of using fields parameter! + $titlesOnParse = !empty($this->data) ? array_keys(reset($this->data)) : array(); + + // both are identical, also in ordering OR we have no data (only titles) + if (empty($titlesOnParse) || array_values($fields) === array_values($titlesOnParse)) { + return array_combine($fields, $fields); + } + + // if renaming given by: $oldName => $newName (maybe with reorder and / or subset): + // todo: this will only work if titles are unique + $fieldOrder = array_intersect(array_flip($fields), $titlesOnParse); + if (!empty($fieldOrder)) { + return array_flip($fieldOrder); + } + + $fieldOrder = array_intersect($fields, $titlesOnParse); + if (!empty($fieldOrder)) { + return array_combine($fieldOrder, $fieldOrder); + } + + // original titles are not given in fields. that is okay if count is okay. + if (count($fields) != count($titlesOnParse)) { + throw new \UnexpectedValueException( + "The specified fields do not match any titles and do not match column count.\n" . + "\$fields was " . print_r($fields, true) . + "\$titlesOnParse was " . print_r($titlesOnParse, true)); + } + + return array_combine($titlesOnParse, $fields); + } + + /** + * Load local file or string. + * + * Only use this function if auto() and parse() don't handle your data well. + * + * This function load_data() is able to handle BOMs and encodings. The data + * is stored within the $this->file_data class field. + * + * @param string|null $input CSV file path or CSV data as a string + * + * Supplying CSV data (file content) here is deprecated. + * For CSV data, please use loadDataString(). + * Support for CSV data will be removed in v2.0.0. + * + * @return bool True on success + * @deprecated Use loadDataString() or loadFile() instead. + */ + public function load_data($input = null) { + return $this->loadFile($input); + } + + /** + * Load a file, but don't parse it. + * + * Only use this function if auto() and parseFile() don't handle your data well. + * + * This function is able to handle BOMs and encodings. The data + * is stored within the $this->file_data class field. + * + * @param string|null $file CSV file path + * + * @return bool True on success + */ + public function loadFile($file = null) { + $data = null; + + if (is_null($file)) { + $data = $this->_rfile($this->file); + } elseif (\strlen($file) <= PHP_MAXPATHLEN && file_exists($file)) { + $data = $this->_rfile($file); + if ($this->file != $file) { + $this->file = $file; + } + } else { + // It is CSV data as a string. + + // WARNING: + // Supplying CSV data to load_data() will no longer + // be supported in a future version of ParseCsv. + // This function will return false for invalid paths from v2.0.0 onwards. + + // Use ->loadDataString() instead. + + $data = $file; + } + + return $this->loadDataString($data); + } + + /** + * Load a data string, but don't parse it. + * + * Only use this function if autoDetectionForDataString() and parse() don't handle your data well. + * + * This function is able to handle BOMs and encodings. The data + * is stored within the $this->file_data class field. + * + * @param string|null $file_path CSV file path + * + * @return bool True on success + */ + public function loadDataString($data) { + if (!empty($data)) { + if (strpos($data, "\xef\xbb\xbf") === 0) { + // strip off BOM (UTF-8) + $data = substr($data, 3); + $this->encoding('UTF-8'); + } elseif (strpos($data, "\xff\xfe") === 0) { + // strip off BOM (UTF-16 little endian) + $data = substr($data, 2); + $this->encoding("UCS-2LE"); + } elseif (strpos($data, "\xfe\xff") === 0) { + // strip off BOM (UTF-16 big endian) + $data = substr($data, 2); + $this->encoding("UTF-16"); + } + + if ($this->convert_encoding && $this->input_encoding !== $this->output_encoding) { + /** @noinspection PhpComposerExtensionStubsInspection + * + * If you receive an error at the following 3 lines, you must enable + * the following PHP extension: + * + * - if $use_mb_convert_encoding is true: mbstring + * - if $use_mb_convert_encoding is false: iconv + */ + $data = $this->use_mb_convert_encoding ? + mb_convert_encoding($data, $this->output_encoding, $this->input_encoding) : + iconv($this->input_encoding, $this->output_encoding, $data); + } + + if (substr($data, -1) != "\n") { + $data .= "\n"; + } + + $this->file_data = &$data; + return true; + } + + return false; + } + + // ============================================== + // ----- [ Internal Functions ] ----------------- + // ============================================== + + /** + * Validate a row against specified conditions + * + * @param array $row array with values from a row + * @param string|null $conditions specified conditions that the row must match + * + * @return bool + */ + protected function _validate_row_conditions($row = array(), $conditions = null) { + if (!empty($row)) { + if (!empty($conditions)) { + $condition_array = (strpos($conditions, ' OR ') !== false) ? + explode(' OR ', $conditions) : + array($conditions); + $or = ''; + foreach ($condition_array as $key => $value) { + if (strpos($value, ' AND ') !== false) { + $value = explode(' AND ', $value); + $and = ''; + + foreach ($value as $k => $v) { + $and .= $this->_validate_row_condition($row, $v); + } + + $or .= (strpos($and, '0') !== false) ? '0' : '1'; + } else { + $or .= $this->_validate_row_condition($row, $value); + } + } + + return strpos($or, '1') !== false; + } + + return true; + } + + return false; + } + + /** + * Validate a row against a single condition + * + * @param array $row array with values from a row + * @param string $condition specified condition that the row must match + * + * @return string single 0 or 1 + */ + protected function _validate_row_condition($row, $condition) { + $operators = array( + '=', + 'equals', + 'is', + '!=', + 'is not', + '<', + 'is less than', + '>', + 'is greater than', + '<=', + 'is less than or equals', + '>=', + 'is greater than or equals', + 'contains', + 'does not contain', + ); + + $operators_regex = array(); + + foreach ($operators as $value) { + $operators_regex[] = preg_quote($value, '/'); + } + + $operators_regex = implode('|', $operators_regex); + + if (preg_match('/^(.+) (' . $operators_regex . ') (.+)$/i', trim($condition), $capture)) { + $field = $capture[1]; + $op = strtolower($capture[2]); + $value = $capture[3]; + if ($op == 'equals' && preg_match('/^(.+) is (less|greater) than or$/i', $field, $m)) { + $field = $m[1]; + $op = strtolower($m[2]) == 'less' ? '<=' : '>='; + } + if ($op == 'is' && preg_match('/^(less|greater) than (.+)$/i', $value, $m)) { + $value = $m[2]; + $op = strtolower($m[1]) == 'less' ? '<' : '>'; + } + if ($op == 'is' && preg_match('/^not (.+)$/i', $value, $m)) { + $value = $m[1]; + $op = '!='; + } + + if (preg_match('/^([\'"])(.*)([\'"])$/', $value, $capture) && $capture[1] == $capture[3]) { + $value = strtr($capture[2], array( + "\\n" => "\n", + "\\r" => "\r", + "\\t" => "\t", + )); + + $value = stripslashes($value); + } + + if (array_key_exists($field, $row)) { + $op_equals = in_array($op, ['=', 'equals', 'is'], true); + if ($op_equals && $row[$field] == $value) { + return '1'; + } elseif (($op == '!=' || $op == 'is not') && $row[$field] != $value) { + return '1'; + } elseif (($op == '<' || $op == 'is less than') && $row[$field] < $value) { + return '1'; + } elseif (($op == '>' || $op == 'is greater than') && $row[$field] > $value) { + return '1'; + } elseif (($op == '<=' || $op == 'is less than or equals') && $row[$field] <= $value) { + return '1'; + } elseif (($op == '>=' || $op == 'is greater than or equals') && $row[$field] >= $value) { + return '1'; + } elseif ($op == 'contains' && preg_match('/' . preg_quote($value, '/') . '/i', $row[$field])) { + return '1'; + } elseif ($op == 'does not contain' && !preg_match('/' . preg_quote($value, '/') . '/i', $row[$field])) { + return '1'; + } else { + return '0'; + } + } + } + + return '1'; + } + + /** + * Validates if the row is within the offset or not if sorting is disabled + * + * @param int $current_row the current row number being processed + * + * @return bool + */ + protected function _validate_offset($current_row) { + return + $this->sort_by !== null || + $this->offset === null || + $current_row >= $this->offset || + ($this->heading && $current_row == 0); + } + + /** + * Enclose values if needed + * - only used by unparse() + * + * @param string|null $value Cell value to process + * @param string $delimiter Character to put between cells on the same row + * + * @return string Processed value + */ + protected function _enclose_value($value, $delimiter) { + if ($value !== null && $value != '') { + $delimiter_quoted = $delimiter ? + preg_quote($delimiter, '/') . "|" + : ''; + $enclosure_quoted = preg_quote($this->enclosure, '/'); + $pattern = "/" . $delimiter_quoted . $enclosure_quoted . "|\n|\r/i"; + if ($this->enclose_all || preg_match($pattern, $value) || strpos($value, ' ') === 0 || substr($value, -1) == ' ') { + $value = str_replace($this->enclosure, $this->enclosure . $this->enclosure, $value); + $value = $this->enclosure . $value . $this->enclosure; + } + } + + return $value; + } + + /** + * Check file data + * + * @param string|null $file local filename + * + * @return bool + */ + protected function _check_data($file = null) { + if (empty($this->file_data)) { + if (is_null($file)) { + $file = $this->file; + } + + return $this->loadFile($file); + } + + return true; + } + + /** + * Check if passed info might be delimiter. + * Only used by find_delimiter + * + * @param string $char Potential field separating character + * @param array $array Frequency + * @param int $depth Number of analyzed rows + * @param string $preferred Preferred delimiter characters + * + * @return string|false special string used for delimiter selection, or false + */ + protected function _check_count($char, $array, $depth, $preferred) { + if ($depth === count($array)) { + $first = null; + $equal = null; + $almost = false; + foreach ($array as $value) { + if ($first == null) { + $first = $value; + } elseif ($value == $first && $equal !== false) { + $equal = true; + } elseif ($value == $first + 1 && $equal !== false) { + $equal = true; + $almost = true; + } else { + $equal = false; + } + } + + if ($equal || $depth === 1) { + $match = $almost ? 2 : 1; + $pref = strpos($preferred, $char); + $pref = ($pref !== false) ? str_pad($pref, 3, '0', STR_PAD_LEFT) : '999'; + + return $pref . $match . '.' . (99999 - str_pad($first, 5, '0', STR_PAD_LEFT)); + } else { + return false; + } + } + return false; + } + + /** + * Read local file. + * + * @param string $filePath local filename + * + * @return string|false Data from file, or false on failure + */ + protected function _rfile($filePath) { + if (is_readable($filePath)) { + $data = file_get_contents($filePath); + if ($data === false) { + return false; + } + + if (preg_match('/\.php$/i', $filePath) && preg_match('/<\?.*?\?>(.*)/ms', $data, $strip)) { + // Return section behind closing tags. + // This parsing is deprecated and will be removed in v2.0.0. + $data = ltrim($strip[1]); + } + + return rtrim($data, "\r\n"); + } + + return false; + } + + /** + * Write to local file + * + * @param string $file local filename + * @param string $content data to write to file + * @param string $mode fopen() mode + * @param int $lock flock() mode + * + * @return bool + * True on success + * + */ + protected function _wfile($file, $content = '', $mode = 'wb', $lock = LOCK_EX) { + if ($fp = fopen($file, $mode)) { + flock($fp, $lock); + $re = fwrite($fp, $content); + $re2 = fclose($fp); + + if ($re !== false && $re2 !== false) { + return true; + } + } + + return false; + } + + /** + * Detect separator using a nonstandard hack: such file starts with the + * first line containing only "sep=;", where the last character is the + * separator. Microsoft Excel is able to open such files. + * + * @param string $data file data + * + * @return string|false detected delimiter, or false if none found + */ + protected function _get_delimiter_from_sep_row($data) { + $sep = false; + // 32 bytes should be quite enough data for our sniffing, chosen arbitrarily + $sepPrefix = substr($data, 0, 32); + if (preg_match('/^sep=(.)\\r?\\n/i', $sepPrefix, $sepMatch)) { + // we get separator. + $sep = $sepMatch[1]; + } + return $sep; + } + + /** + * Support for Excel-compatible sep=? row. + * + * @param string $data_string file data to be updated + * + * @return bool TRUE if sep= line was found at the very beginning of the file + */ + protected function _detect_and_remove_sep_row_from_data(&$data_string) { + $sep = $this->_get_delimiter_from_sep_row($data_string); + if ($sep === false) { + return false; + } + + $this->delimiter = $sep; + + // likely to be 5, but let's not assume we're always single-byte. + $pos = 4 + strlen($sep); + // the next characters should be a line-end + if (substr($data_string, $pos, 1) === "\r") { + $pos++; + } + if (substr($data_string, $pos, 1) === "\n") { + $pos++; + } + + // remove delimiter and its line-end (the data param is by-ref!) + $data_string = substr($data_string, $pos); + return true; + } + + /** + * @param int $search_depth Number of rows to analyze + * @param string $preferred Preferred delimiter characters + * @param string $enclosure Enclosure character, default is double quote + * @param string $data The file content + */ + protected function _guess_delimiter($search_depth, $preferred, $enclosure, $data) { + $chars = []; + $strlen = strlen($data); + $enclosed = false; + $current_row = 1; + $to_end = true; + + // The dash is the only character we don't want quoted, as it would + // prevent character ranges within $auto_non_chars: + $quoted_auto_non_chars = preg_quote($this->auto_non_chars, '/'); + $quoted_auto_non_chars = str_replace('\-', '-', $quoted_auto_non_chars); + $pattern = '/[' . $quoted_auto_non_chars . ']/i'; + + // walk specific depth finding possible delimiter characters + for ($i = 0; $i < $strlen; $i++) { + $ch = $data[$i]; + $nch = isset($data[$i + 1]) ? $data[$i + 1] : false; + $pch = isset($data[$i - 1]) ? $data[$i - 1] : false; + + // open and closing quotes + $is_newline = ($ch == "\n" && $pch != "\r") || $ch == "\r"; + if ($ch == $enclosure) { + if (!$enclosed || $nch != $enclosure) { + $enclosed = !$enclosed; + } elseif ($enclosed) { + $i++; + } + + // end of row + } elseif ($is_newline && !$enclosed) { + if ($current_row >= $search_depth) { + $strlen = 0; + $to_end = false; + } else { + $current_row++; + } + + // count character + } elseif (!$enclosed) { + if (!preg_match($pattern, $ch)) { + if (!isset($chars[$ch][$current_row])) { + $chars[$ch][$current_row] = 1; + } else { + $chars[$ch][$current_row]++; + } + } + } + } + + // filtering + $depth = $to_end ? $current_row - 1 : $current_row; + $filtered = []; + foreach ($chars as $char => $value) { + if ($match = $this->_check_count($char, $value, $depth, $preferred)) { + $filtered[$match] = $char; + } + } + + // capture most probable delimiter + ksort($filtered); + $this->delimiter = reset($filtered); + } + + /** + * getCollection + * Returns a Illuminate/Collection object + * This may prove to be helpful to people who want to + * create macros, and or use map functions + * + * @access public + * @link https://laravel.com/docs/5.6/collections + * + * @throws \ErrorException - If the Illuminate\Support\Collection class is not found + * + * @return Collection + */ + public function getCollection() { + //does the Illuminate\Support\Collection class exists? + //this uses the autoloader to try to determine + //@see http://php.net/manual/en/function.class-exists.php + if (class_exists('Illuminate\Support\Collection', true) == false) { + throw new \ErrorException('It would appear you have not installed the illuminate/support package!'); + } + + //return the collection + return new Collection($this->data); + } +} diff --git a/vendor/parsecsv/php-parsecsv/src/enums/AbstractEnum.php b/vendor/parsecsv/php-parsecsv/src/enums/AbstractEnum.php new file mode 100644 index 0000000..19dae3f --- /dev/null +++ b/vendor/parsecsv/php-parsecsv/src/enums/AbstractEnum.php @@ -0,0 +1,38 @@ +isValid($value)) { + throw new \UnexpectedValueException("Value '$value' is not part of the enum " . get_called_class()); + } + $this->value = $value; + } + + public static function getConstants() { + $class = get_called_class(); + $reflection = new \ReflectionClass($class); + + return $reflection->getConstants(); + } + + /** + * Check if enum value is valid + * + * @param $value + * + * @return bool + */ + public static function isValid($value) { + return in_array($value, static::getConstants(), true); + } +} diff --git a/vendor/parsecsv/php-parsecsv/src/enums/DatatypeEnum.php b/vendor/parsecsv/php-parsecsv/src/enums/DatatypeEnum.php new file mode 100644 index 0000000..7f490e9 --- /dev/null +++ b/vendor/parsecsv/php-parsecsv/src/enums/DatatypeEnum.php @@ -0,0 +1,120 @@ + null, + self::TYPE_INT => 'isValidInteger', + self::TYPE_BOOL => 'isValidBoolean', + self::TYPE_FLOAT => 'isValidFloat', + self::TYPE_DATE => 'isValidDate', + ); + + /** + * Checks data type for given string. + * + * @param string $value + * + * @return bool|string + */ + public static function getValidTypeFromSample($value) { + $value = trim((string) $value); + + if (empty($value)) { + return false; + } + + foreach (self::$validators as $type => $validator) { + if ($validator === null) { + continue; + } + + if (method_exists(__CLASS__, $validator) && self::$validator($value)) { + return $type; + } + } + + return self::__DEFAULT; + } + + /** + * Check if string is float value. + * + * @param string $value + * + * @return bool + */ + private static function isValidFloat($value) { + return (bool) preg_match(self::REGEX_FLOAT, $value); + } + + /** + * Check if string is integer value. + * + * @param string $value + * + * @return bool + */ + private static function isValidInteger($value) { + return (bool) preg_match(self::REGEX_INT, $value); + } + + /** + * Check if string is boolean. + * + * @param string $value + * + * @return bool + */ + private static function isValidBoolean($value) { + return (bool) preg_match(self::REGEX_BOOL, $value); + } + + /** + * Check if string is date. + * + * @param string $value + * + * @return bool + */ + private static function isValidDate($value) { + return (bool) strtotime($value); + } +} diff --git a/vendor/parsecsv/php-parsecsv/src/enums/FileProcessingModeEnum.php b/vendor/parsecsv/php-parsecsv/src/enums/FileProcessingModeEnum.php new file mode 100644 index 0000000..b545c68 --- /dev/null +++ b/vendor/parsecsv/php-parsecsv/src/enums/FileProcessingModeEnum.php @@ -0,0 +1,28 @@ + SORT_REGULAR, + self::SORT_TYPE_STRING => SORT_STRING, + self::SORT_TYPE_NUMERIC => SORT_NUMERIC, + ); + + public static function getSorting($type) { + if (array_key_exists($type, self::$sorting)) { + return self::$sorting[$type]; + } + + return self::$sorting[self::__DEFAULT]; + } +} diff --git a/vendor/parsecsv/php-parsecsv/src/extensions/DatatypeTrait.php b/vendor/parsecsv/php-parsecsv/src/extensions/DatatypeTrait.php new file mode 100644 index 0000000..c0a4c10 --- /dev/null +++ b/vendor/parsecsv/php-parsecsv/src/extensions/DatatypeTrait.php @@ -0,0 +1,102 @@ += 5.5 + * + * @uses DatatypeEnum::getValidTypeFromSample + * + * @return array|bool + */ + public function getDatatypes() { + if (empty($this->data)) { + $this->data = $this->_parse_string(); + } + if (!is_array($this->data)) { + throw new \UnexpectedValueException('No data set yet.'); + } + + $result = []; + foreach ($this->titles as $cName) { + $column = array_column($this->data, $cName); + $cDatatypes = array_map(DatatypeEnum::class . '::getValidTypeFromSample', $column); + + $result[$cName] = $this->getMostFrequentDatatypeForColumn($cDatatypes); + } + + $this->data_types = $result; + + return !empty($this->data_types) ? $this->data_types : []; + } + + /** + * Check data type of titles / first row for auto detecting if this could be + * a heading line. + * + * Requires PHP >= 5.5 + * + * @uses DatatypeEnum::getValidTypeFromSample + * + * @return bool + */ + public function autoDetectFileHasHeading() { + if (empty($this->data)) { + throw new \UnexpectedValueException('No data set yet.'); + } + + if ($this->heading) { + $firstRow = $this->titles; + } else { + $firstRow = $this->data[0]; + } + + $firstRow = array_filter($firstRow); + if (empty($firstRow)) { + return false; + } + + $firstRowDatatype = array_map(DatatypeEnum::class . '::getValidTypeFromSample', $firstRow); + + return $this->getMostFrequentDatatypeForColumn($firstRowDatatype) === DatatypeEnum::TYPE_STRING; + } +}