├── .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 = "No teachers assigned! "; }
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 "";
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$tag>";
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 |
74 |
75 |
76 | CREDITS / BUILT WITH
77 |
78 |
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 | =$edit?"EDIT":"ADD"?> CLASS
13 |
--------------------------------------------------------------------------------
/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 | [=$c["course_code"]?>] =$c["course_name"]?>
10 | =$c["cd"]?>
11 | =$c["user_name"]?>
12 | =$c["class_desc"]?>
13 |
14 |
15 |
16 |
30 |
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 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/pages/A-course-form.php:
--------------------------------------------------------------------------------
1 | autoCall("Courses", "get"); }
5 |
6 | // (B) COURSE FORM ?>
7 | =$edit?"EDIT":"ADD"?> COURSE
8 |
--------------------------------------------------------------------------------
/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 | [=$c["course_code"]?>] =$c["course_name"]?>
10 | =$c["sd"]?> TO =$c["ed"]?>
11 | =$c["course_desc"]?>
12 |
13 |
14 |
15 |
26 |
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 | =$u["user_name"]?>
10 | =USR_LVL[$u["user_level"]]?> |
11 | =$u["user_email"]?>
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 | [=$course["course_code"]?>] =$course["course_name"]?>
10 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
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 |
21 |
--------------------------------------------------------------------------------
/pages/A-import.php:
--------------------------------------------------------------------------------
1 |
2 | IMPORT =$_POST["name"]?>
3 |
4 |
5 |
6 |
7 |
8 |
9 | Select CSV File
10 |
11 |
12 |
13 |
16 | CSV Columns
17 |
18 |
21 |
22 |
23 |
24 |
25 |
26 | $c"; } ?>
27 | Status
28 |
29 |
30 |
31 |
32 |
33 | )" class="my-1 me-1 btn btn-danger d-flex-inline">
34 | Back
35 |
36 |
37 | Start
38 |
--------------------------------------------------------------------------------
/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 |
22 |
--------------------------------------------------------------------------------
/pages/A-users-form.php:
--------------------------------------------------------------------------------
1 | autoCall("Users", "get"); }
5 |
6 | // (B) USER FORM ?>
7 | =$edit?"EDIT":"ADD"?> USER
8 |
--------------------------------------------------------------------------------
/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 | =$u["user_name"]?>
10 | =USR_LVL[$u["user_level"]]?>
11 | =$u["user_email"]?>
12 |
13 |
14 |
15 |
26 |
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 |
)">
13 | Initializing
14 |
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 |
)"=$user["hash_code"]==""?" disabled":""?>>
25 | Nullify Login Token
26 |
27 |
28 | * The user's NFC login token will be nullified, but the login email/password remains unaffected.
29 |
30 |
31 |
32 |
33 | Back
34 |
--------------------------------------------------------------------------------
/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 |
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 =$password?>
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 |
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 |
=$pass ? "DONE!" : "OH NO..."?>
51 |
error; }
54 | ?>
55 |
56 |
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 | =$_CORE->error?>
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
55 |
56 |
57 |
58 |
59 |
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 |
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 |
>
34 | Unregister
35 |
36 |
38 | Register
39 |
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 | =$c["cd"]?>
11 | [=$c["course_code"]?>] =$c["course_name"]?>
12 | =$c["class_desc"]?>
13 |
14 |
15 |
16 |
24 |
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 |
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 | =$s["user_name"]?> | =$s["user_email"]?>
10 |
11 | ">
13 |
14 |
16 | icon icon-=$s["a_status"]==1?"checkmark":"cross"?>">
17 |
18 |
19 |
20 |
21 |
22 | Save
23 |
24 |
25 | load("Courses");
4 | $class = $_CORE->autoCall("Classes", "get");
5 | $course = $_CORE->Courses->get($class["course_code"]); ?>
6 |
7 |
8 |
9 |
[=$course["course_code"]?>] =$course["course_name"]?>
10 | =$class["cd"]?>
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 |
[=$class["course_code"]?>] =$class["course_name"]?>
41 |
=$class["cd"]?>
42 |
43 |
44 |
--------------------------------------------------------------------------------
/pages/TEMPLATE-A-menu.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Reports
6 |
7 |
8 |
9 |
10 | Institute
11 |
12 | Users
13 |
14 |
15 | Courses
16 |
17 |
18 | Classes
19 |
20 |
21 |
22 |
23 | System
24 |
25 | Settings
26 |
27 |
28 | Help & About
29 |
30 |
31 |
--------------------------------------------------------------------------------
/pages/TEMPLATE-T-menu.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Classes
6 |
7 |
8 |
--------------------------------------------------------------------------------
/pages/TEMPLATE-U-menu.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Classes
6 |
7 |
8 | Scan
9 |
10 |
11 |
--------------------------------------------------------------------------------
/pages/TEMPLATE-bottom.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |