From b7764da51f7585bf218799eb54f9ae273c8eaa35 Mon Sep 17 00:00:00 2001
From: geek95dg <52536778+geek95dg@users.noreply.github.com>
Date: Wed, 1 Apr 2026 11:24:34 +0200
Subject: [PATCH 01/14] Add files via upload
---
GLPI_Plugin_Development_Guide.md | 2194 ++++++++++++++++++++++++++++++
1 file changed, 2194 insertions(+)
create mode 100644 GLPI_Plugin_Development_Guide.md
diff --git a/GLPI_Plugin_Development_Guide.md b/GLPI_Plugin_Development_Guide.md
new file mode 100644
index 0000000000..6673494ab3
--- /dev/null
+++ b/GLPI_Plugin_Development_Guide.md
@@ -0,0 +1,2194 @@
+# GLPI 11.x Plugin Development Guide
+
+> Practical, consolidated reference for building GLPI 11.x plugins.
+> Every pattern, gotcha, and rule here comes from real production experience — not theory.
+> Written for AI assistants and developers alike. Update this document as new patterns are discovered.
+
+**Target:** GLPI 11.0–11.99, PHP 8.1+, MySQL 5.7+ / MariaDB 10.3+
+
+---
+
+## Table of Contents
+
+1. [Plugin Structure](#1-plugin-structure)
+2. [setup.php — Registration & Hooks](#2-setupphp--registration--hooks)
+3. [hook.php — Database Schema & Migration](#3-hookphp--database-schema--migration)
+4. [Namespaces & Autoloading](#4-namespaces--autoloading)
+5. [Core Base Classes](#5-core-base-classes)
+6. [Dropdowns — CommonDropdown](#6-dropdowns--commondropdown)
+7. [Forms — showForm() Patterns](#7-forms--showform-patterns)
+8. [Search Options — rawSearchOptions()](#8-search-options--rawsearchoptions)
+9. [Tabs System](#9-tabs-system)
+10. [Front Controllers](#10-front-controllers)
+11. [AJAX Endpoints](#11-ajax-endpoints)
+12. [Database Access](#12-database-access)
+13. [Relations — CommonDBRelation](#13-relations--commondbrelation)
+14. [Configuration Pages](#14-configuration-pages)
+15. [GLPI Group Hierarchy](#15-glpi-group-hierarchy)
+16. [GLPI Hook System](#16-glpi-hook-system)
+17. [Permissions & Rights](#17-permissions--rights)
+18. [Sessions & Authentication](#18-sessions--authentication)
+19. [Translations (i18n)](#19-translations-i18n)
+20. [Frontend (JS/CSS)](#20-frontend-jscss)
+21. [File & Document Handling](#21-file--document-handling)
+22. [PDF Generation](#22-pdf-generation)
+23. [Notifications](#23-notifications)
+24. [Logging](#24-logging)
+25. [Security Checklist](#25-security-checklist)
+26. [What Does NOT Work / Forbidden Patterns](#26-what-does-not-work--forbidden-patterns)
+27. [Common Gotchas & Known Pitfalls](#27-common-gotchas--known-pitfalls)
+28. [Useful Constants & Paths](#28-useful-constants--paths)
+29. [Checklists](#29-checklists)
+
+---
+
+## 1. Plugin Structure
+
+GLPI 11.x supports **two class autoloading schemes**. Both work; modern PSR-4 (`src/`) is recommended for new plugins.
+
+### Modern layout (PSR-4 — recommended)
+
+```
+myplugin/ # lowercase, no hyphens, no underscores
+├── setup.php # REQUIRED — plugin registration, hooks, version
+├── hook.php # REQUIRED — install / upgrade / uninstall
+├── src/ # PSR-4 classes (GLPI autoloads from here)
+│ ├── MyItem.php # extends CommonDBTM
+│ ├── MyItem_User.php # extends CommonDBRelation
+│ ├── MyCategory.php # extends CommonDropdown
+│ └── MyTab.php # extends CommonGLPI (tab-only, no DB table)
+├── front/ # User-facing pages (routing entry points)
+│ ├── myitem.php # list view (Search::show)
+│ ├── myitem.form.php # form view (create/edit + POST handlers)
+│ ├── myitem_user.form.php # relation POST handler
+│ └── config.form.php # plugin configuration page
+├── ajax/ # AJAX endpoints
+│ └── endpoint.php
+├── public/ # Static assets
+│ ├── css/
+│ └── js/
+└── locales/ # Gettext .po translation files
+ ├── en_GB.po
+ └── pl_PL.po
+```
+
+### Legacy layout (inc/ autoloader)
+
+```
+myplugin/
+├── setup.php
+├── hook.php
+├── inc/ # Classes auto-discovered by filename convention
+│ └── feature.class.php # Class: PluginMypluginFeature
+├── front/
+├── ajax/
+├── public/
+└── locales/
+```
+
+**No build tools.** GLPI plugins use plain PHP, CSS, and JS. No npm, no Composer, no webpack. Edit files directly.
+
+### Naming Conventions
+
+| Thing | Convention | Example |
+|-------|-----------|---------|
+| Plugin directory | lowercase alpha only | `myplugin` |
+| DB tables | `glpi_plugin_{name}_{tablename}` | `glpi_plugin_myplugin_myitems` |
+| Relation tables | `glpi_plugin_{name}_{item1s}_{item2s}` | `glpi_plugin_myplugin_myitems_users` |
+| Rights key | `plugin_{name}_{rightname}` | `plugin_myplugin_myitem` |
+| PHP namespace (PSR-4) | `GlpiPlugin\{Pluginname}` | `GlpiPlugin\Myplugin` |
+| Legacy class name | `Plugin{Name}{Class}` | `PluginMypluginFeature` |
+| Legacy file name | `inc/{class}.class.php` | `inc/feature.class.php` |
+| Constants | `PLUGIN_{NAME}_VERSION` | `PLUGIN_MYPLUGIN_VERSION` |
+| Functions | `plugin_{name}_xxx()` | `plugin_myplugin_install()` |
+
+---
+
+## 2. setup.php — Registration & Hooks
+
+This file is loaded on **every GLPI page load** when the plugin is active. Keep it lightweight.
+
+### Required Functions
+
+```php
+ 'My Plugin',
+ 'version' => PLUGIN_MYPLUGIN_VERSION,
+ 'author' => 'Author Name',
+ 'license' => 'GPLv3',
+ 'homepage' => 'https://example.com',
+ 'requirements' => [
+ 'glpi' => ['min' => '11.0', 'max' => '11.99'],
+ 'php' => ['min' => '8.1'],
+ ],
+ ];
+}
+
+function plugin_myplugin_check_prerequisites(): bool {
+ return true; // Add version checks if needed
+}
+
+function plugin_myplugin_check_config($verbose = false): bool {
+ return true; // Validate config state
+}
+```
+
+### plugin_init Function
+
+```php
+function plugin_init_myplugin(): void {
+ global $PLUGIN_HOOKS;
+
+ // MANDATORY — plugin won't load without this
+ $PLUGIN_HOOKS['csrf_compliant']['myplugin'] = true;
+
+ // Register classes
+ Plugin::registerClass(MyItem::class);
+ Plugin::registerClass(MyCategory::class);
+
+ // Inject a tab on User detail pages
+ Plugin::registerClass(MyItem_User::class, [
+ 'addtabon' => ['User'],
+ ]);
+
+ // Menu entry (appears under Assets, Management, Tools, Admin, or Config)
+ if (MyItem::canView()) {
+ $PLUGIN_HOOKS['menu_toadd']['myplugin'] = [
+ 'assets' => MyItem::class, // or 'management', 'tools', 'admin'
+ ];
+ }
+
+ // Register dropdowns in Setup > Dropdowns
+ $PLUGIN_HOOKS['plugin_dropdowns']['myplugin'] = [
+ MyCategory::class,
+ ];
+
+ // Config page (adds "Configure" link on Setup > Plugins)
+ $PLUGIN_HOOKS['config_page']['myplugin'] = 'front/config.form.php';
+
+ // CSS/JS — always append version to bust cache
+ $v = PLUGIN_MYPLUGIN_VERSION;
+ $PLUGIN_HOOKS['add_css']['myplugin'] = ["public/css/myplugin.css?v={$v}"];
+ $PLUGIN_HOOKS['add_javascript']['myplugin'] = ["public/js/myplugin.js?v={$v}"];
+
+ // Hook registrations (see Section 16 for full details)
+ $PLUGIN_HOOKS['item_update']['myplugin'] = [
+ 'Ticket' => 'plugin_myplugin_ticket_update',
+ ];
+
+ // IMPORTANT: Wrap permission checks in try-catch.
+ // Tables don't exist during install — any DB query will throw.
+ if (Session::getLoginUserID()) {
+ try {
+ // plugin-specific permission check
+ } catch (\Throwable $e) {
+ // Silently fail during install/upgrade
+ }
+ }
+}
+```
+
+### Key Rules
+
+- `csrf_compliant` **must** be set to `true` or GLPI blocks all POST requests.
+- Always append `?v=VERSION` to CSS/JS includes to prevent browser caching stale files.
+- Wrap **all** DB-dependent code in `try-catch` — `plugin_init` runs during install when tables don't exist yet.
+- Keep `plugin_init` fast — it runs on every page load.
+- `Plugin::registerClass()` with `'addtabon'` is how you inject tabs into other item types. Tab-only classes (extending `CommonGLPI`, no DB table) typically only need registration if they inject tabs into **other** item types. For tabs on your own item type, `addStandardTab()` in `defineTabs()` is sufficient.
+- Menu categories: `'assets'`, `'management'`, `'tools'`, `'admin'`, `'config'`.
+
+---
+
+## 3. hook.php — Database Schema & Migration
+
+### Install
+
+```php
+tableExists('glpi_plugin_myplugin_myitems')) {
+ $DB->doQuery("CREATE TABLE `glpi_plugin_myplugin_myitems` (
+ `id` INT unsigned NOT NULL AUTO_INCREMENT,
+ `name` VARCHAR(255) NOT NULL DEFAULT '',
+ `comment` TEXT,
+ `status` VARCHAR(50) NOT NULL DEFAULT 'active',
+ `entities_id` INT unsigned NOT NULL DEFAULT 0,
+ `is_recursive` TINYINT NOT NULL DEFAULT 0,
+ `is_deleted` TINYINT NOT NULL DEFAULT 0,
+ `users_id` INT unsigned NOT NULL DEFAULT 0,
+ `date_mod` TIMESTAMP NULL DEFAULT NULL,
+ `date_creation` TIMESTAMP NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `name` (`name`),
+ KEY `entities_id` (`entities_id`),
+ KEY `is_deleted` (`is_deleted`),
+ KEY `users_id` (`users_id`),
+ KEY `status` (`status`),
+ KEY `date_mod` (`date_mod`),
+ KEY `date_creation` (`date_creation`)
+ ) ENGINE=InnoDB DEFAULT CHARSET={$charset}
+ COLLATE={$collation} ROW_FORMAT=DYNAMIC");
+ }
+
+ // Insert default config values (if using a config table)
+ if ($DB->tableExists('glpi_plugin_myplugin_configs')) {
+ $defaults = ['feature_enabled' => '1'];
+ foreach ($defaults as $key => $value) {
+ $existing = $DB->request([
+ 'FROM' => 'glpi_plugin_myplugin_configs',
+ 'WHERE' => ['config_key' => $key],
+ 'LIMIT' => 1,
+ ]);
+ if (count($existing) === 0) {
+ $DB->insert('glpi_plugin_myplugin_configs', [
+ 'config_key' => $key,
+ 'value' => $value,
+ ]);
+ }
+ }
+ }
+
+ // Provision rights via GLPI's native rights system
+ ProfileRight::addProfileRights([MyItem::$rightname]);
+
+ // Grant Super-Admin full rights
+ $profile = new Profile();
+ foreach ($profile->find(['interface' => 'central']) as $data) {
+ if ($data['name'] === 'Super-Admin') {
+ $profileRight = new ProfileRight();
+ $profileRight->updateProfileRights($data['id'], [
+ MyItem::$rightname => ALLSTANDARDRIGHT,
+ ]);
+ }
+ }
+
+ $migration->executeMigration();
+ return true;
+}
+```
+
+### Upgrade
+
+```php
+function plugin_myplugin_upgrade(string $fromVersion): bool {
+ global $DB;
+
+ $migration = new Migration(PLUGIN_MYPLUGIN_VERSION);
+
+ // Guard each migration by checking actual DB state — never compare version strings
+ if (!$DB->fieldExists('glpi_plugin_myplugin_myitems', 'new_column')) {
+ $migration->addField(
+ 'glpi_plugin_myplugin_myitems',
+ 'new_column',
+ 'string', // GLPI type: 'integer', 'string', 'text', 'bool', etc.
+ ['value' => '']
+ );
+ $migration->addKey('glpi_plugin_myplugin_myitems', 'new_column');
+ }
+
+ // For new config keys
+ if ($DB->tableExists('glpi_plugin_myplugin_configs')) {
+ $newConfigs = ['new_setting' => 'default'];
+ foreach ($newConfigs as $key => $value) {
+ $exists = $DB->request([
+ 'FROM' => 'glpi_plugin_myplugin_configs',
+ 'WHERE' => ['config_key' => $key],
+ 'LIMIT' => 1,
+ ]);
+ if (count($exists) === 0) {
+ $DB->insert('glpi_plugin_myplugin_configs', [
+ 'config_key' => $key,
+ 'value' => $value,
+ ]);
+ }
+ }
+ }
+
+ $migration->executeMigration();
+ return true;
+}
+```
+
+### Uninstall
+
+```php
+function plugin_myplugin_uninstall(): bool {
+ global $DB;
+
+ // Drop in reverse dependency order (children first)
+ $tables = [
+ 'glpi_plugin_myplugin_myitems_users',
+ 'glpi_plugin_myplugin_myitems',
+ 'glpi_plugin_myplugin_configs',
+ ];
+
+ foreach ($tables as $table) {
+ if ($DB->tableExists($table)) {
+ $DB->doQuery("DROP TABLE `{$table}`");
+ }
+ }
+
+ // Clean up rights
+ ProfileRight::deleteProfileRights([MyItem::$rightname]);
+
+ return true;
+}
+```
+
+### Migration Field Types
+
+| GLPI Type | SQL Result |
+|-----------|-----------|
+| `'integer'` | `INT unsigned NOT NULL DEFAULT {value}` |
+| `'string'` | `VARCHAR(255)` |
+| `'text'` | `TEXT` |
+| `'bool'` | `TINYINT NOT NULL DEFAULT {value}` |
+| `'decimal(20,4)'` | `DECIMAL(20,4)` |
+
+### Required Columns by Feature
+
+| Feature | Required Columns |
+|---------|-----------------|
+| Entity support | `entities_id` INT unsigned, `is_recursive` TINYINT |
+| Soft delete / Trash | `is_deleted` TINYINT |
+| History / Log tab | `date_mod` TIMESTAMP |
+| Timestamps | `date_mod` TIMESTAMP, `date_creation` TIMESTAMP |
+
+### Key Rules
+
+- Always use `IF NOT EXISTS` / `$DB->tableExists()` / `$DB->fieldExists()` — install and upgrade must be **idempotent** (safe to run multiple times).
+- Use `DBConnection::getDefaultCharset()` / `getDefaultCollation()` instead of hardcoding charset.
+- Never use version string comparisons for migrations — check the actual DB state.
+- Add indexes on foreign key columns and frequently filtered columns.
+- Call `$migration->executeMigration()` at the end of install and upgrade.
+- Provision rights with `ProfileRight::addProfileRights()` during install.
+- Clean up rights with `ProfileRight::deleteProfileRights()` during uninstall.
+- Drop tables in reverse dependency order during uninstall (children first).
+- Bump the version constant whenever the schema changes, or migration code won't run.
+
+---
+
+## 4. Namespaces & Autoloading
+
+GLPI 11.x supports two autoloading mechanisms. **Both work simultaneously**, but PSR-4 is recommended for new plugins.
+
+### PSR-4 (modern — recommended)
+
+Classes live in `src/` under the `GlpiPlugin\{Pluginname}` namespace:
+
+| Class | File |
+|-------|------|
+| `GlpiPlugin\Myplugin\MyItem` | `src/MyItem.php` |
+| `GlpiPlugin\Myplugin\MyItem_User` | `src/MyItem_User.php` |
+| `GlpiPlugin\Myplugin\MyCategory` | `src/MyCategory.php` |
+
+```php
+ self::getMenuName(),
+ 'page' => '/plugins/myplugin/front/myitem.php',
+ 'icon' => 'fas fa-box',
+ ];
+ if (Session::haveRight('config', READ)) {
+ $menu['options']['config'] = [
+ 'title' => __('Configuration', 'myplugin'),
+ 'page' => '/plugins/myplugin/front/config.form.php',
+ 'icon' => 'fas fa-cog',
+ ];
+ }
+ return $menu;
+ }
+}
+```
+
+### Adding a Tab to an Existing GLPI Item
+
+Any class can provide tabs on other GLPI items. Register it in `setup.php`:
+
+```php
+Plugin::registerClass(MyItem_User::class, ['addtabon' => ['User']]);
+```
+
+Then implement the tab interface:
+
+```php
+public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) {
+ if ($item instanceof \User) {
+ $count = countElementsInTable(self::getTable(), [
+ 'users_id' => $item->getID(),
+ ]);
+ return self::createTabEntry(__('My Items', 'myplugin'), $count);
+ }
+ return '';
+}
+
+static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) {
+ if ($item instanceof \User) {
+ self::showForUser($item);
+ return true;
+ }
+ return false;
+}
+```
+
+---
+
+## 6. Dropdowns — CommonDropdown
+
+For simple categorization / lookup lists:
+
+```php
+namespace GlpiPlugin\Myplugin;
+
+use CommonDropdown;
+
+class MyCategory extends CommonDropdown {
+ public $can_be_recursive = true;
+
+ static function getTypeName($nb = 0) {
+ return _n('My Category', 'My Categories', $nb, 'myplugin');
+ }
+}
+```
+
+Register in `setup.php`:
+
+```php
+Plugin::registerClass(MyCategory::class);
+$PLUGIN_HOOKS['plugin_dropdowns']['myplugin'] = [MyCategory::class];
+```
+
+Table name auto-resolves to `glpi_plugin_myplugin_mycategories`.
+
+For **hierarchical / tree dropdowns**, extend `CommonTreeDropdown` instead — this adds parent-child relationships with `completename` (full path like `Root > Child > Grandchild`).
+
+---
+
+## 7. Forms — showForm() Patterns
+
+GLPI supports two form rendering approaches. **Legacy table-based is recommended for plugins** — simpler, well-tested, and stable across GLPI versions.
+
+### Basic showForm() Structure
+
+```php
+function showForm($ID, array $options = []) {
+ $this->initForm($ID, $options); // loads $this->fields from DB
+ $this->showFormHeader($options); // opens
+ return true;
+}
+```
+
+**CRITICAL**: `initForm()` must be called before accessing `$this->fields`. `showFormButtons()` must close the form — without it, no Save/Add button is rendered.
+
+### Form Field Types
+
+```php
+// Text input
+echo Html::input('fieldname', ['value' => $this->fields['fieldname'] ?? '', 'size' => 40]);
+
+// Textarea
+echo "";
+
+// GLPI dropdown (FK to standard GLPI type)
+Dropdown::show(Manufacturer::class, [
+ 'name' => 'manufacturers_id',
+ 'value' => $this->fields['manufacturers_id'] ?? 0,
+ 'entity' => $this->getEntityID(),
+]);
+
+// Plugin dropdown (FK to plugin's own dropdown type)
+Dropdown::show(MyCategory::class, [
+ 'name' => 'plugin_myplugin_mycategories_id',
+ 'value' => $this->fields['plugin_myplugin_mycategories_id'] ?? 0,
+ 'entity' => $this->getEntityID(),
+]);
+
+// User dropdown
+User::dropdown([
+ 'name' => 'users_id_owner',
+ 'value' => $this->fields['users_id_owner'] ?? 0,
+ 'right' => 'all',
+ 'entity' => $this->getEntityID(),
+]);
+
+// Group dropdown
+Group::dropdown([
+ 'name' => 'groups_id_department',
+ 'value' => $this->fields['groups_id_department'] ?? 0,
+ 'entity' => $this->getEntityID(),
+]);
+
+// Yes/No dropdown
+Dropdown::showYesNo('is_paid', $this->fields['is_paid'] ?? 0);
+
+// Static array dropdown
+Dropdown::showFromArray('is_external', [
+ 0 => __('Internal'),
+ 1 => __('External'),
+], ['value' => $this->fields['is_external'] ?? 0]);
+
+// Number input
+echo Html::input('price', [
+ 'id' => 'price',
+ 'value' => $this->fields['price'] ?? '',
+ 'type' => 'number',
+ 'step' => '0.01',
+ 'min' => '0',
+]);
+```
+
+### Inline JavaScript in Forms
+
+```php
+echo Html::scriptBlock("
+ function myPluginToggle(val) {
+ document.getElementById('my_element').style.display = val == 1 ? '' : 'none';
+ }
+");
+```
+
+**WARNING about `on_change` with Select2**: GLPI renders most dropdowns using Select2. The `on_change` parameter works with `Dropdown::showFromArray()` and `Dropdown::showYesNo()`, but the event fires with Select2's value. Use `'on_change' => 'myFunction(this.value)'`.
+
+### Legend / Info Row on Forms
+
+```php
+echo "";
+echo "| ";
+echo " ";
+echo __('Your descriptive text here', 'myplugin');
+echo " | ";
+echo "
";
+```
+
+---
+
+## 8. Search Options — rawSearchOptions()
+
+Search options define columns visible in list views, searchable fields, and export data.
+
+```php
+function rawSearchOptions() {
+ $tab = [];
+
+ // Section header (required as first entry)
+ $tab[] = [
+ 'id' => 'common',
+ 'name' => self::getTypeName(2),
+ ];
+
+ // Name (clickable link to item)
+ $tab[] = [
+ 'id' => 1,
+ 'table' => self::getTable(),
+ 'field' => 'name',
+ 'name' => __('Name'),
+ 'datatype' => 'itemlink', // makes it a clickable link
+ 'searchtype' => ['contains'],
+ 'massiveaction' => false,
+ ];
+
+ // Simple text field
+ $tab[] = [
+ 'id' => 3,
+ 'table' => self::getTable(),
+ 'field' => 'comment',
+ 'name' => __('Comments'),
+ 'datatype' => 'text',
+ ];
+
+ // Boolean field
+ $tab[] = [
+ 'id' => 5,
+ 'table' => self::getTable(),
+ 'field' => 'is_recursive',
+ 'name' => __('Child entities'),
+ 'datatype' => 'bool',
+ ];
+
+ // Entity (standard pattern — GLPI auto-joins)
+ $tab[] = [
+ 'id' => 4,
+ 'table' => 'glpi_entities',
+ 'field' => 'completename',
+ 'name' => Entity::getTypeName(1),
+ 'datatype' => 'dropdown',
+ ];
+
+ // FK to standard GLPI type (standard column name like manufacturers_id)
+ $tab[] = [
+ 'id' => 9,
+ 'table' => 'glpi_manufacturers',
+ 'field' => 'name',
+ 'name' => Manufacturer::getTypeName(1),
+ 'datatype' => 'dropdown',
+ ];
+
+ // FK with NON-STANDARD column name (e.g., users_id_owner instead of users_id)
+ $tab[] = [
+ 'id' => 14,
+ 'table' => 'glpi_users',
+ 'field' => 'name',
+ 'name' => __('Owner'),
+ 'datatype' => 'dropdown',
+ 'linkfield' => 'users_id_owner', // <-- REQUIRED for non-standard FK names
+ ];
+
+ // FK to Group with non-standard column
+ $tab[] = [
+ 'id' => 16,
+ 'table' => 'glpi_groups',
+ 'field' => 'completename', // use 'completename' for tree dropdowns
+ 'name' => __('Department'),
+ 'datatype' => 'dropdown',
+ 'linkfield' => 'groups_id_department',
+ ];
+
+ // Join through a relation table (e.g., find items by assigned user)
+ $tab[] = [
+ 'id' => 8,
+ 'table' => 'glpi_users',
+ 'field' => 'name',
+ 'name' => __('User'),
+ 'datatype' => 'dropdown',
+ 'forcegroupby' => true, // groups results (one item can have many users)
+ 'massiveaction' => false,
+ 'joinparams' => [
+ 'beforejoin' => [
+ 'table' => 'glpi_plugin_myplugin_myitems_users',
+ 'joinparams' => [
+ 'jointype' => 'child', // relation table is a "child" of main table
+ ],
+ ],
+ ],
+ ];
+
+ // URL / weblink
+ $tab[] = [
+ 'id' => 11,
+ 'table' => self::getTable(),
+ 'field' => 'portal_url',
+ 'name' => __('Portal URL'),
+ 'datatype' => 'weblink',
+ ];
+
+ // Decimal
+ $tab[] = [
+ 'id' => 13,
+ 'table' => self::getTable(),
+ 'field' => 'price',
+ 'name' => __('Price'),
+ 'datatype' => 'decimal',
+ ];
+
+ return $tab;
+}
+```
+
+### CRITICAL: `linkfield` vs `joinparams`
+
+- **Standard FK** (column name matches GLPI convention like `manufacturers_id`): No extra params needed — GLPI auto-detects the join.
+- **Non-standard FK** (column like `users_id_owner`, `groups_id_department`): **Must use `'linkfield' => 'column_name'`** to tell GLPI which column to join on.
+- **Through a relation table**: Use `joinparams` with `beforejoin`.
+
+**DO NOT USE** `'field_fkey'` — this is NOT a valid GLPI search option parameter. It will be silently ignored and the search will break.
+
+### Available Datatypes
+
+`itemlink`, `string`, `text`, `number`, `integer`, `decimal`, `bool`, `datetime`, `date`, `dropdown`, `weblink`, `email`, `specific`
+
+---
+
+## 9. Tabs System
+
+### Adding tabs to your own item type
+
+In your `CommonDBTM` class:
+
+```php
+function defineTabs($options = []) {
+ $tabs = [];
+ $this->addDefaultFormTab($tabs); // main form
+ $this->addStandardTab(MyItem_User::class, $tabs, $options); // custom tab
+ $this->addStandardTab('Log', $tabs, $options); // history
+ $this->addStandardTab('Notepad', $tabs, $options); // notes
+ return $tabs;
+}
+```
+
+### Adding tabs to OTHER item types (e.g., User pages)
+
+In `setup.php`:
+
+```php
+Plugin::registerClass(MyItem_User::class, [
+ 'addtabon' => ['User'],
+]);
+```
+
+### Tab provider class
+
+```php
+function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) {
+ if ($item instanceof MyItem) {
+ $count = countElementsInTable(self::getTable(), [
+ 'plugin_myplugin_myitems_id' => $item->getID(),
+ ]);
+ return self::createTabEntry(__('Users', 'myplugin'), $count);
+ }
+ return '';
+}
+
+static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) {
+ if ($item instanceof MyItem) {
+ self::showForMyItem($item);
+ return true;
+ }
+ return false;
+}
+```
+
+### CRITICAL: Tabs are loaded via AJAX
+
+Tab content is loaded via AJAX through `/ajax/common.tabs.php`. If your tab class has a PHP error (e.g., missing `use` import), the AJAX request returns a 500 error and the tab shows a blank/error page in the browser.
+
+**Debug tip**: Check the browser's Network tab for the failing AJAX request to `common.tabs.php` and look at the response body for the PHP error message.
+
+---
+
+## 10. Front Controllers
+
+Files in `front/` are the entry points for user-facing pages.
+
+### List Page (front/myitem.php)
+
+```php
+check(-1, CREATE, $_POST);
+ $item->add($_POST);
+ Html::back();
+} else if (isset($_POST['update'])) {
+ $item->check($_POST['id'], UPDATE);
+ $item->update($_POST);
+ Html::back();
+} else if (isset($_POST['delete'])) {
+ $item->check($_POST['id'], DELETE);
+ $item->delete($_POST);
+ $item->redirectToList();
+} else if (isset($_POST['restore'])) {
+ $item->check($_POST['id'], DELETE);
+ $item->restore($_POST);
+ Html::back();
+} else if (isset($_POST['purge'])) {
+ $item->check($_POST['id'], PURGE);
+ $item->delete($_POST, 1);
+ $item->redirectToList();
+} else {
+ $menus = ['assets', MyItem::class];
+ MyItem::displayFullPageForItem($_GET['id'] ?? 0, $menus, [
+ 'formoptions' => "data-track-changes=true",
+ ]);
+}
+```
+
+### CRITICAL RULE: Never Call Session::checkCRSF() in /front/ Files
+
+GLPI's bootstrap (`inc/includes.php`) automatically validates CSRF tokens for all POST requests routed through `/front/`. Calling `Session::checkCRSF()` a second time causes a failure because the token has already been consumed from the session token pool (`$_SESSION['glpicsrftokens']`).
+
+---
+
+## 11. AJAX Endpoints
+
+```php
+ false, 'error' => 'Not authenticated']);
+ return;
+}
+
+try {
+ $param = $_GET['param'] ?? '';
+ $result = PluginMypluginFeature::getData($param);
+ echo json_encode(['success' => true, 'data' => $result]);
+} catch (\Throwable $e) {
+ http_response_code(500);
+ echo json_encode(['success' => false, 'error' => $e->getMessage()]);
+}
+```
+
+### Key Rules
+
+- AJAX endpoints do **not** need CSRF validation — session authentication is sufficient.
+- Always set `Content-Type: application/json; charset=utf-8`.
+- Always check `Session::getLoginUserID()` and return 403 if not authenticated.
+- Wrap in try-catch to prevent PHP errors from corrupting JSON output.
+
+---
+
+## 12. Database Access
+
+GLPI provides a `$DB` global object. **Never use raw `mysqli_*` calls.**
+
+### Read (SELECT)
+
+```php
+global $DB;
+
+// Simple query
+$rows = $DB->request([
+ 'FROM' => 'glpi_plugin_myplugin_myitems',
+ 'WHERE' => ['status' => 'active', 'users_id' => $userId],
+ 'ORDER' => 'name ASC',
+ 'LIMIT' => 50,
+]);
+foreach ($rows as $row) {
+ $id = $row['id'];
+ $name = $row['name'];
+}
+
+// With JOIN
+$rows = $DB->request([
+ 'SELECT' => [
+ 'glpi_plugin_myplugin_myitems.*',
+ 'glpi_users.name AS username',
+ ],
+ 'FROM' => 'glpi_plugin_myplugin_myitems',
+ 'LEFT JOIN' => [
+ 'glpi_users' => [
+ 'FKEY' => [
+ 'glpi_plugin_myplugin_myitems' => 'users_id_owner',
+ 'glpi_users' => 'id',
+ ],
+ ],
+ ],
+ 'WHERE' => [
+ 'glpi_plugin_myplugin_myitems.is_deleted' => 0,
+ ],
+ 'ORDER' => ['glpi_plugin_myplugin_myitems.name ASC'],
+]);
+
+// LIKE search (escape user input)
+$pattern = '%' . $DB->escape($searchTerm) . '%';
+$rows = $DB->request([
+ 'FROM' => 'glpi_plugin_myplugin_myitems',
+ 'WHERE' => [
+ 'OR' => [
+ ['name' => ['LIKE', $pattern]],
+ ['serial' => ['LIKE', $pattern]],
+ ],
+ ],
+]);
+
+// IN clause
+$rows = $DB->request([
+ 'FROM' => 'glpi_plugin_myplugin_myitems',
+ 'WHERE' => ['id' => [1, 2, 3, 4]], // Generates IN (1,2,3,4)
+]);
+```
+
+### Count
+
+```php
+// Using GLPI helper
+$count = countElementsInTable('glpi_plugin_myplugin_myitems', [
+ 'users_id' => $userId,
+]);
+
+// Using $DB->request
+$count = count($DB->request([...]));
+
+// Aggregate (MAX, COUNT, etc.)
+$row = $DB->request([
+ 'SELECT' => ['MAX' => 'level AS max_level'],
+ 'FROM' => 'glpi_groups',
+])->current();
+$maxLevel = (int)$row['max_level'];
+```
+
+### Insert
+
+```php
+$DB->insert('glpi_plugin_myplugin_myitems', [
+ 'name' => $name,
+ 'status' => 'active',
+ 'users_id' => (int)$userId,
+ 'date_creation' => date('Y-m-d H:i:s'),
+ 'date_mod' => date('Y-m-d H:i:s'),
+]);
+$newId = $DB->insertId();
+```
+
+### Update
+
+```php
+$DB->update(
+ 'glpi_plugin_myplugin_myitems',
+ ['status' => 'completed', 'date_mod' => date('Y-m-d H:i:s')], // SET
+ ['id' => $itemId] // WHERE
+);
+```
+
+### Update or Insert (Upsert)
+
+```php
+$DB->updateOrInsert(
+ 'glpi_plugin_myplugin_configs',
+ ['setting' => $value], // data to set
+ ['id' => 1] // WHERE clause
+);
+```
+
+### Delete
+
+```php
+$DB->delete('glpi_plugin_myplugin_myitems', ['id' => $itemId]);
+```
+
+### Schema Checks
+
+```php
+$DB->tableExists('glpi_plugin_myplugin_myitems'); // true/false
+$DB->fieldExists('glpi_plugin_myplugin_myitems', 'new_column'); // true/false
+```
+
+### Key Rules
+
+- All values passed in arrays are auto-escaped — no manual escaping needed for insert/update/where.
+- Use `$DB->escape()` only for LIKE patterns or raw `doQuery()` calls.
+- Always cast integer IDs: `(int)$userId`.
+- `$DB->request()` returns an iterator — use `foreach` or `count()`.
+- For raw SQL (migrations only): `$DB->doQuery("ALTER TABLE ...")`.
+
+---
+
+## 13. Relations — CommonDBRelation
+
+For many-to-many links between items (e.g., linking Users to your plugin items).
+
+### Class Setup
+
+```php
+namespace GlpiPlugin\Myplugin;
+
+use CommonDBRelation;
+
+class MyItem_User extends CommonDBRelation {
+ static public $itemtype_1 = MyItem::class;
+ static public $items_id_1 = 'plugin_myplugin_myitems_id'; // FK column name
+
+ static public $itemtype_2 = 'User';
+ static public $items_id_2 = 'users_id'; // FK column name
+
+ // Rights: check rights on side 1 only
+ static public $checkItem_1_Rights = self::HAVE_SAME_RIGHT_ON_ITEM;
+ static public $checkItem_2_Rights = self::DONT_CHECK_ITEM_RIGHTS;
+
+ // Log history on both sides
+ static public $logs_for_item_1 = true;
+ static public $logs_for_item_2 = true;
+}
+```
+
+### Form URL Override
+
+CommonDBRelation needs a custom `getFormURL()` pointing to the front-end handler:
+
+```php
+static function getFormURL($full = true) {
+ return Plugin::getWebDir('myplugin', $full) . '/front/myitem_user.form.php';
+}
+```
+
+### Front-End Handler File (REQUIRED)
+
+You **must** create `front/myitem_user.form.php` — without it, add/delete buttons on the relation tab will 404:
+
+```php
+check(-1, CREATE, $_POST);
+ $link->add($_POST);
+ Html::back();
+} else if (isset($_POST['purge'])) {
+ $link->check($_POST['id'], PURGE);
+ $link->delete($_POST, 1); // 1 = force purge
+ Html::back();
+}
+```
+
+### Delete/Purge Buttons
+
+```php
+// CORRECT — plain text label
+echo Html::submit(__('Delete'), [
+ 'name' => 'purge',
+ 'class' => 'btn btn-danger btn-sm',
+ 'confirm' => __('Confirm the final deletion?'),
+]);
+
+// WRONG — Html::submit() escapes HTML, so icons show as raw text
+echo Html::submit("", [...]);
+// Result: user sees literal ""
+```
+
+**RULE**: Never pass raw HTML to `Html::submit()` — it HTML-escapes the label. Use plain text like `__('Delete')` or `__('Remove')`.
+
+---
+
+## 14. Configuration Pages
+
+### Registration in setup.php
+
+```php
+$PLUGIN_HOOKS['config_page']['myplugin'] = 'front/config.form.php';
+```
+
+This adds a **"Configure"** button on the Setup > Plugins page next to your plugin.
+
+### Config Form Pattern (front/config.form.php)
+
+```php
+update($_POST);
+ Html::back();
+}
+
+Html::header(__('My Plugin Configuration', 'myplugin'), $_SERVER['PHP_SELF'], 'config');
+
+$config->showConfigForm();
+
+Html::footer();
+```
+
+### Singleton Config Table Pattern
+
+```sql
+CREATE TABLE `glpi_plugin_myplugin_configs` (
+ `id` INT unsigned NOT NULL AUTO_INCREMENT,
+ `setting_one` INT NOT NULL DEFAULT 1,
+ `setting_two` TEXT,
+ PRIMARY KEY (`id`)
+);
+INSERT INTO `glpi_plugin_myplugin_configs` (id, setting_one) VALUES (1, 1);
+```
+
+Read config:
+
+```php
+$row = $DB->request([
+ 'FROM' => 'glpi_plugin_myplugin_configs',
+ 'WHERE' => ['id' => 1],
+ 'LIMIT' => 1,
+])->current();
+```
+
+Write config:
+
+```php
+$DB->updateOrInsert(
+ 'glpi_plugin_myplugin_configs',
+ ['setting_one' => $newValue],
+ ['id' => 1]
+);
+```
+
+---
+
+## 15. GLPI Group Hierarchy
+
+GLPI groups (`glpi_groups`) are a tree structure (CommonTreeDropdown):
+
+| Column | Purpose |
+|--------|---------|
+| `id` | Primary key |
+| `name` | Short name |
+| `completename` | Full path (`Root > Child > Grandchild`) |
+| `level` | Depth in tree (1 = root) |
+| `groups_id` | Parent group ID (0 = root) |
+| `entities_id` | Entity scope |
+| `ancestors_cache` | JSON cache of ancestor IDs |
+| `sons_cache` | JSON cache of descendant IDs |
+
+User-group membership is in `glpi_groups_users`:
+
+| Column | Purpose |
+|--------|---------|
+| `users_id` | FK to user |
+| `groups_id` | FK to group |
+| `is_dynamic` | Synced from LDAP/AD |
+| `is_manager` | User is group manager |
+| `is_userdelegate` | User is delegate |
+
+### Walking the Tree
+
+To find an ancestor at a specific level, walk up via `groups_id` (parent FK):
+
+```php
+$currentId = $groupId;
+while ($cache[$currentId]['level'] > $targetLevel) {
+ $currentId = $cache[$currentId]['groups_id']; // go to parent
+}
+```
+
+### Querying Max Depth
+
+```php
+$maxRow = $DB->request([
+ 'SELECT' => ['MAX' => 'level AS max_level'],
+ 'FROM' => 'glpi_groups',
+])->current();
+$maxLevel = (int)$maxRow['max_level'];
+```
+
+---
+
+## 16. GLPI Hook System
+
+Hooks let your plugin react to events on GLPI items (tickets, users, computers, etc.).
+
+### Registration (in setup.php)
+
+```php
+// Post-action hooks (fired AFTER the DB write)
+$PLUGIN_HOOKS['item_update']['myplugin'] = ['Ticket' => 'PluginMypluginHook::afterTicketUpdate'];
+$PLUGIN_HOOKS['item_add']['myplugin'] = ['Ticket' => 'PluginMypluginHook::afterTicketAdd'];
+
+// Pre-action hooks (fired BEFORE the DB write — can block the operation)
+$PLUGIN_HOOKS['pre_item_update']['myplugin'] = ['Ticket' => 'PluginMypluginHook::beforeTicketUpdate'];
+$PLUGIN_HOOKS['pre_item_add']['myplugin'] = ['ITILSolution' => 'PluginMypluginHook::beforeSolutionAdd'];
+```
+
+### Hook Execution Order
+
+```
+1. pre_item_add / pre_item_update ← Can block the operation
+2. Database write happens
+3. item_add / item_update ← Post-action, informational only
+```
+
+### The fields[] vs input[] Rule (CRITICAL)
+
+This is the single most important thing to understand about GLPI hooks:
+
+```php
+public static function afterTicketUpdate(Ticket $ticket): void {
+ // $ticket->fields = OLD values (current DB state BEFORE update)
+ // $ticket->input = NEW values (what is being written)
+
+ $oldStatus = (int)($ticket->fields['status'] ?? 0);
+ $newStatus = (int)($ticket->input['status'] ?? 0);
+
+ if ($newStatus !== $oldStatus && $newStatus === CommonITILObject::CLOSED) {
+ // Ticket was just closed — react to it
+ }
+}
+```
+
+| Context | `$item->fields` | `$item->input` |
+|---------|-----------------|----------------|
+| `pre_item_update` | Old DB values | New values being applied |
+| `item_update` | Old DB values | New values just applied |
+| `pre_item_add` | Empty/unset | Values being inserted |
+| `item_add` | Values just inserted | Values just inserted |
+
+### Blocking Operations in pre_ Hooks
+
+```php
+public static function beforeTicketUpdate(Ticket $ticket): void {
+ if ($shouldBlockStatusChange) {
+ // Remove the field from input to prevent it from being saved
+ unset($ticket->input['status']);
+ Session::addMessageAfterRedirect(__('Cannot close this ticket yet.', 'myplugin'), true, ERROR);
+ }
+}
+
+public static function beforeSolutionAdd(ITILSolution $solution): void {
+ if ($shouldBlockSolution) {
+ // Set input to false to completely prevent the add
+ $solution->input = false;
+ Session::addMessageAfterRedirect(__('Add a follow-up first.', 'myplugin'), true, ERROR);
+ }
+}
+```
+
+### Ticket Status Detection — Cover All Paths
+
+Ticket status changes can happen through multiple paths. Register hooks for all of them:
+
+```php
+// Direct status field update
+$PLUGIN_HOOKS['pre_item_update']['myplugin'] = ['Ticket' => 'PluginMypluginHook::beforeTicketUpdate'];
+$PLUGIN_HOOKS['item_update']['myplugin'] = ['Ticket' => 'PluginMypluginHook::afterTicketUpdate'];
+
+// Adding a solution (which changes status to SOLVED)
+$PLUGIN_HOOKS['pre_item_add']['myplugin'] = ['ITILSolution' => 'PluginMypluginHook::beforeSolutionAdd'];
+$PLUGIN_HOOKS['item_add']['myplugin'] = ['ITILSolution' => 'PluginMypluginHook::afterSolutionAdd'];
+```
+
+A user can close a ticket via:
+- Changing the status field directly → `pre_item_update` on Ticket
+- Adding an ITILSolution → `pre_item_add` on ITILSolution
+- Approving a pending solution → `pre_item_update` on Ticket
+
+You need hooks on **all paths** to reliably block premature closure.
+
+### Available Hook Names
+
+| Hook | Fires | Can Block? |
+|------|-------|-----------|
+| `pre_item_add` | Before DB insert | Yes (`$item->input = false`) |
+| `item_add` | After DB insert | No |
+| `pre_item_update` | Before DB update | Yes (`unset($item->input['field'])`) |
+| `item_update` | After DB update | No |
+| `pre_item_purge` | Before permanent delete | Yes |
+| `item_purge` | After permanent delete | No |
+| `pre_item_delete` | Before soft delete (trash) | Yes |
+| `item_delete` | After soft delete | No |
+
+### Ticket Status Constants
+
+```php
+CommonITILObject::INCOMING = 1
+CommonITILObject::ASSIGNED = 2
+CommonITILObject::PLANNED = 3
+CommonITILObject::WAITING = 4
+CommonITILObject::SOLVED = 5
+CommonITILObject::CLOSED = 6
+```
+
+**Always compare as integers**: `(int)$ticket->fields['status'] === CommonITILObject::CLOSED`. Never use string comparison like `=== 'closed'`.
+
+---
+
+## 17. Permissions & Rights
+
+### Rights Constants
+
+```php
+READ = 1
+CREATE = 2
+UPDATE = 4
+DELETE = 8
+PURGE = 16
+ALLSTANDARDRIGHT = 31 // READ + CREATE + UPDATE + DELETE + PURGE
+```
+
+### Native GLPI Rights System (recommended for GLPI 11.x)
+
+Use `ProfileRight` to register and manage rights through GLPI's built-in system:
+
+```php
+// Install-time: register the right key and grant to Super-Admin
+ProfileRight::addProfileRights(['plugin_myplugin_myitem']);
+
+$profile = new Profile();
+foreach ($profile->find(['interface' => 'central']) as $data) {
+ if ($data['name'] === 'Super-Admin') {
+ $profileRight = new ProfileRight();
+ $profileRight->updateProfileRights($data['id'], [
+ 'plugin_myplugin_myitem' => ALLSTANDARDRIGHT,
+ ]);
+ }
+}
+
+// Uninstall-time: clean up
+ProfileRight::deleteProfileRights(['plugin_myplugin_myitem']);
+```
+
+### Checking Rights in Code
+
+```php
+// Via item class (uses static $rightname)
+MyItem::canView() // has READ
+MyItem::canUpdate() // has UPDATE
+MyItem::canCreate() // has CREATE
+MyItem::canDelete() // has DELETE
+MyItem::canPurge() // has PURGE
+
+// Via Session (for GLPI global rights or direct checks)
+Session::haveRight('config', READ); // Global config access
+Session::haveRight('ticket', CREATE); // Can create tickets
+Session::checkRight(MyItem::$rightname, READ); // Throws exception if no right
+```
+
+### Custom Profile Rights Table (alternative approach)
+
+For plugins that need more granular control beyond standard CRUD:
+
+```sql
+CREATE TABLE `glpi_plugin_myplugin_profiles` (
+ `id` INT unsigned NOT NULL AUTO_INCREMENT,
+ `profiles_id` INT unsigned NOT NULL DEFAULT 0,
+ `right_feature` INT unsigned NOT NULL DEFAULT 0,
+ `right_config` INT unsigned NOT NULL DEFAULT 0,
+ PRIMARY KEY (`id`),
+ KEY `profiles_id` (`profiles_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+```
+
+```php
+// Check custom rights using bitmask AND
+if (($userRight & CREATE) === CREATE) {
+ // User has CREATE permission
+}
+```
+
+### Layered Permission Checking
+
+Always check both plugin rights AND GLPI global rights as fallback:
+
+```php
+$canConfig = false;
+try {
+ $canConfig = PluginMypluginProfile::hasRight('right_config', UPDATE);
+} catch (\Throwable $e) {}
+
+// Fallback: GLPI super-admin can always access config
+if (!$canConfig && !Session::haveRight('config', UPDATE)) {
+ Html::displayRightError();
+ exit;
+}
+```
+
+---
+
+## 18. Sessions & Authentication
+
+### Session Data Access
+
+```php
+$userId = Session::getLoginUserID(); // 0 if not logged in
+$profileId = $_SESSION['glpiactiveprofile']['id'] ?? 0; // Active profile ID
+$entityId = $_SESSION['glpiactive_entity'] ?? 0; // Active entity ID
+$userName = $_SESSION['glpiname'] ?? ''; // Login username
+$language = $_SESSION['glpilanguage'] ?? 'en_GB'; // User language
+```
+
+### Getting User Details
+
+```php
+$user = new User();
+if ($user->getFromDB($userId)) {
+ $fullName = $user->getFriendlyName(); // "First Last"
+ $email = UserEmail::getDefaultForUser($userId);
+}
+```
+
+### Important: Sessions in Cron Context
+
+Cron tasks run without a user session. `Session::getLoginUserID()` returns `0`. Do not rely on `$_SESSION` in cron task code — pass entity/user IDs explicitly.
+
+### Html::redirect() Does Not Exit
+
+After `Html::redirect($url)`, your PHP code **continues executing**. Always call `exit;` or `return` after redirect if subsequent code should not run.
+
+---
+
+## 19. Translations (i18n)
+
+### PHP Usage
+
+```php
+__('String to translate', 'myplugin'); // Simple translation
+_n('One item', '%d items', $count, 'myplugin'); // Plural form
+_x('String', 'context', 'myplugin'); // With disambiguation context
+sprintf(__('Hello %s', 'myplugin'), $name); // With parameters
+```
+
+### .po File Format (locales/en_GB.po, locales/pl_PL.po)
+
+```po
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "Equipment Transfer"
+msgstr "Equipment Transfer"
+
+msgid "Hello %s"
+msgstr "Hello %s"
+```
+
+### Plural Forms
+
+```po
+msgid "Access"
+msgid_plural "Accesses"
+msgstr[0] "Access"
+msgstr[1] "Accesses"
+```
+
+Polish requires 3 plural forms:
+
+```po
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 "
+"&& (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+msgstr[0] "Dostęp"
+msgstr[1] "Dostępy"
+msgstr[2] "Dostępów"
+```
+
+### Rules
+
+- **Always** use the plugin domain (second parameter): `__('text', 'myplugin')`.
+- **Never** hardcode user-visible strings without a translation wrapper.
+- Add every new string to **both** language files.
+- The domain must match the plugin directory name.
+
+---
+
+## 20. Frontend (JS/CSS)
+
+### JavaScript
+
+No build tools — vanilla JS only. Edit files directly.
+
+```javascript
+// AJAX call pattern
+fetch(pluginBaseUrl + '/ajax/getData.php?param=' + encodeURIComponent(value), {
+ method: 'GET',
+})
+.then(response => response.json())
+.then(data => {
+ if (data.success) {
+ // Process data.data
+ }
+})
+.catch(error => console.error('AJAX error:', error));
+```
+
+### Passing Config from PHP to JS
+
+```php
+// In your PHP form/page:
+$jsConfig = [
+ 'ajaxUrl' => Plugin::getWebDir('myplugin') . '/ajax/',
+ 'csrfToken' => Session::getNewCSRFToken(), // Only if needed for non-/front/ POST
+ 'labels' => [
+ 'confirm' => __('Confirm', 'myplugin'),
+ 'cancel' => __('Cancel', 'myplugin'),
+ ],
+];
+echo '';
+```
+
+### CSS
+
+```css
+/* Prefix plugin classes to avoid collisions */
+.plugin-myplugin-container { }
+.plugin-myplugin-button { }
+
+/* Use GLPI's Bootstrap variables where possible */
+```
+
+### CSS/JS Cache Busting
+
+In `setup.php`, always append version to asset URLs:
+
+```php
+$v = PLUGIN_MYPLUGIN_VERSION;
+$PLUGIN_HOOKS['add_css']['myplugin'] = ["public/css/myplugin.css?v={$v}"];
+$PLUGIN_HOOKS['add_javascript']['myplugin'] = ["public/js/myplugin.js?v={$v}"];
+```
+
+Browsers and GLPI cache static assets aggressively. Without version query strings, users will see stale JS/CSS after updates. Always use `?v=VERSION` and increment on every release.
+
+---
+
+## 21. File & Document Handling
+
+### Plugin Storage Directories
+
+```php
+$pluginDir = GLPI_DOC_DIR . '/_plugins/myplugin/';
+@mkdir($pluginDir . 'uploads/', 0755, true);
+```
+
+### Creating a GLPI Document Record
+
+```php
+$doc = new Document();
+$docId = $doc->add([
+ 'name' => 'Protocol_' . $itemId . '.pdf',
+ 'filename' => $filename,
+ 'filepath' => $relativePath, // Relative to GLPI_DOC_DIR
+ 'mime' => 'application/pdf',
+ 'entities_id' => $_SESSION['glpiactive_entity'],
+ 'is_recursive' => 1,
+]);
+```
+
+### Linking Documents to Items
+
+```php
+(new Document_Item())->add([
+ 'documents_id' => $docId,
+ 'itemtype' => 'User', // Or 'Ticket', 'Computer', etc.
+ 'items_id' => $userId,
+ 'entities_id' => $_SESSION['glpiactive_entity'],
+]);
+```
+
+### File Upload Validation (Multi-Layer)
+
+```php
+// 1. Check upload success
+if ($_FILES['upload']['error'] !== UPLOAD_ERR_OK) { return; }
+
+// 2. Validate MIME type
+$mime = mime_content_type($_FILES['upload']['tmp_name']);
+if (!in_array($mime, ['image/png', 'image/jpeg'], true)) { reject(); }
+
+// 3. Validate extension
+$ext = strtolower(pathinfo($_FILES['upload']['name'], PATHINFO_EXTENSION));
+if (!in_array($ext, ['png', 'jpg', 'jpeg'], true)) { reject(); }
+
+// 4. Check file size
+if ($_FILES['upload']['size'] > 2 * 1024 * 1024) { reject(); }
+
+// 5. Validate image integrity (skip for SVG)
+if ($mime !== 'image/svg+xml') {
+ $info = @getimagesize($_FILES['upload']['tmp_name']);
+ if ($info === false) { reject(); }
+}
+
+// 6. Generate safe filename (never use user-supplied filename directly)
+$safeFilename = 'upload_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
+```
+
+---
+
+## 22. PDF Generation
+
+### Fallback Chain Pattern
+
+```php
+public static function generatePDF(string $html): ?string {
+ // 1. Try wkhtmltopdf (best quality, 1:1 with browser Ctrl+P)
+ $wkPath = self::findWkhtmltopdf();
+ if ($wkPath) {
+ return self::renderWithWkhtmltopdf($wkPath, $html);
+ }
+
+ // 2. Try Chromium headless
+ $chromePath = self::findChromium();
+ if ($chromePath) {
+ return self::renderWithChromium($chromePath, $html);
+ }
+
+ // 3. Try mPDF (PHP library, limited CSS)
+ if (class_exists('\\Mpdf\\Mpdf')) {
+ return self::renderWithMpdf($html);
+ }
+
+ // 4. Fallback: save as HTML
+ return self::saveAsHtml($html);
+}
+```
+
+### Shell Command Safety
+
+```php
+$cmd = sprintf(
+ '%s --quiet --page-size A4 --encoding utf-8 %s %s 2>&1',
+ escapeshellarg($binaryPath),
+ escapeshellarg($inputHtmlPath),
+ escapeshellarg($outputPdfPath)
+);
+exec($cmd, $output, $exitCode);
+```
+
+### Temp File Handling
+
+```php
+$tmpDir = GLPI_TMP_DIR;
+$htmlPath = $tmpDir . '/protocol_' . uniqid() . '.html';
+file_put_contents($htmlPath, $html);
+// ... generate PDF ...
+@unlink($htmlPath); // Always clean up
+```
+
+### mPDF Adaptation
+
+mPDF has limited CSS support. Adapt HTML before passing:
+
+```php
+private static function adaptForMpdf(string $html): string {
+ // mPDF doesn't support Segoe UI
+ $html = str_replace("'Segoe UI'", "'DejaVu Sans'", $html);
+ // mPDF handles max-width differently
+ $html = str_replace('max-width: 800px;', '', $html);
+ return $html;
+}
+```
+
+### Design Rules for Printable HTML
+
+- Use **table-based layout** only (no flexbox, no CSS grid) — mPDF can't render them.
+- Use **inline styles** — external CSS classes don't carry into PDF rendering.
+- Embed images as **base64 data URLs** — PDF engines can't fetch external URLs.
+- Include `@page { size: A4; margin: 15mm 20mm; }` in `
+
+
+
+
+
+ | OT |
+
+
+ |
+ Potwierdzenie włączenia do użytku / Lokalizacja środków trwałych
+ |
+
+
+ |
+ (Nachweis der Inbetriebnahme / Verteilung der Sachanlagen)
+ |
+
+
+
+
+
+ |
+ Numer faktury: _______________
+ |
+
+ Dostawca / Producent: {$supplier_esc}
+ |
+
+
+
+
+
+
+
+
+ | Poz |
+ Ilość/ Menge |
+ Nazwa środka trwałego / Bezeichnung |
+ Nr seryjny / Seriennummer |
+ Nr środka trwałego / Anlagennumm. |
+ Wartość / Betrag |
+ Lokaliz. / Standort |
+ Cost Center |
+ Order |
+ Data włącz. do użytku / Datum Inbetriebnahme |
+ Data zdep. w magazynie / Datum ins Lager |
+
+
+
+ {$items_html}
+
+ | SUMA / Summe: |
+ {$total_formatted} |
+ |
+
+
+
+
+
+
+ | ____________________________ |
+ | czytelny podpis |
+
+
+
+
+
+
+
+
+
+ |
+ Zmiany lokalizacji środków trwałych / (Verschiebung von Anlagenvermögen)
+ |
+
+
+
+
+
+
+ | Poz |
+ Ilość/ Menge |
+ Nazwa środka trwałego / Bezeichnung |
+ Nr seryjny / Seriennummer |
+ Nr środka trwałego / Anlagennumm. |
+ Lokaliz. / Standort |
+ Cost Center |
+ Order |
+ Data przes. / Datum von Verschiebung |
+ Data włącz. do użytku / Datum Inbetriebnahme |
+
+
+
+HTML;
+
+ // 10 empty rows for the relocation section
+ for ($i = 1; $i <= 10; $i++) {
+ $html .= "";
+ for ($c = 0; $c < 10; $c++) {
+ $html .= "| | ";
+ }
+ $html .= "
\n";
+ }
+
+ $html .= <<
+
+
+
+
+ | ____________________________ |
+ | czytelny podpis |
+
+ |
+ * nach Bearbeitung durch Rechnungsprüfer Kopie an LZ
+ |
+
+
+
+
+
+HTML;
+
+ return $html;
+ }
+
+
+ /**
+ * Generate PDF from HTML using fallback chain: wkhtmltopdf -> Chromium -> mPDF -> HTML file.
+ *
+ * @param string $html Full HTML document
+ * @param string $base_name Base filename (without extension)
+ * @return string|false Path to generated file, or false on failure
+ */
+ public function generatePdf(string $html, string $base_name)
+ {
+ $tmp_dir = GLPI_TMP_DIR;
+ $html_path = $tmp_dir . '/' . $base_name . '_' . uniqid() . '.html';
+ $pdf_path = $tmp_dir . '/' . $base_name . '_' . uniqid() . '.pdf';
+
+ file_put_contents($html_path, $html);
+
+ // 1. Try wkhtmltopdf
+ $wk_path = $this->findBinary('wkhtmltopdf');
+ if ($wk_path) {
+ $cmd = sprintf(
+ '%s --quiet --page-size A4 --orientation Landscape --encoding utf-8 --margin-top 10 --margin-bottom 10 --margin-left 12 --margin-right 12 %s %s 2>&1',
+ escapeshellarg($wk_path),
+ escapeshellarg($html_path),
+ escapeshellarg($pdf_path),
+ );
+ exec($cmd, $output, $exit_code);
+ @unlink($html_path);
+ if ($exit_code === 0 && file_exists($pdf_path)) {
+ return $pdf_path;
+ }
+ }
+
+ // 2. Try Chromium headless
+ $chrome_path = $this->findBinary('chromium-browser') ?: $this->findBinary('chromium') ?: $this->findBinary('google-chrome');
+ if ($chrome_path) {
+ $cmd = sprintf(
+ '%s --headless --disable-gpu --no-sandbox --print-to-pdf=%s --no-pdf-header-footer file://%s 2>&1',
+ escapeshellarg($chrome_path),
+ escapeshellarg($pdf_path),
+ escapeshellarg($html_path),
+ );
+ exec($cmd, $output, $exit_code);
+ @unlink($html_path);
+ if ($exit_code === 0 && file_exists($pdf_path)) {
+ return $pdf_path;
+ }
+ }
+
+ // 3. Try mPDF
+ if (class_exists('\\Mpdf\\Mpdf')) {
+ try {
+ $mpdf = new \Mpdf\Mpdf([
+ 'mode' => 'utf-8',
+ 'format' => 'A4-L',
+ 'margin_left' => 12,
+ 'margin_right' => 12,
+ 'margin_top' => 10,
+ 'margin_bottom' => 10,
+ 'tempDir' => $tmp_dir,
+ ]);
+ // Adapt for mPDF limitations
+ $adapted_html = str_replace("'DejaVu Sans'", "'dejavusans'", $html);
+ $mpdf->WriteHTML($adapted_html);
+ $mpdf->Output($pdf_path, \Mpdf\Output\Destination::FILE);
+ @unlink($html_path);
+ if (file_exists($pdf_path)) {
+ return $pdf_path;
+ }
+ } catch (\Throwable $e) {
+ // mPDF failed, continue to fallback
+ }
+ }
+
+ // 4. Fallback: save as HTML
+ @unlink($pdf_path);
+ // Return the HTML file path directly
+ return $html_path;
+ }
+
+
+ /**
+ * Find a binary in common system paths.
+ *
+ * @param string $name Binary name
+ * @return string|null Full path if found, null otherwise
+ */
+ private function findBinary(string $name): ?string
+ {
+ $search_paths = [
+ '/usr/bin/',
+ '/usr/local/bin/',
+ '/snap/bin/',
+ ];
+
+ foreach ($search_paths as $dir) {
+ $path = $dir . $name;
+ if (is_executable($path)) {
+ return $path;
+ }
+ }
+
+ // Try `which` as last resort
+ $result = trim((string) shell_exec('which ' . escapeshellarg($name) . ' 2>/dev/null'));
+ if ($result && is_executable($result)) {
+ return $result;
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Save the generated file as a GLPI Document linked to the order.
+ *
+ * @param PluginOrderOrder $order The order (already loaded)
+ * @param string $filepath Absolute path to the generated file
+ * @param string $filename Document filename (e.g. "OT_PO123.pdf")
+ * @param string $mime MIME type
+ * @return int|false Document ID on success, false on failure
+ */
+ public function saveAsDocument(PluginOrderOrder $order, string $filepath, string $filename, string $mime)
+ {
+ if (!file_exists($filepath)) {
+ return false;
+ }
+
+ // Move file to GLPI document storage
+ $doc_dir = GLPI_DOC_DIR . '/_plugins/order/ot/';
+ @mkdir($doc_dir, 0755, true);
+
+ $dest_path = $doc_dir . $filename;
+ // If file already exists, add unique suffix
+ if (file_exists($dest_path)) {
+ $info = pathinfo($filename);
+ $dest_path = $doc_dir . $info['filename'] . '_' . date('YmdHis') . '.' . $info['extension'];
+ $filename = basename($dest_path);
+ }
+
+ if (!rename($filepath, $dest_path)) {
+ if (!copy($filepath, $dest_path)) {
+ return false;
+ }
+ @unlink($filepath);
+ }
+
+ // Relative path from GLPI_DOC_DIR
+ $relative_path = '_plugins/order/ot/' . $filename;
+
+ // Create GLPI Document record
+ $doc = new Document();
+ $doc_id = $doc->add([
+ 'name' => $filename,
+ 'filename' => $filename,
+ 'filepath' => $relative_path,
+ 'mime' => $mime,
+ 'entities_id' => $order->fields['entities_id'],
+ 'is_recursive' => $order->fields['is_recursive'] ?? 0,
+ ]);
+
+ if (!$doc_id) {
+ return false;
+ }
+
+ // Link document to the order
+ $doc_item = new Document_Item();
+ $doc_item->add([
+ 'documents_id' => $doc_id,
+ 'itemtype' => PluginOrderOrder::class,
+ 'items_id' => $order->getID(),
+ 'entities_id' => $order->fields['entities_id'],
+ ]);
+
+ return $doc_id;
+ }
+
+
+ /**
+ * Stream a document file as a download response.
+ *
+ * @param int $doc_id GLPI Document ID
+ * @return void
+ */
+ public static function downloadDocument(int $doc_id): void
+ {
+ $doc = new Document();
+ if (!$doc->getFromDB($doc_id)) {
+ return;
+ }
+
+ $filepath = GLPI_DOC_DIR . '/' . $doc->fields['filepath'];
+ if (!file_exists($filepath)) {
+ return;
+ }
+
+ $filename = $doc->fields['filename'];
+ $mime = $doc->fields['mime'] ?: 'application/octet-stream';
+
+ header('Content-Type: ' . $mime);
+ header('Content-Disposition: attachment; filename="' . addslashes($filename) . '"');
+ header('Content-Length: ' . filesize($filepath));
+ header('Cache-Control: private, max-age=0, must-revalidate');
+ header('Pragma: public');
+
+ readfile($filepath);
+ }
+}
From c374176bdbf3b6233b19d2116e3693b7ea046af4 Mon Sep 17 00:00:00 2001
From: geek95dg <52536778+geek95dg@users.noreply.github.com>
Date: Wed, 1 Apr 2026 13:06:09 +0200
Subject: [PATCH 05/14] Update README.md with new features and changelog
Document GLPI 11+ custom assets support and OT protocol PDF generation
features in the feature list and add a changelog section.
https://claude.ai/code/session_01RHQ2rYmPwHepbdTrCpsDCZ
---
README.md | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index d411d7e511..4f76d0842e 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,8 @@ This plugin allows you to manage order management within GLPIi:
- Products references management
- Order management (with approval workflow)
- Budgets management
+- GLPI 11+ user-defined custom assets support as product references
+- OT (fixed assets protocol) PDF generation from orders
## Documentation
@@ -34,6 +36,16 @@ The GLPI Network services are available through our [Partner's Network](http://w
Obtain a personalized service experience, associated with benefits and opportunities.
+## Changelog
+
+### 2.0.0 (Unreleased)
+
+**New Features**
+
+- **GLPI 11+ Custom Assets Support**: User-defined custom assets (stored in `glpi_assets_assets`) are now dynamically discovered and available as product references. Type, Model, and Template dropdowns work correctly for custom asset classes, including proper scoping via `assets_assetdefinitions_id`.
+
+- **OT Protocol PDF Generation**: Generate OT (fixed assets protocol) documents directly from orders. Accessible via the "Generate OT" action in the Actions dropdown. Prompts for a Cost Center (MPK) value, then generates a PDF containing all delivered items with their serial numbers, values, and delivery dates. Uses a system binary fallback chain (wkhtmltopdf → Chromium → mPDF → HTML). Generated documents are saved to the GLPI document system and linked to the order.
+
## Contributing
* Open a ticket for each bug/feature so it can be discussed
@@ -46,4 +58,3 @@ Obtain a personalized service experience, associated with benefits and opportuni
* **Code**: you can redistribute it and/or modify
it under the terms of the GNU General Public License ([GPL-2.0](https://www.gnu.org/licenses/gpl-2.0.en.html)).
-
From 2bd9cc500d62c52a8db96aa196f27c1956c47cb3 Mon Sep 17 00:00:00 2001
From: Claude
Date: Wed, 1 Apr 2026 12:50:29 +0000
Subject: [PATCH 06/14] Include vendor directory in repository for direct
deployment
Remove vendor/ from .gitignore and commit composer dependencies
so the plugin works when deployed directly from git to
glpi/plugins/order/ without requiring composer install.
https://claude.ai/code/session_01RHQ2rYmPwHepbdTrCpsDCZ
---
.gitignore | 1 -
vendor/autoload.php | 22 +
vendor/composer/ClassLoader.php | 579 ++
vendor/composer/InstalledVersions.php | 396 ++
vendor/composer/LICENSE | 21 +
vendor/composer/autoload_classmap.php | 21 +
vendor/composer/autoload_files.php | 10 +
vendor/composer/autoload_namespaces.php | 9 +
vendor/composer/autoload_psr4.php | 10 +
vendor/composer/autoload_real.php | 50 +
vendor/composer/autoload_static.php | 51 +
vendor/composer/installed.json | 63 +
vendor/composer/installed.php | 32 +
vendor/composer/platform_check.php | 25 +
vendor/sboden/odtphp/.gitattributes | 65 +
vendor/sboden/odtphp/.gitignore | 11 +
vendor/sboden/odtphp/LICENSE | 674 ++
vendor/sboden/odtphp/README.md | 494 ++
vendor/sboden/odtphp/composer.json | 45 +
.../documentation/odtphp_documentation.pdf | Bin 0 -> 313864 bytes
vendor/sboden/odtphp/examples/form.php | 67 +
.../sboden/odtphp/examples/form_template.odt | Bin 0 -> 9381 bytes
.../sboden/odtphp/examples/images/anaska.gif | Bin 0 -> 4852 bytes
.../sboden/odtphp/examples/images/anaska.jpg | Bin 0 -> 41431 bytes
.../sboden/odtphp/examples/images/apache.gif | Bin 0 -> 16647 bytes
.../sboden/odtphp/examples/images/mysql.gif | Bin 0 -> 1957 bytes
vendor/sboden/odtphp/examples/images/php.gif | Bin 0 -> 2523 bytes
vendor/sboden/odtphp/examples/simplecheck.odt | Bin 0 -> 9307 bytes
vendor/sboden/odtphp/examples/simplecheck.php | 110 +
vendor/sboden/odtphp/examples/tutorial1.odt | Bin 0 -> 7921 bytes
vendor/sboden/odtphp/examples/tutorial1.php | 39 +
vendor/sboden/odtphp/examples/tutorial2.odt | Bin 0 -> 7859 bytes
vendor/sboden/odtphp/examples/tutorial2.php | 35 +
vendor/sboden/odtphp/examples/tutorial3.odt | Bin 0 -> 8978 bytes
vendor/sboden/odtphp/examples/tutorial3.php | 54 +
vendor/sboden/odtphp/examples/tutorial4.odt | Bin 0 -> 10880 bytes
vendor/sboden/odtphp/examples/tutorial4.php | 43 +
vendor/sboden/odtphp/examples/tutorial5.odt | Bin 0 -> 11750 bytes
vendor/sboden/odtphp/examples/tutorial5.php | 58 +
vendor/sboden/odtphp/examples/tutorial6.odt | Bin 0 -> 9574 bytes
vendor/sboden/odtphp/examples/tutorial6.php | 54 +
vendor/sboden/odtphp/examples/tutorial7.odt | Bin 0 -> 7841 bytes
vendor/sboden/odtphp/examples/tutorial7.php | 40 +
vendor/sboden/odtphp/lib/pclzip.lib.php | 5464 +++++++++++++++++
vendor/sboden/odtphp/phpunit-runner.php | 36 +
vendor/sboden/odtphp/phpunit.xml | 26 +
vendor/sboden/odtphp/run-tests.bat | 8 +
vendor/sboden/odtphp/run-tests.sh | 7 +
.../src/Attributes/AllowDynamicProperties.php | 18 +
.../odtphp/src/Exceptions/OdfException.php | 12 +
.../src/Exceptions/PclZipProxyException.php | 12 +
.../src/Exceptions/PhpZipProxyException.php | 12 +
.../src/Exceptions/SegmentException.php | 12 +
vendor/sboden/odtphp/src/Odf.php | 951 +++
vendor/sboden/odtphp/src/Segment.php | 381 ++
vendor/sboden/odtphp/src/SegmentIterator.php | 73 +
vendor/sboden/odtphp/src/Zip/PclZipProxy.php | 249 +
vendor/sboden/odtphp/src/Zip/PhpZipProxy.php | 122 +
vendor/sboden/odtphp/src/Zip/ZipInterface.php | 74 +
vendor/sboden/odtphp/tests/src/Basic1Test.php | 56 +
vendor/sboden/odtphp/tests/src/Basic2Test.php | 54 +
vendor/sboden/odtphp/tests/src/BasicTest1.odt | Bin 0 -> 9857 bytes
vendor/sboden/odtphp/tests/src/ConfigTest.php | 45 +
.../tests/src/CustomPropertyExistsTest.php | 91 +
.../sboden/odtphp/tests/src/EdgeCaseTest.php | 177 +
vendor/sboden/odtphp/tests/src/ImageTest.php | 124 +
.../tests/src/NullValueRegressionTest.php | 235 +
.../sboden/odtphp/tests/src/OdtTestCase.php | 100 +
.../src/RecursiveHtmlspecialcharsTest.php | 315 +
.../tests/src/SetCustomPropertyTest.php | 168 +
.../sboden/odtphp/tests/src/VariableTest.php | 46 +
.../gold_pclzip/AdvancedImageTestGold.odt | Bin 0 -> 229879 bytes
.../src/files/gold_pclzip/BasicTest1Gold.odt | Bin 0 -> 8029 bytes
.../src/files/gold_pclzip/BasicTest2Gold.odt | Bin 0 -> 48311 bytes
.../src/files/gold_pclzip/ConfigTestGold.odt | Bin 0 -> 9264 bytes
.../files/gold_pclzip/EncodingTestGold.odt | Bin 0 -> 10205 bytes
.../src/files/gold_pclzip/ImageTestGold.odt | Bin 0 -> 313871 bytes
.../files/gold_pclzip/ImageTestOutputGold.odt | Bin 0 -> 313871 bytes
.../gold_pclzip/ImageTestResizedGold.odt | Bin 0 -> 313863 bytes
.../ImageTestResizedOutputGold.odt | Bin 0 -> 11086 bytes
.../gold_pclzip/LargeVariableTestGold.odt | Bin 0 -> 10532 bytes
.../gold_pclzip/NestedSegmentTestGold.odt | Bin 0 -> 12362 bytes
.../gold_pclzip/SetCustomPropertyTestGold.odt | Bin 0 -> 11086 bytes
.../files/gold_pclzip/VariableTestGold.odt | 8 +
.../gold_phpzip/AdvancedImageTestGold.odt | Bin 0 -> 229879 bytes
.../src/files/gold_phpzip/BasicTest1Gold.odt | Bin 0 -> 8029 bytes
.../src/files/gold_phpzip/BasicTest2Gold.odt | Bin 0 -> 48311 bytes
.../src/files/gold_phpzip/ConfigTestGold.odt | Bin 0 -> 9264 bytes
.../files/gold_phpzip/EncodingTestGold.odt | Bin 0 -> 10205 bytes
.../files/gold_phpzip/EncodingTestPhpGold.odt | Bin 0 -> 10205 bytes
.../src/files/gold_phpzip/ImageTestGold.odt | Bin 0 -> 101609 bytes
.../gold_phpzip/ImageTestResizedGold.odt | Bin 0 -> 138497 bytes
.../gold_phpzip/ImageTestResizedMmGold.odt | Bin 0 -> 138517 bytes
.../ImageTestResizedMmOffsetGold.odt | Bin 0 -> 138525 bytes
.../ImageTestResizedMmOffsetIgnoredGold.odt | Bin 0 -> 138517 bytes
.../gold_phpzip/ImageTestResizedPixelGold.odt | Bin 0 -> 138517 bytes
.../ImageTestResizedPixelOffsetGold.odt | Bin 0 -> 138532 bytes
...ImageTestResizedPixelOffsetIgnoredGold.odt | Bin 0 -> 138517 bytes
.../gold_phpzip/LargeVariableTestGold.odt | Bin 0 -> 10532 bytes
.../gold_phpzip/NestedSegmentTestGold.odt | Bin 0 -> 12362 bytes
.../gold_phpzip/SetCustomPropertyTestGold.odt | Bin 0 -> 11079 bytes
.../files/gold_phpzip/VariableTestGold.odt | 8 +
.../gold_phpzip/VariableTestMultilineGold.odt | Bin 0 -> 9222 bytes
.../VariableTestSpecialCharsGold.odt | Bin 0 -> 9226 bytes
.../odtphp/tests/src/files/images/anaska.gif | Bin 0 -> 4852 bytes
.../odtphp/tests/src/files/images/anaska.jpg | Bin 0 -> 41431 bytes
.../src/files/images/circle_transparent.gif | Bin 0 -> 1244 bytes
.../odtphp/tests/src/files/images/mysql.gif | Bin 0 -> 1957 bytes
.../tests/src/files/images/overschot.jpg | Bin 0 -> 303490 bytes
.../odtphp/tests/src/files/images/php.gif | Bin 0 -> 2523 bytes
.../odtphp/tests/src/files/images/test.jpg | Bin 0 -> 129770 bytes
.../tests/src/files/images/voronoi_sphere.png | Bin 0 -> 92703 bytes
.../tests/src/files/input/BasicTest1.odt | Bin 0 -> 7921 bytes
.../tests/src/files/input/BasicTest2.odt | Bin 0 -> 7859 bytes
.../tests/src/files/input/ConfigTest.odt | Bin 0 -> 9385 bytes
.../src/files/input/CustomPropertiesTest.odt | Bin 0 -> 11193 bytes
.../files/input/CustomPropertyExistsTest.odt | Bin 0 -> 11364 bytes
.../tests/src/files/input/EncodingTest.odt | Bin 0 -> 10276 bytes
.../tests/src/files/input/ImageTest.odt | Bin 0 -> 11679 bytes
.../src/files/input/LargeVariableTest.odt | Bin 0 -> 10367 bytes
.../src/files/input/LargeVariableTest1.odt | 1 +
.../src/files/input/NestedSegmentTest.odt | Bin 0 -> 12468 bytes
.../tests/src/files/input/VariableTest.odt | Bin 0 -> 9331 bytes
.../odtphp/tests/src/files/output/.gitkeep | 0
124 files changed, 11963 insertions(+), 1 deletion(-)
create mode 100644 vendor/autoload.php
create mode 100644 vendor/composer/ClassLoader.php
create mode 100644 vendor/composer/InstalledVersions.php
create mode 100644 vendor/composer/LICENSE
create mode 100644 vendor/composer/autoload_classmap.php
create mode 100644 vendor/composer/autoload_files.php
create mode 100644 vendor/composer/autoload_namespaces.php
create mode 100644 vendor/composer/autoload_psr4.php
create mode 100644 vendor/composer/autoload_real.php
create mode 100644 vendor/composer/autoload_static.php
create mode 100644 vendor/composer/installed.json
create mode 100644 vendor/composer/installed.php
create mode 100644 vendor/composer/platform_check.php
create mode 100644 vendor/sboden/odtphp/.gitattributes
create mode 100644 vendor/sboden/odtphp/.gitignore
create mode 100755 vendor/sboden/odtphp/LICENSE
create mode 100644 vendor/sboden/odtphp/README.md
create mode 100755 vendor/sboden/odtphp/composer.json
create mode 100755 vendor/sboden/odtphp/documentation/odtphp_documentation.pdf
create mode 100755 vendor/sboden/odtphp/examples/form.php
create mode 100755 vendor/sboden/odtphp/examples/form_template.odt
create mode 100755 vendor/sboden/odtphp/examples/images/anaska.gif
create mode 100755 vendor/sboden/odtphp/examples/images/anaska.jpg
create mode 100755 vendor/sboden/odtphp/examples/images/apache.gif
create mode 100755 vendor/sboden/odtphp/examples/images/mysql.gif
create mode 100755 vendor/sboden/odtphp/examples/images/php.gif
create mode 100755 vendor/sboden/odtphp/examples/simplecheck.odt
create mode 100755 vendor/sboden/odtphp/examples/simplecheck.php
create mode 100755 vendor/sboden/odtphp/examples/tutorial1.odt
create mode 100755 vendor/sboden/odtphp/examples/tutorial1.php
create mode 100755 vendor/sboden/odtphp/examples/tutorial2.odt
create mode 100755 vendor/sboden/odtphp/examples/tutorial2.php
create mode 100755 vendor/sboden/odtphp/examples/tutorial3.odt
create mode 100755 vendor/sboden/odtphp/examples/tutorial3.php
create mode 100755 vendor/sboden/odtphp/examples/tutorial4.odt
create mode 100755 vendor/sboden/odtphp/examples/tutorial4.php
create mode 100755 vendor/sboden/odtphp/examples/tutorial5.odt
create mode 100755 vendor/sboden/odtphp/examples/tutorial5.php
create mode 100755 vendor/sboden/odtphp/examples/tutorial6.odt
create mode 100755 vendor/sboden/odtphp/examples/tutorial6.php
create mode 100755 vendor/sboden/odtphp/examples/tutorial7.odt
create mode 100755 vendor/sboden/odtphp/examples/tutorial7.php
create mode 100755 vendor/sboden/odtphp/lib/pclzip.lib.php
create mode 100755 vendor/sboden/odtphp/phpunit-runner.php
create mode 100755 vendor/sboden/odtphp/phpunit.xml
create mode 100755 vendor/sboden/odtphp/run-tests.bat
create mode 100755 vendor/sboden/odtphp/run-tests.sh
create mode 100755 vendor/sboden/odtphp/src/Attributes/AllowDynamicProperties.php
create mode 100755 vendor/sboden/odtphp/src/Exceptions/OdfException.php
create mode 100755 vendor/sboden/odtphp/src/Exceptions/PclZipProxyException.php
create mode 100755 vendor/sboden/odtphp/src/Exceptions/PhpZipProxyException.php
create mode 100755 vendor/sboden/odtphp/src/Exceptions/SegmentException.php
create mode 100755 vendor/sboden/odtphp/src/Odf.php
create mode 100755 vendor/sboden/odtphp/src/Segment.php
create mode 100755 vendor/sboden/odtphp/src/SegmentIterator.php
create mode 100755 vendor/sboden/odtphp/src/Zip/PclZipProxy.php
create mode 100755 vendor/sboden/odtphp/src/Zip/PhpZipProxy.php
create mode 100755 vendor/sboden/odtphp/src/Zip/ZipInterface.php
create mode 100755 vendor/sboden/odtphp/tests/src/Basic1Test.php
create mode 100755 vendor/sboden/odtphp/tests/src/Basic2Test.php
create mode 100755 vendor/sboden/odtphp/tests/src/BasicTest1.odt
create mode 100755 vendor/sboden/odtphp/tests/src/ConfigTest.php
create mode 100644 vendor/sboden/odtphp/tests/src/CustomPropertyExistsTest.php
create mode 100755 vendor/sboden/odtphp/tests/src/EdgeCaseTest.php
create mode 100755 vendor/sboden/odtphp/tests/src/ImageTest.php
create mode 100644 vendor/sboden/odtphp/tests/src/NullValueRegressionTest.php
create mode 100755 vendor/sboden/odtphp/tests/src/OdtTestCase.php
create mode 100644 vendor/sboden/odtphp/tests/src/RecursiveHtmlspecialcharsTest.php
create mode 100644 vendor/sboden/odtphp/tests/src/SetCustomPropertyTest.php
create mode 100755 vendor/sboden/odtphp/tests/src/VariableTest.php
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_pclzip/AdvancedImageTestGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_pclzip/BasicTest1Gold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_pclzip/BasicTest2Gold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_pclzip/ConfigTestGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_pclzip/EncodingTestGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_pclzip/ImageTestGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_pclzip/ImageTestOutputGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_pclzip/ImageTestResizedGold.odt
create mode 100644 vendor/sboden/odtphp/tests/src/files/gold_pclzip/ImageTestResizedOutputGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_pclzip/LargeVariableTestGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_pclzip/NestedSegmentTestGold.odt
create mode 100644 vendor/sboden/odtphp/tests/src/files/gold_pclzip/SetCustomPropertyTestGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_pclzip/VariableTestGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_phpzip/AdvancedImageTestGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_phpzip/BasicTest1Gold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_phpzip/BasicTest2Gold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_phpzip/ConfigTestGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_phpzip/EncodingTestGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_phpzip/EncodingTestPhpGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedGold.odt
create mode 100644 vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedMmGold.odt
create mode 100644 vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedMmOffsetGold.odt
create mode 100644 vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedMmOffsetIgnoredGold.odt
create mode 100644 vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedPixelGold.odt
create mode 100644 vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedPixelOffsetGold.odt
create mode 100644 vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedPixelOffsetIgnoredGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_phpzip/LargeVariableTestGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_phpzip/NestedSegmentTestGold.odt
create mode 100644 vendor/sboden/odtphp/tests/src/files/gold_phpzip/SetCustomPropertyTestGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_phpzip/VariableTestGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_phpzip/VariableTestMultilineGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/gold_phpzip/VariableTestSpecialCharsGold.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/images/anaska.gif
create mode 100755 vendor/sboden/odtphp/tests/src/files/images/anaska.jpg
create mode 100755 vendor/sboden/odtphp/tests/src/files/images/circle_transparent.gif
create mode 100755 vendor/sboden/odtphp/tests/src/files/images/mysql.gif
create mode 100755 vendor/sboden/odtphp/tests/src/files/images/overschot.jpg
create mode 100755 vendor/sboden/odtphp/tests/src/files/images/php.gif
create mode 100755 vendor/sboden/odtphp/tests/src/files/images/test.jpg
create mode 100755 vendor/sboden/odtphp/tests/src/files/images/voronoi_sphere.png
create mode 100755 vendor/sboden/odtphp/tests/src/files/input/BasicTest1.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/input/BasicTest2.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/input/ConfigTest.odt
create mode 100644 vendor/sboden/odtphp/tests/src/files/input/CustomPropertiesTest.odt
create mode 100644 vendor/sboden/odtphp/tests/src/files/input/CustomPropertyExistsTest.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/input/EncodingTest.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/input/ImageTest.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/input/LargeVariableTest.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/input/LargeVariableTest1.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/input/NestedSegmentTest.odt
create mode 100755 vendor/sboden/odtphp/tests/src/files/input/VariableTest.odt
create mode 100644 vendor/sboden/odtphp/tests/src/files/output/.gitkeep
diff --git a/.gitignore b/.gitignore
index 044ece9716..433abf068e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
dist/
-vendor/
.gh_token
*.min.*
var/
diff --git a/vendor/autoload.php b/vendor/autoload.php
new file mode 100644
index 0000000000..272e155647
--- /dev/null
+++ b/vendor/autoload.php
@@ -0,0 +1,22 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ * $loader = new \Composer\Autoload\ClassLoader();
+ *
+ * // register classes with namespaces
+ * $loader->add('Symfony\Component', __DIR__.'/component');
+ * $loader->add('Symfony', __DIR__.'/framework');
+ *
+ * // activate the autoloader
+ * $loader->register();
+ *
+ * // to enable searching the include path (eg. for PEAR packages)
+ * $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier
+ * @author Jordi Boggiano
+ * @see https://www.php-fig.org/psr/psr-0/
+ * @see https://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+ /** @var \Closure(string):void */
+ private static $includeFile;
+
+ /** @var string|null */
+ private $vendorDir;
+
+ // PSR-4
+ /**
+ * @var array>
+ */
+ private $prefixLengthsPsr4 = array();
+ /**
+ * @var array>
+ */
+ private $prefixDirsPsr4 = array();
+ /**
+ * @var list
+ */
+ private $fallbackDirsPsr4 = array();
+
+ // PSR-0
+ /**
+ * List of PSR-0 prefixes
+ *
+ * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
+ *
+ * @var array>>
+ */
+ private $prefixesPsr0 = array();
+ /**
+ * @var list
+ */
+ private $fallbackDirsPsr0 = array();
+
+ /** @var bool */
+ private $useIncludePath = false;
+
+ /**
+ * @var array
+ */
+ private $classMap = array();
+
+ /** @var bool */
+ private $classMapAuthoritative = false;
+
+ /**
+ * @var array
+ */
+ private $missingClasses = array();
+
+ /** @var string|null */
+ private $apcuPrefix;
+
+ /**
+ * @var array
+ */
+ private static $registeredLoaders = array();
+
+ /**
+ * @param string|null $vendorDir
+ */
+ public function __construct($vendorDir = null)
+ {
+ $this->vendorDir = $vendorDir;
+ self::initializeIncludeClosure();
+ }
+
+ /**
+ * @return array>
+ */
+ public function getPrefixes()
+ {
+ if (!empty($this->prefixesPsr0)) {
+ return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
+ }
+
+ return array();
+ }
+
+ /**
+ * @return array>
+ */
+ public function getPrefixesPsr4()
+ {
+ return $this->prefixDirsPsr4;
+ }
+
+ /**
+ * @return list
+ */
+ public function getFallbackDirs()
+ {
+ return $this->fallbackDirsPsr0;
+ }
+
+ /**
+ * @return list
+ */
+ public function getFallbackDirsPsr4()
+ {
+ return $this->fallbackDirsPsr4;
+ }
+
+ /**
+ * @return array Array of classname => path
+ */
+ public function getClassMap()
+ {
+ return $this->classMap;
+ }
+
+ /**
+ * @param array $classMap Class to filename map
+ *
+ * @return void
+ */
+ public function addClassMap(array $classMap)
+ {
+ if ($this->classMap) {
+ $this->classMap = array_merge($this->classMap, $classMap);
+ } else {
+ $this->classMap = $classMap;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix, either
+ * appending or prepending to the ones previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param list|string $paths The PSR-0 root directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @return void
+ */
+ public function add($prefix, $paths, $prepend = false)
+ {
+ $paths = (array) $paths;
+ if (!$prefix) {
+ if ($prepend) {
+ $this->fallbackDirsPsr0 = array_merge(
+ $paths,
+ $this->fallbackDirsPsr0
+ );
+ } else {
+ $this->fallbackDirsPsr0 = array_merge(
+ $this->fallbackDirsPsr0,
+ $paths
+ );
+ }
+
+ return;
+ }
+
+ $first = $prefix[0];
+ if (!isset($this->prefixesPsr0[$first][$prefix])) {
+ $this->prefixesPsr0[$first][$prefix] = $paths;
+
+ return;
+ }
+ if ($prepend) {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ $paths,
+ $this->prefixesPsr0[$first][$prefix]
+ );
+ } else {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ $this->prefixesPsr0[$first][$prefix],
+ $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace, either
+ * appending or prepending to the ones previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param list|string $paths The PSR-4 base directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @return void
+ */
+ public function addPsr4($prefix, $paths, $prepend = false)
+ {
+ $paths = (array) $paths;
+ if (!$prefix) {
+ // Register directories for the root namespace.
+ if ($prepend) {
+ $this->fallbackDirsPsr4 = array_merge(
+ $paths,
+ $this->fallbackDirsPsr4
+ );
+ } else {
+ $this->fallbackDirsPsr4 = array_merge(
+ $this->fallbackDirsPsr4,
+ $paths
+ );
+ }
+ } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+ // Register directories for a new namespace.
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = $paths;
+ } elseif ($prepend) {
+ // Prepend directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ $paths,
+ $this->prefixDirsPsr4[$prefix]
+ );
+ } else {
+ // Append directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ $this->prefixDirsPsr4[$prefix],
+ $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix,
+ * replacing any others previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param list|string $paths The PSR-0 base directories
+ *
+ * @return void
+ */
+ public function set($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr0 = (array) $paths;
+ } else {
+ $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace,
+ * replacing any others previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param list|string $paths The PSR-4 base directories
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @return void
+ */
+ public function setPsr4($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr4 = (array) $paths;
+ } else {
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Turns on searching the include path for class files.
+ *
+ * @param bool $useIncludePath
+ *
+ * @return void
+ */
+ public function setUseIncludePath($useIncludePath)
+ {
+ $this->useIncludePath = $useIncludePath;
+ }
+
+ /**
+ * Can be used to check if the autoloader uses the include path to check
+ * for classes.
+ *
+ * @return bool
+ */
+ public function getUseIncludePath()
+ {
+ return $this->useIncludePath;
+ }
+
+ /**
+ * Turns off searching the prefix and fallback directories for classes
+ * that have not been registered with the class map.
+ *
+ * @param bool $classMapAuthoritative
+ *
+ * @return void
+ */
+ public function setClassMapAuthoritative($classMapAuthoritative)
+ {
+ $this->classMapAuthoritative = $classMapAuthoritative;
+ }
+
+ /**
+ * Should class lookup fail if not found in the current class map?
+ *
+ * @return bool
+ */
+ public function isClassMapAuthoritative()
+ {
+ return $this->classMapAuthoritative;
+ }
+
+ /**
+ * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
+ *
+ * @param string|null $apcuPrefix
+ *
+ * @return void
+ */
+ public function setApcuPrefix($apcuPrefix)
+ {
+ $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
+ }
+
+ /**
+ * The APCu prefix in use, or null if APCu caching is not enabled.
+ *
+ * @return string|null
+ */
+ public function getApcuPrefix()
+ {
+ return $this->apcuPrefix;
+ }
+
+ /**
+ * Registers this instance as an autoloader.
+ *
+ * @param bool $prepend Whether to prepend the autoloader or not
+ *
+ * @return void
+ */
+ public function register($prepend = false)
+ {
+ spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+
+ if (null === $this->vendorDir) {
+ return;
+ }
+
+ if ($prepend) {
+ self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
+ } else {
+ unset(self::$registeredLoaders[$this->vendorDir]);
+ self::$registeredLoaders[$this->vendorDir] = $this;
+ }
+ }
+
+ /**
+ * Unregisters this instance as an autoloader.
+ *
+ * @return void
+ */
+ public function unregister()
+ {
+ spl_autoload_unregister(array($this, 'loadClass'));
+
+ if (null !== $this->vendorDir) {
+ unset(self::$registeredLoaders[$this->vendorDir]);
+ }
+ }
+
+ /**
+ * Loads the given class or interface.
+ *
+ * @param string $class The name of the class
+ * @return true|null True if loaded, null otherwise
+ */
+ public function loadClass($class)
+ {
+ if ($file = $this->findFile($class)) {
+ $includeFile = self::$includeFile;
+ $includeFile($file);
+
+ return true;
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds the path to the file where the class is defined.
+ *
+ * @param string $class The name of the class
+ *
+ * @return string|false The path if found, false otherwise
+ */
+ public function findFile($class)
+ {
+ // class map lookup
+ if (isset($this->classMap[$class])) {
+ return $this->classMap[$class];
+ }
+ if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
+ return false;
+ }
+ if (null !== $this->apcuPrefix) {
+ $file = apcu_fetch($this->apcuPrefix.$class, $hit);
+ if ($hit) {
+ return $file;
+ }
+ }
+
+ $file = $this->findFileWithExtension($class, '.php');
+
+ // Search for Hack files if we are running on HHVM
+ if (false === $file && defined('HHVM_VERSION')) {
+ $file = $this->findFileWithExtension($class, '.hh');
+ }
+
+ if (null !== $this->apcuPrefix) {
+ apcu_add($this->apcuPrefix.$class, $file);
+ }
+
+ if (false === $file) {
+ // Remember that this class does not exist.
+ $this->missingClasses[$class] = true;
+ }
+
+ return $file;
+ }
+
+ /**
+ * Returns the currently registered loaders keyed by their corresponding vendor directories.
+ *
+ * @return array
+ */
+ public static function getRegisteredLoaders()
+ {
+ return self::$registeredLoaders;
+ }
+
+ /**
+ * @param string $class
+ * @param string $ext
+ * @return string|false
+ */
+ private function findFileWithExtension($class, $ext)
+ {
+ // PSR-4 lookup
+ $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+ $first = $class[0];
+ if (isset($this->prefixLengthsPsr4[$first])) {
+ $subPath = $class;
+ while (false !== $lastPos = strrpos($subPath, '\\')) {
+ $subPath = substr($subPath, 0, $lastPos);
+ $search = $subPath . '\\';
+ if (isset($this->prefixDirsPsr4[$search])) {
+ $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
+ foreach ($this->prefixDirsPsr4[$search] as $dir) {
+ if (file_exists($file = $dir . $pathEnd)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-4 fallback dirs
+ foreach ($this->fallbackDirsPsr4 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 lookup
+ if (false !== $pos = strrpos($class, '\\')) {
+ // namespaced class name
+ $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+ . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+ } else {
+ // PEAR-like class name
+ $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+ }
+
+ if (isset($this->prefixesPsr0[$first])) {
+ foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+ if (0 === strpos($class, $prefix)) {
+ foreach ($dirs as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-0 fallback dirs
+ foreach ($this->fallbackDirsPsr0 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 include paths.
+ if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+ return $file;
+ }
+
+ return false;
+ }
+
+ /**
+ * @return void
+ */
+ private static function initializeIncludeClosure()
+ {
+ if (self::$includeFile !== null) {
+ return;
+ }
+
+ /**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ *
+ * @param string $file
+ * @return void
+ */
+ self::$includeFile = \Closure::bind(static function($file) {
+ include $file;
+ }, null, null);
+ }
+}
diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php
new file mode 100644
index 0000000000..2052022fd8
--- /dev/null
+++ b/vendor/composer/InstalledVersions.php
@@ -0,0 +1,396 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer;
+
+use Composer\Autoload\ClassLoader;
+use Composer\Semver\VersionParser;
+
+/**
+ * This class is copied in every Composer installed project and available to all
+ *
+ * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
+ *
+ * To require its presence, you can require `composer-runtime-api ^2.0`
+ *
+ * @final
+ */
+class InstalledVersions
+{
+ /**
+ * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
+ * @internal
+ */
+ private static $selfDir = null;
+
+ /**
+ * @var mixed[]|null
+ * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null
+ */
+ private static $installed;
+
+ /**
+ * @var bool
+ */
+ private static $installedIsLocalDir;
+
+ /**
+ * @var bool|null
+ */
+ private static $canGetVendors;
+
+ /**
+ * @var array[]
+ * @psalm-var array}>
+ */
+ private static $installedByVendor = array();
+
+ /**
+ * Returns a list of all package names which are present, either by being installed, replaced or provided
+ *
+ * @return string[]
+ * @psalm-return list
+ */
+ public static function getInstalledPackages()
+ {
+ $packages = array();
+ foreach (self::getInstalled() as $installed) {
+ $packages[] = array_keys($installed['versions']);
+ }
+
+ if (1 === \count($packages)) {
+ return $packages[0];
+ }
+
+ return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
+ }
+
+ /**
+ * Returns a list of all package names with a specific type e.g. 'library'
+ *
+ * @param string $type
+ * @return string[]
+ * @psalm-return list
+ */
+ public static function getInstalledPackagesByType($type)
+ {
+ $packagesByType = array();
+
+ foreach (self::getInstalled() as $installed) {
+ foreach ($installed['versions'] as $name => $package) {
+ if (isset($package['type']) && $package['type'] === $type) {
+ $packagesByType[] = $name;
+ }
+ }
+ }
+
+ return $packagesByType;
+ }
+
+ /**
+ * Checks whether the given package is installed
+ *
+ * This also returns true if the package name is provided or replaced by another package
+ *
+ * @param string $packageName
+ * @param bool $includeDevRequirements
+ * @return bool
+ */
+ public static function isInstalled($packageName, $includeDevRequirements = true)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (isset($installed['versions'][$packageName])) {
+ return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks whether the given package satisfies a version constraint
+ *
+ * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
+ *
+ * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
+ *
+ * @param VersionParser $parser Install composer/semver to have access to this class and functionality
+ * @param string $packageName
+ * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
+ * @return bool
+ */
+ public static function satisfies(VersionParser $parser, $packageName, $constraint)
+ {
+ $constraint = $parser->parseConstraints((string) $constraint);
+ $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
+
+ return $provided->matches($constraint);
+ }
+
+ /**
+ * Returns a version constraint representing all the range(s) which are installed for a given package
+ *
+ * It is easier to use this via isInstalled() with the $constraint argument if you need to check
+ * whether a given version of a package is installed, and not just whether it exists
+ *
+ * @param string $packageName
+ * @return string Version constraint usable with composer/semver
+ */
+ public static function getVersionRanges($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ $ranges = array();
+ if (isset($installed['versions'][$packageName]['pretty_version'])) {
+ $ranges[] = $installed['versions'][$packageName]['pretty_version'];
+ }
+ if (array_key_exists('aliases', $installed['versions'][$packageName])) {
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
+ }
+ if (array_key_exists('replaced', $installed['versions'][$packageName])) {
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
+ }
+ if (array_key_exists('provided', $installed['versions'][$packageName])) {
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
+ }
+
+ return implode(' || ', $ranges);
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+ */
+ public static function getVersion($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ if (!isset($installed['versions'][$packageName]['version'])) {
+ return null;
+ }
+
+ return $installed['versions'][$packageName]['version'];
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+ */
+ public static function getPrettyVersion($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ if (!isset($installed['versions'][$packageName]['pretty_version'])) {
+ return null;
+ }
+
+ return $installed['versions'][$packageName]['pretty_version'];
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
+ */
+ public static function getReference($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ if (!isset($installed['versions'][$packageName]['reference'])) {
+ return null;
+ }
+
+ return $installed['versions'][$packageName]['reference'];
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
+ */
+ public static function getInstallPath($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @return array
+ * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
+ */
+ public static function getRootPackage()
+ {
+ $installed = self::getInstalled();
+
+ return $installed[0]['root'];
+ }
+
+ /**
+ * Returns the raw installed.php data for custom implementations
+ *
+ * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
+ * @return array[]
+ * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}
+ */
+ public static function getRawData()
+ {
+ @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
+
+ if (null === self::$installed) {
+ // only require the installed.php file if this file is loaded from its dumped location,
+ // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+ if (substr(__DIR__, -8, 1) !== 'C') {
+ self::$installed = include __DIR__ . '/installed.php';
+ } else {
+ self::$installed = array();
+ }
+ }
+
+ return self::$installed;
+ }
+
+ /**
+ * Returns the raw data of all installed.php which are currently loaded for custom implementations
+ *
+ * @return array[]
+ * @psalm-return list}>
+ */
+ public static function getAllRawData()
+ {
+ return self::getInstalled();
+ }
+
+ /**
+ * Lets you reload the static array from another file
+ *
+ * This is only useful for complex integrations in which a project needs to use
+ * this class but then also needs to execute another project's autoloader in process,
+ * and wants to ensure both projects have access to their version of installed.php.
+ *
+ * A typical case would be PHPUnit, where it would need to make sure it reads all
+ * the data it needs from this class, then call reload() with
+ * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
+ * the project in which it runs can then also use this class safely, without
+ * interference between PHPUnit's dependencies and the project's dependencies.
+ *
+ * @param array[] $data A vendor/composer/installed.php data set
+ * @return void
+ *
+ * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data
+ */
+ public static function reload($data)
+ {
+ self::$installed = $data;
+ self::$installedByVendor = array();
+
+ // when using reload, we disable the duplicate protection to ensure that self::$installed data is
+ // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
+ // so we have to assume it does not, and that may result in duplicate data being returned when listing
+ // all installed packages for example
+ self::$installedIsLocalDir = false;
+ }
+
+ /**
+ * @return string
+ */
+ private static function getSelfDir()
+ {
+ if (self::$selfDir === null) {
+ self::$selfDir = strtr(__DIR__, '\\', '/');
+ }
+
+ return self::$selfDir;
+ }
+
+ /**
+ * @return array[]
+ * @psalm-return list}>
+ */
+ private static function getInstalled()
+ {
+ if (null === self::$canGetVendors) {
+ self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
+ }
+
+ $installed = array();
+ $copiedLocalDir = false;
+
+ if (self::$canGetVendors) {
+ $selfDir = self::getSelfDir();
+ foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+ $vendorDir = strtr($vendorDir, '\\', '/');
+ if (isset(self::$installedByVendor[$vendorDir])) {
+ $installed[] = self::$installedByVendor[$vendorDir];
+ } elseif (is_file($vendorDir.'/composer/installed.php')) {
+ /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */
+ $required = require $vendorDir.'/composer/installed.php';
+ self::$installedByVendor[$vendorDir] = $required;
+ $installed[] = $required;
+ if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
+ self::$installed = $required;
+ self::$installedIsLocalDir = true;
+ }
+ }
+ if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
+ $copiedLocalDir = true;
+ }
+ }
+ }
+
+ if (null === self::$installed) {
+ // only require the installed.php file if this file is loaded from its dumped location,
+ // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+ if (substr(__DIR__, -8, 1) !== 'C') {
+ /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */
+ $required = require __DIR__ . '/installed.php';
+ self::$installed = $required;
+ } else {
+ self::$installed = array();
+ }
+ }
+
+ if (self::$installed !== array() && !$copiedLocalDir) {
+ $installed[] = self::$installed;
+ }
+
+ return $installed;
+ }
+}
diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE
new file mode 100644
index 0000000000..f27399a042
--- /dev/null
+++ b/vendor/composer/LICENSE
@@ -0,0 +1,21 @@
+
+Copyright (c) Nils Adermann, Jordi Boggiano
+
+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.
+
diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php
new file mode 100644
index 0000000000..8ed8a70aab
--- /dev/null
+++ b/vendor/composer/autoload_classmap.php
@@ -0,0 +1,21 @@
+ $vendorDir . '/composer/InstalledVersions.php',
+ 'Odtphp\\Attributes\\AllowDynamicProperties' => $vendorDir . '/sboden/odtphp/src/Attributes/AllowDynamicProperties.php',
+ 'Odtphp\\Exceptions\\OdfException' => $vendorDir . '/sboden/odtphp/src/Exceptions/OdfException.php',
+ 'Odtphp\\Exceptions\\PclZipProxyException' => $vendorDir . '/sboden/odtphp/src/Exceptions/PclZipProxyException.php',
+ 'Odtphp\\Exceptions\\PhpZipProxyException' => $vendorDir . '/sboden/odtphp/src/Exceptions/PhpZipProxyException.php',
+ 'Odtphp\\Exceptions\\SegmentException' => $vendorDir . '/sboden/odtphp/src/Exceptions/SegmentException.php',
+ 'Odtphp\\Odf' => $vendorDir . '/sboden/odtphp/src/Odf.php',
+ 'Odtphp\\Segment' => $vendorDir . '/sboden/odtphp/src/Segment.php',
+ 'Odtphp\\SegmentIterator' => $vendorDir . '/sboden/odtphp/src/SegmentIterator.php',
+ 'Odtphp\\Zip\\PclZipProxy' => $vendorDir . '/sboden/odtphp/src/Zip/PclZipProxy.php',
+ 'Odtphp\\Zip\\PhpZipProxy' => $vendorDir . '/sboden/odtphp/src/Zip/PhpZipProxy.php',
+ 'Odtphp\\Zip\\ZipInterface' => $vendorDir . '/sboden/odtphp/src/Zip/ZipInterface.php',
+);
diff --git a/vendor/composer/autoload_files.php b/vendor/composer/autoload_files.php
new file mode 100644
index 0000000000..abe1efd8a6
--- /dev/null
+++ b/vendor/composer/autoload_files.php
@@ -0,0 +1,10 @@
+ $vendorDir . '/sboden/odtphp/lib/pclzip.lib.php',
+);
diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php
new file mode 100644
index 0000000000..15a2ff3ad6
--- /dev/null
+++ b/vendor/composer/autoload_namespaces.php
@@ -0,0 +1,9 @@
+ array($vendorDir . '/sboden/odtphp/src'),
+);
diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php
new file mode 100644
index 0000000000..87617a25fc
--- /dev/null
+++ b/vendor/composer/autoload_real.php
@@ -0,0 +1,50 @@
+register(true);
+
+ $filesToLoad = \Composer\Autoload\ComposerStaticInit477a35eb4fd921399aa876f53273c09d::$files;
+ $requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
+ if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+ $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
+
+ require $file;
+ }
+ }, null, null);
+ foreach ($filesToLoad as $fileIdentifier => $file) {
+ $requireFile($fileIdentifier, $file);
+ }
+
+ return $loader;
+ }
+}
diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php
new file mode 100644
index 0000000000..414bf23242
--- /dev/null
+++ b/vendor/composer/autoload_static.php
@@ -0,0 +1,51 @@
+ __DIR__ . '/..' . '/sboden/odtphp/lib/pclzip.lib.php',
+ );
+
+ public static $prefixLengthsPsr4 = array (
+ 'O' =>
+ array (
+ 'Odtphp\\' => 7,
+ ),
+ );
+
+ public static $prefixDirsPsr4 = array (
+ 'Odtphp\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/sboden/odtphp/src',
+ ),
+ );
+
+ public static $classMap = array (
+ 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+ 'Odtphp\\Attributes\\AllowDynamicProperties' => __DIR__ . '/..' . '/sboden/odtphp/src/Attributes/AllowDynamicProperties.php',
+ 'Odtphp\\Exceptions\\OdfException' => __DIR__ . '/..' . '/sboden/odtphp/src/Exceptions/OdfException.php',
+ 'Odtphp\\Exceptions\\PclZipProxyException' => __DIR__ . '/..' . '/sboden/odtphp/src/Exceptions/PclZipProxyException.php',
+ 'Odtphp\\Exceptions\\PhpZipProxyException' => __DIR__ . '/..' . '/sboden/odtphp/src/Exceptions/PhpZipProxyException.php',
+ 'Odtphp\\Exceptions\\SegmentException' => __DIR__ . '/..' . '/sboden/odtphp/src/Exceptions/SegmentException.php',
+ 'Odtphp\\Odf' => __DIR__ . '/..' . '/sboden/odtphp/src/Odf.php',
+ 'Odtphp\\Segment' => __DIR__ . '/..' . '/sboden/odtphp/src/Segment.php',
+ 'Odtphp\\SegmentIterator' => __DIR__ . '/..' . '/sboden/odtphp/src/SegmentIterator.php',
+ 'Odtphp\\Zip\\PclZipProxy' => __DIR__ . '/..' . '/sboden/odtphp/src/Zip/PclZipProxy.php',
+ 'Odtphp\\Zip\\PhpZipProxy' => __DIR__ . '/..' . '/sboden/odtphp/src/Zip/PhpZipProxy.php',
+ 'Odtphp\\Zip\\ZipInterface' => __DIR__ . '/..' . '/sboden/odtphp/src/Zip/ZipInterface.php',
+ );
+
+ public static function getInitializer(ClassLoader $loader)
+ {
+ return \Closure::bind(function () use ($loader) {
+ $loader->prefixLengthsPsr4 = ComposerStaticInit477a35eb4fd921399aa876f53273c09d::$prefixLengthsPsr4;
+ $loader->prefixDirsPsr4 = ComposerStaticInit477a35eb4fd921399aa876f53273c09d::$prefixDirsPsr4;
+ $loader->classMap = ComposerStaticInit477a35eb4fd921399aa876f53273c09d::$classMap;
+
+ }, null, ClassLoader::class);
+ }
+}
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
new file mode 100644
index 0000000000..4790d1ac03
--- /dev/null
+++ b/vendor/composer/installed.json
@@ -0,0 +1,63 @@
+{
+ "packages": [
+ {
+ "name": "sboden/odtphp",
+ "version": "3.2.1",
+ "version_normalized": "3.2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sboden/odtphp.git",
+ "reference": "989e51fa6edaaf9010b27aa14c5556ca4f14f6f2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sboden/odtphp/zipball/989e51fa6edaaf9010b27aa14c5556ca4f14f6f2",
+ "reference": "989e51fa6edaaf9010b27aa14c5556ca4f14f6f2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+ "drupal/coder": "^8.3",
+ "phpunit/phpunit": "^10.4",
+ "scrutinizer/ocular": "^1.9",
+ "symfony/var-dumper": "^6.4"
+ },
+ "time": "2025-07-24T20:26:09+00:00",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "lib/pclzip.lib.php"
+ ],
+ "psr-4": {
+ "Odtphp\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "GPL-3.0-only"
+ ],
+ "description": "ODT document generator (plugin replacement for cybermonde/odtphp)",
+ "homepage": "https://github.com/sboden/odtphp",
+ "keywords": [
+ "odt",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/sboden/odtphp/issues",
+ "source": "https://github.com/sboden/odtphp/tree/3.2.1"
+ },
+ "install-path": "../sboden/odtphp"
+ }
+ ],
+ "dev": false,
+ "dev-package-names": []
+}
diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php
new file mode 100644
index 0000000000..e2b4cc5381
--- /dev/null
+++ b/vendor/composer/installed.php
@@ -0,0 +1,32 @@
+ array(
+ 'name' => 'glpi-plugin/order',
+ 'pretty_version' => 'dev-main',
+ 'version' => 'dev-main',
+ 'reference' => 'c374176bdbf3b6233b19d2116e3693b7ea046af4',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../../',
+ 'aliases' => array(),
+ 'dev' => false,
+ ),
+ 'versions' => array(
+ 'glpi-plugin/order' => array(
+ 'pretty_version' => 'dev-main',
+ 'version' => 'dev-main',
+ 'reference' => 'c374176bdbf3b6233b19d2116e3693b7ea046af4',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../../',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'sboden/odtphp' => array(
+ 'pretty_version' => '3.2.1',
+ 'version' => '3.2.1.0',
+ 'reference' => '989e51fa6edaaf9010b27aa14c5556ca4f14f6f2',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../sboden/odtphp',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ ),
+);
diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php
new file mode 100644
index 0000000000..14bf88da3a
--- /dev/null
+++ b/vendor/composer/platform_check.php
@@ -0,0 +1,25 @@
+= 80200)) {
+ $issues[] = 'Your Composer dependencies require a PHP version ">= 8.2.0". You are running ' . PHP_VERSION . '.';
+}
+
+if ($issues) {
+ if (!headers_sent()) {
+ header('HTTP/1.1 500 Internal Server Error');
+ }
+ if (!ini_get('display_errors')) {
+ if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+ fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
+ } elseif (!headers_sent()) {
+ echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
+ }
+ }
+ throw new \RuntimeException(
+ 'Composer detected issues in your platform: ' . implode(' ', $issues)
+ );
+}
diff --git a/vendor/sboden/odtphp/.gitattributes b/vendor/sboden/odtphp/.gitattributes
new file mode 100644
index 0000000000..2f673eb9b3
--- /dev/null
+++ b/vendor/sboden/odtphp/.gitattributes
@@ -0,0 +1,65 @@
+# Drupal git normalization
+# @see https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html
+# @see https://www.drupal.org/node/1542048
+
+# Normally these settings would be done with macro attributes for improved
+# readability and easier maintenance. However macros can only be defined at the
+# repository root directory. Drupal avoids making any assumptions about where it
+# is installed.
+
+# Define text file attributes.
+# - Treat them as text.
+# - Ensure no CRLF line-endings, neither on checkout nor on checkin.
+# - Detect whitespace errors.
+# - Exposed by default in `git diff --color` on the CLI.
+# - Validate with `git diff --check`.
+# - Deny applying with `git apply --whitespace=error-all`.
+# - Fix automatically with `git apply --whitespace=fix`.
+
+*.config text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
+*.css text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
+*.dist text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
+*.engine text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php
+*.html text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=html
+*.inc text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php
+*.install text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php
+*.js text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
+*.json text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
+*.lock text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
+*.map text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
+*.md text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
+*.module text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php
+*.php text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php
+*.po text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
+*.profile text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php
+*.script text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
+*.sh text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php
+*.sql text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
+*.svg text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
+*.theme text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php
+*.twig text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
+*.txt text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
+*.xml text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
+*.yml text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
+
+# PHPStan's baseline uses tabs instead of spaces.
+core/.phpstan-baseline.php text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tabwidth=2 diff=php linguist-language=php
+
+# Define binary file attributes.
+# - Do not treat them as text.
+# - Include binary diff in patches instead of "binary files differ."
+*.eot binary -text diff
+*.exe binary -text diff
+*.gif binary -text diff
+*.gz binary -text diff
+*.ico binary -text diff
+*.jpeg binary -text diff
+*.jpg binary -text diff
+*.otf binary -text diff
+*.phar binary -text diff
+*.png binary -text diff
+*.svgz binary -text diff
+*.ttf binary -text diff
+*.woff binary -text diff
+*.woff2 binary -text diff
+*.odt binary -text diff
diff --git a/vendor/sboden/odtphp/.gitignore b/vendor/sboden/odtphp/.gitignore
new file mode 100644
index 0000000000..241f3f05e8
--- /dev/null
+++ b/vendor/sboden/odtphp/.gitignore
@@ -0,0 +1,11 @@
+vendor/
+build/
+bin/
+.idea/
+.github/
+.phpunit.result.cache
+test-results.xml
+
+# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file
+# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
+composer.lock
diff --git a/vendor/sboden/odtphp/LICENSE b/vendor/sboden/odtphp/LICENSE
new file mode 100755
index 0000000000..9cecc1d466
--- /dev/null
+++ b/vendor/sboden/odtphp/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ {one line to give the program's name and a brief idea of what it does.}
+ Copyright (C) {year} {name of author}
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ {project} Copyright (C) {year} {fullname}
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/vendor/sboden/odtphp/README.md b/vendor/sboden/odtphp/README.md
new file mode 100644
index 0000000000..3866e116aa
--- /dev/null
+++ b/vendor/sboden/odtphp/README.md
@@ -0,0 +1,494 @@
+# ODTPHP
+
+A PHP library for creating and manipulating ODT (OpenDocument Text) files.
+
+## Requirements
+
+- PHP 8.1 or higher
+- PHP ZIP Extension or PclZip library. PHP ZIP Extension is recommended as we will probably
+ remove the PclZip library sometime in the future.
+
+## Installation
+
+Install via Composer:
+
+```bash
+composer require sboden/odtphp:^3
+```
+
+## Basic Usage
+
+### Simple Variable Substitution
+
+```php
+setVars('company_name', 'ACME Corporation');
+$odt->setVars('current_date', date('Y-m-d'));
+
+// Save the modified document
+$odt->saveToDisk('output.odt');
+```
+
+### Advanced Usage with Images and Segments
+
+```php
+ '{',
+ 'DELIMITER_RIGHT' => '}',
+];
+$odt = new Odf("invoice_template.odt", $config);
+
+// Insert a company logo
+$odt->setImage('company_logo', 'path/to/logo.png');
+
+// Create a repeatable segment for line items
+$lineItems = $odt->setSegment('invoice_items');
+foreach ($invoiceData['items'] as $item) {
+ $lineItems->setVars('item_name', $item->name);
+ $lineItems->setVars('item_price', number_format($item->price, 2));
+ $lineItems->setVars('item_quantity', $item->quantity);
+ $lineItems->merge();
+}
+
+// Set summary variables
+$odt->setVars('total_amount', number_format($invoiceData['total'], 2));
+$odt->setVars('invoice_number', $invoiceData['number']);
+
+// Save the completed invoice
+$odt->saveToDisk('invoice.odt');
+```
+
+### Custom Properties
+
+You can set custom document properties in your ODT file using the `setCustomProperty()` method:
+
+```php
+$odf->setCustomProperty('Author', 'John Doe');
+$odf->setCustomProperty('Version', '1.0');
+```
+
+Important notes about custom properties:
+- It's recommended to use only strings for custom properties. The reason is that the value will be replaced by odtphp,
+ but the value is supposed to be in the internal format as defined by odt for that particular type. Odtphp only
+ replaces the value of the custom property, odtphp will not do data conversions.
+- For dates e.g., make sure to use YYYY-MM-DD as format as that is what ODT wants as internal date format.
+- Special characters will be HTML encoded by default.
+- Properties must exist in the document before they can be set.
+- Setting custom properties works when using LibreOffice/OpenOffice only. It seems to fail using Microsoft Word as
+ Microsoft Word does not update the custom property values upon opening an ODT file.
+- Newlines are not supported in custom property values because the LibreOffice GUI does not allow newlines.
+ setCustomProperty() will happily insert newlines and the newlines will also be outputted, but this
+ "unofficial" functionality which may be blocked in the future by ODT.
+
+Examples:
+```php
+// Basic string properties
+$odf->setCustomProperty('Author', 'John Doe');
+$odf->setCustomProperty('Department', 'Engineering');
+
+// Dates should use YYYY-MM-DD format
+$odf->setCustomProperty('Creation Date', '2024-01-20');
+
+// Special characters are encoded by default
+$odf->setCustomProperty('Note', ' & urgent'); // Will be encoded
+$odf->setCustomProperty('Note', ' & urgent', FALSE); // Won't be encoded
+```
+
+### Images
+
+You can insert images into your ODT file using different measurement units:
+
+```php
+// Using centimeters (original function)
+$odf->setImage('logo', 'path/to/logo.png', -1, 5, 7.5); // 5cm width, 7.5cm height
+$odf->setImage('photo', 'path/to/photo.jpg', 1, 10, 15, 2, 2); // On page 1 with 2cm offsets
+
+// Using millimeters
+$odf->setImageMm('logo', 'path/to/logo.png', -1, 50, 75); // 50mm width, 75mm height
+$odf->setImageMm('photo', 'path/to/photo.jpg', 1, 100, 150, 20, 20); // On page 1 with 20mm offsets
+
+// Using pixels (automatically converts to mm)
+$odf->setImagePixel('logo', 'path/to/logo.png', -1, 189, 283); // 189px ≈ 5cm, 283px ≈ 7.5cm
+$odf->setImagePixel('photo', 'path/to/photo.jpg', 1, 378, 567, 76, 76); // On page 1 with 76px offsets
+
+// Keep original image size
+$odf->setImage('icon1', 'path/to/icon.png'); // Will convert to cm
+$odf->setImageMm('icon2', 'path/to/icon.png'); // Will convert to mm
+$odf->setImagePixel('icon3', 'path/to/icon.png'); // Will keep original pixel dimensions
+```
+
+Parameters for all image functions:
+- `$key`: Name of the variable in the template
+- `$value`: Path to the image file
+- `$page`: Page number (-1 for as-char anchoring)
+- `$width`: Width in respective units (null to keep original)
+- `$height`: Height in respective units (null to keep original)
+- `$offsetX`: Horizontal offset (ignored if $page is -1)
+- `$offsetY`: Vertical offset (ignored if $page is -1)
+
+Note: While `setImagePixel` accepts measurements in pixels, the ODT format requires millimeters or
+centimeters internally. The function automatically converts pixel measurements to the appropriate
+format.
+
+### Error Handling
+
+```php
+setVars('non_existent_variable', 'Some Value');
+} catch (OdfException $e) {
+ // Handle template-related errors
+ error_log("ODT Processing Error: " . $e->getMessage());
+}
+```
+
+### Testing
+
+1. Install dependencies:
+```
+composer install
+```
+
+2. Run the tests:
+
+On Linux:
+```bash
+# Go to the root of odtphp, then:
+./run-tests.sh
+```
+
+On Windows:
+```bash
+# Go to the root of odtphp, then:
+run-tests.bat
+
+# Note that depending on your PHP installation you may have to edit the
+# script to include the path to php.exe
+```
+
+You can also run the PHPUnit tests e.g. in PHPStorm, but you have to exclude
+the vendor directory to avoid warnings about the PHPUnit framework itself.
+
+
+#### Test Structure
+
+The test suite covers various aspects of the ODT templating functionality:
+
+1. **Basic Tests** (`Basic1Test.php` and `Basic2Test.php`):
+ - Verify basic variable substitution in ODT templates
+ - Test both PhpZip and PclZip ZIP handling methods
+ - Demonstrate simple text replacement and encoding
+
+2. **Configuration Tests** (`ConfigTest.php`):
+ - Validate configuration handling
+ - Test custom delimiters
+ - Check error handling for invalid configurations
+
+3. **Edge Case Tests** (`EdgeCaseTest.php`):
+ - Handle complex scenarios like:
+ * Large variable substitution
+ * Nested segment merging
+ * Special character encoding
+ * Advanced image insertion
+ * Invalid template handling
+
+4. **Image Tests** (`ImageTest.php`):
+ - Verify image insertion functionality
+ - Test image resizing
+ - Check error handling for invalid image paths
+
+5. **Variable Tests** (`VariableTest.php`):
+ - Test variable existence checks
+ - Verify special character handling
+ - Check multiline text substitution
+
+5. **Custom Property Test** (`SetCustomPropertyTest.php`):
+ - Test custom propertyfunctionality
+
+Each test is designed to ensure the robustness and reliability of the ODT
+templating library across different use cases and configurations.
+
+A lot of tests run twice:
+- Once using PclZip library
+- Once using PHP's native Zip extension
+
+The tests generate ODT files and compare them with gold standard files located in:
+- `tests/src/files/gold_phpzip/` - Gold files for PHP Zip tests
+- `tests/src/files/gold_pclzip/` - Gold files for PclZip tests
+
+
+### Linting
+
+On Linux:
+```bash
+# Go to the root of odtphp, then:
+composer install
+vendor/bin/phpcs --standard="Drupal,DrupalPractice" -n --extensions="php,module,inc,install,test,profile,theme" src
+```
+
+
+## Features
+
+- Template variable substitution
+- Image insertion and manipulation
+- Segment management for repeatable content
+- Support for custom delimiters
+- Type-safe implementation
+- Modern PHP 8.1+ features
+
+## Configuration
+
+```php
+$config = [
+ 'ZIP_PROXY' => \Odtphp\Zip\PhpZipProxy::class, // or PclZipProxy
+ 'DELIMITER_LEFT' => '{',
+ 'DELIMITER_RIGHT' => '}',
+ 'PATH_TO_TMP' => '/tmp'
+];
+
+$odt = new Odf("template.odt", $config);
+```
+
+## Type Safety
+
+This library is built with PHP 8.1's type system in mind:
+- Strict typing enabled
+- Property type declarations
+- Return type declarations
+- Parameter type hints
+- Improved error handling
+
+## Contributing
+
+Contributions are welcome! Please feel free to submit a Pull Request.
+
+## License
+
+This project is licensed under the GPL-3.0 License - see the LICENSE file for details.
+
+The GPL-3.0 license is because of what the cybermonde/odtphp author put it
+to, I (sboden) don't want to change it. I don't consider odtphp to be "my" software, I
+only tried to make the cybermonde/odtphp fork work properly in PHP v8.3
+and beyond.
+
+Personally (sboden) I would have used an MIT or BSD license for odtphp, but I
+will happily abide with the original author's intent.
+
+The original version/source code of odtphp is at [https://sourceforge.
+net/projects/odtphp/](https://sourceforge.net/projects/odtphp/). The
+cybermonde/odtphp fork I started from is at [https://github.
+com/cybermonde/odtphp](https://github.com/cybermonde/odtphp).
+
+
+### Usage justification
+How and why we use odtphp in our projects.
+
+Some of our projects send out letters (either PDF or on physical paper) to people:
+the idea is to have a nice template, fill in some data, create a PDF file from the
+filled-in template, and send the resulting PDF to the recipient.
+
+The template is an ODT file, the data is stored in a database. We use odtphp to fill
+in the template. The filled-in ODT file is then transformed into a PDF file using
+(originally) jodconverter, and in newer projects using gotenberg.dev (which is
+essentially jodconverter using golang). The PDF converters run in a different
+container than the main applications.
+
+The reason for using gotenberg over jodconverter is that gotenberg is better
+supported. And jodconverter was initially not able to handle large volumes since
+the most commonly used Docker container did not clean up after itself, so I had to
+build this in myself.
+
+Why not generate PDFs directly from PHP? We tried, but it's difficult to
+have nice-looking PDFs using the existing PHP libraries. One PHP library has
+problems with images, another PHP PDF library faces issues with margins and
+headers/footers, ... We tried a lot of different setups and finally settled
+for odtphp/gotenberg to have the PDF output "pixel perfect". Another
+limitation was that we needed about 50,000 PDFs per day for which we run
+parallel queues to the PDF converters.
+
+
+### Technical informations
+
+#### Why do my variables sometimes not get filled in?
+
+Because LibreOffice adds metadata to text that you change. E.g. when you have
+something as follows in your ODT file:
+
+```
+{variable_text}
+```
+
+And you make a small change to the previous text it may very well end up as:
+
+```
+{variables_text}]
+```
+where you don't see the "text:span" part visually, but it is there. And since
+odtphp plainly matches variables in an ODT file, the variable name will not match
+anymore and will not be replaced.
+
+How to get around that: When you change variable texts, select the variable
+text (including the begin and end delimiters), copy it, and then use "Paste
+special/Paste unformatted text" to copy the text in the same place without any
+invisible metadata.
+
+#### Variables vs Custom Properties
+
+ODT files support two different ways to insert dynamic content: variables and custom properties.
+
+#### Variables
+Variables are an ODTPhp feature that works by replacing text anchors (like `{variable_name}`) with new content:
+```php
+$odf->setVars('company_name', 'ACME Corporation');
+$odf->setImage('logo', 'path/to/logo.png', -1, 50, 75); // 50mm width, 75mm height
+```
+
+Key points about variables:
+- Variables are specific to ODTPhp, variables are not a feature of the ODT specification
+- Work as simple text replacements in the document
+- Support images through `setImage`, `setImageMm`, and `setImagePixel` functions
+- Can be affected by LibreOffice's invisible metadata when editing the template (see previous topic)
+- Must be surrounded by delimiters (default: `{` and `}`)
+- Can be used anywhere in the document text
+
+#### Custom Properties
+Custom properties are an official ODT feature for storing (usually) metadata about the document:
+```php
+$odf->setCustomProperty('Author', 'John Doe');
+$odf->setCustomProperty('Version', '1.0');
+```
+
+Key points about custom properties:
+- Official ODT feature supported by all ODT-compatible software
+- Stored in the document's meta.xml file
+- Not affected by LibreOffice's invisible metadata
+- Must exist in the document before they can be set
+- Cannot be used with image functions
+- Must be created through the LibreOffice GUI (File > Properties > Custom Properties)
+
+When to use which:
+- Use variables for:
+ * Any text content in the document body
+ * Images and other media
+ * Dynamic content that needs formatting
+ * Repeatable segments
+
+- Use custom properties for:
+ * Small single line text fields: name, address line 1, address line2, postal code, city, ...
+
+
+#### Why does "Catégorie 1" sometimes appear as "Catégorie 1" in the output?
+
+Usually this is a double-encoding problem. When UTF-8 text is encoded again
+as UTF-8, special characters like 'é' can appear as 'é'. This typically
+happens when the text is already in UTF-8 but gets encoded again.
+
+While setting variables you can specify the encoding as needed, e.g. as:
+
+```php
+$categorie->setVars('TitreCategorie', 'Catégorie 1', true, 'UTF-8');
+```
+
+#### The default charset has changed?
+
+One of the "major" changes I made is to put UTF-8 as the default charset when
+setting variables. Before the default was ISO-8859-1, but UTF-8 currently
+makes more sense to me.
+
+I always recommend using UTF-8 internally within the application and
+handling encoding/decoding at the boundaries.
+
+#### How do I upgrade from v1/v2 to v3?
+
+While it is a breaking change, as long as you only use `odtphp` and don't use
+any of its internals you should be fine by just upgrading the odtphp library
+to v3.
+
+If you inherit from Odf or you use some internal things in the odtphp library,
+then all bets are off.
+
+### History
+
+The odtphp project was initially started by Julien Pauli, Olivier Booklage,
+Vincent Brouté and published at http://www.odtphp.com (the website no longer
+exists).
+
+As DXC Technology working for the Flemish government we started using
+the `cybermonde/odtphp` fork ([source](https://github.com/cybermonde/odtphp))
+in a couple of projects to fill in template ODT files with data, then transforming
+the filled-in ODT to PDF using `gotenberg`.
+We ran into a couple of problems with the `cybermonde/odtphp` library for
+which it was easier to just create my own fork, hence `sboden/odtphp`: we
+sometimes generate 50,000 forms daily, and `cybermonde/odtphp` would
+occasionally overwrite outputs when processing a lot of ODT files
+simultaneously (because of non-random random numbers).
+
+Why do I try to keep this "corpse" alive? Simply because I found no
+replacement for it. The projects I work that use odtphp are now (= end 2024) on
+PHP 8.2 moving to PHP 8.3, and some pieces of the original odtphp library
+are starting to spew warnings. During my 2024 Christmas holidays I was
+writing some unit test cases for odtphp, and while testing the AI tool
+"Windsurf", I tried to have Windsurf automatically update odtphp to a newer
+PHP version, and sboden/odtphp v3 is the result of that (after some extra
+manual human changes).
+
+While this fork `sboden/odtphp` is not officially supported, maintenance
+and bug fixes are provided on a best-effort basis. The `sboden/odtphp` library
+is actively used in production applications with planned lifecycles extending
+to at least 2030.
+
+This software is **not** by DXC Technology, else it would have been called
+`dxc/odtphp`. This software is **not** by the Flemish government, else it
+would probably have been called `vo/odtphp`. I have always worked on odtphp
+during my personal time.
+
+### Upgrade plan
+- From v3.0.3 upto v3.2.1:
+ There should be no issues in upgrading sboden/odtphp, only new functionality
+ and some small bugfixes were done.
+
+### Version History
+- v3.2.1 - 24Jul2025: Small edge case fix in Odf::recursiveHtmlspecialchars()
+- v3.2.0 - 24Mar2025: Introduction of customPropertyExists()
+- v3.1.0 - 21Mar2025: Introduction of functions setCustomProperty()/setImageMm()/setImagePixel()
+- v3.0.3 - 29Dec2024: Odtphp version for PHP 8.x
+- v2.2.1 - 07Dec2022: Parallel processing by odtphp does not overwrite outputs
+
+### Disclaimer
+
+This software is provided "as is" without warranty of any kind, either
+expressed or implied. The entire risk as to the quality and performance of
+the software is with you.
+
+In no event will the authors, contributors, or licensors be liable for any
+damages, including but not limited to, direct, indirect, special, incidental,
+or consequential damages arising out of the use or inability to use this
+software, even if advised of the possibility of such damage.
+
+By using this software, you acknowledge that you have read this disclaimer,
+understand it, and agree to be bound by its terms.
+
+
+### Links:
+
+* http://sourceforge.net/projects/odtphp/ Sourceforge Project of the initial library (stale)
+
+[1]: http://vikasmahajan.wordpress.com/2010/12/09/odtphp-bug-solved/
+[2]: https://web.archive.org/web/20120531095719/http://www.odtphp.com/index.php?i=home
+[3]: https://en.wikipedia.org/wiki/OpenDocument_software#Text_documents_.28.odt.29
diff --git a/vendor/sboden/odtphp/composer.json b/vendor/sboden/odtphp/composer.json
new file mode 100755
index 0000000000..66dec65985
--- /dev/null
+++ b/vendor/sboden/odtphp/composer.json
@@ -0,0 +1,45 @@
+{
+ "name": "sboden/odtphp",
+ "description": "ODT document generator (plugin replacement for cybermonde/odtphp)",
+ "keywords": [
+ "odt",
+ "php"
+ ],
+ "license": "GPL-3.0-only",
+ "type": "library",
+ "homepage": "https://github.com/sboden/odtphp",
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+ "drupal/coder": "^8.3",
+ "phpunit/phpunit": "^10.4",
+ "scrutinizer/ocular": "^1.9",
+ "symfony/var-dumper": "^6.4"
+ },
+ "autoload": {
+ "psr-4": {
+ "Odtphp\\": "src"
+ },
+ "files": ["lib/pclzip.lib.php"]
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Odtphp\\Test\\": "tests"
+ },
+ "files": ["lib/pclzip.lib.php"]
+ },
+ "config": {
+ "sort-packages": true,
+ "optimize-autoloader": true,
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ }
+}
diff --git a/vendor/sboden/odtphp/documentation/odtphp_documentation.pdf b/vendor/sboden/odtphp/documentation/odtphp_documentation.pdf
new file mode 100755
index 0000000000000000000000000000000000000000..8e78454f645c15dd216c4c5b8c2935ec62d48527
GIT binary patch
literal 313864
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
z
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