├── Database - tasksdb └── tasksdb database - SQL Dump File - PhpMyAdmin Export.sql ├── Postman Collection of API Endpoints └── Plain PHP REST API with Token-based Authentication and Image Uploading.postman_collection.json ├── Project Screenshots └── REST-API-Constraints.png ├── README.md └── v1 ├── .htaccess ├── controller ├── db.php ├── dbtest.php ├── images.php ├── sessions.php ├── task.php └── users.php ├── model ├── Response.php ├── Task.php └── image.php └── taskimages ├── 1 ├── cat.jpg └── updated_filename.jpg ├── 7 └── study.jpg └── 8 ├── homework-UPDATED.jpg └── sweeping.jpg /Database - tasksdb/tasksdb database - SQL Dump File - PhpMyAdmin Export.sql: -------------------------------------------------------------------------------- 1 | -- phpMyAdmin SQL Dump 2 | -- version 5.2.1 3 | -- https://www.phpmyadmin.net/ 4 | -- 5 | -- Host: localhost 6 | -- Generation Time: Jul 07, 2023 at 10:01 PM 7 | -- Server version: 8.0.28 8 | -- PHP Version: 8.1.4 9 | 10 | SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; 11 | START TRANSACTION; 12 | SET time_zone = "+00:00"; 13 | 14 | 15 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 16 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 17 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 18 | /*!40101 SET NAMES utf8mb4 */; 19 | 20 | -- 21 | -- Database: `tasksdb` 22 | -- 23 | 24 | -- -------------------------------------------------------- 25 | 26 | -- 27 | -- Table structure for table `tblimages` 28 | -- 29 | 30 | CREATE TABLE `tblimages` ( 31 | `id` bigint NOT NULL COMMENT 'Image ID Number - Primary Key', 32 | `title` varchar(255) NOT NULL COMMENT 'Image Title', 33 | `filename` varchar(30) NOT NULL COMMENT 'Image Filename', 34 | `mimetype` varchar(255) NOT NULL COMMENT 'Image Mime Type - e.g. image/png', 35 | `taskid` bigint NOT NULL COMMENT 'Task ID Number - Foreign Key' 36 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT='Table to store task images'; 37 | 38 | -- 39 | -- Dumping data for table `tblimages` 40 | -- 41 | 42 | INSERT INTO `tblimages` (`id`, `title`, `filename`, `mimetype`, `taskid`) VALUES 43 | (9, 'test updating the image title', 'updated_filename.jpg', 'image/jpeg', 1), 44 | (10, 'test image title', 'cat.jpg', 'image/jpeg', 1), 45 | (14, 'Sweeping the floor', 'sweeping.jpg', 'image/jpeg', 8), 46 | (15, 'Do the homework UPDATED', 'homework-UPDATED.jpg', 'image/jpeg', 8), 47 | (16, 'Study some topics', 'study.jpg', 'image/jpeg', 7); 48 | 49 | -- -------------------------------------------------------- 50 | 51 | -- 52 | -- Table structure for table `tblsessions` 53 | -- 54 | 55 | CREATE TABLE `tblsessions` ( 56 | `id` bigint NOT NULL COMMENT 'Session ID', 57 | `userid` bigint NOT NULL COMMENT 'User ID (foreign key to `id` column in `users` table)', 58 | `accesstoken` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT 'Access Token', 59 | `accesstokenexpiry` datetime NOT NULL COMMENT 'Access Token Expiry Date/Time', 60 | `refreshtoken` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT 'Refresh Token', 61 | `refreshtokenexpiry` datetime NOT NULL COMMENT 'Refresh Token Expiry Date/Time' 62 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT='Sessions Table'; 63 | 64 | -- 65 | -- Dumping data for table `tblsessions` 66 | -- 67 | 68 | INSERT INTO `tblsessions` (`id`, `userid`, `accesstoken`, `accesstokenexpiry`, `refreshtoken`, `refreshtokenexpiry`) VALUES 69 | (13, 4, 'M2FhZjVhZGM0NGNiMjE0YmYxMTYzZjhkMDcwMTA3ZDI3MzFiZDYxMGUwMmFhZGNmMTY1NzU1MjI2NA==', '2022-07-11 17:31:04', 'YWMzZWFlMGUxZTdjNzZlNDIxMGYyY2FiMWZhMzhiN2YwY2IzZGZiYTExYmQ5YjRhMTY1NzU1MjI2NA==', '2022-07-25 17:11:04'), 70 | (14, 4, 'MTZiYTVkZDhiNmE3Zjc4ZDUwZGIxY2IxYmEzY2NlMDA1ODBiZmRkYzk0OTA2OGZmMTY1NzU2Mjk3OA==', '2022-07-11 20:29:38', 'NzkzNDliODYzMWNkYzM2ZjgxNzk3YTVlMDI2ZGRmZWIwNjNmYzMwYTY5ZDVkOWM1MTY1NzU2Mjk3OA==', '2022-07-25 20:09:38'), 71 | (15, 5, 'YTgwYTQ3NGNjYzg2MjU3MmQwODMwYWRkNjUxOGI1NjgzZGQwZjA1OTlhYTEzYzc4MTY1NzU2MTY4Nw==', '2022-07-11 20:08:07', 'ZmU2OGZhYzcxNWU2Mjc2Mzk2YTE1YmZkNmZjMDRhN2QwNDliYmU2OTIwNmViN2Q1MTY1NzU2MTY4Nw==', '2022-07-25 19:48:07'), 72 | (16, 4, 'ZGU3ZWJiZDQyZDBiMjc0ODdmZjRlZTc3NDQzMDJmOWQ5YjAzMDM2YWYxMjdjNzFhMTY1Nzk3NjE0Mg==', '2022-07-16 15:15:42', 'YzM5ZTA5Y2I5MjkzYjVlMmU4MDBiNGQ1ZDU1OTg0Yzc0OGY2Y2ViNDI0ZDYxZTk3MTY1Nzk3NjE0Mg==', '2022-07-30 14:55:42'), 73 | (17, 4, 'MjkxMTBiNTkzZTMwYWNhOGI0OTVhMmQ1MjkyM2Q2ZTJhZmEyNjlkNGY1YzMwMGU2MTY1Nzk5NjQ1Mw==', '2022-07-16 20:54:13', 'YjViNGJmMzUwZjM2ZTE1NjE1YWE3MTA0YTYyZjcwZTU2ZWVmMDRlZmI4NjdlYWI1MTY1Nzk5NjQ1Mw==', '2022-07-30 20:34:13'), 74 | (18, 4, 'OTVjNjI1Mzg3ZjExNzg0OTUyNGZiZTM4MzAxYWM2YzdhNzc4YzhlNjY2ZTBmZjljMTY1Nzk5NjY4NA==', '2022-07-16 20:58:04', 'YmRhZmY3NDg5MjBhN2Y3Y2M1MTljZmM0YWI4NDg1NWQ3ZTRiMjk3NWJmMjMxNGFiMTY1Nzk5NjY4NA==', '2022-07-30 20:38:04'), 75 | (19, 4, 'ZjdhMTNkMTE1YjIxMDI4NGM3MDBjYWI1ZjIwMmUxZGFmYjEyOTk0ODY4NzU0Yzc1MTY1Nzk5Njg0Nw==', '2022-07-16 21:00:47', 'ZGRjNWQ4YzQ5ZDNkYWRkNzVhZjgyMTdiYjk2MjhmYjE3NDMzN2Y0NTkyMGVjNjM5MTY1Nzk5Njg0Nw==', '2022-07-30 20:40:47'), 76 | (20, 4, 'MzI1YzE1NDg4ODNhNTRmM2NkNTRlMmVjMjY2NWNkMTdlYjc3ZGI4ODExNmU5M2QxMTY1ODA5ODY4OQ==', '2022-07-18 01:18:09', 'ZmExNjY2Yzg2YTcwNTE4YzM4YjdkOTZkYTgzMzgwNTY4MTI0MDJhMzgzZGY5YjBhMTY1ODA5ODY4OQ==', '2022-08-01 00:58:09'), 77 | (21, 4, 'ZmQzMDk2MTBmNDAyYWU5YjE0NzY4MGJlZjJkN2Y2NDgzNTZmNzhiMTg0ZGY3N2JmMTY1ODEwMzg3NA==', '2022-07-18 02:44:34', 'YTY5NWYyYmY5ZWZiYWI5NWM5MTIxMDEzZWU2MzcwYjUxYjkwN2IzNTFmMDg3YzA5MTY1ODEwMzg3NA==', '2022-08-01 02:24:34'), 78 | (22, 4, 'ZjRkOGI0NmI1M2IzZDk1ZjU4MDI0YTc5Yzc0NmIxNmEyMTMyNzUxOTljN2QzZGZhMTY1ODEwMzkyNg==', '2022-07-18 01:45:26', 'NjI2ZjQ3ODRjYmU0ZDVjMDE4MzVkNWY4ZWFhMWJjYzZiYmM4YWJmNTFkNDVhMTMwMTY1ODEwMzkyNg==', '2022-08-01 02:25:26'), 79 | (23, 4, 'NGQxMzlkZTZlYmM3NGM1NDU2NDA1ZGQyNjBkZjBmMDQxZmNkMzVmOWE1MjdlZjcyMTY1ODE4OTM1MA==', '2022-07-19 02:29:10', 'YzIwMDhmM2U2OTQ4ODJjNTVhYTIzZmY3NDMyMTczYTZkZjFlNGQyNzQxMmQ1Yzc3MTY1ODE4OTM1MA==', '2022-08-02 02:09:10'), 80 | (24, 4, 'Zjk3NTJmZTc4ZWQ4YmZmZDI1MDc1OWNkMDNmYjNiMzFhNzNlOTY5OWIwNmY4Yzk5MTY1ODM0OTM3OA==', '2022-07-20 22:56:19', 'NTE4YWZiZDkwMDY3N2Q2MWFlYWNmNzRlOGRjZWU5NDczMjNmYmViNGJmYzI0ZTRiMTY1ODM0OTM3OA==', '2022-08-03 22:36:19'), 81 | (25, 4, 'N2Y5ZDRiM2UzMmRmY2UzMjFhZTU3YThkZjYzMzdhNDk3ZDYyNGIyOTljMmJjM2UxMTY1ODUzNjY3MQ==', '2022-07-23 02:57:51', 'YWRiOGQwMjk5ZTIyYTFiMWYyYTBjMTA4OWY3NGVlOGZmN2I4MTMwNjVhOTg4NzczMTY1ODUzNjY3MQ==', '2022-08-06 02:37:51'), 82 | (26, 4, 'OTgyNDczOWNjYzE0ZjljNjUzYjQxMjIyMGRmNWIwY2E0NjJjMzE4NDdjYzVhMzY4MTY1ODU0NzkyMw==', '2022-07-23 06:05:23', 'NzVmMzAxMjM2N2YyNTkzYjQzOTJlNGExYzY3NmJhOTczOGVmZmEyNjc1NjE5MTVjMTY1ODU0NzkyMw==', '2022-08-06 05:45:23'), 83 | (27, 4, 'ODI5NWZmNjJhY2JjOWJiYTk1MGNjOTM2ZjRlMjA0YWU2Zjk1NGNiYTg2N2IzOTU2MTY1ODU1NTI4OA==', '2022-07-23 08:08:08', 'OGIyMmZmZDJlNDljNDhjOTIxYWVmZGU2YWIzNWYwYTdhYTgxZmMwNWE1MWRjYzk3MTY1ODU1NTI4OA==', '2022-08-06 07:48:08'), 84 | (28, 4, 'MTFiYmE3NjUwZGFkMjdhMjlmNmYwZmU3MTM4NTk2OGQ2NWI3NzhhODBmNzQ2OWUyMTY1ODU2NDE1MQ==', '2022-07-23 10:35:51', 'ZDQ2N2IyYzI2M2YzN2U2NTBiMmIxM2EyN2ZkZTUyZTM0MWRiM2U0ZGFmNDM4N2FmMTY1ODU2NDE1MQ==', '2022-08-06 10:15:51'), 85 | (29, 4, 'MjYwYTRjOGNmNDQzZDI3MjkwZDAwMTIzNGY3ZmMzMjhkNzU1MmVkMTcwZmEwODk4MTY4ODQyNzkxOQ==', '2023-07-04 03:05:19', 'YzYwZjNlN2I2ZDRlNWY1MThkNjZjZTJkMmM5YjUzOWVlZTJhOTdhZjRmYjY1NDRjMTY4ODQyNzkxOQ==', '2023-07-18 02:45:19'), 86 | (30, 4, 'YWRjOWNiNWM2NTk3YzI5NzFkODc3MGJmMmM4ZDAwNWYyZTBiNTZmNjE1Y2UzMmZmMTY4ODQyODQ2NA==', '2023-07-04 03:14:24', 'YzBhMWFiYmUwZGIyZmZkMjQ3NTRiYWQyMjMzNmU2NWY5NzUyNjgxYWYwZTUyOGQ5MTY4ODQyODQ2NA==', '2023-07-18 02:54:24'), 87 | (31, 6, 'Y2Q4NTAzZjFkZWZhOGUyZThiOTJlNGEwMGJmOWJlYzgxNDI4YzI2MjlkNDc4NmMxMTY4ODQzMDMxMA==', '2023-07-04 03:45:10', 'NDk1YjM1YTYxM2Q0OWRjYTVmODk5NThhOTg5ZDk2NjRjNjgzNGM3N2Y1YmNhNGVmMTY4ODQzMDMxMA==', '2023-07-18 03:25:10'), 88 | (32, 6, 'NWI2MWNmZjc2YTgyOTkyZjM4NjQ3OTkwZmM4ODRlZTk5N2IxZmI4MmNkNTgxMWRhMTY4ODQ3MDU3MA==', '2023-07-04 14:56:10', 'Y2ViMzRmNGJiMTY5NzI4YTliNjIwZmM0NTM3NWI5OWUwY2EyMzNkNjMwMmEzYzUxMTY4ODQ3MDU3MA==', '2023-07-18 14:36:10'), 89 | (35, 6, 'ZThlZGFhZmM3ZWQ0MDQzZmE1ZTE5YzBmMDJiZGUxNjUxMjIxNzQ0N2I1YTU3OTM3MTY4ODc1ODk4OQ==', '2023-07-07 23:03:09', 'ODlkNTE2ZmU4NzMyODE5Nzc1MjE0ZDkxMDliNzdjOTJkMzlhYzQwYjkyNzM5MTI0MTY4ODc1ODk4OQ==', '2023-07-21 22:43:09'); 90 | 91 | -- -------------------------------------------------------- 92 | 93 | -- 94 | -- Table structure for table `tbltasks` 95 | -- 96 | 97 | CREATE TABLE `tbltasks` ( 98 | `id` bigint NOT NULL COMMENT 'Task ID - Primary Key', 99 | `title` varchar(255) NOT NULL COMMENT 'Task Title', 100 | `description` mediumtext COMMENT 'Task Description', 101 | `deadline` datetime DEFAULT NULL COMMENT 'Task Deadline Date', 102 | `completed` enum('Y','N') NOT NULL DEFAULT 'N' COMMENT 'Task Completion STATUS', 103 | `userid` bigint NOT NULL COMMENT 'User ID of owner of task' 104 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT='Tasks table'; 105 | 106 | -- 107 | -- Dumping data for table `tbltasks` 108 | -- 109 | 110 | INSERT INTO `tbltasks` (`id`, `title`, `description`, `deadline`, `completed`, `userid`) VALUES 111 | (1, 'New Task 1 Title updated', NULL, NULL, 'N', 4), 112 | (2, 'Michael\'s task to cut the lawn', NULL, NULL, 'N', 4), 113 | (3, 'John\'s task to paint the fence', NULL, NULL, 'N', 5), 114 | (6, 'test title', NULL, NULL, 'Y', 6), 115 | (7, 'A task title example', NULL, NULL, 'N', 6), 116 | (8, 'A task title example', 'New Task 8 Description updated', NULL, 'Y', 6); 117 | 118 | -- -------------------------------------------------------- 119 | 120 | -- 121 | -- Table structure for table `tblusers` 122 | -- 123 | 124 | CREATE TABLE `tblusers` ( 125 | `id` bigint NOT NULL COMMENT 'User ID', 126 | `fullname` varchar(255) NOT NULL COMMENT 'Users Full Name', 127 | `username` varchar(255) NOT NULL COMMENT 'Users Username', 128 | `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT 'Users Password', 129 | `useractive` enum('Y','N') NOT NULL DEFAULT 'Y' COMMENT 'Is User Active', 130 | `loginattempts` int NOT NULL DEFAULT '0' COMMENT 'Attempts to login' 131 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT='Users Table'; 132 | 133 | -- 134 | -- Dumping data for table `tblusers` 135 | -- 136 | 137 | INSERT INTO `tblusers` (`id`, `fullname`, `username`, `password`, `useractive`, `loginattempts`) VALUES 138 | (4, 'Michael Jones', 'michael', '$2y$10$GAqE6GuAJECSlSZB/Y7Y0uBVnxtIoleZky0uiJ5UtaksHOKXDegCC', 'Y', 0), 139 | (5, 'John Smith', 'john', '$2y$10$LenFpsHEdlX3BvYVM3aVkO7zzirRZgTagwPs0WEhGwDXjCZtQzspm', 'Y', 0), 140 | (6, 'Ahmed Yahya', 'Ahmed', '$2y$10$7tB2RomsZ7CnzZAEbRkdd.EDVKnL9edOrisIlY90MMvTyDO0LRlSe', 'Y', 0); 141 | 142 | -- 143 | -- Indexes for dumped tables 144 | -- 145 | 146 | -- 147 | -- Indexes for table `tblimages` 148 | -- 149 | ALTER TABLE `tblimages` 150 | ADD PRIMARY KEY (`id`), 151 | ADD UNIQUE KEY `filenamefortaskid` (`taskid`,`filename`); 152 | 153 | -- 154 | -- Indexes for table `tblsessions` 155 | -- 156 | ALTER TABLE `tblsessions` 157 | ADD PRIMARY KEY (`id`), 158 | ADD UNIQUE KEY `accesstoken` (`accesstoken`), 159 | ADD UNIQUE KEY `refreshtoken` (`refreshtoken`), 160 | ADD KEY `sessionuserid_fk` (`userid`); 161 | 162 | -- 163 | -- Indexes for table `tbltasks` 164 | -- 165 | ALTER TABLE `tbltasks` 166 | ADD PRIMARY KEY (`id`), 167 | ADD KEY `taskuserid_fk` (`userid`); 168 | 169 | -- 170 | -- Indexes for table `tblusers` 171 | -- 172 | ALTER TABLE `tblusers` 173 | ADD PRIMARY KEY (`id`), 174 | ADD UNIQUE KEY `username` (`username`); 175 | 176 | -- 177 | -- AUTO_INCREMENT for dumped tables 178 | -- 179 | 180 | -- 181 | -- AUTO_INCREMENT for table `tblimages` 182 | -- 183 | ALTER TABLE `tblimages` 184 | MODIFY `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'Image ID Number - Primary Key', AUTO_INCREMENT=18; 185 | 186 | -- 187 | -- AUTO_INCREMENT for table `tblsessions` 188 | -- 189 | ALTER TABLE `tblsessions` 190 | MODIFY `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'Session ID', AUTO_INCREMENT=36; 191 | 192 | -- 193 | -- AUTO_INCREMENT for table `tbltasks` 194 | -- 195 | ALTER TABLE `tbltasks` 196 | MODIFY `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'Task ID - Primary Key', AUTO_INCREMENT=9; 197 | 198 | -- 199 | -- AUTO_INCREMENT for table `tblusers` 200 | -- 201 | ALTER TABLE `tblusers` 202 | MODIFY `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'User ID', AUTO_INCREMENT=7; 203 | 204 | -- 205 | -- Constraints for dumped tables 206 | -- 207 | 208 | -- 209 | -- Constraints for table `tblimages` 210 | -- 211 | ALTER TABLE `tblimages` 212 | ADD CONSTRAINT `imagetaskid_fk` FOREIGN KEY (`taskid`) REFERENCES `tbltasks` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT; 213 | 214 | -- 215 | -- Constraints for table `tblsessions` 216 | -- 217 | ALTER TABLE `tblsessions` 218 | ADD CONSTRAINT `sessionuserid_fk` FOREIGN KEY (`userid`) REFERENCES `tblusers` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT; 219 | 220 | -- 221 | -- Constraints for table `tbltasks` 222 | -- 223 | ALTER TABLE `tbltasks` 224 | ADD CONSTRAINT `taskuserid_fk` FOREIGN KEY (`userid`) REFERENCES `tblusers` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT; 225 | COMMIT; 226 | 227 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 228 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 229 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 230 | -------------------------------------------------------------------------------- /Postman Collection of API Endpoints/Plain PHP REST API with Token-based Authentication and Image Uploading.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "41805882-779b-42f7-a246-e96e32633ff5", 4 | "name": "Plain PHP REST API with Token-based Authentication and Image Uploading", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "28181483", 7 | "_collection_link": "https://www.postman.com/ahmed-yahya/workspace/my-public-portfolio-postman-workspace/collection/28181483-41805882-779b-42f7-a246-e96e32633ff5?action=share&creator=28181483&source=collection_link" 8 | }, 9 | "item": [ 10 | { 11 | "name": "Register/Sign up/Create a new user - POST", 12 | "request": { 13 | "method": "POST", 14 | "header": [], 15 | "body": { 16 | "mode": "raw", 17 | "raw": "{\r\n \"fullname\": \"Ahmed Yahya\",\r\n \"username\": \"Ahmed\",\r\n \"password\": \"123456\"\r\n}", 18 | "options": { 19 | "raw": { 20 | "language": "json" 21 | } 22 | } 23 | }, 24 | "url": { 25 | "raw": "http://127.0.0.1/v1/users", 26 | "protocol": "http", 27 | "host": [ 28 | "127", 29 | "0", 30 | "0", 31 | "1" 32 | ], 33 | "path": [ 34 | "v1", 35 | "users" 36 | ] 37 | } 38 | }, 39 | "response": [] 40 | }, 41 | { 42 | "name": "Log in / Create a new Session, new Access Token and new Refresh Token - POST", 43 | "protocolProfileBehavior": { 44 | "disabledSystemHeaders": {} 45 | }, 46 | "request": { 47 | "method": "POST", 48 | "header": [ 49 | { 50 | "key": "Content-Type", 51 | "value": "application/json", 52 | "type": "text" 53 | } 54 | ], 55 | "body": { 56 | "mode": "raw", 57 | "raw": "{\r\n \"username\": \"Ahmed\",\r\n \"password\": \"123456\"\r\n}", 58 | "options": { 59 | "raw": { 60 | "language": "json" 61 | } 62 | } 63 | }, 64 | "url": { 65 | "raw": "http://127.0.0.1/v1/sessions", 66 | "protocol": "http", 67 | "host": [ 68 | "127", 69 | "0", 70 | "0", 71 | "1" 72 | ], 73 | "path": [ 74 | "v1", 75 | "sessions" 76 | ] 77 | } 78 | }, 79 | "response": [] 80 | }, 81 | { 82 | "name": "Log out/Delete a session - POST", 83 | "protocolProfileBehavior": { 84 | "disabledSystemHeaders": { 85 | "content-type": true 86 | } 87 | }, 88 | "request": { 89 | "method": "DELETE", 90 | "header": [ 91 | { 92 | "key": "Authorization", 93 | "value": "ODVkMTkzYWFlZmZiZTk1YWY5MDJmYWFlZDc5NGMzYjU1YWJmODA1NDIyOGIxNmYxMTY1NzUwMDYyNA==", 94 | "type": "text" 95 | } 96 | ], 97 | "url": { 98 | "raw": "http://127.0.0.1/v1/sessions/12", 99 | "protocol": "http", 100 | "host": [ 101 | "127", 102 | "0", 103 | "0", 104 | "1" 105 | ], 106 | "path": [ 107 | "v1", 108 | "sessions", 109 | "12" 110 | ] 111 | } 112 | }, 113 | "response": [] 114 | }, 115 | { 116 | "name": "Refresh a session (Get a new Access Token and Refresh Token instead of the expired Access Token) - PATCH", 117 | "protocolProfileBehavior": { 118 | "disabledSystemHeaders": { 119 | "content-type": true 120 | } 121 | }, 122 | "request": { 123 | "method": "PATCH", 124 | "header": [ 125 | { 126 | "key": "Authorization", 127 | "value": "Y2I0NDNiOTg3NTE0MWNlYWY1ODM1NzkzNmY5OTNmZDE5ZDkwMWE4ZTIwMjg5YTM2MTY4ODc0NjUyNA==", 128 | "type": "text" 129 | }, 130 | { 131 | "key": "Content-Type", 132 | "value": "application/json", 133 | "type": "text" 134 | } 135 | ], 136 | "body": { 137 | "mode": "raw", 138 | "raw": "{\r\n \"refresh_token\": \"MmI4M2E3ZmFlNzcyZDdjMWM3NjgxNmU0MzhlZDc3OGM2ODI1MjM1ZGJmOTg4N2U5MTY4ODc0NjUyNA==\"\r\n}" 139 | }, 140 | "url": { 141 | "raw": "http://127.0.0.1/v1/sessions/35", 142 | "protocol": "http", 143 | "host": [ 144 | "127", 145 | "0", 146 | "0", 147 | "1" 148 | ], 149 | "path": [ 150 | "v1", 151 | "sessions", 152 | "35" 153 | ] 154 | } 155 | }, 156 | "response": [] 157 | }, 158 | { 159 | "name": "Create a task - POST", 160 | "protocolProfileBehavior": { 161 | "disabledSystemHeaders": {} 162 | }, 163 | "request": { 164 | "method": "POST", 165 | "header": [ 166 | { 167 | "key": "Authorization", 168 | "value": "MTQzZjZhM2YzMGRlZDdiNzM3M2Q5ODQzNTkwM2E1ZWQ3NTBmMGRmODMzOGNiMTJiMTY4ODU5ODI4MQ==", 169 | "type": "text" 170 | } 171 | ], 172 | "body": { 173 | "mode": "raw", 174 | "raw": "{\r\n \"title\": \"A task title example\",\r\n \"completed\": \"Y\"\r\n}", 175 | "options": { 176 | "raw": { 177 | "language": "json" 178 | } 179 | } 180 | }, 181 | "url": { 182 | "raw": "http://127.0.0.1/v1/tasks", 183 | "protocol": "http", 184 | "host": [ 185 | "127", 186 | "0", 187 | "0", 188 | "1" 189 | ], 190 | "path": [ 191 | "v1", 192 | "tasks" 193 | ] 194 | } 195 | }, 196 | "response": [] 197 | }, 198 | { 199 | "name": "Get All tasks that belong to the authenticated/logged-in user - GET", 200 | "request": { 201 | "method": "GET", 202 | "header": [ 203 | { 204 | "key": "Authorization", 205 | "value": "ZTgyOWRlMmM4MTM3OGJhNzU3MjFjOTkwMjdiZjdlYmE0NmY4ODBhNzcxNTk2MWE4MTY4ODY2MzU5NA==", 206 | "type": "text" 207 | } 208 | ], 209 | "url": { 210 | "raw": "http://127.0.0.1/v1/tasks", 211 | "protocol": "http", 212 | "host": [ 213 | "127", 214 | "0", 215 | "0", 216 | "1" 217 | ], 218 | "path": [ 219 | "v1", 220 | "tasks" 221 | ] 222 | } 223 | }, 224 | "response": [] 225 | }, 226 | { 227 | "name": "Get a Single task - GET", 228 | "request": { 229 | "method": "GET", 230 | "header": [ 231 | { 232 | "key": "Authorization", 233 | "value": "ZTgyOWRlMmM4MTM3OGJhNzU3MjFjOTkwMjdiZjdlYmE0NmY4ODBhNzcxNTk2MWE4MTY4ODY2MzU5NA==", 234 | "type": "text" 235 | } 236 | ], 237 | "url": { 238 | "raw": "http://127.0.0.1/v1/tasks/8", 239 | "protocol": "http", 240 | "host": [ 241 | "127", 242 | "0", 243 | "0", 244 | "1" 245 | ], 246 | "path": [ 247 | "v1", 248 | "tasks", 249 | "8" 250 | ] 251 | } 252 | }, 253 | "response": [] 254 | }, 255 | { 256 | "name": "Delete a task (and delete its related images and delete its tasks folder inside the 'taskimages' folder) - DELETE", 257 | "request": { 258 | "method": "DELETE", 259 | "header": [ 260 | { 261 | "key": "Authorization", 262 | "value": "ZTgyOWRlMmM4MTM3OGJhNzU3MjFjOTkwMjdiZjdlYmE0NmY4ODBhNzcxNTk2MWE4MTY4ODY2MzU5NA==", 263 | "type": "text" 264 | } 265 | ], 266 | "url": { 267 | "raw": "http://127.0.0.1/v1/tasks/5", 268 | "protocol": "http", 269 | "host": [ 270 | "127", 271 | "0", 272 | "0", 273 | "1" 274 | ], 275 | "path": [ 276 | "v1", 277 | "tasks", 278 | "5" 279 | ] 280 | } 281 | }, 282 | "response": [] 283 | }, 284 | { 285 | "name": "Update a task (that belongs to the authenticated/logged-in user) - PATCH", 286 | "request": { 287 | "method": "PATCH", 288 | "header": [ 289 | { 290 | "key": "Authorization", 291 | "value": "ZTgyOWRlMmM4MTM3OGJhNzU3MjFjOTkwMjdiZjdlYmE0NmY4ODBhNzcxNTk2MWE4MTY4ODY2MzU5NA==", 292 | "type": "text" 293 | } 294 | ], 295 | "body": { 296 | "mode": "raw", 297 | "raw": "{\r\n \"description\": \"New Task 8 Description updated\"\r\n}", 298 | "options": { 299 | "raw": { 300 | "language": "json" 301 | } 302 | } 303 | }, 304 | "url": { 305 | "raw": "http://127.0.0.1/v1/tasks/8", 306 | "protocol": "http", 307 | "host": [ 308 | "127", 309 | "0", 310 | "0", 311 | "1" 312 | ], 313 | "path": [ 314 | "v1", 315 | "tasks", 316 | "8" 317 | ] 318 | } 319 | }, 320 | "response": [] 321 | }, 322 | { 323 | "name": "Get all Complete tasks (of the authenticated/logged-in user) - GET", 324 | "request": { 325 | "method": "GET", 326 | "header": [ 327 | { 328 | "key": "Authorization", 329 | "value": "ZTgyOWRlMmM4MTM3OGJhNzU3MjFjOTkwMjdiZjdlYmE0NmY4ODBhNzcxNTk2MWE4MTY4ODY2MzU5NA==", 330 | "type": "text" 331 | } 332 | ], 333 | "url": { 334 | "raw": "http://127.0.0.1/v1/tasks/complete", 335 | "protocol": "http", 336 | "host": [ 337 | "127", 338 | "0", 339 | "0", 340 | "1" 341 | ], 342 | "path": [ 343 | "v1", 344 | "tasks", 345 | "complete" 346 | ] 347 | } 348 | }, 349 | "response": [] 350 | }, 351 | { 352 | "name": "Get all Incomplete tasks (of the authenticated/logged-in user) - GET", 353 | "request": { 354 | "method": "GET", 355 | "header": [ 356 | { 357 | "key": "Authorization", 358 | "value": "ZTgyOWRlMmM4MTM3OGJhNzU3MjFjOTkwMjdiZjdlYmE0NmY4ODBhNzcxNTk2MWE4MTY4ODY2MzU5NA==", 359 | "type": "text" 360 | } 361 | ], 362 | "url": { 363 | "raw": "http://127.0.0.1/v1/tasks/incomplete", 364 | "protocol": "http", 365 | "host": [ 366 | "127", 367 | "0", 368 | "0", 369 | "1" 370 | ], 371 | "path": [ 372 | "v1", 373 | "tasks", 374 | "incomplete" 375 | ] 376 | } 377 | }, 378 | "response": [] 379 | }, 380 | { 381 | "name": "Get All tasks with Pagination(URL must contain the 'page' Query String Parameter) - GET", 382 | "request": { 383 | "method": "GET", 384 | "header": [ 385 | { 386 | "key": "Authorization", 387 | "value": "ZTgyOWRlMmM4MTM3OGJhNzU3MjFjOTkwMjdiZjdlYmE0NmY4ODBhNzcxNTk2MWE4MTY4ODY2MzU5NA==", 388 | "type": "text" 389 | } 390 | ], 391 | "url": { 392 | "raw": "http://127.0.0.1/v1/tasks/page/1", 393 | "protocol": "http", 394 | "host": [ 395 | "127", 396 | "0", 397 | "0", 398 | "1" 399 | ], 400 | "path": [ 401 | "v1", 402 | "tasks", 403 | "page", 404 | "1" 405 | ] 406 | } 407 | }, 408 | "response": [] 409 | }, 410 | { 411 | "name": "Create/Upload an Image for a certain task (of the authenticated/logged-in user) - POST", 412 | "protocolProfileBehavior": { 413 | "disabledSystemHeaders": {} 414 | }, 415 | "request": { 416 | "method": "POST", 417 | "header": [ 418 | { 419 | "key": "Authorization", 420 | "value": "ZThlZGFhZmM3ZWQ0MDQzZmE1ZTE5YzBmMDJiZGUxNjUxMjIxNzQ0N2I1YTU3OTM3MTY4ODc1ODk4OQ==", 421 | "type": "text" 422 | } 423 | ], 424 | "body": { 425 | "mode": "formdata", 426 | "formdata": [ 427 | { 428 | "key": "attributes", 429 | "value": "{\"title\": \"Study some topics 2\", \"filename\": \"study-2\"}", 430 | "type": "text" 431 | }, 432 | { 433 | "key": "imagefile", 434 | "type": "file", 435 | "src": "/C:/study.jpg" 436 | } 437 | ] 438 | }, 439 | "url": { 440 | "raw": "http://127.0.0.1/v1/tasks/7/images", 441 | "protocol": "http", 442 | "host": [ 443 | "127", 444 | "0", 445 | "0", 446 | "1" 447 | ], 448 | "path": [ 449 | "v1", 450 | "tasks", 451 | "7", 452 | "images" 453 | ] 454 | } 455 | }, 456 | "response": [] 457 | }, 458 | { 459 | "name": "Get/Download an actual physical imagae binary file of a certain task - GET", 460 | "protocolProfileBehavior": { 461 | "disableBodyPruning": true 462 | }, 463 | "request": { 464 | "method": "GET", 465 | "header": [ 466 | { 467 | "key": "Authorization", 468 | "value": "ZThlZGFhZmM3ZWQ0MDQzZmE1ZTE5YzBmMDJiZGUxNjUxMjIxNzQ0N2I1YTU3OTM3MTY4ODc1ODk4OQ==", 469 | "type": "text" 470 | } 471 | ], 472 | "body": { 473 | "mode": "formdata", 474 | "formdata": [] 475 | }, 476 | "url": { 477 | "raw": "http://127.0.0.1/v1/tasks/7/images/16", 478 | "protocol": "http", 479 | "host": [ 480 | "127", 481 | "0", 482 | "0", 483 | "1" 484 | ], 485 | "path": [ 486 | "v1", 487 | "tasks", 488 | "7", 489 | "images", 490 | "16" 491 | ] 492 | } 493 | }, 494 | "response": [] 495 | }, 496 | { 497 | "name": "Delete an actual physical image of a certain task - DELETE", 498 | "request": { 499 | "method": "DELETE", 500 | "header": [ 501 | { 502 | "key": "Authorization", 503 | "value": "ZThlZGFhZmM3ZWQ0MDQzZmE1ZTE5YzBmMDJiZGUxNjUxMjIxNzQ0N2I1YTU3OTM3MTY4ODc1ODk4OQ==", 504 | "type": "text" 505 | } 506 | ], 507 | "url": { 508 | "raw": "http://127.0.0.1/v1/tasks/7/images/17", 509 | "protocol": "http", 510 | "host": [ 511 | "127", 512 | "0", 513 | "0", 514 | "1" 515 | ], 516 | "path": [ 517 | "v1", 518 | "tasks", 519 | "7", 520 | "images", 521 | "17" 522 | ] 523 | } 524 | }, 525 | "response": [] 526 | }, 527 | { 528 | "name": "Get a certain image Attributes (of a certain task that belongs to the authenticated/logged-in user) - GET", 529 | "protocolProfileBehavior": { 530 | "disableBodyPruning": true 531 | }, 532 | "request": { 533 | "method": "GET", 534 | "header": [ 535 | { 536 | "key": "Authorization", 537 | "value": "ZThlZGFhZmM3ZWQ0MDQzZmE1ZTE5YzBmMDJiZGUxNjUxMjIxNzQ0N2I1YTU3OTM3MTY4ODc1ODk4OQ==", 538 | "type": "text" 539 | } 540 | ], 541 | "body": { 542 | "mode": "formdata", 543 | "formdata": [ 544 | { 545 | "key": "", 546 | "value": "", 547 | "type": "text", 548 | "disabled": true 549 | } 550 | ] 551 | }, 552 | "url": { 553 | "raw": "http://127.0.0.1/v1/tasks/8/images/15/attributes", 554 | "protocol": "http", 555 | "host": [ 556 | "127", 557 | "0", 558 | "0", 559 | "1" 560 | ], 561 | "path": [ 562 | "v1", 563 | "tasks", 564 | "8", 565 | "images", 566 | "15", 567 | "attributes" 568 | ] 569 | } 570 | }, 571 | "response": [] 572 | }, 573 | { 574 | "name": "Update a certain image Attributes (of a certain task that belongs to the authenticated/logged-in user) - PATCH", 575 | "request": { 576 | "method": "PATCH", 577 | "header": [ 578 | { 579 | "key": "Authorization", 580 | "value": "ZThlZGFhZmM3ZWQ0MDQzZmE1ZTE5YzBmMDJiZGUxNjUxMjIxNzQ0N2I1YTU3OTM3MTY4ODc1ODk4OQ==", 581 | "type": "text" 582 | }, 583 | { 584 | "key": "Content-Type", 585 | "value": "application/json", 586 | "type": "text" 587 | } 588 | ], 589 | "body": { 590 | "mode": "raw", 591 | "raw": "{\r\n \"title\": \"Do the homework UPDATED\",\r\n \"filename\": \"homework-UPDATED\"\r\n}", 592 | "options": { 593 | "raw": { 594 | "language": "json" 595 | } 596 | } 597 | }, 598 | "url": { 599 | "raw": "http://127.0.0.1/v1/tasks/8/images/15/attributes", 600 | "protocol": "http", 601 | "host": [ 602 | "127", 603 | "0", 604 | "0", 605 | "1" 606 | ], 607 | "path": [ 608 | "v1", 609 | "tasks", 610 | "8", 611 | "images", 612 | "15", 613 | "attributes" 614 | ] 615 | } 616 | }, 617 | "response": [] 618 | } 619 | ] 620 | } -------------------------------------------------------------------------------- /Project Screenshots/REST-API-Constraints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedYahyaE/plain-php-rest-api-with-authentication/e2d98e430057739d0d5e5e306dd18e94a88f8a2f/Project Screenshots/REST-API-Constraints.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plain PHP REST/RESTful API with Token-based Authentication and Image Uploading 2 | A Comprehensive Plain PHP & MySQL REST/RESTful API with Token-based Authentication and Image Uploading feature. The API is built following the MVC (Model-View-Controller) Design Pattern, and is totally Object-oriented (OOP). 3 | 4 | The idea is an API for creating personal To-Do Lists (Tasks) and their associated Images. You can create a To-Do List and assign an image to each task. 5 | 6 | This project script is entirely written in plain PHP (OOP) and aims to showcase the implementation of an API with Token-based Authentication without relying on any external libraries or frameworks. 7 | 8 | ## Screenshots: 9 | ***REST/RESTful API Constraints:*** 10 | 11 | ![REST-API-Constraints](https://github.com/AhmedYahyaE/plain-php-rest-api-with-authentication/assets/118033266/36e5c1ff-10f3-49d9-a6d6-638227d6ab78) 12 | 13 | ## Features: 14 | 1- MVC Design Pattern. 15 | 16 | 2- Advanced use of the ***.htaccess*** "per-directory" Apache configuration file for routing control and URL redirection (URL Rewriting using the RewriteEngine). Check the API's ***[.htaccess file](v1/.htaccess)***. 17 | 18 | 3- Token-based Authentication using a short-lived "**Access Token**" (`20 minutes`) and a longer-term "**Refresh Token**" (`2 weeks`). 19 | 20 | 4- Totally Object-oriented design. 21 | 22 | 5- Protected Routes. 23 | 24 | 6- HTTP Responses with Pagination. 25 | 26 | 7- Advanced Error Handling involving custom Exception classes which return appropriate **JSON** format error messages with the right HTTP Status Codes. 27 | 28 | 8- Advanced SQL INNER JOIN clauses and CRUD operations. 29 | 30 | 9- File Upload and File Download API Endpoints. 31 | 32 | 10- Registration, Validation, Authentication, and Authorization. 33 | 34 | ## API Endpoints: 35 | > ***\*\* Check the API Collection on my Postman Profile: https://www.postman.com/ahmed-yahya/workspace/my-public-portfolio-postman-workspace/collection/28181483-41805882-779b-42f7-a246-e96e32633ff5*** 36 | 37 | > ***\*\* Also, you can test the API Endpoints using Postman. Here is the API's Postman Collection .json file [Postman Collection file]() you can download and import in your Postman.*** 38 | 39 | **1- Register/Sign up/Create a new user (POST):** 40 | 41 | **POST /v1/users** 42 | 43 | - "Content-Type" HTTP Request Header must be set to "application/". 44 | 45 | - Mandatory fields in the JSON HTTP Request Body: fullname, username, and password. 46 | 47 | **2- Log in and Create a new session with a new Access Token and a new Refresh Token (POST):** 48 | 49 | **POST /v1/sessions** 50 | 51 | - "Content-Type" HTTP Request Header must be set to "application/json". 52 | 53 | - Mandatory fields in the JSON HTTP Request Body: username and password. 54 | 55 | **3- Log out and delete a session (DELETE):** 56 | 57 | **DELETE /v1/sessions/{sessionid}** 58 | 59 | - {sessionid} Query String Parameter in the URL must be provided. 60 | 61 | - "Authorization" HTTP Request Header (Access Token) must be provided. 62 | 63 | **4- Refresh a session (update a session to get a new Access Token and a new Refresh Token instead of the expired Access Token) (PATCH):** 64 | 65 | **PATCH /v1/sessions/{sessionid}** 66 | 67 | - {sessionid} Query String Parameter in the URL must be provided. 68 | 69 | - "Content-Type" HTTP Request Header must be set to "application/json". 70 | 71 | - "Refresh Token" must be provided as JSON in the HTTP Request Body (Not as an 'Authorization' HTTP Request Header). 72 | 73 | - "Access Token" must be provided as an "Authorization" HTTP Request Header. 74 | 75 | **5- Create a new task (POST):** 76 | 77 | **POST /v1/tasks** 78 | 79 | - "Authorization" HTTP Request Header (Access Token) must be provided. 80 | 81 | - "Content-Type" HTTP Request Header must be set to "application/json". 82 | 83 | - Mandatory fields in the JSON HTTP Request Body: `title` and `completed`. 84 | 85 | **6- Get ALL tasks that belong to the authenticated/logged-in user (GET):** 86 | 87 | **GET /v1/tasks** 88 | 89 | - "Authorization" HTTP Request Header (Access Token) must be provided. 90 | 91 | **7- Get a Single task (GET):** 92 | 93 | **GET /v1/tasks/{taskid}** 94 | 95 | - {taskid} Query String Parameter in the URL must be provided. 96 | 97 | - "Authorization" HTTP Request Header (Access Token) must be provided. 98 | 99 | **8- Delete a single task (DELETE) (that belongs to the authenticated/logged-in user):** (this also deletes all of the associated images as well as the task images folder inside the 'taskimages' folder) 100 | 101 | **DELETE /v1/tasks/{taskid}** 102 | 103 | - {taskid} Query String Parameter in the URL must be provided. 104 | 105 | - "Authorization" HTTP Request Header (Access Token) must be provided. 106 | 107 | **9- Update a single task (PATCH):** 108 | 109 | **PATCH /v1/tasks/{taskid}** 110 | 111 | - {taskid} Query String Parameter in the URL must be provided. 112 | 113 | - "Authorization" HTTP Request Header (Access Token) must be provided. 114 | 115 | - "Content-Type" HTTP Request Header must be set to "application/json". 116 | 117 | - Mandatory fields in the JSON HTTP Request Body: At least one of the fields: `title`, `description`, `deadline`, and `completed`. 118 | 119 | **10- Get all 'Complete' tasks (GET):** 120 | 121 | **GET /v1/tasks/complete** 122 | 123 | - {complete} or {incomplete} Query String Parameter in the URL must be provided. 124 | 125 | - "Authorization" HTTP Request Header (Access Token) must be provided. 126 | 127 | **11 - Get all 'Incomplete' tasks (GET):** 128 | 129 | **GET /v1/tasks/incomplete** 130 | 131 | - {complete} or {incomplete} Query String Parameter in the URL must be provided. 132 | 133 | - "Authorization" HTTP Request Header (Access Token) must be provided. 134 | 135 | **12- Get All tasks with Pagination (tasks that belong to the authenticated/logged-in user) (GET):** 136 | 137 | **GET /v1/tasks/page/{pagenumber}** 138 | 139 | - {page} Query String Parameter and its value {pagenumber} in the URL must be provided. 140 | 141 | - "Authorization" HTTP Request Header (Access Token) must be provided. 142 | 143 | **13- Create (Upload) an image for a certain task (of the authenticated/logged-in user):** 144 | 145 | **POST /tasks/{taskid}/images** 146 | 147 | - {taskid} Query String Parameter in the URL must be provided. 148 | 149 | - "Authorization" HTTP Request Header (Access Token) must be provided. 150 | 151 | - "multipart/form-data; boundary=" HTTP Request Header must be provided. 152 | 153 | - In Postman, click on "Body", then "form-data", then enter two fields: "attributes" and "imagefile" fields. For the "attributes" field, set it to "Text" and enter the Value as JSON (Example: {"title": "Image Title 1", "filename": "carimage"}) and don't mention the file extension in the file name. For the"imagefile" field, set it to "File", and upload an image file in the Value. Only .jgp, .gif, or .png images are allowed. 154 | 155 | **14- Get (Download) an actual physical image of a certain task (of the authenticated/logged-in user):** 156 | 157 | **GET /tasks/{taskid}/images/{imageid}** 158 | 159 | - {taskid} and {imageid} Query String Parameters in the URL must be provided. 160 | 161 | - "Authorization" HTTP Request Header (Access Token) must be provided. 162 | 163 | **15- Delete an actual physical image of a certain task (of the authenticated/logged-in user):** 164 | 165 | **DELETE /tasks/{taskid}/images/{imageid}** 166 | 167 | - {taskid} and {imageid} Query String Parameters in the URL must be provided. 168 | 169 | - "Authorization" HTTP Request Header (Access Token) must be provided. 170 | 171 | **16- Get a certain image Attributes (of a certain task that belongs to the authenticated/logged-in user):** 172 | 173 | **GET /tasks/{taskid}/images/{imageid}/attributes** 174 | 175 | - The three of {taskid}, {imageid} and {attributes} Query String Parameters in the URL must be provided. 176 | 177 | - "Authorization" HTTP Request Header (Access Token) must be provided. 178 | 179 | **17- Update a certain image Attributes (of a certain task that belongs to the authenticated/logged-in user):** 180 | 181 | **PATCH /tasks/{taskid}/images/{imageid}/attributes** 182 | 183 | - The three of {taskid}, {imageid} and {attributes} Query String Parameters in the URL must be provided. 184 | 185 | - "Authorization" HTTP Request Header (Access Token) must be provided. 186 | 187 | - "Content-Type" HTTP Request Header must be set to "application/json". 188 | 189 | - Mandatory fields in the JSON HTTP Request Body: At least one of the two fields: `title` and `filename`. N.B. File Name must be provided WITHOUT the file extension. 190 | 191 | ## Installation & Configuration: 192 | 1- Clone the project or download it. 193 | 194 | 2- Create a MySQL database named **\`tasksdb\`** and import the database SQL Dump file **[tasksdb Database SQL Dump File]()**. 195 | 196 | 3- Navigate to the Database Connection Class file **[db.php](v1/controller/db.php)** and configure/edit/update it with your MySQL database credentials and other configuration settings. 197 | 198 | 4- Important Note: "**`Apache`**" Web Server must be used for serving this API in order for the ***.htaccess*** file to work, as I based all of the routing system work and URL redirection/URL Rewriting on the ***.htaccess*** file (RewriteEngine and RewriteRule-s). Check the API's ***[.htaccess file](v1/.htaccess)***. Start by typing in the API Endpoint: **`POST http://127.0.0.1/v1/users`** in your Postman to Sign up. 199 | 200 | - Credentials of a ready-to-use registered user account are: (Use this account to Log in and create a new session i.e. get a new "**Access Token**" and "**Refresh Token**" with the following Endpoint: **`POST http://127.0.0.1/v1/sessions`**) 201 | 202 | > **Username**: **Ahmed**, **Password**: **123456** 203 | 204 | ## Contribution: 205 | Contributions to my plain PHP REST/RESTful API are most welcome! If you find any issues or have suggestions for improvements, want to add new features or want to contribute code or documentation, please open an issue or submit a pull request. 206 | -------------------------------------------------------------------------------- /v1/.htaccess: -------------------------------------------------------------------------------- 1 | # CORS options: 2 | 3 | Header set Access-Control-Allow-Origin "*" 4 | 5 | 6 | php_flag display_errors on 7 | 8 | 9 | # Apache by default prevents/strips the AUTHORIZATION HTTP header from being sent, so we have to enable it: 10 | # Fix for Apache AUTHORIZATION HTTP header as it's stripped by default for security and should be enabled explicitly when needed: 11 | SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0 12 | 13 | 14 | # https://docs.oracle.com/cd/B14099_19/web.1012/q20206/mod/core.html#options 15 | Options -Indexes 16 | 17 | 18 | # Turn on the rewriting engine 19 | RewriteEngine On 20 | # If the browser has a URL of anything doesn't exist whether it's a d (directory) or f (folder), then proceed the rule below: 21 | RewriteCond %{REQUEST_FILENAME} !-d 22 | RewriteCond %{REQUEST_FILENAME} !-f 23 | 24 | 25 | 26 | # Our routes (API Endpoints): 27 | # Note: ^ and $ are REGUALR EXPRESSION METACHARACTERS 28 | 29 | # Tasks: 30 | RewriteRule ^tasks/([0-9]+)$ controller/task.php?taskid=$1 [L] 31 | 32 | RewriteRule ^tasks/complete$ controller/task.php?completed=Y [L] 33 | RewriteRule ^tasks/incomplete$ controller/task.php?completed=N [L] 34 | 35 | # RewriteRule ^tasks/page/(.+)$ controller/task.php?page=$1 [L] 36 | RewriteRule ^tasks/page/([0-9]+)$ controller/task.php?page=$1 [L] 37 | RewriteRule ^tasks$ controller/task.php [L] 38 | 39 | 40 | # Users: 41 | RewriteRule ^users$ controller/users.php [L] 42 | 43 | 44 | 45 | # Authentication: 46 | RewriteRule ^sessions$ controller/sessions.php [L] 47 | RewriteRule ^sessions/([0-9]+)$ controller/sessions.php?sessionid=$1 [L] 48 | 49 | 50 | 51 | # Images: 52 | 53 | # Retrieve or Update a certain image Attributes (GET or PATCH) 54 | RewriteRule ^tasks/([0-9]+)/images/([0-9]+)/attributes$ controller/images.php?taskid=$1&imageid=$2&attributes=true [L] 55 | 56 | # Handle Download a certain actual physical image of a certain (id) task (GET) Or Delete a certain physical image of a certain task (DELETE) 57 | RewriteRule ^tasks/([0-9]+)/images/([0-9]+)$ controller/images.php?taskid=$1&imageid=$2 [L] 58 | 59 | # Handle Creating (Uploading) an image for a certain task (POST) 60 | RewriteRule ^tasks/([0-9]+)/images$ controller/images.php?taskid=$1 [L] -------------------------------------------------------------------------------- /v1/controller/db.php: -------------------------------------------------------------------------------- 1 | setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // the error mode is Exception. They are good because you can catch them, deal with them, and they are a good way to error handling in PHP 20 | self::$writeDBConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // means emulate prepare() statements because not every database management system can handle prepared statements 21 | } 22 | 23 | 24 | return self::$writeDBConnection; 25 | } 26 | 27 | 28 | public static function connectReadDB() { 29 | // Database connection must be a singleton object (there must be a one connection only across/throughout your whole project/application, and you just reuse it whenever you need it) 30 | if (self::$readDBConnection === null) { // to start initiation of the connection 31 | self::$readDBConnection = new PDO('mysql:host=localhost;dbname=tasksdb;charset=utf8', 'root', ''); // Create the database connection 32 | 33 | // Setting some attributes on the connection: 34 | self::$readDBConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // the error mode is Exception. They are good because you can catch them, deal with them, and they are a good way to error handling in PHP 35 | self::$readDBConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // means emulate prepare() statements because not every database management system can handle prepared statements 36 | } 37 | 38 | 39 | return self::$readDBConnection; 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /v1/controller/dbtest.php: -------------------------------------------------------------------------------- 1 | setHttpStatusCode(500); // A server error 14 | $response->setSuccess(false); 15 | $response->addMessage('Database Connection error'); 16 | $response->send(); 17 | exit; // exit; is a good practice here 18 | } 19 | // catch(MyException $ex) { 20 | // $response = new Response(); 21 | // $response->setHttpStatusCode(500); // A server error 22 | // $response->setSuccess(false); 23 | // $response->addMessage('Database Connection error'); 24 | // $response->send(); 25 | // exit; // exit; is a good practice here 26 | // } -------------------------------------------------------------------------------- /v1/controller/images.php: -------------------------------------------------------------------------------- 1 | setHttpStatusCode($statusCode); 17 | $response->setSuccess($success); 18 | 19 | if ($message != null) { 20 | $response->addMessage($message); 21 | } 22 | 23 | $response->toCache($toCache); 24 | if ($data != null) { 25 | $response->setData($data); 26 | } 27 | 28 | $response->send(); 29 | exit; // YOU MUST USE exit HERE TO EXIT OUT OF THE SCRIPT AFTER YOU SEND THE RESPONSE OF THE DESIRED ENDPOINT OF YOUR API (to not continue the script after the proper response has been sent) 30 | } 31 | 32 | 33 | 34 | function uploadImageRoute($readDB, $writeDB, $taskid, $returned_userid) { 35 | // We're going to query the database and use our image model for validation of inserted data and grabbing data, so we need to use a try ... catch ... statement, to BOTH catch BOTH PODException errors AND catch ImageException errors: 36 | try { 37 | // Do some validation: 38 | if (!isset($_SERVER['CONTENT_TYPE']) || strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data; boundary=') === false) { // Note: 'boundary' is sort of a random character string (a delimiter) // strpos() is used to check if a string contains a certain word or sentence 39 | sendResponse(400, false, 'Content type header not set to multipart/form-data with a boundary'); // 400 means client error 40 | } 41 | 42 | // Check in the database if the task (that we want upload its image) of the task id exists (comes from the URL query string parameters) and user id (comes from the checkAuthStatusAndReturnUserID() function) exists (meaning Check if there's a task with the provided task id and it belongs to the authenticated/logged-in user) 43 | $query = $readDB->prepare('SELECT id FROM tbltasks WHERE id = :taskid AND userid = :userid'); 44 | 45 | $query->bindParam(':taskid', $taskid , PDO::PARAM_INT); 46 | $query->bindParam(':userid', $returned_userid, PDO::PARAM_INT); 47 | 48 | $query->execute(); 49 | 50 | $rowCount = $query->rowCount(); 51 | 52 | if ($rowCount === 0) { // if it doesn't find the task with the specific criteria 53 | sendResponse(404, false, 'Task Not Found'); // 404 means Not Found 54 | } 55 | 56 | 57 | 58 | // Important Note: To upload an image: In Postman, Go "Body", then "form-data", then you'll use TWO "Key" and "Values" fields: the first one: from the drop-down menu, choose "Text" and then enter "attributes" in the "Key" field, and then enter the "attributes" filed as JSON Text as follows: {"title": "cat", "filename":"beautiful_cat"} ("title" will be used to fill in the `title` column, and "filename" will be used to fill in the `filename` column in `tblimages` table (Note: We'll disregard the original uploaded file name! We'll take the file name that is with the "filename" attribute)) (N.B. Don't enter the file extension, we'll automatically determine it!) in the , and the second one: from the drop-down menu, choose "File" and enter "imagefile" in the "Key" field, and upload the file in "Value" field. 59 | 60 | 61 | 62 | // Perform some additional validation on the 'attributes' provided while uploading an image in the response body: 63 | // Note: User must enter the filename without its file extension 64 | // Note: Using "form-data" as a "Body" in Postman mimics/imitates the HTML Form body HTML elements/fields (and the HTML
element "enctype" attribute is set to enctype="multipart/form-data" (Check the "Content-Type" Header in Postman after entering the "form-data" fields (in "Body" in Postman) and their "Keys" and "Values") in Postman). And to access/inspect the submitted data in PHP in the backend, as expected, we use the $_POST Supergloabl array. 65 | if (!isset($_POST['attributes'])) { // 'attributes' are sent in Postman from 'Body' tab, then choose 'form-data', then write 'attributes' in the 'Key' field (Choode 'Text' from drop-down menu), and the 'attributes' JSON value in the 'Value' field 66 | sendResponse(400, false, 'Attributes missing from body of request'); // 400 means client error (because the client hasn't submitted the right/proper data) 67 | } 68 | 69 | // Make sure that the passed in "attributes" Value are in a JSON format 70 | if (!$jsonImageAttributes = json_decode($_POST['attributes'])) { 71 | sendResponse(400, false, 'Attributes field is not valid JSON'); // 400 means client error (because the client hasn't submitted the right/proper data) 72 | } 73 | 74 | // Perform some basic validation on the submitted attributes within the valid JSON (Requiring the mandatory fields: title and filename) 75 | // (like an HTML element "name" attribute) attributes: {"title": "title value", "filename": "filename value without extension"} 76 | // Note: Using "form-data" as a "Body" in Postman mimics/imitates the HTML Form body HTML elements/fields (and the HTML element "enctype" attribute is set to enctype="multipart/form-data" (Check the "Content-Type" Header in Postman after entering the "form-data" fields (in "Body" in Postman) and their "Keys" and "Values") in Postman). And to access/inspect the submitted data in PHP in the backend, as expected, we use the $_POST Supergloabl array. 77 | if (!isset($jsonImageAttributes->title) || !isset($jsonImageAttributes->filename) || $jsonImageAttributes->title == '' || $jsonImageAttributes->filename == '') { 78 | sendResponse(400, false, 'Title and Filename are mandatory'); // 400 means client error (because the client hasn't submitted the right/proper data) 79 | } 80 | 81 | // Make sure the filename doesn't contain a file extension (user must enter the file name without its file extension), as we're going to AUTOMATICALLY determine the file extension from the file type as we're uploading the image 82 | if (strpos($jsonImageAttributes->filename, '.') > 0) { // if the filename contains a dot (e.g. xxxxxx.jpg) 83 | sendResponse(400, false, 'Filename must not contain a file extension'); // 400 means client error (because the client hasn't submitted the right/proper data) 84 | } 85 | 86 | // Make sure the image file itself is provided (uploaded) 87 | // We're going to check three things: check if the file is successfully uploaded, file size (we're going to limit the file size to less than < 5 Megabytes) and determine the MIME type of the file 88 | // Uploaded files are found in the superglobal $_FILES (not $_POST) // When the file is first uploaded to server using PHP web server, the web server stores it temporarily in a location (you can access the file path/location using $_FILES['file attribute name']['tmp_name']), and if you don't do anything with that file, the web server deletes that file automatically once the script ends (so we don't have to deal with the tidy up), so we have to move the file where we want it to go and name it what we want 89 | if (!isset($_FILES['imagefile']) || $_FILES['imagefile']['error'] !== 0) { // Make sure the file is provided (uploaded) and there're no errors while uploading the file // 'imagefile' is the attribute that we're going to use in Postman // $_FILES['imagefile']['error'] should be zero 0 if it's been uploaded successfully to server 90 | sendResponse(500, false, 'Image file upload unsuccessful - make sure you selected a file'); // 500 means Internal Server Error 91 | } 92 | 93 | // Do some validation on the uploaded image 94 | // getimagesize() function takes in the image location as an argument, and checks the uploaded file to make sure it's an image file and provides a lot of metadata about the image itself (image dimensions, type of image (jpg, png, gif, ...) (MIME type)) 95 | $imageFileDetails = getimagesize($_FILES['imagefile']['tmp_name']); // 'tmp_name' is the temporary location of the uploaded file on the server 96 | 97 | if (isset($_FILES['imagefile']['size']) && $_FILES['imagefile']['size'] > 5242880) { // size in bytes (5 MB = 5 * 1024 * 1024 = 5242880 bytes) // Note: 1048576 (mega) = 1024 * 1024 98 | sendResponse(400, false, 'File must be under 5MB'); // 400 means client error (because the client hasn't submitted the right/proper data) 99 | } 100 | 101 | $allowedImageFileTypes = array('image/jpeg', 'image/gif', 'image/png'); // allowd MIME types 102 | 103 | if (!in_array($imageFileDetails['mime'], $allowedImageFileTypes)) { // make sure that the uploaded image MIME type are one of the three MIME types we allow only 104 | sendResponse(400, false, 'File type not supported'); // 400 means client error (because the client hasn't submitted the right/proper data) 105 | } 106 | 107 | $fileExtension = ''; 108 | 109 | // Determine the uploaded file extension based on the MIME type 110 | switch ($imageFileDetails['mime']) { 111 | case 'image/jpeg': $fileExtension = '.jpg'; 112 | break; 113 | 114 | case 'image/gif': $fileExtension = '.gif'; 115 | break; 116 | 117 | case 'image/png': $fileExtension = '.png'; 118 | break; 119 | 120 | default: break; // if the MIME type is not one of the past three, just drop out of it (then the $fileExtension would be blank) 121 | } 122 | 123 | if ($fileExtension == '') { // if the $fileExtension is blank 124 | sendResponse(400, false, 'No valid file extension found for mimetype'); // 400 means client error (because the client hasn't submitted the right/proper data) 125 | } 126 | 127 | // Validation 128 | // After passing all that validation, we use image model now to build the uploaded image up to perform validation on it, and move it where we want, and save its path into the database 129 | $image = new Image(null, $jsonImageAttributes->title, $jsonImageAttributes->filename . $fileExtension, $imageFileDetails['mime'], $taskid); // This will perform validation on the image, and if there're any errors thrown (ImageException class errors), it will be caught (catch-ed) a bit down using the catch statement ( catch (ImageException $ex) ) 130 | 131 | $title = $image->getTitle(); 132 | $newFileName = $image->getFilename(); 133 | $mimetype = $image->getMimetype(); 134 | 135 | // We need to query the database to make sure that the file name that the user provided $newFileName (which is the $jsonImageAttributes->filename) does NOT already exist in our database for that set (specific) task, because on filesystem (in a folder), we can only store a unique file name (a one folder can't contain two files with the same name) 136 | $query = $readDB->prepare('SELECT tblimages.id FROM tblimages, tbltasks 137 | WHERE tblimages.taskid = tbltasks.id 138 | AND tbltasks.id = :taskid 139 | AND tbltasks.userid = :userid 140 | AND tblimages.filename = :filename' 141 | ); // We use the $readDB (not $writeDB) because we're obviosuly not writing anything now 142 | 143 | $query->bindParam(':taskid' , $taskid , PDO::PARAM_INT); 144 | $query->bindParam(':userid' , $returned_userid, PDO::PARAM_INT); 145 | $query->bindParam(':filename', $newFileName , PDO::PARAM_STR); 146 | 147 | $query->execute(); 148 | 149 | $rowCount = $query->rowCount(); 150 | 151 | if ($rowCount !== 0) { // if there's a filename, with the same filename that the user submitted, already exists in our database 152 | sendResponse(409, false, 'A file with that filename already exists for this task - try a different filename'); // 409 means Conflict (the data provided is conflicting with something else) 153 | } 154 | 155 | // Now that the image has passed all that validation, we need to BOTH rename it and move it where we want AND save its path into the database, so here now we need to use a Database Transaction, to gurantee that we BOTH moved the image and added its path into database (and if one of the two operations fails, the whole transaction fails, meaning the two operations fail) 156 | // If the transaction fails at any point, so we need to write what would happen in such case inside BOTH the catch (PDOException $ex) statement and catch (ImageException $ex) statement down below, which will be that we end the transaction and roll back (and it must be placed after the error_log() to allow the error to be logged to administrator) 157 | // Database Transaction 158 | $writeDB->beginTransaction(); // We use the $writeDB (not $readDB) because we're writing now 159 | 160 | // Insert the file (image) information into the database 161 | $query = $writeDB->prepare('INSERT INTO tblimages (title, filename, mimetype, taskid) VALUES (:title, :filename, :mimetype, :taskid)'); 162 | 163 | $query->bindParam(':title' , $title , PDO::PARAM_STR); // $title is "title" field inside the JSON entered as a Value for the "attributes" field/Key 164 | $query->bindParam(':filename', $newFileName, PDO::PARAM_STR); // $newFileName is "filename" field inside the JSON entered as a Value for the "attributes" field/Key 165 | $query->bindParam(':mimetype', $mimetype , PDO::PARAM_STR); 166 | $query->bindParam(':taskid' , $taskid , PDO::PARAM_INT); 167 | 168 | $query->execute(); 169 | 170 | $rowCount = $query->rowCount(); 171 | 172 | if ($rowCount === 0) { // if inserting the image fails 173 | // roll back in case of insertion failure 174 | if ($writeDB->inTransaction()) { 175 | $writeDB->rollBack(); 176 | } 177 | 178 | sendResponse(500, false, 'Failed to upload image'); // 500 means Internal Server Error 179 | } 180 | 181 | // Respond to the user who uploaded the image with a JSON response of the image attributes: 182 | 183 | $lastImageID = $writeDB->lastInsertId(); 184 | 185 | // Combining/JOIN-ing two tables (`tblimages` and `tbltasks`) to grab information from both of them (grab all the image attributes to send them to the user in a JSON response) 186 | // Retrieving the image with that $lastImageID for a given task, for the authenticated/logged-in user 187 | $query = $writeDB->prepare('SELECT tblimages.id, tblimages.title, tblimages.filename, tblimages.mimetype, tblimages.taskid 188 | FROM tblimages, tbltasks 189 | WHERE tblimages.id = :imageid AND tbltasks.id = :taskid AND tbltasks.userid = :userid 190 | AND tblimages.taskid = tbltasks.id' 191 | ); // We use $writeDB (not $readDB), because if 'replicated slaves' for MySQL are used, it can take a bit time to push across the updates from $writeDB to $readDB, so wee're going to use $writeDB to make sure we're getting the uploaded image successfully 192 | 193 | $query->bindParam(':imageid', $lastImageID , PDO::PARAM_INT); 194 | $query->bindParam(':taskid' , $taskid , PDO::PARAM_INT); 195 | $query->bindParam(':userid' , $returned_userid, PDO::PARAM_INT); 196 | 197 | $query->execute(); 198 | 199 | $rowCount = $query->rowCount(); 200 | 201 | if ($rowCount === 0) { // if grabbing the image details fails 202 | // roll back in case of failure 203 | if ($writeDB->inTransaction()) { 204 | $writeDB->rollBack(); 205 | } 206 | 207 | sendResponse(500, false, 'Failed to retrieve image attributes after upload - try uploading image again'); // 500 means Internal Server Error 208 | } 209 | 210 | $imageArray = array(); 211 | 212 | while ($row = $query->fetch(PDO::FETCH_ASSOC)) { 213 | $image = new Image($row['id'], $row['title'], $row['filename'], $row['mimetype'], $row['taskid']); // Pass in the image details to the Imgae model for validation and getting them back again 214 | $imageArray[] = $image->returnImageAsArray(); 215 | } 216 | 217 | // Note: We'll place images of a certain task in a folder named with the task id 218 | // Note: If moving the file using move_uploaded_file() function fails, we roll back the Database Transaction 219 | 220 | // Upload the image to server 221 | $image->saveImageFile($_FILES['imagefile']['tmp_name']); 222 | 223 | $writeDB->commit(); // Commit the transaction query to database 224 | 225 | // Send back to user a successful CREATED response with the uploaded image attribute 226 | sendResponse(201, true, 'Image uploaded successfully', false, $imageArray); // 201 means Created // We never cache the uploaded image response 227 | 228 | 229 | 230 | // We catch TWO catch-es (PDOException and ImageException) 231 | } catch (PDOException $ex) { // In case there's an error querying the database 232 | echo '
', var_dump($ex), '
'; // Display the thrown Exception Object 233 | echo '
', var_dump($ex->getMessage()), '
'; // Display the thrown Exception message 234 | 235 | error_log('Database Query Error: - ' . $ex, 0); // Send the real error to the system administrator // 0 means it stores/logs the error in the PHP error log file 236 | 237 | // In case that the transaction fails (one of the two operations fails) (We must place this after the error_log() function to allow the error to be logged for the administrator) 238 | if ($writeDB->inTransaction()) { 239 | $writeDB->rollBack(); 240 | } 241 | 242 | sendResponse(500, false, 'Failed to upload the image'); // 500 means Internal Server Error 243 | } catch (ImageException $ex) { 244 | // In case that the transaction fails (one of the two operations fails) (We must place this after the error_log() function to allow the error to be logged for the administrator) 245 | if ($writeDB->inTransaction()) { 246 | $writeDB->rollBack(); 247 | } 248 | 249 | sendResponse(500, false, $ex->getMessage()); // 500 means Internal Server Error // here we get the Exception class message (that we created inside the image.php model) using getMessage() 250 | } 251 | } 252 | 253 | 254 | 255 | function getImageAttributesRoute($readDB, $taskid, $imageid, $returned_userid) { // Get a certain task image attributes which are "title" and "filename" (not getting the image itself (not downloading image)) // e.g. GET /tasks/1/images/5/attributes 256 | // We perform our logic with try ... catch ... statement to be able to catch errors (we catch two exception classes: ImageExceptin and PDOException) 257 | try { 258 | // We need to combine/JOIN the two tables: `tblimages` and `tbltasks` because we need to grab data from both of them to make sure that for a given $taskid and imageid, we make sure it belongs to the user that's authenticated/logged-in: 259 | $query = $readDB->prepare('SELECT tblimages.id, tblimages.title, tblimages.filename, tblimages.mimetype, tblimages.taskid 260 | FROM tblimages, tbltasks 261 | WHERE tblimages.id = :imageid AND tbltasks.id = :taskid AND tbltasks.userid = :userid 262 | AND tblimages.taskid = tbltasks.id' 263 | ); 264 | 265 | $query->bindParam(':imageid', $imageid , PDO::PARAM_INT); 266 | $query->bindParam(':taskid' , $taskid , PDO::PARAM_INT); 267 | $query->bindParam(':userid' , $returned_userid , PDO::PARAM_INT); 268 | 269 | $query->execute(); 270 | 271 | $rowCount = $query->rowCount(); 272 | 273 | if ($rowCount === 0) { 274 | sendResponse(404, false, 'Image Not Found'); // 404 means Not Found 275 | } 276 | 277 | $imageArray = array(); 278 | 279 | while ($row = $query->fetch(PDO::FETCH_ASSOC)) { 280 | $image = new Image($row['id'], $row['title'], $row['filename'], $row['mimetype'], $row['taskid']); 281 | $imageArray[] = $image->returnImageAsArray(); 282 | } 283 | 284 | sendResponse(200, true, null, true, $imageArray); // 200 means Success or OK 285 | 286 | } catch (ImageException $ex) { 287 | sendResponse(500, false, $ex->getMessage()); // 500 means Internal Server Error // here we get the Exception class message (that we created inside the image.php model) using getMessage() 288 | error_log('Database Query Error: ' . $ex, 0); // Send the real error to the system administrator // 0 means it stores/logs the error in the PHP error log file 289 | sendResponse(500, false, 'Failed to get image attributes'); // 500 means Internal Server Error 290 | } 291 | } 292 | 293 | 294 | 295 | // Download an actual physical image 296 | function getImageRoute($readDB, $taskid, $imageid, $returned_userid) { // Get a certain task physical image itself (the binary image file) (download a task image) // e.g. GET /tasks/1/images/5 297 | try { 298 | // We need to combine/JOIN the two tables: `tblimages` and `tbltasks` because we need to grab data from both of them to make sure that for a given $taskid and imageid, we make sure it belongs to the user that's authenticated/logged in: 299 | $query = $readDB->prepare('SELECT tblimages.id, tblimages.title, tblimages.filename, tblimages.mimetype, tblimages.taskid 300 | FROM tblimages, tbltasks 301 | WHERE tblimages.id = :imageid AND tbltasks.id = :taskid AND tbltasks.userid = :userid 302 | AND tblimages.taskid = tbltasks.id' 303 | ); 304 | $query->bindParam(':imageid', $imageid , PDO::PARAM_INT); 305 | $query->bindParam(':taskid' , $taskid , PDO::PARAM_INT); 306 | $query->bindParam(':userid' , $returned_userid , PDO::PARAM_INT); 307 | $query->execute(); 308 | 309 | $rowCount = $query->rowCount(); 310 | 311 | if ($rowCount === 0) { 312 | sendResponse(404, false, 'Image Not Found'); // 404 means Not Found 313 | } 314 | 315 | $image = null; 316 | 317 | while ($row = $query->fetch(PDO::FETCH_ASSOC)) { 318 | $image = new Image($row['id'], $row['title'], $row['filename'], $row['mimetype'], $row['taskid']); 319 | } 320 | 321 | if ($image == null) { 322 | sendResponse(500, false, 'Image not found'); // 500 means Internal Server Error 323 | } 324 | 325 | // Return (Download) the actual physical image itself 326 | $image->returnImageFile(); 327 | 328 | 329 | } catch (ImageException $ex) { 330 | sendResponse(500, false, $ex->getMessage()); // 500 means Internal Server Error // here we get the Exception class message (that we created inside the image.php model) using getMessage() 331 | } catch (PDOException $ex) { 332 | error_log('Database Query Error: ' . $ex, 0); // Send the real error to the system administrator // 0 means it stores/logs the error in the PHP error log file 333 | sendResponse(500, false, 'Error getting Image (downloading)'); // 500 means Internal Server Error 334 | } 335 | } 336 | 337 | 338 | 339 | function updateImageAttributesRoute($writeDB, $taskid, $imageid, $returned_userid) { // Route (URL): UPDATE /tasks/{taskid}/images/{imageid}/attributes // e.g. PATCH /tasks/7/images/15/attributes 340 | try { 341 | // Make sure 'Content-Type' header is 'application/json' 342 | if ($_SERVER['CONTENT_TYPE'] != 'application/json') { 343 | sendResponse(400, false, 'Content type header not set to JSON');// 400 means client error (because the client hasn't submitted the right/proper data) 344 | } 345 | 346 | // Get the contents of the HTTP request Body 347 | $rawPatchData = file_get_contents('php://input'); // Retrieve the raw HTTP request body data 348 | 349 | // Check if the Body is JSON type 350 | if (!$jsonData = json_decode($rawPatchData)) { 351 | sendResponse(400, false, 'Request body is not valid JSON');// 400 means client error (because the client hasn't submitted the right/proper data) 352 | } 353 | 354 | // Keep track of which fields are asked to be updated: (initially set all of them to 'false') 355 | $title_updated = false; 356 | $filename_updated = false; 357 | 358 | // Formatting the SQL statement: (SQL statement e.g. 'UPDATE tbltasks SET title = :title, description = :description, deadline = :deadline, completed = :completed WHERE id = :taskid') 359 | // $queryFields will contain the previous fields that are set to 'true' 360 | $queryFields = ''; 361 | 362 | // Check which fields are asked to be updated, and set them to 'true' and add them to $queryFields 363 | if (isset($jsonData->title)) { 364 | $title_updated = true; // set/convert it from 'false' to 'true' 365 | $queryFields .= 'tblimages.title = :title, '; 366 | } 367 | 368 | if (isset($jsonData->filename)) { 369 | // We make sure that the file extension is NOT provided by user in the file name (we DON'T want the file extension to be provided by user), because the file extension is automatically (and dynamically) determined (is not part of the file name), so if the file name has extension, we send an error response back 370 | if (strpos($jsonData->filename, '.') !== false) { // if the filename has an extension // e.g. image.jpg 371 | sendResponse(400, false, 'Filename cannot contain any dots or file extensions');// 400 means client error (because the client hasn't submitted the right/proper data) 372 | } 373 | 374 | $filename_updated = true; // set/convert it from 'false' to 'true' 375 | $queryFields .= 'tblimages.filename = :filename, '; 376 | } 377 | 378 | // All the previous $queryFields have a comma and a space at the end of them, so we need to strip the comma and the space of the LAST queryField off: 379 | $queryFields = rtrim($queryFields, ', '); // strip off the LAST comma and space 380 | 381 | // Check if there's any data (title, filename) provided to be updated (set to 'true'), other than that, there's no an UPDATE query: 382 | if ($title_updated === false && $filename_updated === false) { 383 | sendResponse(400, false, 'No image fields provided');// 400 means client error (because the client hasn't submitted the right/proper data) 384 | } 385 | 386 | // We'll use a Database Transaction here, because We'll query the database with SELECT query to make sure the image exists and the image id exists, and we'll physically rename the image file as well. For example, if physically renaming the file fails and we've provided a new filename, then we mustn't update the database with that new filename, and then we do a rollback 387 | $query = $writeDB->beginTransaction(); // Because we use beginTransaction() function, we must use rollBack() function inside the TWO catch() statements (i.e. catch (ImageException $ex) and catch (PDOException $ex) ) and inside the if statement that checks if the query is successful 388 | 389 | // We need to combine/JOIN the two tables: `tblimages` and `tbltasks` because we need to grab data from both of them to make sure that for a given $taskid, we make sure the image exists and belongs to the user that's authenticated/logged-in: 390 | // We query the database to get the task image, to make sure it exists, before we update it 391 | $query = $writeDB->prepare('SELECT tblimages.id, tblimages.title, tblimages.filename, tblimages.mimetype, tblimages.taskid 392 | FROM tblimages, tbltasks 393 | WHERE tblimages.id = :imageid AND tblimages.taskid = :taskid 394 | AND tblimages.taskid = tbltasks.id 395 | AND tbltasks.userid = :userid' 396 | ); 397 | 398 | $query->bindParam(':imageid', $imageid , PDO::PARAM_INT); 399 | $query->bindParam(':taskid' , $taskid , PDO::PARAM_INT); 400 | $query->bindParam(':userid' , $returned_userid, PDO::PARAM_INT); 401 | 402 | $query->execute(); 403 | 404 | $rowCount = $query->rowCount(); 405 | 406 | if ($rowCount === 0) { 407 | if ($writeDB->inTransaction()) { // If we're in the middle of a Database Transaction, roll back 408 | $writeDB->rollBack(); 409 | } 410 | 411 | sendResponse(404, false, 'No image found to update'); // 404 means Not Found 412 | } 413 | 414 | while ($row = $query->fetch(PDO::FETCH_ASSOC)) { 415 | $image = new Image($row['id'], $row['title'], $row['filename'], $row['mimetype'], $row['taskid']); 416 | } 417 | 418 | // Build the UPDATE SQL query: 419 | // Because we're linking that to a $taskid, we need to JOIN tblimages to tbltasks table 420 | $queryString = 'UPDATE tblimages INNER JOIN tbltasks ON tblimages.taskid = tbltasks.id 421 | SET ' . $queryFields . ' WHERE tblimages.id = :imageid AND tblimages.taskid = tbltasks.id AND tblimages.taskid = :taskid AND tbltasks.userid = :userid'; 422 | $query = $writeDB->prepare($queryString); // Using the $writeDB (not $readDB) because we're UPDATE -ing 423 | 424 | // Bind the UPDATE parameters if they have been set 425 | // Overriding the new updated values in the $image class object which is the record that is being updated: (to be validated) // Set the updated new value, and get it again back out from the model 426 | if ($title_updated === true) { 427 | $image->setTitle($jsonData->title); // overriding (updating) - overwriting the new value over the old one in the object 428 | $up_title = $image->getTitle(); // Getting the title again, after having had been validated 429 | $query->bindParam(':title', $up_title, PDO::PARAM_STR); 430 | } 431 | 432 | if ($filename_updated === true) { 433 | $originalFilename = $image->getFilename(); // We need to store the file name before we've updated it temporarily 434 | 435 | $image->setFilename($jsonData->filename . '.' . $image->getFileExtension()); // overriding (updating) - overwriting the new value over the old one in the object 436 | $up_filename = $image->getFilename(); // Getting the filename again, after having had been validated 437 | $query->bindParam(':filename', $up_filename, PDO::PARAM_STR); 438 | } 439 | 440 | // Bind the rest of the parameters: 441 | $query->bindParam(':imageid', $imageid, PDO::PARAM_INT); 442 | $query->bindParam(':taskid' , $taskid , PDO::PARAM_INT); 443 | $query->bindParam(':userid' , $returned_userid , PDO::PARAM_INT); 444 | 445 | $query->execute(); 446 | 447 | $rowCount = $query->rowCount(); 448 | 449 | if ($rowCount === 0) { // if it's 0 zero, this means the update submitted data are the same as the already existing data // Because the number of the rows affected resulting from the UPDATE query are 0 zero, this means that the submitted data to be updated are exactly the same as the already existing data 450 | if ($writeDB->inTransaction()) { // If we're in the middle of a Database Transaction, roll back 451 | $writeDB->rollBack(); 452 | } 453 | 454 | sendResponse(400, false, 'Image attributes not updated - the given values may be the same as the stored values'); // Because the number of the rows affected resulting from the UPDATE query are 0 zero, this means that the submitted data to be updated are exactly the same as the already existing data // 400 Status Code means client error 455 | } 456 | 457 | // Return the updated record back to the client after having been updated: 458 | // Using the $writeDB (not $readDB) here because by the time that record has been UPDATE-ed, we immediately try to return it from the database, and it may not have had time to populate or push this record data from the writeDB to readDB, because readDB-s are Asynchronous (while writeDB-s are Synchronous), so we must use the writeDB here in this case even though it's just a READ statement: 459 | $query = $writeDB->prepare('SELECT tblimages.id, tblimages.title, tblimages.filename, tblimages.mimetype, tblimages.taskid FROM tblimages, tbltasks 460 | WHERE tblimages.id = :imageid AND tbltasks.id = :taskid 461 | AND tbltasks.id = tblimages.taskid 462 | AND tbltasks.userid = :userid' 463 | ); 464 | 465 | $query->bindParam(':imageid', $imageid, PDO::PARAM_INT); 466 | $query->bindParam(':taskid' , $taskid , PDO::PARAM_INT); 467 | $query->bindParam(':userid' , $returned_userid , PDO::PARAM_INT); 468 | 469 | $query->execute(); 470 | 471 | $rowCount = $query->rowCount(); 472 | 473 | if ($rowCount === 0) { 474 | if ($writeDB->inTransaction()) { // If we're in the middle of a Database Transaction, roll back 475 | $writeDB->rollBack(); 476 | } 477 | 478 | sendResponse(404, false, 'No Image Found'); // 404 means Not Found 479 | } 480 | 481 | // Put the image back into the image model, and return it back to the user as a JSON response 482 | $imageArray = array(); 483 | 484 | while ($row = $query->fetch(PDO::FETCH_ASSOC)) { 485 | $image = new Image($row['id'], $row['title'], $row['filename'], $row['mimetype'], $row['taskid']); 486 | $imageArray[] = $image->returnImageAsArray(); 487 | } 488 | 489 | // Rename the physical image file: 490 | if ($filename_updated === true) { // if the filename was updated (in the database), we need to update the actual file name on the server (on the filesystem) 491 | $image->renameImageFile($originalFilename, $up_filename); 492 | } 493 | 494 | // Save (commit) the Database Transaction changes: 495 | $writeDB->commit(); 496 | 497 | sendResponse(200, true, 'Image attributes updated', false, $imageArray); // 200 means Success or OK 498 | 499 | } catch (PDOException $ex) { 500 | echo '
', var_dump($ex), '
'; // Display the thrown Exception Object 501 | echo '
', var_dump($ex->getMessage()), '
'; // Display the thrown Exception message 502 | 503 | error_log('Database Query Error - ' . $ex, 0); // Send the real error to the system administrator // 0 means it stores/logs the error in the PHP error log file 504 | 505 | // In case that the transaction fails (one of the two operations fails) (We must place this after the error_log() function to allow the error to be logged for the administrator) 506 | if ($writeDB->inTransaction()) { 507 | $writeDB->rollBack(); 508 | } 509 | 510 | sendResponse(500, false, 'Failed to update image attributes - check your data for errors'); // 500 means Internal Server Error 511 | } catch (ImageException $ex) { 512 | echo '
', var_dump($ex), '
'; // Display the thrown Exception Object 513 | echo '
', var_dump($ex->getMessage()), '
'; // Display the thrown Exception message 514 | 515 | // In case that the transaction fails (one of the two operations fails) (We must place this after the error_log() function to allow the error to be logged for the administrator) 516 | if ($writeDB->inTransaction()) { 517 | $writeDB->rollBack(); 518 | } 519 | 520 | sendResponse(400, false, $ex->getMessage());// 400 means client error (because the client hasn't submitted the right/proper data) // here we get the Exception class message (that we created inside the image.php model) using getMessage() 521 | } 522 | } 523 | 524 | 525 | 526 | function deleteImageRoute($writeDB, $taskid, $imageid, $returned_userid) { // Delete the physical image file on the server (on the filesystem) and delete the image row out of the `tblimages` database table // Route (URL): DELETE /tasks/{taskid}/images/{imageid} // e.g. DELETE /tasks/1/images/5 527 | try { 528 | // Here we need a Database Transaction because we need BOTH delete the actual physical image file AND delete the image row in the database table AT THE SAME TIME, so we want if one the two operations fails, the other one gets undone 529 | // Because we use beginTransaction() method, we need to use rollBack() inside all of the catch statements blocks 530 | $writeDB->beginTransaction(); 531 | 532 | // Make sure that the $taskid, $imageid and $returned_userid that are passed in in the URL query string parameters all exist 533 | // Make sure the owner of the task is the authenticated user (logged-in user) 534 | $query = $writeDB->prepare( 535 | 'SELECT tblimages.id, tblimages.title, tblimages.filename, tblimages.mimetype, tblimages.taskid 536 | FROM tblimages, tbltasks 537 | WHERE tblimages.id = :imageid AND tbltasks.id = :taskid AND tbltasks.userid = :userid 538 | AND tblimages.taskid = tbltasks.id' 539 | ); 540 | 541 | $query->bindParam(':imageid', $imageid , PDO::PARAM_INT); 542 | $query->bindParam(':taskid' , $taskid , PDO::PARAM_INT); 543 | $query->bindParam(':userid' , $returned_userid, PDO::PARAM_INT); 544 | 545 | $query->execute(); 546 | 547 | $rowCount = $query->rowCount(); 548 | 549 | if ($rowCount === 0) { // if there's a filename, with the same filename that the user submitted, already exists in our database 550 | $writeDB->rollBack(); 551 | 552 | sendResponse(404, false, 'Image not found'); // 404 means Not Found 553 | } 554 | 555 | // Get the image back: 556 | $image = null; 557 | 558 | while ($row = $query->fetch(PDO::FETCH_ASSOC)) { 559 | $image = new Image($row['id'], $row['title'], $row['filename'], $row['mimetype'], $row['taskid']); 560 | } 561 | 562 | if ($image == null) { 563 | $writeDB->rollBack(); 564 | 565 | sendResponse(500, false, 'Failed to get Image'); // 500 means Internal Server Error 566 | } 567 | 568 | // Delete the image row in `tblimages` database table 569 | // Using 'JOIN' with DELETE query 570 | $query = $writeDB->prepare( 571 | 'DELETE tblimages FROM tblimages, tbltasks 572 | WHERE tblimages.id = :imageid AND tbltasks.id = :taskid 573 | AND tblimages.taskid = tbltasks.id 574 | AND tbltasks.userid = :userid' 575 | ); 576 | 577 | $query->bindParam(':imageid', $imageid , PDO::PARAM_INT); 578 | $query->bindParam(':taskid' , $taskid , PDO::PARAM_INT); 579 | $query->bindParam(':userid' , $returned_userid, PDO::PARAM_INT); 580 | 581 | $query->execute(); 582 | 583 | $rowCount = $query->rowCount(); 584 | 585 | if ($rowCount === 0) { 586 | $writeDB->rollBack(); 587 | 588 | sendResponse(404, false, 'Image Not Found'); // 404 means Not Found 589 | } 590 | 591 | // Delete the actual physical image file from the server (from the filesystem): 592 | $image->deleteImageFile(); 593 | 594 | // Save (commit) the Database Transaction changes: 595 | $writeDB->commit(); 596 | 597 | sendResponse(200, true, 'Image Deleted'); // 200 means Success or OK 598 | 599 | } catch (PDOException $ex) { // In case there's an error querying the database 600 | error_log('Database Query Error: - ' . $ex, 0); // Send the real error to the system administrator // 0 means it stores/logs the error in the PHP error log file 601 | 602 | $writeDB->rollBack(); 603 | 604 | sendResponse(500, false, 'Failed to delete image'); // 500 means Internal Server Error 605 | } catch (ImageException $ex) { 606 | $writeDB->rollBack(); 607 | 608 | sendResponse(500, false, $ex->getMessage()); // 500 means Internal Server Error // here we get the Exception class message (that we created inside the image.php model) using getMessage() 609 | } 610 | } 611 | 612 | 613 | 614 | // This method return-s the `userid` of the authenticated/logged-in user 615 | function checkAuthStatusAndReturnUserID($writeDB) { // A helper function to check the authorization status (logged in and using access token) and return-s the user id `userid` of the authenticated/logged-in user 616 | // Before deciding which route is used (BEFORE anything and everything), we add the authorization (authentication) script here (to make sure user has provided the 'access token' and check it to make sure it's valid and hasn't expired and that the user is valid): 617 | 618 | // Start authorization (authentication) script: 619 | // Make sure the access token from the HTTP header (the authorization header) is provided and it's not empty (left blank) 620 | if (!isset($_SERVER['HTTP_AUTHORIZATION']) || strlen($_SERVER['HTTP_AUTHORIZATION']) < 1) { 621 | // Doing some Code Refactoring: 622 | $message = null; 623 | 624 | if (!isset($_SERVER['HTTP_AUTHORIZATION'])) { 625 | $message = 'Access token is missing from the header'; 626 | } else { // if the access token exits but no value provided for it 627 | if (strlen($_SERVER['HTTP_AUTHORIZATION']) < 1) { 628 | $message = 'Access token cannot be blank'; 629 | } 630 | } 631 | sendResponse(401, false, $message); // 401 means Unauthorized 632 | } 633 | 634 | $accesstoken = $_SERVER['HTTP_AUTHORIZATION']; 635 | 636 | // We're going to query the database, so we should use try ... catch ... statement: 637 | try { 638 | // Perform a database query based on that $accesstoken to bring back its user details and session details so we can check `useractive`, `loginattempts` and the access token hasn't expired, ...etc 639 | $query = $writeDB->prepare( 640 | 'SELECT tblsessions.userid, tblsessions.accesstokenexpiry, tblusers.useractive, tblusers.loginattempts 641 | FROM tblsessions, tblusers WHERE tblsessions.userid = tblusers.id 642 | AND accesstoken = :accesstoken' 643 | ); 644 | 645 | $query->bindParam(':accesstoken', $accesstoken, PDO::PARAM_STR); 646 | 647 | $query->execute(); 648 | 649 | $rowCount = $query->rowCount(); 650 | 651 | if ($rowCount === 0) { 652 | sendResponse(401, false, 'Invalid Access Token'); // 401 means Unauthorized 653 | } 654 | 655 | // Here, we're not doing anything with the 'refresh toke' as it's handled by the Authentication API (in sessions.php page) only. Here, we just check if the 'Access Token' is still valid (i.e. not expired) 656 | 657 | $row = $query->fetch(PDO::FETCH_ASSOC); 658 | 659 | $returned_userid = $row['userid']; 660 | $returned_accesstokenexpiry = $row['accesstokenexpiry']; 661 | $returned_useractive = $row['useractive']; 662 | $returned_loginattempts = $row['loginattempts']; 663 | 664 | // Making sure the user is active: 665 | if ($returned_useractive !== 'Y') { // if the user is not active ('Y' means yes) 666 | sendResponse(401, false, 'User account not active'); // 401 Status Code means Unauthorized 667 | } 668 | 669 | // Making sure the user is not currently locked out: 670 | if ($returned_loginattempts >= 3) { // if number of login attemps is equal to or greater than 3, user is locked out of their account and they need a reset 671 | sendResponse(401, false, 'User account is currently locked out'); // 401 means Unauthorized 672 | } 673 | 674 | // Checking if the access token is still valid or has expired: 675 | if (strtotime($returned_accesstokenexpiry) < time()) { // if the time stored in the database (the access token expiry date) is less than the current time, this means the access token has expired 676 | sendResponse(401, false, 'Access token has expired'); // 401 Status Code means Unauthorized 677 | } 678 | 679 | 680 | 681 | // Returning the user ID (based on the 'Access Token' that they provided): 682 | return $returned_userid; 683 | 684 | 685 | } catch (PDOException $ex) { // If querying the database fails 686 | echo '
', var_dump($ex), '
'; // Display the thrown Exception Object 687 | echo '
', var_dump($ex->getMessage()), '
'; // Display the thrown Exception message 688 | sendResponse(500, false, 'There was an issue authenticating - please try again'); // 500 means Internal Server Error 689 | } 690 | // End authorization (authentication) script 691 | 692 | // NEXT: FOR EACH SQL QUERY IN THAT PAGE (FOR EACH ROUTE), WE MUST MAKE SURE IT HAS TAKEN INTO ACCOUNT THE `userid`! (in order to limit that a user can only view their own images only and not other people images (to view only the images that belong to a user only, and not other users/people images)) (user-centric or per-user basis) 693 | } 694 | 695 | 696 | // Note: Every task has one or multiple images 697 | 698 | /* 699 | Our Routes/Endpoints are: 700 | Get a certain task's certain image attributes: GET /tasks/{taskid}/images/{imageid}/attributes 701 | Get (Download) an image itself of a certain task: GET /tasks/{taskid}/images/{imageid} 702 | Create (upload) an image for a certain task: POST /tasks/{taskid}/images 703 | Delete an image itself of a certain task: DELETE /tasks/{taskid}/images/{imageid} 704 | */ 705 | 706 | 707 | 708 | // Connect to the database: 709 | try { 710 | $writeDB = DB::connectWriteDB(); 711 | $readDB = DB::connectReadDB(); 712 | } catch (PDOException $ex) { // If there's an error connecting to one of the two databases 713 | echo '
', var_dump($ex), '
'; // Display the thrown Exception Object 714 | echo '
', var_dump($ex->getMessage()), '
'; // Display the thrown Exception message 715 | 716 | error_log('Connection Error - ' . $ex, 0); // Send the real error to the system administrator // 0 means it stores/logs the error in the PHP error log file 717 | sendResponse(500, false, 'Database connectin error'); // 500 means Internal Server Error 718 | } 719 | 720 | 721 | 722 | // Doing Authentication and Getting the user id of the user that's trying to access our API endpoints (or routes): 723 | $returned_userid = checkAuthStatusAndReturnUserID($writeDB); // Authentication should be done always against the master database (write database) because it's synchronous and up-to-date, as apposed to the read database which is asynchronous 724 | 725 | 726 | 727 | // If the three of {taskid}, {imageid} and {attributes} Query String Parameters are provided in the URL, we handle either Get a certain image Attributes (of a certain task that belongs to the authenticated/logged-in user) (GET), or Update a certain image Attributes (of a certain task that belongs to the authenticated/logged-in user) (PATCH): GET /tasks/{taskid}/images/{imageid}/attributes // e.g. GET /tasks/3/images/5/attributes Or PATCH /tasks/{taskid}/images/{imageid}/attributes // e.g. PATCH /tasks/3/images/5/attributes 728 | if (array_key_exists('taskid', $_GET) && array_key_exists('imageid', $_GET) && array_key_exists('attributes', $_GET)) { 729 | $taskid = $_GET['taskid']; 730 | $imageid = $_GET['imageid']; 731 | $attributes = $_GET['attributes']; 732 | 733 | // Do some Validation: 734 | if ($taskid == '' || !is_numeric($taskid) || $imageid == '' || !is_numeric($imageid)) { 735 | sendResponse(400, false, 'Image ID or Task ID cannot be blank and must be numeric'); // 400 means client error (because the client hasn't submitted the right/proper data) 736 | } 737 | 738 | // Route: Get a certain image attributes of a certain task: GET /tasks/{taskid}/images/{imageid}/attributes // e.g. GET /tasks/1/images/5/attributes 739 | if ($_SERVER['REQUEST_METHOD'] === 'GET') { 740 | getImageAttributesRoute($readDB, $taskid, $imageid, $returned_userid); 741 | 742 | // Route: Update a certain image attributes of a certain task: PATCH /tasks/{taskid}/images/{imageid}/attributes // e.g. PATCH /tasks/1/images/5/attributes 743 | } elseif ($_SERVER['REQUEST_METHOD'] === 'PATCH') { 744 | updateImageAttributesRoute($writeDB, $taskid, $imageid, $returned_userid); 745 | 746 | } else { // We don't allow any request methods other than GET or PATCH for this route 747 | sendResponse(405, false, 'Request method not allowed'); // 405 means Request Method Not Allowed 748 | } 749 | 750 | // If both {taskid} and {imageid} Query String Parameters are provided in the URL, we handle either Get (Download) an actual physical image of a certain task (GET) or Delete an actual physical image of a certain task (of the authenticated/logged-in user): GET /tasks/{taskid}/images/{imageid} // e.g. GET /tasks/3/images/5 or DELETE /tasks/{taskid}/images/{imageid} // e.g. DELETE /tasks/3/images/5 751 | } elseif (array_key_exists('taskid', $_GET) && array_key_exists('imageid', $_GET)) { 752 | $taskid = $_GET['taskid']; 753 | $imageid = $_GET['imageid']; 754 | 755 | // Do some validation: 756 | if ($taskid == '' || !is_numeric($taskid) || $imageid == '' || !is_numeric($imageid)) { 757 | sendResponse(400, false, 'Image ID or Task ID cannot be blank and must be numeric'); // 400 means client error (because the client hasn't submitted the right/proper data) 758 | } 759 | 760 | // Get (Download) an actual physical image (the binary image file) of a certain task: GET /tasks/{taskid}/images/{imageid} // e.g. GET /tasks/5/images/2 761 | if ($_SERVER['REQUEST_METHOD'] === 'GET') { 762 | getImageRoute($readDB, $taskid, $imageid, $returned_userid); 763 | 764 | // Delete an actual physical image of a certain task: DELETE /tasks/{taskid}/images/{imageid} // e.g. DELETE /tasks/5/images/2 765 | } elseif ($_SERVER['REQUEST_METHOD'] === 'DELETE') { 766 | deleteImageRoute($writeDB, $taskid, $imageid, $returned_userid); 767 | 768 | } else { // We don't allow any request methods other than GET or DELETE for this route 769 | sendResponse(405, false, 'Request method not allowed'); // 405 means Request Method Not Allowed 770 | } 771 | 772 | // If the {taskid} Query String Parameter is provided in the URL while the {imageid} is NOT provided, we handle Create (upload) an image for a certain task (that belongs to the authenticated/logged-in user): POST /tasks/{taskid}/images // e.g. POST /tasks/3/images 773 | } elseif (array_key_exists('taskid', $_GET) && !array_key_exists('imageid', $_GET)) { // making sure that 'imageid' does NOT exist in the URL 774 | $taskid = $_GET['taskid']; 775 | 776 | // Do some validation: 777 | if ($taskid == '' || !is_numeric($taskid)) { 778 | sendResponse(400, false, 'Task ID cannot be blank and must be numeric'); // 400 means client error (because the client hasn't submitted the right/proper data) 779 | } 780 | 781 | if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Create (upload) an image belonging to a certain task 782 | uploadImageRoute($readDB, $writeDB, $taskid, $returned_userid); 783 | 784 | } else { // We don't allow any request methods other than POST for this route 785 | sendResponse(405, false, 'Request method not allowed'); // 405 means Request Method Not Allowed 786 | } 787 | 788 | 789 | } else { // if the URL were random like /v1/ghag254152 or anything other than that what we previously specified 790 | sendResponse(404, false, 'Endpoint not found'); // 404 means Not Found 791 | } -------------------------------------------------------------------------------- /v1/controller/sessions.php: -------------------------------------------------------------------------------- 1 | setHTTPStatusCode(500); // 500 is Internal Server Error 20 | $response->setSuccess(false); 21 | $response->addMessage('Database connection error'); 22 | $response->send(); 23 | exit; // to guarantee exiting out of script after sending the response 24 | } 25 | 26 | 27 | // 'if statements' that will select the logic based on the route: 28 | // Our routes here are two (two routes and three HTTP request methods (POST (create), DELETE (delete), PATCH (update)): /sessions which will be a POST request because it's going to be used to CREATE a session (i.e. login), or /sessions/{sessionid} e.g. /sessions/3 which will be a DELETE request because it's going to be used to DELETE a session (i.e. logging out or logout) Or a PATCH request to UPDATE/refresh a session (i.e. refresh the 'access token' to get a new one): 29 | /* 30 | Our routes/endpoints: 31 | v1/sessions is for POST - CREATE a session/log in 32 | v1/sessions/3 is for DELETE - Log out a user 33 | v1/sessions/3 is for PATCH - Refresh session 34 | */ 35 | 36 | 37 | // If the {sessionid} Query String Parameter is provided / exists in the URL, the we handle either DELETE or PATCH requests (log out (DELETE) and refresh tokens (PATCH)) // API Endpoint: DELETE or PATCH v1/sessions/34 38 | if (array_key_exists('sessionid', $_GET)) { // the route/endpoint has a query string which contains '/sessions.php/?sessionid=5' which maps to /sessions/5 in our .htaccess file, for example /sessions.php?sessionid=3 (equivalent to or maps to /sessions/3 in .htaccess file) which is either DELETE request (logging out) or PATCH request (refresh session or access token) 39 | // Getting the session id from the URL: // API Endpoint: DELETE or PATCH v1/sessions/34 40 | $sessionid = $_GET['sessionid']; 41 | 42 | // Do some Validation check (make sure it's not blank and it's a numerical number (not text)): 43 | if ($sessionid === '' || !is_numeric($sessionid)) { 44 | $response = new Response(); 45 | $response->setHttpStatusCode(400); // 400 means client error (because the client hasn't submitted the right type of data) 46 | $response->setSuccess(false); 47 | ($sessionid === '' ? $response->addMessage('Session ID cannot be blank') : false); 48 | (!is_numeric($sessionid) ? $response->addMessage('Session ID must be numeric') : false); 49 | $response->send(); 50 | exit; // to guarantee exiting out of script after sending the response 51 | } 52 | 53 | // Do some validation on the 'access token' that the user is going to send in the HTTP request header: 54 | // Note: Apache web server doesn't allow sending authentication HTTP request header out of the box (by default), so we need to explicitly enable that in the .htaccess file. 55 | // Making sure the authorization header with the access token is submitted/provided by the client and not blank: 56 | if (!isset($_SERVER['HTTP_AUTHORIZATION']) || strlen($_SERVER['HTTP_AUTHORIZATION']) < 1) { 57 | $response = new Response(); 58 | $response->setHTTPStatusCode(401); // 401 means Unauthorized (because client didn't provide the authorization token) 59 | $response->setSuccess(false); 60 | (!isset($_SERVER['HTTP_AUTHORIZATION']) ? $response->addMessage('Access token is missing from the header') : false); 61 | (strlen($_SERVER['HTTP_AUTHORIZATION']) < 1 ? $response->addMessage('Access token cannot be blank') : false); 62 | 63 | $response->send(); 64 | exit; // to guarantee exiting out of script after sending the response 65 | } 66 | 67 | $accesstoken = $_SERVER['HTTP_AUTHORIZATION']; 68 | 69 | // We'll handle either a DELETE request to delete a session (logout) or a PATCH request to update a session (refresh an access token): 70 | 71 | if ($_SERVER['REQUEST_METHOD'] === 'DELETE') { // Handling the DELETE to delete a session (log out) // API Endpoint: DELETE /v1/sessions/{sessionid} 72 | // Delete the session row from the `tblsessions` database table: 73 | try { 74 | $query = $writeDB->prepare('DELETE FROM tblsessions WHERE id = :sessionid AND accesstoken = :accesstoken'); 75 | $query->bindParam(':sessionid', $sessionid, PDO::PARAM_INT); 76 | $query->bindParam(':accesstoken', $accesstoken, PDO::PARAM_STR); 77 | $query->execute(); 78 | 79 | $rowCount = $query->rowCount(); 80 | 81 | if ($rowCount === 0) { 82 | $response = new Response(); 83 | $response->setHttpStatusCode(400); // 400 means client error 84 | $response->setSuccess(false); 85 | $response->addMessage('Failed to log out of this session using access token provided'); 86 | $response->send(); 87 | exit; // to guarantee exiting out of script after sending the response 88 | } 89 | 90 | $returnData = array(); 91 | $returnData['session_id'] = intval($sessionid); 92 | 93 | // Send a successful response (successful logout): 94 | $response = new Response(); 95 | $response->setHttpStatusCode(200); // 200 means Success or OK 96 | $response->setSuccess(true); 97 | $response->addMessage('Logged out'); 98 | $response->setData($returnData); 99 | $response->send(); 100 | exit; // to guarantee exiting out of script after sending the response 101 | 102 | 103 | } catch (PDOException $ex) { // if there's someting wrong with querying the database: 104 | $response = new Response(); 105 | $response->setHTTPStatusCode(500); // 500 is Internal Server Error 106 | $response->setSuccess(false); 107 | $response->addMessage('There was an issue logging out - please try again'); 108 | $response->send(); 109 | exit; // to guarantee exiting out of script after sending the response 110 | } 111 | 112 | 113 | // Handling the PATCH to refresh session / refresh an access token // API Endpoint: PATCH /v1/sessions/{sessionid} 114 | } elseif ($_SERVER['REQUEST_METHOD'] === 'PATCH') { // Handling the PATCH to update a session (refresh the 'access token') // this will allow a client to refresh an 'access token' when it's expired (or just before it expires), in return, this will allow a client to get a new 'access token' that will be valid for the next 20 minutes. Within this functionality, we'll be performing a check on the passed in 'refresh token' (that was passed in the request body using JSON format) to make sure that 'refresh token' hasn't expired. If the 'refresh token' has also expired, then we won't be able to refresh the 'access token' and the user would be required to fully log in again. 115 | // Both "Access Token" and "Refresh Token" are required // "Access Token" is required to be sent as an "Authorization" HTTP Request Header, and "Refresh Token" is required to be sent as JSON in the HTTP Request Body // The 'refresh token' will be passed in in the HTTP request body (unlike the 'access token' that's passed in an 'Authorizatin' HTTP request header, not in body) // the 'access token' will be required too, which must be sent as an "Authorization" HTTP Request Header 116 | // Note: To get rid of the `tblsessions` database table rows that have EXPIRED sessions, on the server you should create a scheduled script (Cron jobs) that runs every one or two days to clear out (delete) the expired sessions. 117 | 118 | // Making sure that the content type of the HTTP request of the data sent is JSON type: 119 | if ($_SERVER['CONTENT_TYPE'] !== 'application/json') { // if the HTTP request header content type is not JSON or not provided in the HTTP Request 120 | $response = new Response(); 121 | $response->setHttpStatusCode(400); // 400 means client error (because the client hasn't submitted the right type of data (the right type of header)) 122 | $response->setSuccess(false); 123 | $response->addMessage('Content Type header not set to JSON'); 124 | $response->send(); 125 | exit; // to guarantee exiting out of script after sending the response 126 | } 127 | 128 | // Do some validation checks: 129 | 130 | // making sure that the 'refresh token' exists and is not empty: 131 | // Inspect the HTTP request Body (not the HTTP headers): 132 | $rawPatchData = file_get_contents('php://input'); 133 | 134 | // Making sure the data that are submitted are of JSON type: 135 | if (!$jsonData = json_decode($rawPatchData)) { // If it doesn't succeed to decode the data (JSON) sent, this means the sent/submitted body data is not valid JSON 136 | $response = new Response(); 137 | $response->setHttpStatusCode(400); // 400 means client error (because the client hasn't submitted the right type of data) 138 | $response->setSuccess(false); 139 | $response->addMessage('Request body is not valid JSON'); 140 | $response->send(); 141 | exit; // to guarantee exiting out of script after sending the response 142 | } 143 | 144 | // Making sure the 'refresh token' has been submitted/provided and not empty (not blank) in the HTTP Request Body (not as an "Authorizatin" HTTP Request Header): 145 | if (!isset($jsonData->refresh_token) || strlen($jsonData->refresh_token) < 1) { 146 | $response = new Response(); 147 | $response->setHttpStatusCode(400); // 400 means client error (because the client hasn't submitted the right type of data) 148 | $response->setSuccess(false); 149 | (!isset($jsonData->refresh_token) ? $response->addMessage('Refresh token not supplied') : false); 150 | (strlen($jsonData->refresh_token) < 1 ? $response->addMessage('Refresh token cannot be blank') : false); 151 | $response->send(); 152 | exit; // to guarantee exiting out of script after sending the response 153 | } 154 | 155 | 156 | try { 157 | $refreshtoken = $jsonData->refresh_token; 158 | 159 | // We need to perfrom an SQL query using JOIN statement because we're going to bring back data from two tables: `tblsessions` and `tblusers` through the common column `tblsessions`.`userid` which is a FOREIGN KEY in the `tblsessions` table which REFERENCES the `id` column in the `tblusers` table. That's because we need to bring back the session details to validate the 'refresh token' and 'access token' and things like that, and we also need to bring the `users` table row back or the user details because we need to perform `useractive` and `loginattempts` because these are still valid if we want to refresh the token. We need to perform validation checks. 160 | $query = $writeDB->prepare( 161 | 'SELECT tblsessions.id AS sessionid, tblsessions.userid AS userid, tblsessions.accesstoken, tblsessions.refreshtoken, tblusers.useractive, tblusers.loginattempts, tblsessions.accesstokenexpiry, tblsessions.refreshtokenexpiry 162 | FROM tblsessions, tblusers 163 | WHERE tblusers.id = tblsessions.userid 164 | AND tblsessions.id = :sessionid AND tblsessions.accesstoken = :accesstoken AND tblsessions.refreshtoken = :refreshtoken' 165 | ); 166 | 167 | $query->bindParam(':sessionid' , $sessionid , PDO::PARAM_INT); 168 | $query->bindParam(':accesstoken' , $accesstoken , PDO::PARAM_STR); 169 | $query->bindParam(':refreshtoken', $refreshtoken, PDO::PARAM_STR); 170 | $query->execute(); 171 | 172 | $rowCount = $query->rowCount(); 173 | 174 | if ($rowCount === 0) { 175 | $response = new Response(); 176 | $response->setHTTPStatusCode(401); // 401 means Unauthorized 177 | $response->setSuccess(false); 178 | $response->addMessage('Access token or refresh token is incorrect for session id'); // For security reasons, don't give very specific messages about what's wrong (username specifically or password specifically) because that could allow a potential hacker to work out based on your logic of what's right and what's wrong 179 | $response->send(); 180 | exit; // to guarantee exiting out of script after sending the response 181 | } 182 | 183 | // Bring back the row with that specific session id, access token and refresh token: 184 | $row = $query->fetch(PDO::FETCH_ASSOC); 185 | 186 | $returned_sessionid = $row['sessionid']; // The SQL Alias 187 | $returned_userid = $row['userid']; // The SQL Alias 188 | $returned_accesstoken = $row['accesstoken']; 189 | $returned_refreshtoken = $row['refreshtoken']; 190 | $returned_useractive = $row['useractive']; 191 | $returned_loginattempts = $row['loginattempts']; 192 | $returned_accesstokenexpiry = $row['accesstokenexpiry']; 193 | $returned_refreshtokenexpiry = $row['refreshtokenexpiry']; 194 | 195 | if ($returned_useractive !== 'Y') { 196 | $response = new Response(); 197 | $response->setHTTPStatusCode(401); // 401 means Unauthorized 198 | $response->setSuccess(false); 199 | $response->addMessage('User account is not active'); 200 | $response->send(); 201 | exit; // to guarantee exiting out of script after sending the response 202 | } 203 | 204 | if ($returned_loginattempts >= 3) { // if the account is locked out 205 | $response = new Response(); 206 | $response->setHTTPStatusCode(401); // 401 means Unauthorized 207 | $response->setSuccess(false); 208 | $response->addMessage('User account is currently locked out'); 209 | $response->send(); 210 | exit; // to guarantee exiting out of script after sending the response 211 | } 212 | 213 | // Checking if the refresh token is still valid or has expired: 214 | if (strtotime($returned_refreshtokenexpiry) < time()) { // if the time stored in the database (the refresh token expiry date) is less than the current time (i.e. the NOW!), this means the refresh token has expired 215 | $response = new Response(); 216 | $response->setHTTPStatusCode(401); // 401 means Unauthorized 217 | $response->setSuccess(false); 218 | $response->addMessage('Refresh token has expired - please log in again'); 219 | $response->send(); 220 | exit; // to guarantee exiting out of script after sending the response 221 | } 222 | 223 | // Regenerate a new access token with a new access token expiry time: (and every time when we generate a new access token, we regenrate a new refresh token too) 224 | // Every time we generate a new 'access token', we must generate a new 'refresh token' too 225 | $accesstoken = base64_encode(bin2hex(openssl_random_pseudo_bytes(24)) . time()); 226 | $refreshtoken = base64_encode(bin2hex(openssl_random_pseudo_bytes(24)) . time()); 227 | 228 | $access_token_expiry_seconds = 1200; // 20 minutes 229 | $refresh_token_expiry_seconds = 1209600; // 14 days (2 weeks) 230 | 231 | // Update our current session (we update the access token for the current session): 232 | // We must make sure we're updating the right session, because a user might have multiple sessions (using another browser or using other devices like mobile phone, laptop, table, ...), so must use 'WHERE' in the SQL query with the right access token: 233 | $query = $writeDB->prepare( 234 | 'UPDATE tblsessions SET 235 | accesstoken = :accesstoken, accesstokenexpiry = DATE_ADD(NOW(), INTERVAL :accesstokenexpiryseconds SECOND), refreshtoken = :refreshtoken, refreshtokenexpiry = DATE_ADD(NOW(), INTERVAL :refreshtokenexpiryseconds SECOND) 236 | WHERE id = :sessionid AND userid = :userid AND accesstoken = :returnedaccesstoken AND refreshtoken = :returnedrefreshtoken' 237 | ); 238 | 239 | $query->bindParam(':userid' , $returned_userid , PDO::PARAM_INT); 240 | $query->bindParam(':sessionid' , $returned_sessionid , PDO::PARAM_INT); 241 | $query->bindParam(':accesstoken' , $accesstoken , PDO::PARAM_STR); 242 | $query->bindParam(':accesstokenexpiryseconds' , $access_token_expiry_seconds , PDO::PARAM_INT); 243 | $query->bindParam(':refreshtoken' , $refreshtoken , PDO::PARAM_STR); 244 | $query->bindParam(':refreshtokenexpiryseconds', $refresh_token_expiry_seconds, PDO::PARAM_INT); 245 | $query->bindParam(':returnedaccesstoken' , $returned_accesstoken , PDO::PARAM_STR); 246 | $query->bindParam(':returnedrefreshtoken' , $returned_refreshtoken , PDO::PARAM_STR); 247 | $query->execute(); 248 | // Note: To get rid of the `tblsessions` database table rows that have EXPIRED sessions, on the server you should create a scheduled script that runs every one or two days to clear out (delete) the expired sessions. 249 | 250 | $rowCount = $query->rowCount(); 251 | 252 | if ($rowCount === 0) { 253 | $response = new Response(); 254 | $response->setHTTPStatusCode(401); // 401 means Unauthorized 255 | $response->setSuccess(false); 256 | $response->addMessage('Access token could not be refreshed - please log in again'); 257 | $response->send(); 258 | exit; // to guarantee exiting out of script after sending the response 259 | } 260 | 261 | // Return the new session details to the user: 262 | // Every time we generate a new 'access token', we must generate a new 'refresh token' too 263 | $returnData = array(); 264 | $returnData['sessionid'] = $returned_sessionid; 265 | $returnData['access_token'] = $accesstoken; 266 | $returnData['access_token_expiry'] = $access_token_expiry_seconds; 267 | $returnData['refresh_token'] = $refreshtoken; 268 | $returnData['refresh_token_expiry'] = $refresh_token_expiry_seconds; 269 | 270 | // Send a successful response with newly updated session details: 271 | $response = new Response(); 272 | $response->setHttpStatusCode(200); // 200 means Success or OK 273 | $response->setSuccess(true); 274 | $response->addMessage('Token refreshed'); 275 | $response->setData($returnData); 276 | $response->send(); 277 | exit; // to guarantee exiting out of script after sending the response 278 | 279 | } catch (PDOException $ex) { 280 | $response = new Response(); 281 | $response->setHTTPStatusCode(500); // 500 is Internal Server Error 282 | $response->setSuccess(false); 283 | $response->addMessage('There was an issue refreshing access token - please log in again'); 284 | $response->send(); 285 | exit; // to guarantee exiting out of script after sending the response 286 | } 287 | 288 | // Note: To get rid of the `tblsessions` database table rows that have EXPIRED sessions, on the server you should create a scheduled script that runs every one or two days to clear out (delete) the expired sessions. 289 | 290 | } else { // anything other than DELETE or PATCH request methods, we send an error response: 291 | $response = new Response(); 292 | $response->setHttpStatusCode(405); // 405 means Request Method Not Allowed 293 | $response->setSuccess(false); 294 | $response->addMessage('Request method not allowed'); 295 | $response->send(); 296 | exit; // to guarantee exiting out of script after sending the response 297 | } 298 | 299 | // If the URL doesn't contain the {sessionid} Query String Parameter, then it's Log in: Create a new session or log in (a new access token and refresh token) // API Endpoint: POST /v1/sessions 300 | } elseif (empty($_GET)) { // check if the route doesn't have a query string of '?sessionid=' (equivalent to or maps to /sessions.php) which means it's a POST request 301 | // Note: To get rid of the `tblsessions` database table rows that have EXPIRED sessions, on the server you should create a scheduled script that runs every one or two days to clear out (delete) the expired sessions. 302 | // We will hand POST methods only (to create a session (i.e. login a user)): 303 | if ($_SERVER['REQUEST_METHOD'] !== 'POST') { // We won't allow any method other than 'POST' to create a session (i.e. login) 304 | $response = new Response(); 305 | $response->setHttpStatusCode(405); // 405 means Request Method Not Allowed 306 | $response->setSuccess(false); 307 | $response->addMessage('Request method not allowed'); 308 | $response->send(); 309 | exit; // to guarantee exiting out of script after sending the response 310 | } else { 311 | // A security measure: if some hacker uses brute force to hit one of our API endpoints (in case we have a powerful server (not a small server that can't handle many requests per second) that could handle like 50 hundred requests per second which means a hacker could try a hundred passwords per second (from a dictionary of username and passwords combinations)), so we're going to delay requests by 1 second between every request using sleep() function (which will affect and delay a hacker with many requests but a normal user logging in almost won't feel delay) 312 | sleep(1); // sleep or hold or delay for one second 313 | 314 | 315 | // Validation 316 | // Making sure that the content type of the HTTP request of the data sent to Create a session is JSON type: 317 | if ($_SERVER['CONTENT_TYPE'] !== 'application/json') { // if the HTTP request header content type is not JSON or not provided in the HTTP Request 318 | $response = new Response(); 319 | $response->setHttpStatusCode(400); // 400 means client error (because the client hasn't submitted the right type of data (JSON)) 320 | $response->setSuccess(false); 321 | $response->addMessage('Content Type header not set to JSON'); 322 | $response->send(); 323 | exit; // to guarantee exiting out of script after sending the response 324 | } 325 | 326 | // Getting and handling the data that is coming from the POST request: 327 | $rawPostData = file_get_contents('php://input'); // Retrieve the raw HTTP request body data // getting the POST request BODY 328 | 329 | // Validation 330 | // Making sure the data submitted is valid JSON: 331 | if (!$jsonData = json_decode($rawPostData)) { 332 | $response = new Response(); 333 | $response->setHttpStatusCode(400); // 400 means client error (because the client hasn't submitted the right type of data) 334 | $response->setSuccess(false); 335 | $response->addMessage('Request body is not valid JSON'); 336 | $response->send(); 337 | exit; // to guarantee exiting out of script after sending the response 338 | } 339 | 340 | // Validation 341 | // Making sure the mandatory fields (username and password) are filld in or provided (submitted) with the POST request: 342 | if (!isset($jsonData->username) || !isset($jsonData->password)) { 343 | $response = new Response(); 344 | $response->setHttpStatusCode(400); // 400 means client error 345 | $response->setSuccess(false); 346 | (!isset($jsonData->username) ? $response->addMessage('Username not supplied') : false); 347 | (!isset($jsonData->password) ? $response->addMessage('Password not supplied') : false); 348 | $response->send(); 349 | exit; // to guarantee exiting out of script after sending the response 350 | } 351 | 352 | // Validation: Making sure the POST submitted data is valid (not a blank string, doesn't exceed 255 characters (as we specified while we created the `users` database table)): 353 | if (strlen($jsonData->username) < 1 || strlen($jsonData->username) > 255 || strlen($jsonData->password) < 1 || strlen($jsonData->password) > 255) { // Check if username is an empty string (i.e. {"username": ""} ), or exceeds 255 characters (as we specified when we created the database table) 354 | $response = new Response(); 355 | $response->setHttpStatusCode(400); // 400 means client error 356 | $response->setSuccess(false); 357 | (strlen($jsonData->username) < 1 ? $response->addMessage('Username cannot be blank') : false); 358 | (strlen($jsonData->username) > 255 ? $response->addMessage('Username must be less than 255 characters') : false); 359 | (strlen($jsonData->password) < 1 ? $response->addMessage('Password cannot be blank') : false); 360 | (strlen($jsonData->password) > 255 ? $response->addMessage('Password must be less than 255 characters') : false); 361 | $response->send(); 362 | exit; // to guarantee exiting out of script after sending the response 363 | } 364 | 365 | // Querying the database to look for the username trying to log in our database `users` table (to retrieve a row based on a valid username): 366 | try { 367 | $username = $jsonData->username; 368 | $password = $jsonData->password; 369 | 370 | $query = $writeDB->prepare('SELECT id, fullname, username, password, useractive, loginattempts FROM tblusers WHERE username = :username'); 371 | $query->bindParam(':username', $username, PDO::PARAM_STR); 372 | $query->execute(); 373 | 374 | // Note: To get rid of the `tblsessions` database table rows that have EXPIRED sessions, on the server you should create a scheduled script that runs every one or two days to clear out (delete) the expired sessions. 375 | 376 | $rowCount = $query->rowCount(); // it should find 1 row only at maximum (user logging in exists), or zero 0 rows (user doesn't exist), because username column is UNIQUE in the `users` database table 377 | 378 | if ($rowCount === 0) { // If username trying to login doesn't exist in our `users` database table 379 | $response = new Response(); 380 | $response->setHTTPStatusCode(401); // 401 means Unauthorized (there's no username identical to the user provided, so they're unauthorized to login) 381 | $response->setSuccess(false); 382 | $response->addMessage('Username or password is incorrect! (User doesn\'t exist in our database!)'); // For security reasons, don't give very specific messages about what's wrong (username specifically or password specifically) because that could allow a potential hacker to work out based on your logic of what's right and what's wrong 383 | $response->send(); 384 | exit; // to guarantee exiting out of script after sending the response 385 | } 386 | 387 | $row = $query->fetch(PDO::FETCH_ASSOC); // We won't use a while loop here like we did in the previous ones because threre's only ever going to be 0 or 1 rows 388 | 389 | $returned_id = $row['id']; 390 | $returned_fullname = $row['fullname']; 391 | $returned_username = $row['username']; 392 | $returned_password = $row['password']; 393 | $returned_useractive = $row['useractive']; 394 | $returned_loginattempts = $row['loginattempts']; 395 | 396 | 397 | // Doing some validation: 398 | 399 | // Making sure the user is active: 400 | if ($returned_useractive !== 'Y') { // if the user is not active ('Y' means yes) 401 | $response = new Response(); 402 | $response->setHTTPStatusCode(401); // 401 means Unauthorized 403 | $response->setSuccess(false); 404 | $response->addMessage('User account not active'); // For security reasons, don't give very specific messages about what's wrong (username specifically or password specifically) because that could allow a potential hacker to work out based on your logic of what's right and what's wrong 405 | $response->send(); 406 | exit; // to guarantee exiting out of script after sending the response 407 | } 408 | 409 | // Making sure the user is not currently locked out: 410 | if ($returned_loginattempts >= 3) { // if number of login attemps is equal to or greater than 3, user is locked out of their account and they need a reset 411 | $response = new Response(); 412 | $response->setHTTPStatusCode(401); // 401 means Unauthorized 413 | $response->setSuccess(false); 414 | $response->addMessage('User account is currently locked out'); // For security reasons, don't give very specific messages about what's wrong (username specifically or password specifically) because that could allow a potential hacker to work out based on your logic of what's right and what's wrong 415 | $response->send(); 416 | exit; // to guarantee exiting out of script after sending the response 417 | } 418 | 419 | // Verifying password: 420 | if (!password_verify($password, $returned_password)) { // comparing the trying to login password to the database stored hashed password 421 | // increase the loginattemps by 1: (user would get locked out of their account if they fail login 3 times i.e. they get locked out of their account if they enter wrong password for 3 times during login) 422 | $query = $writeDB->prepare('UPDATE tblusers SET loginattempts = loginattempts + 1 WHERE id = :id'); // increment the alreay existing `loginattempts` by +1 423 | $query->bindParam(':id', $returned_id, PDO::PARAM_INT); 424 | $query->execute(); 425 | 426 | // Send a failed login response: 427 | $response = new Response(); 428 | $response->setHTTPStatusCode(401); // 401 means Unauthorized 429 | $response->setSuccess(false); 430 | $response->addMessage('Username or password is incorrect'); // For security reasons, don't give very specific messages about what's wrong (username specifically or password specifically) because that could allow a potential hacker to work out based on your logic of what's right and what's wrong 431 | $response->send(); 432 | exit; // to guarantee exiting out of script after sending the response 433 | } 434 | 435 | // Creating the 'access token' and 'refresh token' (generate some random text (random characters) then return them to the client): 436 | // openssl_random_pseudo_bytes() function generates random bytes value that has NEVER been used before. Then we need to convert those bytes to hexadecimal using bin2hex() function then we need to base64-encode it to get a character string that we can pass in and out an HTTP request header 437 | $accesstoken = base64_encode(bin2hex(openssl_random_pseudo_bytes(24)) . time()); // 24 is the length of the bytes (characters) (of the 'access token'). We need to convert it to hexadecimal to be able to deal with it using bin2hex() function. Then we need to base64-encode it using base64_encode() function. In order to add more security (to prevent what's called a 'stale token' sitting on a client device), we suffix the bin2hex() value with time() function to be encoded with it as well. 438 | $refreshtoken = base64_encode(bin2hex(openssl_random_pseudo_bytes(24)) . time()); // 24 is the length of the bytes (characters) (of the 'access token'). We need to convert it to hexadecimal to be able to deal with it using bin2hex() function. Then we need to base64-encode it using base64_encode() function. In order to add more security (to prevent what's called a 'stale token' sitting on a client device), we suffix the bin2hex() value with time() function to be encoded with it as well. 439 | 440 | $access_token_expiry_seconds = 1200; // the 'access token' will last 20 minutes before it's expired 441 | $refresh_token_expiry_seconds = 1209600; // the 'refresh token' will last 14 days before it's expired 442 | 443 | // At this point here, this is a SUCCESSFUL login, so we reset the loginattempts counter back to zero 0 (to prevent from user getting locked out) 444 | // We here use a new separate try ... catch ... statement because we're going to perform two different database queries (a Database Transaction). The first is UPDATE to reset the loginattempts back to zero 0, and the second is INSERT to insert a new row in the tblsessions table to save the generated 'access token' and 'refresh token', so we need to catch ... if either of those two queries fails. And then in case of either one of those two queries fails, we need to ROLL BACK (for example, if the the query of resetting the loginattempts to zero 0 is successful, but the query of INSERT in the tblsessions table fails, we need to do a rollback to roll back any changes we've done, so we need to get the loginattempts back to its old value again before resetting to zero 0) 445 | 446 | } catch (PDOException $ex) { // If the database query fails 447 | // Important Note: We're not going to log the exception error in the error log (a plaintext file) in this case, because if someone (hacker) gets access to the server, then they could read potential log files and passwords in plaintext 448 | // error_log('Database query error - ' . $ex, 0); // Send the real error to the system administrator // 0 means it stores/logs the error in the PHP error log file 449 | $response = new Response(); 450 | $response->setHTTPStatusCode(500); // 500 is Internal Server Error 451 | $response->setSuccess(false); 452 | $response->addMessage('There was an issue logging in'); 453 | $response->send(); 454 | exit; // to guarantee exiting out of script after sending the response 455 | } 456 | 457 | 458 | // At this point here, this is a SUCCESSFUL login, so we reset the loginattempts counter back to zero 0 (to prevent from user getting locked out) 459 | // We here use a new separate try ... catch ... statement because we're going to perform two different database queries (a transaction). The first is UPDATE to reset the loginattempts back to zero 0, and the second is INSERT to insert a new row in the tblsessions table to save the generated 'access token' and 'refresh token', so we need to catch ... if either of those two queries fails. And then in case of either one of those two queries fails, we need to ROLL BACK (for example, if the the query of resetting the loginattempts to zero 0 is successful, but the query of INSERT in the tblsessions table fails, we need to do a rollback to roll back any changes we've done, so we need to get the loginattempts back to its old value again before resetting to zero 0) 460 | try { 461 | // We're going to use Database Transaction (atomic) and committing and Rollback. This means if you're performing multiple database queries, they all have to succeed before the data is saved into the database (by committing them). If there's an error with anyone of the queries halfway through, the catch statement will catch the error and then ROLLBACK any changes happened with the queries. 462 | // Creating the transaction: 463 | $writeDB->beginTransaction(); // Note: We'll rollback changes in the catch block! 464 | 465 | // Resetting the loginattempts to zero 0 after the successful login: 466 | $query = $writeDB->prepare('UPDATE tblusers SET loginattempts = 0 WHERE id = :id'); 467 | $query->bindParam(':id', $returned_id, PDO::PARAM_INT); 468 | $query->execute(); 469 | 470 | // INSERT the session and 'accesstoken' and 'refreshtoken' into the `tblsessions` table: 471 | $query = $writeDB->prepare('INSERT INTO tblsessions (userid, accesstoken, accesstokenexpiry, refreshtoken, refreshtokenexpiry) VALUES (:userid, :accesstoken, DATE_ADD(NOW(), INTERVAL :accesstokenexpiryseconds SECOND), :refreshtoken, DATE_ADD(NOW(), INTERVAL :refreshtokenexpiryseconds SECOND))'); // for `accesstokenexpiry` and `refreshtokenexpiry` columns we need to get the current DATE/Time and then add the period (amount of seconds) that we previously specified up there 472 | $query->bindParam(':userid' , $returned_id , PDO::PARAM_INT); 473 | $query->bindParam(':accesstoken' , $accesstoken , PDO::PARAM_STR); 474 | $query->bindParam(':accesstokenexpiryseconds' , $access_token_expiry_seconds , PDO::PARAM_INT); 475 | $query->bindParam(':refreshtoken' , $refreshtoken , PDO::PARAM_STR); 476 | $query->bindParam(':refreshtokenexpiryseconds', $refresh_token_expiry_seconds, PDO::PARAM_INT); 477 | $query->execute(); 478 | 479 | // Getting the session `id` and returning it to the user because the user should know it because they'll going to use it in a route like /sessions/3, and they'll use it to logout or DELETE a session (DELETE HTTP request with a route like DELETE /sessions/3 ) or when they refresh it they'll need to know the session id 480 | $lastSessionID = $writeDB->lastInsertId(); // it's the session `id` 481 | 482 | // Because we're using a transaction, we need to commit these data (save them into the database), because till now they are not saved into the database yet, they're just hold in a session: 483 | $writeDB->commit(); // committing the transaction // Note: We'll rollback changes in the catch block! 484 | 485 | // Sending the user's 'access token' and the access token expiry seconds, session id, ...etc in a response: 486 | $returnData = array(); 487 | $returnData['session_id'] = intval($lastSessionID); 488 | $returnData['access_token'] = $accesstoken; 489 | $returnData['access_token_expires_in'] = $access_token_expiry_seconds; 490 | $returnData['refresh_token'] = $refreshtoken; 491 | $returnData['refresh_token_expires_in'] = $refresh_token_expiry_seconds; 492 | 493 | // Send a successful response for the Successful login with the $returnData array so that the user can use them on future requests to access the Tasks API (AT THE MOMENT we're dealing with the AUTHENTICATION API): 494 | $response = new Response(); 495 | $response->setHttpStatusCode(201); // 201 means CREATED successfully and then returned 496 | $response->setSuccess(true); 497 | $response->setData($returnData); 498 | $response->send(); 499 | exit; // to guarantee exiting out of script after sending the response 500 | 501 | } catch (PDOException $ex) { // If there's any error while performing any of the two queries 502 | 503 | // If there's an error querying the database, rollback the successful query again of the two queries, because the other one has failed: 504 | $writeDB->rollBack(); 505 | 506 | $response = new Response(); 507 | $response->setHttpStatusCode(500); // 500 is Internal Server Error 508 | $response->setSuccess(false); 509 | $response->addMessage('There was an issue logging in - please try again'); 510 | $response->send(); 511 | exit; // to guarantee exiting out of script after sending the response 512 | } 513 | 514 | } 515 | 516 | } else { // if the route is anything other than the ones we've stated before (for example /sessions/test) 517 | $response = new Response(); 518 | $response->setHttpStatusCode(404); // 404 means Not Found 519 | $response->setSuccess(false); 520 | $response->addMessage('Endpoint not found'); 521 | $response->send(); 522 | exit; // to guarantee exiting out of script after sending the response 523 | } -------------------------------------------------------------------------------- /v1/controller/users.php: -------------------------------------------------------------------------------- 1 | setHTTPStatusCode(500); // 500 is Internal Server Error 18 | $response->setSuccess(false); 19 | $response->addMessage('Database connection error'); 20 | $response->send(); 21 | exit; // to guarantee exiting out of script after sending the response 22 | } 23 | 24 | 25 | // Handling OPTIONS HTTP request method (preflight request) for CORS: 26 | if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { 27 | header('Access-Control-Allow-Methods: POST, OPTIONS'); // we define what HTTP methods to allow only for that endpoint (i.e. POST /users ) 28 | header('Access-Control-Allow-Headers: Content-Type'); // we define what HTTP headers to allow only for that endpoint (i.e. POST /users ) // If it were the /tasks endpoint, we would add the 'Authorization' header too: header('Access-Control-Allow-Headers: Content-Type, Authorization'); 29 | header('Access-Control-Max-Age: 86400'); // 86400 seconds is 1 day, which is 24 hours 30 | $response = new Response(); 31 | $response->setHTTPStatusCode(200); // 200 means Success or OK 32 | $response->setSuccess(true); 33 | $response->send(); 34 | exit; // to guarantee exiting out of script after sending the response 35 | } 36 | 37 | 38 | // Route is: POST /users (Check the .htaccess file) 39 | // Check if the HTTP request method is POST only, because we're going to handle CREATE (which matches to the method POST only) a new user only here, or else Request Method Is Not Allowed 40 | if ($_SERVER['REQUEST_METHOD'] !== 'POST') { // Any HTTP Request Method other than 'POST' is not allowed for this route/endpoint 41 | $response = new Response(); 42 | $response->setHTTPStatusCode(405); // 405 means Request Method Not Allowed 43 | $response->setSuccess(false); 44 | $response->addMessage('Request method not allowed'); 45 | $response->send(); 46 | exit; // to guarantee exiting out of script after sending the response 47 | } 48 | 49 | 50 | // Making sure that the content type of the HTTP request of the data sent to create a user (username, password, ...) is JSON type 51 | if ($_SERVER['CONTENT_TYPE'] !== 'application/json') { // if the HTTP request header content type is not JSON or not provided in the HTTP Request 52 | $response = new Response(); 53 | $response->setHttpStatusCode(400); // 400 means Bad Request/client error (because the client hasn't submitted the right type of data) 54 | $response->setSuccess(false); 55 | $response->addMessage('Content Type header not set to JSON'); 56 | $response->send(); 57 | exit; // to guarantee exiting out of script after sending the response 58 | } 59 | 60 | 61 | // Getting and handling the data that is coming from the POST request: 62 | $rawPostData = file_get_contents('php://input'); // Retrieve the raw HTTP request body data // getting the POST HTTP Request BODY 63 | 64 | 65 | // Validation 66 | // Making sure the data submitted is valid JSON: 67 | if (!$jsonData = json_decode($rawPostData)) { 68 | $response = new Response(); 69 | $response->setHttpStatusCode(400); // 400 means Bad Request/client error (because the client hasn't submitted the right type of data) 70 | $response->setSuccess(false); 71 | $response->addMessage('Request body is not valid JSON'); 72 | $response->send(); 73 | exit; // to guarantee exiting out of script after sending the response 74 | } 75 | 76 | // Validation 77 | // Making sure the mandatory fields are filld in or provided (submitted) with the POST request: 78 | // Mandatory fields are: fullname, username, password 79 | if (!isset($jsonData->fullname) || !isset($jsonData->username) || !isset($jsonData->password)) { 80 | $response = new Response(); 81 | $response->setHttpStatusCode(400); // 400 means client error 82 | $response->setSuccess(false); 83 | (!isset($jsonData->fullname) ? $response->addMessage('Full name not supplied') : false); 84 | (!isset($jsonData->username) ? $response->addMessage('Username not supplied') : false); 85 | (!isset($jsonData->password) ? $response->addMessage('Password not supplied') : false); 86 | $response->send(); 87 | exit; // to guarantee exiting out of script after sending the response 88 | } 89 | 90 | // Validation: Making sure the POST submitted data is valid (not blank string, doesn't exceed 255 characters (as we specified while we created the `users` database table)): 91 | if (strlen($jsonData->fullname) < 1 || strlen($jsonData->fullname) > 255 || strlen($jsonData->username) < 1 || strlen($jsonData->username) > 255 || strlen($jsonData->password) < 1 || strlen($jsonData->password) > 255) { // Check if fullname is not an empty string (i.e. {"fullnanme": ""} ), or exceeds 255 characters (as we specified when we created the database table) 92 | $response = new Response(); 93 | $response->setHttpStatusCode(400); // 400 means client error 94 | $response->setSuccess(false); 95 | (strlen($jsonData->fullname) < 1 ? $response->addMessage('Full name cannot be blank') : false); 96 | (strlen($jsonData->fullname) > 255 ? $response->addMessage('Full name cannot be greater than 255 characters') : false); 97 | (strlen($jsonData->username) < 1 ? $response->addMessage('Username cannot be blank') : false); 98 | (strlen($jsonData->username) > 255 ? $response->addMessage('Username cannot be greater than 255 characters') : false); 99 | (strlen($jsonData->password) < 1 ? $response->addMessage('Password cannot be blank') : false); 100 | (strlen($jsonData->password) > 255 ? $response->addMessage('Password cannot be greater than 255 characters') : false); 101 | $response->send(); 102 | exit; // to guarantee exiting out of script after sending the response 103 | } 104 | 105 | // Doing some tidying up for the data submitted (trimming extra spaces, stripping whitespaces off, ...) 106 | $fullname = trim($jsonData->fullname); 107 | $username = trim($jsonData->username); 108 | $password = trim($jsonData->password); 109 | 110 | // Making sure the username doesn't already exist and is not currently used by another user i.e. not repeated (also, we specified that username is UNIQUE key when we created the database table `users`): 111 | try { 112 | $query = $writeDB->prepare('SELECT id FROM tblusers WHERE username = :username'); // We're using the $writeDB here because we're registering a new user into the database table i.e. We'll use the INSERT SQL statement, which means it's a WRITING operation to the database 113 | // $query = $writeDB->prepare('SELECT COUNT(username) FROM tblusers WHERE username = :username'); // We're using the $writeDB here because we're registering a new user into the database table i.e. We'll use the INSERT SQL statement, which means it's a WRITING operation to the database 114 | $query->bindParam(':username', $username, PDO::PARAM_STR); 115 | $query->execute(); 116 | 117 | $rowCount = $query->rowCount(); 118 | 119 | if ($rowCount !== 0) { // If it's anything other than zero, this means the username already exists (used by another user) 120 | $response = new Response(); 121 | $response->setHTTPStatusCode(409); // 409 means Conflict (the data provided is conflicting with something else) 122 | $response->setSuccess(false); 123 | $response->addMessage('Username already exists'); 124 | $response->send(); 125 | exit; // to guarantee exiting out of script after sending the response 126 | } 127 | 128 | $hashed_password = password_hash($password, PASSWORD_DEFAULT); // hash the password 129 | 130 | // Inserting the user request into database: 131 | $query = $writeDB->prepare('INSERT INTO tblusers (fullname, username, password) VALUES (:fullname, :username, :password)'); // We're using the $writeDB here because we're registering a new user into the database table i.e. We'll use the INSERT SQL statement, which means it's a WRITING operation to the database 132 | $query->bindParam(':fullname', $fullname, PDO::PARAM_STR); 133 | $query->bindParam(':username', $username, PDO::PARAM_STR); 134 | $query->bindParam(':password', $hashed_password, PDO::PARAM_STR); 135 | 136 | $query->execute(); 137 | 138 | $rowCount= $query->rowCount(); 139 | 140 | if ($rowCount === 0) { // if the query fails 141 | // echo '
', var_dump($ex), '
'; 142 | $response = new Response(); 143 | $response->setHTTPStatusCode(500); // 500 Status Code is Internal Server Error 144 | $response->setSuccess(false); 145 | $response->addMessage('There was an issue creating a user account - please try again'); 146 | $response->send(); 147 | exit; // to guarantee exiting out of script after sending the response 148 | } 149 | 150 | // Returning the same data (except the hashed password) submitted by the user back to them again after being inserted into the database: 151 | $lastUserID = $writeDB->lastInsertId(); 152 | 153 | $returnData = array(); // Create an empty array 154 | 155 | $returnData['user_id'] = $lastUserID; 156 | $returnData['fullname'] = $fullname; 157 | $returnData['username'] = $username; 158 | 159 | // Send a successful response: 160 | $response = new Response(); 161 | $response->setHttpStatusCode(201); // Note: 201 means CREATED successfully and then returned 162 | $response->setSuccess(true); 163 | $response->addMessage('User created'); 164 | $response->setData($returnData); 165 | $response->send(); 166 | exit; // to guarantee exiting out of script after sending the response 167 | 168 | } catch (PDOException $ex) { // If there's an error querying the database: 169 | // echo '
', var_dump($ex), '
'; // Display the exception 170 | error_log('Database query error - ' . $ex, 0); // Send the real error to the system administrator // 0 means it stores/logs the error in the PHP error log file 171 | $response = new Response(); 172 | $response->setHTTPStatusCode(500); // 500 is Internal Server Error 173 | $response->setSuccess(false); 174 | $response->addMessage('There was an issue creating a user account - please try again'); 175 | $response->send(); 176 | exit; // to guarantee exiting out of script after sending the response 177 | } -------------------------------------------------------------------------------- /v1/model/Response.php: -------------------------------------------------------------------------------- 1 | _success = $success; 24 | } 25 | 26 | public function setHttpStatusCode($httpStatusCode) { 27 | $this->_httpStatusCode = $httpStatusCode; 28 | } 29 | 30 | public function addMessage($message) { // Add a message to the $_messages array 31 | $this->_messages[] = $message; 32 | } 33 | 34 | public function setData($data) { 35 | $this->_data = $data; 36 | } 37 | 38 | public function toCache($toCache) { // The question is: to cache or not to cache? 39 | $this->_toCache = $toCache; 40 | } 41 | 42 | 43 | 44 | public function send() { // use all the previous data to send the response to the user/client 45 | // Send a raw HTTP "Content-Type" Header 46 | header('Content-type: application/json;charset=utf-8'); // define what we're returning as a response. We're returning JSON, with a character set of type utf-8 47 | 48 | 49 | if ($this->_toCache == true) { // to cache the respone or not 50 | header('Cache-control: max-age=60'); // cache or store the response for a maximum of 60 seconds 51 | } else { // You must explicitly declare the else statement (You must write the ELSE STATEMENT, you can't ingore it!!), or rather it would result in an error 52 | header('Cache-control: no-cache, no-store'); // tells the client you can't store any response on the browser (client), you have to always come back to the server to get a response 53 | } 54 | 55 | 56 | if (($this->_success !== false && $this->_success !== true) || !is_numeric($this->_httpStatusCode)) { // If $_succes is something other than a Boolean (Not a true or false) or the $_httpStatusCode is not numeric // We want to make sure if the response that we're creating is valid before we send it to the client, and make sure it's a standard response 57 | http_response_code(500); // set 500 as a status code, which means: Internal Server Error // This appears in the Network tab in Inspect tools in browser 58 | $this->_responseData['statusCode'] = 500; 59 | $this->_responseData['success'] = false; 60 | $this->addMessage('Response creation error'); 61 | // $this->addMessage('Test message'); // Add another message 62 | $this->_responseData['messages'] = $this->_messages; 63 | } else { // Send a successful response 64 | http_response_code($this->_httpStatusCode); 65 | 66 | // Fill in the $_responseData array with the previous gathered information like $_httpStatusCode, $_success, $_messages and $_data 67 | $this->_responseData['statusCode'] = $this->_httpStatusCode; 68 | $this->_responseData['success'] = $this->_success; 69 | $this->_responseData['messages'] = $this->_messages; 70 | $this->_responseData['data'] = $this->_data; 71 | } 72 | 73 | 74 | echo json_encode($this->_responseData); // encode/convert the PHP array into JSON and echo it 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /v1/model/Task.php: -------------------------------------------------------------------------------- 1 | setID($id); 26 | $this->setTitle($title); 27 | $this->setDescription($description); 28 | $this->setDeadline($deadline); 29 | $this->setCompleted($completed); 30 | $this->setImages($images); 31 | } 32 | 33 | 34 | 35 | // Getters (for the private properties): 36 | 37 | public function getID() { 38 | return $this->_id; 39 | } 40 | 41 | public function getTitle() { 42 | return $this->_title; 43 | } 44 | 45 | public function getDescription() { 46 | return $this->_description; 47 | } 48 | 49 | public function getDeadline() { 50 | return $this->_deadline; 51 | } 52 | 53 | public function getCompleted() { 54 | return $this->_completed; 55 | } 56 | 57 | public function getImages() { 58 | return $this->_images; 59 | } 60 | 61 | 62 | 63 | // Setters (for the private properties): (contains the Validation for the values for example for mandatory fields and their data value types) 64 | 65 | public function setID($id) { // $id is generated automatically by database (NOT NULL AUTO_INCREMENT in database) 66 | // Validation: 67 | if (($id !== null) && (!is_numeric($id) || $id <= 0 || $id > 9223372036854775807 || $this->_id !== null)) { // 1 to 9223372036854775807 is the id column size in database // $this->_id !== null is to make sure we're not overriding a task id that's already populated (already existing) in the database table in case of UPDATE SQL queries (N.B. We pass in 'null' to the setID($id) method in case of INSERT SQL queries) 68 | throw new TaskException('Task ID error'); // new TaskException() means we create a TaskException class object and pass in that message to its constructor // We can get that message we passed in to the class using the Exception class getMessage() method 69 | } 70 | 71 | $this->_id = $id; 72 | } 73 | 74 | public function setTitle($title) { // mandatory (NOT NULL in database) 75 | // Validation: 76 | if (strlen($title) < 1 || strlen($title) > 255) { // Maximum character size of the title column in database is 255 characters 77 | throw new TaskException('Task title error'); // new TaskException() means we create a TaskException class object and pass in that message to its constructor // We can get that message we passed in to the class using the Exception class getMessage() method 78 | } 79 | 80 | $this->_title = $title; 81 | } 82 | 83 | public function setDescription($description) { // optional (NULL in database) 84 | // Validation: 85 | if (($description !== null) && (strlen($description) > 16777215)) { // Maximum character size of the description column in database is 16777215 characters 86 | throw new TaskException('Task description error'); // new TaskException() means we create a TaskException class object and pass in that message to its constructor // We can get that message we passed in to the class using the Exception class getMessage() method 87 | } 88 | 89 | $this->_description = $description; 90 | } 91 | 92 | public function setDeadline($deadline) { // optional (NULL in database) 93 | // echo '
', var_dump(date_create_from_format("m/Y/d H:i:s", "02/2019/12 07:30:25")), '
'; 94 | // echo '
', var_dump(date_format(date_create_from_format("m/Y/d H:i:s", "02/2019/12 07:30:25"), 'Y/m/d H:i')), '
'; 95 | 96 | // Validation: 97 | if (($deadline !== null) && date_format(date_create_from_format('d/m/Y H:i', $deadline), 'd/m/Y H:i') != $deadline) { // date_create_from_format() takes a time string and returns a DateTime ojbect // date_format() returns a formatted date string on success // converting the coming in date string into an object and then converting it to string again, then checking the converted string if it matches the coming in string // 'd/m/Y H:i' means for example 15/01/2019 13:30 98 | throw new TaskException('Task deadline date time error'); // new TaskException() means we create a TaskException class object and pass in that message to its constructor // We can get that message we passed in to the class using the Exception class getMessage() method 99 | } 100 | 101 | $this->_deadline = $deadline; 102 | } 103 | 104 | public function setCompleted($completed) { // optional (NULL in database and DEFAULT to 'N') 105 | // Validation: 106 | if (strtoupper($completed) !== 'Y' && strtoupper($completed !== 'N')) { // 107 | throw new TaskException('Task completed must be Y or N'); // new TaskException() means we create a TaskException class object and pass in that message to its constructor // We can get that message we passed in to the class using the Exception class getMessage() method 108 | } 109 | 110 | $this->_completed = $completed; 111 | } 112 | 113 | public function setImages($images) { // $images array 114 | // Validation: 115 | if (!is_array($images)) { 116 | throw new TaskException('Images is not an array'); // new TaskException() means we create a TaskException class object and pass in that message to its constructor // We can get that message we passed in to the class using the Exception class getMessage() method 117 | } 118 | 119 | $this->_images = $images; 120 | } 121 | 122 | 123 | 124 | public function returnTaskAsArray() { 125 | $task = array(); 126 | 127 | // Use the Getters: 128 | $task['id'] = $this->getID(); 129 | $task['title'] = $this->getTitle(); 130 | $task['description'] = $this->getDescription(); 131 | $task['deadline'] = $this->getDeadline(); 132 | $task['completed'] = $this->getCompleted(); 133 | $task['images'] = $this->getImages(); 134 | 135 | return $task; 136 | } 137 | 138 | } -------------------------------------------------------------------------------- /v1/model/image.php: -------------------------------------------------------------------------------- 1 | setID($id); 25 | $this->setTitle($title); 26 | $this->setFilename($filename); 27 | $this->setMimetype($mimetype); 28 | $this->setTaskID($taskid); 29 | 30 | $this->_uploadFolderLocation = '../taskimages/'; // We'll place images of a certain task in a folder named with the task id 31 | } 32 | 33 | 34 | 35 | // Getters (for the private properties): 36 | 37 | public function getID () { 38 | return $this->_id; 39 | } 40 | 41 | public function getTitle () { 42 | return $this->_title; 43 | } 44 | 45 | public function getFilename () { 46 | return $this->_filename; 47 | } 48 | 49 | public function getFileExtension() { // A Helper Function 50 | // e.g. 'imagename.jpg' 51 | $filenameParts = explode('.', $this->_filename); // e.g. ['imagename', 'jpg'] 52 | $lastArrayElement = count($filenameParts) - 1; // because length = index + 1 i.e. index = length - 1 53 | $fileExtension = $filenameParts[$lastArrayElement]; 54 | 55 | return $fileExtension; 56 | } 57 | 58 | public function getMimetype () { 59 | return $this->_mimetype; 60 | } 61 | 62 | public function getTaskID() { 63 | return $this->_taskid; 64 | } 65 | 66 | public function getUploadFolderLocation() { 67 | return $this->_uploadFolderLocation; // Which is determined by the Constructor Function 68 | } 69 | 70 | 71 | 72 | // Setters (for the private properties): (contains the Validation for the values) 73 | public function setID($id) { 74 | // Validation: 75 | if (($id !== null) && (!is_numeric($id) || $id <= 0 || $id > 9223372036854775807 || $this->_id !== null)) { // 1 to 9223372036854775807 is the id column size in database // $this->_id !== null is to make sure we're not overriding an image id that's already populated in the database table 76 | throw new ImageException('Image ID Error'); // new ImageException() means we create an ImageException class object and pass in that message to its constructor // We can get that message we passed in to the class using the Exception class getMessage() method 77 | } 78 | 79 | $this->_id = $id; 80 | } 81 | 82 | public function setTitle($title) { // mandatory (NOT NULL in database) 83 | // Validation: 84 | if (strlen($title) < 1 || strlen($title) > 255) { // Maximum character size of the title column in database is 255 characters 85 | throw new ImageException('Image title error'); // new ImageException() means we create an ImageException class object and pass in that message to its constructor // We can get that message we passed in to the class using the Exception class getMessage() method 86 | } 87 | 88 | $this->_title = $title; 89 | } 90 | 91 | public function setFilename($filename) { // mandatory (NOT NULL in database) 92 | // Validation: 93 | if (strlen($filename) < 1 || strlen($filename) > 30 || preg_match('/^[a-zA-Z0-9_-]+(.jpg|.gif|.png)$/', $filename) != 1) { // Maximum character size of the filename column in database is 30 characters // Regular Expression to make sure the image name contains an image name and the image extension of one of OUR CHOICE file extensions 94 | throw new ImageException('Image filename error - must be between 1 and 30 characters, only be .jpg .png .gif and without spaces'); // new ImageException() means we create an ImageException class object and pass in that message to its constructor // We can get that message we passed in to the class using the Exception class getMessage() method 95 | } 96 | 97 | $this->_filename = $filename; 98 | } 99 | 100 | public function setMimetype($mimetype) { // mandatory (NOT NULL in database) 101 | // Validation: 102 | if (strlen($mimetype) < 1 || strlen($mimetype) > 255) { // Maximum character size of the mimetype column in database is 255 characters 103 | throw new ImageException('Image mimetype error'); // new ImageException() means we create an ImageException class object and pass in that message to its constructor // We can get that message we passed in to the class using the Exception class getMessage() method 104 | } 105 | 106 | $this->_mimetype = $mimetype; 107 | } 108 | 109 | public function setTaskID($taskid) { 110 | // Validation: 111 | if (($taskid !== null) && (!is_numeric($taskid) || $taskid <= 0 || $taskid > 9223372036854775807 || $this->_taskid !== null)) { // 1 to 9223372036854775807 is the id column size in database // $this->_id !== null is to make sure we're not overriding an image id that's already populated in the database table 112 | throw new ImageException('Image Task ID Error'); // new ImageException() means we create an ImageException class object and pass in that message to its constructor // We can get that message we passed in to the class using the Exception class getMessage() method 113 | } 114 | 115 | $this->_taskid = $taskid; 116 | } 117 | 118 | public function getImageURL() { 119 | // e.g. http://localhost:8888/v1/tasks/2/images/1 120 | // We build up the URL as follows: 121 | $httpOrHttps = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http'); // Check whether the "HTTPs" or "HTTP" protocol is used with the HTTP Request 122 | $host = $_SERVER['HTTP_HOST']; // hostname and port number (e.g. wwe.com:80 ) 123 | $url = '/v1/tasks/' . $this->getTaskID() . '/images/' . $this->getID(); // Check our API routes (e.g. /tasks/3/images/2) 124 | return $httpOrHttps . '://' . $host . $url; // e.g. http://localhost:8888/v1/tasks/2/images/1 125 | } 126 | 127 | 128 | 129 | public function returnImageFile() { // return the physical image file (the binary file itself) of a certain task 130 | $filepath = $this->getUploadFolderLocation() . $this-> getTaskID() . '/' . $this->getFilename(); // e.g. '../taskimages/' . '3' . 'car.jpg' 131 | 132 | // Check if this file with those critera exists 133 | if (!file_exists($filepath)) { 134 | throw new ImageException('Image file not found'); // new ImageException() means we create an ImageException class object and pass in that message to its constructor // We can get that message we passed in to the class using the Exception class getMessage() method 135 | } 136 | 137 | // Now, all our current HTTP responses at the minute are of JSON type, but we're going to return an actual binary file here, so we need to switch "Content-Type" now to the content type of the mimetype of the file we're downloading (dynamically) 138 | // Check https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition 139 | header('Content-Type: ' . $this->getMimetype()); // e.g. 'Content-Type: image/jpge' 140 | header('Content-Disposition: inline; filename="' . $this->getFilename() . '"'); // "Content-disposition" could be either 'attachment' (will force the attachment to download) or 'inline' (browser/client (Postman) will try to open the file within itself (within the client) without downloading) // If you don't use the 'filename' parameter to provide the file name, file would get a random name // Check https://stackoverflow.com/questions/1395151/content-dispositionwhat-are-the-differences-between-inline-and-attachment and https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition and https://www.geeksforgeeks.org/http-headers-content-disposition/ 141 | 142 | if (!readfile($filepath)) { // Here we read the file back to client (stream the binary file back to the client) and, AT THE SAME TIME, if it can't stream it to the client, it sends a 404 response back 143 | http_response_code(404); // 404 Not Found // We HERE can't send an error response back as a JSON response, because the trouble is HERE we already switched headers from "Content-Type" 'JSON' to the 'mime type' of the file we're downloading 144 | exit; 145 | } 146 | exit; // If it successfully streams and reads the file, please exit the script 147 | } 148 | 149 | 150 | 151 | public function saveImageFile($tempFileName) { // we'll place images of a certain task in a folder named with the task id // $tempFileName is the uploaded file temporary location on the server, which we can get from using $_FILES['imagefile']['tmp_name'] 152 | // If moving the file using move_uploaded_file() function fails, we roll back the database transaction 153 | 154 | // Build the file path inside the main uploaded files folder ('taskimages' folder) that we're going to upload the file of a specific task 155 | // We're going to name the folder which contains the files of a specific task with the task id e.g. folder name is '29' 156 | $uploadedFilePath = $this->getUploadFolderLocation() . $this->getTaskID() . '/' . $this->getFilename(); // $this->getFilename() will return the complete file name with its extension 157 | 158 | if (!is_dir($this->getUploadFolderLocation() . $this->getTaskID())) { // check if the uploaded files folder OF A SPECIFIC TASK exists (NOT the main uploaded files folder), and if not, we create it 159 | if (!mkdir($this->getUploadFolderLocation() . $this->getTaskID())) { 160 | throw new ImageException('Failed to create image upload folder for task'); // new ImageException() means we create an ImageException class object and pass in that message to its constructor // We can get that message we passed in to the class using the Exception class getMessage() method 161 | } 162 | } 163 | 164 | if (!file_exists($tempFileName)) { // check if the temporary file on the server exists 165 | throw new ImageException('Failed to upload image file'); // new ImageException() means we create an ImageException class object and pass in that message to its constructor // We can get that message we passed in to the class using the Exception class getMessage() method 166 | } 167 | 168 | if (!move_uploaded_file($tempFileName, $uploadedFilePath)) { // check (and PRACTICALLY MOVES the uploaded file) if moving the temporary uploaded file to our path is successful 169 | throw new ImageException('Failed to upload image file (to path)'); // new ImageException() means we create an ImageException class object and pass in that message to its constructor // We can get that message we passed in to the class using the Exception class getMessage() method 170 | } 171 | } 172 | 173 | 174 | 175 | public function renameImageFile($oldFileName, $newFileName) { // Rename the actual physical file name on the server (on the filesystem) 176 | $originalFilePath = $this->getUploadFolderLocation() . $this->getTaskID() . '/' . $oldFileName; 177 | $renamedFilePath = $this->getUploadFolderLocation() . $this->getTaskID() . '/' . $newFileName; 178 | 179 | if (!file_exists($originalFilePath)) { // Check if the old file exists 180 | throw new ImageException('Cannot find image file to rename'); 181 | } 182 | 183 | if (!rename($originalFilePath, $renamedFilePath)) { // Rename the file and check if this fails AT THE SAME TIME 184 | throw new ImageException('Failded to update the filename'); 185 | } 186 | } 187 | 188 | 189 | 190 | public function deleteImageFile() { // delete the actual physical file on the server (on the filesystem) 191 | $filepath = $this->getUploadFolderLocation() . $this->getTaskID() . '/' . $this->getFilename(); 192 | 193 | if (file_exists($filepath)) { // Check if the old file exists 194 | if (!unlink($filepath)) { // checks success and deletes the file at the same time 195 | throw new ImageException('Failded to delete image file'); 196 | } 197 | } 198 | } 199 | 200 | 201 | 202 | // Return an image Attributes: 203 | public function returnImageAsArray() { // A helper function 204 | $image = array(); 205 | 206 | // Use the getters: 207 | $image['id'] = $this->getID(); 208 | $image['title'] = $this->getTitle(); 209 | $image['filename'] = $this->getFilename(); 210 | $image['mimetype'] = $this->getMimetype(); 211 | $image['taskid'] = $this->getTaskID(); 212 | $image['imageurl'] = $this->getImageURL(); 213 | 214 | return $image; 215 | } 216 | 217 | } -------------------------------------------------------------------------------- /v1/taskimages/1/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedYahyaE/plain-php-rest-api-with-authentication/e2d98e430057739d0d5e5e306dd18e94a88f8a2f/v1/taskimages/1/cat.jpg -------------------------------------------------------------------------------- /v1/taskimages/1/updated_filename.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedYahyaE/plain-php-rest-api-with-authentication/e2d98e430057739d0d5e5e306dd18e94a88f8a2f/v1/taskimages/1/updated_filename.jpg -------------------------------------------------------------------------------- /v1/taskimages/7/study.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedYahyaE/plain-php-rest-api-with-authentication/e2d98e430057739d0d5e5e306dd18e94a88f8a2f/v1/taskimages/7/study.jpg -------------------------------------------------------------------------------- /v1/taskimages/8/homework-UPDATED.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedYahyaE/plain-php-rest-api-with-authentication/e2d98e430057739d0d5e5e306dd18e94a88f8a2f/v1/taskimages/8/homework-UPDATED.jpg -------------------------------------------------------------------------------- /v1/taskimages/8/sweeping.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedYahyaE/plain-php-rest-api-with-authentication/e2d98e430057739d0d5e5e306dd18e94a88f8a2f/v1/taskimages/8/sweeping.jpg --------------------------------------------------------------------------------