├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── CB-manifest.json ├── CB-worker.js ├── README.md ├── assets ├── .htaccess ├── A-classes.js ├── A-course-user.js ├── A-course.js ├── A-import.js ├── A-reports.js ├── A-settings.js ├── A-users-nfc.js ├── A-users.js ├── CB-autocomplete.js ├── PAGE-cb.css ├── PAGE-cb.js ├── PAGE-forgot.js ├── PAGE-login-nfc.js ├── PAGE-login-wa.js ├── PAGE-login.js ├── PAGE-myaccount.js ├── PAGE-nfc.js ├── PAGE-wa-helper.js ├── PAGE-wa.js ├── T-classes.js ├── TA-attend.js ├── U-classes.js ├── U-qr.js ├── bootstrap.bundle.min.js ├── bootstrap.bundle.min.js.map ├── bootstrap.min.css ├── bootstrap.min.css.map ├── csv.min.js ├── dummy-classes.csv ├── dummy-course-users.csv ├── dummy-courses.csv ├── dummy-users.csv ├── favicon.png ├── head-iwh.webp ├── html5-qrcode.min.js ├── ico-512.png ├── icomoon.woff ├── iwh-1.png ├── iwh-2.png ├── iwh-3.png ├── iwh-4.png ├── iwh-5.png ├── iwh-6.png ├── qrcode.min.js └── users.webp ├── index.php ├── lib ├── .htaccess ├── API-attend.php ├── API-autocomplete.php ├── API-classes.php ├── API-courses.php ├── API-nfcin.php ├── API-session.php ├── API-settings.php ├── API-users.php ├── API-wain.php ├── CORE-Config.php ├── CORE-Go.php ├── CORE-Install-HTML.php ├── CORE-Install-JS.php ├── CORE-Install.php ├── HOOK-API-CORS.php ├── HOOK-Routes.php ├── HOOK-SESS-Load.php ├── HOOK-SESS-Save.php ├── LIB-Attend.php ├── LIB-Autocomplete.php ├── LIB-Classes.php ├── LIB-Core.php ├── LIB-Courses.php ├── LIB-DB.php ├── LIB-Forgot.php ├── LIB-Install.php ├── LIB-MInstall.php ├── LIB-Mail.php ├── LIB-NFCIN.php ├── LIB-Page.php ├── LIB-Report.php ├── LIB-Route.php ├── LIB-Session.php ├── LIB-Settings.php ├── LIB-Users.php ├── LIB-WAIN.php ├── SQL-I-WAS-HERE-1.sql ├── WebAuthn │ ├── autoload.php │ ├── composer │ │ ├── ClassLoader.php │ │ ├── InstalledVersions.php │ │ ├── LICENSE │ │ ├── autoload_classmap.php │ │ ├── autoload_namespaces.php │ │ ├── autoload_psr4.php │ │ ├── autoload_real.php │ │ ├── autoload_static.php │ │ ├── installed.json │ │ ├── installed.php │ │ └── platform_check.php │ └── lbuchs │ │ └── webauthn │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── _test │ │ ├── client.html │ │ ├── rootCertificates │ │ │ ├── apple.pem │ │ │ ├── globalSign.pem │ │ │ ├── googleHardware.pem │ │ │ ├── hypersecu.pem │ │ │ ├── mds │ │ │ │ └── .gitkeep │ │ │ ├── microsoftTpmCollection.pem │ │ │ ├── solo.pem │ │ │ └── yubico.pem │ │ └── server.php │ │ ├── composer.json │ │ └── src │ │ ├── Attestation │ │ ├── AttestationObject.php │ │ ├── AuthenticatorData.php │ │ └── Format │ │ │ ├── AndroidKey.php │ │ │ ├── AndroidSafetyNet.php │ │ │ ├── Apple.php │ │ │ ├── FormatBase.php │ │ │ ├── None.php │ │ │ ├── Packed.php │ │ │ ├── Tpm.php │ │ │ └── U2f.php │ │ ├── Binary │ │ └── ByteBuffer.php │ │ ├── CBOR │ │ └── CborDecoder.php │ │ ├── WebAuthn.php │ │ └── WebAuthnException.php └── jwt │ ├── autoload.php │ ├── composer │ ├── ClassLoader.php │ ├── InstalledVersions.php │ ├── LICENSE │ ├── autoload_classmap.php │ ├── autoload_namespaces.php │ ├── autoload_psr4.php │ ├── autoload_real.php │ ├── autoload_static.php │ ├── installed.json │ ├── installed.php │ └── platform_check.php │ └── firebase │ └── php-jwt │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── composer.json │ └── src │ ├── BeforeValidException.php │ ├── CachedKeySet.php │ ├── ExpiredException.php │ ├── JWK.php │ ├── JWT.php │ ├── Key.php │ └── SignatureInvalidException.php └── pages ├── .htaccess ├── A-about.php ├── A-class-form.php ├── A-class-list.php ├── A-classes.php ├── A-course-form.php ├── A-course-list.php ├── A-course-user-list.php ├── A-course-user.php ├── A-courses.php ├── A-home.php ├── A-import.php ├── A-settings.php ├── A-users-form.php ├── A-users-list.php ├── A-users-nfc.php ├── A-users.php ├── MAIL-forgot-a.php ├── MAIL-forgot-b.php ├── PAGE-404.php ├── PAGE-empty.php ├── PAGE-forgot.php ├── PAGE-home.php ├── PAGE-icon.php ├── PAGE-login.php ├── PAGE-myaccount.php ├── PAGE-passwordless.php ├── REPORT-loader.php ├── T-class-list.php ├── T-home.php ├── TA-attend-list.php ├── TA-attend.php ├── TA-classqr.php ├── TEMPLATE-A-menu.php ├── TEMPLATE-T-menu.php ├── TEMPLATE-U-menu.php ├── TEMPLATE-bottom.php ├── TEMPLATE-top.php ├── U-class-list.php ├── U-home.php ├── U-qr.php └── USR-check.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: @code-boxx 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /CB-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "I Was Here", 3 | "name": "I Was Here", 4 | "icons": [{ 5 | "src": "assets/favicon.png", 6 | "sizes": "64x64", 7 | "type": "image/png" 8 | }, { 9 | "src": "assets/ico-512.png", 10 | "sizes": "512x512", 11 | "type": "image/png" 12 | }], 13 | "start_url": "/", 14 | "scope": "/", 15 | "background_color": "white", 16 | "theme_color": "white", 17 | "display": "standalone" 18 | } -------------------------------------------------------------------------------- /CB-worker.js: -------------------------------------------------------------------------------- 1 | // (A) CREATE/INSTALL CACHE 2 | self.addEventListener("install", evt => { 3 | self.skipWaiting(); 4 | evt.waitUntil( 5 | caches.open("IWASHERE") 6 | .then(cache => cache.addAll([ 7 | // (A1) ADMIN 8 | "assets/A-classes.js", 9 | "assets/A-course.js", 10 | "assets/A-course-user.js", 11 | "assets/A-import.js", 12 | "assets/A-reports.js", 13 | "assets/A-settings.js", 14 | "assets/A-users.js", 15 | "assets/A-users-nfc.js", 16 | 17 | // (A2) BOOTSTRAP 18 | "assets/bootstrap.bundle.min.js", 19 | "assets/bootstrap.bundle.min.js.map", 20 | "assets/bootstrap.min.css", 21 | "assets/bootstrap.min.css.map", 22 | 23 | // (A3) COMMON INTERFACE 24 | "assets/CB-autocomplete.js", 25 | "assets/csv.min.js", 26 | "assets/html5-qrcode.min.js", 27 | "assets/icomoon.woff", 28 | "assets/PAGE-cb.js", 29 | "assets/PAGE-cb.css", 30 | "assets/qrcode.min.js", 31 | "CB-manifest.json", 32 | 33 | // (A4) ICONS + IMAGES 34 | "assets/favicon.png", 35 | "assets/ico-512.png", 36 | "assets/users.webp", 37 | 38 | // (A5) PAGES 39 | "assets/PAGE-forgot.js", 40 | "assets/PAGE-login.js", 41 | "assets/PAGE-login-nfc.js", 42 | "assets/PAGE-login-wa.js", 43 | "assets/PAGE-myaccount.js", 44 | "assets/PAGE-nfc.js", 45 | "assets/PAGE-wa.js", 46 | "assets/PAGE-wa-helper.js", 47 | 48 | // (A6) TEACHER & STUDENT 49 | "assets/TA-attend.js", 50 | "assets/T-classes.js", 51 | "assets/U-classes.js", 52 | "assets/U-qr.js", 53 | ])) 54 | .catch(err => console.error(err)) 55 | ); 56 | }); 57 | 58 | // (B) CLAIM CONTROL INSTANTLY 59 | self.addEventListener("activate", evt => self.clients.claim()); 60 | 61 | // (C) LOAD FROM CACHE FIRST, FALLBACK TO NETWORK IF NOT FOUND 62 | self.addEventListener("fetch", evt => evt.respondWith( 63 | caches.match(evt.request).then(res => res || fetch(evt.request)) 64 | )); 65 | 66 | // (D) LISTEN TO PUSH NOTIFICATIONS 67 | self.addEventListener("push", evt => { 68 | const data = evt.data.json(); 69 | self.registration.showNotification(data.title, { 70 | body: data.body, 71 | icon: data.icon, 72 | image: data.image 73 | }); 74 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## I WAS HERE 2 | I Was Here is an open-source PHP Student Attendance Management System. Featuring NFC and passwordless login, also allows students to take attendance by scanning a QR code. 3 |

4 | 5 | ## :white_check_mark: FEATURES 6 | 1) NFC and passwordless login. 7 | 2) Installble progressive web app. 8 | 3) Take attendance manually, or let students scan a QR code. 9 |

10 | 11 | ## :camera: SCREENSHOTS 12 |

13 | 14 | 15 | 16 | 17 | 18 | 19 |


20 | 21 | ## :ballot_box_with_check: REQUIREMENTS 22 | 1) LAMP/WAMP/MAMP/XAMPP 23 | 2) Apache Mod Rewrite 24 | 3) PHP MYSQL PDO Extension 25 | 4) At least PHP 8.0 26 |

27 | 28 | ## :floppy_disk: INSTALLATION 29 | 1) Just copy/unzip into your `http` folder. 30 | 2) Access `http://your-site.com/` in your browser and walk through the installer. 31 |

32 | 33 | ## :bulb: DOCUMENTATION 34 | 1) [How To Use](https://code-boxx.com/i-was-here-php-attendance-system/#sec-use) 35 | 2) [FAQ](https://code-boxx.com/core-boxx-php-framework/#sec-faq) 36 | 3) [For The Developers](https://code-boxx.com/i-was-here-php-attendance-system/#sec-dev) 37 | 38 | ## :electric_plug: FRAMEWORKS 39 | 1) PHP Packages 40 | - [Core Boxx](https://code-boxx.com/core-boxx-php-framework/) 41 | - [PHP-JWT](https://github.com/firebase/php-jwt) 42 | - [PHP WebAuthn](https://github.com/lbuchs/WebAuthn/tree/master) 43 | 2) HTML JS 44 | - [Bootstrap](https://getbootstrap.com/) 45 | - [IconMoon](https://icomoon.io/) 46 | - [HTML5 QRCode Scanner](https://github.com/mebjas/html5-qrcode) 47 | - [QRCodeJS](https://davidshimjs.github.io/qrcodejs/) 48 | - [csv.js](https://github.com/okfn/csv.js/) 49 |

50 | 51 | ## :star: SUPPORT 52 | Like this project? Just give it a star. That will indirectly help grow my blog a little bit. :wink: 53 |

