commit b4fda6b9c982264760129f354149d6c038bdeee2 Author: mwpn Date: Thu Mar 5 14:37:36 2026 +0700 init backend presensi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87e86b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,126 @@ +#------------------------- +# Operating Specific Junk Files +#------------------------- + +# OS X +.DS_Store +.AppleDouble +.LSOverride + +# OS X Thumbnails +._* + +# Windows image file caches +Thumbs.db +ehthumbs.db +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Linux +*~ + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +#------------------------- +# Environment Files +#------------------------- +# These should never be under version control, +# as it poses a security risk. +.env +.vagrant +Vagrantfile + +#------------------------- +# Temporary Files +#------------------------- +writable/cache/* +!writable/cache/index.html + +writable/logs/* +!writable/logs/index.html + +writable/session/* +!writable/session/index.html + +writable/uploads/* +!writable/uploads/index.html + +writable/debugbar/* +!writable/debugbar/index.html + +php_errors.log + +#------------------------- +# User Guide Temp Files +#------------------------- +user_guide_src/build/* +user_guide_src/cilexer/build/* +user_guide_src/cilexer/dist/* +user_guide_src/cilexer/pycilexer.egg-info/* + +#------------------------- +# Test Files +#------------------------- +tests/coverage* + +# Don't save phpunit under version control. +phpunit + +#------------------------- +# Composer +#------------------------- +vendor/ + +#------------------------- +# IDE / Development Files +#------------------------- + +# Modules Testing +_modules/* + +# phpenv local config +.php-version + +# Jetbrains editors (PHPStorm, etc) +.idea/ +*.iml + +# NetBeans +/nbproject/ +/build/ +/nbbuild/ +/dist/ +/nbdist/ +/nbactions.xml +/nb-configuration.xml +/.nb-gradle/ + +# Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache +*.sublime-workspace +*.sublime-project +.phpintel +/api/ + +# Visual Studio Code +.vscode/ + +/results/ +/phpunit*.xml diff --git a/ACADEMIC_MASTER_AUDIT.md b/ACADEMIC_MASTER_AUDIT.md new file mode 100644 index 0000000..9bc48e3 --- /dev/null +++ b/ACADEMIC_MASTER_AUDIT.md @@ -0,0 +1,68 @@ +# Academic Master Audit + +**Date:** 2026-02-22 + +## STEP 1 — AUDIT EXISTING FEATURES + +| # | Item | Status | Notes | +|---|------|--------|-------| +| 1 | SubjectModel exists | **OK** | `App\Modules\Academic\Models\SubjectModel` | +| 2 | Subject CRUD API | **Exists but incomplete** | GET only; no POST/PUT/DELETE. No `code` field in DB. | +| 3 | Dashboard page for Subjects | **Missing** | No route `/dashboard/academic/subjects` | +| 4 | Teacher management page | **Missing** | No `/dashboard/academic/teachers` | +| 5 | Users (GURU_MAPEL/WALI_KELAS) manageable via UI | **Missing** | GET /api/users?role= exists; no POST/PUT/DELETE for users; no UI | +| 6 | Lesson Slots page in dashboard | **Missing** | Full CRUD API exists; no dashboard page | +| 7 | Schedule Builder linked from settings | **Missing** | No Academic Settings hub; Schedule Builder only in sidebar | +| 8 | Navigation menu groups academic features | **Exists but incomplete** | Only Classes + Schedule Builder under ADMIN; no group label, no Settings/Subjects/Teachers/Lesson Slots | + +--- + +## Summary (after implementation) + +- **OK:** SubjectModel, Classes page, Schedule Builder API & page, Lesson Slots API. +- **Incomplete:** Subject API (GET only), Sidebar (no grouping, missing links). +- **Missing:** Academic Settings hub, Subjects dashboard page, Teachers dashboard + user CRUD API, Lesson Slots dashboard page, Sidebar Academic group with all links. + +--- + +## STEP 6 — IMPLEMENTATION SUMMARY + +### What was added + +1. **Academic Settings Hub** + - Route: `GET /dashboard/academic/settings` (filter: `dashboard_admin_page`) + - View: `app/Views/dashboard/academic_settings.php` — card grid linking to Classes, Subjects, Teachers, Lesson Slots, Schedule Builder + +2. **Subject management (full)** + - Migration: `AddCodeToSubjectsTable` — added `code` VARCHAR(50) NULL to `subjects` + - SubjectModel + Subject entity: added `code` to allowedFields and validation (optional) + - API: `POST /api/academic/subjects`, `PUT /api/academic/subjects/{id}`, `DELETE /api/academic/subjects/{id}` + - Dashboard: `GET /dashboard/academic/subjects`, view `dashboard/subjects.php` — table (Name, Code, Actions), Add/Edit modal, Delete confirm, vanilla JS fetch + +3. **Teacher management** + - API: `GET /api/academic/teachers` — list users with role GURU_MAPEL or WALI_KELAS (with role names) + - API: `POST /api/users` (body: name, email, password, role_code), `PUT /api/users/{id}`, `DELETE /api/users/{id}` — admin_only + - Dashboard: `GET /dashboard/academic/teachers`, view `dashboard/teachers.php` — list (name, email, role), Add/Edit (name, email, password, role), Delete confirm + +4. **Lesson Slots dashboard page** + - Route: `GET /dashboard/academic/lesson-slots` + - View: `dashboard/lesson_slots.php` — table (No, Jam Mulai, Jam Selesai, Actions), Add/Edit modal, Delete confirm; uses existing `/api/academic/lesson-slots` CRUD + +5. **Sidebar (Academic group)** + - Group label "Academic" (ADMIN only) + - Links: Settings, Classes, Students, Subjects, Teachers, Lesson Slots, Schedule Builder + - Students still points to `#` (no page yet) + +### What was fixed + +- Subject API completed with create/update/delete and optional `code` field. +- Navigation: academic features grouped under "Academic" with all management links. + +### What was already existing + +- SubjectModel, Subject entity, GET /api/academic/subjects. +- Classes: full CRUD API and dashboard page. +- Lesson Slots: full CRUD API (no dashboard page before). +- Schedule Builder: route and page; now linked from Settings hub and sidebar. +- GET /api/users?role= for listing by role. +- Attendance logic, ScheduleResolver, RBAC — not modified. diff --git a/DAPODIK_SYNC_AUDIT.md b/DAPODIK_SYNC_AUDIT.md new file mode 100644 index 0000000..233cb8a --- /dev/null +++ b/DAPODIK_SYNC_AUDIT.md @@ -0,0 +1,62 @@ +# Dapodik Sync — Pre-Implementation Audit + +| # | Item | Status | Notes | +|---|------|--------|-------| +| 1 | Students dashboard page + API | **EXISTS** | GET/POST/PUT/DELETE /api/academic/students; GET /dashboard/academic/students | +| 2 | Existing Dapodik service code | **MISSING** | No DapodikClient or sync logic; only comments in migrations/model | +| 3 | Existing mapping table | **MISSING** | No dapodik_rombel_mappings or similar | +| 4 | Settings page link for Dapodik | **MISSING** | No Dapodik card in Academic Settings hub | + +**Summary:** Students management exists and is ready. Dapodik integration (client, mapping table, settings link) is missing and will be implemented. + +--- + +## Implemented (post-implementation) + +- Migration `dapodik_rombel_mappings`, DapodikClient, DapodikSyncService, DapodikSyncController, dashboard Dapodik page, Students UNMAPPED badge + filter. + +### Required .env (add to backend/.env) + +``` +DAPODIK_BASE_URL=http://192.168.7.5:5774/WebService +DAPODIK_TOKEN=bt5ON1LZPSsaJ6h +DAPODIK_NPSN=20227474 +``` + +### Brief test steps + +**1) cURL — Sync (requires valid token + base URL)** + +```bash +# Login as ADMIN first; use session cookie or pass auth. +curl -X POST "http://localhost/sman1/backend/public/api/academic/dapodik/sync/students" \ + -H "Content-Type: application/json" \ + -H "X-Requested-With: XMLHttpRequest" \ + -d "{\"limit\":50,\"max_pages\":2}" \ + --cookie "ci_session=YOUR_SESSION" +``` + +**2) cURL — List rombel mappings** + +```bash +curl "http://localhost/sman1/backend/public/api/academic/dapodik/rombels" \ + -H "X-Requested-With: XMLHttpRequest" \ + --cookie "ci_session=YOUR_SESSION" +``` + +**3) cURL — Update mapping** + +```bash +curl -X PUT "http://localhost/sman1/backend/public/api/academic/dapodik/rombels/1" \ + -H "Content-Type: application/json" \ + -H "X-Requested-With: XMLHttpRequest" \ + -d "{\"class_id\":1}" \ + --cookie "ci_session=YOUR_SESSION" +``` + +**4) UI** + +- Login as ADMIN → Academic → Settings → open **Dapodik** card. +- Click **Jalankan Sinkronisasi**; check summary in pre block and toast. +- In **Mapping Rombel**, set "Kelas (internal)" and click **Simpan** per row; toggle **Hanya unmapped**. +- Go to **Siswa** → tick **Hanya unmapped** to see only unmapped students; confirm **UNMAPPED** badge in Kelas column when class_id is null. diff --git a/DASHBOARD_FEATURE_AUDIT.md b/DASHBOARD_FEATURE_AUDIT.md new file mode 100644 index 0000000..67c8e06 --- /dev/null +++ b/DASHBOARD_FEATURE_AUDIT.md @@ -0,0 +1,227 @@ +# Full Dashboard Feature Audit Summary + +**Goal:** Understand why only a few menus appear in the dashboard UI even though many modules exist. + +--- + +## 1. LIST ALL EXISTING DASHBOARD PAGES + +**Sources scanned:** `app/Config/Routes.php`, all `app/Modules/*/Routes.php`, `DashboardPageController`, `DashboardAcademicController`, any controller rendering `layouts/main`. + +**Finding:** All dashboard **web** routes are defined in `app/Config/Routes.php` only. No module defines HTML page routes; modules define only API routes. + +| # | route | controller | view | required_filter | roles_allowed_if_known | +|---|--------|------------|------|------------------|------------------------| +| 1 | `GET /dashboard` | `DashboardPageController::index` | `dashboard/index` | `dashboard_page_auth` | Any authenticated user | +| 2 | `GET /dashboard/attendance/report/(:num)` | `DashboardPageController::attendanceReport` | `dashboard/attendance_report` | `dashboard_page_auth` | Any authenticated user | +| 3 | `GET /dashboard/schedule/today` | `DashboardPageController::scheduleToday` | `dashboard/schedule_today` | `dashboard_page_auth` | Any authenticated user | +| 4 | `GET /dashboard/academic/schedule-builder/(:num)` | `DashboardAcademicController::scheduleBuilder` | `dashboard/schedule_builder` | `dashboard_admin_page` | **ADMIN only** | + +**JSON output (as requested):** + +```json +[ + { + "route": "GET /dashboard", + "controller": "DashboardPageController::index", + "view": "dashboard/index", + "required_filter": "dashboard_page_auth", + "roles_allowed_if_known": "any authenticated" + }, + { + "route": "GET /dashboard/attendance/report/(:num)", + "controller": "DashboardPageController::attendanceReport", + "view": "dashboard/attendance_report", + "required_filter": "dashboard_page_auth", + "roles_allowed_if_known": "any authenticated" + }, + { + "route": "GET /dashboard/schedule/today", + "controller": "DashboardPageController::scheduleToday", + "view": "dashboard/schedule_today", + "required_filter": "dashboard_page_auth", + "roles_allowed_if_known": "any authenticated" + }, + { + "route": "GET /dashboard/academic/schedule-builder/(:num)", + "controller": "DashboardAcademicController::scheduleBuilder", + "view": "dashboard/schedule_builder", + "required_filter": "dashboard_admin_page", + "roles_allowed_if_known": "ADMIN" + } +] +``` + +--- + +## 2. LIST ALL DASHBOARD API ENDPOINTS + +**Sources:** `app/Modules/Dashboard/Routes.php`, `app/Modules/Attendance/Routes.php`, `app/Modules/Academic/Routes.php`, `app/Config/Routes.php` (api/users). +**Note:** `api/dashboard/*` is protected by global filter `dashboard_auth` in `app/Config/Filters.php` (session required). + +### Grouped by feature + +**schedules** +- `GET /api/dashboard/schedules/today` — Today's schedules (role-filtered). +- `GET /api/dashboard/schedules/current` — Current lesson or next (role-filtered). +- `GET /api/academic/schedules/class/(:num)` — Weekly schedule grid for class (admin_only). +- `POST /api/academic/schedules/bulk-save` — Bulk save schedules (admin_only). +- `GET /api/academic/lesson-slots` — List lesson slots (admin_only). +- `GET /api/academic/subjects` — List subjects (admin_only). + +**attendance** +- `GET /api/dashboard/attendance/progress/current` — Live attendance progress for current schedule. +- `POST /api/attendance/checkin` — Device/mobile check-in (no dashboard filter; used by devices). +- `GET /api/attendance/report/schedule/(:num)` — Schedule report data (used by attendance report page). + +**realtime** +- `GET /api/dashboard/summary` — Dashboard summary. +- `GET /api/dashboard/realtime` — Realtime stats. +- `GET /api/dashboard/stream` — SSE stream for live attendance. +- `GET /api/dashboard/devices` — Devices list. + +**reports** +- Attendance report page uses: `GET /api/attendance/report/schedule/(:num)` (plus date query). No dedicated “reports” API group. + +**devices** +- `GET /api/dashboard/devices` — Used by dashboard UI. +- `POST /api/device/login` — Device auth (not dashboard UI). +- `GET /api/mobile/ping`, `GET /api/mobile/bootstrap` — Mobile (not dashboard UI). + +**analytics** +- No dedicated analytics API. Summary/realtime are the closest. + +**other (used for dashboard UI)** +- `GET /api/users?role=GURU_MAPEL` — Schedule Builder teacher dropdown (admin_only). + +--- + +## 3. SIDEBAR MENU SOURCE + +**Location:** `app/Views/partials/sidebar.php` +**Included by:** `app/Views/layouts/main.php` via ``. +No separate “layouts/sidebar.php”; no config/menu builder found. + +### How menu items are added + +- **Fully static:** The sidebar is a single PHP file with hardcoded `` links. +- **No config:** No array or config file drives the menu. +- **No role logic:** The same HTML is rendered for every role; there is no `if (user has role X) show link Y`. +- **No dynamic highlighting:** The first item (Dashboard) uses `bg-primary/10 text-primary`; no logic sets “active” by current URI. + +### Current sidebar entries + +| Order | Label | href | Notes | +|-------|-----------------|------|--------| +| 1 | Dashboard | `/dashboard` | Real route. | +| 2 | Daily Schedule | `/dashboard/schedule/today` | Real route. | +| 3 | Schedule Builder| `/dashboard/academic/schedule-builder/1` | Real route; class ID hardcoded to 1. | +| 4 | Students | `#` | Placeholder; no page. | +| 5 | Attendance | `#` | Placeholder; no page. | +| 6 | Devices | `#` | Placeholder; no page. | + +### Why some features are not visible + +1. **Attendance Report** — There is a full page (`/dashboard/attendance/report/{id}`) but **no sidebar link**. Users only reach it via “Open Attendance” on the main dashboard (current lesson) or on Daily Schedule (per row). So the feature exists but is not in the menu. +2. **Students, Attendance (list), Devices** — Sidebar shows labels and icons but links are `#`. There are no corresponding dashboard pages or list views in the codebase; only placeholders. +3. **Current Lesson** — Not a separate page; it’s a **card on the main dashboard** plus live progress. No sidebar entry (by design). +4. **Academic setup** — Lesson slots, subjects, schedule management exist as **APIs** (admin_only) and one **page** (Schedule Builder). There is no “Academic Setup” or “Lesson Slots” menu item; only “Schedule Builder” with a fixed class ID. + +--- + +## 4. DETECT MISSING MENU ITEMS + +**Comparison:** Existing dashboard pages (Section 1) vs actual sidebar links (Section 3). + +| route | feature_name | reason_not_visible | +|-------|--------------|--------------------| +| `GET /dashboard/attendance/report/(:num)` | Attendance Report (per schedule) | No sidebar entry; only reachable via “Open Attendance” on dashboard or daily schedule. | + +**Missing menu list (structured):** + +```json +[ + { + "route": "GET /dashboard/attendance/report/(:num)", + "feature_name": "Attendance Report", + "reason_not_visible": "Not in sidebar; only linked from Current Lesson card and Daily Schedule row (Open Attendance)." + } +] +``` + +**Pages that are in the sidebar:** Dashboard, Daily Schedule, Schedule Builder. +**Placeholder items (no real route):** Students, Attendance (list), Devices — these are visible in the menu but point to `#` and have no backend page. + +--- + +## 5. ROLE FILTER CHECK + +**Filters used on dashboard-related routes:** + +| Filter | Purpose | Used on | +|--------|---------|--------| +| `dashboard_page_auth` | Must be logged in (session). No role check. Redirects to `/login` if not authenticated. | `GET /dashboard`, `GET /dashboard/attendance/report/(:num)`, `GET /dashboard/schedule/today` | +| `dashboard_admin_page` | Must be logged in **and** have role **ADMIN**. Otherwise redirect to `/dashboard` with error. | `GET /dashboard/academic/schedule-builder/(:num)` | +| `admin_only` | API filter: must be logged in and ADMIN. Returns 401/403 JSON for API. | All `api/academic/*`, `GET /api/users` | +| `dashboard_auth` | Session required for API. Applied globally to `api/dashboard/*` in `Config/Filters.php`. No role check. | All `api/dashboard/*` | + +**Which roles can see which pages:** + +- **Dashboard (main), Daily Schedule, Attendance Report:** Any **authenticated** user (any role). Backend may still filter data by role (e.g. schedules by WALI_KELAS, GURU_MAPEL, ORANG_TUA). +- **Schedule Builder:** **ADMIN only.** Others get redirect to `/dashboard` with “Akses hanya untuk Admin.” +- **API:** + - `api/dashboard/*`: any authenticated user (data filtered by role in services). + - `api/academic/*` and `api/users`: **ADMIN only** (403 if not admin). + +The sidebar does **not** hide Schedule Builder for non-admins; a non-admin who clicks it is redirected after the filter runs. + +--- + +## 6. FINAL SUMMARY + +### Current dashboard capability level + +- **Implemented and reachable from UI:** Main dashboard (realtime + current lesson + live progress), Daily Schedule, Schedule Builder (class 1 hardcoded), Attendance Report (via links only, not menu). +- **Implemented but not exposed in menu:** Attendance Report as a direct menu item; no “list of schedules” or “pick a class” for Schedule Builder. +- **Placeholder only:** Students, Attendance (list), Devices — labels in sidebar but no pages or list views. + +### Features implemented but hidden or partially hidden + +1. **Attendance Report** — Full page and API exist; no sidebar link. Users must use “Open Attendance” from dashboard or daily schedule. +2. **Schedule Builder** — Only one class (ID 1) linked from sidebar; no class selector or list. +3. **Current Lesson + Live Progress** — On main dashboard only; no separate “Current Lesson” menu (by design). +4. **Academic APIs** — Lesson slots, subjects, schedules CRUD and bulk-save exist for admin; only Schedule Builder page is in the menu. + +### Why only a few menus “work” + +- **Three items** point to real routes: Dashboard, Daily Schedule, Schedule Builder. +- **One real page** (Attendance Report) has **no** sidebar entry. +- **Three items** (Students, Attendance, Devices) are **placeholders** (`href="#"`) with no backend pages. + +So the menu looks like “many” items, but only three are real destinations; one important page is missing from the menu, and three are non-functional placeholders. + +### Recommended menu structure for school system + +Aligning with your desired structure and current codebase: + +| # | Menu label | Purpose | Current status / suggestion | +|---|-------------------|--------|------------------------------| +| 1 | Dashboard | Home, current lesson, realtime | Exists. Keep. | +| 2 | Daily Schedule | Today’s schedules by role | Exists. Keep. | +| 3 | Current Lesson | — | No separate page; keep as card on Dashboard. Optional: menu item that scrolls to or highlights that card. | +| 4 | Attendance Reports| List or pick schedule → report | Add link: e.g. “Attendance Reports” → new page that lists schedules (or today’s) with “Open Report” per row, or link to a report index. Currently only per-schedule report exists. | +| 5 | Schedule Builder | Weekly schedule per class | Exists; add class selector or “Schedule Builder” → page that selects class then redirects to `schedule-builder/(:num)`. | +| 6 | Academic Setup | Lesson slots, subjects, maybe classes | No page yet. Add menu item when you have a page (e.g. lesson slots + subjects management). APIs exist (admin_only). | +| 7 | Devices | Device list / management | Add when you have a dashboard page for devices. API `GET /api/dashboard/devices` exists. | +| 8 | Analytics | Future | No implementation; add when built. | + +**Concrete next steps (no code change in this audit):** + +- Add a **sidebar link** for “Attendance Reports” (e.g. to a report index or today’s schedules with report links). +- Replace **Students**, **Attendance**, **Devices** placeholders: either add real routes and link them or remove/hide until implemented. +- Add **class selection** for Schedule Builder (new page or dropdown) so the menu is not tied to class 1 only. +- Optionally add **Academic Setup** and **Devices** when corresponding pages exist; keep **Analytics** for later. + +--- + +*End of audit. No code was modified.* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..24728f6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014-2019 British Columbia Institute of Technology +Copyright (c) 2019-present CodeIgniter Foundation + +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/PROJECT_CONDITION_AUDIT.md b/PROJECT_CONDITION_AUDIT.md new file mode 100644 index 0000000..913f15a --- /dev/null +++ b/PROJECT_CONDITION_AUDIT.md @@ -0,0 +1,257 @@ +# Audit Kondisi Project SMAN 1 — Detail + +**Tanggal:** 2026-02-23 + +--- + +## 1. Struktur Module + +### Module yang sudah ada + +| Module | Lokasi | Isi utama | +|--------|--------|-----------| +| **Academic** | `app/Modules/Academic/` | Classes, Students, Subjects, Teachers, Schedules, Lesson Slots, **DapodikSync** (controller + service + mapping model) | +| **Attendance** | `app/Modules/Attendance/` | AttendanceSession, Checkin, Report | +| **Auth** | `app/Modules/Auth/` | Login, User, Role, UserRole | +| **Dashboard** | `app/Modules/Dashboard/` | Dashboard, Schedule, Attendance, Realtime | +| **Devices** | `app/Modules/Devices/` | Device, Mobile, DeviceAuth | +| **Notification** | `app/Modules/Notification/` | Telegram, Parent, StudentParent | +| **Geo** | `app/Modules/Geo/` | Zone, GeoFence | + +### Cek per modul yang Anda tanyakan + +| Modul | Status | Keterangan | +|-------|--------|------------| +| **Classes** | ✅ Ada | Di dalam **Academic**: ClassModel, ClassEntity, ClassController (CRUD), route GET/POST/PUT/DELETE `/api/academic/classes`, halaman `/dashboard/academic/classes` | +| **Students** | ✅ Ada | Di dalam **Academic**: StudentModel, StudentEntity, StudentController (CRUD + paginasi), route GET/POST/PUT/DELETE `/api/academic/students`, halaman `/dashboard/academic/students` | +| **Attendance** | ✅ Ada | Module **Attendance** terpisah: AttendanceSessionModel, AttendanceCheckinService, AttendanceReportService, dll. Tabel: `attendance_sessions` (bukan `attendance_logs`) | +| **Schedule** | ✅ Ada | Di dalam **Academic**: ScheduleModel, ScheduleManagementController, ScheduleResolverService, lesson_slots. Tabel: `schedules`, `lesson_slots` | +| **DapodikSync** | ✅ Ada | Di dalam **Academic**: DapodikClient, DapodikSyncService, DapodikSyncController, DapodikRombelMappingModel. Route: POST sync/students, GET/PUT rombels. Halaman `/dashboard/academic/dapodik` | + +**Kesimpulan:** Semua modul yang Anda sebut (Classes, Students, Attendance, Schedule, DapodikSync) **sudah ada**. Tidak ada modul terpisah bernama "Classes" atau "Students"; semuanya di bawah **Academic**. + +--- + +## 2. Database + +### Tabel yang ada (beserta struktur) + +**Catatan:** Tidak ada tabel bernama `attendance_logs`. Yang dipakai: **`attendance_sessions`**. + +### 2.1 Tabel `classes` + +| Field | Type | Relasi | +|-------|------|--------| +| id | INT PK AUTO_INCREMENT | - | +| name | VARCHAR(100) | - (rombel, mis. A, B, C) | +| grade | VARCHAR(50) | - (tingkat, mis. 10, X) | +| major | VARCHAR(50) NOT NULL DEFAULT 'IPA' | - (jurusan) | +| wali_user_id | INT NULL | FK → users.id (SET NULL) | +| created_at | DATETIME NULL | - | +| updated_at | DATETIME NULL | - | + +**Relasi keluar:** `students.class_id`, `schedules.class_id`, `dapodik_rombel_mappings.class_id` → classes.id + +--- + +### 2.2 Tabel `students` + +| Field | Type | Relasi | +|-------|------|--------| +| id | INT PK AUTO_INCREMENT | - | +| nisn | VARCHAR(50) NOT NULL UNIQUE | - | +| name | VARCHAR(255) | - | +| gender | VARCHAR(1) NULL | L/P | +| class_id | INT NULL | FK → classes.id (SET NULL) — **nullable untuk unmapped** | +| is_active | TINYINT(1) DEFAULT 1 | - | +| parent_link_code | VARCHAR | (dari migration lain) | +| created_at | DATETIME NULL | - | +| updated_at | DATETIME NULL | - | + +**Relasi:** class_id → classes.id (boleh NULL untuk siswa belum di-map dari Dapodik). + +--- + +### 2.3 Tabel `attendance_sessions` (bukan attendance_logs) + +| Field | Type | Relasi | +|-------|------|--------| +| id | INT PK | - | +| student_id | INT | FK → students.id | +| schedule_id | INT NULL | FK → schedules.id | +| device_id | INT | FK → devices.id | +| attendance_date | DATE | (dari migration unik) | +| checkin_at | DATETIME | - | +| latitude, longitude, confidence | DECIMAL | - | +| status | ENUM(PRESENT, LATE, OUTSIDE_ZONE, NO_SCHEDULE, INVALID_DEVICE) | - | +| created_at, updated_at | DATETIME NULL | - | + +**Relasi:** student_id → students, schedule_id → schedules, device_id → devices. + +--- + +### 2.4 Tabel `schedules` + +| Field | Type | Relasi | +|-------|------|--------| +| id | INT PK | - | +| class_id | INT | FK → classes.id | +| lesson_slot_id | INT NULL | FK → lesson_slots.id | +| subject_id | INT | FK → subjects.id | +| teacher_user_id | INT NULL | FK → users.id | +| teacher_name | VARCHAR(255) | - | +| day_of_week | TINYINT (1=Senin, 7=Minggu) | - | +| start_time, end_time | TIME | - | +| room | VARCHAR(100) NULL | - | +| is_active | TINYINT DEFAULT 1 | - | +| created_at, updated_at | DATETIME NULL | - | + +**Unique:** (class_id, lesson_slot_id, day_of_week). + +**Relasi:** class_id → classes, subject_id → subjects, teacher_user_id → users, lesson_slot_id → lesson_slots. + +--- + +### 2.5 Tabel tambahan terkait + +- **dapodik_rombel_mappings:** id, dapodik_rombel (UNIQUE), class_id (FK → classes, NULL), last_seen_at, created_at, updated_at. +- **lesson_slots:** id, slot_number, start_time, end_time, is_active. +- **subjects:** id, name, code (nullable). + +--- + +## 3. Flow Master Data + +### 3.1 Kelas: input manual atau belum? + +- **Input kelas:** **manual** lewat halaman **Classes** (`/dashboard/academic/classes`), hanya untuk role ADMIN. +- Form: Tingkat (text), Jurusan (text), Rombel (text), Wali Kelas (dropdown user). +- Data disimpan ke tabel **classes** (name = rombel, grade, major, wali_user_id). +- **Jika belum pernah input:** tabel `classes` **kosong** → tidak ada pilihan kelas di mana pun (Schedule Builder, Students, dll.). + +### 3.2 Schedule Builder mengambil data kelas dari mana? + +- **Dari API:** `GET /api/academic/classes` (admin_only). +- Halaman Schedule Builder index (`/dashboard/academic/schedule-builder`) memanggil API ini lalu mengisi **dropdown "Kelas"** (option value = class id, text = nama/rombel). +- Data sumber: **tabel `classes`** via ClassController::index(). + +### 3.3 Kenapa dropdown kelas bisa kosong? + +- **Penyebab:** Tabel **classes tidak punya data** (belum ada satu baris pun). +- **Alur:** Tidak ada kelas → API `/api/academic/classes` mengembalikan array kosong → dropdown hanya berisi "— Pilih kelas —" → Open Builder tidak bisa dipakai. +- **Solusi:** Login sebagai ADMIN → buka **Academic → Classes** → tambah minimal satu kelas (mis. Tingkat 10, Jurusan IPA, Rombel 1). Setelah itu dropdown di Schedule Builder akan terisi. + +--- + +## 4. Dapodik Integration + +| Aspek | Status | Keterangan | +|-------|--------|------------| +| **Controller Dapodik Sync** | ✅ Ada | `App\Modules\Academic\Controllers\DapodikSyncController` | +| **Endpoint API** | ✅ Ada | POST `/api/academic/dapodik/sync/students`, GET `/api/academic/dapodik/rombels`, PUT `/api/academic/dapodik/rombels/{id}` (semua admin_only) | +| **Rencana vs realisasi** | ✅ Sudah diimplementasi | DapodikClient (getSekolah, getPesertaDidik), DapodikSyncService (sync siswa + rombel mapping), halaman Dapodik di dashboard | + +**Env yang dipakai:** DAPODIK_BASE_URL, DAPODIK_TOKEN, DAPODIK_NPSN (di .env). + +--- + +## 5. Student Page + +### Apakah halaman Student sudah tampil? + +- **Route:** ✅ Ada — `GET /dashboard/academic/students` → `DashboardAcademicController::students`, filter `dashboard_admin_page`. +- **Controller:** ✅ Ada — method `students()` me-render view `dashboard/students`. +- **View:** ✅ Ada — `app/Views/dashboard/students.php` (tabel, filter, paginasi, modal tambah/edit/hapus). +- **Query/relasi:** ✅ API GET `/api/academic/students` jalan; join ke `classes` untuk class_label; `class_id` boleh NULL (tidak gagal). + +### Kenapa bisa “belum tampil”? + +Kemungkinan: + +1. **Bukan ADMIN** — halaman dan menu Students hanya untuk role ADMIN. Jika login bukan ADMIN, menu Academic (termasuk Students) tidak muncul. +2. **URL salah** — harus akses tepat: `/dashboard/academic/students` (mis. `http://localhost/sman1/backend/public/dashboard/academic/students`). +3. **Session/redirect** — jika belum login atau filter redirect, akan ke halaman login/dashboard. +4. **Data kosong** — halaman tetap tampil; yang kosong hanya **tabel siswa** (belum ada data siswa). Bukan penyebab “halaman tidak tampil”. + +**Kesimpulan:** Secara kode, route, controller, view, dan relasi **sudah ada dan konsisten**. Kalau “belum tampil”, paling mungkin karena **bukan user ADMIN** atau **salah URL/base URL**. + +--- + +## 6. Problem Diagnosis + +### “Schedule Builder meminta kelas, tapi tidak ada tempat input kelas” + +- **Faktanya:** **Sudah ada tempat input kelas**, yaitu halaman **Classes** (`/dashboard/academic/classes`), hanya untuk ADMIN. +- **Alur yang benar:** + 1. ADMIN buka **Academic → Classes** (atau lewat **Academic → Settings** → kartu Classes). + 2. Klik **Tambah Kelas** → isi Tingkat, Jurusan, Rombel, (opsional) Wali Kelas → Simpan. + 3. Setelah ada minimal 1 kelas, buka **Schedule Builder** → dropdown **Kelas** terisi → pilih kelas → Open Builder. + +Jadi masalahnya bukan “tidak ada tempat input”, melainkan: + +- **User belum pernah mengisi kelas** di halaman Classes, atau +- **Menu Classes tidak terlihat** (mis. karena tidak login sebagai ADMIN). + +**Rekomendasi:** Di halaman Schedule Builder index bisa ditambah kalimat singkat: “Jika dropdown kosong, tambah dulu kelas di **Academic → Classes**.” + +--- + +## 7. Ringkasan Kondisi Project + +| Aspek | Status | Keterangan | +|-------|--------|------------| +| **Module Academic (Classes, Students, Subjects, Teachers, Schedules, Lesson Slots)** | ✅ Sudah jadi | CRUD API + halaman dashboard per entitas | +| **Module Attendance** | ✅ Sudah jadi | attendance_sessions, check-in, report | +| **Module Schedule (jadwal per kelas)** | ✅ Sudah jadi | Schedule Builder, bulk save, lesson_slots | +| **Module DapodikSync** | ✅ Sudah jadi | Sync siswa, mapping rombel → class, halaman Dapodik | +| **Tabel classes, students, schedules, attendance_sessions** | ✅ Sudah jadi | Struktur + relasi jelas (termasuk lesson_slots, subjects, dapodik_rombel_mappings) | +| **Input kelas manual** | ✅ Sudah jadi | Halaman Classes, form lengkap | +| **Schedule Builder pakai data kelas** | ✅ Sudah jadi | Dari GET /api/academic/classes | +| **Halaman Students** | ✅ Sudah jadi | Route, controller, view, API, paginasi, filter | +| **Dropdown kelas kosong** | ⚠️ Setengah jadi | Fitur jalan; kosong karena **belum ada data kelas** di DB | +| **Dapodik sebagai sumber master kelas** | ⚠️ Setengah jadi | Saat ini Dapodik hanya **sync siswa** + mapping **rombel → class**. Kelas internal tetap **buat manual** di Classes; belum ada “sync rombel → buat kelas otomatis” | + +--- + +## 8. Arsitektur saran: Dapodik sebagai sumber utama master data + +Target alur: **DAPODIK → Sync → Classes → Students → Schedule → Attendance**. + +### Kondisi saat ini + +- **Classes:** input manual; tidak ada sync dari Dapodik. +- **Students:** bisa dari Dapodik (sync siswa + mapping rombel → class_id). class_id bisa NULL (unmapped). +- **Schedule & Attendance:** sudah mengandalkan classes + students. + +### Opsi arsitektur + +**Opsi A — Kelas tetap manual (minimal change)** +- Tetap: Classes diisi manual (grade + major + rombel). +- Dapodik: sync siswa + mapping **dapodik_rombel → class_id**. +- Sesuai jika: rombel Dapodik banyak dan Anda ingin satu rombel Dapodik = satu kelas internal yang sudah Anda buat manual. + +**Opsi B — Sync rombel Dapodik → buat/update kelas (disarankan untuk “Dapodik sumber utama”)** + +1. **Tambah sync kelas/rombel dari Dapodik** + - Endpoint Dapodik: daftar rombongan belajar (rombel) per sekolah. + - Logic: untuk setiap rombel Dapodik, **buat atau update** satu baris di `classes` (mis. parse nama rombel jadi grade + major + name/rombel), plus **tetap** isi `dapodik_rombel_mappings` (dapodik_rombel → class_id) agar siswa sync bisa map ke kelas yang sama. + - Aturan: satu `dapodik_rombel` = satu `class_id`; kelas bisa dibuat otomatis dari nama rombel Dapodik (mis. "10 IPA 1" → grade=10, major=IPA, name=1). + +2. **Urutan sync disarankan** + - **Sync rombel/kelas dulu** (baca Dapodik → insert/update `classes` + `dapodik_rombel_mappings`). + - **Lalu sync siswa** (seperti sekarang); mapping rombel → class_id sudah terisi, sehingga siswa dapat class_id otomatis. + +3. **Tetap boleh edit manual** + - Halaman **Classes** tetap dipakai untuk koreksi/naming (grade, major, rombel, wali). + - Setelah sync, admin bisa ubah nama/struktur kelas jika perlu. + +**Opsi C — Hybrid (seperti B + flag sumber)** +- Tambah kolom mis. `source` (manual / dapodik) di `classes` supaya bisa bedakan kelas yang dari Dapodik vs yang buat manual. + +### Rekomendasi singkat + +- **Langkah 1:** Pastikan **ada data kelas** (manual) dulu agar Schedule Builder dan dropdown siswa tidak kosong. +- **Langkah 2:** Jika ingin Dapodik sebagai sumber utama kelas, tambah **sync rombel Dapodik → classes** (Opsi B); jadwalkan sync rombel dulu, baru sync siswa. +- **Langkah 3:** Tetap pakai **Dapodik Sync** yang sudah ada untuk siswa dan mapping rombel → class; setelah kelas bisa dari Dapodik, mapping itu akan mengisi class_id siswa tanpa perlu input kelas manual dulu. + +Dengan ini, arsitektur bergerak ke: **DAPODIK → Sync (rombel + siswa) → Classes & Students terisi → Schedule (manual di builder) → Attendance**. diff --git a/README.md b/README.md new file mode 100644 index 0000000..45f98af --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# CodeIgniter 4 Application Starter + +## What is CodeIgniter? + +CodeIgniter is a PHP full-stack web framework that is light, fast, flexible and secure. +More information can be found at the [official site](https://codeigniter.com). + +This repository holds a composer-installable app starter. +It has been built from the +[development repository](https://github.com/codeigniter4/CodeIgniter4). + +More information about the plans for version 4 can be found in [CodeIgniter 4](https://forum.codeigniter.com/forumdisplay.php?fid=28) on the forums. + +You can read the [user guide](https://codeigniter.com/user_guide/) +corresponding to the latest version of the framework. + +## Installation & updates + +`composer create-project codeigniter4/appstarter` then `composer update` whenever +there is a new release of the framework. + +When updating, check the release notes to see if there are any changes you might need to apply +to your `app` folder. The affected files can be copied or merged from +`vendor/codeigniter4/framework/app`. + +## Setup + +Copy `env` to `.env` and tailor for your app, specifically the baseURL +and any database settings. + +## Important Change with index.php + +`index.php` is no longer in the root of the project! It has been moved inside the *public* folder, +for better security and separation of components. + +This means that you should configure your web server to "point" to your project's *public* folder, and +not to the project root. A better practice would be to configure a virtual host to point there. A poor practice would be to point your web server to the project root and expect to enter *public/...*, as the rest of your logic and the +framework are exposed. + +**Please** read the user guide for a better explanation of how CI4 works! + +## Repository Management + +We use GitHub issues, in our main repository, to track **BUGS** and to track approved **DEVELOPMENT** work packages. +We use our [forum](http://forum.codeigniter.com) to provide SUPPORT and to discuss +FEATURE REQUESTS. + +This repository is a "distribution" one, built by our release preparation script. +Problems with it can be raised on our forum, or as issues in the main repository. + +## Server Requirements + +PHP version 8.2 or higher is required, with the following extensions installed: + +- [intl](http://php.net/manual/en/intl.requirements.php) +- [mbstring](http://php.net/manual/en/mbstring.installation.php) + +> [!WARNING] +> - The end of life date for PHP 7.4 was November 28, 2022. +> - The end of life date for PHP 8.0 was November 26, 2023. +> - The end of life date for PHP 8.1 was December 31, 2025. +> - If you are still using below PHP 8.2, you should upgrade immediately. +> - The end of life date for PHP 8.2 will be December 31, 2026. + +Additionally, make sure that the following extensions are enabled in your PHP: + +- json (enabled by default - don't turn it off) +- [mysqlnd](http://php.net/manual/en/mysqlnd.install.php) if you plan to use MySQL +- [libcurl](http://php.net/manual/en/curl.requirements.php) if you plan to use the HTTP\CURLRequest library diff --git a/SETUP_PRESENSI.md b/SETUP_PRESENSI.md new file mode 100644 index 0000000..517be4d --- /dev/null +++ b/SETUP_PRESENSI.md @@ -0,0 +1,43 @@ +# Pengaturan Presensi di Backend (Dashboard) + +Semua pengaturan presensi **terpusat** di satu tempat. Login sebagai **Admin** dulu. + +--- + +## Satu halaman: Pengaturan Presensi + +**Dashboard** → sidebar kiri → **Pengaturan Presensi** (icon pin map). + +Di sini admin mengatur: + +1. **Koordinat Sekolah (Zona Presensi)** + Satu koordinat untuk **seluruh sekolah**: Latitude, Longitude, Radius (meter). + Semua absen (mobile & device) hanya valid jika siswa berada di dalam radius ini. Tidak ada lagi pengaturan koordinat per device. + +2. **Jadwal Masuk & Pulang** + Window waktu untuk absen masuk dan absen pulang (jam mulai–akhir). + Contoh: Masuk 06:30–07:00, Pulang 14:00–14:30. + +Simpan → dipakai untuk validasi geofence dan jadwal. + +--- + +## Jam per mapel (opsional) + +Kalau ingin jadwal per mata pelajaran (slot jam pelajaran, Schedule Builder), tetap lewat: + +- **Pengaturan Academic** → **Jam Pelajaran** (slot jam) + **Schedule Builder** (jadwal per kelas). + +Pengaturan Presensi dipakai untuk **base** masuk/pulang; detail slot mapel tetap di Academic bila dipakai. + +--- + +## Ringkas + +| Yang diatur | Menu | Keterangan | +|--------------------|-------------------------|-------------------------------------| +| Koordinat sekolah | **Pengaturan Presensi** | Satu zona untuk semua absen | +| Jam masuk & pulang | **Pengaturan Presensi** | Satu set jam masuk dan pulang | +| Jam per mapel | Pengaturan Academic | Jam Pelajaran + Schedule Builder | + +**Device Absen** di sidebar hanya untuk daftar/monitoring device; koordinat tidak lagi diatur per device. diff --git a/app/.htaccess b/app/.htaccess new file mode 100644 index 0000000..3462048 --- /dev/null +++ b/app/.htaccess @@ -0,0 +1,6 @@ + + Require all denied + + + Deny from all + diff --git a/app/Common.php b/app/Common.php new file mode 100644 index 0000000..95f5544 --- /dev/null +++ b/app/Common.php @@ -0,0 +1,15 @@ + + */ + public array $allowedHostnames = []; + + /** + * -------------------------------------------------------------------------- + * Index File + * -------------------------------------------------------------------------- + * + * Typically, this will be your `index.php` file, unless you've renamed it to + * something else. If you have configured your web server to remove this file + * from your site URIs, set this variable to an empty string. + */ + public string $indexPage = 'index.php'; + + /** + * -------------------------------------------------------------------------- + * URI PROTOCOL + * -------------------------------------------------------------------------- + * + * This item determines which server global should be used to retrieve the + * URI string. The default setting of 'REQUEST_URI' works for most servers. + * If your links do not seem to work, try one of the other delicious flavors: + * + * 'REQUEST_URI': Uses $_SERVER['REQUEST_URI'] + * 'QUERY_STRING': Uses $_SERVER['QUERY_STRING'] + * 'PATH_INFO': Uses $_SERVER['PATH_INFO'] + * + * WARNING: If you set this to 'PATH_INFO', URIs will always be URL-decoded! + */ + public string $uriProtocol = 'REQUEST_URI'; + + /* + |-------------------------------------------------------------------------- + | Allowed URL Characters + |-------------------------------------------------------------------------- + | + | This lets you specify which characters are permitted within your URLs. + | When someone tries to submit a URL with disallowed characters they will + | get a warning message. + | + | As a security measure you are STRONGLY encouraged to restrict URLs to + | as few characters as possible. + | + | By default, only these are allowed: `a-z 0-9~%.:_-` + | + | Set an empty string to allow all characters -- but only if you are insane. + | + | The configured value is actually a regular expression character group + | and it will be used as: '/\A[]+\z/iu' + | + | DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!! + | + */ + public string $permittedURIChars = 'a-z 0-9~%.:_\-'; + + /** + * -------------------------------------------------------------------------- + * Default Locale + * -------------------------------------------------------------------------- + * + * The Locale roughly represents the language and location that your visitor + * is viewing the site from. It affects the language strings and other + * strings (like currency markers, numbers, etc), that your program + * should run under for this request. + */ + public string $defaultLocale = 'en'; + + /** + * -------------------------------------------------------------------------- + * Negotiate Locale + * -------------------------------------------------------------------------- + * + * If true, the current Request object will automatically determine the + * language to use based on the value of the Accept-Language header. + * + * If false, no automatic detection will be performed. + */ + public bool $negotiateLocale = false; + + /** + * -------------------------------------------------------------------------- + * Supported Locales + * -------------------------------------------------------------------------- + * + * If $negotiateLocale is true, this array lists the locales supported + * by the application in descending order of priority. If no match is + * found, the first locale will be used. + * + * IncomingRequest::setLocale() also uses this list. + * + * @var list + */ + public array $supportedLocales = ['en']; + + /** + * -------------------------------------------------------------------------- + * Application Timezone + * -------------------------------------------------------------------------- + * + * The default timezone that will be used in your application to display + * dates with the date helper, and can be retrieved through app_timezone() + * + * @see https://www.php.net/manual/en/timezones.php for list of timezones + * supported by PHP. + */ + public string $appTimezone = 'UTC'; + + /** + * -------------------------------------------------------------------------- + * Default Character Set + * -------------------------------------------------------------------------- + * + * This determines which character set is used by default in various methods + * that require a character set to be provided. + * + * @see http://php.net/htmlspecialchars for a list of supported charsets. + */ + public string $charset = 'UTF-8'; + + /** + * -------------------------------------------------------------------------- + * Force Global Secure Requests + * -------------------------------------------------------------------------- + * + * If true, this will force every request made to this application to be + * made via a secure connection (HTTPS). If the incoming request is not + * secure, the user will be redirected to a secure version of the page + * and the HTTP Strict Transport Security (HSTS) header will be set. + */ + public bool $forceGlobalSecureRequests = false; + + /** + * -------------------------------------------------------------------------- + * Reverse Proxy IPs + * -------------------------------------------------------------------------- + * + * If your server is behind a reverse proxy, you must whitelist the proxy + * IP addresses from which CodeIgniter should trust headers such as + * X-Forwarded-For or Client-IP in order to properly identify + * the visitor's IP address. + * + * You need to set a proxy IP address or IP address with subnets and + * the HTTP header for the client IP address. + * + * Here are some examples: + * [ + * '10.0.1.200' => 'X-Forwarded-For', + * '192.168.5.0/24' => 'X-Real-IP', + * ] + * + * @var array + */ + public array $proxyIPs = []; + + /** + * -------------------------------------------------------------------------- + * Content Security Policy + * -------------------------------------------------------------------------- + * + * Enables the Response's Content Secure Policy to restrict the sources that + * can be used for images, scripts, CSS files, audio, video, etc. If enabled, + * the Response object will populate default values for the policy from the + * `ContentSecurityPolicy.php` file. Controllers can always add to those + * restrictions at run time. + * + * For a better understanding of CSP, see these documents: + * + * @see http://www.html5rocks.com/en/tutorials/security/content-security-policy/ + * @see http://www.w3.org/TR/CSP/ + */ + public bool $CSPEnabled = false; +} diff --git a/app/Config/Attendance.php b/app/Config/Attendance.php new file mode 100644 index 0000000..2c8dc33 --- /dev/null +++ b/app/Config/Attendance.php @@ -0,0 +1,28 @@ +|string> + */ + public $psr4 = [ + APP_NAMESPACE => APPPATH, + 'App\\Modules\\' => APPPATH . 'Modules' . DIRECTORY_SEPARATOR, + ]; + + /** + * ------------------------------------------------------------------- + * Class Map + * ------------------------------------------------------------------- + * The class map provides a map of class names and their exact + * location on the drive. Classes loaded in this manner will have + * slightly faster performance because they will not have to be + * searched for within one or more directories as they would if they + * were being autoloaded through a namespace. + * + * Prototype: + * $classmap = [ + * 'MyClass' => '/path/to/class/file.php' + * ]; + * + * @var array + */ + public $classmap = []; + + /** + * ------------------------------------------------------------------- + * Files + * ------------------------------------------------------------------- + * The files array provides a list of paths to __non-class__ files + * that will be autoloaded. This can be useful for bootstrap operations + * or for loading functions. + * + * Prototype: + * $files = [ + * '/path/to/my/file.php', + * ]; + * + * @var list + */ + public $files = []; + + /** + * ------------------------------------------------------------------- + * Helpers + * ------------------------------------------------------------------- + * Prototype: + * $helpers = [ + * 'form', + * ]; + * + * @var list + */ + public $helpers = []; +} diff --git a/app/Config/Boot/development.php b/app/Config/Boot/development.php new file mode 100644 index 0000000..a868447 --- /dev/null +++ b/app/Config/Boot/development.php @@ -0,0 +1,34 @@ + + * + * @see https://www.php.net/manual/en/curl.constants.php#constant.curl-lock-data-connect + */ + public array $shareConnectionOptions = [ + CURL_LOCK_DATA_CONNECT, + CURL_LOCK_DATA_DNS, + ]; + + /** + * -------------------------------------------------------------------------- + * CURLRequest Share Options + * -------------------------------------------------------------------------- + * + * Whether share options between requests or not. + * + * If true, all the options won't be reset between requests. + * It may cause an error request with unnecessary headers. + */ + public bool $shareOptions = false; +} diff --git a/app/Config/Cache.php b/app/Config/Cache.php new file mode 100644 index 0000000..38ac541 --- /dev/null +++ b/app/Config/Cache.php @@ -0,0 +1,198 @@ + WRITEPATH . 'cache/', + 'mode' => 0640, + ]; + + /** + * ------------------------------------------------------------------------- + * Memcached settings + * ------------------------------------------------------------------------- + * + * Your Memcached servers can be specified below, if you are using + * the Memcached drivers. + * + * @see https://codeigniter.com/user_guide/libraries/caching.html#memcached + * + * @var array{host?: string, port?: int, weight?: int, raw?: bool} + */ + public array $memcached = [ + 'host' => '127.0.0.1', + 'port' => 11211, + 'weight' => 1, + 'raw' => false, + ]; + + /** + * ------------------------------------------------------------------------- + * Redis settings + * ------------------------------------------------------------------------- + * + * Your Redis server can be specified below, if you are using + * the Redis or Predis drivers. + * + * @var array{ + * host?: string, + * password?: string|null, + * port?: int, + * timeout?: int, + * async?: bool, + * persistent?: bool, + * database?: int + * } + */ + public array $redis = [ + 'host' => '127.0.0.1', + 'password' => null, + 'port' => 6379, + 'timeout' => 0, + 'async' => false, // specific to Predis and ignored by the native Redis extension + 'persistent' => false, + 'database' => 0, + ]; + + /** + * -------------------------------------------------------------------------- + * Available Cache Handlers + * -------------------------------------------------------------------------- + * + * This is an array of cache engine alias' and class names. Only engines + * that are listed here are allowed to be used. + * + * @var array> + */ + public array $validHandlers = [ + 'apcu' => ApcuHandler::class, + 'dummy' => DummyHandler::class, + 'file' => FileHandler::class, + 'memcached' => MemcachedHandler::class, + 'predis' => PredisHandler::class, + 'redis' => RedisHandler::class, + 'wincache' => WincacheHandler::class, + ]; + + /** + * -------------------------------------------------------------------------- + * Web Page Caching: Cache Include Query String + * -------------------------------------------------------------------------- + * + * Whether to take the URL query string into consideration when generating + * output cache files. Valid options are: + * + * false = Disabled + * true = Enabled, take all query parameters into account. + * Please be aware that this may result in numerous cache + * files generated for the same page over and over again. + * ['q'] = Enabled, but only take into account the specified list + * of query parameters. + * + * @var bool|list + */ + public $cacheQueryString = false; + + /** + * -------------------------------------------------------------------------- + * Web Page Caching: Cache Status Codes + * -------------------------------------------------------------------------- + * + * HTTP status codes that are allowed to be cached. Only responses with + * these status codes will be cached by the PageCache filter. + * + * Default: [] - Cache all status codes (backward compatible) + * + * Recommended: [200] - Only cache successful responses + * + * You can also use status codes like: + * [200, 404, 410] - Cache successful responses and specific error codes + * [200, 201, 202, 203, 204] - All 2xx successful responses + * + * WARNING: Using [] may cache temporary error pages (404, 500, etc). + * Consider restricting to [200] for production applications to avoid + * caching errors that should be temporary. + * + * @var list + */ + public array $cacheStatusCodes = []; +} diff --git a/app/Config/Constants.php b/app/Config/Constants.php new file mode 100644 index 0000000..fb56bb1 --- /dev/null +++ b/app/Config/Constants.php @@ -0,0 +1,79 @@ +|string|null + */ + public $defaultSrc; + + /** + * Lists allowed scripts' URLs. + * + * @var list|string + */ + public $scriptSrc = 'self'; + + /** + * Specifies valid sources for JavaScript diff --git a/app/Views/dashboard/attendance_reports.php b/app/Views/dashboard/attendance_reports.php new file mode 100644 index 0000000..df7fe34 --- /dev/null +++ b/app/Views/dashboard/attendance_reports.php @@ -0,0 +1,378 @@ +
+
+
+

Laporan Absensi

+

+ Filter berdasarkan rentang tanggal, kelas, siswa, dan status. Tersedia rekap cepat per hari & kelas. +

+
+
+ + +
+

Filter

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

+ Catatan: Rekap berdasarkan data absensi yang terekam (anti double-scan sudah dijaga di level database). +

+
+ + + + + + + + + + +
+ + diff --git a/app/Views/dashboard/classes.php b/app/Views/dashboard/classes.php new file mode 100644 index 0000000..e71b934 --- /dev/null +++ b/app/Views/dashboard/classes.php @@ -0,0 +1,352 @@ +
+
+
+

Kelas

+

Kelola data kelas dan wali kelas.

+
+ +
+ +
+ Memuat… +
+ + +
+ + + + + + + + +
+ + diff --git a/app/Views/dashboard/dapodik.php b/app/Views/dashboard/dapodik.php new file mode 100644 index 0000000..df81667 --- /dev/null +++ b/app/Views/dashboard/dapodik.php @@ -0,0 +1,255 @@ +
+
+

Dapodik

+

Sinkron peserta didik dari Dapodik WebService dan mapping rombel ke kelas internal.

+
+ + +
+

Sinkron Siswa

+

Ambil data peserta didik dari Dapodik dan upsert ke data siswa. Rombel yang belum di-map akan tetap unmapped.

