diff --git a/Dockerfile b/Dockerfile index cbf0a8e..e0f4a7c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:slim-bullseye +FROM python:slim-bookworm RUN <> /etc/sudoers EOF + ENV NAME="Scanner" ENV MODEL="MFC-L2700DW" ENV IPADDRESS="192.168.1.123" @@ -69,13 +71,12 @@ ENV TELEGRAM_CHATID="" # Make sure this ends in a slash. ENV FTP_PATH="/scans/" -#ADD files/gui/index.php /var/www/html -#ADD files/gui/main.css /var/www/html -#ADD files/api/scan.php /var/www/html -#ADD files/api/active.php /var/www/html -#ADD files/api/list.php /var/www/html -#ADD files/api/download.php /var/www/html -COPY html /var/www/html +EXPOSE 54925 +EXPOSE 54921 +EXPOSE 80 + +# Copy the web files to the web directory +COPY www /var/www RUN chown -R www-data /var/www/ #directory for scans: diff --git a/README.md b/README.md index c983600..9dcf988 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ You can configure the tool via environment variables: | USE_JPEG_COMPRESSION | optional | use JPEG compression when creating PDFs | | TELEGRAM_TOKEN | optional | If TELEGRAM_TOKEN and TELEGRAM_CHATID are set, then this sends notification | | TELEGRAM_CHATID | optional | If TELEGRAM_TOKEN and TELEGRAM_CHATID are set, then this sends notification | +| ALLOW_GUI_FILEOPERATIONS | optional | true/false. Let you delete and rename files in files list | ### FTPS upload @@ -203,9 +204,8 @@ Thus, make sure to wait for your scan to complete, before pressing another butto #### API The GUI uses a minimal "API" at the backend, which you can also use from other tooling (e.g., Home Assistant or a control panel near your printer). -To scan, simply call `http://:/scan.php?target=` -Also check out the endpoints `list.php`, `download.php`, `active.php`. -Maybe one day an OpenAPI Spec will be included. +To scan, simply call `http://:/api/scanner/scanto/` +Also check out the swagger file in the doc directory to see all available endpoints. ## Full Docker Compose Example diff --git a/doc/swagger.yaml b/doc/swagger.yaml new file mode 100755 index 0000000..2a21525 --- /dev/null +++ b/doc/swagger.yaml @@ -0,0 +1,284 @@ +openapi: 3.0.0 +info: + title: BrotherScannerDocker API + description: API for managing and accessing scans and files. + version: 1.0.0 +servers: + - url: http://localhost:8080/api + description: Local development server +paths: + /scanner/status: + get: + summary: Returns the current status of the scanner + responses: + '200': + description: Scanner status + content: + application/json: + schema: + type: object + properties: + scan: + type: boolean + description: Indicates if a scan is in progress + waiting: + type: boolean + description: Indicates if the scanner is waiting + ocr: + type: boolean + description: Indicates if an OCR process is running + /scanner/scanto: + post: + summary: Initiates a scan operation + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + target: + type: string + description: Target for the scan operation + enum: + - file + - email + - image + - ocr + responses: + '200': + description: Scan operation successfully initiate + /scanner/scanto/{parameter}: + get: + summary: Initiates a scan operation with a specified parameter + parameters: + - name: parameter + in: path + required: true + schema: + type: string + enum: + - file + - email + - image + - ocr + responses: + '200': + description: Scan operation successfully initiated, waiting for backend response + /file-list: + get: + summary: List Scanned Files + responses: + '200': + description: Returns a list of scanned files. + content: + application/json: + schema: + type: array + items: + type: object + properties: + full_path: + type: string + file: + type: string + name: + type: string + name_clean: + type: string + dir: + type: string + date_from_name: + type: string + time_from_name: + type: string + fileCreationTime: + type: integer + fileModificationTime: + type: integer + date_from_file: + type: string + time_from_file: + type: string + extension: + type: string + mimetype: + type: string + size: + type: integer + example: + - full_path: "/scans/2024-09-21 Invoice Company A.pdf" + file: "Invoice Company A.pdf" + name: "Invoice Company A" + name_clean: "Company A Invoice" + dir: "/scans" + date_from_name: "2024-09-21" + time_from_name: "" + fileCreationTime: 1726902248 + fileModificationTime: 1726902199 + date_from_file: "2024-09-21" + time_from_file: "09-03-19" + extension: "pdf" + mimetype: "application/pdf" + size: 2970761 + - full_path: "/scans/2024-09-22 Insurance Policy Document.pdf" + file: "Insurance Policy Document.pdf" + name: "Insurance Policy Document" + name_clean: "Insurance Policy" + dir: "/scans" + date_from_name: "2024-09-22" + time_from_name: "" + fileCreationTime: 1727016766 + fileModificationTime: 1726582762 + date_from_file: "2024-09-17" + time_from_file: "16-19-22" + extension: "pdf" + mimetype: "application/pdf" + size: 2947326 + /file/{file}/info: + get: + summary: Provides extended information about a file + parameters: + - name: file + in: path + required: true + schema: + type: string + responses: + '200': + description: Extended file information + content: + application/json: + schema: + type: object + properties: + full_path: + type: string + description: The full path to the file + file: + type: string + description: The file name + name: + type: string + description: The file name without extension + name_clean: + type: string + description: Cleaned version of the file name + dir: + type: string + description: Directory where the file is located + date_from_name: + type: string + format: date + description: Date extracted from the file name + time_from_name: + type: string + format: time + description: Time extracted from the file name + fileCreationTime: + type: integer + description: File creation time as a Unix timestamp + fileModificationTime: + type: integer + description: File modification time as a Unix timestamp + date_from_file: + type: string + format: date + description: Date extracted from the file metadata + time_from_file: + type: string + description: Time extracted from the file metadata + extension: + type: string + description: File extension + mimetype: + type: string + description: MIME type of the file + size: + type: integer + description: Size of the file in bytes + example: + full_path: "/scans/2024-09-13-13-08-46 ganz anderes.pdf" + file: "2024-09-13-13-08-46 ganz anderes.pdf" + name: "2024-09-13-13-08-46 ganz anderes" + name_clean: "ganz anderes" + dir: "/scans" + date_from_name: "2024-09-13" + time_from_name: "13:08:46" + fileCreationTime: 1727176003 + fileModificationTime: 1726225726 + date_from_file: "2024-09-13" + time_from_file: "13-08-46" + extension: "pdf" + mimetype: "application/pdf" + size: 1161527 + /file/{file}/download: + get: + summary: Download a File + parameters: + - name: file + in: path + required: true + description: The name of the file to download. + schema: + type: string + responses: + '200': + description: The file is downloaded. + content: + application/pdf: + schema: + type: string + format: binary + /file/{file}/delete: + delete: + summary: Deletes the specified file + parameters: + - name: file + in: path + required: true + schema: + type: string + responses: + '200': + description: File successfully deleted + /file/{file}/rename: + put: + summary: Renames the specified file + parameters: + - name: file + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + new_filename: + type: string + new_filename_prefix: + type: string + enum: + - none + - date + - datetime + responses: + '200': + description: File successfully renamed + /dev/timezone: + get: + summary: Returns the current timezone + responses: + '200': + description: Current timezone + content: + application/json: + schema: + type: object + properties: + timezone: + type: string diff --git a/docker-compose-example.yml b/docker-compose-example.yml index ba1dc10..7e4b982 100755 --- a/docker-compose-example.yml +++ b/docker-compose-example.yml @@ -28,6 +28,7 @@ services: - OCR_PATH=ocr.php - TELEGRAM_TOKEN="" # note: keep the word bot in the string - TELEGRAM_CHATID=127585497 # note: target chat id. can be person or group + - ALLOW_GUI_FILEOPERATIONS=true restart: unless-stopped # optional, for OCR diff --git a/files/runScanner.sh b/files/runScanner.sh index b52f0d3..deb6be8 100755 --- a/files/runScanner.sh +++ b/files/runScanner.sh @@ -18,6 +18,7 @@ mkdir -p /scans chmod 777 /scans echo -n "" >/var/log/scanner.log chown "$NAME" /var/log/scanner.log +chmod 666 /var/log/scanner.log env >/opt/brother/scanner/env.txt chmod -R 777 /opt/brother echo "-----" @@ -55,6 +56,7 @@ echo "-----" echo "setting up webserver:" if [ "$WEBSERVER" == "true" ]; then + echo "www-data ALL=($NAME) NOPASSWD:ALL" >>/etc/sudoers echo "starting webserver for API & GUI..." @@ -85,18 +87,37 @@ if [ "$WEBSERVER" == "true" ]; then fi if [[ -n "$DISABLE_GUI_SCANTOOCR" ]]; then echo "\$DISABLE_GUI_SCANTOOCR=$DISABLE_GUI_SCANTOOCR;" + fi + if [[ -n "$ALLOW_GUI_FILEOPERATIONS" ]]; then + echo "\$ALLOW_GUI_FILEOPERATIONS=$ALLOW_GUI_FILEOPERATIONS;" fi echo "?>" } >/var/www/html/config.php + + + # Add rewrite rules to the Lighttpd configuration + cat <> /etc/lighttpd/lighttpd.conf + +server.modules += ( "mod_rewrite" ) + +url.rewrite-if-not-file = ( + "^/(.*)$" => "/index.php" +) + +EOL + chown www-data /var/www/html/config.php if [[ -z ${PORT} ]]; then PORT=80 fi + echo "running on port $PORT" sed -i "s/server.port\W*= 80/server.port = $PORT/" /etc/lighttpd/lighttpd.conf /usr/sbin/lighttpd -f /etc/lighttpd/lighttpd.conf echo "webserver started" + + else echo "webserver not configured" fi diff --git a/html/download.php b/html/download.php deleted file mode 100644 index d2c8c68..0000000 --- a/html/download.php +++ /dev/null @@ -1,25 +0,0 @@ - diff --git a/html/index.php b/html/index.php deleted file mode 100644 index 6171fa6..0000000 --- a/html/index.php +++ /dev/null @@ -1,226 +0,0 @@ - - - - - - - Brother <?php echo($MODEL); ?> - - - - - - - - - - - - - - - -
-
-
-
- -