54 | 55 | ## :newspaper: LICENSE 56 | Copyright by Code Boxx 57 | 58 | Permission is hereby granted, free of charge, to any person obtaining a copy 59 | of this software and associated documentation files (the "Software"), to deal 60 | in the Software without restriction, including without limitation the rights 61 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 62 | copies of the Software, and to permit persons to whom the Software is 63 | furnished to do so, subject to the following conditions: 64 | 65 | The above copyright notice and this permission notice shall be included in all 66 | copies or substantial portions of the Software. 67 | 68 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 69 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 70 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 71 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 72 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 73 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 74 | SOFTWARE. -------------------------------------------------------------------------------- /assets/.htaccess: -------------------------------------------------------------------------------- 1 | Options -Indexes -------------------------------------------------------------------------------- /assets/A-classes.js: -------------------------------------------------------------------------------- 1 | var classes = { 2 | // (A) SHOW COURSE CLASSES PAGE 3 | pg : 1, // current page 4 | find : "", // current search 5 | list : silent => { 6 | if (silent!==true) { cb.page(1); } 7 | cb.load({ 8 | page : "A/class/list", 9 | target : "class-list", 10 | data : { 11 | page : classes.pg, 12 | search : classes.find 13 | } 14 | }); 15 | }, 16 | 17 | // (B) GO TO PAGE 18 | // pg : int, page number 19 | goToPage : pg => { if (pg!=classes.pg) { 20 | classes.pg = pg; 21 | classes.list(); 22 | }}, 23 | 24 | // (C) SEARCH CLASSES 25 | search : () => { 26 | classes.find = document.getElementById("class-search").value; 27 | classes.pg = 1; 28 | classes.list(); 29 | return false; 30 | }, 31 | 32 | // (D) SHOW ADD/EDIT DOCKET 33 | // id : class ID, for edit only 34 | addEdit : id => cb.load({ 35 | page : "A/class/form", 36 | target : "cb-page-2", 37 | data : { id : id ? id : "" }, 38 | onload : () => { 39 | cb.page(2); 40 | autocomplete.attach({ 41 | target : document.getElementById("class_course"), 42 | mod : "autocomplete", act : "course", 43 | onpick : r => { 44 | document.getElementById("class_course").value = `[${r.v}] ${r.n}`; 45 | document.getElementById("class_course_code").value = r.v; 46 | classes.toggle(true); 47 | } 48 | }); 49 | } 50 | }), 51 | 52 | // (E) TOGGLE ADD/EDIT FORM ON SELECTING COURSE 53 | toggle : set => { 54 | // (E1) HTML ELEMENTS 55 | let hCourse = document.getElementById("class_course"), 56 | hCode = document.getElementById("class_course_code"), 57 | hCNote = document.getElementById("class_course_note"), 58 | hDate = document.getElementById("class_date"), 59 | hTeacher = document.getElementById("class_teacher"), 60 | hDesc = document.getElementById("class_desc"); 61 | 62 | // (E2) COURSE CHOSEN - UPDATE FORM 63 | if (set) { 64 | cb.api({ 65 | mod : "autocomplete", act : "icourse", 66 | data : { code : hCode.value }, 67 | passmsg : false, 68 | onpass : res => { 69 | // (E2-1) "DISABLE" COURSE FIRST 70 | hCourse.readOnly = true; 71 | 72 | // (E2-2) DATE 73 | hDate.setAttribute("min", res.data.c["course_start"] + " 00:00:00"); 74 | hDate.setAttribute("max", res.data.c["course_end"] + " 23:59:59"); 75 | hDate.value = res.data.c["course_start"] + " 00:00:00"; 76 | hDate.disabled = false; 77 | 78 | // (E2-3) TEACHER 79 | hTeacher.disabled = false; 80 | if (res.data.t!=null) { 81 | hTeacher.innerHTML = ""; 82 | Object.entries(res.data.t).forEach(([i,t]) => { 83 | let opt = document.createElement("option"); 84 | opt.value = i; 85 | opt.innerHTML = `${t["user_name"]} (${t["user_email"]})`; 86 | hTeacher.appendChild(opt); 87 | }); 88 | } else { hTeacher.innerHTML = ""; } 89 | 90 | // (E2-4) DESCRIPTION 91 | hDesc.disabled = false; 92 | 93 | // (E2-5) CLICK COURSE TO CHANGE 94 | hCourse.onclick = () => classes.toggle(false); 95 | hCNote.classList.remove("d-none"); 96 | } 97 | }); 98 | } 99 | 100 | // (E3) UNSET COURSE 101 | else { 102 | // (E3-1) RESET COURSE 103 | hCourse.value = ""; 104 | hCourse.readOnly = false; 105 | hCourse.onclick = ""; 106 | hCNote.classList.add("d-none"); 107 | hCode.value = ""; 108 | 109 | // (E3-2) RESET + DISABLE FIELDS 110 | hDate.disabled = true; 111 | hDate.value = ""; 112 | hTeacher.disabled = true; 113 | hTeacher.innerHTML = ""; 114 | hDesc.disabled = true; 115 | hDesc.value = ""; 116 | } 117 | }, 118 | 119 | // (F) SAVE CLASS 120 | save : () => { 121 | // (F1) GET DATA 122 | var data = { 123 | code : document.getElementById("class_course_code").value, // course code 124 | uid : document.getElementById("class_teacher").value, // user id 125 | desc : document.getElementById("class_desc").value, // description 126 | date : document.getElementById("class_date").value.replace("T", " ") // date 127 | }; 128 | var id = document.getElementById("class_id").value; // class id 129 | if (id!="") { data.id = id; } 130 | 131 | // (F2) AJAX 132 | cb.api({ 133 | mod : "classes", act : "save", 134 | data : data, 135 | passmsg : "Class Saved", 136 | onpass : classes.list 137 | }); 138 | return false; 139 | }, 140 | 141 | // (G) DELETE CLASS 142 | // id : int, class ID 143 | del : id => cb.modal("Please confirm", "Attendance records of this class will be lost!", () => cb.api({ 144 | mod : "classes", act : "del", 145 | data : { id: id }, 146 | passmsg : "Class Deleted", 147 | onpass : classes.list 148 | })), 149 | 150 | // (H) IMPORT CLASSES 151 | import : () => im.init({ 152 | name : "CLASSES", 153 | at : 2, back : 1, 154 | eg : "dummy-classes.csv", 155 | api : { mod : "classes", act : "import" }, 156 | after : () => classes.list(true), 157 | cols : [ 158 | ["Course Code", "code", true], 159 | ["Date (YYYY-MM-DD HH:MM:SS)", "date", true], 160 | ["Teacher's Email", "email", true], 161 | ["Description (If any)", "desc"] 162 | ] 163 | }) 164 | }; 165 | 166 | window.addEventListener("load", () => { 167 | classes.list(); 168 | autocomplete.attach({ 169 | target : document.getElementById("class-search"), 170 | mod : "autocomplete", act : "course", 171 | onpick : res => classes.search() 172 | }); 173 | }); -------------------------------------------------------------------------------- /assets/A-course-user.js: -------------------------------------------------------------------------------- 1 | var cuser = { 2 | // (A) SHOW COURSE USERS PAGE 3 | pg : 1, // current page 4 | code : null, // current course code 5 | show : code => { 6 | cuser.code = code; 7 | cb.page(2); 8 | cb.load({ 9 | page : "A/course/user", 10 | target : "cb-page-2", 11 | data : { code : code }, 12 | onload : () => { 13 | autocomplete.attach({ 14 | target : document.getElementById("course-user-add"), 15 | mod : "autocomplete", act : "userEmail", 16 | onpick : res => cuser.add() 17 | }); 18 | cuser.list(); 19 | } 20 | }); 21 | }, 22 | 23 | // (B) SHOW ALL USERS IN COURSE 24 | list : () => cb.load({ 25 | page : "A/course/user/list", 26 | target : "course-user-list", 27 | data : { 28 | page : cuser.pg, 29 | code : cuser.code 30 | } 31 | }), 32 | 33 | // (C) GO TO PAGE 34 | // pg : int, page number 35 | goToPage : pg => { if (pg!=cuser.pg) { 36 | cuser.pg = pg; 37 | cuser.list(); 38 | }}, 39 | 40 | // (D) ADD USER TO COURSE 41 | add : () => { 42 | // (D1) ADD EMAIL FIELD 43 | var field = document.getElementById("course-user-add"); 44 | 45 | // (D2) AJAX 46 | cb.api({ 47 | mod : "courses", act : "addUser", 48 | data : { 49 | code : cuser.code, 50 | uid : field.value 51 | }, 52 | passmsg : "User Added", 53 | onpass : () => { 54 | field.value = ""; 55 | cuser.list(); 56 | } 57 | }); 58 | return false; 59 | }, 60 | 61 | // (E) REMOVE USER FROM COURSE 62 | // id : user id 63 | del : id => cb.modal("Please confirm", "User will be removed from the course, but past attendance will be kept.", () => cb.api({ 64 | mod : "courses", act : "delUser", 65 | data : { 66 | code : cuser.code, 67 | uid : id 68 | }, 69 | passmsg : "User removed from course", 70 | onpass : cuser.list 71 | })), 72 | 73 | // (F) IMPORT USERS TO COURSE 74 | import : () => im.init({ 75 | name : "USERS TO COURSE", 76 | at : 3, back : 2, 77 | eg : "dummy-course-users.csv", 78 | api : { mod : "courses", act : "addUser" }, 79 | after : () => cuser.list(), 80 | data : { code : cuser.code }, 81 | cols : [ 82 | ["User's Email", "uid", true] 83 | ] 84 | }) 85 | }; -------------------------------------------------------------------------------- /assets/A-course.js: -------------------------------------------------------------------------------- 1 | var course = { 2 | // (A) SHOW ALL COURSES 3 | pg : 1, // current page 4 | find : "", // current search 5 | list : silent => { 6 | if (silent!==true) { cb.page(1); } 7 | cb.load({ 8 | page : "A/course/list", 9 | target : "course-list", 10 | data : { 11 | page : course.pg, 12 | search : course.find 13 | } 14 | }); 15 | }, 16 | 17 | // (B) GO TO PAGE 18 | // pg : int, page number 19 | goToPage : pg => { if (pg!=course.pg) { 20 | course.pg = pg; 21 | course.list(); 22 | }}, 23 | 24 | // (C) SEARCH COURSES 25 | search : () => { 26 | course.find = document.getElementById("course-search").value; 27 | course.pg = 1; 28 | course.list(); 29 | return false; 30 | }, 31 | 32 | // (D) SHOW ADD/EDIT DOCKET 33 | // code : course code, for edit only 34 | addEdit : code => cb.load({ 35 | page : "A/course/form", 36 | target : "cb-page-2", 37 | data : { code : code ? code : "" }, 38 | onload : () => cb.page(2) 39 | }), 40 | 41 | // (E) SAVE COURSE 42 | save : () => { 43 | // (E1) GET DATA 44 | var data = { 45 | code : document.getElementById("course_code").value, 46 | name : document.getElementById("course_name").value, 47 | desc : document.getElementById("course_desc").value, 48 | start : document.getElementById("course_start").value, 49 | end : document.getElementById("course_end").value 50 | }; 51 | var ocode = document.getElementById("course_ocode").value; 52 | if (ocode!="") { data.ocode = ocode; } 53 | 54 | // (E2) DATE CHECK 55 | let start = new Date(data.start), 56 | end = new Date(data.end); 57 | if (start > end) { 58 | cb.modal("Error!", "Start date cannot be later than end date."); 59 | return false; 60 | } 61 | 62 | // (E3) AJAX 63 | cb.api({ 64 | mod : "courses", act : "save", 65 | data : data, 66 | passmsg : "Course Saved", 67 | onpass : course.list 68 | }); 69 | return false; 70 | }, 71 | 72 | // (F) DELETE COURSE 73 | // code : course code 74 | del : code => cb.modal("Please confirm", "All course data and attendance will be lost!", () => cb.api({ 75 | mod : "courses", act : "del", 76 | data : { code : code }, 77 | passmsg : "Course Deleted", 78 | onpass : course.list 79 | })), 80 | 81 | // (G) IMPORT COURSES 82 | import : () => im.init({ 83 | name : "COURSES", 84 | at : 2, back : 1, 85 | eg : "dummy-courses.csv", 86 | api : { mod : "courses", act : "import" }, 87 | after : () => course.list(true), 88 | cols : [ 89 | ["Course Code", "code", true], 90 | ["Course Name", "name", true], 91 | ["Description (if any)", "desc"], 92 | ["Start Date (YYYY-MM-DD)", "start", true], 93 | ["End Date (YYYY-MM-DD)", "end", true] 94 | ] 95 | }) 96 | }; 97 | 98 | window.addEventListener("load", () => { 99 | course.list(); 100 | autocomplete.attach({ 101 | target : document.getElementById("course-search"), 102 | mod : "autocomplete", act : "course", 103 | onpick : res => course.search() 104 | }); 105 | }); -------------------------------------------------------------------------------- /assets/A-reports.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load", () => { 2 | autocomplete.attach({ 3 | target : document.getElementById("attend-course"), 4 | mod : "autocomplete", act : "course", 5 | onpick : res => { 6 | document.getElementById("attend-course").value = ""; 7 | document.getElementById("attend-code").value = res.v; 8 | document.getElementById("report-attend").submit(); 9 | } 10 | }); 11 | }); -------------------------------------------------------------------------------- /assets/A-settings.js: -------------------------------------------------------------------------------- 1 | function save () { 2 | // (A) GET ALL DATA 3 | let data = {}; 4 | for (let i of document.querySelectorAll("#set-list input[type=text]")) { 5 | data[i.name] = i.value; 6 | } 7 | 8 | // (B) API CALL 9 | cb.api({ 10 | mod : "settings", act : "save", 11 | data : { settings : JSON.stringify(data) }, 12 | passmsg : "Settings Saved" 13 | }); 14 | return false; 15 | } -------------------------------------------------------------------------------- /assets/A-users-nfc.js: -------------------------------------------------------------------------------- 1 | var unfc = { 2 | // (A) SHOW WRITE NFC PAGE 3 | hnBtn : null, // html write nfc button 4 | hnStat : null, // html write nfc button status 5 | hnNull : null, // html null token button 6 | show : id => cb.load({ 7 | page : "A/users/nfc", target : "cb-page-2", 8 | data : { id : id }, 9 | onload : () => { 10 | unfc.hnBtn = document.getElementById("nfc-btn"); 11 | unfc.hnStat = document.getElementById("nfc-stat"); 12 | unfc.hnNull = document.getElementById("nfc-null"); 13 | if ("NDEFReader" in window) { 14 | unfc.hnStat.innerHTML = "Create Login Token"; 15 | unfc.hnBtn.disabled = false; 16 | } else { 17 | unfc.hnStat.innerHTML = "Web NFC not available"; 18 | } 19 | cb.page(2); 20 | } 21 | }), 22 | 23 | // (B) CREATE NEW NFC LOGIN TAG 24 | add : id => { 25 | // (B1) DISABLE "WRITE NFC" BUTTON 26 | unfc.hnBtn.disabled = true; 27 | 28 | // (B2) REGISTER WITH SERVER + GET JWT 29 | cb.api({ 30 | mod : "nfcin", act : "add", 31 | data : { id : id }, 32 | passmsg : false, 33 | onpass : res => { 34 | // (B2-1) ENABLE "NULLIFY" BUTTTON 35 | unfc.hnNull.disabled = false; 36 | 37 | // (B2-2) ON SUCCESSFUL NFC WRITE 38 | nfc.onwrite = () => { 39 | nfc.standby(); 40 | cb.modal("Successful", "Login token successfully created."); 41 | unfc.hnStat.innerHTML = "Done"; 42 | }; 43 | 44 | // (B2-3) ON FAILED NFC WRITE 45 | nfc.onerror = err => { 46 | nfc.stop(); 47 | console.error(err); 48 | cb.modal("ERROR", err.message); 49 | unfc.hnStat.innerHTML = "ERROR!"; 50 | unfc.hnBtn.disabled = false; 51 | }; 52 | 53 | // (B2-4) START NFC WRITE 54 | nfc.write(res.data); 55 | unfc.hnStat.innerHTML = "Tap NFC tag to write"; 56 | } 57 | }) 58 | }, 59 | 60 | // (C) NULLIFY NFC TOKEN 61 | del : id => cb.api({ 62 | mod : "nfcin", act : "del", 63 | data : { id : id }, 64 | passmsg : "Login token nullified.", 65 | onpass : res => unfc.hnNull.disabled = true 66 | }), 67 | 68 | // (D) END WRITE NFC SESSION 69 | back : () => { 70 | nfc.stop(); 71 | cb.page(1); 72 | } 73 | }; -------------------------------------------------------------------------------- /assets/A-users.js: -------------------------------------------------------------------------------- 1 | var usr = { 2 | // (A) SHOW ALL USERS 3 | pg : 1, // current page 4 | find : "", // current search 5 | list : silent => { 6 | if (silent!==true) { cb.page(1); } 7 | cb.load({ 8 | page : "A/users/list", target : "user-list", 9 | data : { 10 | page : usr.pg, 11 | search : usr.find 12 | } 13 | }); 14 | }, 15 | 16 | // (B) GO TO PAGE 17 | // pg : int, page number 18 | goToPage : pg => { if (pg!=usr.pg) { 19 | usr.pg = pg; 20 | usr.list(); 21 | }}, 22 | 23 | // (C) SEARCH USER 24 | search : () => { 25 | usr.find = document.getElementById("user-search").value; 26 | usr.pg = 1; 27 | usr.list(); 28 | return false; 29 | }, 30 | 31 | // (D) SHOW ADD/EDIT DOCKET 32 | // id : user ID, for edit only 33 | addEdit : id => cb.load({ 34 | page : "A/users/form", target : "cb-page-2", 35 | data : { id : id ? id : "" }, 36 | onload : () => cb.page(2) 37 | }), 38 | 39 | // (E) SAVE USER 40 | save : () => { 41 | // (E1) GET DATA 42 | var data = { 43 | name : document.getElementById("user_name").value, 44 | email : document.getElementById("user_email").value, 45 | password : document.getElementById("user_password").value, 46 | lvl : document.getElementById("user_level").value 47 | }; 48 | var id = document.getElementById("user_id").value; 49 | if (id!="") { data.id = id; } 50 | 51 | // (E2) PASSWORD STRENGTH 52 | if (!cb.checker(data.password)) { 53 | cb.modal("Error", "Password must be at least 8 characters alphanumeric"); 54 | return false; 55 | } 56 | 57 | // (E3) AJAX 58 | cb.api({ 59 | mod : "users", act : "save", 60 | data : data, 61 | passmsg : "User Saved", 62 | onpass : usr.list 63 | }); 64 | return false; 65 | }, 66 | 67 | // (F) SUSPEND USER 68 | // id : int, user ID 69 | // confirm : boolean, confirmed delete 70 | del : id => cb.modal("Please confirm", "Suspend this user?", () => cb.api({ 71 | mod : "users", act : "suspend", 72 | data : { id: id }, 73 | passmsg : "User Account Suspended", 74 | onpass : usr.list 75 | })), 76 | 77 | // (G) IMPORT USERS 78 | import : () => im.init({ 79 | name : "USERS", 80 | at : 2, back : 1, 81 | eg : "dummy-users.csv", 82 | api : { mod : "users", act : "import" }, 83 | after : () => usr.list(true), 84 | cols : [ 85 | ["Name", "name", true], 86 | ["Email", "email", true], 87 | ["Password", "password", true], 88 | ["Level (A,T,U,S)", "level", true] 89 | ] 90 | }) 91 | }; 92 | 93 | window.addEventListener("load", () => { 94 | usr.list(); 95 | autocomplete.attach({ 96 | target : document.getElementById("user-search"), 97 | mod : "autocomplete", act : "user", 98 | onpick : res => usr.search() 99 | }); 100 | }); -------------------------------------------------------------------------------- /assets/CB-autocomplete.js: -------------------------------------------------------------------------------- 1 | var autocomplete = { 2 | // (A) SETTINGS & PROPERTIES 3 | min : 2, // minimum 2 characters to trigger suggestions 4 | delay : 500, // delay before suggestion in ms 5 | active : null, // current active suggestion box 6 | 7 | // (B) ATTACH AUTOCOMPLETE 8 | // target : target html field 9 | // mod : api module 10 | // act : api action 11 | // data : additional data to send, optional 12 | // onpick : callback function, optional 13 | attach : i => { 14 | // (B1) CREATE SUGGESTION BOX + NATIVE AUTOCOMPLETE OFF 15 | i.suggest = document.createElement("ul"); 16 | i.suggest.className = "list-group position-absolute z-3 d-none"; 17 | i.suggest.style.top = "100%"; 18 | i.target.setAttribute("autocomplete", "off"); 19 | 20 | // (B2) FLOATING FORM - DIRECT INSERT SUGGESTION BOX 21 | if (i.target.parentElement.classList.contains("form-floating")) { 22 | i.wrapper = i.target.parentElement; 23 | i.wrapper.appendChild(i.suggest); 24 | } 25 | 26 | // (B3) "NORMAL FIELD" 27 | else { 28 | i.wrapper = document.createElement("div"); 29 | 30 | // CRAZY CSS STYLES - CHANGE THESE IF IT LOOKS STRANGE 31 | let d = window.getComputedStyle(i.target).getPropertyValue("display"); 32 | if (i.target.classList.contains("form-control")) { i.wrapper.style.width = "100%"; } 33 | i.wrapper.style.display = d.includes("inline") ? "inline-flex" : "flex" ; 34 | i.wrapper.style.position = "relative"; 35 | 36 | i.target.parentElement.insertBefore(i.wrapper, i.target); 37 | i.wrapper.appendChild(i.target); 38 | i.wrapper.appendChild(i.suggest); 39 | } 40 | 41 | // (B4) INSTANCE PROPERTIES 42 | i.timer = null; 43 | if (i.data==undefined) { i.data = {}; } 44 | 45 | // (B5) CLOSE THIS SUGGESTION BOX 46 | i.close = () => { 47 | window.clearTimeout(i.timer); 48 | i.suggest.innerHTML = ""; 49 | i.suggest.classList.add("d-none"); 50 | }; 51 | 52 | // (B6) FETCH DATA FROM API 53 | i.fetch = () => { 54 | // (B6-1) CLEAR PREVIOUS TIMER 55 | window.clearTimeout(i.timer); 56 | 57 | // (B6-2) POST DATA 58 | let data = { search : i.target.value }; 59 | if (i.data) { for (let k in i.data) { 60 | if (i.data[k] instanceof HTMLElement) { data[k] = i.data[k].value; } 61 | else { data[k] = i.data[k]; } 62 | }} 63 | 64 | // (B6-3) API CALL 65 | cb.api({ 66 | mod : i.mod, act : i.act, data : data, 67 | loading : false, passmsg : false, 68 | onpass : res => { 69 | // (B6-4) NO RESULTS 70 | if (res.data==null) { i.close(); } 71 | 72 | // (B6-5) DRAW RESULTS & SET "CURRENTLY ACTIVE" 73 | else { 74 | i.suggest.innerHTML = ""; 75 | for (let r of res.data) { 76 | let row = document.createElement("li"); 77 | row.className = "list-group-item"; 78 | row.innerHTML = r.n; 79 | row.onclick = () => { 80 | i.target.value = r.v ? r.v : r.n ; 81 | i.close(); 82 | if (i.onpick) { i.onpick(r); } 83 | }; 84 | i.suggest.appendChild(row); 85 | } 86 | i.suggest.classList.remove("d-none"); 87 | autocomplete.active = i; 88 | } 89 | } 90 | }); 91 | }; 92 | 93 | // (B7) LISTEN TO KEY PRESS 94 | i.target.addEventListener("keyup", evt => { 95 | // (B7-1) CLEAR OLD TIMER & SUGGESTION BOX 96 | i.close(); 97 | 98 | // (B7-2) CREATE NEW TIMER - FETCH DATA FROM SERVER 99 | if (i.target.value.length >= autocomplete.min) { 100 | i.timer = setTimeout(i.fetch, autocomplete.delay); 101 | } 102 | }); 103 | }, 104 | 105 | // (C) AUTOCLOSE SUGGESTION BOX ON CLICK ELSEWHERE 106 | checkclose : evt => { 107 | if (autocomplete.active!=null && 108 | autocomplete.active.wrapper.contains(evt.target)==false) { 109 | autocomplete.active.close(); 110 | } 111 | } 112 | }; 113 | document.addEventListener("click", autocomplete.checkclose); -------------------------------------------------------------------------------- /assets/PAGE-forgot.js: -------------------------------------------------------------------------------- 1 | function forgot () { 2 | cb.api({ 3 | mod : "session", act : "forgotA", 4 | data : { email : document.getElementById("forgot-email").value }, 5 | passmsg : false, 6 | onpass : () => cb.modal("Reset Link Sent", "Click on the reset link in your email.") 7 | }); 8 | return false; 9 | } -------------------------------------------------------------------------------- /assets/PAGE-login-nfc.js: -------------------------------------------------------------------------------- 1 | var nin = { 2 | // (A) INITIALIZE - CHECK NFC 3 | hStatus : null, // html hfc login button text 4 | init : () => { if ("NDEFReader" in window) { 5 | nin.hStatus = document.getElementById("nfc-b"); 6 | document.getElementById("nfc-a").disabled = false; 7 | }}, 8 | 9 | // (B) NFC LOGIN 10 | go : () => { 11 | // (B1) ON NFC READ 12 | nfc.onread = evt => { 13 | // (B1-1) GET TOKEN 14 | nfc.standby(); 15 | const decoder = new TextDecoder(); 16 | let token = ""; 17 | for (let record of evt.message.records) { 18 | token = decoder.decode(record.data); 19 | } 20 | 21 | // (B1-2) API LOGIN 22 | cb.api({ 23 | mod : "nfcin", act : "login", 24 | data : { token : token }, 25 | passmsg : false, 26 | onpass : () => location.href = cbhost.base, 27 | onfail : () => nin.go() 28 | }); 29 | }; 30 | 31 | // (B2) ON NFC ERROR 32 | nfc.onerror = err => { 33 | nfc.stop(); 34 | console.error(err); 35 | cb.modal("ERROR", err.msg); 36 | nin.hStatus.innerHTML = "ERROR!"; 37 | }; 38 | 39 | // (B3) START SCAN 40 | nin.hStatus.innerHTML = "Scanning - Tap Token"; 41 | nfc.scan(); 42 | } 43 | }; 44 | window.addEventListener("load", nin.init); -------------------------------------------------------------------------------- /assets/PAGE-login-wa.js: -------------------------------------------------------------------------------- 1 | var wa = { 2 | // (A) INIT 3 | init : () => { if ("credentials" in navigator) { 4 | document.getElementById("wa-in").disabled = false; 5 | }}, 6 | 7 | // (B) WEBAUTH LOGIN PART A 8 | go : () => { 9 | const email = document.getElementById("login-email"); 10 | if (email.validity.valid) { 11 | cb.api({ 12 | mod : "wain", act : "loginA", 13 | data : { email: email.value }, 14 | passmsg : false, 15 | onpass : async res => { 16 | let pk = JSON.parse(res.data); 17 | helper.bta(pk); 18 | console.log(pk); 19 | wa.login(await navigator.credentials.get(pk)); 20 | } 21 | }); 22 | } else { 23 | cb.modal("ERROR", "Please enter a valid email address.") 24 | } 25 | }, 26 | 27 | // (C) WEBAUTH LOGIN PART B 28 | login : cred => { 29 | const email = document.getElementById("login-email"); 30 | cb.api({ 31 | mod : "wain", act : "loginB", 32 | data : { 33 | email: email.value, 34 | id : cred.rawId ? helper.atb(cred.rawId) : null, 35 | client : cred.response.clientDataJSON ? helper.atb(cred.response.clientDataJSON) : null, 36 | auth : cred.response.authenticatorData ? helper.atb(cred.response.authenticatorData) : null, 37 | sig : cred.response.signature ? helper.atb(cred.response.signature) : null, 38 | user : cred.response.userHandle ? helper.atb(cred.response.userHandle) : null 39 | }, 40 | passmsg : false, 41 | onpass : res => location.href = cbhost.base 42 | }); 43 | } 44 | }; 45 | window.addEventListener("load", wa.init); -------------------------------------------------------------------------------- /assets/PAGE-login.js: -------------------------------------------------------------------------------- 1 | function login () { 2 | cb.api({ 3 | mod : "session", act : "login", 4 | data : { 5 | email : document.getElementById("login-email").value, 6 | password : document.getElementById("login-pass").value 7 | }, 8 | passmsg : false, 9 | onpass : () => location.href = cbhost.base 10 | }); 11 | return false; 12 | } -------------------------------------------------------------------------------- /assets/PAGE-myaccount.js: -------------------------------------------------------------------------------- 1 | function save () { 2 | // (A) GET DATA 3 | var data = { 4 | name : document.getElementById("user-name").value, 5 | cpass : document.getElementById("user-cpass").value, 6 | pass : document.getElementById("user-npass").value 7 | }; 8 | 9 | // (B) PASSWORD CHECK 10 | if (data.pass != document.getElementById("user-ncpass").value) { 11 | cb.modal("Please Check", "Passwords do not match."); 12 | return false; 13 | } 14 | 15 | // (C) PASSWORD STRENGTH 16 | if (!cb.checker(data.pass)) { 17 | cb.modal("Please Check", "Password must be at least 8 characters, alphanumeric."); 18 | return false; 19 | } 20 | // @T 21 | // (D) API CALL 22 | cb.api({ 23 | mod : "session", act : "update", 24 | data : data, 25 | passmsg : "Account Updated", 26 | onpass : () => { 27 | document.getElementById("user-cpass").value = ""; 28 | document.getElementById("user-npass").value = ""; 29 | document.getElementById("user-ncpass").value = ""; 30 | } 31 | }); 32 | return false; 33 | } -------------------------------------------------------------------------------- /assets/PAGE-nfc.js: -------------------------------------------------------------------------------- 1 | var nfc = { 2 | // (A) INITIALIZE WEB NFC 3 | ndef : null, ctrl : null, // ndef object 4 | onread : null, onwrite : null, onerror : null, // functions to run on read, write, error 5 | init : () => { 6 | nfc.stop(); 7 | nfc.ctrl = new AbortController(); 8 | nfc.ndef = new NDEFReader(); 9 | }, 10 | 11 | // (B) STOP - MISSION ABORT 12 | stop : () => { if (nfc.ndef!=null) { 13 | nfc.ctrl.abort(); 14 | nfc.ndef = null; 15 | nfc.ctrl = null; 16 | }}, 17 | 18 | // (C) STANDBY - SCAN & DO NOTHING 19 | standby : () => { 20 | nfc.init(); 21 | nfc.ndef.onreading = null; 22 | nfc.ndef.onreadingerror = null; 23 | nfc.ndef.scan({ signal: nfc.ctrl.signal }); 24 | }, 25 | 26 | // (D) SCAN NFC TAG 27 | scan : () => { 28 | nfc.init(); 29 | nfc.ndef.scan({ signal: nfc.ctrl.signal }) 30 | .then(() => { 31 | if (nfc.onread!=null) { nfc.ndef.onreading = nfc.onread; } 32 | if (nfc.onerror!=null) { nfc.ndef.onreadingerror = nfc.onerror; } 33 | }) 34 | .catch(err => { if (nfc.onerror!=null) { nfc.onerrorerr(err); } }); 35 | }, 36 | 37 | // (E) WRITE NFC TAG 38 | write : data => { 39 | nfc.init(); 40 | nfc.ndef.write(data, { signal: nfc.ctrl.signal }) 41 | .then(() => { if (nfc.onwrite!=null) { nfc.onwrite(); } }) 42 | .catch(err => { if (nfc.onerror!=null) { nfc.onerrorerr(err); } }); 43 | }, 44 | 45 | // (F) CREATE READ-ONLY NFC TAG 46 | readonly : () => { 47 | nfc.init(); 48 | nfc.ndef.makeReadOnly({ signal: nfc.ctrl.signal }) 49 | .then(() => { if (nfc.onwrite!=null) { nfc.onwrite(); } }) 50 | .catch(err => { if (nfc.onerror!=null) { nfc.onerrorerr(err); } }); 51 | } 52 | }; -------------------------------------------------------------------------------- /assets/PAGE-wa-helper.js: -------------------------------------------------------------------------------- 1 | var helper = { 2 | // (A) ARRAY BUFFER TO BASE 64 3 | atb : b => { 4 | let u = new Uint8Array(b), s = ""; 5 | for (let i=0; i { 11 | let pre = "=?BINARY?B?", suf = "?="; 12 | for (let k in o) { if (typeof o[k] == "string") { 13 | let s = o[k]; 14 | if (s.substring(0, pre.length)==pre && s.substring(s.length - suf.length)==suf) { 15 | let b = window.atob(s.substring(pre.length, s.length - suf.length)), 16 | u = new Uint8Array(b.length); 17 | for (let i=0; i { 7 | wa.hReg = document.getElementById("wa-reg"); 8 | wa.hUnreg = document.getElementById("wa-unreg"); 9 | wa.hTxt = document.getElementById("wa-txt"); 10 | 11 | if ("credentials" in navigator) { 12 | wa.hReg.disabled = false; 13 | } else { 14 | wa.hTxt.innerHTML = " Web Authentication not supported on your device."; 15 | wa.hTxt.classList.remove("d-none"); 16 | } 17 | }, 18 | 19 | // (B) REGISTER PART A 20 | regA : () => cb.api({ 21 | mod : "wain", act : "regA", 22 | passmsg : false, 23 | onpass : async res => { 24 | let pk = JSON.parse(res.data); 25 | helper.bta(pk); 26 | wa.regB(await navigator.credentials.create(pk)); 27 | } 28 | }), 29 | 30 | // (C) REGISTER PART B 31 | regB : cred => cb.api({ 32 | mod : "wain", act : "regB", 33 | data : { 34 | transport : cred.response.getTransports ? cred.response.getTransports() : null, 35 | client : cred.response.clientDataJSON ? helper.atb(cred.response.clientDataJSON) : null, 36 | attest : cred.response.attestationObject ? helper.atb(cred.response.attestationObject) : null 37 | }, 38 | passmsg : "Passwordless login registered", 39 | onpass : () => wa.hUnreg.disabled = false 40 | }), 41 | 42 | // (D) UNREGISTER 43 | unreg : () => cb.api({ 44 | mod : "wain", act : "unreg", 45 | passmsg : "Passwordless login unregistered", 46 | onpass : () => wa.hUnreg.disabled = true 47 | }) 48 | }; 49 | window.addEventListener("load", wa.init); -------------------------------------------------------------------------------- /assets/T-classes.js: -------------------------------------------------------------------------------- 1 | var classes = { 2 | // (A) SHOW ALL CLASSES 3 | pg : 1, // current page 4 | range : "", // search range 5 | date : "", // search date 6 | list : () => cb.load({ 7 | page : "T/class/list", 8 | target : "class-list", 9 | data : { 10 | page : classes.pg, 11 | range : classes.range, 12 | date : classes.date 13 | } 14 | }), 15 | 16 | // (B) GO TO PAGE 17 | // pg : int, page number 18 | goToPage : pg => { if (pg!=classes.pg) { 19 | classes.pg = pg; 20 | classes.list(); 21 | }}, 22 | 23 | // (C) SEARCH FORM TOGGLE 24 | stog : () => 25 | document.getElementById("search-date").disabled = 26 | document.getElementById("search-range").value=="" ? true : false , 27 | 28 | // (D) SEARCH FOR CLASS 29 | search : () => { 30 | classes.range = document.getElementById("search-range").value; 31 | classes.date = document.getElementById("search-date").value; 32 | classes.pg = 1; 33 | classes.list(); 34 | return false; 35 | } 36 | }; 37 | window.addEventListener("load", () => classes.list()); -------------------------------------------------------------------------------- /assets/TA-attend.js: -------------------------------------------------------------------------------- 1 | var attend = { 2 | // (A) SHOW CLASS ATTENDANCE PAGE 3 | id : null, // current class id 4 | show : id => { 5 | attend.id = id; 6 | cb.page(2); 7 | cb.load({ 8 | page : "TA/attend", 9 | target : "cb-page-2", 10 | data : { id : id }, 11 | onload : () => attend.list() 12 | }); 13 | }, 14 | 15 | // (B) SHOW CLASS ATTENDANCE 16 | list : () => cb.load({ 17 | page : "TA/attend/list", 18 | target : "attend-list", 19 | data : { id : attend.id } 20 | }), 21 | 22 | // (C) TOGGLE ATTENDANCE STATUS 23 | toggle : uid => { 24 | // (C1) GET BUTTON 25 | let btn = document.getElementById("att-s"+uid); 26 | 27 | // (C2) TOGGLE STATUS 28 | if (btn.classList.contains("btn-primary")) { 29 | btn.classList.remove("btn-primary"); 30 | btn.classList.add("btn-danger"); 31 | btn.classList.remove("icon-checkmark"); 32 | btn.classList.add("icon-cross"); 33 | } else { 34 | btn.classList.remove("btn-danger"); 35 | btn.classList.add("btn-primary"); 36 | btn.classList.remove("icon-cross"); 37 | btn.classList.add("icon-checkmark"); 38 | } 39 | }, 40 | 41 | // (D) SAVE ATTENDANCE 42 | save : () => { 43 | // (D1) GET ALL ATTENDANCE RECORDS 44 | let all = document.querySelectorAll(".att-i"), att = {}; 45 | for (let a of all) { 46 | let id = a.value; 47 | att[id] = { 48 | s : document.getElementById("att-s" + id).classList.contains("btn-primary") ? "1" : "0", 49 | n : document.getElementById("att-n" + id).value 50 | }; 51 | } 52 | 53 | // (D2) SEND! 54 | cb.api({ 55 | mod : "attend", act : "save", 56 | data : { 57 | id : attend.id, 58 | att : JSON.stringify(att) 59 | }, 60 | passmsg : "Attendance Updated" 61 | }); 62 | } 63 | }; -------------------------------------------------------------------------------- /assets/U-classes.js: -------------------------------------------------------------------------------- 1 | var classes = { 2 | // (A) SHOW ALL CLASSES 3 | pg : 1, // current page 4 | range : "", // search range 5 | date : "", // search date 6 | list : () => cb.load({ 7 | page : "U/class/list", 8 | target : "class-list", 9 | data : { 10 | page : classes.pg, 11 | range : classes.range, 12 | date : classes.date 13 | } 14 | }), 15 | 16 | // (B) GO TO PAGE 17 | // pg : int, page number 18 | goToPage : pg => { if (pg!=classes.pg) { 19 | classes.pg = pg; 20 | classes.list(); 21 | }}, 22 | 23 | // (C) SEARCH FORM TOGGLE 24 | stog : () => 25 | document.getElementById("search-date").disabled = 26 | document.getElementById("search-range").value=="" ? true : false , 27 | 28 | // (D) SEARCH FOR CLASS 29 | search : () => { 30 | classes.range = document.getElementById("search-range").value; 31 | classes.date = document.getElementById("search-date").value; 32 | classes.pg = 1; 33 | classes.list(); 34 | return false; 35 | } 36 | }; 37 | window.addEventListener("load", classes.list); -------------------------------------------------------------------------------- /assets/U-qr.js: -------------------------------------------------------------------------------- 1 | var check = { 2 | // (A) INIT 3 | scanner : null, // qr scanner 4 | init : () => { 5 | // (A1) START QR SCANNER 6 | check.scanner = new Html5QrcodeScanner("reader", { fps: 10, qrbox: 250 }); 7 | check.scanner.render((data, res) => { 8 | // (A2) STOP ON SCAN 9 | let buttons = document.querySelectorAll("#reader button"); 10 | buttons[1].click(); 11 | 12 | // (A3) GET DATA 13 | try { data = JSON.parse(data); } 14 | catch (err) { 15 | cb.modal("Error", err.message); 16 | console.log(err); 17 | } 18 | 19 | // (A4) API CALL 20 | cb.api({ 21 | mod : "attend", act : "attendQR", 22 | data : { 23 | id : data.i, 24 | hash : data.h 25 | }, 26 | passmsg : false, 27 | onpass : () => cb.modal("OK", "Your attendance has been taken.") 28 | }); 29 | }); 30 | } 31 | }; 32 | window.addEventListener("load", check.init); -------------------------------------------------------------------------------- /assets/csv.min.js: -------------------------------------------------------------------------------- 1 | var CSV={};!function(p){"use strict";p.__type__="csv";var o="undefined"!=typeof jQuery&&jQuery.Deferred||"undefined"!=typeof _&&_.Deferred||function(){var t,n,e=new Promise(function(e,r){t=e,n=r});return{resolve:t,reject:n,promise:function(){return e}}};p.fetch=function(t){var n=new o;if(t.file){var e=new FileReader,r=t.encoding||"UTF-8";e.onload=function(e){var r=p.extractFields(p.parse(e.target.result,t),t);r.useMemoryStore=!0,r.metadata={filename:t.file.name},n.resolve(r)},e.onerror=function(e){n.reject({error:{message:"Failed to load file. Code: "+e.target.error.code}})},e.readAsText(t.file,r)}else if(t.data){var i=p.extractFields(p.parse(t.data,t),t);i.useMemoryStore=!0,n.resolve(i)}else if(t.url){(window.fetch||function(e){var r=jQuery.get(e),t={then:function(e){return r.done(e),t},catch:function(e){return r.fail(e),t}};return t})(t.url).then(function(e){return e.text?e.text():e}).then(function(e){var r=p.extractFields(p.parse(e,t),t);r.useMemoryStore=!0,n.resolve(r)}).catch(function(e,r){n.reject({error:{message:"Failed to load file. "+e.statusText+". Code: "+e.status,request:e}})})}return n.promise()},p.extractFields=function(e,r){return!0!==r.noHeaderRow&&0autoAPI([ 4 | "save" => ["Attend", "save", ["A", "T"]], 5 | "attendQR" => ["Attend", "attendQR", "U"] 6 | ]); 7 | 8 | // (B) INVALID REQUEST 9 | $_CORE->respond(0, "Invalid request", null, null, 400); -------------------------------------------------------------------------------- /lib/API-autocomplete.php: -------------------------------------------------------------------------------- 1 | ucheck("A"); 4 | 5 | // (B) API ENDPOINTS 6 | $_CORE->autoAPI([ 7 | "user" => ["Autocomplete", "user"], 8 | "userEmail" => ["Autocomplete", "userEmail"], 9 | "course" => ["Autocomplete", "course"], 10 | "icourse" => ["Autocomplete", "icourse"] 11 | ]); 12 | 13 | // (C) INVALID REQUEST 14 | $_CORE->respond(0, "Invalid request", null, null, 400); -------------------------------------------------------------------------------- /lib/API-classes.php: -------------------------------------------------------------------------------- 1 | ucheck("A"); 4 | 5 | // (B) API ENDPOINTS 6 | $_CORE->autoAPI([ 7 | "save" => ["Classes", "save"], 8 | "del" => ["Classes", "del"], 9 | "import" => ["Classes", "import"] 10 | ]); 11 | 12 | // (C) INVALID REQUEST 13 | $_CORE->respond(0, "Invalid request", null, null, 400); -------------------------------------------------------------------------------- /lib/API-courses.php: -------------------------------------------------------------------------------- 1 | ucheck("A"); 4 | 5 | // (B) API ENDPOINTS 6 | $_CORE->autoAPI([ 7 | "save" => ["Courses", "save"], 8 | "import" => ["Courses", "import"], 9 | "del" => ["Courses", "del"], 10 | "addUser" => ["Courses", "addUser"], 11 | "delUser" => ["Courses", "delUser"] 12 | ]); 13 | 14 | // (C) INVALID REQUEST 15 | $_CORE->respond(0, "Invalid request", null, null, 400); -------------------------------------------------------------------------------- /lib/API-nfcin.php: -------------------------------------------------------------------------------- 1 | autoAPI([ 4 | "add" => ["NFCIN", "add", "A"], 5 | "del" => ["NFCIN", "del", "A"], 6 | "login" => ["NFCIN", "login"] 7 | ]); 8 | 9 | // (B) INVALID REQUEST 10 | $_CORE->respond(0, "Invalid request", null, null, 400); -------------------------------------------------------------------------------- /lib/API-session.php: -------------------------------------------------------------------------------- 1 | autoAPI([ 4 | "login" => ["Users", "login"], 5 | "logout" => ["Users", "logout"], 6 | "update" => ["Users", "update"], 7 | "forgotA" => ["Forgot", "request"], 8 | "forgotB" => ["Forgot", "reset"] 9 | ]); 10 | 11 | // (B) INVALID REQUEST 12 | $_CORE->respond(0, "Invalid request", null, null, 400); -------------------------------------------------------------------------------- /lib/API-settings.php: -------------------------------------------------------------------------------- 1 | ucheck("A"); 4 | 5 | // (B) API ENDPOINTS 6 | $_CORE->autoAPI([ 7 | "save" => ["Settings", "save"] 8 | ]); 9 | 10 | // (C) INVALID REQUEST 11 | $_CORE->respond(0, "Invalid request", null, null, 400); -------------------------------------------------------------------------------- /lib/API-users.php: -------------------------------------------------------------------------------- 1 | ucheck("A"); 4 | 5 | // (B) API ENDPOINTS 6 | $_CORE->autoAPI([ 7 | "get" => ["Users", "get"], 8 | "getAll" => ["Users", "getAll"], 9 | "save" => ["Users", "save"], 10 | "suspend" => ["Users", "suspend"], 11 | "import" => ["Users", "import"] 12 | ]); 13 | 14 | // (C) INVALID REQUEST 15 | $_CORE->respond(0, "Invalid request", null, null, 400); -------------------------------------------------------------------------------- /lib/API-wain.php: -------------------------------------------------------------------------------- 1 | autoAPI([ 4 | "regA" => ["WAIN", "regA", true], 5 | "regB" => ["WAIN", "regB", true], 6 | "unreg" => ["WAIN", "unreg", true], 7 | "loginA" => ["WAIN", "loginA"], 8 | "loginB" => ["WAIN", "loginB"] 9 | ]); 10 | 11 | // (B) INVALID REQUEST 12 | $_CORE->respond(0, "Invalid request", null, null, 400); -------------------------------------------------------------------------------- /lib/CORE-Config.php: -------------------------------------------------------------------------------- 1 | "Admin", "T" => "Teacher", "U" => "Student", "S" => "Suspended" 61 | ]); -------------------------------------------------------------------------------- /lib/CORE-Go.php: -------------------------------------------------------------------------------- 1 | load("DB"); 6 | $_CORE->load("Settings"); 7 | $_CORE->load("Session"); 8 | 9 | // (B) LOAD MODULES AS REQUIRED -------------------------------------------------------------------------------- /lib/CORE-Install-JS.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/CORE-Install.php: -------------------------------------------------------------------------------- 1 | load("Install"); 12 | $_PHASE = isset($_POST["phase"]) ? $_POST["phase"] : "B"; 13 | while ($_PHASE != null) { $_PHASE = $_CORE->Install->$_PHASE(); } -------------------------------------------------------------------------------- /lib/HOOK-API-CORS.php: -------------------------------------------------------------------------------- 1 | ROUTES->API() 3 | // USE THIS TO OVERRIDE API CORS PERMISSION 4 | // $this->origin : client origin, e.g. http://site.com 5 | // $this->orihost : client origin host, e.g. site.com 6 | // $this->mod : requested module. e.g. users 7 | // $this->act : requested action. e.g. save 8 | 9 | /* 10 | // (A) EXAMPLE - ALLOW "FOO.COM" TO ACCESS "TEST" MODULE 11 | if ($this->orihost=="foo.com" && $this->mod=="test") { 12 | $access = true; 13 | } 14 | 15 | // (B) EXAMPLE - ALLOW "BAR.COM" TO ACCESS SOME ACTIONS IN "TEST" MODULE 16 | $allowed = ["get", "getAll"]; 17 | if ($this->orihost=="bar.com" && $this->mod=="test" && in_array($this->act, $allowed)) { 18 | $access = true; 19 | } 20 | */ -------------------------------------------------------------------------------- /lib/HOOK-Routes.php: -------------------------------------------------------------------------------- 1 | ROUTES->RESOLVE() 3 | // USE THIS TO OVERRIDE URL PAGE ROUTES 4 | 5 | // (A) EXACT PATH ROUTING 6 | $routes = [ 7 | // EXAMPLES 8 | // "/" => "myhome.php", // http://site.com/ > pages/myhome.php 9 | // "foo/" => "bar.php", // http://site.com/foo/ > pages/bar.php 10 | ]; 11 | 12 | // (B) WILDCARD PATH ROUTING 13 | $wild = [ 14 | "T/" => "USR-check.php", 15 | "A/" => "USR-check.php", 16 | "TA/" => "USR-check.php", 17 | "U/" => "USR-check.php", 18 | "report/" => "REPORT-loader.php" 19 | ]; 20 | 21 | // (C) MANUAL PATH OVERRIDE - LOGIN CHECK 22 | $override = function ($path) { 23 | if (!isset($_SESSION["user"]) && $path!="login/" && $path!="forgot/") { 24 | if (isset($_POST["ajax"])) { exit("E"); } 25 | else { header("Location: ".HOST_BASE."login"); exit(); } 26 | } 27 | return $path; 28 | }; -------------------------------------------------------------------------------- /lib/HOOK-SESS-Load.php: -------------------------------------------------------------------------------- 1 | SESSION->__CONSTRUCT() 3 | // USE THIS TO BUILD/OVERRIDE SESSION DATA WHEN UNPACKING THE JWT 4 | 5 | /* 6 | // EXAMPLE - LOAD USER CUSTOM SETTINGS 7 | if (isset($_SESSION["user"])) { 8 | $_SESSION["settings"] = $this->DB->fetchAll( 9 | "SELECT * FROM `user_settings` WHERE `user_id`=?", 10 | [$_SESSION["user"]["user_id"]] 11 | ); 12 | } 13 | 14 | // EXAMPLE - CHECK IF COUPON STILL VALID 15 | if (isset($_SESSION["coupon"])) { 16 | $coupon = $this->DB->fetchAll( 17 | "SELECT * FROM `coupons` WHERE `coupon_id`=?", 18 | [$_SESSION["coupon"]] 19 | ); 20 | if ($coupon["expire"] >= strtotime("now")) { 21 | unset($_SESSION["coupon"]); 22 | } 23 | } 24 | */ 25 | 26 | // ADDED BY INSTALLER - LOAD USER INFO INTO SESSION 27 | if (isset($_SESSION["user"])) { 28 | $user = $this->DB->fetch( 29 | "SELECT * FROM `users` WHERE `user_id`=?", 30 | [$_SESSION["user"]["user_id"]] 31 | ); 32 | if (!is_array($user) || (isset($user["user_level"]) && $user["user_level"]=="S")) { 33 | $this->destroy(); 34 | throw new Exception("Invalid or expired session."); 35 | } else { 36 | unset($user["user_password"]); 37 | $_SESSION["user"] = $user; 38 | } 39 | } -------------------------------------------------------------------------------- /lib/HOOK-SESS-Save.php: -------------------------------------------------------------------------------- 1 | SESSION->SAVE() 3 | // USE THIS TO OVERRIDE DATA TO BE SAVED INTO THE JWT 4 | 5 | // EXAMPLE - REMOVE USER SETTINGS 6 | // if (isset($data["settings"])) { unset($data["settings"]); } 7 | 8 | // ADDED BY INSTALLER - ONLY SAVE USER ID INTO JWT 9 | if (isset($data["user"])) { 10 | $data["user"] = ["user_id" => $data["user"]["user_id"]]; 11 | } -------------------------------------------------------------------------------- /lib/LIB-Attend.php: -------------------------------------------------------------------------------- 1 | [s : status, n : notes] 7 | function save ($id, $att) { 8 | $att = json_decode($att, true); 9 | foreach ($att as $uid=>$a) { 10 | $this->DB->replace("attendance", 11 | ["class_id", "user_id", "a_status", "a_by", "a_date", "a_notes"], 12 | [ 13 | $id, $uid, $a["s"], 14 | isset($_SESSION["user"]["user_id"]) ? $_SESSION["user"]["user_id"] : 0, 15 | date("Y-m-d H:i:s"), $a["n"]=="" ? null : $a["n"] 16 | ] 17 | ); 18 | } 19 | return true; 20 | } 21 | 22 | // (B) ATTENDANCE VIA QR 23 | // $id : class id 24 | // $hash : class hash 25 | function attendQR ($id, $hash) { 26 | // (B1) GET CLASS 27 | $this->Core->load("Classes"); 28 | $class = $this->Classes->get($id); 29 | if (!is_array($class)) { 30 | $this->error = "Invalid class."; 31 | return false; 32 | } 33 | 34 | // (B2) VERIFY 35 | if ($class["class_hash"]!=$hash) { 36 | $this->error = "Invalid class."; 37 | return false; 38 | } 39 | 40 | // (B3) SAVE ATTENDANCE 41 | $this->DB->replace("attendance", 42 | ["class_id", "user_id", "a_status", "a_by", "a_date", "a_notes"], 43 | [$id, $_SESSION["user"]["user_id"], 1, $_SESSION["user"]["user_id"], date("Y-m-d H:i:s"), "QR"] 44 | ); 45 | return true; 46 | } 47 | 48 | // (C) GET STUDENT & ATTENDANCE FOR CLASS 49 | // $id : class id 50 | function getStudents ($id) { 51 | // (C1) GET COURSE CODE 52 | $code = $this->DB->fetchCol( 53 | "SELECT `course_code` FROM `classes` WHERE `class_id`=?", [$id] 54 | ); 55 | 56 | // (C2) GET STUDENTS + ATTENDANCE FOR THE CLASS 57 | $this->DB->query( 58 | "SELECT u.`user_id`, u.`user_name`, u.`user_email`, a.`a_status`, a.`a_notes` 59 | FROM `courses_users` cu 60 | LEFT JOIN `users` u ON (cu.`user_id`=u.`user_id`) 61 | LEFT JOIN `attendance` a ON (cu.`user_id`=a.`user_id` AND a.class_id=?) 62 | WHERE cu.`course_code`=? AND u.`user_level`='U' 63 | ORDER BY `user_name`", 64 | [$id, $code] 65 | ); 66 | $students = []; 67 | while ($r = $this->DB->stmt->fetch()) { 68 | $students[$r["user_id"]] = $r; 69 | } 70 | 71 | // (C3) RESULTS 72 | return $students; 73 | } 74 | } -------------------------------------------------------------------------------- /lib/LIB-Autocomplete.php: -------------------------------------------------------------------------------- 1 | DB->query($sql . " LIMIT " . SUGGEST_LIMIT, $data); 10 | $res = []; 11 | if ($v==null) { 12 | while ($r = $this->DB->stmt->fetch()) { 13 | $res[] = ["n" => $r[$n]]; 14 | } 15 | } else { 16 | while ($r = $this->DB->stmt->fetch()) { 17 | $res[] = ["n" => $r[$n], "v" => $r[$v]]; 18 | } 19 | } 20 | return $res; 21 | } 22 | 23 | // (B) SUGGEST USER 24 | function user ($search) { 25 | return $this->query( 26 | "SELECT * FROM `users` WHERE `user_name` LIKE ?", 27 | ["%$search%"], "user_name" 28 | ); 29 | } 30 | 31 | // (C) SUGGEST USER EMAIL 32 | function userEmail ($search) { 33 | return $this->query( 34 | "SELECT * FROM `users` WHERE `user_name` LIKE ? OR `user_email` LIKE ?", 35 | ["%$search%", "%$search%"], "user_name", "user_email" 36 | ); 37 | } 38 | 39 | // (D) SUGGEST COURSE 40 | function course ($search) { 41 | return $this->query( 42 | "SELECT * FROM `courses` WHERE `course_code` LIKE ? OR `course_name` LIKE ?", 43 | ["%$search%", "%$search%"], "course_name", "course_code" 44 | ); 45 | } 46 | 47 | // (E) "SPECIAL ENDPOINT" USED BY ADD/EDIT CLASS 48 | function icourse ($code) { 49 | $this->Core->load("Courses"); 50 | return [ 51 | "c" => $this->Courses->get($code), 52 | "t" => $this->Courses->getTeachers($code) 53 | ]; 54 | } 55 | } -------------------------------------------------------------------------------- /lib/LIB-Courses.php: -------------------------------------------------------------------------------- 1 | error = "End date cannot be earlier than start"; 14 | return false; 15 | } 16 | $fields = ["course_code", "course_name", "course_start", "course_end", "course_desc"]; 17 | $data = [$code, $name, $start, $end, $desc]; 18 | 19 | // (A2) ADD/UPDATE COURSE 20 | if ($ocode==null) { 21 | $this->DB->insert("courses", $fields, $data); 22 | } else { 23 | $data[] = $ocode; 24 | $this->DB->update("courses", $fields, "`course_code`=?", $data); 25 | } 26 | return true; 27 | } 28 | 29 | // (B) IMPORT COURSE (OVERRIDES OLD ENTRY) 30 | // $code : course code 31 | // $name : course name 32 | // $start : start date 33 | // $end : end date 34 | // $desc : course description 35 | function import ($code, $name, $start, $end, $desc=null) { 36 | // (B1) GET COURSE 37 | $course = $this->get($code); 38 | 39 | // (B2) UPDATE OR INSERT 40 | $this->save($code, $name, $start, $end, $desc, is_array($course)?$course["course_code"]:null); 41 | return true; 42 | } 43 | 44 | // (C) DELETE COURSE 45 | // $code : course code 46 | function del ($code) { 47 | $this->DB->start(); 48 | $this->DB->query("DELETE `attendance` FROM `attendance` LEFT JOIN `classes` USING (`class_id`) WHERE `course_code`=?", [$code]); 49 | $this->DB->delete("classes", "`course_code`=?", [$code]); 50 | $this->DB->delete("courses_users", "`course_code`=?", [$code]); 51 | $this->DB->delete("courses", "`course_code`=?", [$code]); 52 | $this->DB->end(); 53 | return true; 54 | } 55 | 56 | // (D) GET COURSE 57 | // $code : course code 58 | function get ($code) { 59 | return $this->DB->fetch( 60 | "SELECT * FROM `courses` WHERE `course_code`=?", 61 | [$code] 62 | ); 63 | } 64 | 65 | // (E) GET ALL OR SEARCH COURSES 66 | // $search : optional, course code or name 67 | // $page : optional, current page number 68 | function getAll ($search=null, $page=null) { 69 | // (E1) PARITAL SQL + DATA 70 | $sql = "FROM `courses`"; 71 | $data = null; 72 | if ($search != null) { 73 | $sql .= " WHERE `course_code` LIKE ? OR `course_name` LIKE ?"; 74 | $data = ["%$search%", "%$search%"]; 75 | } 76 | 77 | // (E2) PAGINATION 78 | if ($page != null) { 79 | $this->Core->paginator( 80 | $this->DB->fetchCol("SELECT COUNT(*) $sql", $data), $page 81 | ); 82 | $sql .= $this->Core->page["lim"]; 83 | } 84 | 85 | // (E3) RESULTS 86 | return $this->DB->fetchAll( 87 | "SELECT *, DATE_FORMAT(`course_start`, '".D_SHORT."') `sd`, DATE_FORMAT(`course_end`, '".D_SHORT."') `ed` $sql", 88 | $data, "course_code" 89 | ); 90 | } 91 | 92 | // (F) ADD USER TO COURSE 93 | // $code : course code 94 | // $uid : user id or email 95 | function addUser ($code, $uid) { 96 | // (F1) VERIFY VALID USER 97 | $this->Core->load("Users"); 98 | $user = $this->Users->get($uid); 99 | if (!is_array($user) || $user["user_level"]=="S") { 100 | $this->error = "Invalid user"; 101 | return false; 102 | } 103 | 104 | // (F2) ADD TO COURSE 105 | $this->DB->replace("courses_users", ["course_code", "user_id"], [$code, $user["user_id"]]); 106 | return true; 107 | } 108 | 109 | // (G) DELETE USER FROM COURSE 110 | // $code : course code 111 | // $uid : user id or email 112 | function delUser ($code, $uid) { 113 | $this->DB->delete("courses_users", "`course_code`=? AND `user_id`=?", [$code, $uid]); 114 | return true; 115 | } 116 | 117 | // (H) GET ALL USERS IN COURSE 118 | // $code : course code 119 | // $page : optional, current page number 120 | function getUsers ($code, $page=null) { 121 | // (H1) PARITAL SQL + DATA 122 | $sql = "FROM `courses_users` cu 123 | JOIN `users` u USING (`user_id`) 124 | WHERE cu.`course_code`=? AND u.`user_level`!='S'"; 125 | $data = [$code]; 126 | 127 | // (H2) PAGINATION 128 | if ($page != null) { 129 | $this->Core->paginator( 130 | $this->DB->fetchCol("SELECT COUNT(*) $sql", $data), $page 131 | ); 132 | } 133 | 134 | // (H3) "MAIN SQL" 135 | $sql .= " ORDER BY FIELD(`user_level`, 'A','T','U'), `user_name`"; 136 | if ($page != null) { $sql .= $this->Core->page["lim"]; } 137 | 138 | // (H4) RESULTS 139 | return $this->DB->fetchAll("SELECT * $sql", $data, "user_id"); 140 | } 141 | 142 | // (I) GET TEACHERS IN COURSE 143 | // $code : course code 144 | function getTeachers ($code) { 145 | return $this->DB->fetchAll( 146 | "SELECT u.`user_id`, u.`user_name`, u.`user_email` 147 | FROM `courses_users` c 148 | JOIN `users` u USING (`user_id`) 149 | WHERE u.`user_level` IN ('A', 'T') 150 | AND c.`course_code`=? 151 | ORDER BY `user_name` ASC", 152 | [$code], "user_id" 153 | ); 154 | } 155 | } -------------------------------------------------------------------------------- /lib/LIB-DB.php: -------------------------------------------------------------------------------- 1 | pdo = new PDO( 13 | "mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=".DB_CHARSET, 14 | DB_USER, DB_PASSWORD, [ 15 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 16 | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC 17 | ]); 18 | $this->query("SET time_zone='".SYS_TZ_OFFSET."'"); 19 | } 20 | 21 | // (C) DESTRUCTOR - CLOSE DATABASE CONNECTION 22 | function __destruct () { 23 | if ($this->stmt!==null) { $this->stmt = null; } 24 | if ($this->pdo!==null) { $this->pdo = null; } 25 | } 26 | 27 | // (D) AUTO-COMMIT OFF 28 | function start () : void { $this->pdo->beginTransaction(); } 29 | 30 | // (E) COMMIT OR ROLLBACK? 31 | // $pass : commit or rollback? 32 | function end ($pass=true) : void { 33 | if ($pass) { $this->pdo->commit(); } 34 | else { $this->pdo->rollBack(); } 35 | } 36 | 37 | // (F) EXECUTE SQL QUERY 38 | // $sql : sql query 39 | // $data : array of parameters for query 40 | function query ($sql, $data=null) : void { 41 | $this->stmt = $this->pdo->prepare($sql); 42 | $this->stmt->execute($data); 43 | } 44 | 45 | // (G) FETCH ALL (MULTIPLE ROWS) 46 | // $sql : sql query 47 | // $data : array, parameters for sql query 48 | // $key : optional, arrange results by this key 49 | // null : pdo::fetch_assoc 50 | // string : use this field as the array key 51 | // true : flat array, make sure select query only has 1 column! 52 | // * returns null if no results 53 | function fetchAll ($sql, $data=null, $key=null) { 54 | $this->query($sql, $data); 55 | if ($key === null) { $results = $this->stmt->fetchAll(); } 56 | else if ($key === true) { $results = $this->stmt->fetchAll(PDO::FETCH_COLUMN); } 57 | else { 58 | $results = []; 59 | while ($row = $this->stmt->fetch()) { $results[$row[$key]] = $row; } 60 | } 61 | return count($results)>0 ? $results : null ; 62 | } 63 | 64 | // (H) FETCH ALL (KEY => VALUE) 65 | // $sql : sql query 66 | // $data : array, parameters for sql query 67 | // $key : use this field as the array key 68 | // $value : use this field as the value 69 | // * returns null if no results 70 | function fetchKV ($sql, $data, $key, $value) { 71 | $this->query($sql, $data); 72 | $results = []; 73 | while ($row = $this->stmt->fetch()) { 74 | $results[$row[$key]] = $row[$value]; 75 | } 76 | return count($results)>0 ? $results : null ; 77 | } 78 | 79 | // (I) FETCH (SINGLE ROW) 80 | // $sql : sql query 81 | // $data : array, parameters for sql query 82 | // * returns null if no results 83 | function fetch ($sql, $data=null) { 84 | $this->query($sql, $data); 85 | $result = $this->stmt->fetch(); 86 | return $result===false ? null : $result ; 87 | } 88 | 89 | // (J) FETCH (SINGLE COLUMN) 90 | // $sql : sql query 91 | // $data : array, parameters for sql query 92 | // * returns null if no results 93 | function fetchCol ($sql, $data=null) { 94 | $this->query($sql, $data); 95 | $result = $this->stmt->fetchColumn(); 96 | return $result===false ? null : $result ; 97 | } 98 | 99 | // (K) INSERT OR REPLACE SQL HELPER 100 | // $table : table to insert into 101 | // $fields : array of fields to insert 102 | // $data : data array to insert 103 | // $replace : replace instead of insert? 104 | function insert ($table, $fields, $data, $replace=false) : void { 105 | // (K1) QUICK CHECK 106 | $cfields = count($fields); 107 | $cdata = count($data); 108 | $segments = $cdata / $cfields; 109 | if (is_float($segments)) { 110 | throw new Exception("Number of data elements do not match with number of fields"); 111 | } 112 | 113 | // (K2) FORM SQL 114 | $sql = $replace ? "REPLACE" : "INSERT" ; 115 | $sql .= " INTO `$table` ("; 116 | foreach ($fields as $f) { $sql .= "`$f`,"; } 117 | $sql = substr($sql, 0, -1).") VALUES "; 118 | $sql .= str_repeat("(". substr(str_repeat("?,", $cfields), 0, -1) ."),", $segments); 119 | $sql = substr($sql, 0, -1).";"; 120 | 121 | // (K3) RUN QUERY 122 | $this->query($sql, $data); 123 | if (!$replace) { 124 | $this->lastID = $this->pdo->lastInsertId(); 125 | $this->lastRows = $this->stmt->rowCount(); 126 | } 127 | } 128 | 129 | // (L) REPLACE - INSERT(), BUT WITH $REPLACE=TRUE 130 | function replace ($table, $fields, $data) : void { $this->insert($table, $fields, $data, true); } 131 | 132 | // (M) UPDATE SQL HELPER 133 | // $table : table to update 134 | // $fields : array of fields to update 135 | // $where : where clause for update SQL 136 | // $data : data array to update 137 | function update ($table, $fields, $where, $data) : void { 138 | $sql = "UPDATE `$table` SET "; 139 | foreach ($fields as $f) { $sql .= "`$f`=?,"; } 140 | $sql = substr($sql, 0, -1) . " WHERE $where"; 141 | $this->query($sql, $data); 142 | $this->lastRows = $this->stmt->rowCount(); 143 | } 144 | 145 | // (N) DELETE SQL HELPER 146 | // $table : table to update 147 | // $where : where clause for delete SQL 148 | // $data : data array 149 | function delete ($table, $where, $data=null) : void { 150 | $sql = "DELETE FROM `$table` WHERE $where"; 151 | $this->query($sql, $data); 152 | $this->lastRows = $this->stmt->rowCount(); 153 | } 154 | } -------------------------------------------------------------------------------- /lib/LIB-Forgot.php: -------------------------------------------------------------------------------- 1 | error = "You are already signed in."; 13 | return false; 14 | } 15 | 16 | // (B2) CHECK IF VALID USER 17 | $this->Core->load("Users"); 18 | $user = $this->Users->get($email, "A"); 19 | if (!is_array($user)) { 20 | $this->error = "$email is not registered."; 21 | return false; 22 | } 23 | if (isset($user["hash_code"])) { 24 | $this->error = "$email is not an active account."; 25 | return false; 26 | } 27 | if ($user["user_level"] == "S") { 28 | $this->error = "$email is not an active account."; 29 | return false; 30 | } 31 | 32 | // (B3) CHECK PREVIOUS REQUEST (PREVENT SPAM) 33 | $req = $this->Users->hashGet($user["user_id"], "P"); 34 | if (is_array($req)) { 35 | $expire = strtotime($req["hash_time"]) + $this->valid; 36 | $now = strtotime("now"); 37 | $left = $now - $expire; 38 | if ($left <0) { 39 | $this->error = "Please wait another ".abs($left)." seconds."; 40 | return false; 41 | } 42 | } 43 | 44 | // (B4) CHECKS OK - CREATE NEW RESET REQUEST 45 | $now = strtotime("now"); 46 | $hash = $this->Core->random($this->hlen); 47 | $this->Users->hashAdd($user["user_id"], "P", $hash); 48 | 49 | // (B5) SEND EMAIL TO USER 50 | $this->Core->load("Mail"); 51 | return $this->Mail->send([ 52 | "to" => $user["user_email"], 53 | "subject" => "Password Reset", 54 | "template" => PATH_PAGES . "MAIL-forgot-a.php", 55 | "vars" => [ 56 | "link" => HOST_BASE."forgot?i={$user["user_id"]}&h={$hash}" 57 | ] 58 | ]); 59 | } 60 | 61 | // (C) PROCESS PASSWORD RESET 62 | function reset ($id, $hash) { 63 | // (C1) ALREADY SIGNED IN 64 | if (isset($_SESSION["user"])) { 65 | $this->error = "You are already signed in."; 66 | return false; 67 | } 68 | 69 | // (C2) CHECK REQUEST 70 | $this->Core->load("Users"); 71 | $req = $this->Users->hashGet($id, "P"); 72 | $pass = is_array($req); 73 | 74 | // (C3) CHECK EXPIRE 75 | if ($pass) { 76 | $expire = strtotime($req["hash_time"]) + $this->valid; 77 | $now = strtotime("now"); 78 | $pass = $now <= $expire; 79 | } 80 | 81 | // (C4) CHECK HASH 82 | if ($pass) { $pass = $hash==$req["hash_code"]; } 83 | 84 | // (C5) GET USER 85 | if ($pass) { 86 | $user = $this->Users->get($id); 87 | $pass = is_array($user); 88 | } 89 | 90 | // (C6) CHECK FAIL - INVALID REQUEST 91 | if (!$pass) { 92 | $this->error = "Invalid request."; 93 | return false; 94 | } 95 | 96 | // (C7) CHECK PASS - PROCEED RESET 97 | // (C7-1) UPDATE USER PASSWORD 98 | $this->DB->start(); 99 | $password = $this->Core->random($this->plen); 100 | $this->DB->update( 101 | "users", ["user_password"], "`user_id`=?", 102 | [password_hash($password, PASSWORD_DEFAULT), $id] 103 | ); 104 | 105 | // (C7-2) REMOVE REQUEST 106 | $this->Users->hashDel($id, "P"); 107 | 108 | // (C7-3) EMAIL TO USER 109 | $this->Core->load("Mail"); 110 | $pass = $this->Mail->send([ 111 | "to" => $user["user_email"], 112 | "subject" => "Password Reset", 113 | "template" => PATH_PAGES . "MAIL-forgot-b.php", 114 | "vars" => [ 115 | "password" => $password 116 | ] 117 | ]); 118 | 119 | // (C8) CLOSE 120 | $this->DB->end($pass); 121 | return true; 122 | } 123 | } -------------------------------------------------------------------------------- /lib/LIB-MInstall.php: -------------------------------------------------------------------------------- 1 | DB->query(file_get_contents($file)); 9 | } catch (Exception $ex) { 10 | exit("Unable to import $file - " . $ex->getMessage()); 11 | } 12 | } 13 | 14 | // (B) BACKUP FILE 15 | function backup ($file) { 16 | if (!file_exists($file)) { exit("$file not found!"); } 17 | $ext = pathinfo($file, PATHINFO_EXTENSION); 18 | $bak = $ext == "htaccess" ? "$file.old" : str_replace(".$ext", ".old", $file) ; 19 | if (!copy($file, $bak)) { exit("Failed to backup $file"); } 20 | } 21 | 22 | // (C) APPEND TO FILE 23 | function append ($file, $add) { 24 | $this->backup($file); 25 | $fh = fopen($file, "a") or exit("Cannot open $file"); 26 | if (fwrite($fh, $add) === false) { 27 | fclose($fh); 28 | exit("Failed to write to $file"); 29 | } 30 | fclose($fh); 31 | } 32 | 33 | // (D) INSERT INTO FILE 34 | function insert ($file, $search, $add, $offset=0) { 35 | // (D1) BACKUP SPECIFIED FILE 36 | $this->backup($file); 37 | 38 | // (D2) SEEK "LINE TO INSERT AT" 39 | $lines = file($file); 40 | $at = -1; 41 | foreach ($lines as $l=>$line) { 42 | if (strpos($line, $search) !== false) { $at = $l + 1 + $offset; break; } 43 | } 44 | if ($at == -1) { exit("Failed to update $file"); } 45 | 46 | // (D3) INSERT INTO FILE 47 | array_splice($lines, $at, 0, $add); 48 | if (file_put_contents($file, implode("", $lines)) == false) { 49 | exit("Failed to update $file"); 50 | } 51 | } 52 | 53 | // (E) CONDITIONAL INSERT 54 | function cinsert ($condition, $file, $search, $add, $offset=0) { 55 | $insert = true; 56 | $stream = fopen($file, "r"); 57 | while($line = fgets($stream)) { 58 | if (strpos($line, $condition) !== false) { $insert = false; break; } 59 | } 60 | if ($insert) { $this->insert($file, $search, $add, $offset); } 61 | } 62 | 63 | // (F) CLEAN UP 64 | function clean ($module) { 65 | $file = PATH_PAGES . "PAGE-install-$module.php"; 66 | if (!unlink($file)) { echo "Failed to delete $file, please do so manually."; } 67 | echo "Installation complete"; 68 | } 69 | } -------------------------------------------------------------------------------- /lib/LIB-Mail.php: -------------------------------------------------------------------------------- 1 | error = "Please set to, subject, body (or template)."; 19 | return false; 20 | } 21 | 22 | // (A2) ATTACHMENT CHECK 23 | if (isset($mail["attach"])) { 24 | if (!is_array($mail["attach"])) { $mail["attach"] = [$mail["attach"]]; } 25 | foreach ($mail["attach"] as $f) { if (!file_exists($f)) { 26 | $this->error = "$f does not exist!"; 27 | return false; 28 | }} 29 | } 30 | 31 | // (A3) TEMPLATE FILE CHECK 32 | if (isset($mail["template"]) && !file_exists($mail["template"])) { 33 | $this->error = "Template ". $mail["template"] ." does not exist!"; 34 | return false; 35 | } 36 | 37 | // (A4) BUILD MAIL HEADERS 38 | $boundary = isset($mail["attach"]) ? md5(time()) : null ; 39 | $headers = [ 40 | "MIME-Version: 1.0", 41 | "Content-type: " . (isset($mail["attach"]) 42 | ? "multipart/mixed; boundary=\"$boundary\"" 43 | : "text/html; charset=utf-8"), 44 | "From: " . (isset($mail["from"]) ? $mail["from"] : EMAIL_FROM) 45 | ]; 46 | if (isset($mail["cc"])) { 47 | $headers[] = "Cc: " . (is_array($mail["cc"]) ? implode(", ", $mail["cc"]) : $mail["cc"]); 48 | } 49 | if (isset($mail["bcc"])) { 50 | $headers[] = "Bcc: " . (is_array($mail["bcc"]) ? implode(", ", $mail["bcc"]) : $mail["bcc"]); 51 | } 52 | $headers = implode("\r\n", $headers); 53 | 54 | // (A5) BUILD TEMPLATE 55 | if (isset($mail["template"])) { 56 | $mail["body"] = $this->template( 57 | $mail["template"], is_array($mail["vars"]) ? $mail["vars"] : null 58 | ); 59 | } 60 | 61 | // (A6) ADD ATTACHMENT(S) 62 | if (isset($mail["attach"])) { 63 | // (A6-1) MAIL MESSAGE 64 | $mail["body"] = implode("\r\n", [ 65 | "--$boundary", 66 | "Content-type: text/html; charset=utf-8", 67 | "", $mail["body"] 68 | ]); 69 | 70 | // (A6-2) MAIL ATTACHMENTS 71 | $attachments = count($mail["attach"]) - 1; 72 | for ($i=0; $i<=$attachments; $i++) { 73 | $mail["body"] .= implode("\r\n", [ 74 | "", "--$boundary", 75 | "Content-Type: ".mime_content_type($mail["attach"][$i])."; name=\"".basename($mail["attach"][$i])."\"", 76 | "Content-Transfer-Encoding: base64", 77 | "Content-Disposition: attachment", 78 | "", chunk_split(base64_encode(file_get_contents($mail["attach"][$i]))), 79 | "--$boundary" 80 | ]); 81 | if ($i==$attachments) { $mail["body"] .= "--"; } 82 | } 83 | } 84 | 85 | // (A7) MAIL SEND 86 | if (is_array($mail["to"])) { $mail["to"] = implode(", ", $mail["to"]); } 87 | if (@mail($mail["to"], $mail["subject"], $mail["body"], $headers)) { return true; } 88 | else { 89 | $this->error = "Error sending mail"; 90 | return false; 91 | } 92 | } 93 | 94 | // (B) LOAD TEMPLATE 95 | function template ($file, $vars=null) { 96 | ob_start(); 97 | if ($vars!==null) { extract($vars); } 98 | include $file; 99 | $content = ob_get_contents(); 100 | ob_end_clean(); 101 | return $content; 102 | } 103 | } -------------------------------------------------------------------------------- /lib/LIB-NFCIN.php: -------------------------------------------------------------------------------- 1 | load("Users"); 8 | } 9 | 10 | // (B) CREATE NEW NFC LOGIN TOKEN 11 | // $id : user id 12 | function add ($id) { 13 | // (B1) UPDATE TOKEN 14 | $token = $this->Core->random($this->nlen); 15 | $this->Users->hashAdd($id, "NFC", password_hash($token, PASSWORD_DEFAULT)); 16 | 17 | // (B2) RETURN ENCODED TOKEN 18 | require PATH_LIB . "JWT/autoload.php"; 19 | return Firebase\JWT\JWT::encode([$id, $token], JWT_SECRET, JWT_ALGO); 20 | } 21 | 22 | // (C) NULLIFY NFC TOKEN 23 | // $id : user id 24 | function del ($id) { 25 | $this->Users->hashDel($id, "NFC"); 26 | return true; 27 | } 28 | 29 | // (D) NFC TOKEN LOGIN 30 | function login ($token) { 31 | // (D1) DECODE TOKEN 32 | $valid = true; 33 | try { 34 | require PATH_LIB . "JWT/autoload.php"; 35 | $token = Firebase\JWT\JWT::decode( 36 | $token, new Firebase\JWT\Key(JWT_SECRET, JWT_ALGO) 37 | ); 38 | $valid = is_object($token); 39 | if ($valid) { 40 | $token = (array) $token; 41 | $valid = count($token)==2; 42 | } 43 | } catch (Exception $e) { $valid = false; } 44 | 45 | // (D2) VERIFY TOKEN 46 | if ($valid) { 47 | $user = $this->Users->get($token[0], "NFC"); 48 | $valid = (is_array($user) && $user["user_level"]!="S" && password_verify($token[1], $user["hash_code"])); 49 | } 50 | 51 | // (D3) SESSION START 52 | if ($valid) { 53 | $_SESSION["user"] = $user; 54 | unset($_SESSION["user"]["user_password"]); 55 | unset($_SESSION["user"]["hash_code"]); 56 | unset($_SESSION["user"]["hash_time"]); 57 | unset($_SESSION["user"]["hash_tries"]); 58 | $this->Session->save(); 59 | return true; 60 | } 61 | 62 | // (D4) NADA 63 | $this->error = "Invalid token"; 64 | return false; 65 | } 66 | } -------------------------------------------------------------------------------- /lib/LIB-Page.php: -------------------------------------------------------------------------------- 1 | Core->page != null && $this->Core->page["total"]!=0) { 9 | echo "
    "; 10 | 11 | // (A1) ENOUGH PAGES TO HIDE - DRAW WITH ... SQUARES 12 | if ($this->Core->page["total"]>5 + ($adj*2)) { 13 | // (A1-1) CURRENT PAGE IS CLOSE TO BEGINNING - HIDE LATER PAGES 14 | if ($this->Core->page["now"] < 2 + ($adj*2)) { 15 | for ($i=1; $i<3 + ($adj*2); $i++) { 16 | $this->cell($i, $action, $mode, $i==$this->Core->page["now"]); 17 | } 18 | $this->cell("..."); 19 | for ($i=$this->Core->page["total"]-1; $i<=$this->Core->page["total"]; $i++) { 20 | $this->cell($i, $action, $mode, $i==$this->Core->page["now"]); 21 | } 22 | } 23 | 24 | // (A1-2) CURRENT PAGE SOMEWHERE IN THE MIDDLE 25 | else if ($this->Core->page["total"] - ($adj*2) > $this->Core->page["now"] && $this->Core->page["now"] > ($adj*2)) { 26 | for ($i=1; $i<3; $i++) { 27 | $this->cell($i, $action, $mode, $i==$this->Core->page["now"]); 28 | } 29 | $this->cell("..."); 30 | for ($i=$this->Core->page["now"]-$adj; $i<=$this->Core->page["now"]+$adj; $i++) { 31 | $this->cell($i, $action, $mode, $i==$this->Core->page["now"]); 32 | } 33 | $this->cell("..."); 34 | for ($i=$this->Core->page["total"]-1; $i<=$this->Core->page["total"]; $i++) { 35 | $this->cell($i, $action, $mode, $i==$this->Core->page["now"]); 36 | } 37 | } 38 | 39 | // (A1-3) CURRENT PAGE SOMEWHERE IN THE MIDDLE - HIDE EARLY PAGES 40 | else { 41 | for ($i=1; $i<3; $i++) { 42 | $this->cell($i, $action, $mode, $i==$this->Core->page["now"]); 43 | } 44 | $this->cell("..."); 45 | for ($i=$this->Core->page["total"] - (2+($adj * 2)); $i<=$this->Core->page["total"]; $i++) { 46 | $this->cell($i, $action, $mode, $i==$this->Core->page["now"]); 47 | } 48 | } 49 | } 50 | 51 | // (A2) NOT ENOUGH PAGES - JUST DRAW ALL 52 | else { 53 | for ($i=1; $i<=$this->Core->page["total"]; $i++) { 54 | $this->cell($i, $action, $mode, $i==$this->Core->page["now"]); 55 | } 56 | } 57 | echo "