+ + + +
+ + +
+
+

Mapping Rombel

+ +
+
Memuat…
+ + +
+
+ +
+ + diff --git a/app/Views/dashboard/devices.php b/app/Views/dashboard/devices.php new file mode 100644 index 0000000..ad50a44 --- /dev/null +++ b/app/Views/dashboard/devices.php @@ -0,0 +1,114 @@ +
+

Device Absen

+

Daftar perangkat absensi dan status online. Koordinat presensi diatur terpusat di Pengaturan Presensi.

+ +
+ Loading… +
+ + +
+ + diff --git a/app/Views/dashboard/discipline.php b/app/Views/dashboard/discipline.php new file mode 100644 index 0000000..3c72768 --- /dev/null +++ b/app/Views/dashboard/discipline.php @@ -0,0 +1,507 @@ +
+
+
+

Poin Pelanggaran Siswa

+

+ Guru/Wali Kelas dapat mencatat pelanggaran dan melihat rekap poin perilaku siswa. +

+
+
+ +
+ +
+
+

Filter

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

Catat Pelanggaran Baru

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

Rekap Pelanggaran

+ +
+
+

Belum ada data untuk filter ini.

+ +
+
+ +
+
+

Riwayat Pelanggaran

+
+ + + +
+
+
+
+ +
+ + + diff --git a/app/Views/dashboard/discipline_settings.php b/app/Views/dashboard/discipline_settings.php new file mode 100644 index 0000000..2a376a3 --- /dev/null +++ b/app/Views/dashboard/discipline_settings.php @@ -0,0 +1,270 @@ +
+
+

Aturan Poin Pelanggaran

+

+ Kelola daftar jenis pelanggaran dan skor poinnya. Perubahan akan otomatis mempengaruhi perhitungan total poin siswa. +

+
+ +
+ +
+
+

Form Pelanggaran

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

Daftar Pelanggaran

+
+
Memuat…
+ + +
+
+
+
+ +
+ + + diff --git a/app/Views/dashboard/index.php b/app/Views/dashboard/index.php new file mode 100644 index 0000000..4a7635a --- /dev/null +++ b/app/Views/dashboard/index.php @@ -0,0 +1,224 @@ + + +
+
+

Realtime Attendance

+

Live stream via Server-Sent Events. Newest at top.

+
+
+ + + + + + + + + + + + + + + +
TimeStudentClassSubjectStatus
Connecting to stream…
+
+
+ + diff --git a/app/Views/dashboard/lesson_slots.php b/app/Views/dashboard/lesson_slots.php new file mode 100644 index 0000000..142afc4 --- /dev/null +++ b/app/Views/dashboard/lesson_slots.php @@ -0,0 +1,227 @@ +
+
+
+

Jam Pelajaran

+

Kelola slot jam pelajaran (urutan, jam mulai, jam selesai).

+
+ +
+ +
Memuat…
+ + +
+ + + + + + + +
+ + diff --git a/app/Views/dashboard/parent.php b/app/Views/dashboard/parent.php new file mode 100644 index 0000000..201aa83 --- /dev/null +++ b/app/Views/dashboard/parent.php @@ -0,0 +1,339 @@ +
+
+
+