-

Ready to scan

- - '.$button_file.'

'); - } - if (!isset($DISABLE_GUI_SCANTOEMAIL) || $DISABLE_GUI_SCANTOEMAIL != true) { - echo('

'.$button_email.'

'); - } - if (!isset($DISABLE_GUI_SCANTOIMAGE) || $DISABLE_GUI_SCANTOIMAGE != true) { - echo('

'.$button_image.'

'); - } - if (!isset($DISABLE_GUI_SCANTOOCR) || $DISABLE_GUI_SCANTOOCR != true) { - echo('

'.$button_ocr.'

'); - } - ?> -
-
-
-
- - -
- -
-
Last scanned
- -
-
- -
-
- - - - - - - - - - diff --git a/html/list.php b/html/list.php deleted file mode 100644 index e502a48..0000000 --- a/html/list.php +++ /dev/null @@ -1,56 +0,0 @@ -
- -
- - filemtime($filePath), - 'size' => filesize($filePath), - 'permissions' => substr(sprintf('%o', fileperms($filePath)), -4), - 'owner' => posix_getpwuid(fileowner($filePath))['name'], - 'group' => posix_getgrgid(filegroup($filePath))['name'], - ); - } -} - -// Sort files by modification time (newest first) -uasort($filesWithMtime, function($a, $b) { - return $b['mtime'] <=> $a['mtime']; -}); - -// Output sorted files -foreach ($filesWithMtime as $file => $attributes) { -?> - -
- - -
-
Bytes
-
- - - - -
-
- - diff --git a/html/scan.php b/html/scan.php deleted file mode 100644 index 8490893..0000000 --- a/html/scan.php +++ /dev/null @@ -1,47 +0,0 @@ -)"); -} -if (in_array($target, array('file','email','image','ocr'))) { - if ($_SERVER['REQUEST_METHOD'] == 'POST') { - // return immediately - $handle = popen('sudo -b -u \#'.$UID.' /opt/brother/scanner/brscan-skey/script/scanto'.$target.'.py', 'r'); - } else if ($_SERVER['REQUEST_METHOD'] == 'GET') { - // wait for completion - $output=shell_exec('sudo -u \#'.$UID.' /opt/brother/scanner/brscan-skey/script/scanto'.$target.'.py'); - } -} -else -{ - header($_SERVER["SERVER_PROTOCOL"] . " 400 OK"); - die("Error: Thou shalt not inject unknown script names!"); -} - -//TODO: Fix serving of file on get -//if ($_SERVER['REQUEST_METHOD'] == 'GET') { -// $files = scandir('/scans', SCANDIR_SORT_DESCENDING); -// $newest_file = $files[0]; -// header($_SERVER["SERVER_PROTOCOL"] . " 200 OK"); -// header("Cache-Control: public"); // needed for internet explorer -// header("Content-Type: application/pdf"); -// header("Content-Transfer-Encoding: Binary"); -// header("Content-Length:".filesize($newest_file)); -// readfile($newest_file); -// die(); -//} - -?> diff --git a/html/timezone.php b/html/timezone.php deleted file mode 100755 index 7d5a4e9..0000000 --- a/html/timezone.php +++ /dev/null @@ -1,12 +0,0 @@ -"; -echo "Time: " . date("Y-m-d H:i:s"); -?> \ No newline at end of file diff --git a/update-container.sh b/update-container.sh index 11eece0..901f0e2 100755 --- a/update-container.sh +++ b/update-container.sh @@ -1,2 +1,4 @@ -docker cp ./html brotherscannerdocker-brother-scanner-1:/var/www/ -docker cp ./script brotherscannerdocker-brother-scanner-1:/opt/brother/scanner/brscan-skey/ \ No newline at end of file +docker cp ./www brotherscannerdocker-brother-scanner-1:/var/ +docker exec brotherscannerdocker-brother-scanner-1 chown -R www-data:root /var/www/ +docker cp ./script brotherscannerdocker-brother-scanner-1:/opt/brother/scanner/brscan-skey/ +docker exec brotherscannerdocker-brother-scanner-1 chown -R root:root /opt/brother/scanner/brscan-skey/ \ No newline at end of file diff --git a/html/assets/bootstrap.5.1.3/bootstrap.bundle.min.js b/www/html/assets/bootstrap.5.1.3/bootstrap.bundle.min.js similarity index 100% rename from html/assets/bootstrap.5.1.3/bootstrap.bundle.min.js rename to www/html/assets/bootstrap.5.1.3/bootstrap.bundle.min.js diff --git a/html/assets/bootstrap.5.1.3/bootstrap.min.css b/www/html/assets/bootstrap.5.1.3/bootstrap.min.css similarity index 100% rename from html/assets/bootstrap.5.1.3/bootstrap.min.css rename to www/html/assets/bootstrap.5.1.3/bootstrap.min.css diff --git a/html/assets/fontawesome.5.15.4/LICENSE.txt b/www/html/assets/fontawesome.5.15.4/LICENSE.txt similarity index 100% rename from html/assets/fontawesome.5.15.4/LICENSE.txt rename to www/html/assets/fontawesome.5.15.4/LICENSE.txt diff --git a/html/assets/fontawesome.5.15.4/css/all.min.css b/www/html/assets/fontawesome.5.15.4/css/all.min.css similarity index 100% rename from html/assets/fontawesome.5.15.4/css/all.min.css rename to www/html/assets/fontawesome.5.15.4/css/all.min.css diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.eot b/www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.eot similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.eot rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.eot diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.svg b/www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.svg similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.svg rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.svg diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.ttf b/www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.ttf similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.ttf rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.ttf diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.woff b/www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.woff similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.woff rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.woff diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.woff2 b/www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.woff2 similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.woff2 rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.woff2 diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.eot b/www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.eot similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.eot rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.eot diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.svg b/www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.svg similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.svg rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.svg diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.ttf b/www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.ttf similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.ttf rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.ttf diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.woff b/www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.woff similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.woff rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.woff diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.woff2 b/www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.woff2 similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.woff2 rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.woff2 diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.eot b/www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.eot similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.eot rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.eot diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.svg b/www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.svg similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.svg rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.svg diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.ttf b/www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.ttf similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.ttf rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.ttf diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.woff b/www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.woff similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.woff rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.woff diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.woff2 b/www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.woff2 similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.woff2 rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.woff2 diff --git a/html/assets/jquery.3.7.1/jquery.min.js b/www/html/assets/jquery.3.7.1/jquery.min.js similarity index 100% rename from html/assets/jquery.3.7.1/jquery.min.js rename to www/html/assets/jquery.3.7.1/jquery.min.js diff --git a/www/html/assets/scripts.js b/www/html/assets/scripts.js new file mode 100755 index 0000000..db1a36a --- /dev/null +++ b/www/html/assets/scripts.js @@ -0,0 +1,302 @@ +function set_state_idle() { + $('#status-image').html(''); + $('#status-text').text('Ready to scan'); + $('.trigger-scan').removeClass('disabled'); +} + +function set_state_waiting() { + $('#status-image').html(''); + $('#status-text').text('Waiting for rear pages'); + $('.trigger-scan').removeClass('disabled'); +} + +function set_state_scan() { + let spinnerimage = ''; + if (spinnerimage != $('#status-image').html()) { + $('#status-image').html(spinnerimage); + } + $('#status-text').text('Scan in progress'); + $('.trigger-scan').addClass('disabled'); +} + +function set_state_ocr() { + $('#status-image').html(''); + $('#status-text').text('OCR in progress'); + $('.trigger-scan').removeClass('disabled'); +} + +function set_state(state) { + switch (state) { + case 'idle': + set_state_idle(); + break; + case 'waiting': + set_state_waiting(); + break; + case 'scan': + set_state_scan(); + break; + case 'ocr': + set_state_ocr(); + break; + default: + set_state_idle(); + } +} + +function load_files_offcanvas(){ + $.ajax({ + url: '/list-files', + method: 'GET', + success: function(response) { + // Populate the Offcanvas with the response content + $('#offcanvasContent').html(response); + + }, + error: function(xhr, status, error) { + console.error('Failed to load content:', error); + } + }); +} + +$(document).ready(function() { + + + $('.trigger-scan').click(function() { + var target = $(this).data('trigger'); + console.log('Triggered click event on element with class "trigger-scan" and data-trigger "' + target + '"'); + $.post('/api/scanner/scanto', { + target: target + }, function(data) { + console.log(data); + $(this).blur(); + + }); + }); + + + setInterval(function() { + $.get('/api/scanner/status', function(data) { + + + let state = 'idle'; + + + if (data.ocr && data.waiting && !data.scan) { + state = 'ocr'; + } else if (data.scan && data.waiting) { + state = 'scan'; + } else if (data.scan) { + state = 'scan'; + } else if (data.ocr && !data.scan) { + state = 'ocr'; + } else if (!data.ocr && !data.scan && data.waiting) { + state = 'waiting'; + } else if (!data.ocr && !data.scan && !data.waiting) { + state = 'idle'; + } + set_state(state); + }); + }, 1000); + + + + $('#triggerFiles').on('click', function(e) { + e.preventDefault(); + load_files_offcanvas(); + var offcanvas = new bootstrap.Offcanvas($('#offcanvasFiles')[0]); + offcanvas.show(); + + }); + + + + +}); + +function toggle_file_rename(source_element){ + var parentDiv = source_element.closest('.list-group-item'); + var parentId = parentDiv.attr('id'); + + + $("#"+parentId+" .file-info-label-default").toggleClass('d-none'); + $("#"+parentId+" .file-info-label-rename").toggleClass('d-none'); + + $("#"+parentId+" .file-name").toggleClass('d-none'); + $("#"+parentId+" .file-name-new").toggleClass('d-none'); + + $("#"+parentId+" .file-buttons-default").toggleClass('d-none'); + $("#"+parentId+" .file-buttons-rename").toggleClass('d-none'); + + $("#"+parentId+" .file-rename-prefix-date").checked = true; +} + +function toggle_file_delete(source_element){ + var parentDiv = source_element.closest('.list-group-item'); + var parentId = parentDiv.attr('id'); + + + $("#"+parentId+" .file-info-label-default").toggleClass('d-none'); + $("#"+parentId+" .file-info-label-delete").toggleClass('d-none'); + + $("#"+parentId+" .file-buttons-default").toggleClass('d-none'); + $("#"+parentId+" .file-buttons-delete").toggleClass('d-none'); +} + + +function toggle_file_error(parentId, message){ + + $('#'+parentId).addClass('bg-danger text-white'); + + var html =`
+ `+message+` + +
`; + $('#'+parentId).html(html); + +} + + +function toggle_file_success(parentId, message){ + + $('#'+parentId).addClass('bg-success text-white'); + + var html =`
+ `+message+` + +
`; + $('#'+parentId).html(html); + +} + + +$(document).on("click", ".refresh-files", function (e) { + e.preventDefault(); + load_files_offcanvas(); + +}); + + +$(document).on("click", ".file-rename", function (e) { + e.preventDefault(); + toggle_file_rename($(this)); + +}); + +$(document).on("click", ".file-rename-confirm", function (e) { + e.preventDefault(); + url = $(this).attr('href'); + var parentDiv = $(this).closest('.list-group-item'); + var parentId = parentDiv.attr('id'); + + var filename = $("#"+parentId+" .file-name-original").val(); + var new_filename = $("#"+parentId+" .file-name-new").val(); + + + var new_filename_prefix = 'none'; + + if($("#"+parentId+" .file-rename-prefix-none").is(':checked')) { + new_filename_prefix = 'none'; + } + if($("#"+parentId+" .file-rename-prefix-date").is(':checked')) { + new_filename_prefix = 'date'; + } + if($("#"+parentId+" .file-rename-prefix-datetime").is(':checked')) { + new_filename_prefix = 'datetime'; + } + + + console.log('url: '+url); + console.log('filename: '+filename); + console.log('new_filename: '+new_filename); + console.log('new_filename_prefix: '+new_filename_prefix); + + + if (new_filename == '' && new_filename_prefix == 'none') { + alert('Please enter a new filename or select a prefix'); + }else{ + + + payload = { + 'new_filename': new_filename, + 'new_filename_prefix': new_filename_prefix + }; + console.log(payload); + $.ajax({ + url: url, + type: 'PUT', + contentType: 'application/json', + data: JSON.stringify(payload), + success: function(response) { + + console.log(response); + console.log('File renamed'); + toggle_file_rename($(this)); + load_files_offcanvas(); + }, + error: function(xhr, status, error) { + + console.log('File NOT renamed'); + toggle_file_error(parentId, 'Rename failed'); + } + }); + + } + + + + + +}); + +$(document).on("click", ".file-rename-cancel", function (e) { + e.preventDefault(); + toggle_file_rename($(this)); +}); + + + +$(document).on("click", ".file-delete", function (e) { + e.preventDefault(); + toggle_file_delete($(this)); +}); + +$(document).on("click", ".file-delete-confirm", function (e) { + e.preventDefault(); + var parentDiv = $(this).closest('.list-group-item'); + var parentId = parentDiv.attr('id'); + + url = $(this).attr('href'); + $.ajax({ + url: url, + type: 'DELETE', + success: function(response) { + toggle_file_success(parentId, 'File deleted'); + setTimeout(() => { + load_files_offcanvas(); + }, 500); + + + }, + error: function(response, xhr, status, error) { + toggle_file_error(parentId, 'Delete failed'); + } + }); + +}); + +$(document).on("click", ".file-delete-cancel", function (e) { + e.preventDefault(); + toggle_file_delete($(this)); +}); + + + +//document.addEventListener('DOMContentLoaded', function () { +// var offcanvasElement = document.getElementById('offcanvasExample'); +// offcanvasElement.addEventListener('shown.bs.offcanvas', function () { +// // Trigger any necessary initialization here +// // For example, you can reinitialize the radio buttons or any other components +// console.log('Offcanvas is shown'); +// }); +//}); diff --git a/www/html/assets/scripts.min.js b/www/html/assets/scripts.min.js new file mode 100755 index 0000000..08ab578 --- /dev/null +++ b/www/html/assets/scripts.min.js @@ -0,0 +1 @@ +function set_state_idle(){$("#status-image").html(''),$("#status-text").text("Ready to scan"),$(".trigger-scan").removeClass("disabled")}function set_state_waiting(){$("#status-image").html(''),$("#status-text").text("Waiting for rear pages"),$(".trigger-scan").removeClass("disabled")}function set_state_scan(){let spinnerimage='';spinnerimage!=$("#status-image").html()&&$("#status-image").html(spinnerimage),$("#status-text").text("Scan in progress"),$(".trigger-scan").addClass("disabled")}function set_state_ocr(){$("#status-image").html(''),$("#status-text").text("OCR in progress"),$(".trigger-scan").removeClass("disabled")}function set_state(state){switch(state){case"idle":set_state_idle();break;case"waiting":set_state_waiting();break;case"scan":set_state_scan();break;case"ocr":set_state_ocr();break;default:set_state_idle()}}function load_files_offcanvas(){$.ajax({url:"/list-files",method:"GET",success:function(response){$("#offcanvasContent").html(response)},error:function(xhr,status,error){console.error("Failed to load content:",error)}})}function toggle_file_rename(source_element){var parentDiv,parentId=source_element.closest(".list-group-item").attr("id");$("#"+parentId+" .file-info-label-default").toggleClass("d-none"),$("#"+parentId+" .file-info-label-rename").toggleClass("d-none"),$("#"+parentId+" .file-name").toggleClass("d-none"),$("#"+parentId+" .file-name-new").toggleClass("d-none"),$("#"+parentId+" .file-buttons-default").toggleClass("d-none"),$("#"+parentId+" .file-buttons-rename").toggleClass("d-none"),$("#"+parentId+" .file-rename-prefix-date").checked=!0}function toggle_file_delete(source_element){var parentDiv,parentId=source_element.closest(".list-group-item").attr("id");$("#"+parentId+" .file-info-label-default").toggleClass("d-none"),$("#"+parentId+" .file-info-label-delete").toggleClass("d-none"),$("#"+parentId+" .file-buttons-default").toggleClass("d-none"),$("#"+parentId+" .file-buttons-delete").toggleClass("d-none")}function toggle_file_error(parentId,message){$("#"+parentId).addClass("bg-danger text-white");var html='
\n '+message+'\n \n
';$("#"+parentId).html(html)}function toggle_file_success(parentId,message){$("#"+parentId).addClass("bg-success text-white");var html='
\n '+message+'\n \n
';$("#"+parentId).html(html)}$(document).ready((function(){$(".trigger-scan").click((function(){var target=$(this).data("trigger");console.log('Triggered click event on element with class "trigger-scan" and data-trigger "'+target+'"'),$.post("/api/scanner/scanto",{target:target},(function(data){console.log(data),$(this).blur()}))})),setInterval((function(){$.get("/api/scanner/status",(function(data){let state="idle";data.ocr&&data.waiting&&!data.scan?state="ocr":data.scan&&data.waiting?state="scan":data.scan?state="scan":data.ocr&&!data.scan?state="ocr":data.ocr||data.scan||!data.waiting?data.ocr||data.scan||data.waiting||(state="idle"):state="waiting",set_state(state)}))}),1e3),$("#triggerFiles").on("click",(function(e){var offcanvas;e.preventDefault(),load_files_offcanvas(),new bootstrap.Offcanvas($("#offcanvasFiles")[0]).show()}))})),$(document).on("click",".refresh-files",(function(e){e.preventDefault(),load_files_offcanvas()})),$(document).on("click",".file-rename",(function(e){e.preventDefault(),toggle_file_rename($(this))})),$(document).on("click",".file-rename-confirm",(function(e){e.preventDefault(),url=$(this).attr("href");var parentDiv,parentId=$(this).closest(".list-group-item").attr("id"),filename=$("#"+parentId+" .file-name-original").val(),new_filename=$("#"+parentId+" .file-name-new").val(),new_filename_prefix="none";$("#"+parentId+" .file-rename-prefix-none").is(":checked")&&(new_filename_prefix="none"),$("#"+parentId+" .file-rename-prefix-date").is(":checked")&&(new_filename_prefix="date"),$("#"+parentId+" .file-rename-prefix-datetime").is(":checked")&&(new_filename_prefix="datetime"),console.log("url: "+url),console.log("filename: "+filename),console.log("new_filename: "+new_filename),console.log("new_filename_prefix: "+new_filename_prefix),""==new_filename&&"none"==new_filename_prefix?alert("Please enter a new filename or select a prefix"):(payload={new_filename:new_filename,new_filename_prefix:new_filename_prefix},console.log(payload),$.ajax({url:url,type:"PUT",contentType:"application/json",data:JSON.stringify(payload),success:function(response){console.log(response),console.log("File renamed"),toggle_file_rename($(this)),load_files_offcanvas()},error:function(xhr,status,error){console.log("File NOT renamed"),toggle_file_error(parentId,"Rename failed")}}))})),$(document).on("click",".file-rename-cancel",(function(e){e.preventDefault(),toggle_file_rename($(this))})),$(document).on("click",".file-delete",(function(e){e.preventDefault(),toggle_file_delete($(this))})),$(document).on("click",".file-delete-confirm",(function(e){e.preventDefault();var parentDiv,parentId=$(this).closest(".list-group-item").attr("id");url=$(this).attr("href"),$.ajax({url:url,type:"DELETE",success:function(response){toggle_file_success(parentId,"File deleted"),setTimeout(()=>{load_files_offcanvas()},500)},error:function(response,xhr,status,error){toggle_file_error(parentId,"Delete failed")}})})),$(document).on("click",".file-delete-cancel",(function(e){e.preventDefault(),toggle_file_delete($(this))})); \ No newline at end of file diff --git a/www/html/assets/style.css b/www/html/assets/style.css new file mode 100755 index 0000000..4bb2206 --- /dev/null +++ b/www/html/assets/style.css @@ -0,0 +1,16 @@ +.list-group-hover, +.list-group-item:hover { + background-color: #f5f5f5; +} + +.file-name-new{ + font-weight:bolder; + font-size: 1rem; +} + +#fileslist :focus, +#fileslist :active { + box-shadow: none; + outline: none; +} + diff --git a/www/html/assets/style.min.css b/www/html/assets/style.min.css new file mode 100755 index 0000000..f494bd9 --- /dev/null +++ b/www/html/assets/style.min.css @@ -0,0 +1 @@ +.list-group-hover,.list-group-item:hover{background-color:#f5f5f5}.file-name-new{font-weight:bolder;font-size:1rem}#fileslist :active,#fileslist :focus{box-shadow:none;outline:0} \ No newline at end of file diff --git a/www/html/index.php b/www/html/index.php new file mode 100644 index 0000000..1cb3507 --- /dev/null +++ b/www/html/index.php @@ -0,0 +1,130 @@ + "Warning", + E_NOTICE => "Notice", + E_ERROR => "Error", + E_API => "API", // Custom log type + E_FRONTEND => "Frontend", // Custom log type + default => "Unknown" + }; + + if (($errno == E_API) OR ($errno == E_FRONTEND)) { + $logMessage = "[$errorType] $errstr\n"; + } else { + $logMessage = "[$date] [$errorType] $errstr in $errfile on line $errline\n"; + } + + error_log($logMessage, 3, '/var/log/scanner.log'); +} + +set_error_handler("customErrorHandler"); +set_include_path('/var/www/private/'); + +include('config.php'); +require_once('classes/AltoRouter.php'); +require_once('helper.php'); + +if (!isset($TZ)) { + $TZ = 'Europe/Berlin'; +} +date_default_timezone_set($TZ); + +#session_start(); + +$router = new AltoRouter(); +$router->addMatchTypes(array('char' => '(?:[^\/]*)')); + + +// Frontend routes + +$router->map( 'GET', '/', function() { + require_once 'views/frontend/home.php'; +}); + + +$router->map( 'GET', '/list-files', function() { + require_once 'views/frontend/file-list.php'; +}); + + +$router->map( 'GET', '/file/[char:file]/rename', function( $file) { + require_once 'views/frontend/file-rename.php'; +}); + + +$router->map( 'GET', '/file/[char:file]/delete', function( $file) { + require_once 'views/frontend/file-delete.php'; +}); + + +// API routes + +$router->map( 'GET', '/api/scanner/status', function() { + require_once 'views/api/scanner-status.php'; +}); + + +$router->map( 'POST', '/api/scanner/scanto', function() { + $scanto = $_POST["target"]; + $method = 'return'; + require_once 'views/api/scanner-scanto.php'; +}); + + +$router->map( 'GET', '/api/scanner/scanto/[char:parameter]', function( $parameter) { + $scanto = $parameter; + $method = 'wait'; + require_once 'views/api/scanner-scanto.php'; +}); + + +$router->map( 'GET', '/api/file-list', function() { + require_once 'views/api/file-list.php'; +}); + +$router->map( 'GET', '/api/file/[char:file]/info', function( $file) { + require_once 'views/api/file-info.php'; +}); + + +$router->map( 'GET', '/api/file/[char:file]/download', function( $file) { + require_once 'views/api/file-download.php'; +}); + +$router->map( 'DELETE', '/api/file/[char:file]/delete', function( $file) { + require_once 'views/api/file-delete.php'; +}); + + +$router->map( 'PUT', '/api/file/[char:file]/rename', function( $file) { + require_once 'views/api/file-rename.php'; +}); + + +$router->map( 'GET', '/api/dev/timezone', function() { + require_once 'views/api/dev-timezone.php'; +}); + + +$match = $router->match(); + + +// Call closure or throw 404 status if route not found + +if( is_array($match) && is_callable( $match['target'] ) ) { + call_user_func_array( $match['target'], $match['params'] ); +} else { + if (str_contains($_SERVER['REQUEST_URI'], '/api')) { + send_json_error(404, 'Not Found'); + } else { + send_error_page(404, $page_title='404', $page_message='Sorry, the page you are looking for could not be found.'); + } + exit(); +} +?> \ No newline at end of file diff --git a/www/private/classes/AltoRouter.php b/www/private/classes/AltoRouter.php new file mode 100755 index 0000000..daeab62 --- /dev/null +++ b/www/private/classes/AltoRouter.php @@ -0,0 +1,300 @@ + + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +class AltoRouter +{ + + /** + * @var array Array of all routes (incl. named routes). + */ + protected $routes = []; + + /** + * @var array Array of all named routes. + */ + protected $namedRoutes = []; + + /** + * @var string Can be used to ignore leading part of the Request URL (if main file lives in subdirectory of host) + */ + protected $basePath = ''; + + /** + * @var array Array of default match types (regex helpers) + */ + protected $matchTypes = [ + 'i' => '[0-9]++', + 'a' => '[0-9A-Za-z]++', + 'h' => '[0-9A-Fa-f]++', + '*' => '.+?', + '**' => '.++', + '' => '[^/\.]++' + ]; + + /** + * Create router in one call from config. + * + * @param array $routes + * @param string $basePath + * @param array $matchTypes + * @throws Exception + */ + public function __construct(array $routes = [], string $basePath = '', array $matchTypes = []) + { + $this->addRoutes($routes); + $this->setBasePath($basePath); + $this->addMatchTypes($matchTypes); + } + + /** + * Retrieves all routes. + * Useful if you want to process or display routes. + * @return array All routes. + */ + public function getRoutes(): array + { + return $this->routes; + } + + /** + * Add multiple routes at once from array in the following format: + * + * $routes = [ + * [$method, $route, $target, $name] + * ]; + * + * @param array $routes + * @return void + * @author Koen Punt + * @throws Exception + */ + public function addRoutes($routes) + { + if (!is_array($routes) && !$routes instanceof Traversable) { + throw new RuntimeException('Routes should be an array or an instance of Traversable'); + } + foreach ($routes as $route) { + call_user_func_array([$this, 'map'], $route); + } + } + + /** + * Set the base path. + * Useful if you are running your application from a subdirectory. + * @param string $basePath + */ + public function setBasePath(string $basePath) + { + $this->basePath = $basePath; + } + + /** + * Add named match types. It uses array_merge so keys can be overwritten. + * + * @param array $matchTypes The key is the name and the value is the regex. + */ + public function addMatchTypes(array $matchTypes) + { + $this->matchTypes = array_merge($this->matchTypes, $matchTypes); + } + + /** + * Map a route to a target + * + * @param string $method One of 5 HTTP Methods, or a pipe-separated list of multiple HTTP Methods (GET|POST|PATCH|PUT|DELETE) + * @param string $route The route regex, custom regex must start with an @. You can use multiple pre-set regex filters, like [i:id] + * @param mixed $target The target where this route should point to. Can be anything. + * @param string $name Optional name of this route. Supply if you want to reverse route this url in your application. + * @throws Exception + */ + public function map(string $method, string $route, $target, string $name = null) + { + + $this->routes[] = [$method, $route, $target, $name]; + + if ($name) { + if (isset($this->namedRoutes[$name])) { + throw new RuntimeException("Can not redeclare route '{$name}'"); + } + $this->namedRoutes[$name] = $route; + } + } + + /** + * Reversed routing + * + * Generate the URL for a named route. Replace regexes with supplied parameters + * + * @param string $routeName The name of the route. + * @param array $params Associative array of parameters to replace placeholders with. + * @return string The URL of the route with named parameters in place. + * @throws Exception + */ + public function generate(string $routeName, array $params = []): string + { + + // Check if named route exists + if (!isset($this->namedRoutes[$routeName])) { + throw new RuntimeException("Route '{$routeName}' does not exist."); + } + + // Replace named parameters + $route = $this->namedRoutes[$routeName]; + + // prepend base path to route url again + $url = $this->basePath . $route; + + if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) { + foreach ($matches as $index => $match) { + list($block, $pre, $type, $param, $optional) = $match; + + if ($pre) { + $block = substr($block, 1); + } + + if (isset($params[$param])) { + // Part is found, replace for param value + $url = str_replace($block, $params[$param], $url); + } elseif ($optional && $index !== 0) { + // Only strip preceding slash if it's not at the base + $url = str_replace($pre . $block, '', $url); + } else { + // Strip match block + $url = str_replace($block, '', $url); + } + } + } + + return $url; + } + + /** + * Match a given Request Url against stored routes + * @param string $requestUrl + * @param string $requestMethod + * @return array|boolean Array with route information on success, false on failure (no match). + */ + public function match(string $requestUrl = null, string $requestMethod = null) + { + + $params = []; + + // set Request Url if it isn't passed as parameter + if ($requestUrl === null) { + $requestUrl = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'; + } + + // strip base path from request url + $requestUrl = substr($requestUrl, strlen($this->basePath)); + + // Strip query string (?a=b) from Request Url + if (($strpos = strpos($requestUrl, '?')) !== false) { + $requestUrl = substr($requestUrl, 0, $strpos); + } + + $lastRequestUrlChar = $requestUrl ? $requestUrl[strlen($requestUrl)-1] : ''; + + // set Request Method if it isn't passed as a parameter + if ($requestMethod === null) { + $requestMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; + } + + foreach ($this->routes as $handler) { + list($methods, $route, $target, $name) = $handler; + + $method_match = (stripos($methods, $requestMethod) !== false); + + // Method did not match, continue to next route. + if (!$method_match) { + continue; + } + + if ($route === '*') { + // * wildcard (matches all) + $match = true; + } elseif (isset($route[0]) && $route[0] === '@') { + // @ regex delimiter + $pattern = '`' . substr($route, 1) . '`u'; + $match = preg_match($pattern, $requestUrl, $params) === 1; + } elseif (($position = strpos($route, '[')) === false) { + // No params in url, do string comparison + $match = strcmp($requestUrl, $route) === 0; + } else { + // Compare longest non-param string with url before moving on to regex + // Check if last character before param is a slash, because it could be optional if param is optional too (see https://github.com/dannyvankooten/AltoRouter/issues/241) + if (strncmp($requestUrl, $route, $position) !== 0 && ($lastRequestUrlChar === '/' || $route[$position-1] !== '/')) { + continue; + } + + $regex = $this->compileRoute($route); + $match = preg_match($regex, $requestUrl, $params) === 1; + } + + if ($match) { + if ($params) { + foreach ($params as $key => $value) { + if (is_numeric($key)) { + unset($params[$key]); + } + } + } + + return [ + 'target' => $target, + 'params' => $params, + 'name' => $name + ]; + } + } + + return false; + } + + /** + * Compile the regex for a given route (EXPENSIVE) + * @param string $route + * @return string + */ + protected function compileRoute(string $route): string + { + if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) { + $matchTypes = $this->matchTypes; + foreach ($matches as $match) { + list($block, $pre, $type, $param, $optional) = $match; + + if (isset($matchTypes[$type])) { + $type = $matchTypes[$type]; + } + if ($pre === '.') { + $pre = '\.'; + } + + $optional = $optional !== '' ? '?' : null; + + //Older versions of PCRE require the 'P' in (?P) + $pattern = '(?:' + . ($pre !== '' ? $pre : null) + . '(' + . ($param !== '' ? "?P<$param>" : null) + . $type + . ')' + . $optional + . ')' + . $optional; + + $route = str_replace($block, $pattern, $route); + } + } + return "`^$route$`u"; + } +} \ No newline at end of file diff --git a/www/private/helper.php b/www/private/helper.php new file mode 100755 index 0000000..133e644 --- /dev/null +++ b/www/private/helper.php @@ -0,0 +1,161 @@ + $http_code, 'message' => $message)); + die(); +} + + +function send_error_page($http_code, $page_title='', $page_message=''){ + http_response_code($http_code); + if ($page_title != '' && $page_message != ''){ + require 'views/frontend/error.php'; + } + die(); +} + +/** + * Constructs a safe file path within a specified directory. + * + * This function takes a directory and a filename, constructs the full file path, + * and ensures that the file path is within the specified directory. It prevents + * directory traversal attacks by validating the real path of the constructed file path. + * + * @param string $directory The directory in which the file should be located. + * @param string $filename The name of the file. + * @return string|false The real path to the file if it is within the specified directory, or false if it is not. + */ +function file_get_real_filepath($directory, $filename) { + + $filename = basename($filename); + $filePath = $directory . DIRECTORY_SEPARATOR . $filename; + $realPath = realpath($filePath); + + if ($realPath === false || strpos($realPath, realpath($directory)) !== 0) { + return false; + } + + return $realPath; +} + + +function file_get_verified_fileinfo($dir, $file) { + + + $filename = file_get_real_filepath($dir, $file); + + if ($filename === false) { + send_json_error(400, "No valid file specified"); + } + + if(!file_exists($filename)){ + send_json_error(404, "File does not exist"); + } + + $pathInfo = pathinfo($filename); + $filenameWithoutExtension = pathinfo($filename, PATHINFO_FILENAME); + $fileCreationTime = filectime($filename); + $fileModificationTime = filemtime($filename); + $finfo = new finfo(FILEINFO_MIME_TYPE); + $mimetype = $finfo->file($filename); + + $file_info = array( + 'full_path' => $filename, + 'file' => $pathInfo['basename'] ?? '', + 'name' => $filenameWithoutExtension ?? '', + 'name_clean' => '', + 'dir' => $pathInfo['dirname'] ?? '', + 'date_from_name' => '', + 'time_from_name' => '', + 'fileCreationTime' => $fileCreationTime, + 'fileModificationTime' => $fileModificationTime, + 'date_from_file' => date('Y-m-d', $fileModificationTime), + 'time_from_file' => date('H-i-s', $fileModificationTime), + 'extension' => $pathInfo['extension'] ?? '', + 'mimetype' => $mimetype, + 'size' => filesize($filename) + ); + + if (preg_match('/(\d{4}-\d{2}-\d{2})(?:-(\d{2})(?:-(\d{2})(?:-(\d{2}))?)?)?/', $filename, $matches)) { + + $file_info['date_from_name'] = $matches[1] ?? ''; + + if (isset($matches[2])) { + $file_info['time_from_name'] = $matches[2] . ':' . ($matches[3] ?? '00') . ':' . ($matches[4] ?? '00'); + } + } + // Combine date and time with the dash to form the full datetime string + $pattern = '/^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\s*/'; + $remove_datetime = $file_info['date_from_name'].'-'.$file_info['time_from_name']; + $clean_name = preg_replace($pattern, '', $filenameWithoutExtension); + + // Remove only the date and time without extra spaces around + $remove_date = $file_info['date_from_name']; + $clean_name = str_replace($remove_date, '', $clean_name); + + $remove_time = $file_info['time_from_name']; + $clean_name = str_replace($remove_time, '', $clean_name); + + // Trim any remaining leading or trailing spaces + $clean_name = trim($clean_name); + $file_info['name_clean'] = $clean_name; + + if($file_info['mimetype'] != 'application/pdf'){ + send_json_error(400, "No valid file specified"); + } + + + return $file_info; +} + + +function file_is_valid_name_string($filename) { + + $pattern = '/[<>:"\/\\|?*\x00-\x1F]/'; // Invalid characters for filenames + + if (preg_match($pattern, $filename)) { + return false; + } + + if (strlen($filename) > 255) { + return false; + } + + if (strlen($filename) < 3) { + return false; + } + + + return true; +} + +function list_files($dir){ + $files = scandir($dir); + $files = array_diff($files, array('.', '..')); + + $data = array(); + foreach ($files as $file) { + $filePath = $dir . '/' . $file; + if (is_file($filePath) && pathinfo($filePath, PATHINFO_EXTENSION) === 'pdf') { + $data[] = file_get_verified_fileinfo($dir, $file); + } + + } + uasort($data, function($a, $b) { + return $b['fileModificationTime'] <=> $a['fileModificationTime']; + }); + return array_values($data); +} +?> \ No newline at end of file diff --git a/www/private/views/api/dev-timezone.php b/www/private/views/api/dev-timezone.php new file mode 100755 index 0000000..a812b54 --- /dev/null +++ b/www/private/views/api/dev-timezone.php @@ -0,0 +1,12 @@ + date_default_timezone_get(), + 'datetime' => date("Y-m-d H:i:s") +); +trigger_error("Timezone: ".$timezone_data['timezone'] . " DateTime: ".$timezone_data['datetime'], E_API); +json($timezone_data); + +?> \ No newline at end of file diff --git a/www/private/views/api/file-delete.php b/www/private/views/api/file-delete.php new file mode 100755 index 0000000..06e8a3a --- /dev/null +++ b/www/private/views/api/file-delete.php @@ -0,0 +1,26 @@ + 'success')); +} else { + trigger_error("can not deleted file ".$file_info['full_path'], E_API); + send_json_error(500, "Could not delete file"); +} + +?> \ No newline at end of file diff --git a/www/private/views/api/file-download.php b/www/private/views/api/file-download.php new file mode 100644 index 0000000..f831d08 --- /dev/null +++ b/www/private/views/api/file-download.php @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/www/private/views/api/file-info.php b/www/private/views/api/file-info.php new file mode 100755 index 0000000..8b5c952 --- /dev/null +++ b/www/private/views/api/file-info.php @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/www/private/views/api/file-list.php b/www/private/views/api/file-list.php new file mode 100755 index 0000000..1a1e782 --- /dev/null +++ b/www/private/views/api/file-list.php @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/www/private/views/api/file-rename.php b/www/private/views/api/file-rename.php new file mode 100755 index 0000000..a7b6049 --- /dev/null +++ b/www/private/views/api/file-rename.php @@ -0,0 +1,81 @@ + fileatime($original_filename), // Access time + 'modification_time' => filemtime($original_filename) // Modification time + ]; +} + +$file_info = file_get_verified_fileinfo('/scans/', urldecode($file)); + +$original_filename = $file_info['full_path']; + +$jsonData = file_get_contents('php://input'); +$data = json_decode($jsonData, true); + + + +if ($data !== null) { + $new_filename = $data['new_filename']; + $new_filename_prefix = $data['new_filename_prefix']; + + +} else { + trigger_error("JSON decoding error", E_API); + send_json_error(400, "JSON decoding error"); +} + +if (file_is_valid_name_string($new_filename)){ + + + $target_filename = preg_replace('/[^a-zA-Z0-9äöüßÄÖÜ\-\_ ]/u', '', $new_filename) . '.' . strtolower($file_info['extension']); + + $final_filename = $file_info['dir'] . '/' . $target_filename; + + + if ($new_filename_prefix == 'date') { + + $final_filename = $file_info['dir'] . '/' . $file_info['date_from_file'] . ' ' . $target_filename; + + } elseif ($new_filename_prefix == 'datetime') { + $final_filename = $file_info['dir'] . '/' . $file_info['date_from_file'] . '-' . $file_info['time_from_file'] . ' ' . $target_filename; + } + + + // Get access and modification times of the old file + $times = getFileTimes($original_filename); + + // Rename the file + if (rename($original_filename, $final_filename)) { + + // Restore access and modification time using 'touch' + @touch($final_filename, $times['modification_time'], $times['access_time']); + send_json_error(200, "Renamed file successfully"); + } else { + trigger_error("Error renaming the file", E_API); + send_json_error(400, "Error renaming the file"); + } + +}else{ + send_json_error(400, "Invalid filename"); +} + + + +?> \ No newline at end of file diff --git a/www/private/views/api/scanner-scanto.php b/www/private/views/api/scanner-scanto.php new file mode 100644 index 0000000..a1cfa19 --- /dev/null +++ b/www/private/views/api/scanner-scanto.php @@ -0,0 +1,58 @@ + 'Scan triggered','method' => 'post','target' => $target)); + } else if ($method == 'wait') { + shell_exec('sudo -u \#'.$UID.' /opt/brother/scanner/brscan-skey/script/scanto'.$target.'.py'); + json(array('message' => 'Scan triggered','method' => 'get','target' => $target)); + } +} + + +$target = safe_guard_target($scanto); + +trigger_script($target, $UID, $method); + +?> \ No newline at end of file diff --git a/html/active.php b/www/private/views/api/scanner-status.php similarity index 88% rename from html/active.php rename to www/private/views/api/scanner-status.php index a88f1cd..889b7e4 100644 --- a/html/active.php +++ b/www/private/views/api/scanner-status.php @@ -1,4 +1,6 @@ diff --git a/www/private/views/frontend/common-head.php b/www/private/views/frontend/common-head.php new file mode 100755 index 0000000..0fa1695 --- /dev/null +++ b/www/private/views/frontend/common-head.php @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/www/private/views/frontend/common-javascript.php b/www/private/views/frontend/common-javascript.php new file mode 100755 index 0000000..164d00f --- /dev/null +++ b/www/private/views/frontend/common-javascript.php @@ -0,0 +1,2 @@ + + diff --git a/www/private/views/frontend/error.php b/www/private/views/frontend/error.php new file mode 100755 index 0000000..c02d8b2 --- /dev/null +++ b/www/private/views/frontend/error.php @@ -0,0 +1,45 @@ + + + + + + + <?php echo($page_title); ?> + + + + + +
+
+
+
+ +

+

+ + +
+
+
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/www/private/views/frontend/file-list.php b/www/private/views/frontend/file-list.php new file mode 100644 index 0000000..4f8a6af --- /dev/null +++ b/www/private/views/frontend/file-list.php @@ -0,0 +1,89 @@ + +
+ +
+ + +
+
+ + + + +
+
+
KB
+
Are you sure you want to delete this file?
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+ + + + + + + +
+
+
+
+ + DELETE +
+
+
+
+ + SAVE +
+
+
+
+ + + +
+
+ + diff --git a/www/private/views/frontend/home.php b/www/private/views/frontend/home.php new file mode 100755 index 0000000..8135db6 --- /dev/null +++ b/www/private/views/frontend/home.php @@ -0,0 +1,113 @@ + + + + + + + Brother <?php echo($MODEL); ?> + + + + + + +
+
+
+
+ +

+

Ready to scan

+ + '.$button_file.'

'); + } + if (!isset($DISABLE_GUI_SCANTOEMAIL) || $DISABLE_GUI_SCANTOEMAIL != true) { + echo('

'.$button_email.'

'); + } + if (!isset($DISABLE_GUI_SCANTOIMAGE) || $DISABLE_GUI_SCANTOIMAGE != true) { + echo('

'.$button_image.'

'); + } + if (!isset($DISABLE_GUI_SCANTOOCR) || $DISABLE_GUI_SCANTOOCR != true) { + echo('

'.$button_ocr.'

'); + } + ?> +
+
+
+
+ + +
+ +
+
Last scanned
+ +
+
+ + + + + +
+
+ + + + + + + + + + + + + + + \ No newline at end of file