11<?php
22
3- // JWT Configuration
4- define ('JWT_SECRET ' , 'your-secret-key-change-this-in-production ' ); // TODO: change to a secure secret
5- define ('JWT_ALGORITHM ' , 'HS256 ' );
6- define ('JWT_EXPIRY ' , 3600 ); // 1 hour
7- define ('DATA_DIR ' , __DIR__ . '/data ' ); // TODO: change to your data directory
3+ require "config.php " ;
84
9- // User credentials (in production, use a database)
10- $ validUsers = [
11- 'admin ' => password_hash ('secret123 ' , PASSWORD_DEFAULT )
12- ];
5+ // Debug configuration
6+ define ('CSV_DEBUG_ENABLED ' , true ); // Set to false to disable CSV debug logging
137
148// JWT Helper Functions
159function base64url_encode ($ data ) {
@@ -79,8 +73,8 @@ function sanitizeInput($input) {
7973 if (!is_string ($ input )) {
8074 return $ input ;
8175 }
82- // Remove HTML tags and encode special characters
83- return htmlspecialchars ( strip_tags ($ input), ENT_QUOTES , ' UTF-8 ' );
76+ // Remove HTML tags but don't encode special characters
77+ return strip_tags ($ input );
8478}
8579
8680function sanitizeFilename ($ filename ) {
@@ -177,35 +171,101 @@ public function __construct(string $filePath) {
177171
178172 private function loadData (): void {
179173 if (!file_exists ($ this ->filePath )) {
174+ if (CSV_DEBUG_ENABLED ) {
175+ error_log ("CSV Debug: File not found: {$ this ->filePath }" );
176+ }
180177 throw new RuntimeException ("CSV file not found: {$ this ->filePath }" );
181178 }
182179
180+ if (CSV_DEBUG_ENABLED ) {
181+ error_log ("CSV Debug: Opening file for reading: {$ this ->filePath }" );
182+ }
183+
183184 $ file = fopen ($ this ->filePath , 'r ' );
184185 if ($ file === false ) {
186+ if (CSV_DEBUG_ENABLED ) {
187+ error_log ("CSV Debug: Failed to open file: {$ this ->filePath }" );
188+ }
185189 throw new RuntimeException ("Could not open file: {$ this ->filePath }" );
186190 }
187191
188192 // Read headers
189193 $ csvFile = fgetcsv ($ file );
190194 if (!$ csvFile ) {
195+ if (CSV_DEBUG_ENABLED ) {
196+ error_log ("CSV Debug: No headers found in file: {$ this ->filePath }" );
197+ }
191198 fclose ($ file );
192199 throw new RuntimeException ("Invalid CSV file: No headers found " );
193200 }
194201 $ this ->headers = array_map ('sanitizeInput ' , $ csvFile );
195202 if ($ this ->headers === false ) {
203+ if (CSV_DEBUG_ENABLED ) {
204+ error_log ("CSV Debug: Failed to sanitize headers in file: {$ this ->filePath }" );
205+ }
196206 fclose ($ file );
197207 throw new RuntimeException ("Invalid CSV file: No headers found " );
198208 }
199209
210+ // Debug: Log headers
211+ if (CSV_DEBUG_ENABLED ) {
212+ error_log ("CSV Debug: Headers loaded for file {$ this ->filePath }: " . implode (', ' , $ this ->headers ));
213+ }
214+
200215 // Read data
201216 $ this ->data = [];
217+ $ lineNumber = 1 ; // Start at 1 since we already read the header
218+ $ validLines = 0 ;
219+
202220 while (($ row = fgetcsv ($ file )) !== false ) {
203- if (count ($ row ) === count ($ this ->headers )) {
204- $ this ->data [] = array_combine ($ this ->headers , array_map ('sanitizeInput ' , $ row ));
221+ $ lineNumber ++;
222+
223+ // Debug: Log each line being read
224+ if (CSV_DEBUG_ENABLED ) {
225+ error_log ("CSV Debug: Reading line {$ lineNumber } from {$ this ->filePath }: " . implode (', ' , $ row ));
226+ }
227+
228+ // Handle column count mismatches by padding or truncating
229+ if (count ($ row ) !== count ($ this ->headers )) {
230+ if (CSV_DEBUG_ENABLED ) {
231+ error_log ("CSV Debug: Column count mismatch on line {$ lineNumber }. Expected: " . count ($ this ->headers ) . ", Got: " . count ($ row ));
232+ }
233+
234+ if (count ($ row ) < count ($ this ->headers )) {
235+ // Fill missing columns with null
236+ $ missingColumns = count ($ this ->headers ) - count ($ row );
237+ $ row = array_merge ($ row , array_fill (0 , $ missingColumns , null ));
238+
239+ if (CSV_DEBUG_ENABLED ) {
240+ error_log ("CSV Debug: Filled {$ missingColumns } missing columns with null on line {$ lineNumber }" );
241+ }
242+ } else {
243+ // Truncate extra columns
244+ $ extraColumns = count ($ row ) - count ($ this ->headers );
245+ $ row = array_slice ($ row , 0 , count ($ this ->headers ));
246+
247+ if (CSV_DEBUG_ENABLED ) {
248+ error_log ("CSV Debug: Truncated {$ extraColumns } extra columns on line {$ lineNumber }" );
249+ }
250+ }
251+ }
252+
253+ // Now the row should have the correct number of columns
254+ $ this ->data [] = array_combine ($ this ->headers , array_map ('sanitizeInput ' , $ row ));
255+ $ validLines ++;
256+
257+ // Debug: Log successful line processing
258+ if (CSV_DEBUG_ENABLED ) {
259+ error_log ("CSV Debug: Successfully processed line {$ lineNumber } - Record added " );
205260 }
206261 }
207262
208263 fclose ($ file );
264+
265+ // Debug: Log summary
266+ if (CSV_DEBUG_ENABLED ) {
267+ error_log ("CSV Debug: File {$ this ->filePath } loaded successfully. Total lines read: {$ lineNumber }, Valid records: {$ validLines }" );
268+ }
209269 }
210270
211271 private function saveData (): void {
@@ -233,7 +293,7 @@ private function formatResourceObject(array $row, int $index): array {
233293 ];
234294 }
235295
236- public function getAll (int $ offset = 0 , int $ perPage = 10 ): array {
296+ public function getAll (int $ offset = 0 , int $ perPage = 50 ): array {
237297 $ resources = [];
238298 foreach ($ this ->data as $ index => $ row ) {
239299 $ resources [] = $ this ->formatResourceObject ($ row , $ index );
@@ -254,6 +314,20 @@ public function getAll(int $offset = 0, int $perPage = 10): array {
254314 ];
255315 }
256316
317+ public function getAllRecords (): array {
318+ $ resources = [];
319+ foreach ($ this ->data as $ index => $ row ) {
320+ $ resources [] = $ this ->formatResourceObject ($ row , $ index );
321+ }
322+
323+ return [
324+ 'data ' => $ resources ,
325+ 'meta ' => [
326+ 'totalRecords ' => count ($ resources )
327+ ]
328+ ];
329+ }
330+
257331 public function getById (int $ id ): ?array {
258332 if (!isset ($ this ->data [$ id ])) {
259333 return null ;
@@ -263,7 +337,7 @@ public function getById(int $id): ?array {
263337 ];
264338 }
265339
266- public function search (array $ criteria , bool $ exactMatch = false , int $ offset = 0 , int $ perPage = 10 ): array {
340+ public function search (array $ criteria , bool $ exactMatch = false , int $ offset = 0 , int $ perPage = 50 ): array {
267341 // Sanitize search criteria
268342 $ criteria = sanitizeInput ($ criteria );
269343
@@ -339,14 +413,15 @@ public function update(int $id, array $attributes): bool {
339413 // Sanitize input attributes
340414 $ attributes = sanitizeInput ($ attributes );
341415
342- // Validate that all required headers are present
343- foreach ($ this -> headers as $ header ) {
344- if (!isset ( $ attributes [ $ header ] )) {
345- throw new InvalidArgumentException ("Missing required field: {$ header }" );
416+ // Validate that all provided attributes correspond to valid headers
417+ foreach ($ attributes as $ key => $ value ) {
418+ if (!in_array ( $ key , $ this -> headers )) {
419+ throw new InvalidArgumentException ("Invalid field: {$ key }" );
346420 }
347421 }
348422
349- $ this ->data [$ id ] = $ attributes ;
423+ // Merge provided attributes with existing data (partial update)
424+ $ this ->data [$ id ] = array_merge ($ this ->data [$ id ], $ attributes );
350425 $ this ->saveData ();
351426 return true ;
352427 }
@@ -372,6 +447,40 @@ public function getHeaders(): array {
372447 ]
373448 ];
374449 }
450+
451+ public function debugLine (int $ lineNumber ): array {
452+ if (!file_exists ($ this ->filePath )) {
453+ throw new RuntimeException ("CSV file not found: {$ this ->filePath }" );
454+ }
455+
456+ $ file = fopen ($ this ->filePath , 'r ' );
457+ if ($ file === false ) {
458+ throw new RuntimeException ("Could not open file: {$ this ->filePath }" );
459+ }
460+
461+ $ currentLine = 0 ;
462+ $ targetLine = $ lineNumber ;
463+
464+ while (($ row = fgetcsv ($ file )) !== false ) {
465+ $ currentLine ++;
466+ if ($ currentLine === $ targetLine ) {
467+ fclose ($ file );
468+ return [
469+ 'line_number ' => $ lineNumber ,
470+ 'raw_content ' => $ row ,
471+ 'column_count ' => count ($ row ),
472+ 'expected_columns ' => count ($ this ->headers ),
473+ 'headers ' => $ this ->headers ,
474+ 'is_valid ' => count ($ row ) === count ($ this ->headers ),
475+ 'missing_columns ' => count ($ row ) < count ($ this ->headers ) ? array_slice ($ this ->headers , count ($ row )) : [],
476+ 'extra_columns ' => count ($ row ) > count ($ this ->headers ) ? array_slice ($ row , count ($ this ->headers )) : []
477+ ];
478+ }
479+ }
480+
481+ fclose ($ file );
482+ throw new RuntimeException ("Line {$ lineNumber } not found in file " );
483+ }
375484}
376485
377486
@@ -445,6 +554,36 @@ public function getHeaders(): array {
445554 ]));
446555}
447556
557+ // Handle download endpoints
558+ if (count ($ parts ) >= 4 && $ parts [0 ] === 'api ' && $ parts [1 ] === 'csv ' && $ parts [3 ] === 'download ' ) {
559+ $ filename = $ parts [2 ];
560+ $ filePath = DATA_DIR . '/ ' . $ filename ;
561+
562+ if (!file_exists ($ filePath )) {
563+ http_response_code (404 );
564+ die (json_encode ([
565+ 'errors ' => [
566+ [
567+ 'status ' => '404 ' ,
568+ 'title ' => 'Not Found ' ,
569+ 'detail ' => 'The requested file was not found '
570+ ]
571+ ]
572+ ]));
573+ }
574+
575+ // Set headers for file download
576+ header ('Content-Type: text/csv ' );
577+ header ('Content-Disposition: attachment; filename=" ' . $ filename . '" ' );
578+ header ('Content-Length: ' . filesize ($ filePath ));
579+ header ('Cache-Control: no-cache, must-revalidate ' );
580+ header ('Expires: Sat, 26 Jul 1997 05:00:00 GMT ' );
581+
582+ // Output the file content
583+ readfile ($ filePath );
584+ exit ;
585+ }
586+
448587// Validate path structure for CSV endpoints
449588if (count ($ parts ) < 2 || $ parts [0 ] !== 'api ' || $ parts [1 ] !== 'csv ' ) {
450589 http_response_code (404 );
@@ -645,6 +784,33 @@ public function getHeaders(): array {
645784 case 'GET ' :
646785 if (isset ($ parts [3 ]) && $ parts [3 ] === 'structure ' ) {
647786 die (json_encode ($ csvHandler ->getHeaders ()));
787+ } else if (isset ($ parts [3 ]) && $ parts [3 ] === 'all ' ) {
788+ // Get all records without pagination
789+ die (json_encode ($ csvHandler ->getAllRecords ()));
790+ } else if (isset ($ parts [3 ]) && $ parts [3 ] === 'debug ' && isset ($ parts [4 ])) {
791+ // Debug specific line
792+ $ lineNumber = (int )$ parts [4 ];
793+ try {
794+ $ debugInfo = $ csvHandler ->debugLine ($ lineNumber );
795+ die (json_encode ([
796+ 'data ' => [
797+ 'type ' => 'debug_info ' ,
798+ 'id ' => 'line_ ' . $ lineNumber ,
799+ 'attributes ' => $ debugInfo
800+ ]
801+ ]));
802+ } catch (Exception $ e ) {
803+ http_response_code (400 );
804+ die (json_encode ([
805+ 'errors ' => [
806+ [
807+ 'status ' => '400 ' ,
808+ 'title ' => 'Bad Request ' ,
809+ 'detail ' => $ e ->getMessage ()
810+ ]
811+ ]
812+ ]));
813+ }
648814 } else if (isset ($ parts [3 ]) && $ parts [3 ] === 'search ' ) {
649815
650816 } else if (isset ($ parts [3 ])) {
@@ -670,7 +836,7 @@ public function getHeaders(): array {
670836 } else {
671837 parse_str ($ query , $ params );
672838 $ offset = isset ($ params ['page ' ][$ filename ]['offset ' ]) ? (int )(sanitizeInput ($ params ['page ' ][$ filename ]['offset ' ])) : 0 ;
673- $ perPage = isset ($ params ['page ' ][$ filename ]['limit ' ]) ? (int )(sanitizeInput ($ params ['page ' ][$ filename ]['limit ' ])) : 10 ;
839+ $ perPage = isset ($ params ['page ' ][$ filename ]['limit ' ]) ? (int )(sanitizeInput ($ params ['page ' ][$ filename ]['limit ' ])) : 50 ;
674840 $ exactMatch = false ;
675841 if (isset ($ params ['filter ' ])) {
676842 // Handle search request
@@ -778,14 +944,11 @@ public function getHeaders(): array {
778944 exit ;
779945 }
780946
947+ // Get the updated record to return complete data
948+ $ updatedRecord = $ csvHandler ->getById ($ id );
949+
781950 http_response_code (200 );
782- die (json_encode ([
783- 'data ' => [
784- 'type ' => pathinfo ($ filename , PATHINFO_FILENAME ),
785- 'id ' => (string )$ id ,
786- 'attributes ' => $ input ['data ' ]['attributes ' ]
787- ]
788- ]));
951+ die (json_encode ($ updatedRecord ));
789952 } catch (InvalidArgumentException $ e ) {
790953 http_response_code (400 );
791954 die (json_encode ([
0 commit comments