Portal Orang Tua

+

+ Lihat data absensi dan pelanggaran anak Anda. +

+
+
+ + +
+

Pilih Anak

+ +
+ + + +
+ + diff --git a/app/Views/dashboard/presence_settings.php b/app/Views/dashboard/presence_settings.php new file mode 100644 index 0000000..9d446dc --- /dev/null +++ b/app/Views/dashboard/presence_settings.php @@ -0,0 +1,178 @@ +
+
+

Pengaturan Presensi

+

Satu pengaturan terpusat: koordinat sekolah (untuk semua absen) dan jadwal jam masuk & jam pulang.

+
+ +
+ Memuat… +
+ + +
+ + diff --git a/app/Views/dashboard/schedule_builder.php b/app/Views/dashboard/schedule_builder.php new file mode 100644 index 0000000..ec5e01e --- /dev/null +++ b/app/Views/dashboard/schedule_builder.php @@ -0,0 +1,279 @@ + +
+
+
+ + + Dashboard + +

Schedule Builder —

+
+ +
+ +
+ Loading… +
+ + + + + +
+ + diff --git a/app/Views/dashboard/schedule_builder_index.php b/app/Views/dashboard/schedule_builder_index.php new file mode 100644 index 0000000..8ce7e47 --- /dev/null +++ b/app/Views/dashboard/schedule_builder_index.php @@ -0,0 +1,99 @@ +
+

Schedule Builder

+

Pilih kelas lalu buka builder untuk mengatur jadwal mingguan.

+ +
+ Loading kelas… +
+ + +
+ + diff --git a/app/Views/dashboard/schedule_today.php b/app/Views/dashboard/schedule_today.php new file mode 100644 index 0000000..0bb4343 --- /dev/null +++ b/app/Views/dashboard/schedule_today.php @@ -0,0 +1,279 @@ +
+

Jadwal Hari Ini

+ + + + + +
+ Loading schedules… +
+ + + + + + +
+ + + diff --git a/app/Views/dashboard/students.php b/app/Views/dashboard/students.php new file mode 100644 index 0000000..7bb49c8 --- /dev/null +++ b/app/Views/dashboard/students.php @@ -0,0 +1,379 @@ +
+
+
+

Siswa

+

Kelola data siswa.

+
+ +
+ +
+
+ + +
+
+ + +
+ +
+ + +
+
+ +
Memuat…
+ + +
+ + + + + + + +
+ + diff --git a/app/Views/dashboard/subjects.php b/app/Views/dashboard/subjects.php new file mode 100644 index 0000000..4707604 --- /dev/null +++ b/app/Views/dashboard/subjects.php @@ -0,0 +1,215 @@ +
+
+
+

Mata Pelajaran

+

Kelola data mata pelajaran.

+
+ +
+ +
Memuat…
+ + +
+ + + + + + + +
+ + diff --git a/app/Views/dashboard/teachers.php b/app/Views/dashboard/teachers.php new file mode 100644 index 0000000..53b6028 --- /dev/null +++ b/app/Views/dashboard/teachers.php @@ -0,0 +1,393 @@ +
+
+
+

Guru / PTK

+

Kelola data guru dan wali kelas.

+
+ +
+ +
Memuat…
+ + +
+ + + + + + + + + + +
+ + diff --git a/app/Views/errors/cli/error_404.php b/app/Views/errors/cli/error_404.php new file mode 100644 index 0000000..456ea3e --- /dev/null +++ b/app/Views/errors/cli/error_404.php @@ -0,0 +1,7 @@ +getFile()) . ':' . $exception->getLine(), 'green')); +CLI::newLine(); + +$last = $exception; + +while ($prevException = $last->getPrevious()) { + $last = $prevException; + + CLI::write(' Caused by:'); + CLI::write(' [' . $prevException::class . ']', 'red'); + CLI::write(' ' . $prevException->getMessage()); + CLI::write(' at ' . CLI::color(clean_path($prevException->getFile()) . ':' . $prevException->getLine(), 'green')); + CLI::newLine(); +} + +// The backtrace +if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) { + $backtraces = $last->getTrace(); + + if ($backtraces) { + CLI::write('Backtrace:', 'green'); + } + + foreach ($backtraces as $i => $error) { + $padFile = ' '; // 4 spaces + $padClass = ' '; // 7 spaces + $c = str_pad($i + 1, 3, ' ', STR_PAD_LEFT); + + if (isset($error['file'])) { + $filepath = clean_path($error['file']) . ':' . $error['line']; + + CLI::write($c . $padFile . CLI::color($filepath, 'yellow')); + } else { + CLI::write($c . $padFile . CLI::color('[internal function]', 'yellow')); + } + + $function = ''; + + if (isset($error['class'])) { + $type = ($error['type'] === '->') ? '()' . $error['type'] : $error['type']; + $function .= $padClass . $error['class'] . $type . $error['function']; + } elseif (! isset($error['class']) && isset($error['function'])) { + $function .= $padClass . $error['function']; + } + + $args = implode(', ', array_map(static fn ($value): string => match (true) { + is_object($value) => 'Object(' . $value::class . ')', + is_array($value) => $value !== [] ? '[...]' : '[]', + $value === null => 'null', // return the lowercased version + default => var_export($value, true), + }, array_values($error['args'] ?? []))); + + $function .= '(' . $args . ')'; + + CLI::write($function); + CLI::newLine(); + } +} diff --git a/app/Views/errors/cli/production.php b/app/Views/errors/cli/production.php new file mode 100644 index 0000000..7db744e --- /dev/null +++ b/app/Views/errors/cli/production.php @@ -0,0 +1,5 @@ + + + + + <?= lang('Errors.badRequest') ?> + + + + +
+

400

+ +

+ + + + + +

+
+ + diff --git a/app/Views/errors/html/error_404.php b/app/Views/errors/html/error_404.php new file mode 100644 index 0000000..e506f08 --- /dev/null +++ b/app/Views/errors/html/error_404.php @@ -0,0 +1,84 @@ + + + + + <?= lang('Errors.pageNotFound') ?> + + + + +
+

404

+ +

+ + + + + +

+
+ + diff --git a/app/Views/errors/html/error_exception.php b/app/Views/errors/html/error_exception.php new file mode 100644 index 0000000..2c4e009 --- /dev/null +++ b/app/Views/errors/html/error_exception.php @@ -0,0 +1,429 @@ + + + + + + + + <?= esc($title) ?> + + + + + + + +
+
+ Displayed at — + PHP: — + CodeIgniter: -- + Environment: +
+
+

getCode() ? ' #' . $exception->getCode() : '') ?>

+

+ getMessage())) ?> + getMessage())) ?>" + rel="noreferrer" target="_blank">search → +

+
+
+ + +
+

at line

+ + +
+ +
+ +
+ +
+ getPrevious()) { + $last = $prevException; + ?> + +
+    Caused by:
+    getCode() ? ' #' . $prevException->getCode() : '') ?>
+
+    getMessage())) ?>
+    getMessage())) ?>"
+       rel="noreferrer" target="_blank">search →
+    getFile()) . ':' . $prevException->getLine()) ?>
+    
+ + +
+ + +
+ + + +
+ + +
+ +
    + $row) : ?> + +
  1. +

    + + + + + {PHP internal code} + + + + +   —   + + + ( arguments ) +

    + + + getParameters(); + } + + foreach ($row['args'] as $key => $value) : ?> + + + + + + +
    name : "#{$key}") ?>
    +
    + + () + + + + +   —   () + +

    + + + +
    + +
    + +
  2. + + +
+ +
+ + +
+ + + +

$

+ + + + + + + + + + $value) : ?> + + + + + + +
KeyValue
+ + + +
+ +
+ + + + + + +

Constants

+ + + + + + + + + + $value) : ?> + + + + + + +
KeyValue
+ + + +
+ +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathgetUri()) ?>
HTTP MethodgetMethod()) ?>
IP AddressgetIPAddress()) ?>
Is AJAX Request?isAJAX() ? 'yes' : 'no' ?>
Is CLI Request?isCLI() ? 'yes' : 'no' ?>
Is Secure Request?isSecure() ? 'yes' : 'no' ?>
User AgentgetUserAgent()->getAgentString()) ?>
+ + + + + + + + +

$

+ + + + + + + + + + $value) : ?> + + + + + + +
KeyValue
+ + + +
+ +
+ + + + + +
+ No $_GET, $_POST, or $_COOKIE Information to show. +
+ + + + headers(); ?> + + +

Headers