"; 58 | }} 59 | 60 | // (B) SUPPORT FUNCTION, DRAW AN HTML PAGINATION CELL 61 | // $pg : page number (or text) 62 | // $action : URL link or Javascript function 63 | // $mode : "J"avascript function or "A"nchor links 64 | // $current : is current page? 65 | function cell ($pg, $action=null, $mode="J", $current=false) : void { 66 | // (B1) OPENING
  • TAG 67 | echo "
  • "; 68 | 69 | // (B2) INNER OR 70 | if ($mode=="A") { 71 | $tag = "a"; 72 | $act = $action!==null ? " href='$action?pg=$pg'" : ""; 73 | } else { 74 | $tag = "span"; 75 | $act = $action!==null ? " onclick='$action($pg)'" : ""; 76 | } 77 | echo "<$tag class='page-link'$act>$pg"; 78 | 79 | // (B3) CLOSING
  • TAG 80 | echo ""; 81 | } 82 | } -------------------------------------------------------------------------------- /lib/LIB-Report.php: -------------------------------------------------------------------------------- 1 | DB->fetch( 7 | "SELECT *, DATE_FORMAT(`course_start`, '".D_LONG."') `sd`, DATE_FORMAT(`course_end`, '".D_LONG."') `ed` 8 | FROM `courses` 9 | WHERE `course_code`=?", 10 | [$code] 11 | ); 12 | 13 | // (A2) OUTPUT CSV HEADER 14 | header("Content-Disposition: attachment; filename={$course["course_code"]}.csv;"); 15 | $f = fopen("php://output", "w"); 16 | fputcsv($f, [strtoupper("[{$course["course_code"]}] {$course["course_name"]}")]); 17 | fputcsv($f, ["FROM {$course["sd"]} TO {$course["ed"]}"]); 18 | unset($course); 19 | 20 | // (A3) CLASSES 21 | $this->DB->query( 22 | "SELECT *, DATE_FORMAT(`class_date`, '".DT_LONG."') `cd` 23 | FROM `classes` 24 | WHERE `course_code`=? 25 | ORDER BY `class_date` ASC", [$code] 26 | ); 27 | $class = []; $i = 1; 28 | $classdate = ["STUDENT/CLASS"]; 29 | while ($r = $this->DB->stmt->fetch()) { 30 | $class[$r["class_id"]] = $i; $i++; 31 | $classdate[] = $r["cd"]; 32 | } 33 | fputcsv($f, $classdate); 34 | unset($classdate); unset($i); 35 | 36 | // (A4) ASSUME "ABSENT" IF NO ATTENDANCE RECORDS ARE FOUND 37 | $preattend = []; 38 | for ($i=0; $iDB->query( 42 | "SELECT u.`user_name`, u.`user_email`, a.`user_id`, a.`class_id` 43 | FROM `attendance` a 44 | LEFT JOIN `users` u ON (a.`user_id`=u.`user_id`) 45 | LEFT JOIN `courses_users` cu ON (a.`user_id`=cu.`user_id`) 46 | WHERE a.`a_status`=? AND cu.`course_code`=? AND u.`user_level`='U' 47 | ORDER BY u.`user_id`, a.`class_id`", 48 | [1, $code] 49 | ); 50 | $uid = 0; 51 | while ($r = $this->DB->stmt->fetch()) { 52 | // (A5-1) NEXT STUDENT 53 | if ($r["user_id"]!=$uid) { 54 | if ($uid!=0) { fputcsv($f, $row); } 55 | $row = ["{$r["user_name"]} ({$r["user_email"]})"]; 56 | $row = array_merge($row, $preattend); 57 | $uid = $r["user_id"]; 58 | } 59 | 60 | // (A5-2) ATTENDANCE RECORD 61 | $row[$class[$r["class_id"]]] = 1; 62 | } 63 | 64 | // (A5-3) LAST STUDENT 65 | if ($uid!=0) { fputcsv($f, $row); } 66 | 67 | // (A6) DONE 68 | fclose($f); 69 | } 70 | } -------------------------------------------------------------------------------- /lib/LIB-Session.php: -------------------------------------------------------------------------------- 1 | HOST_NAME, 6 | "path" => "/", 7 | "httponly" => true, 8 | "expires" => 0, 9 | // "secure" => true, 10 | "samesite" => "Lax" 11 | ]; 12 | 13 | // (B) CONSTRUCTOR - AUTO VALIDATE JWT COOKIE & RESTORE SESSION DATA 14 | function __construct ($core) { 15 | // (B1) INIT - CORE LINKS 16 | parent::__construct($core); 17 | $_SESSION = []; 18 | $valid = false; 19 | 20 | // (B2) DECODE JWT COOKIE 21 | if (isset($_COOKIE["cbsess"])) { try { 22 | require PATH_LIB . "JWT/autoload.php"; 23 | $token = Firebase\JWT\JWT::decode( 24 | $_COOKIE["cbsess"], new Firebase\JWT\Key(JWT_SECRET, JWT_ALGO) 25 | ); 26 | $valid = is_object($token); 27 | } catch (Exception $e) { $valid = false; }} 28 | 29 | // (B3) EXPIRED? VALID ISSUER? VALID AUDIENCE? 30 | if ($valid) { 31 | $now = strtotime("now"); 32 | $valid = $token->iss == JWT_ISSUER && 33 | $token->aud == HOST_NAME && 34 | $token->nbf <= $now; 35 | if ($valid && JWT_EXPIRE!=0) { 36 | $valid = isset($token->exp) ? ($token->exp < $now) : false; 37 | } 38 | } 39 | 40 | // (B4) UNPACK COOKIE DATA INTO SESSION 41 | if ($valid) { 42 | $_SESSION = (array) $token->data; 43 | foreach ($_SESSION as $k=>$v) { 44 | if (is_object($v)) { $_SESSION[$k] = (array) $v; } 45 | } 46 | unset($token); 47 | } 48 | 49 | // (B5) INVALID SESSION 50 | if (!$valid && isset($_COOKIE["cbsess"])) { 51 | $this->destroy(); 52 | throw new Exception("Invalid or expired session."); 53 | } 54 | 55 | // (B6) OK - VALID SESSION HOOK 56 | unset($_COOKIE["cbsess"]); 57 | require PATH_LIB . "HOOK-SESS-Load.php"; 58 | } 59 | 60 | // (C) CREATE CBSESS COOKIE 61 | function save () { 62 | // (C1) FILTER SESSION DATA TO PUT INTO COOKIE 63 | $data = $_SESSION; 64 | require PATH_LIB . "HOOK-SESS-Save.php"; 65 | 66 | // (C2) GENERATE JWT COOKIE 67 | require PATH_LIB . "JWT/autoload.php"; 68 | $now = strtotime("now"); 69 | $token = [ 70 | "iat" => $now, // issued at 71 | "nbf" => $now, // not before 72 | "jti" => base64_encode(random_bytes(16)), // json token id 73 | "iss" => JWT_ISSUER, // issuer 74 | "aud" => HOST_NAME, // audience 75 | "data" => $data // additional data 76 | ]; 77 | if (JWT_EXPIRE > 0) { $token["exp"] = $now + JWT_EXPIRE; } // expiry 78 | $token = Firebase\JWT\JWT::encode($token, JWT_SECRET, JWT_ALGO); 79 | setcookie("cbsess", $token, $this->cookie); 80 | } 81 | 82 | // (D) DESTROY SESSION + COOKIE 83 | function destroy () { 84 | // (D1) EXPIRE HTTP COOKIE 85 | $options = $this->cookie; 86 | $options["expires"] = -1; 87 | setcookie("cbsess", "", $options); 88 | 89 | // (D2) CLEAR ALL SESSION DATA 90 | $_SESSION = []; 91 | } 92 | } -------------------------------------------------------------------------------- /lib/LIB-Settings.php: -------------------------------------------------------------------------------- 1 | defineG(1); 7 | } 8 | 9 | // (B) AUTO DEFINE BY SETTING GROUP 10 | // $group : setting group 11 | function defineG ($group) : void { 12 | foreach ($this->DB->fetchKV( 13 | "SELECT * FROM `settings` WHERE `setting_group`=?", 14 | [$group], "setting_name", "setting_value" 15 | ) as $k=>$v) { define($k, $v); } 16 | } 17 | 18 | // (C) AUTO DEFINE BY SETTING NAME 19 | // $name : setting name (string or array) 20 | // $json : json decode setting value? 21 | function defineN ($name, $json=false) : void { 22 | // (C1) SQL & DATA 23 | $sql = "SELECT * FROM `settings` WHERE `setting_name`"; 24 | if (is_array($name)) { 25 | $sql .= " IN ("; 26 | foreach ($name as $n) { $sql .= "?,"; } 27 | $sql = substr($sql,0,-1) . ")"; 28 | $data = $name; 29 | } else { 30 | $sql .= "=?"; 31 | $data = [$name]; 32 | } 33 | 34 | // (C2) GET & DEFINE 35 | foreach ($this->DB->fetchKV( 36 | $sql, $data, "setting_name", "setting_value" 37 | ) as $k=>$v) { define($k, ($json?json_decode($v,true):$v)); } 38 | } 39 | 40 | // (D) GET SETTINGS 41 | // $group : setting group 42 | function getAll ($group=1) { 43 | return $this->DB->fetchAll("SELECT * FROM `settings` WHERE `setting_group`=?", [$group]); 44 | } 45 | 46 | // (E) SAVE SETTINGS 47 | // $settings : string, json encoded array of settings in key => value 48 | // $settings : array, key => value 49 | function save ($settings) { 50 | if (!is_array($settings)) { 51 | $settings = json_decode($settings, true); 52 | } 53 | foreach ($settings as $k=>$v) { 54 | $this->DB->update("settings", ["setting_value"], "`setting_name`=?", [$v, $k]); 55 | } 56 | return true; 57 | } 58 | } -------------------------------------------------------------------------------- /lib/LIB-WAIN.php: -------------------------------------------------------------------------------- 1 | wa = new lbuchs\WebAuthn\WebAuthn(HOST_NAME, HOST_NAME); 13 | $core->load("Users"); 14 | } 15 | 16 | // (B) HELPER - CREATE CHALLENGE KEY 17 | function setChallenge ($id) : void { 18 | $this->Users->hashAdd( 19 | $id, "PLC", 20 | bin2hex(($this->wa->getChallenge())->getBinaryString()) 21 | ); 22 | } 23 | 24 | // (C) HELPER - GET CHALLENGE KEY 25 | function getChallenge ($id) { 26 | $challenge = $this->Users->hashGet($id, "PLC"); 27 | if (!is_array($challenge)) { 28 | $this->error = "Invalid credentials"; 29 | return false; 30 | } 31 | return hex2bin($challenge["hash_code"]); 32 | } 33 | 34 | // (D) HELPER - GET USER & CREDENTIAL 35 | // $email : user email 36 | function getUser ($email) { 37 | $user = $this->Users->get($email, "PL"); 38 | if (!is_array($user)) { 39 | $this->error = "Invalid user"; 40 | return false; 41 | } 42 | if ($user["hash_code"]==null) { 43 | $this->error = "Please register for passwordless login first."; 44 | return false; 45 | } 46 | if ($user["user_level"]=="S") { 47 | $this->error = "Invalid user or password."; 48 | return false; 49 | } 50 | $user["hash_code"] = json_decode($user["hash_code"]); 51 | $user["hash_code"]->credentialId = hex2bin($user["hash_code"]->credentialId); 52 | $user["hash_code"]->AAGUID = hex2bin($user["hash_code"]->AAGUID); 53 | return $user; 54 | } 55 | 56 | // (E) REGISTRATION PART 1 - GENERATE PUBLIC KEY 57 | function regA () { 58 | $args = $this->wa->getCreateArgs( 59 | \decbin($_SESSION["user"]["user_id"]), $_SESSION["user"]["user_email"], $_SESSION["user"]["user_name"], 60 | $this->timeout, false, true 61 | ); 62 | $this->setChallenge($_SESSION["user"]["user_id"]); 63 | return json_encode($args); 64 | } 65 | 66 | // (F) REGISTRATION PART 2 - CHECK & SAVE CREDENTIAL 67 | function regB () { 68 | // (F1) GET CHALLENGE 69 | $challenge = $this->getChallenge($_SESSION["user"]["user_id"]); 70 | 71 | // (F2) VERIFY & CREATE CREDENTIAL 72 | try { 73 | $data = $this->wa->processCreate( 74 | base64_decode($_POST["client"]), 75 | base64_decode($_POST["attest"]), 76 | $challenge, 77 | true, true, false 78 | ); 79 | $data->credentialId = bin2hex($data->credentialId); 80 | $data->AAGUID = bin2hex($data->AAGUID); 81 | $data = json_encode((array)$data); 82 | } catch (Exception $ex) { 83 | $this->error = $ex->getMessage(); 84 | return false; 85 | } 86 | 87 | // (F3) SAVE 88 | $this->Users->hashAdd($_SESSION["user"]["user_id"], "PL", $data); 89 | $this->Users->hashDel($_SESSION["user"]["user_id"], "PLC"); 90 | return true; 91 | } 92 | 93 | // (G) UNREGISTER 94 | function unreg () { 95 | $this->Users->hashDel($_SESSION["user"]["user_id"], "PL"); 96 | $this->Users->hashDel($_SESSION["user"]["user_id"], "PLC"); 97 | return true; 98 | } 99 | 100 | // (H) LOGIN VALIDATION PART 1 - GENERATE PUBLIC KEY 101 | // $email : user email 102 | function loginA ($email) { 103 | $user = $this->getUser($email); 104 | if ($user===false) { return false; } 105 | $args = $this->wa->getGetArgs([$user["hash_code"]->credentialId], $this->timeout); 106 | $this->setChallenge($user["user_id"]); 107 | return json_encode($args); 108 | } 109 | 110 | // (I) LOGIN VALIDATION PART 2 - CHECK & PROCEED 111 | function loginB ($email) { 112 | // (I1) GET USER, CREDENTIAL, CHALLENGE 113 | $user = $this->getUser($email); 114 | if ($user===false) { return false; } 115 | $challenge = $this->getChallenge($user["user_id"]); 116 | $id = base64_decode($_POST["id"]); 117 | 118 | // (I2) CHECK CREDENTIAL 119 | if ($user["hash_code"]->credentialId !== $id) { 120 | $this->error = "Invalid credentials"; 121 | return false; 122 | } 123 | $this->wa->processGet( 124 | base64_decode($_POST["client"]), 125 | base64_decode($_POST["auth"]), 126 | base64_decode($_POST["sig"]), 127 | $user["hash_code"]->credentialPublicKey, 128 | $challenge 129 | ); 130 | 131 | // (I3) PROCESS LOGIN 132 | unset($user["user_password"]); 133 | unset($user["hash_code"]); 134 | unset($user["hash_time"]); 135 | unset($user["hash_tries"]); 136 | $_SESSION["user"] = $user; 137 | $this->Session->save(); 138 | return true; 139 | } 140 | } -------------------------------------------------------------------------------- /lib/SQL-I-WAS-HERE-1.sql: -------------------------------------------------------------------------------- 1 | -- (A) SETTINGS 2 | CREATE TABLE `settings` ( 3 | `setting_name` varchar(255) NOT NULL, 4 | `setting_description` varchar(255) DEFAULT NULL, 5 | `setting_value` varchar(255) NOT NULL, 6 | `setting_group` int(11) NOT NULL DEFAULT 1 7 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 8 | 9 | INSERT INTO `settings` (`setting_name`, `setting_description`, `setting_value`, `setting_group`) VALUES 10 | ('APP_VER', 'App version', '1', 0), 11 | ('EMAIL_FROM', 'System email from', 'sys@site.com', 1), 12 | ('PAGE_PER', 'Number of entries per page', '20', 1), 13 | ('D_LONG', 'MYSQL date format (long)', '%e %M %Y', 1), 14 | ('D_SHORT', 'MYSQL date format (short)', '%Y-%m-%d', 1), 15 | ('DT_LONG', 'MYSQL date time format (long)', '%e %M %Y %l:%i:%S %p', 1), 16 | ('DT_SHORT', 'MYSQL date time format (short)', '%Y-%m-%d %H:%i:%S', 1), 17 | ('SUGGEST_LIMIT', 'Autocomplete suggestion limit', 5, 1); 18 | 19 | ALTER TABLE `settings` 20 | ADD PRIMARY KEY (`setting_name`), 21 | ADD KEY `setting_group` (`setting_group`); 22 | 23 | -- (B) USERS 24 | CREATE TABLE `users` ( 25 | `user_id` bigint(20) NOT NULL, 26 | `user_level` varchar(1) NOT NULL DEFAULT 'U', 27 | `user_name` varchar(255) NOT NULL, 28 | `user_email` varchar(255) NOT NULL, 29 | `user_password` varchar(255) NOT NULL 30 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; 31 | 32 | ALTER TABLE `users` 33 | ADD PRIMARY KEY (`user_id`), 34 | ADD UNIQUE KEY `user_email` (`user_email`), 35 | ADD KEY `user_name` (`user_name`), 36 | ADD KEY `user_level` (`user_level`); 37 | 38 | ALTER TABLE `users` 39 | MODIFY `user_id` bigint(20) NOT NULL AUTO_INCREMENT; 40 | 41 | -- (C) HASH 42 | CREATE TABLE `users_hash` ( 43 | `user_id` bigint(20) NOT NULL, 44 | `hash_for` varchar(3) NOT NULL, 45 | `hash_code` text NOT NULL, 46 | `hash_time` datetime NOT NULL, 47 | `hash_tries` int(11) NOT NULL DEFAULT '0' 48 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; 49 | 50 | ALTER TABLE `users_hash` 51 | ADD PRIMARY KEY (`user_id`, `hash_for`); 52 | 53 | -- (D) COURSES 54 | CREATE TABLE `courses` ( 55 | `course_code` varchar(255) NOT NULL, 56 | `course_name` varchar(255) NOT NULL, 57 | `course_desc` text DEFAULT NULL, 58 | `course_start` date NOT NULL, 59 | `course_end` date NOT NULL 60 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 61 | 62 | ALTER TABLE `courses` 63 | ADD PRIMARY KEY (`course_code`), 64 | ADD KEY `course_name` (`course_name`), 65 | ADD KEY `course_start` (`course_start`), 66 | ADD KEY `course_end` (`course_end`); 67 | 68 | -- (E) COURSES-USERS 69 | CREATE TABLE `courses_users` ( 70 | `course_code` varchar(255) NOT NULL, 71 | `user_id` bigint(20) NOT NULL 72 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 73 | 74 | ALTER TABLE `courses_users` 75 | ADD PRIMARY KEY (`course_code`,`user_id`); 76 | 77 | -- (F) CLASSES 78 | CREATE TABLE `classes` ( 79 | `class_id` bigint(20) NOT NULL, 80 | `course_code` varchar(255) NOT NULL, 81 | `user_id` bigint(20) NOT NULL, 82 | `class_date` datetime NOT NULL DEFAULT current_timestamp(), 83 | `class_desc` text DEFAULT NULL, 84 | `class_hash` varchar(32) NOT NULL 85 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 86 | 87 | ALTER TABLE `classes` 88 | ADD PRIMARY KEY (`class_id`), 89 | ADD KEY `course_code` (`course_code`), 90 | ADD KEY `user_id` (`user_id`), 91 | ADD KEY `class_date` (`class_date`); 92 | 93 | ALTER TABLE `classes` 94 | MODIFY `class_id` bigint(20) NOT NULL AUTO_INCREMENT; 95 | 96 | -- (G) ATTENDANCE 97 | CREATE TABLE `attendance` ( 98 | `class_id` bigint(20) NOT NULL, 99 | `user_id` bigint(20) NOT NULL, 100 | `a_status` tinyint(1) NOT NULL, 101 | `a_by` bigint(20) NOT NULL, 102 | `a_date` datetime NOT NULL DEFAULT current_timestamp(), 103 | `a_notes` varchar(255) NULL 104 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 105 | 106 | ALTER TABLE `attendance` 107 | ADD PRIMARY KEY (`class_id`,`user_id`), 108 | ADD KEY `a_status` (`a_status`), 109 | ADD KEY `a_by` (`a_by`), 110 | ADD KEY `a_date` (`a_date`); -------------------------------------------------------------------------------- /lib/WebAuthn/autoload.php: -------------------------------------------------------------------------------- 1 | $vendorDir . '/composer/InstalledVersions.php', 10 | ); 11 | -------------------------------------------------------------------------------- /lib/WebAuthn/composer/autoload_namespaces.php: -------------------------------------------------------------------------------- 1 | array($vendorDir . '/lbuchs/webauthn/src'), 10 | ); 11 | -------------------------------------------------------------------------------- /lib/WebAuthn/composer/autoload_real.php: -------------------------------------------------------------------------------- 1 | register(true); 35 | 36 | return $loader; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/WebAuthn/composer/autoload_static.php: -------------------------------------------------------------------------------- 1 | 11 | array ( 12 | 'lbuchs\\WebAuthn\\' => 16, 13 | ), 14 | ); 15 | 16 | public static $prefixDirsPsr4 = array ( 17 | 'lbuchs\\WebAuthn\\' => 18 | array ( 19 | 0 => __DIR__ . '/..' . '/lbuchs/webauthn/src', 20 | ), 21 | ); 22 | 23 | public static $classMap = array ( 24 | 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 25 | ); 26 | 27 | public static function getInitializer(ClassLoader $loader) 28 | { 29 | return \Closure::bind(function () use ($loader) { 30 | $loader->prefixLengthsPsr4 = ComposerStaticInit8c9f25d2c3842b097ca941e347932e41::$prefixLengthsPsr4; 31 | $loader->prefixDirsPsr4 = ComposerStaticInit8c9f25d2c3842b097ca941e347932e41::$prefixDirsPsr4; 32 | $loader->classMap = ComposerStaticInit8c9f25d2c3842b097ca941e347932e41::$classMap; 33 | 34 | }, null, ClassLoader::class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/WebAuthn/composer/installed.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | { 4 | "name": "lbuchs/webauthn", 5 | "version": "v2.0.1", 6 | "version_normalized": "2.0.1.0", 7 | "source": { 8 | "type": "git", 9 | "url": "https://github.com/lbuchs/WebAuthn.git", 10 | "reference": "7d3ea0d83ac1dac35bb159f166202ee7a55ef1fe" 11 | }, 12 | "dist": { 13 | "type": "zip", 14 | "url": "https://api.github.com/repos/lbuchs/WebAuthn/zipball/7d3ea0d83ac1dac35bb159f166202ee7a55ef1fe", 15 | "reference": "7d3ea0d83ac1dac35bb159f166202ee7a55ef1fe", 16 | "shasum": "" 17 | }, 18 | "require": { 19 | "php": ">=8.0.0" 20 | }, 21 | "time": "2023-05-16T07:27:49+00:00", 22 | "type": "library", 23 | "installation-source": "dist", 24 | "autoload": { 25 | "psr-4": { 26 | "lbuchs\\WebAuthn\\": "src" 27 | } 28 | }, 29 | "notification-url": "https://packagist.org/downloads/", 30 | "license": [ 31 | "MIT" 32 | ], 33 | "authors": [ 34 | { 35 | "name": "Lukas Buchs", 36 | "role": "Developer" 37 | } 38 | ], 39 | "description": "A simple PHP WebAuthn (FIDO2) server library", 40 | "homepage": "https://github.com/lbuchs/webauthn", 41 | "keywords": [ 42 | "Authentication", 43 | "webauthn" 44 | ], 45 | "support": { 46 | "issues": "https://github.com/lbuchs/WebAuthn/issues", 47 | "source": "https://github.com/lbuchs/WebAuthn/tree/v2.0.1" 48 | }, 49 | "install-path": "../lbuchs/webauthn" 50 | } 51 | ], 52 | "dev": true, 53 | "dev-package-names": [] 54 | } 55 | -------------------------------------------------------------------------------- /lib/WebAuthn/composer/installed.php: -------------------------------------------------------------------------------- 1 | array( 3 | 'name' => '__root__', 4 | 'pretty_version' => '1.0.0+no-version-set', 5 | 'version' => '1.0.0.0', 6 | 'reference' => NULL, 7 | 'type' => 'library', 8 | 'install_path' => __DIR__ . '/../../', 9 | 'aliases' => array(), 10 | 'dev' => true, 11 | ), 12 | 'versions' => array( 13 | '__root__' => array( 14 | 'pretty_version' => '1.0.0+no-version-set', 15 | 'version' => '1.0.0.0', 16 | 'reference' => NULL, 17 | 'type' => 'library', 18 | 'install_path' => __DIR__ . '/../../', 19 | 'aliases' => array(), 20 | 'dev_requirement' => false, 21 | ), 22 | 'lbuchs/webauthn' => array( 23 | 'pretty_version' => 'v2.0.1', 24 | 'version' => '2.0.1.0', 25 | 'reference' => '7d3ea0d83ac1dac35bb159f166202ee7a55ef1fe', 26 | 'type' => 'library', 27 | 'install_path' => __DIR__ . '/../lbuchs/webauthn', 28 | 'aliases' => array(), 29 | 'dev_requirement' => false, 30 | ), 31 | ), 32 | ); 33 | -------------------------------------------------------------------------------- /lib/WebAuthn/composer/platform_check.php: -------------------------------------------------------------------------------- 1 | = 80000)) { 8 | $issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.0". You are running ' . PHP_VERSION . '.'; 9 | } 10 | 11 | if ($issues) { 12 | if (!headers_sent()) { 13 | header('HTTP/1.1 500 Internal Server Error'); 14 | } 15 | if (!ini_get('display_errors')) { 16 | if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { 17 | fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); 18 | } elseif (!headers_sent()) { 19 | echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; 20 | } 21 | } 22 | trigger_error( 23 | 'Composer detected issues in your platform: ' . implode(' ', $issues), 24 | E_USER_ERROR 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /lib/WebAuthn/lbuchs/webauthn/.gitignore: -------------------------------------------------------------------------------- 1 | # Netbeans project 2 | nbproject/ 3 | /index.php 4 | 5 | 6 | # .pem files from FIDO Alliance Metadata Service (MDS) 7 | _test/rootCertificates/mds/*.pem 8 | _test/rootCertificates/mds/lastMdsFetch.txt 9 | -------------------------------------------------------------------------------- /lib/WebAuthn/lbuchs/webauthn/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2022 Lukas Buchs 4 | Copyright © 2018 Thomas Bleeker (CBOR & ByteBuffer part) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/WebAuthn/lbuchs/webauthn/_test/rootCertificates/apple.pem: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | 68:1d:01:6c:7a:3c:e3:02:25:a5:01:94:28:47:57:71 6 | 7 | Signature Algorithm: ecdsa-with-SHA384 8 | 9 | Issuer: 10 | stateOrProvinceName = California 11 | organizationName = Apple Inc. 12 | commonName = Apple WebAuthn Root CA 13 | 14 | Validity 15 | Not Before: Mar 18 18:21:32 2020 GMT 16 | Not After : Mar 15 00:00:00 2045 GMT 17 | 18 | Subject: 19 | stateOrProvinceName = California 20 | organizationName = Apple Inc. 21 | commonName = Apple WebAuthn Root CA 22 | 23 | Subject Public Key Info: 24 | Public Key Algorithm: id-ecPublicKey 25 | ASN1 OID: secp384r1 26 | 27 | X509v3 extensions: 28 | X509v3 Basic Constraints: critical 29 | CA:TRUE 30 | X509v3 Subject Key Identifier: 31 | 26:D7:64:D9:C5:78:C2:5A:67:D1:A7:DE:6B:12:D0:1B:63:F1:C6:D7 32 | X509v3 Key Usage: critical 33 | Certificate Sign, CRL Sign 34 | 35 | -----BEGIN CERTIFICATE----- 36 | MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w 37 | HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ 38 | bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx 39 | NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG 40 | A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49 41 | AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k 42 | xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/ 43 | pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk 44 | 2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA 45 | MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3 46 | jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B 47 | 1bWeT0vT 48 | -----END CERTIFICATE----- 49 | -------------------------------------------------------------------------------- /lib/WebAuthn/lbuchs/webauthn/_test/rootCertificates/globalSign.pem: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | 04:00:00:00:00:01:0f:86:26:e6:0d 6 | Signature Algorithm: sha1WithRSAEncryption 7 | Issuer: OU=GlobalSign Root CA - R2, O=GlobalSign, CN=GlobalSign 8 | Validity 9 | Not Before: Dec 15 08:00:00 2006 GMT 10 | Not After : Dec 15 08:00:00 2021 GMT 11 | Subject: OU=GlobalSign Root CA - R2, O=GlobalSign, CN=GlobalSign 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | Public-Key: (2048 bit) 15 | 16 | -----BEGIN CERTIFICATE----- 17 | MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G 18 | A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp 19 | Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 20 | MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG 21 | A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI 22 | hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL 23 | v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 24 | eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq 25 | tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd 26 | C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa 27 | zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB 28 | mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH 29 | V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n 30 | bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG 31 | 3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs 32 | J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO 33 | 291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS 34 | ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd 35 | AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 36 | TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== 37 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /lib/WebAuthn/lbuchs/webauthn/_test/rootCertificates/hypersecu.pem: -------------------------------------------------------------------------------- 1 | HyperFIDO U2F Security Key Attestation CA 2 | https://hypersecu.com/support/downloads/attestation 3 | 4 | Last Update: 2017-01-01 5 | 6 | HyperFIDO U2F Security Key devices which contain attestation certificates signed by a set of CAs. 7 | This file contains the CA certificates that Relying Parties (RP) need to configure their software 8 | with to be able to verify U2F device certificates. 9 | 10 | The file will be updated as needed when we publish more CA certificates. 11 | 12 | Issuer: CN=FT FIDO 0100 13 | 14 | -----BEGIN CERTIFICATE----- 15 | MIIBjTCCATOgAwIBAgIBATAKBggqhkjOPQQDAjAXMRUwEwYDVQQDEwxGVCBGSURP 16 | IDAxMDAwHhcNMTQwNzAxMTUzNjI2WhcNNDQwNzAzMTUzNjI2WjAXMRUwEwYDVQQD 17 | EwxGVCBGSURPIDAxMDAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASxdLxJx8ol 18 | S3DS5cIHzunPF0gg69d+o8ZVCMJtpRtlfBzGuVL4YhaXk2SC2gptPTgmpZCV2vbN 19 | fAPi5gOF0vbZo3AwbjAdBgNVHQ4EFgQUXt4jWlYDgwhaPU+EqLmeM9LoPRMwPwYD 20 | VR0jBDgwNoAUXt4jWlYDgwhaPU+EqLmeM9LoPROhG6QZMBcxFTATBgNVBAMTDEZU 21 | IEZJRE8gMDEwMIIBATAMBgNVHRMEBTADAQH/MAoGCCqGSM49BAMCA0gAMEUCIQC2 22 | D9o9cconKTo8+4GZPyZBJ3amc8F0/kzyidX9dhrAIAIgM9ocs5BW/JfmshVP9Mb+ 23 | Joa/kgX4dWbZxrk0ioTfJZg= 24 | -----END CERTIFICATE----- 25 | 26 | 27 | Certificate: 28 | Data: 29 | Version: 3 (0x2) 30 | Serial Number: 4107 (0x100b) 31 | Signature Algorithm: ecdsa-with-SHA256 32 | Issuer: 33 | commonName = HYPERFIDO 0200 34 | organizationName = HYPERSECU 35 | countryName = CA 36 | Validity 37 | Not Before: Jan 1 00:00:00 2018 GMT 38 | Not After : Dec 31 23:59:59 2047 GMT 39 | Subject: 40 | commonName = HYPERFIDO 0200 41 | organizationName = HYPERSECU 42 | countryName = CA 43 | 44 | 45 | -----BEGIN CERTIFICATE----- 46 | MIIBxzCCAWygAwIBAgICEAswCgYIKoZIzj0EAwIwOjELMAkGA1UEBhMCQ0ExEjAQ 47 | BgNVBAoMCUhZUEVSU0VDVTEXMBUGA1UEAwwOSFlQRVJGSURPIDAyMDAwIBcNMTgw 48 | MTAxMDAwMDAwWhgPMjA0NzEyMzEyMzU5NTlaMDoxCzAJBgNVBAYTAkNBMRIwEAYD 49 | VQQKDAlIWVBFUlNFQ1UxFzAVBgNVBAMMDkhZUEVSRklETyAwMjAwMFkwEwYHKoZI 50 | zj0CAQYIKoZIzj0DAQcDQgAErKUI1G0S7a6IOLlmHipLlBuxTYjsEESQvzQh3dB7 51 | dvxxWWm7kWL91rq6S7ayZG0gZPR+zYqdFzwAYDcG4+aX66NgMF4wHQYDVR0OBBYE 52 | FLZYcfMMwkQAGbt3ryzZFPFypmsIMB8GA1UdIwQYMBaAFLZYcfMMwkQAGbt3ryzZ 53 | FPFypmsIMAwGA1UdEwQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMC 54 | A0kAMEYCIQCG2/ppMGt7pkcRie5YIohS3uDPIrmiRcTjqDclKVWg0gIhANcPNDZH 55 | E2/zZ+uB5ThG9OZus+xSb4knkrbAyXKX2zm/ 56 | -----END CERTIFICATE----- 57 | -------------------------------------------------------------------------------- /lib/WebAuthn/lbuchs/webauthn/_test/rootCertificates/mds/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/WebAuthn/lbuchs/webauthn/_test/rootCertificates/solo.pem: -------------------------------------------------------------------------------- 1 | Solokeys FIDO2/U2F Device Attestation CA 2 | ======================================== 3 | Data: 4 | Version: 1 (0x0) 5 | Serial Number: 14143382635911888524 (0xc44763928ff4be8c) 6 | Signature Algorithm: ecdsa-with-SHA256 7 | 8 | Issuer: 9 | emailAddress = hello@solokeys.com 10 | commonName = solokeys.com 11 | organizationalUnitName = Root CA 12 | organizationName = Solo Keys 13 | stateOrProvinceName = Maryland 14 | countryName = US 15 | 16 | Validity 17 | Not Before: Nov 11 12:51:42 2018 GMT 18 | Not After : Oct 29 12:51:42 2068 GMT 19 | 20 | Subject: 21 | emailAddress = hello@solokeys.com 22 | commonName = solokeys.com 23 | organizationalUnitName = Root CA 24 | organizationName = Solo Keys 25 | stateOrProvinceName = Maryland 26 | countryName = US 27 | 28 | 29 | -----BEGIN CERTIFICATE----- 30 | MIIB9DCCAZoCCQDER2OSj/S+jDAKBggqhkjOPQQDAjCBgDELMAkGA1UEBhMCVVMx 31 | ETAPBgNVBAgMCE1hcnlsYW5kMRIwEAYDVQQKDAlTb2xvIEtleXMxEDAOBgNVBAsM 32 | B1Jvb3QgQ0ExFTATBgNVBAMMDHNvbG9rZXlzLmNvbTEhMB8GCSqGSIb3DQEJARYS 33 | aGVsbG9Ac29sb2tleXMuY29tMCAXDTE4MTExMTEyNTE0MloYDzIwNjgxMDI5MTI1 34 | MTQyWjCBgDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1hcnlsYW5kMRIwEAYDVQQK 35 | DAlTb2xvIEtleXMxEDAOBgNVBAsMB1Jvb3QgQ0ExFTATBgNVBAMMDHNvbG9rZXlz 36 | LmNvbTEhMB8GCSqGSIb3DQEJARYSaGVsbG9Ac29sb2tleXMuY29tMFkwEwYHKoZI 37 | zj0CAQYIKoZIzj0DAQcDQgAEWHAN0CCJVZdMs0oktZ5m93uxmB1iyq8ELRLtqVFL 38 | SOiHQEab56qRTB/QzrpGAY++Y2mw+vRuQMNhBiU0KzwjBjAKBggqhkjOPQQDAgNI 39 | ADBFAiEAz9SlrAXIlEu87vra54rICPs+4b0qhp3PdzcTg7rvnP0CIGjxzlteQQx+ 40 | jQGd7rwSZuE5RWUPVygYhUstQO9zNUOs 41 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /lib/WebAuthn/lbuchs/webauthn/_test/rootCertificates/yubico.pem: -------------------------------------------------------------------------------- 1 | Yubico U2F Device Attestation CA 2 | ================================ 3 | 4 | Last Update: 2014-09-01 5 | 6 | Yubico manufacturer U2F devices that contains device attestation 7 | certificates signed by a set of Yubico CAs. This file contains the CA 8 | certificates that Relying Parties (RP) need to configure their 9 | software with to be able to verify U2F device certificates. 10 | 11 | This file has been signed with OpenPGP and you should verify the 12 | signature and the authenticity of the public key before trusting the 13 | content. The signature is located next to the file: 14 | 15 | https://developers.yubico.com/u2f/yubico-u2f-ca-certs.txt 16 | https://developers.yubico.com/u2f/yubico-u2f-ca-certs.txt.sig 17 | 18 | We will update this file from time to time when we publish more CA 19 | certificates. 20 | 21 | Name: Yubico U2F Root CA Serial 457200631 22 | Issued: 2014-08-01 23 | 24 | -----BEGIN CERTIFICATE----- 25 | MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ 26 | dWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw 27 | MDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290 28 | IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK 29 | AoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk 30 | 5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep 31 | 8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw 32 | nebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT 33 | 9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw 34 | LvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ 35 | hjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN 36 | BgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4 37 | MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt 38 | hX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k 39 | LVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U 40 | sG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc 41 | U9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw== 42 | -----END CERTIFICATE----- 43 | -------------------------------------------------------------------------------- /lib/WebAuthn/lbuchs/webauthn/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lbuchs/webauthn", 3 | "description": "A simple PHP WebAuthn (FIDO2) server library", 4 | "keywords": [ 5 | "webauthn", "authentication" 6 | ], 7 | "homepage": "https://github.com/lbuchs/webauthn", 8 | "license": "MIT", 9 | "authors": [ 10 | { 11 | "name": "Lukas Buchs", 12 | "role": "Developer" 13 | } 14 | ], 15 | "require": { 16 | "php" : ">=8.0.0" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "lbuchs\\WebAuthn\\": "src" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/WebAuthn/lbuchs/webauthn/src/Attestation/Format/AndroidKey.php: -------------------------------------------------------------------------------- 1 | _attestationObject['attStmt']; 18 | 19 | if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) { 20 | throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA); 21 | } 22 | 23 | if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) { 24 | throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA); 25 | } 26 | 27 | if (!\array_key_exists('x5c', $attStmt) || !\is_array($attStmt['x5c']) || \count($attStmt['x5c']) < 1) { 28 | throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA); 29 | } 30 | 31 | if (!\is_object($attStmt['x5c'][0]) || !($attStmt['x5c'][0] instanceof ByteBuffer)) { 32 | throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA); 33 | } 34 | 35 | $this->_alg = $attStmt['alg']; 36 | $this->_signature = $attStmt['sig']->getBinaryString(); 37 | $this->_x5c = $attStmt['x5c'][0]->getBinaryString(); 38 | 39 | if (count($attStmt['x5c']) > 1) { 40 | for ($i=1; $i_x5c_chain[] = $attStmt['x5c'][$i]->getBinaryString(); 42 | } 43 | unset ($i); 44 | } 45 | } 46 | 47 | 48 | /* 49 | * returns the key certificate in PEM format 50 | * @return string 51 | */ 52 | public function getCertificatePem() { 53 | return $this->_createCertificatePem($this->_x5c); 54 | } 55 | 56 | /** 57 | * @param string $clientDataHash 58 | */ 59 | public function validateAttestation($clientDataHash) { 60 | $publicKey = \openssl_pkey_get_public($this->getCertificatePem()); 61 | 62 | if ($publicKey === false) { 63 | throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY); 64 | } 65 | 66 | // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash 67 | // using the attestation public key in attestnCert with the algorithm specified in alg. 68 | $dataToVerify = $this->_authenticatorData->getBinary(); 69 | $dataToVerify .= $clientDataHash; 70 | 71 | $coseAlgorithm = $this->_getCoseAlgorithm($this->_alg); 72 | 73 | // check certificate 74 | return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1; 75 | } 76 | 77 | /** 78 | * validates the certificate against root certificates 79 | * @param array $rootCas 80 | * @return boolean 81 | * @throws WebAuthnException 82 | */ 83 | public function validateRootCertificate($rootCas) { 84 | $chainC = $this->_createX5cChainFile(); 85 | if ($chainC) { 86 | $rootCas[] = $chainC; 87 | } 88 | 89 | $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas); 90 | if ($v === -1) { 91 | throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED); 92 | } 93 | return $v; 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /lib/WebAuthn/lbuchs/webauthn/src/Attestation/Format/None.php: -------------------------------------------------------------------------------- 1 | _attestationObject['attStmt']; 19 | 20 | if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) { 21 | throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA); 22 | } 23 | 24 | if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) { 25 | throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA); 26 | } 27 | 28 | $this->_alg = $attStmt['alg']; 29 | $this->_signature = $attStmt['sig']->getBinaryString(); 30 | 31 | // certificate for validation 32 | if (\array_key_exists('x5c', $attStmt) && \is_array($attStmt['x5c']) && \count($attStmt['x5c']) > 0) { 33 | 34 | // The attestation certificate attestnCert MUST be the first element in the array 35 | $attestnCert = array_shift($attStmt['x5c']); 36 | 37 | if (!($attestnCert instanceof ByteBuffer)) { 38 | throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA); 39 | } 40 | 41 | $this->_x5c = $attestnCert->getBinaryString(); 42 | 43 | // certificate chain 44 | foreach ($attStmt['x5c'] as $chain) { 45 | if ($chain instanceof ByteBuffer) { 46 | $this->_x5c_chain[] = $chain->getBinaryString(); 47 | } 48 | } 49 | } 50 | } 51 | 52 | 53 | /* 54 | * returns the key certificate in PEM format 55 | * @return string|null 56 | */ 57 | public function getCertificatePem() { 58 | if (!$this->_x5c) { 59 | return null; 60 | } 61 | return $this->_createCertificatePem($this->_x5c); 62 | } 63 | 64 | /** 65 | * @param string $clientDataHash 66 | */ 67 | public function validateAttestation($clientDataHash) { 68 | if ($this->_x5c) { 69 | return $this->_validateOverX5c($clientDataHash); 70 | } else { 71 | return $this->_validateSelfAttestation($clientDataHash); 72 | } 73 | } 74 | 75 | /** 76 | * validates the certificate against root certificates 77 | * @param array $rootCas 78 | * @return boolean 79 | * @throws WebAuthnException 80 | */ 81 | public function validateRootCertificate($rootCas) { 82 | if (!$this->_x5c) { 83 | return false; 84 | } 85 | 86 | $chainC = $this->_createX5cChainFile(); 87 | if ($chainC) { 88 | $rootCas[] = $chainC; 89 | } 90 | 91 | $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas); 92 | if ($v === -1) { 93 | throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED); 94 | } 95 | return $v; 96 | } 97 | 98 | /** 99 | * validate if x5c is present 100 | * @param string $clientDataHash 101 | * @return bool 102 | * @throws WebAuthnException 103 | */ 104 | protected function _validateOverX5c($clientDataHash) { 105 | $publicKey = \openssl_pkey_get_public($this->getCertificatePem()); 106 | 107 | if ($publicKey === false) { 108 | throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY); 109 | } 110 | 111 | // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash 112 | // using the attestation public key in attestnCert with the algorithm specified in alg. 113 | $dataToVerify = $this->_authenticatorData->getBinary(); 114 | $dataToVerify .= $clientDataHash; 115 | 116 | $coseAlgorithm = $this->_getCoseAlgorithm($this->_alg); 117 | 118 | // check certificate 119 | return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1; 120 | } 121 | 122 | /** 123 | * validate if self attestation is in use 124 | * @param string $clientDataHash 125 | * @return bool 126 | */ 127 | protected function _validateSelfAttestation($clientDataHash) { 128 | // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash 129 | // using the credential public key with alg. 130 | $dataToVerify = $this->_authenticatorData->getBinary(); 131 | $dataToVerify .= $clientDataHash; 132 | 133 | $publicKey = $this->_authenticatorData->getPublicKeyPem(); 134 | 135 | // check certificate 136 | return \openssl_verify($dataToVerify, $this->_signature, $publicKey, OPENSSL_ALGO_SHA256) === 1; 137 | } 138 | } 139 | 140 | -------------------------------------------------------------------------------- /lib/WebAuthn/lbuchs/webauthn/src/Attestation/Format/U2f.php: -------------------------------------------------------------------------------- 1 | _attestationObject['attStmt']; 19 | 20 | if (\array_key_exists('alg', $attStmt) && $attStmt['alg'] !== $this->_alg) { 21 | throw new WebAuthnException('u2f only accepts algorithm -7 ("ES256"), but got ' . $attStmt['alg'], WebAuthnException::INVALID_DATA); 22 | } 23 | 24 | if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) { 25 | throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA); 26 | } 27 | 28 | if (!\array_key_exists('x5c', $attStmt) || !\is_array($attStmt['x5c']) || \count($attStmt['x5c']) !== 1) { 29 | throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA); 30 | } 31 | 32 | if (!\is_object($attStmt['x5c'][0]) || !($attStmt['x5c'][0] instanceof ByteBuffer)) { 33 | throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA); 34 | } 35 | 36 | $this->_signature = $attStmt['sig']->getBinaryString(); 37 | $this->_x5c = $attStmt['x5c'][0]->getBinaryString(); 38 | } 39 | 40 | 41 | /* 42 | * returns the key certificate in PEM format 43 | * @return string 44 | */ 45 | public function getCertificatePem() { 46 | $pem = '-----BEGIN CERTIFICATE-----' . "\n"; 47 | $pem .= \chunk_split(\base64_encode($this->_x5c), 64, "\n"); 48 | $pem .= '-----END CERTIFICATE-----' . "\n"; 49 | return $pem; 50 | } 51 | 52 | /** 53 | * @param string $clientDataHash 54 | */ 55 | public function validateAttestation($clientDataHash) { 56 | $publicKey = \openssl_pkey_get_public($this->getCertificatePem()); 57 | 58 | if ($publicKey === false) { 59 | throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY); 60 | } 61 | 62 | // Let verificationData be the concatenation of (0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F) 63 | $dataToVerify = "\x00"; 64 | $dataToVerify .= $this->_authenticatorData->getRpIdHash(); 65 | $dataToVerify .= $clientDataHash; 66 | $dataToVerify .= $this->_authenticatorData->getCredentialId(); 67 | $dataToVerify .= $this->_authenticatorData->getPublicKeyU2F(); 68 | 69 | $coseAlgorithm = $this->_getCoseAlgorithm($this->_alg); 70 | 71 | // check certificate 72 | return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1; 73 | } 74 | 75 | /** 76 | * validates the certificate against root certificates 77 | * @param array $rootCas 78 | * @return boolean 79 | * @throws WebAuthnException 80 | */ 81 | public function validateRootCertificate($rootCas) { 82 | $chainC = $this->_createX5cChainFile(); 83 | if ($chainC) { 84 | $rootCas[] = $chainC; 85 | } 86 | 87 | $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas); 88 | if ($v === -1) { 89 | throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED); 90 | } 91 | return $v; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/WebAuthn/lbuchs/webauthn/src/WebAuthnException.php: -------------------------------------------------------------------------------- 1 | $vendorDir . '/composer/InstalledVersions.php', 10 | ); 11 | -------------------------------------------------------------------------------- /lib/jwt/composer/autoload_namespaces.php: -------------------------------------------------------------------------------- 1 | array($vendorDir . '/firebase/php-jwt/src'), 10 | ); 11 | -------------------------------------------------------------------------------- /lib/jwt/composer/autoload_real.php: -------------------------------------------------------------------------------- 1 | register(true); 35 | 36 | return $loader; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/jwt/composer/autoload_static.php: -------------------------------------------------------------------------------- 1 | 11 | array ( 12 | 'Firebase\\JWT\\' => 13, 13 | ), 14 | ); 15 | 16 | public static $prefixDirsPsr4 = array ( 17 | 'Firebase\\JWT\\' => 18 | array ( 19 | 0 => __DIR__ . '/..' . '/firebase/php-jwt/src', 20 | ), 21 | ); 22 | 23 | public static $classMap = array ( 24 | 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 25 | ); 26 | 27 | public static function getInitializer(ClassLoader $loader) 28 | { 29 | return \Closure::bind(function () use ($loader) { 30 | $loader->prefixLengthsPsr4 = ComposerStaticInit3b0a7942a58597309a1a94ad80ddc804::$prefixLengthsPsr4; 31 | $loader->prefixDirsPsr4 = ComposerStaticInit3b0a7942a58597309a1a94ad80ddc804::$prefixDirsPsr4; 32 | $loader->classMap = ComposerStaticInit3b0a7942a58597309a1a94ad80ddc804::$classMap; 33 | 34 | }, null, ClassLoader::class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/jwt/composer/installed.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | { 4 | "name": "firebase/php-jwt", 5 | "version": "v6.8.1", 6 | "version_normalized": "6.8.1.0", 7 | "source": { 8 | "type": "git", 9 | "url": "https://github.com/firebase/php-jwt.git", 10 | "reference": "5dbc8959427416b8ee09a100d7a8588c00fb2e26" 11 | }, 12 | "dist": { 13 | "type": "zip", 14 | "url": "https://api.github.com/repos/firebase/php-jwt/zipball/5dbc8959427416b8ee09a100d7a8588c00fb2e26", 15 | "reference": "5dbc8959427416b8ee09a100d7a8588c00fb2e26", 16 | "shasum": "" 17 | }, 18 | "require": { 19 | "php": "^7.4||^8.0" 20 | }, 21 | "require-dev": { 22 | "guzzlehttp/guzzle": "^6.5||^7.4", 23 | "phpspec/prophecy-phpunit": "^2.0", 24 | "phpunit/phpunit": "^9.5", 25 | "psr/cache": "^1.0||^2.0", 26 | "psr/http-client": "^1.0", 27 | "psr/http-factory": "^1.0" 28 | }, 29 | "suggest": { 30 | "ext-sodium": "Support EdDSA (Ed25519) signatures", 31 | "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" 32 | }, 33 | "time": "2023-07-14T18:33:00+00:00", 34 | "type": "library", 35 | "installation-source": "dist", 36 | "autoload": { 37 | "psr-4": { 38 | "Firebase\\JWT\\": "src" 39 | } 40 | }, 41 | "notification-url": "https://packagist.org/downloads/", 42 | "license": [ 43 | "BSD-3-Clause" 44 | ], 45 | "authors": [ 46 | { 47 | "name": "Neuman Vong", 48 | "email": "neuman+pear@twilio.com", 49 | "role": "Developer" 50 | }, 51 | { 52 | "name": "Anant Narayanan", 53 | "email": "anant@php.net", 54 | "role": "Developer" 55 | } 56 | ], 57 | "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", 58 | "homepage": "https://github.com/firebase/php-jwt", 59 | "keywords": [ 60 | "jwt", 61 | "php" 62 | ], 63 | "support": { 64 | "issues": "https://github.com/firebase/php-jwt/issues", 65 | "source": "https://github.com/firebase/php-jwt/tree/v6.8.1" 66 | }, 67 | "install-path": "../firebase/php-jwt" 68 | } 69 | ], 70 | "dev": true, 71 | "dev-package-names": [] 72 | } 73 | -------------------------------------------------------------------------------- /lib/jwt/composer/installed.php: -------------------------------------------------------------------------------- 1 | array( 3 | 'name' => '__root__', 4 | 'pretty_version' => '1.0.0+no-version-set', 5 | 'version' => '1.0.0.0', 6 | 'reference' => NULL, 7 | 'type' => 'library', 8 | 'install_path' => __DIR__ . '/../../', 9 | 'aliases' => array(), 10 | 'dev' => true, 11 | ), 12 | 'versions' => array( 13 | '__root__' => array( 14 | 'pretty_version' => '1.0.0+no-version-set', 15 | 'version' => '1.0.0.0', 16 | 'reference' => NULL, 17 | 'type' => 'library', 18 | 'install_path' => __DIR__ . '/../../', 19 | 'aliases' => array(), 20 | 'dev_requirement' => false, 21 | ), 22 | 'firebase/php-jwt' => array( 23 | 'pretty_version' => 'v6.8.1', 24 | 'version' => '6.8.1.0', 25 | 'reference' => '5dbc8959427416b8ee09a100d7a8588c00fb2e26', 26 | 'type' => 'library', 27 | 'install_path' => __DIR__ . '/../firebase/php-jwt', 28 | 'aliases' => array(), 29 | 'dev_requirement' => false, 30 | ), 31 | ), 32 | ); 33 | -------------------------------------------------------------------------------- /lib/jwt/composer/platform_check.php: -------------------------------------------------------------------------------- 1 | = 70400)) { 8 | $issues[] = 'Your Composer dependencies require a PHP version ">= 7.4.0". You are running ' . PHP_VERSION . '.'; 9 | } 10 | 11 | if ($issues) { 12 | if (!headers_sent()) { 13 | header('HTTP/1.1 500 Internal Server Error'); 14 | } 15 | if (!ini_get('display_errors')) { 16 | if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { 17 | fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); 18 | } elseif (!headers_sent()) { 19 | echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; 20 | } 21 | } 22 | trigger_error( 23 | 'Composer detected issues in your platform: ' . implode(' ', $issues), 24 | E_USER_ERROR 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /lib/jwt/firebase/php-jwt/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Neuman Vong 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /lib/jwt/firebase/php-jwt/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebase/php-jwt", 3 | "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", 4 | "homepage": "https://github.com/firebase/php-jwt", 5 | "keywords": [ 6 | "php", 7 | "jwt" 8 | ], 9 | "authors": [ 10 | { 11 | "name": "Neuman Vong", 12 | "email": "neuman+pear@twilio.com", 13 | "role": "Developer" 14 | }, 15 | { 16 | "name": "Anant Narayanan", 17 | "email": "anant@php.net", 18 | "role": "Developer" 19 | } 20 | ], 21 | "license": "BSD-3-Clause", 22 | "require": { 23 | "php": "^7.4||^8.0" 24 | }, 25 | "suggest": { 26 | "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present", 27 | "ext-sodium": "Support EdDSA (Ed25519) signatures" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Firebase\\JWT\\": "src" 32 | } 33 | }, 34 | "require-dev": { 35 | "guzzlehttp/guzzle": "^6.5||^7.4", 36 | "phpspec/prophecy-phpunit": "^2.0", 37 | "phpunit/phpunit": "^9.5", 38 | "psr/cache": "^1.0||^2.0", 39 | "psr/http-client": "^1.0", 40 | "psr/http-factory": "^1.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/jwt/firebase/php-jwt/src/BeforeValidException.php: -------------------------------------------------------------------------------- 1 | keyMaterial = $keyMaterial; 44 | $this->algorithm = $algorithm; 45 | } 46 | 47 | /** 48 | * Return the algorithm valid for this key 49 | * 50 | * @return string 51 | */ 52 | public function getAlgorithm(): string 53 | { 54 | return $this->algorithm; 55 | } 56 | 57 | /** 58 | * @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate 59 | */ 60 | public function getKeyMaterial() 61 | { 62 | return $this->keyMaterial; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/jwt/firebase/php-jwt/src/SignatureInvalidException.php: -------------------------------------------------------------------------------- 1 | 2 |

    I WAS HERE

    3 |
    open source php attendance system
    4 | 5 | 6 |
    7 | OFFICIAL 8 |
    9 |
    28 | 29 |
    30 | SUPPORT 31 |
    32 | 43 | 44 |
    45 | EQUIPMENT 46 |
    47 |
    48 | 56 |
    57 | * These are affiliate links, I earn a small commission when you make a purchase from eBay. 58 |
    59 |
    60 | 61 |
    62 | SOCIALS 63 |
    64 |
    65 | 73 |
    74 | 75 |
    76 | CREDITS / BUILT WITH 77 |
    78 |
    79 | 89 |
    90 | -------------------------------------------------------------------------------- /pages/A-class-form.php: -------------------------------------------------------------------------------- 1 | autoCall("Classes", "get"); 6 | $_CORE->load("Courses"); 7 | $course = $_CORE->Courses->get($class["course_code"]); 8 | $teachers = $_CORE->Courses->getTeachers($class["course_code"]); 9 | } 10 | 11 | // (B) CLASS FORM ?> 12 |

    CLASS

    13 |
    14 |
    COURSE
    15 |
    Enter course code/name, and select from the autocomplete.
    16 |
    17 |
    18 | 20 | value=""> 21 | "> 22 | 23 |
    24 | "> 25 | * Click to change course. 26 | 27 |
    28 | 29 |
    CLASS
    30 |
    31 | "> 32 | 33 |
    34 | 35 | value="" 36 | min="" 37 | max=""> 38 | 39 |
    40 | 41 |
    42 | 50 | 51 |
    52 | 53 |
    54 | 55 | value=""> 56 | 57 |
    58 |
    59 | 60 | 63 | 66 |
    -------------------------------------------------------------------------------- /pages/A-class-list.php: -------------------------------------------------------------------------------- 1 | autoCall("Classes", "getAll"); 4 | 5 | // (B) DRAW CLASSES LIST 6 | if (is_array($classes)) { foreach ($classes as $id=>$c) { ?> 7 |
    8 |
    9 | []
    10 |
    11 |
    12 | 13 |
    14 | 31 |
    32 | load("Page"); 36 | $_CORE->Page->draw("classes.goToPage"); -------------------------------------------------------------------------------- /pages/A-classes.php: -------------------------------------------------------------------------------- 1 | [ 4 | ["s", HOST_ASSETS."CB-autocomplete.js", "defer"], 5 | ["s", HOST_ASSETS."csv.min.js", "defer"], 6 | ["s", HOST_ASSETS."A-import.js", "defer"], 7 | ["s", HOST_ASSETS."A-classes.js", "defer"], 8 | ["s", HOST_ASSETS."TA-attend.js", "defer"] 9 | ]]; 10 | 11 | // (B) HTML 12 | require PATH_PAGES . "TEMPLATE-top.php"; ?> 13 | 14 |

    MANAGE CLASSES

    15 | 16 | 17 |
    18 | 19 | 20 | 21 | 29 |
    30 | 31 | 32 |
    33 | -------------------------------------------------------------------------------- /pages/A-course-form.php: -------------------------------------------------------------------------------- 1 | autoCall("Courses", "get"); } 5 | 6 | // (B) COURSE FORM ?> 7 |

    COURSE

    8 |
    9 |
    BASIC COURSE INFORMATION
    10 |
    11 |
    12 | "> 13 | "> 14 | 15 |
    16 | 17 |
    18 | "> 19 | 20 |
    21 | 22 |
    23 | "> 24 | 25 |
    26 |
    27 | 28 |
    START-END DATE
    29 |
    30 |
    31 | "> 32 | 33 |
    34 | 35 |
    36 | "> 37 | 38 |
    39 |
    40 | 41 | 44 | 47 |
    -------------------------------------------------------------------------------- /pages/A-course-list.php: -------------------------------------------------------------------------------- 1 | autoCall("Courses", "getAll"); 4 | 5 | // (B) DRAW COURSES LIST 6 | if (is_array($courses)) { foreach ($courses as $code=>$c) { ?> 7 |
    8 |
    9 | []
    10 | TO
    11 | 12 |
    13 | 27 |
    28 | load("Page"); 32 | $_CORE->Page->draw("course.goToPage"); -------------------------------------------------------------------------------- /pages/A-course-user-list.php: -------------------------------------------------------------------------------- 1 | autoCall("Courses", "getUsers"); 4 | 5 | // (B) DRAW USERS LIST 6 | if (is_array($users)) { foreach ($users as $id=>$u) { ?> 7 |
    8 |
    9 |
    10 | | 11 | 12 |
    13 | 14 |
    15 | load("Page"); 19 | $_CORE->Page->draw("cuser.goToPage"); -------------------------------------------------------------------------------- /pages/A-course-user.php: -------------------------------------------------------------------------------- 1 | autoCall("Courses", "get"); ?> 4 | 5 |
    6 |
    7 |

    COURSE COHORT

    8 | 9 | [] 10 | 11 |
    12 | 13 |
    14 | 15 | 16 |
    17 | 18 | 19 | 20 |
    21 | 22 | 23 |
    -------------------------------------------------------------------------------- /pages/A-courses.php: -------------------------------------------------------------------------------- 1 | [ 4 | ["s", HOST_ASSETS."CB-autocomplete.js", "defer"], 5 | ["s", HOST_ASSETS."csv.min.js", "defer"], 6 | ["s", HOST_ASSETS."A-import.js", "defer"], 7 | ["s", HOST_ASSETS."A-course.js", "defer"], 8 | ["s", HOST_ASSETS."A-course-user.js", "defer"] 9 | ]]; 10 | 11 | // (B) HTML 12 | require PATH_PAGES . "TEMPLATE-top.php"; ?> 13 | 14 |

    MANAGE COURSES

    15 | 16 | 17 |
    18 | 19 | 20 | 21 | 29 |
    30 | 31 | 32 |
    33 | -------------------------------------------------------------------------------- /pages/A-home.php: -------------------------------------------------------------------------------- 1 | [ 4 | ["s", HOST_ASSETS."CB-autocomplete.js", "defer"], 5 | ["s", HOST_ASSETS."A-reports.js", "defer"] 6 | ]]; 7 | 8 | // (B) HTML 9 | require PATH_PAGES . "TEMPLATE-top.php"; ?> 10 |
    11 | 12 |
    13 |
    ATTENDANCE REPORT
    14 |
    15 | 16 | 17 |
    18 | 19 |
    20 |
    21 | -------------------------------------------------------------------------------- /pages/A-import.php: -------------------------------------------------------------------------------- 1 | 2 |

    IMPORT

    3 | 4 | 5 | 6 |
    7 |
    8 | 9 | 10 |
    11 | 12 |
    13 | 16 | 17 |
    18 | 21 |
    22 | 23 | 24 | 25 | 26 | $c"; } ?> 27 | 28 | 29 | 30 |
    Status
    31 | 32 | 33 | 36 | -------------------------------------------------------------------------------- /pages/A-settings.php: -------------------------------------------------------------------------------- 1 | Settings->getAll(); 4 | 5 | // (B) SETTINGS LIST 6 | $_PMETA = ["load" => [["s", HOST_ASSETS."A-settings.js", "defer"]]]; 7 | require PATH_PAGES . "TEMPLATE-top.php"; ?> 8 |

    SYSTEM SETTINGS

    9 | 10 | 11 |
    12 | " value=""> 14 | 15 |
    16 | 17 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /pages/A-users-form.php: -------------------------------------------------------------------------------- 1 | autoCall("Users", "get"); } 5 | 6 | // (B) USER FORM ?> 7 |

    USER

    8 | 9 |
    10 | "> 11 |
    12 | "> 13 | 14 |
    15 | 16 |
    17 | "> 18 | 19 |
    20 | 21 |
    22 | 30 | 31 |
    32 | 33 |
    34 | 35 | 36 |
    37 |
    * At least 8 alphanumeric characters.
    38 |
    39 | 40 | 43 | 46 | -------------------------------------------------------------------------------- /pages/A-users-list.php: -------------------------------------------------------------------------------- 1 | autoCall("Users", "getAll"); 4 | 5 | // (B) DRAW USERS LIST 6 | if (is_array($users)) { foreach ($users as $id=>$u) { ?> 7 |
    8 |
    9 |
    10 |
    11 | 12 |
    13 | 27 |
    28 | load("Page"); 32 | $_CORE->Page->draw("usr.goToPage"); -------------------------------------------------------------------------------- /pages/A-users-nfc.php: -------------------------------------------------------------------------------- 1 | autoCall("Users", "get"); 5 | if (!is_array($user)) { exit("Invalid user"); } 6 | ?> 7 |

    USER NFC LOGIN TOKEN

    8 | 9 | 10 |
    CREATE NEW TOKEN
    11 |
    12 | 15 |
    16 | * A user can only have one login token, creating a new token will nullify the previous one. 17 |
    18 |
    19 | 20 | 21 |
    NULLIFY NFC TOKEN
    22 |
    23 | 27 |
    28 | * The user's NFC login token will be nullified, but the login email/password remains unaffected. 29 |
    30 |
    31 | 32 | -------------------------------------------------------------------------------- /pages/A-users.php: -------------------------------------------------------------------------------- 1 | [ 4 | ["s", HOST_ASSETS."CB-autocomplete.js", "defer"], 5 | ["s", HOST_ASSETS."csv.min.js", "defer"], 6 | ["s", HOST_ASSETS."A-import.js", "defer"], 7 | ["s", HOST_ASSETS."PAGE-nfc.js", "defer"], 8 | ["s", HOST_ASSETS."A-users-nfc.js", "defer"], 9 | ["s", HOST_ASSETS."A-users.js", "defer"] 10 | ]]; 11 | 12 | // (B) HTML 13 | require PATH_PAGES . "TEMPLATE-top.php"; ?> 14 | 15 |

    MANAGE USERS

    16 | 17 | 18 | 19 | 20 | 21 | 22 | 30 | 31 | 32 | 33 |
    34 | -------------------------------------------------------------------------------- /pages/MAIL-forgot-a.php: -------------------------------------------------------------------------------- 1 | 2 | Click here to reset your password. 3 | -------------------------------------------------------------------------------- /pages/MAIL-forgot-b.php: -------------------------------------------------------------------------------- 1 | 2 | Your new password is 3 | -------------------------------------------------------------------------------- /pages/PAGE-404.php: -------------------------------------------------------------------------------- 1 | 2 |

    NOT FOUND

    3 |

    It may have been abducted by aliens.

    4 | -------------------------------------------------------------------------------- /pages/PAGE-empty.php: -------------------------------------------------------------------------------- 1 | "TITLE", 4 | "desc" => "OPTIONAL DESCRIPTION", 5 | /* OPTIONAL - LOAD EXTRA SCRIPTS 6 | "load" => [ 7 | ["s", HOST_ASSETS."A.js"], 8 | ["s", HOST_ASSETS."B.js", "defer"], 9 | ["s", HOST_ASSETS."C.js", "defer async"], 10 | ["c", HOST_ASSETS."D.css"], 11 | ] 12 | */ 13 | ]; 14 | require PATH_PAGES . "TEMPLATE-top.php"; ?> 15 | YOUR CONTENT HERE 16 | -------------------------------------------------------------------------------- /pages/PAGE-forgot.php: -------------------------------------------------------------------------------- 1 | redirect(); } 4 | 5 | // (B) PART 1 - ENTER EMAIL 6 | if (!isset($_GET["i"]) && !isset($_GET["h"])) { 7 | $_PMETA = ["load" => [["s", HOST_ASSETS."PAGE-forgot.js", "defer"]]]; 8 | require PATH_PAGES . "TEMPLATE-top.php"; ?> 9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    15 | 16 |

    FORGOT PASSWORD

    17 |
    No worries. Enter your email, and we will send you a password reset link.
    18 | 19 |
    20 | 21 | 22 |
    23 | 24 | 27 | 28 |
    29 | Back To Login 30 |
    31 | 32 |
    33 |
    34 |
    35 |
    36 | load("Forgot"); 41 | $pass = $_CORE->Forgot->reset($_GET["i"], $_GET["h"]); 42 | require PATH_PAGES . "TEMPLATE-top.php"; ?> 43 |
    44 |
    45 |
    46 |
    47 |
    48 |
    49 | 50 |

    51 |
    error; } 54 | ?>
    55 | 56 |
    57 | Back To Login 58 |
    59 |
    60 |
    61 |
    62 |
    63 |
    64 | -------------------------------------------------------------------------------- /pages/PAGE-home.php: -------------------------------------------------------------------------------- 1 | redirect($_SESSION["user"]["user_level"]); -------------------------------------------------------------------------------- /pages/PAGE-login.php: -------------------------------------------------------------------------------- 1 | redirect(); } 4 | 5 | // (B) PAGE META & SCRIPTS 6 | $_PMETA = ["load" => [ 7 | ["s", HOST_ASSETS."PAGE-wa-helper.js", "defer"], 8 | ["s", HOST_ASSETS."PAGE-login-wa.js", "defer"], 9 | ["s", HOST_ASSETS."PAGE-nfc.js", "defer"], 10 | ["s", HOST_ASSETS."PAGE-login-nfc.js", "defer"], 11 | ["s", HOST_ASSETS."PAGE-login.js", "defer"] 12 | ]]; 13 | 14 | // (C) HTML PAGE 15 | require PATH_PAGES . "TEMPLATE-top.php"; ?> 16 | error!="") { ?> 17 | 18 |
    error?>
    19 | 20 | 21 | 22 |
    23 |
    24 |
    25 |
    26 |
    27 |
    28 |
    29 | 30 | 31 |

    PLEASE SIGN IN

    32 | 33 |
    34 | 35 | 36 |
    37 | 38 |
    39 | 40 | 41 |
    42 | 43 | 46 | 47 | 48 | 51 | 54 | 55 | 56 | 57 | 58 | 59 |
    60 | Forgot Password 61 |
    62 |
    63 |
    64 |
    65 |
    66 |
    67 | -------------------------------------------------------------------------------- /pages/PAGE-myaccount.php: -------------------------------------------------------------------------------- 1 | redirect(); } 4 | 5 | // (B) PAGE META & SCRIPTS 6 | $_PMETA = ["load" => [ 7 | ["s", HOST_ASSETS."PAGE-myaccount.js", "defer"] 8 | ]]; 9 | 10 | // (C) HTML PAGE 11 | require PATH_PAGES . "TEMPLATE-top.php"; ?> 12 |
    13 |
    14 |
    15 |
    16 |
    17 | 18 |

    MY ACCOUNT

    19 |
    20 | "> 21 | 22 |
    23 | 24 |
    25 | "> 26 | 27 |
    28 | 29 |
    30 | 31 | 32 |
    33 | 34 |
    35 | 36 | 37 |
    38 |
    * At least 8 alphanumeric characters.
    39 | 40 |
    41 | 42 | 43 |
    44 | 45 | 48 | 49 |
    50 |
    51 |
    52 | redirect(); } 4 | 5 | // (B) PAGE META & SCRIPTS 6 | $_PMETA = ["load" => [ 7 | ["s", HOST_ASSETS."PAGE-wa-helper.js", "defer"], 8 | ["s", HOST_ASSETS."PAGE-wa.js", "defer"] 9 | ]]; 10 | 11 | // (C) HAS REGISTERED 12 | $_CORE->load("Users"); 13 | $regged = is_array($_CORE->Users->hashGet($_SESSION["user"]["user_id"], "PL")); 14 | 15 | // (D) HTML PAGE 16 | require PATH_PAGES . "TEMPLATE-top.php"; ?> 17 |
    18 |
    19 |
    20 |
    21 |
    22 |
    23 | 24 | 25 |

    PASSWORDLESS LOGIN

    26 |
    27 | Login with fingerprint, face recognition, pin code, or USB keypass. 28 | Take note - This can only be registered to one device and one mode of passwordless login. 29 |
    30 | 31 | 32 | 36 | 40 | 41 | 42 |
    43 |
    44 |
    45 |
    46 |
    47 |
    48 | -------------------------------------------------------------------------------- /pages/REPORT-loader.php: -------------------------------------------------------------------------------- 1 | ucheck("A"); 4 | 5 | // (B) LOAD REPORT 6 | $_CORE->Route->path = explode("/", $_CORE->Route->path); 7 | if (count($_CORE->Route->path)!=3) { exit("Invalid report"); } 8 | $_CORE->autoCall("Report", $_CORE->Route->path[1]); -------------------------------------------------------------------------------- /pages/T-class-list.php: -------------------------------------------------------------------------------- 1 | autoCall("Classes", "getByTeacher"); 5 | 6 | // (B) DRAW CLASSES LIST 7 | if (is_array($classes)) { foreach ($classes as $id=>$c) { ?> 8 |
    9 |
    10 |
    11 | []
    12 | 13 |
    14 | 25 |
    26 | 27 |
    No classes found.
    28 | load("Page"); 32 | $_CORE->Page->draw("classes.goToPage"); -------------------------------------------------------------------------------- /pages/T-home.php: -------------------------------------------------------------------------------- 1 | [ 4 | ["s", HOST_ASSETS."T-classes.js", "defer"], 5 | ["s", HOST_ASSETS."TA-attend.js", "defer"] 6 | ]]; 7 | 8 | // (B) HTML 9 | require PATH_PAGES . "TEMPLATE-top.php"; ?> 10 | 11 |

    MY CLASSES

    12 | 13 | 14 | 15 | 21 | "> 22 | 23 | 24 | 25 | 26 |
    27 | -------------------------------------------------------------------------------- /pages/TA-attend-list.php: -------------------------------------------------------------------------------- 1 | autoCall("Attend", "getStudents"); 4 | 5 | // (B) DRAW STUDENTS LIST 6 | if (is_array($students)) { foreach ($students as $id=>$s) { ?> 7 |
    8 |
    9 | | 10 | 11 | "> 13 |
    14 | 18 |
    19 | 20 |
    21 | 24 |
    25 | load("Courses"); 4 | $class = $_CORE->autoCall("Classes", "get"); 5 | $course = $_CORE->Courses->get($class["course_code"]); ?> 6 | 7 |
    8 |
    9 |

    []

    10 | 11 |
    12 | 13 |
    14 | 15 | 16 |
    17 | * Blue check is "present", red cross is "absent".
    18 | * Remember to "save attendance" below. 19 |
    20 | 21 | 22 |
    -------------------------------------------------------------------------------- /pages/TA-classqr.php: -------------------------------------------------------------------------------- 1 | ucheck(["A", "T"]); 4 | 5 | // (B) GET CLASS 6 | $_CORE->load("Classes"); 7 | $class = $_CORE->Classes->get($_GET["c"]); 8 | if (!is_array($class)) { exit("Invalid request"); } 9 | 10 | // (C) HTML PAGE ?> 11 | 12 | 13 | 14 | QR Code Generator 15 | 21 | 22 | 36 | 37 | 38 |
    39 |
    40 |
    []
    41 |
    42 |
    43 | 44 | -------------------------------------------------------------------------------- /pages/TEMPLATE-A-menu.php: -------------------------------------------------------------------------------- 1 | 9 | 22 | -------------------------------------------------------------------------------- /pages/TEMPLATE-T-menu.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/TEMPLATE-U-menu.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/TEMPLATE-bottom.php: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 |
    5 |
    6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /pages/U-class-list.php: -------------------------------------------------------------------------------- 1 | autoCall("Classes", "getByStudent"); 5 | 6 | // (B) DRAW CLASSES LIST 7 | if (is_array($classes)) { foreach ($classes as $id=>$c) { ?> 8 |
    9 |
    10 |
    11 | []
    12 | 13 |
    14 |
    "> 15 | "> 16 |
    17 |
    18 | load("Page"); 22 | $_CORE->Page->draw("classes.goToPage"); -------------------------------------------------------------------------------- /pages/U-home.php: -------------------------------------------------------------------------------- 1 | [ 4 | ["s", HOST_ASSETS."U-classes.js", "defer"] 5 | ]]; 6 | 7 | // (B) HTML 8 | require PATH_PAGES . "TEMPLATE-top.php"; ?> 9 | 10 |

    MY CLASSES

    11 | 12 | 13 | 14 | 20 | "> 21 | 22 | 23 | 24 | 25 |
    26 | -------------------------------------------------------------------------------- /pages/U-qr.php: -------------------------------------------------------------------------------- 1 | [ 4 | ["s", HOST_ASSETS."html5-qrcode.min.js", "defer"], 5 | ["s", HOST_ASSETS."U-qr.js", "defer"] 6 | ]]; 7 | 8 | // (B) HTML 9 | require PATH_PAGES . "TEMPLATE-top.php"; ?> 10 | 11 |

    TAKE ATTENDANCE

    12 |
    13 | Scan the QR code that the teacher has provided. 14 |
    15 | 16 | 17 |
    18 | -------------------------------------------------------------------------------- /pages/USR-check.php: -------------------------------------------------------------------------------- 1 | Route->path = explode("/", rtrim($_CORE->Route->path, "/\\")); 5 | if ($_CORE->Route->path[0]=="TA") { 6 | $access = in_array($_SESSION["user"]["user_level"], ["T","A"]); 7 | } else { 8 | $access = $_SESSION["user"]["user_level"] == $_CORE->Route->path[0]; 9 | } 10 | 11 | // (B) NO ACCESS 12 | if (!$access) { 13 | if (isset($_POST["ajax"])) { http_response_code(405); exit(); } 14 | else { $_CORE->redirect(); } 15 | } 16 | 17 | // (C) RESOLVE PAGE 18 | if (count($_CORE->Route->path)==1) { $_CORE->Route->path[1] = "home"; } 19 | $_CORE->Route->load(implode("-", $_CORE->Route->path) . ".php"); --------------------------------------------------------------------------------