diff --git a/CHANGELOG.md b/CHANGELOG.md index c335c57..a487e6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,70 @@ # CHANGELOG +## v8.7 + +* Added automatic update check within the Settings page allowing to see the changelogs and a link to the new Release. +* Updated `README.md` +* Admins can now select the default worktime type to be selected in the form within the app.json `config` section via the `default_worktime_type` key. +* Added function to automatically add keys to app.json after update +* Admins can now customize the look and feel of the PDF exports. Please check `README.md` `Exports` section for more information. + +## v8.6 + +* Telemetry statistics page for environments using the Telemetry Server. Please check `README.md` + +## v8.5.1 + +* Fixed undefined variable warning message +* Changed `app.json.sample` default values +* Updated README.md +* Added `update.sh` script +* Internal plugin views can now be hidden +* Fix utility plugin 500 error when trying to export data for user that doesn't exist + +## v8.5 + +* Fixed an issue with IDs not generated correctly for project items. +* Added functionality to delete and edit project items. +* Adding users to a project has been made easier. +* Internal changes +* Added additional plugin permission level +* Added 2 new themes + +## v8.4.3 + +* Now displaying the instance uuid within the settings page. +* Added ability to reset the instance uuid via the settings page. + +## v8.4.2 + +* Added user based permissions for plugin views. +* Updated plugins to use new permission system. + +## v8.4.1 + +**This update requires DB migration** - see `README.md` section `Database` + +* Added telemetry support (disabled by default, can be enabled within `app.json`) +* Added telemetry server to receive telemetry data (see `api/v1/class/telemetry/server/README.md` for more information) + +## v8.4 + +* Added `nfcclock` plugin to allow clocking in and out with NFC tags (requires `nfclogin` plugin) +* Updated `nfclogin` plugin to version `1.2` (added Toil API route for `nfcclock` called `nfclclock`) +* Removed unused `extract_plugin` function from `PluginDevTool` class +* Custom language files can now be used with the `i18n` class by placing them within the `data/i18n/custom/` directory. This also works for plugins. + +## v8.3.3 + +**This update might require DB migration** - see `README.md` section `Database` + +* Added migration script for adding the `pid` column to the `projects_items` table for already existing installations. The script will not be executed for new installations. + +## v8.3.2 + +* Added script to initialize the database for demo purposes when using Docker (See `README.md`) +* Fixed missing `active` column within the `users` table when initializing the database for the first time + ## v8.3.1 * Removed deprecated `app` attribute from `general` section within `app.json` diff --git a/README.md b/README.md index 0456851..3d81b39 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,12 @@ TimeTrack aims to be an easy-to-use time recording software for small enterprise - Plugin Support - Exporting to PDF/CSV +A demo is available here: [https://tt-demo.openducks.org](https://tt-demo.openducks.org) +**The demo is available with limited features only, e.g. the plugin system disabled.** +**The demo currently does not work as intended...** + +> You would like to support the project? Consider helping out with the documentation at [https://timetrackd.openducks.org](https://timetrackd.openducks.org) or by contributing to the code. + ## Installation ### Quick Install with Docker @@ -24,7 +30,7 @@ TimeTrack aims to be an easy-to-use time recording software for small enterprise You can quickly get started with TimeTrack using Docker. Follow these steps: * Ensure you have Docker and Docker Compose installed on your system. -* Clone the TimeTrack repository: `git clone https://github.com/Ente/timetrack.git` & `cd timetrack` +* Clone the TimeTrack repository: `git clone https://github.com/Ente/timetrack.git` & `cd timetrack` - **The develop branch should not be used unless you know what you are doing. Download the latest release [https://github.com/ente/timetrack/releases/latest](here)** * Build the Docker image: `docker build -t openducks/timetrack .` * Create a `app.json` configuration file based on the provided sample below: `cp api/v1/inc/app.json.sample api/v1/inc/app.json` and edit it to fit your needs. * Adjust the database settings if needed (at least `db_password`) @@ -36,6 +42,9 @@ You can quickly get started with TimeTrack using Docker. Follow these steps: Certain features, like the NFC login may require additional setup for parsing the USB device. +If you want to use the demo, you can run the provided `demo_setup.sh` script within the project root. This will automatically setup the database with demo data (worktimes and users) and rebuilds the entire container. +You may want to set the `demo` setting within the `app.json` to `true` to display the demo credentials on the login page. + ### Requirements - PHP 8.2 (`curl|gd|gmp|intl|mbstring|mysqli|openssl|xsl|gettext|dom|ldap`) - tested with PHP 8.2.26 @@ -49,16 +58,48 @@ This software has been tested on Debian 11/12, XAMPP, PHP internal server (e.g. Simply install the software by following these steps: - Install php and requirements: `sudo apt update && sudo apt install php8.2 php8.2-curl php8.2-gd php8.2-gmp php8.2-intl php8.2-mbstring php8.2-mysqli php8.2-pgsql php8.2-xsl php8.2-gettext php8.2-dom php8.2-ldap composer git mariadb-server apache2 -y` and enable the apache rewrite mod `a2enmod rewrite && service apache2 restart`. If you do not want to use apache2 you can skip this step. -- Git clone timetrack to e.g. `/var/www`: `cd /var/www && git clone https://github.com/Ente/timetrack.git && cd timetrack` +- Git clone timetrack to e.g. `/var/www`: `cd /var/www && git clone https://github.com/Ente/timetrack.git && cd timetrack` - **The develop branch should not be used unless you know what you are doing. Download the latest release [https://github.com/ente/timetrack/releases/latest](here)** - Install requirements for composer `composer install` - Create a new database, e.g. with the name `ab` and create a dedicated user, login (`mysql -u root -p`) then e.g. `timetool`: `CREATE DATABASE ab;` and `CREATE USER 'timetool'@'localhost' IDENTIFIED BY 'yourpassword';` and `GRANT ALL PRIVILEGES ON ab.* TO 'timetool'@'localhost';` don't forget to `FLUSH PRIVILEGES;`! - Configure `app.json` (see below - required changes: `base_url`, `db_user`, `db_password`, `smtp` section and any other if your installation is different) then `mv api/v1/inc/app.json.sample app.json && cd /var/www/timetrack` - Run DB migrations: `vendor/bin/phinx migrate` -- Start webserver e.g. `service apache2 stop && php -S 0.0.0.0:80` or using apache2 (then you have to configure the `sites-available` conf yourself) -- You can then access TimeTrack in your browser at `http://localhost`, default login is `admin` with password `admin`. Create yourself a new admin account, login and delete the default account afterwards. +- Follow "Use with ..." guides + +#### Use with apache2.4 + +- Create a new virtual host: `sudo nano /etc/apache2/sites-available/timetrack.conf` +- Content: + +```conf + + ServerName timetrack.yourdomain.de + DocumentRoot /var/www/timetrack + + + AllowOverride All + Require all granted + + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + +``` + +- Enable site and module: `sudo a2ensite timetrack && a2enmod rewrite` + +#### Use with PHP development server + +- Start server: `cd /var/www/timetrack && php -S 0.0.0.0:80` + +#### Finalize + +You can now access TimeTrack in your browser at `http://localhost`, default login is `admin` with password `admin`. Create yourself a new admin account, login and delete the default account afterwards. To save log files, please create the subfolder `data/logs` and make it writeable to the web server (e.g. `chown www-data:www-data data/logs && chmod 775 data/logs`). -Please also make sure that the `/data` directory is writable by the webserver, aswell as the plugins directory (default: `api/v1/class/plugins/plugins`). +Please also make sure that the `/data` directory is writable by the webserver, aswell as the plugins directory (default: `api/v1/class/plugins/plugins`). The `/api/v1/toil/permissions.json` also needs to be writeable by the webserver. + +**You can run the `update.sh` script to update your instance: `sudo sh update.sh`** ### Configure app.json @@ -74,6 +115,10 @@ In step 2, you need to configure the `app.json.sample` within the `api/v1/inc` f - `timezone`: Set the timezone of your application, e.g. `Europe/Berlin` or `America/New_York` (default: `UTC`) - `force_theme`: Force a theme for all users, this disables the feature allowing users to set their own theme. - `theme_file`: If `force_theme` is true, the specified theme is used (default: `/assets/css/v8.css`) +- `demo`: If set to `true`, demo credentials are shown on the login page. Useful for demo installations. +- `telemetry`: Enable/disable telemetry (Default: `enabled` - **PLEASE DISABLE IF NEEDED**) +- `telemetry_server_url`: Full server url to telemetry upload +- `telemetryServer`: Enables/disables the Server Telemetry Statistics page #### **SMTP section** @@ -179,6 +224,15 @@ $arbeit->exportModule()->getExportModule("MyExportExportModule")->export($data); All existing export modules can be accessed with the `ExportManager` Plugin. You can specify your own CSS file within the `app.json` `exports -> pdf -> css` setting (full path) - the default is `api/v1/class/exports/modules/PDFExportModule/css/index.css` +### Custom contents + +You can use custom contents within your PDF exports by placing HTML/PHP files into `api/v1/class/exports/modules/PDFExportModule/php/`. +Two files can be placed there: + +- `user_content_ending.php`: This file is included at the end of the PDF export, e.g. for signatures or custom footers +- `user_content_starting.php`: This file is included at the beginning of the PDF export, e.g. for custom headers. +You can put normal PHP and HTML code into these files. + ## QR codes You can use the plugin `QRClock` to generate QR codes for yourself to either clock in or out. The QR code generated can be saved for later use, e.g. print it out. @@ -212,9 +266,29 @@ To upload a new theme, simply place it into the `/assets/css` folder. The theme the user selected is saved as a cookie, meaning it is only selected on the current device. On mobile or on another device, the user has to set the desired theme again. +## Run as Telemetry server + +Basically, there are only a few steps to do + +1. Navigate to the telemetry server directory: `cd api/v1/class/telemetry/server` +2. Start server: `nohup php -S 0.0.0.0:8888 server.php > telemetry.log 2>&1 &` +3. Set `telemetryServer` inside app.json `general` section to `true`. +4. Check received telemetry data on the "Server Telemetry" page. + +## Change Telemetry server URL for managed environments + +If you want all of your TimeTrack instances to point to your telemetry server instance, you need to change the app.json `general` `telemetry_server_url` attribute to your URL. +Also make sure the `telemetry` attribute is set to `"enabled"`. + +To check if all worked simply visit the Settings page using an admin account. Check the checkbox within the Telemetry section at the bottom of the page and click the Submit button. +On your server timetrack instance you need to visit the "Server Telemetry" page to check if you received the new/updated telemetry data. + ## Updates TimeTrack has to be updated in two ways: database and application. +A full update on linux based machines can also be performed by executing the `update.sh` file inside the root directory. In any other cases follow the steps below: + +If you were seeking assistance and were asked to try out the changes in a branch, please execute this command inside the timetrack root directory: `git fetch && git checkout BRANCH` - replace BRANCH with the actual branch name, e.g. TT-24 or develop. ### Application diff --git a/VERSION b/VERSION index 905c243..12b73c3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.3.1 \ No newline at end of file +8.7 diff --git a/api/v1/class/arbeitszeit.inc.php b/api/v1/class/arbeitszeit.inc.php index 8222d10..73477bb 100644 --- a/api/v1/class/arbeitszeit.inc.php +++ b/api/v1/class/arbeitszeit.inc.php @@ -15,6 +15,7 @@ use Arbeitszeit\Nodes; use Arbeitszeit\Projects; use Arbeitszeit\StatusMessages; + use Arbeitszeit\Telemetry; use Arbeitszeit\Events\EventDispatcherService; use Arbeitszeit\Events\EasymodeWorktimeAddedEvent; // "EasymodeWorktimeSTARTED" Event, actually. use Arbeitszeit\Events\EasymodeWorktimeEndedEvent; @@ -48,6 +49,7 @@ class Arbeitszeit private $mails; private $nodes; private $statusMessages; + private $telemetry; private $projects; @@ -58,6 +60,7 @@ public function __construct() if (isset($this->get_app_ini()["general"]["timezone"])) { try { date_default_timezone_set($this->get_app_ini()["general"]["timezone"]); + $this->app_ini_check(); } catch (\Exception $e) { Exceptions::error_rep("Error setting timezone: " . $e->getMessage()); } @@ -71,6 +74,42 @@ public function __destruct() } } + public function app_ini_check() + { + $base = dirname(__DIR__, 3) . "/api/v1/inc/"; + $sample = json_decode(file_get_contents($base . "app.json.sample"), true); + $current = json_decode(file_get_contents($base . "app.json"), true); + + $updated = false; + + foreach ($sample as $section => $values) { + + if (!isset($current[$section]) || !is_array($current[$section])) { + $current[$section] = []; + $updated = true; + Exceptions::error_rep("App config section '{$section}' was missing and has been created."); + } + + foreach ($values as $key => $value) { + if (!array_key_exists($key, $current[$section])) { + $current[$section][$key] = $value; + $updated = true; + Exceptions::error_rep( + "App config key '{$key}' in section '{$section}' was missing and added with default value." + ); + } + } + } + + if ($updated) { + file_put_contents( + $base . "app.json", + json_encode($current, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) + ); + } + } + + public function init_lang() { Exceptions::error_rep("Initializing language for Arbeitszeit class"); @@ -126,8 +165,8 @@ public static function add_easymode_worktime($username) return false; } else { Exceptions::error_rep("Creating easymode worktime entry for user '{$username}'..."); - $sql = "INSERT INTO `arbeitszeiten` (`name`, `id`, `email`, `username`, `schicht_tag`, `schicht_anfang`, `schicht_ende`, `ort`, `active`, `review`) VALUES ( ?, '0', ?, ?, ?, ?, '00:00', '-', '1', '0');"; - $data = $conn->sendQuery($sql)->execute([$usr["name"], $usr["email"], $username, $date, $time]); + $sql = "INSERT INTO `arbeitszeiten` (`name`, `id`, `email`, `username`, `schicht_tag`, `schicht_anfang`, `schicht_ende`, `ort`, `active`, `review`, `wtype`) VALUES ( ?, '0', ?, ?, ?, ?, '00:00', '-', '1', '0', ?);"; + $data = $conn->sendQuery($sql)->execute([$usr["name"], $usr["email"], $username, $date, $time, Arbeitszeit::get_app_ini()["config"]["default_worktime_type"]]); if ($data == false) { Exceptions::error_rep("An error occurred while creating easymode worktime entry. See previous message for more information"); return false; @@ -221,6 +260,35 @@ public function end_easymode_pause_worktime($username, $id) } } + public function renderUserWorktimeSelect( + string $name, + int $userId, + ?int $selectedWorktime = null, + string $placeholder = "—", + string $class = "" + ): void { + $userId = $this->benutzer()->get_user_from_id($userId)["username"]; + $sql = "SELECT * FROM arbeitszeiten WHERE username = ?"; + $stmt = $this->db->sendQuery($sql); + $stmt->execute([$userId]); + $times = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + echo ''; + } + + public function toggle_easymode($username) { if (!$this->nodes()->checkNode("arbeitszeit.inc", "toggle_easymode")) { @@ -372,7 +440,7 @@ public function add_worktime($start, $end, $location, $date, $username, $type, $ public function update_worktime($id, $array) { - if(!$this->check_if_for_review($id)){ + if (!$this->check_if_for_review($id)) { return false; } $allowed = [ @@ -807,6 +875,92 @@ public function blockIfNotAdmin() return false; } + public function checkForUpdate() + { + $currentVersion = $this->getTimeTrackVersion(); + $latestVersion = file_get_contents("https://raw.githubusercontent.com/Ente/timetrack/refs/heads/develop/VERSION"); + + $currentVersion = trim($currentVersion); + $latestVersion = trim($latestVersion); + + if (version_compare($currentVersion, $latestVersion, '<')) { + return $latestVersion; + } else { + return false; + } + } + + public function getChanges(string $version_tag = "latest") + { + if ($version_tag !== "latest") { + $url = "https://api.github.com/repos/ente/timetrack/releases/tags/v{$version_tag}"; + } else { + $url = "https://api.github.com/repos/ente/timetrack/releases/latest"; + } + + $context = stream_context_create([ + "http" => [ + "method" => "GET", + "header" => [ + "User-Agent: TimeTrack-Updater", + "Accept: application/vnd.github+json" + ], + "timeout" => 10 + ] + ]); + + $json = @file_get_contents($url, false, $context); + + if ($json === false) { + return null; + #throw new \RuntimeException("GitHub API request failed"); + } + + return json_decode($json, true); + } + + + public function renderGUIUpdateCheck() + { + + $text = ""; + $current = trim($this->getTimeTrackVersion()); + $latest = trim($this->checkForUpdate()); + + + if ($this->checkForUpdate() != false) { + $latestChanges = $this->getChanges($latest) ?? "NULL"; + $fullChangelogUrl = "https://github.com/ente/timetrack/compare/v{$current}...v{$latest}"; + $latestVersionLink = "https://github.com/ente/timetrack/releases/tag/v{$latest}"; + $text .= "
"; + $text .= "

Update available!


"; + $text .= "You are currently using TimeTrack version {$current}, the latest version is {$latest}.
"; + $text .= "Please check the changelog for more information about the changes.
"; + $text .= "It is recommended to update as soon as possible to benefit from the latest features and security improvements."; + ## changelog + parsedown + $text .= "
"; + $text .= "Changelog for version {$latest}:
"; + $parsedown = new \Parsedown(); + $text .= $parsedown->text($latestChanges["body"]); + $text .= "
"; + return $text; + } else { + $text .= "
"; + $text .= "You are using the latest version of TimeTrack ({$current}). No update is required."; + ## current changelog + $text .= "
"; + $text .= "Changelog for version {$current}:
"; + $parsedown = new \Parsedown(); + $currentChanges = $this->getChanges($current); + if ($currentChanges == null) { + $currentChanges["body"] = "**No changelogs found. Either you are using a custom build or something went wrong while fetching the changelogs.**"; + } + $text .= $parsedown->text($currentChanges["body"]); + $text .= "
"; + return $text; + } + } + public function global_dispatcher(): \Symfony\Component\EventDispatcher\EventDispatcher { return \Arbeitszeit\Events\EventDispatcherService::get(); @@ -919,9 +1073,12 @@ public function projects(): Projects $this->projects = new Projects; return $this->projects; } + + public function telemetry(): Telemetry + { + if (!$this->telemetry) + $this->telemetry = new Telemetry; + return $this->telemetry; + } } } - - - -?> \ No newline at end of file diff --git a/api/v1/class/benutzer/benutzer.arbeit.inc.php b/api/v1/class/benutzer/benutzer.arbeit.inc.php index 36b62eb..d7dd5df 100644 --- a/api/v1/class/benutzer/benutzer.arbeit.inc.php +++ b/api/v1/class/benutzer/benutzer.arbeit.inc.php @@ -25,7 +25,7 @@ public function __construct() */ public function create_user($username, $name, $email, $password, $isAdmin = 0) { - if($this->nodes()->checkNode("benutzer.inc", "create_user") == false){ + if ($this->nodes()->checkNode("benutzer.inc", "create_user") == false) { return false; } Exceptions::error_rep("Creating user '$username'..."); @@ -47,20 +47,44 @@ public function create_user($username, $name, $email, $password, $isAdmin = 0) } } - public function user_active($username){ + public function renderUserSelect(string $name, ?int $selectedUser = null, string $noAssigneeText = "—"): void + { + $users = $this->get_all_users(); + + echo ''; + } + + + public function user_active($username) + { $user = $this->get_user($username); - if($user["active"] == true || $user["active"] == 1){ + if ($user["active"] == true || $user["active"] == 1) { return true; } else { return false; } } - public function activate_user($username){ + public function activate_user($username) + { return $this->editUserProperties($username, "active", 1); } - public function deactivate_user($username){ + public function deactivate_user($username) + { return $this->editUserProperties($username, "active", 0); } @@ -74,10 +98,10 @@ public function deactivate_user($username){ */ public function delete_user($id) { - if($this->nodes()->checkNode("benutzer.inc", "delete_user") == false){ + if ($this->nodes()->checkNode("benutzer.inc", "delete_user") == false) { return false; } - $user = $this->get_user_from_id($id); + $user = $this->get_user_from_id($id); $username = $user["username"]; $email = $user["email"]; Exceptions::error_rep("Deleting user with id '$id'..."); @@ -217,7 +241,7 @@ public function get_all_users() */ public function get_all_users_html() { - if($this->nodes()->checkNode("benutzer.inc", "get_all_users_html") == false){ + if ($this->nodes()->checkNode("benutzer.inc", "get_all_users_html") == false) { return false; } Exceptions::error_rep("Getting all users..."); @@ -264,7 +288,7 @@ public function get_all_users_html() */ public function get_user_html($username) { - if($this->nodes()->checkNode("benutzer.inc", "get_user_html") == false){ + if ($this->nodes()->checkNode("benutzer.inc", "get_user_html") == false) { return false; } Exceptions::error_rep("Getting user '$username'..."); @@ -314,25 +338,28 @@ public static function is_admin($user) } } - public static function current_user_is_admin(){ - if(self::get_current_user()["isAdmin"] == true){ + public static function current_user_is_admin() + { + if (self::get_current_user()["isAdmin"] == true) { return true; } else { return false; } } - public static function get_current_user(){ + public static function get_current_user() + { return self::get_user($_SESSION["username"]); } - public static function get_name_from_id($id){ + public static function get_name_from_id($id) + { return self::get_user_from_id($id)["name"]; } public function editUserProperties(mixed $username_or_id, string $name, mixed $value): bool { - if($this->nodes()->checkNode("benutzer.inc", "editUserProperties") == false){ + if ($this->nodes()->checkNode("benutzer.inc", "editUserProperties") == false) { return false; } if ( @@ -378,28 +405,33 @@ public function editUserProperties(mixed $username_or_id, string $name, mixed $v } } - public function loadUserTheme(){ + public function loadUserTheme() + { $themes = scandir($_SERVER["DOCUMENT_ROOT"] . "/assets/css"); $themes = array_diff($themes, [".", ".."]); + if(!isset($_COOKIE["theme"])){ + return "/assets/css/v8.css"; + } $check = in_array($_COOKIE["theme"], $themes); - if($this->get_app_ini()["general"]["force_theme"] == "true"){ + if ($this->get_app_ini()["general"]["force_theme"] == "true") { return $this->get_app_ini()["general"]["theme_file"]; } - if(!isset($_COOKIE["theme"]) || !$check){ + if (!$check) { return "/assets/css/v8.css"; } else { return "/assets/css/" . $_COOKIE["theme"]; } } - public function computeUserThemes(){ + public function computeUserThemes() + { $themes = scandir($_SERVER["DOCUMENT_ROOT"] . "/assets/css"); $themes = array_diff($themes, [".", ".."]); $currentTheme = basename($this->loadUserTheme()); - foreach($themes as $theme){ - if($currentTheme == $theme){ + foreach ($themes as $theme) { + if ($currentTheme == $theme) { echo ""; } else { echo ""; @@ -409,13 +441,15 @@ public function computeUserThemes(){ return true; } - public function setUserTheme($theme){ - setcookie("theme", $theme, time()+60*60*24*30, "/"); + public function setUserTheme($theme) + { + setcookie("theme", $theme, time() + 60 * 60 * 24 * 30, "/"); return true; } - public function checkThemeForce(){ - if($this->get_app_ini()["general"]["force_theme"] == "true" || $this->get_app_ini()["general"]["force_theme"] == true){ + public function checkThemeForce() + { + if ($this->get_app_ini()["general"]["force_theme"] == true) { return true; } else { return false; diff --git a/api/v1/class/exports/modules/PDFExportModule/PDFExportModule.em.arbeit.inc.php b/api/v1/class/exports/modules/PDFExportModule/PDFExportModule.em.arbeit.inc.php index 2fe1675..3de0d85 100644 --- a/api/v1/class/exports/modules/PDFExportModule/PDFExportModule.em.arbeit.inc.php +++ b/api/v1/class/exports/modules/PDFExportModule/PDFExportModule.em.arbeit.inc.php @@ -8,36 +8,54 @@ /** * PDFExportModule - Allows you to export worktime sheets */ -class PDFExportModule implements ExportModuleInterface { - public function export($args) { - $i18n = new i18n; - $arbeit = new Arbeitszeit; - $user = $args["user"]; - $month = $args["month"]; - $year = $args["year"]; - $i18nn = $i18n->loadLanguage(null, "class/pdf"); - $ini = Arbeitszeit::get_app_ini(); - $hours = $arbeit->calculate_hours_specific_time($user, $month, $year); - if(is_string($year) != true){ - $year = date("Y"); - } - $sql = "SELECT * FROM `arbeitszeiten` WHERE YEAR(schicht_tag) = ? AND MONTH(schicht_tag) = ? AND username = ? ORDER BY schicht_tag DESC;"; - $statement = $arbeit->db()->sendQuery($sql); - $userdata = $statement->execute([$year, $month, $user]); - if($userdata == false){ - Exceptions::error_rep("An error occurred while generating worktime pdf. See previous message for more information"); - die("An error occurred!"); - } - $user_data = Benutzer::get_user($user); - $user_data["name"] ?? $statement->fetch(\PDO::FETCH_ASSOC)[0]["name"]; # Bug 14 Fix -> http://bugzilla.openducks.org/show_bug.cgi?id=14 - try { - $css = @file_get_contents($ini["exports"]["pdf"]["css"]); - } catch (\ValueError $e) { - $css = file_get_contents(__DIR__ . "/css/index.css"); - } - $data = <<< DATA +class PDFExportModule implements ExportModuleInterface +{ + + function loadUserContent(string $file): string + { + if (!is_file($file)) { + return ''; + } + + ob_start(); + include $file; + return ob_get_clean(); + } + + + public function export($args) + { + $i18n = new i18n; + $arbeit = new Arbeitszeit; + $user = $args["user"]; + $month = $args["month"]; + $year = $args["year"]; + $i18nn = $i18n->loadLanguage(null, "class/pdf"); + $ini = Arbeitszeit::get_app_ini(); + $hours = $arbeit->calculate_hours_specific_time($user, $month, $year); + if (is_string($year) != true) { + $year = date("Y"); + } + $userGeneratedBeginning = $this->loadUserContent(__DIR__ . '/php/user_content_beginning.php') ?? ""; + $userGeneratedEnding = $this->loadUserContent(__DIR__ . '/php/user_content_ending.php') ?? ""; + $sql = "SELECT * FROM `arbeitszeiten` WHERE YEAR(schicht_tag) = ? AND MONTH(schicht_tag) = ? AND username = ? ORDER BY schicht_tag DESC;"; + $statement = $arbeit->db()->sendQuery($sql); + $userdata = $statement->execute([$year, $month, $user]); + if ($userdata == false) { + Exceptions::error_rep("An error occurred while generating worktime pdf. See previous message for more information"); + die("An error occurred!"); + } + $user_data = Benutzer::get_user($user); + $user_data["name"] ?? $statement->fetch(\PDO::FETCH_ASSOC)[0]["name"]; # Bug 14 Fix -> http://bugzilla.openducks.org/show_bug.cgi?id=14 + try { + $css = @file_get_contents($ini["exports"]["pdf"]["css"]); + } catch (\ValueError $e) { + $css = file_get_contents(__DIR__ . "/css/index.css"); + } + $data = << + {$userGeneratedBeginning}

{$i18nn["worktime_note"]} {$user_data["name"]}