+ + + + + + + + + + $value) : ?> + + + + + + +
HeaderValue
+ getValueLine(), 'html'); + } else { + foreach ($value as $i => $header) { + echo ' ('. $i+1 . ') ' . esc($header->getValueLine(), 'html'); + } + } + ?> +
+ + +
+ + + setStatusCode(http_response_code()); + ?> +
+ + + + + +
Response StatusgetStatusCode() . ' - ' . $response->getReasonPhrase()) ?>
+ + headers(); ?> + +

Headers

+ + + + + + + + + + $value) : ?> + + + + + + +
HeaderValue
+ getHeaderLine($name), 'html'); + } else { + foreach ($value as $i => $header) { + echo ' ('. $i+1 . ') ' . esc($header->getValueLine(), 'html'); + } + } + ?> +
+ + +
+ + +
+ + +
    + +
  1. + +
+
+ + +
+ + + + + + + + + + + + + + + + +
Memory Usage
Peak Memory Usage:
Memory Limit:
+ +
+ +
+ +
+ + + + diff --git a/app/Views/errors/html/production.php b/app/Views/errors/html/production.php new file mode 100644 index 0000000..2f59a8d --- /dev/null +++ b/app/Views/errors/html/production.php @@ -0,0 +1,25 @@ + + + + + + + <?= lang('Errors.whoops') ?> + + + + + +
+ +

+ +

+ +
+ + + + diff --git a/app/Views/layouts/main.php b/app/Views/layouts/main.php new file mode 100644 index 0000000..29facc5 --- /dev/null +++ b/app/Views/layouts/main.php @@ -0,0 +1,31 @@ +currentUser(); +$currentUri = service('request')->getPath(); +?> + + + + + + <?= esc($title ?? 'Dashboard') ?> | SMAN 1 Garut + + + + + +
+ $currentUser, 'current_uri' => $currentUri]) ?> +
+ $currentUser ?? $user ?? null]) ?> +
+ +
+ +
+
+ + + diff --git a/app/Views/login/index.php b/app/Views/login/index.php new file mode 100644 index 0000000..2ceb4fc --- /dev/null +++ b/app/Views/login/index.php @@ -0,0 +1,67 @@ + + + + + + Login | SMAN 1 Garut + + + + + +
+
+
+

SMAN 1 Garut

+

Sign in to dashboard

+
+
+
+ + +
+
+ + +
+ + +
+
+
+ + + diff --git a/app/Views/partials/footer.php b/app/Views/partials/footer.php new file mode 100644 index 0000000..04dd18d --- /dev/null +++ b/app/Views/partials/footer.php @@ -0,0 +1,3 @@ + diff --git a/app/Views/partials/header.php b/app/Views/partials/header.php new file mode 100644 index 0000000..898225d --- /dev/null +++ b/app/Views/partials/header.php @@ -0,0 +1,19 @@ +
+
+ +

Dashboard

+
+
+ + +
+ + +
+ +
+
diff --git a/app/Views/partials/scripts.php b/app/Views/partials/scripts.php new file mode 100644 index 0000000..b006b8c --- /dev/null +++ b/app/Views/partials/scripts.php @@ -0,0 +1,20 @@ + diff --git a/app/Views/partials/sidebar.php b/app/Views/partials/sidebar.php new file mode 100644 index 0000000..b11146f --- /dev/null +++ b/app/Views/partials/sidebar.php @@ -0,0 +1,63 @@ + + + diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..69df4e1 --- /dev/null +++ b/app/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/builds b/builds new file mode 100644 index 0000000..cc2ca08 --- /dev/null +++ b/builds @@ -0,0 +1,125 @@ +#!/usr/bin/env php + 'vcs', + 'url' => GITHUB_URL, + ]; + } + + $array['require']['codeigniter4/codeigniter4'] = 'dev-develop'; + unset($array['require']['codeigniter4/framework']); + } else { + unset($array['minimum-stability']); + + if (isset($array['repositories'])) { + foreach ($array['repositories'] as $i => $repository) { + if ($repository['url'] === GITHUB_URL) { + unset($array['repositories'][$i]); + break; + } + } + + if (empty($array['repositories'])) { + unset($array['repositories']); + } + } + + $array['require']['codeigniter4/framework'] = LATEST_RELEASE; + unset($array['require']['codeigniter4/codeigniter4']); + } + + file_put_contents($file, json_encode($array, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL); + + $modified[] = $file; + } else { + echo 'Warning: Unable to decode composer.json! Skipping...' . PHP_EOL; + } + } else { + echo 'Warning: Unable to read composer.json! Skipping...' . PHP_EOL; + } +} + +$files = [ + __DIR__ . DIRECTORY_SEPARATOR . 'app/Config/Paths.php', + __DIR__ . DIRECTORY_SEPARATOR . 'phpunit.xml.dist', + __DIR__ . DIRECTORY_SEPARATOR . 'phpunit.xml', +]; + +foreach ($files as $file) { + if (is_file($file)) { + $contents = file_get_contents($file); + + if ($dev) { + $contents = str_replace('vendor/codeigniter4/framework', 'vendor/codeigniter4/codeigniter4', $contents); + } else { + $contents = str_replace('vendor/codeigniter4/codeigniter4', 'vendor/codeigniter4/framework', $contents); + } + + file_put_contents($file, $contents); + + $modified[] = $file; + } +} + +if ($modified === []) { + echo 'No files modified.' . PHP_EOL; +} else { + echo 'The following files were modified:' . PHP_EOL; + + foreach ($modified as $file) { + echo " * {$file}" . PHP_EOL; + } + + echo 'Run `composer update` to sync changes with your vendor folder.' . PHP_EOL; +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b64f55c --- /dev/null +++ b/composer.json @@ -0,0 +1,44 @@ +{ + "name": "codeigniter4/appstarter", + "description": "CodeIgniter4 starter app", + "license": "MIT", + "type": "project", + "homepage": "https://codeigniter.com", + "support": { + "forum": "https://forum.codeigniter.com/", + "source": "https://github.com/codeigniter4/CodeIgniter4", + "slack": "https://codeigniterchat.slack.com" + }, + "require": { + "php": "^8.2", + "codeigniter4/framework": "^4.7" + }, + "require-dev": { + "fakerphp/faker": "^1.9", + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "^10.5.16" + }, + "autoload": { + "psr-4": { + "App\\": "app/", + "App\\Modules\\": "app/Modules/", + "Config\\": "app/Config/" + }, + "exclude-from-classmap": [ + "**/Database/Migrations/**" + ] + }, + "autoload-dev": { + "psr-4": { + "Tests\\Support\\": "tests/_support" + } + }, + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true + }, + "scripts": { + "test": "phpunit" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..19282fe --- /dev/null +++ b/composer.lock @@ -0,0 +1,2114 @@ +{ + "_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": "f5cce40800fa5dae1504b9364f585e6a", + "packages": [ + { + "name": "codeigniter4/framework", + "version": "v4.7.0", + "source": { + "type": "git", + "url": "https://github.com/codeigniter4/framework.git", + "reference": "e7753bc03f8b74af428f46b5e2bb74925487c930" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/codeigniter4/framework/zipball/e7753bc03f8b74af428f46b5e2bb74925487c930", + "reference": "e7753bc03f8b74af428f46b5e2bb74925487c930", + "shasum": "" + }, + "require": { + "ext-intl": "*", + "ext-mbstring": "*", + "laminas/laminas-escaper": "^2.18", + "php": "^8.2", + "psr/log": "^3.0" + }, + "require-dev": { + "codeigniter/coding-standard": "^1.7", + "fakerphp/faker": "^1.24", + "friendsofphp/php-cs-fixer": "^3.47.1", + "kint-php/kint": "^6.1", + "mikey179/vfsstream": "^1.6.12", + "nexusphp/cs-config": "^3.6", + "phpunit/phpunit": "^10.5.16 || ^11.2", + "predis/predis": "^3.0" + }, + "suggest": { + "ext-apcu": "If you use Cache class ApcuHandler", + "ext-curl": "If you use CURLRequest class", + "ext-dom": "If you use TestResponse", + "ext-exif": "If you run Image class tests", + "ext-fileinfo": "Improves mime type detection for files", + "ext-gd": "If you use Image class GDHandler", + "ext-imagick": "If you use Image class ImageMagickHandler", + "ext-libxml": "If you use TestResponse", + "ext-memcache": "If you use Cache class MemcachedHandler with Memcache", + "ext-memcached": "If you use Cache class MemcachedHandler with Memcached", + "ext-mysqli": "If you use MySQL", + "ext-oci8": "If you use Oracle Database", + "ext-pcntl": "If you use Signals", + "ext-pgsql": "If you use PostgreSQL", + "ext-posix": "If you use Signals", + "ext-readline": "Improves CLI::input() usability", + "ext-redis": "If you use Cache class RedisHandler", + "ext-simplexml": "If you format XML", + "ext-sodium": "If you use Encryption SodiumHandler", + "ext-sqlite3": "If you use SQLite3", + "ext-sqlsrv": "If you use SQL Server", + "ext-xdebug": "If you use CIUnitTestCase::assertHeaderEmitted()" + }, + "type": "project", + "autoload": { + "psr-4": { + "CodeIgniter\\": "system/" + }, + "exclude-from-classmap": [ + "**/Database/Migrations/**" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The CodeIgniter framework v4", + "homepage": "https://codeigniter.com", + "support": { + "forum": "https://forum.codeigniter.com/", + "slack": "https://codeigniterchat.slack.com", + "source": "https://github.com/codeigniter4/CodeIgniter4" + }, + "time": "2026-02-01T20:39:35+00:00" + }, + { + "name": "laminas/laminas-escaper", + "version": "2.18.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-escaper.git", + "reference": "06f211dfffff18d91844c1f55250d5d13c007e18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/06f211dfffff18d91844c1f55250d5d13c007e18", + "reference": "06f211dfffff18d91844c1f55250d5d13c007e18", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-mbstring": "*", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "conflict": { + "zendframework/zend-escaper": "*" + }, + "require-dev": { + "infection/infection": "^0.31.0", + "laminas/laminas-coding-standard": "~3.1.0", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Escaper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs", + "homepage": "https://laminas.dev", + "keywords": [ + "escaper", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-escaper/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-escaper/issues", + "rss": "https://github.com/laminas/laminas-escaper/releases.atom", + "source": "https://github.com/laminas/laminas-escaper" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-14T18:31:13+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + } + ], + "packages-dev": [ + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "mikey179/vfsstream", + "version": "v1.6.12", + "source": { + "type": "git", + "url": "https://github.com/bovigo/vfsStream.git", + "reference": "fe695ec993e0a55c3abdda10a9364eb31c6f1bf0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/fe695ec993e0a55c3abdda10a9364eb31c6f1bf0", + "reference": "fe695ec993e0a55c3abdda10a9364eb31c6f1bf0", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5||^8.5||^9.6", + "yoast/phpunit-polyfills": "^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "org\\bovigo\\vfs\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Frank Kleine", + "homepage": "http://frankkleine.de/", + "role": "Developer" + } + ], + "description": "Virtual file system to mock the real file system in unit tests.", + "homepage": "http://vfs.bovigo.org/", + "support": { + "issues": "https://github.com/bovigo/vfsStream/issues", + "source": "https://github.com/bovigo/vfsStream/tree/master", + "wiki": "https://github.com/bovigo/vfsStream/wiki" + }, + "time": "2024-08-29T18:43:31+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.63", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "33198268dad71e926626b618f3ec3966661e4d90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.5", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.4", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.1", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-01-27T05:48:37+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:25:16+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "0735b90f4da94969541dac1da743446e276defa6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:09:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:50:56+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/docs/SYNC_FOTO_WAJAH.md b/docs/SYNC_FOTO_WAJAH.md new file mode 100644 index 0000000..2b2a95c --- /dev/null +++ b/docs/SYNC_FOTO_WAJAH.md @@ -0,0 +1,48 @@ +# Sync Foto Wajah dari Google Drive untuk Smart Presensi + +Foto wajah siswa dipakai untuk **verifikasi wajah** di aplikasi mobile (absen masuk/pulang). Foto disimpan di backend: `writable/faces/{student_id}.jpg`. + +## Sumber foto + +Folder contoh: [Foto Siswa Kls XII TP 25-26](https://drive.google.com/drive/folders/16E84NFGYPItaTANQwEyMQW5rkTCt6Asy) — berisi subfolder per kelas (XII-1, XII-2, … XII-12, Susulan). + +## Cara 1: Download manual lalu jalankan script + +1. **Download folder dari Google Drive** + - Buka folder di Drive, pilih semua subfolder (XII-1 … XII-12), lalu **Download** (zip). + - Ekstrak ke suatu folder, misalnya `C:\temp\foto-siswa-xii`. + +2. **Konvensi nama file** + - Setiap foto **wajib** bernama **NISN** siswa + ekstensi, contoh: + - `1234567890.jpg` atau `1234567890.png` + - Jika di Drive nama file bukan NISN (misalnya "Budi Santoso.jpg"), rename dulu per file sesuai NISN siswa. + +3. **Jalankan script sync** + - Dari **folder backend** (bukan dari scripts): + ```bash + cd c:\laragon\www\sman1\backend + php scripts/sync_face_photos.php --source=C:\temp\foto-siswa-xii + ``` + - Script akan: + - Scan semua subfolder (XII-1, XII-2, dll.), + - Untuk setiap file gambar (.jpg, .jpeg, .png), ambil nama file tanpa ekstensi = NISN, + - Cari siswa di database berdasarkan NISN, + - Copy file ke `writable/faces/{student_id}.jpg`. + +## Cara 2: Upload per siswa lewat dashboard (rencana) + +Ke depan bisa ditambah halaman di **Pengaturan Academic → Siswa**: per siswa ada tombol "Upload foto wajah", simpan ke `writable/faces/{student_id}.jpg`. + +## API untuk mobile + +- **GET** `/api/mobile/student/face-photo?student_id=123` + Mengembalikan gambar foto wajah siswa (atau 404 jika belum ada). + Dipakai aplikasi mobile untuk memuat foto referensi lalu membandingkan dengan wajah yang terdeteksi kamera (face-api.js). + +## Ringkas + +| Yang diatur | Keterangan | +|-------------|------------| +| Penyimpanan | `writable/faces/{student_id}.jpg` (atau .png) | +| Mapping | Nama file = NISN → cari siswa → simpan dengan student_id | +| Sumber | Download folder Drive → rename file = NISN → jalankan script sync | diff --git a/env b/env new file mode 100644 index 0000000..57e6e82 --- /dev/null +++ b/env @@ -0,0 +1,69 @@ +#-------------------------------------------------------------------- +# Example Environment Configuration file +# +# This file can be used as a starting point for your own +# custom .env files, and contains most of the possible settings +# available in a default install. +# +# By default, all of the settings are commented out. If you want +# to override the setting, you must un-comment it by removing the '#' +# at the beginning of the line. +#-------------------------------------------------------------------- + +#-------------------------------------------------------------------- +# ENVIRONMENT +#-------------------------------------------------------------------- + +CI_ENVIRONMENT = development + +#-------------------------------------------------------------------- +# APP +#-------------------------------------------------------------------- + +app.baseURL = 'http://localhost/sman1/backend/public/' +# If you have trouble with `.`, you could also use `_`. +# app_baseURL = '' +# app.forceGlobalSecureRequests = false +# app.CSPEnabled = false + +#-------------------------------------------------------------------- +# DATABASE +#-------------------------------------------------------------------- + +database.default.hostname = localhost +database.default.database = absen_sman1 +database.default.username = root +database.default.password = dodolgarut +database.default.DBDriver = MySQLi +database.default.DBPrefix = +database.default.port = 3306 + +# If you use MySQLi as tests, first update the values of Config\Database::$tests. +# database.tests.hostname = localhost +# database.tests.database = ci4_test +# database.tests.username = root +# database.tests.password = root +# database.tests.DBDriver = MySQLi +# database.tests.DBPrefix = +# database.tests.charset = utf8mb4 +# database.tests.DBCollat = utf8mb4_general_ci +# database.tests.port = 3306 + +#-------------------------------------------------------------------- +# ENCRYPTION +#-------------------------------------------------------------------- + +# encryption.key = + +#-------------------------------------------------------------------- +# SESSION +#-------------------------------------------------------------------- + +# session.driver = 'CodeIgniter\Session\Handlers\FileHandler' +# session.savePath = null + +#-------------------------------------------------------------------- +# LOGGER +#-------------------------------------------------------------------- + +# logger.threshold = 4 diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..b408a99 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,63 @@ + + + + + + + + + + + + + ./tests + + + + + + + + + + ./app + + + ./app/Views + ./app/Config/Routes.php + + + + + + + + + + + + + + + diff --git a/preload.php b/preload.php new file mode 100644 index 0000000..288b30b --- /dev/null +++ b/preload.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +use CodeIgniter\Boot; +use Config\Paths; + +/* + *--------------------------------------------------------------- + * Sample file for Preloading + *--------------------------------------------------------------- + * See https://www.php.net/manual/en/opcache.preloading.php + * + * How to Use: + * 0. Copy this file to your project root folder. + * 1. Set the $paths property of the preload class below. + * 2. Set opcache.preload in php.ini. + * php.ini: + * opcache.preload=/path/to/preload.php + */ + +// Load the paths config file +require __DIR__ . '/app/Config/Paths.php'; + +// Path to the front controller +define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR); + +class preload +{ + /** + * @var array Paths to preload. + */ + private array $paths = [ + [ + 'include' => __DIR__ . '/vendor/codeigniter4/framework/system', // Change this path if using manual installation + 'exclude' => [ + // Not needed if you don't use them. + '/system/Database/OCI8/', + '/system/Database/Postgre/', + '/system/Database/SQLite3/', + '/system/Database/SQLSRV/', + // Not needed for web apps. + '/system/Database/Seeder.php', + '/system/Test/', + '/system/CLI/', + '/system/Commands/', + '/system/Publisher/', + '/system/ComposerScripts.php', + // Not Class/Function files. + '/system/Config/Routes.php', + '/system/Language/', + '/system/bootstrap.php', + '/system/util_bootstrap.php', + '/system/rewrite.php', + '/Views/', + // Errors occur. + '/system/ThirdParty/', + ], + ], + ]; + + public function __construct() + { + $this->loadAutoloader(); + } + + private function loadAutoloader(): void + { + $paths = new Paths(); + require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'Boot.php'; + + Boot::preload($paths); + } + + /** + * Load PHP files. + */ + public function load(): void + { + foreach ($this->paths as $path) { + $directory = new RecursiveDirectoryIterator($path['include']); + $fullTree = new RecursiveIteratorIterator($directory); + $phpFiles = new RegexIterator( + $fullTree, + '/.+((? $file) { + foreach ($path['exclude'] as $exclude) { + if (str_contains($file[0], $exclude)) { + continue 2; + } + } + + require_once $file[0]; + // Uncomment only for debugging (to inspect which files are included). + // Never use this in production - preload scripts must not generate output. + // echo 'Loaded: ' . $file[0] . "\n"; + } + } + } +} + +(new preload())->load(); diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..729a2b6 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,51 @@ +# Disable directory browsing +Options -Indexes + +# ---------------------------------------------------------------------- +# Rewrite engine +# ---------------------------------------------------------------------- + +# Turning on the rewrite engine is necessary for the following rules and features. +# FollowSymLinks must be enabled for this to work. + + Options +FollowSymlinks + RewriteEngine On + + # Subfolder: supaya /login di-rewrite ke index.php (tanpa index.php di URL) + RewriteBase /sman1/backend/public/ + + # Hilangkan index.php dari URL: redirect .../index.php/xxx -> .../xxx + RewriteCond %{THE_REQUEST} \s/sman1/backend/public/index\.php/([^\s?]+) [NC] + RewriteRule .* /sman1/backend/public/%1 [R=301,L] + + # Redirect Trailing Slashes... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Rewrite "www.example.com -> example.com" + RewriteCond %{HTTPS} !=on + RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] + RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L] + + # Checks to see if the user is attempting to access a valid file, + # such as an image or css document, if this isn't true it sends the + # request to the front controller, index.php + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^([\s\S]*)$ index.php/$1 [L,NC,QSA] + + # Ensure Authorization header is passed along + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + + + # If we don't have mod_rewrite installed, all 404's + # can be sent to index.php, and everything works as normal. + ErrorDocument 404 index.php + + +# Disable server signature start +ServerSignature Off +# Disable server signature end diff --git a/public/assets/img/logo_sman1garut.png b/public/assets/img/logo_sman1garut.png new file mode 100644 index 0000000..5ea8368 Binary files /dev/null and b/public/assets/img/logo_sman1garut.png differ diff --git a/public/assets/webfonts/Outfit-Black.woff2 b/public/assets/webfonts/Outfit-Black.woff2 new file mode 100644 index 0000000..c1897f7 Binary files /dev/null and b/public/assets/webfonts/Outfit-Black.woff2 differ diff --git a/public/assets/webfonts/Outfit-Bold.woff2 b/public/assets/webfonts/Outfit-Bold.woff2 new file mode 100644 index 0000000..6976280 Binary files /dev/null and b/public/assets/webfonts/Outfit-Bold.woff2 differ diff --git a/public/assets/webfonts/Outfit-ExtraBold.woff2 b/public/assets/webfonts/Outfit-ExtraBold.woff2 new file mode 100644 index 0000000..3f18fbe Binary files /dev/null and b/public/assets/webfonts/Outfit-ExtraBold.woff2 differ diff --git a/public/assets/webfonts/Outfit-ExtraLight.woff2 b/public/assets/webfonts/Outfit-ExtraLight.woff2 new file mode 100644 index 0000000..06173bc Binary files /dev/null and b/public/assets/webfonts/Outfit-ExtraLight.woff2 differ diff --git a/public/assets/webfonts/Outfit-Light.woff2 b/public/assets/webfonts/Outfit-Light.woff2 new file mode 100644 index 0000000..97cee48 Binary files /dev/null and b/public/assets/webfonts/Outfit-Light.woff2 differ diff --git a/public/assets/webfonts/Outfit-Medium.woff2 b/public/assets/webfonts/Outfit-Medium.woff2 new file mode 100644 index 0000000..fa5b9f3 Binary files /dev/null and b/public/assets/webfonts/Outfit-Medium.woff2 differ diff --git a/public/assets/webfonts/Outfit-Regular.woff2 b/public/assets/webfonts/Outfit-Regular.woff2 new file mode 100644 index 0000000..fc991db Binary files /dev/null and b/public/assets/webfonts/Outfit-Regular.woff2 differ diff --git a/public/assets/webfonts/Outfit-SemiBold.woff2 b/public/assets/webfonts/Outfit-SemiBold.woff2 new file mode 100644 index 0000000..512663f Binary files /dev/null and b/public/assets/webfonts/Outfit-SemiBold.woff2 differ diff --git a/public/assets/webfonts/Outfit-Thin.woff2 b/public/assets/webfonts/Outfit-Thin.woff2 new file mode 100644 index 0000000..6431a4e Binary files /dev/null and b/public/assets/webfonts/Outfit-Thin.woff2 differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..7ecfce2 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..56ce6de --- /dev/null +++ b/public/index.php @@ -0,0 +1,74 @@ +systemDirectory . '/Boot.php'; + +exit(Boot::bootWeb($paths)); diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..9e60f97 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/scripts/sync_face_photos.php b/scripts/sync_face_photos.php new file mode 100644 index 0000000..4a8103f --- /dev/null +++ b/scripts/sync_face_photos.php @@ -0,0 +1,168 @@ +#!/usr/bin/env php + PDO::ERRMODE_EXCEPTION]); +} catch (Throwable $e) { + echo "Koneksi DB gagal: " . $e->getMessage() . "\n"; + exit(1); +} + +$facesDir = $backendRoot . DIRECTORY_SEPARATOR . 'writable' . DIRECTORY_SEPARATOR . 'faces'; +if (! is_dir($facesDir)) { + mkdir($facesDir, 0755, true); + echo "Folder dibuat: {$facesDir}\n"; +} + +$extensions = ['jpg', 'jpeg', 'png']; +$countOk = 0; +$countSkip = 0; +$countFail = 0; + +$it = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST +); + +foreach ($it as $file) { + if (! $file->isFile()) { + continue; + } + $path = $file->getPathname(); + $ext = strtolower($file->getExtension()); + if (! in_array($ext, $extensions, true)) { + continue; + } + + // Nama file bisa berupa "1234567890.jpg" atau "01. 1234567890.jpg" + $baseName = preg_replace('/\.(jpg|jpeg|png)$/i', '', $file->getFilename()); + $baseName = trim($baseName); + if ($baseName === '') { + continue; + } + + // Ambil deretan digit minimal 8 angka pertama sebagai NISN + $nisn = null; + if (preg_match('/(\d{8,})/', $baseName, $m) === 1) { + $nisn = $m[1]; + } + if ($nisn === null || $nisn === '') { + echo " [skip] Tidak ditemukan NISN di nama file: {$baseName} ({$path})\n"; + $countSkip++; + continue; + } + + $stmt = $pdo->prepare('SELECT id FROM students WHERE nisn = ? LIMIT 1'); + $stmt->execute([$nisn]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (! $row) { + echo " [skip] NISN tidak ada di DB: {$nisn} ({$path})\n"; + $countSkip++; + continue; + } + $studentId = (int) $row['id']; + $dest = $facesDir . DIRECTORY_SEPARATOR . $studentId . '.jpg'; + + // Resize + kompres dengan GD: lebar max 600px, quality 75 + $srcImage = null; + if ($ext === 'jpg' || $ext === 'jpeg') { + $srcImage = @imagecreatefromjpeg($path); + } elseif ($ext === 'png') { + $srcImage = @imagecreatefrompng($path); + } + + if ($srcImage === false || $srcImage === null) { + // Fallback: langsung copy jika GD gagal + if (copy($path, $dest)) { + $hash = md5_file($dest); + $update = $pdo->prepare('UPDATE students SET face_hash = ? WHERE id = ?'); + $update->execute([$hash, $studentId]); + echo " [ok-copy] {$nisn} -> student_id {$studentId}\n"; + $countOk++; + } else { + echo " [fail] Gagal copy (GD error): {$path} -> {$dest}\n"; + $countFail++; + } + continue; + } + + $width = imagesx($srcImage); + $height = imagesy($srcImage); + $maxWidth = 600; + if ($width > $maxWidth) { + $ratio = $maxWidth / $width; + $newWidth = $maxWidth; + $newHeight = (int) round($height * $ratio); + } else { + $newWidth = $width; + $newHeight = $height; + } + + $dstImage = imagecreatetruecolor($newWidth, $newHeight); + imagecopyresampled($dstImage, $srcImage, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height); + + if (imagejpeg($dstImage, $dest, 75)) { + imagedestroy($srcImage); + imagedestroy($dstImage); + $hash = md5_file($dest); + $update = $pdo->prepare('UPDATE students SET face_hash = ? WHERE id = ?'); + $update->execute([$hash, $studentId]); + echo " [ok] {$nisn} -> student_id {$studentId}\n"; + $countOk++; + } else { + imagedestroy($srcImage); + imagedestroy($dstImage); + echo " [fail] Gagal save JPG: {$path} -> {$dest}\n"; + $countFail++; + } +} + +echo "\nSelesai. OK: {$countOk}, Skip (NISN tidak ada): {$countSkip}, Gagal: {$countFail}\n"; diff --git a/spark b/spark new file mode 100644 index 0000000..61bc572 --- /dev/null +++ b/spark @@ -0,0 +1,87 @@ +#!/usr/bin/env php + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +use CodeIgniter\Boot; +use Config\Paths; + +/* + * -------------------------------------------------------------------- + * CODEIGNITER COMMAND-LINE TOOLS + * -------------------------------------------------------------------- + * The main entry point into the CLI system and allows you to run + * commands and perform maintenance on your application. + */ + +/* + *--------------------------------------------------------------- + * CHECK SERVER API + *--------------------------------------------------------------- + */ + +// Refuse to run when called from php-cgi +if (str_starts_with(PHP_SAPI, 'cgi')) { + exit("The cli tool is not supported when running php-cgi. It needs php-cli to function!\n\n"); +} + +/* + *--------------------------------------------------------------- + * CHECK PHP VERSION + *--------------------------------------------------------------- + */ + +$minPhpVersion = '8.2'; // If you update this, don't forget to update `public/index.php`. +if (version_compare(PHP_VERSION, $minPhpVersion, '<')) { + $message = sprintf( + 'Your PHP version must be %s or higher to run CodeIgniter. Current version: %s', + $minPhpVersion, + PHP_VERSION, + ); + + exit($message); +} + +// We want errors to be shown when using it from the CLI. +error_reporting(E_ALL); +ini_set('display_errors', '1'); + +/* + *--------------------------------------------------------------- + * SET THE CURRENT DIRECTORY + *--------------------------------------------------------------- + */ + +// Path to the front controller +define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR); + +// Ensure the current directory is pointing to the front controller's directory +chdir(FCPATH); + +/* + *--------------------------------------------------------------- + * BOOTSTRAP THE APPLICATION + *--------------------------------------------------------------- + * This process sets up the path constants, loads and registers + * our autoloader, along with Composer's, loads our constants + * and fires up an environment-specific bootstrapping. + */ + +// LOAD OUR PATHS CONFIG FILE +// This is the line that might need to be changed, depending on your folder structure. +require FCPATH . '../app/Config/Paths.php'; +// ^^^ Change this line if you move your application folder + +$paths = new Paths(); + +// LOAD THE FRAMEWORK BOOTSTRAP FILE +require $paths->systemDirectory . '/Boot.php'; + +exit(Boot::bootSpark($paths)); diff --git a/tests/.htaccess b/tests/.htaccess new file mode 100644 index 0000000..3462048 --- /dev/null +++ b/tests/.htaccess @@ -0,0 +1,6 @@ + + Require all denied + + + Deny from all + diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..fc40e44 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,118 @@ +# Running Application Tests + +This is the quick-start to CodeIgniter testing. Its intent is to describe what +it takes to set up your application and get it ready to run unit tests. +It is not intended to be a full description of the test features that you can +use to test your application. Those details can be found in the documentation. + +## Resources + +* [CodeIgniter 4 User Guide on Testing](https://codeigniter.com/user_guide/testing/index.html) +* [PHPUnit docs](https://phpunit.de/documentation.html) +* [Any tutorials on Unit testing in CI4?](https://forum.codeigniter.com/showthread.php?tid=81830) + +## Requirements + +It is recommended to use the latest version of PHPUnit. At the time of this +writing, we are running version 9.x. Support for this has been built into the +**composer.json** file that ships with CodeIgniter and can easily be installed +via [Composer](https://getcomposer.org/) if you don't already have it installed globally. + +```console +> composer install +``` + +If running under macOS or Linux, you can create a symbolic link to make running tests a touch nicer. + +```console +> ln -s ./vendor/bin/phpunit ./phpunit +``` + +You also need to install [XDebug](https://xdebug.org/docs/install) in order +for code coverage to be calculated successfully. After installing `XDebug`, you must add `xdebug.mode=coverage` in the **php.ini** file to enable code coverage. + +## Setting Up + +A number of the tests use a running database. +In order to set up the database edit the details for the `tests` group in +**app/Config/Database.php** or **.env**. +Make sure that you provide a database engine that is currently running on your machine. +More details on a test database setup are in the +[Testing Your Database](https://codeigniter.com/user_guide/testing/database.html) section of the documentation. + +## Running the tests + +The entire test suite can be run by simply typing one command-line command from the main directory. + +```console +> ./phpunit +``` + +If you are using Windows, use the following command. + +```console +> vendor\bin\phpunit +``` + +You can limit tests to those within a single test directory by specifying the +directory name after phpunit. + +```console +> ./phpunit app/Models +``` + +## Generating Code Coverage + +To generate coverage information, including HTML reports you can view in your browser, +you can use the following command: + +```console +> ./phpunit --colors --coverage-text=tests/coverage.txt --coverage-html=tests/coverage/ -d memory_limit=1024m +``` + +This runs all of the tests again collecting information about how many lines, +functions, and files are tested. It also reports the percentage of the code that is covered by tests. +It is collected in two formats: a simple text file that provides an overview as well +as a comprehensive collection of HTML files that show the status of every line of code in the project. + +The text file can be found at **tests/coverage.txt**. +The HTML files can be viewed by opening **tests/coverage/index.html** in your favorite browser. + +## PHPUnit XML Configuration + +The repository has a ``phpunit.xml.dist`` file in the project root that's used for +PHPUnit configuration. This is used to provide a default configuration if you +do not have your own configuration file in the project root. + +The normal practice would be to copy ``phpunit.xml.dist`` to ``phpunit.xml`` +(which is git ignored), and to tailor it as you see fit. +For instance, you might wish to exclude database tests, or automatically generate +HTML code coverage reports. + +## Test Cases + +Every test needs a *test case*, or class that your tests extend. CodeIgniter 4 +provides one class that you may use directly: +* `CodeIgniter\Test\CIUnitTestCase` + +Most of the time you will want to write your own test cases that extend `CIUnitTestCase` +to hold functions and services common to your test suites. + +## Creating Tests + +All tests go in the **tests/** directory. Each test file is a class that extends a +**Test Case** (see above) and contains methods for the individual tests. These method +names must start with the word "test" and should have descriptive names for precisely what +they are testing: +`testUserCanModifyFile()` `testOutputColorMatchesInput()` `testIsLoggedInFailsWithInvalidUser()` + +Writing tests is an art, and there are many resources available to help learn how. +Review the links above and always pay attention to your code coverage. + +### Database Tests + +Tests can include migrating, seeding, and testing against a mock or live database. +Be sure to modify the test case (or create your own) to point to your seed and migrations +and include any additional steps to be run before tests in the `setUp()` method. +See [Testing Your Database](https://codeigniter.com/user_guide/testing/database.html) +for details. diff --git a/tests/_support/Database/Migrations/2020-02-22-222222_example_migration.php b/tests/_support/Database/Migrations/2020-02-22-222222_example_migration.php new file mode 100644 index 0000000..a73356d --- /dev/null +++ b/tests/_support/Database/Migrations/2020-02-22-222222_example_migration.php @@ -0,0 +1,37 @@ +forge->addField('id'); + $this->forge->addField([ + 'name' => ['type' => 'varchar', 'constraint' => 31], + 'uid' => ['type' => 'varchar', 'constraint' => 31], + 'class' => ['type' => 'varchar', 'constraint' => 63], + 'icon' => ['type' => 'varchar', 'constraint' => 31], + 'summary' => ['type' => 'varchar', 'constraint' => 255], + 'created_at' => ['type' => 'datetime', 'null' => true], + 'updated_at' => ['type' => 'datetime', 'null' => true], + 'deleted_at' => ['type' => 'datetime', 'null' => true], + ]); + + $this->forge->addKey('name'); + $this->forge->addKey('uid'); + $this->forge->addKey(['deleted_at', 'id']); + $this->forge->addKey('created_at'); + + $this->forge->createTable('factories'); + } + + public function down(): void + { + $this->forge->dropTable('factories'); + } +} diff --git a/tests/_support/Database/Seeds/ExampleSeeder.php b/tests/_support/Database/Seeds/ExampleSeeder.php new file mode 100644 index 0000000..619fc27 --- /dev/null +++ b/tests/_support/Database/Seeds/ExampleSeeder.php @@ -0,0 +1,41 @@ + 'Test Factory', + 'uid' => 'test001', + 'class' => 'Factories\Tests\NewFactory', + 'icon' => 'fas fa-puzzle-piece', + 'summary' => 'Longer sample text for testing', + ], + [ + 'name' => 'Widget Factory', + 'uid' => 'widget', + 'class' => 'Factories\Tests\WidgetPlant', + 'icon' => 'fas fa-puzzle-piece', + 'summary' => 'Create widgets in your factory', + ], + [ + 'name' => 'Evil Factory', + 'uid' => 'evil-maker', + 'class' => 'Factories\Evil\MyFactory', + 'icon' => 'fas fa-book-dead', + 'summary' => 'Abandon all hope, ye who enter here', + ], + ]; + + $builder = $this->db->table('factories'); + + foreach ($factories as $factory) { + $builder->insert($factory); + } + } +} diff --git a/tests/_support/Libraries/ConfigReader.php b/tests/_support/Libraries/ConfigReader.php new file mode 100644 index 0000000..0bb4a8f --- /dev/null +++ b/tests/_support/Libraries/ConfigReader.php @@ -0,0 +1,19 @@ +findAll(); + + // Make sure the count is as expected + $this->assertCount(3, $objects); + } + + public function testSoftDeleteLeavesRow(): void + { + $model = new ExampleModel(); + $this->setPrivateProperty($model, 'useSoftDeletes', true); + $this->setPrivateProperty($model, 'tempUseSoftDeletes', true); + + /** @var stdClass $object */ + $object = $model->first(); + $model->delete($object->id); + + // The model should no longer find it + $this->assertNull($model->find($object->id)); + + // ... but it should still be in the database + $result = $model->builder()->where('id', $object->id)->get()->getResult(); + + $this->assertCount(1, $result); + } +} diff --git a/tests/index.html b/tests/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/tests/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/tests/session/ExampleSessionTest.php b/tests/session/ExampleSessionTest.php new file mode 100644 index 0000000..33242a4 --- /dev/null +++ b/tests/session/ExampleSessionTest.php @@ -0,0 +1,17 @@ +set('logged_in', 123); + $this->assertSame(123, $session->get('logged_in')); + } +} diff --git a/tests/unit/HealthTest.php b/tests/unit/HealthTest.php new file mode 100644 index 0000000..b3e480f --- /dev/null +++ b/tests/unit/HealthTest.php @@ -0,0 +1,49 @@ +assertTrue(defined('APPPATH')); + } + + public function testBaseUrlHasBeenSet(): void + { + $validation = service('validation'); + + $env = false; + + // Check the baseURL in .env + if (is_file(HOMEPATH . '.env')) { + $env = preg_grep('/^app\.baseURL = ./', file(HOMEPATH . '.env')) !== false; + } + + if ($env) { + // BaseURL in .env is a valid URL? + // phpunit.xml.dist sets app.baseURL in $_SERVER + // So if you set app.baseURL in .env, it takes precedence + $config = new App(); + $this->assertTrue( + $validation->check($config->baseURL, 'valid_url'), + 'baseURL "' . $config->baseURL . '" in .env is not valid URL', + ); + } + + // Get the baseURL in app/Config/App.php + // You can't use Config\App, because phpunit.xml.dist sets app.baseURL + $reader = new ConfigReader(); + + // BaseURL in app/Config/App.php is a valid URL? + $this->assertTrue( + $validation->check($reader->baseURL, 'valid_url'), + 'baseURL "' . $reader->baseURL . '" in app/Config/App.php is not valid URL', + ); + } +} diff --git a/writable/.htaccess b/writable/.htaccess new file mode 100644 index 0000000..3462048 --- /dev/null +++ b/writable/.htaccess @@ -0,0 +1,6 @@ + + Require all denied + + + Deny from all + diff --git a/writable/cache/index.html b/writable/cache/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/writable/cache/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/writable/debugbar/index.html b/writable/debugbar/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/writable/debugbar/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/writable/faces/.gitkeep b/writable/faces/.gitkeep new file mode 100644 index 0000000..6f87c50 --- /dev/null +++ b/writable/faces/.gitkeep @@ -0,0 +1,3 @@ +# Foto wajah siswa untuk Smart Presensi (writable/faces/{student_id}.jpg) +# Isi dengan sync dari Google Drive atau upload per siswa. +# Lihat backend/docs/SYNC_FOTO_WAJAH.md diff --git a/writable/index.html b/writable/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/writable/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/writable/logs/index.html b/writable/logs/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/writable/logs/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/writable/session/index.html b/writable/session/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/writable/session/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/writable/uploads/index.html b/writable/uploads/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/writable/uploads/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + +