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/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: {$invoice_display}
+
+
+ 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 Portrait --encoding utf-8 --margin-top 10 --margin-bottom 10 --margin-left 8 --margin-right 8 %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',
+ 'margin_left' => 8,
+ 'margin_right' => 8,
+ '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);
+ }
+}
diff --git a/inc/reference.class.php b/inc/reference.class.php
index a54866540b..18f257f6cc 100644
--- a/inc/reference.class.php
+++ b/inc/reference.class.php
@@ -295,6 +295,169 @@ public function post_getEmpty()
}
+ /**
+ * Get the Type dropdown class for an itemtype, supporting GLPI 11+ custom assets.
+ *
+ * @param string $itemtype The itemtype class name
+ * @return string|null The Type class name, or null if none exists
+ */
+ public static function getTypeClassForItemtype(string $itemtype): ?string
+ {
+ // Standard convention (e.g. Computer → ComputerType)
+ $type_class = $itemtype . 'Type';
+ if (class_exists($type_class)) {
+ return $type_class;
+ }
+
+ // GLPI 11+ custom assets: resolve via AssetDefinition
+ if (class_exists('Glpi\Asset\Asset') && is_a($itemtype, 'Glpi\Asset\Asset', true)) {
+ $item = getItemForItemtype($itemtype);
+ if ($item !== false && method_exists($item, 'getDefinition')) {
+ $definition = $item->getDefinition();
+ if ($definition !== null && method_exists($definition, 'getAssetTypeClassName')) {
+ $custom_type_class = $definition->getAssetTypeClassName();
+ if (!empty($custom_type_class) && class_exists($custom_type_class)) {
+ return $custom_type_class;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Get the Model dropdown class for an itemtype, supporting GLPI 11+ custom assets.
+ *
+ * @param string $itemtype The itemtype class name
+ * @return string|null The Model class name, or null if none exists
+ */
+ public static function getModelClassForItemtype(string $itemtype): ?string
+ {
+ // Standard convention (e.g. Computer → ComputerModel)
+ $model_class = $itemtype . 'Model';
+ if (class_exists($model_class)) {
+ return $model_class;
+ }
+
+ // GLPI 11+ custom assets: resolve via AssetDefinition
+ if (class_exists('Glpi\Asset\Asset') && is_a($itemtype, 'Glpi\Asset\Asset', true)) {
+ $item = getItemForItemtype($itemtype);
+ if ($item !== false && method_exists($item, 'getDefinition')) {
+ $definition = $item->getDefinition();
+ if ($definition !== null && method_exists($definition, 'getAssetModelClassName')) {
+ $custom_model_class = $definition->getAssetModelClassName();
+ if (!empty($custom_model_class) && class_exists($custom_model_class)) {
+ return $custom_model_class;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Check if an itemtype is a GLPI 11+ custom asset.
+ *
+ * @param string $itemtype The itemtype class name
+ * @return bool
+ */
+ public static function isCustomAsset(string $itemtype): bool
+ {
+ // Check by class hierarchy if the class exists
+ if (class_exists('Glpi\Asset\Asset') && is_a($itemtype, 'Glpi\Asset\Asset', true)) {
+ return true;
+ }
+
+ // Check by namespace pattern: Glpi\CustomAsset\{name}Asset
+ return (bool) preg_match('/^Glpi\\\\CustomAsset\\\\/', $itemtype);
+ }
+
+
+ /**
+ * Get the AssetDefinition ID for a GLPI 11+ custom asset itemtype.
+ *
+ * @param string $itemtype The itemtype class name
+ * @return int|null The definition ID, or null if not a custom asset
+ */
+ public static function getAssetDefinitionId(string $itemtype): ?int
+ {
+ if (!self::isCustomAsset($itemtype)) {
+ return null;
+ }
+
+ // Try ORM approach
+ $item = getItemForItemtype($itemtype);
+ if ($item !== false && method_exists($item, 'getDefinition')) {
+ $definition = $item->getDefinition();
+ if ($definition !== null) {
+ return (int) $definition->getID();
+ }
+ }
+
+ // Fallback: extract system_name from class and look up in DB
+ if (preg_match('/^Glpi\\\\CustomAsset\\\\(.+)Asset$/', $itemtype, $matches)) {
+ /** @var DBmysql $DB */
+ global $DB;
+ try {
+ if ($DB->tableExists('glpi_assets_assetdefinitions')) {
+ $result = $DB->request([
+ 'SELECT' => ['id'],
+ 'FROM' => 'glpi_assets_assetdefinitions',
+ 'WHERE' => ['system_name' => $matches[1]],
+ 'LIMIT' => 1,
+ ]);
+ foreach ($result as $row) {
+ return (int) $row['id'];
+ }
+ }
+ } catch (\Throwable $e) {
+ // Fall through
+ }
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Get the display label for a custom asset type from glpi_assets_assetdefinitions.
+ *
+ * @param string $itemtype The itemtype class name (e.g. Glpi\CustomAsset\LaptopAsset)
+ * @return string The label from the definition, or the class name as fallback
+ */
+ public static function getCustomAssetLabel(string $itemtype): string
+ {
+ /** @var DBmysql $DB */
+ global $DB;
+
+ // Extract system_name from class: Glpi\CustomAsset\{system_name}Asset
+ if (preg_match('/^Glpi\\\\CustomAsset\\\\(.+)Asset$/', $itemtype, $matches)) {
+ $system_name = $matches[1];
+ try {
+ if ($DB->tableExists('glpi_assets_assetdefinitions')) {
+ $result = $DB->request([
+ 'SELECT' => ['label'],
+ 'FROM' => 'glpi_assets_assetdefinitions',
+ 'WHERE' => ['system_name' => $system_name],
+ 'LIMIT' => 1,
+ ]);
+ foreach ($result as $row) {
+ return htmlspecialchars($row['label'], ENT_QUOTES, 'UTF-8');
+ }
+ }
+ } catch (\Throwable $e) {
+ // Fall through to default
+ }
+ }
+
+ return htmlspecialchars($itemtype, ENT_QUOTES, 'UTF-8');
+ }
+
+
public function prepareInputForAdd($input)
{
if (!isset($input["name"]) || $input["name"] == '') {
@@ -403,7 +566,7 @@ public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $
}
- public function dropdownTemplate($name, $entity, $table, $value = 0)
+ public function dropdownTemplate($name, $entity, $table, $value = 0, $itemtype = '')
{
/** @var DBmysql $DB */
global $DB;
@@ -420,6 +583,15 @@ public function dropdownTemplate($name, $entity, $table, $value = 0)
'ORDER' => 'template_name',
];
+ // For GLPI 11+ custom assets, scope templates to the specific asset definition
+ // since all custom assets share the glpi_assets_assets table
+ if (!empty($itemtype)) {
+ $definition_id = self::getAssetDefinitionId($itemtype);
+ if ($definition_id !== null) {
+ $query['WHERE']['assets_assetdefinitions_id'] = $definition_id;
+ }
+ }
+
$option[0] = Dropdown::EMPTY_VALUE;
foreach ($DB->request($query) as $data) {
$option[$data["id"]] = $data["template_name"];
@@ -490,6 +662,11 @@ public function checkIfTemplateExistsInEntity($detailID, $itemtype, $entity)
'is_template' => 1,
],
];
+ // For GLPI 11+ custom assets, scope to the specific asset definition
+ $definition_id = self::getAssetDefinitionId($itemtype);
+ if ($definition_id !== null) {
+ $criteria_template['WHERE']['assets_assetdefinitions_id'] = $definition_id;
+ }
$result_template = $DB->request($criteria_template);
if (count($result_template) >= 1) {
$row_template = $result_template->current();
@@ -564,12 +741,20 @@ public function dropdownAllItems($options = [])
foreach ($types as $type) {
$item = getItemForItemtype($type);
- echo "getTypeName();
+ } else {
+ // For custom assets, try to get label from glpi_assets_assetdefinitions
+ $label = self::getCustomAssetLabel($type);
+ }
+
+ echo " " . ($item !== false ? $item->getTypeName() : $type) . " \n";
+ echo " >" . $label . "\n";
}
echo "";
@@ -674,7 +859,7 @@ public function showForm($id, $options = [])
if ($id > 0) {
$itemtype = $this->fields["itemtype"];
$item = getItemForItemtype($itemtype);
- echo($item !== false ? $item->getTypeName() : $itemtype);
+ echo($item !== false ? $item->getTypeName() : self::getCustomAssetLabel($itemtype));
echo Html::hidden('itemtype', ['value' => $itemtype]);
} else {
$this->dropdownAllItems([
@@ -692,8 +877,8 @@ public function showForm($id, $options = [])
echo "";
echo "";
if ($options['item']) {
- $itemtypeclass = $options['item'] . "Type";
- if (class_exists($itemtypeclass)) {
+ $itemtypeclass = self::getTypeClassForItemtype($options['item']);
+ if ($itemtypeclass !== null) {
if (!$reference_in_use) {
Dropdown::show($itemtypeclass, [
'name' => "types_id",
@@ -711,8 +896,9 @@ public function showForm($id, $options = [])
echo "" . __s("Model") . " ";
echo "";
echo "";
- if ($options['item'] && class_exists($itemtypeclass)) {
- Dropdown::show($options['item'] . "Model", [
+ $itemmodelclass = $options['item'] ? self::getModelClassForItemtype($options['item']) : null;
+ if ($itemmodelclass !== null) {
+ Dropdown::show($itemmodelclass, [
'name' => "models_id",
'value' => $this->fields["models_id"],
]);
@@ -724,16 +910,17 @@ public function showForm($id, $options = [])
echo " " . __s("Template name") . " ";
echo "";
echo "";
- if (
- !empty($options['item'])
- && $DB->fieldExists($options['item']::getTable(), 'is_template')
- ) {
- $this->dropdownTemplate(
- 'templates_id',
- $this->fields['entities_id'],
- $options['item']::getTable(),
- $this->fields['templates_id'],
- );
+ if (!empty($options['item'])) {
+ $item_instance = getItemForItemtype($options['item']);
+ if ($item_instance !== false && $item_instance->maybeTemplate()) {
+ $this->dropdownTemplate(
+ 'templates_id',
+ $this->fields['entities_id'],
+ $item_instance->getTable(),
+ $this->fields['templates_id'],
+ $options['item'],
+ );
+ }
}
echo " ";
@@ -794,13 +981,15 @@ public function dropdownAllItemsByType(
}
$condition = [];
- if (class_exists($itemtype . "Type", false) && $types_id != 0) {
- $fk = getForeignKeyFieldForTable(getTableForItemType($itemtype . "Type"));
+ $type_class = self::getTypeClassForItemtype($itemtype);
+ if ($type_class !== null && $types_id != 0) {
+ $fk = getForeignKeyFieldForTable(getTableForItemType($type_class));
$condition[$fk] = $types_id;
}
- if (class_exists($itemtype . "Model", false) && $models_id != 0) {
- $fk = getForeignKeyFieldForTable(getTableForItemType($itemtype . "Model"));
+ $model_class = self::getModelClassForItemtype($itemtype);
+ if ($model_class !== null && $models_id != 0) {
+ $fk = getForeignKeyFieldForTable(getTableForItemType($model_class));
$condition[$fk] = $models_id;
}
diff --git a/setup.php b/setup.php
index 7cd7cfd6b0..2acf71fa00 100644
--- a/setup.php
+++ b/setup.php
@@ -30,7 +30,7 @@
use function Safe\define;
-define('PLUGIN_ORDER_VERSION', '2.12.6');
+define('PLUGIN_ORDER_VERSION', '2.13.1');
// Minimal GLPI version, inclusive
define("PLUGIN_ORDER_MIN_GLPI", "11.0.0");
@@ -125,6 +125,49 @@ function plugin_init_order()
'Pdu',
];
+ // GLPI 11+ custom assets support: dynamically discover user-defined asset types
+ // Custom assets are stored in glpi_assets_assetdefinitions (definitions) and
+ // glpi_assets_assets (instances). The concrete class name follows the pattern:
+ // Glpi\CustomAsset\{system_name}Asset
+ if (class_exists('Glpi\Asset\AssetDefinition')) {
+ try {
+ // Try ORM approach first
+ $asset_definition = new \Glpi\Asset\AssetDefinition();
+ $found_via_orm = false;
+ foreach ($asset_definition->find(['is_active' => 1]) as $def) {
+ $asset_definition->getFromDB($def['id']);
+ if (method_exists($asset_definition, 'getAssetClassName')) {
+ $concrete_class = $asset_definition->getAssetClassName();
+ if (!empty($concrete_class) && !in_array($concrete_class, $ORDER_TYPES)) {
+ $ORDER_TYPES[] = $concrete_class;
+ $found_via_orm = true;
+ }
+ }
+ }
+
+ // Fallback: query DB directly and construct class names
+ if (!$found_via_orm) {
+ /** @var DBmysql $DB */
+ global $DB;
+ if ($DB->tableExists('glpi_assets_assetdefinitions')) {
+ $result = $DB->request([
+ 'SELECT' => ['id', 'system_name', 'label'],
+ 'FROM' => 'glpi_assets_assetdefinitions',
+ 'WHERE' => ['is_active' => 1],
+ ]);
+ foreach ($result as $row) {
+ $concrete_class = 'Glpi\\CustomAsset\\' . $row['system_name'] . 'Asset';
+ if (!in_array($concrete_class, $ORDER_TYPES)) {
+ $ORDER_TYPES[] = $concrete_class;
+ }
+ }
+ }
+ }
+ } catch (\Throwable $e) {
+ // Tables may not exist during install/upgrade — silently ignore
+ }
+ }
+
$CFG_GLPI['plugin_order_types'] = $ORDER_TYPES;
$PLUGIN_HOOKS['pre_item_purge']['order'] = [
@@ -205,8 +248,8 @@ function plugin_version_order()
return [
'name' => __s("Orders management", "order"),
'version' => PLUGIN_ORDER_VERSION,
- 'author' => 'The plugin order team',
- 'homepage' => 'https://github.com/pluginsGLPI/order',
+ 'author' => 'geek95dg',
+ 'homepage' => 'https://github.com/geek95dg/order',
'license' => 'GPLv2+',
'requirements' => [
'glpi' => [
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 0000000000..8e78454f64
Binary files /dev/null and b/vendor/sboden/odtphp/documentation/odtphp_documentation.pdf differ
diff --git a/vendor/sboden/odtphp/examples/form.php b/vendor/sboden/odtphp/examples/form.php
new file mode 100755
index 0000000000..8315cf73f5
--- /dev/null
+++ b/vendor/sboden/odtphp/examples/form.php
@@ -0,0 +1,67 @@
+//////////////////////////////////////////////////////////////
+// ODT Letter Generation Based on a Form //
+// //
+// Laurent Lefèvre - http://www.cybermonde.org //
+// //
+//////////////////////////////////////////////////////////////
+
+// This is a left over example from cybermonde/odtphp, I left it
+// partially in French.
+
+use Odtphp\Odf;
+
+// Make sure you have Zip extension or PclZip library loaded.
+// First : include the library.
+require_once '../vendor/autoload.php';
+
+// base model
+$odf = new Odf("form_template.odt");
+// if nothing has been posted, show the form
+if (!$_POST) {
+?>
+
+
+
+ Form to ODT
+
+
+
+
+
+
+
+setVars('cyb_date', date("d/m/Y"));
+// form data
+$odf->setVars('cyb_invite', $_POST["invite"]);
+// Handle accented characters using this HTML entity decode trick.
+$odf->setVars('cyb_firstname', html_entity_decode(htmlentities($_POST["firstname"],ENT_NOQUOTES, "utf-8")));
+$odf->setVars('cyb_lastname', html_entity_decode(htmlentities($_POST["lastname"],ENT_NOQUOTES, "utf-8")));
+$odf->setVars('cyb_total', $_POST["total"]);
+
+$odf->exportAsAttachedFile();
+}
diff --git a/vendor/sboden/odtphp/examples/form_template.odt b/vendor/sboden/odtphp/examples/form_template.odt
new file mode 100755
index 0000000000..e3e47c4a95
Binary files /dev/null and b/vendor/sboden/odtphp/examples/form_template.odt differ
diff --git a/vendor/sboden/odtphp/examples/images/anaska.gif b/vendor/sboden/odtphp/examples/images/anaska.gif
new file mode 100755
index 0000000000..1ea3f0e68a
Binary files /dev/null and b/vendor/sboden/odtphp/examples/images/anaska.gif differ
diff --git a/vendor/sboden/odtphp/examples/images/anaska.jpg b/vendor/sboden/odtphp/examples/images/anaska.jpg
new file mode 100755
index 0000000000..b02b4c4eff
Binary files /dev/null and b/vendor/sboden/odtphp/examples/images/anaska.jpg differ
diff --git a/vendor/sboden/odtphp/examples/images/apache.gif b/vendor/sboden/odtphp/examples/images/apache.gif
new file mode 100755
index 0000000000..0db293e401
Binary files /dev/null and b/vendor/sboden/odtphp/examples/images/apache.gif differ
diff --git a/vendor/sboden/odtphp/examples/images/mysql.gif b/vendor/sboden/odtphp/examples/images/mysql.gif
new file mode 100755
index 0000000000..6520883e1d
Binary files /dev/null and b/vendor/sboden/odtphp/examples/images/mysql.gif differ
diff --git a/vendor/sboden/odtphp/examples/images/php.gif b/vendor/sboden/odtphp/examples/images/php.gif
new file mode 100755
index 0000000000..f352c7308f
Binary files /dev/null and b/vendor/sboden/odtphp/examples/images/php.gif differ
diff --git a/vendor/sboden/odtphp/examples/simplecheck.odt b/vendor/sboden/odtphp/examples/simplecheck.odt
new file mode 100755
index 0000000000..7b58991210
Binary files /dev/null and b/vendor/sboden/odtphp/examples/simplecheck.odt differ
diff --git a/vendor/sboden/odtphp/examples/simplecheck.php b/vendor/sboden/odtphp/examples/simplecheck.php
new file mode 100755
index 0000000000..b5626f99ba
--- /dev/null
+++ b/vendor/sboden/odtphp/examples/simplecheck.php
@@ -0,0 +1,110 @@
+open($filename) !== true )
+ {
+ throw new OdfException("Error while Opening the file '$filename' - Check your odt file");
+ }
+
+ // read content.xml from the oasis document
+
+ if (($contentXml = $file->getFromName('content.xml')) === false)
+ {
+ throw new OdfException("Nothing to parse - check that the content.xml file is correctly formed");
+ }
+
+ // close the original oasis document
+
+ $file->close();
+
+ // for futur use, with load content.xml via DOMDocument library :
+
+ $odt_content = new DOMDocument('1.0', 'utf-8');
+ if ($odt_content->loadXML( $contentXml ) == FALSE)
+ {
+ throw new OdfException('Unable to load content.xml by DOMDocument library ', __METHOD__);
+ }
+
+ // here, we dont use the temp function but local temporary file
+
+ $tmpfile = md5(uniqid()).'.odt';
+ if( !@copy($filename, $tmpfile) );
+ {
+ // we do not test, because sometime it return false anyway !!
+ // $errors = error_get_last();
+ // throw new OdfException("Can not copy the tempfile in $tmpfile :[".$errors['message'] ."]/[".$errors['type']."]");
+ }
+
+ // futur use here : $odt_content modifications ...
+
+
+
+
+ // open the temporary zipfile
+
+ if( $file->open($tmpfile, ZIPARCHIVE::CREATE) != TRUE )
+ {
+ @unlink($tmpfile); // erase temporary file
+ throw new OdfException("Error while Opening the tempfile '$tmpfile' - Check your odt file");
+ }
+
+ // for futur use here : with overwrite content.xml in zip file via DOMDocument library :
+
+ if (! $file->addFromString('content.xml', $odt_content->saveXML()) )
+ {
+ @unlink($tmpfile); // Erase temporary file.
+ throw new OdfException('Error during file export');
+ }
+
+ // Close the temporary zipfile.
+
+ $file->close();
+
+ // Send the new checkresult.odt file via http:
+
+ $name = "checkresult.odt";
+ $size = filesize($tmpfile);
+ header('Content-type: application/vnd.oasis.opendocument.text');
+ header('Content-Disposition: attachment; filename="' . $name . '"');
+ header("Content-Length: " . $size);
+ readfile($tmpfile); // Output.
+ @unlink($tmpfile); // Erase temporary file.
+ exit; // Be sure nothing else is write after.
diff --git a/vendor/sboden/odtphp/examples/tutorial1.odt b/vendor/sboden/odtphp/examples/tutorial1.odt
new file mode 100755
index 0000000000..f1af2afd13
Binary files /dev/null and b/vendor/sboden/odtphp/examples/tutorial1.odt differ
diff --git a/vendor/sboden/odtphp/examples/tutorial1.php b/vendor/sboden/odtphp/examples/tutorial1.php
new file mode 100755
index 0000000000..521cc33ac9
--- /dev/null
+++ b/vendor/sboden/odtphp/examples/tutorial1.php
@@ -0,0 +1,39 @@
+setVars('titre', 'PHP: Hypertext Preprocessor');
+
+// The original version was French, and we keep it in French to see the accents
+// be outputted correctly.
+$message = "PHP (sigle de PHP: Hypertext Preprocessor), est un langage de scripts libre
+principalement utilisé pour produire des pages Web dynamiques via un serveur HTTP, mais
+pouvant également fonctionner comme n'importe quel langage interprété de façon locale,
+en exécutant les programmes en ligne de commande.";
+
+$odf->setVars('message', $message);
+
+// We export the file. Note that the output will appear on stdout.
+// If you want to capture it, use something as " > output.odt".
+$odf->exportAsAttachedFile();
diff --git a/vendor/sboden/odtphp/examples/tutorial2.odt b/vendor/sboden/odtphp/examples/tutorial2.odt
new file mode 100755
index 0000000000..68ae77677b
Binary files /dev/null and b/vendor/sboden/odtphp/examples/tutorial2.odt differ
diff --git a/vendor/sboden/odtphp/examples/tutorial2.php b/vendor/sboden/odtphp/examples/tutorial2.php
new file mode 100755
index 0000000000..d0f419c8b1
--- /dev/null
+++ b/vendor/sboden/odtphp/examples/tutorial2.php
@@ -0,0 +1,35 @@
+setVars('titre','Anaska formation');
+
+$message = "Anaska, leader français de la formation informatique sur les technologies
+Open Source, propose un catalogue de plus de 50 formations, dont certaines préparent
+aux certifications Linux, MySQL, PHP et PostgreSQL.";
+
+$odf->setVars('message', $message);
+
+$odf->setImage('image', './images/anaska.jpg');
+
+// We export the file. Note that the output will appear on stdout.
+// If you want to capture it, use something as " > output.odt".
+$odf->exportAsAttachedFile();
diff --git a/vendor/sboden/odtphp/examples/tutorial3.odt b/vendor/sboden/odtphp/examples/tutorial3.odt
new file mode 100755
index 0000000000..2fc9201f88
Binary files /dev/null and b/vendor/sboden/odtphp/examples/tutorial3.odt differ
diff --git a/vendor/sboden/odtphp/examples/tutorial3.php b/vendor/sboden/odtphp/examples/tutorial3.php
new file mode 100755
index 0000000000..7d02820e6c
--- /dev/null
+++ b/vendor/sboden/odtphp/examples/tutorial3.php
@@ -0,0 +1,54 @@
+setVars('titre', 'Quelques articles de l\'encyclopédie Wikipédia');
+
+$message = "PHP (sigle de PHP: Hypertext Preprocessor), est un langage de scripts libre
+principalement utilisé pour produire des pages Web dynamiques via un serveur HTTP, mais
+pouvant également fonctionner comme n'importe quel langage interprété de façon locale,
+en exécutant les programmes en ligne de commande.";
+
+$odf->setVars('message', $message);
+
+$listeArticles = array(
+ array( 'titre' => 'PHP',
+ 'texte' => 'PHP (sigle de PHP: Hypertext Preprocessor), est un langage de scripts (...)',
+ ),
+ array( 'titre' => 'MySQL',
+ 'texte' => 'MySQL est un système de gestion de base de données (SGBD). Selon le (...)',
+ ),
+ array( 'titre' => 'Apache',
+ 'texte' => 'Apache HTTP Server, souvent appelé Apache, est un logiciel de serveur (...)',
+ ),
+);
+
+$article = $odf->setSegment('articles');
+foreach($listeArticles AS $element) {
+ $article->titreArticle($element['titre']);
+ $article->texteArticle($element['texte']);
+ $article->merge();
+}
+$odf->mergeSegment($article);
+
+// We export the file. Note that the output will appear on stdout.
+// If you want to capture it, use something as " > output.odt".
+$odf->exportAsAttachedFile();
diff --git a/vendor/sboden/odtphp/examples/tutorial4.odt b/vendor/sboden/odtphp/examples/tutorial4.odt
new file mode 100755
index 0000000000..548473d097
Binary files /dev/null and b/vendor/sboden/odtphp/examples/tutorial4.odt differ
diff --git a/vendor/sboden/odtphp/examples/tutorial4.php b/vendor/sboden/odtphp/examples/tutorial4.php
new file mode 100755
index 0000000000..8481786e61
--- /dev/null
+++ b/vendor/sboden/odtphp/examples/tutorial4.php
@@ -0,0 +1,43 @@
+setVars('titre','Articles disponibles:');
+
+$categorie = $odf->setSegment('categories');
+for ($j = 1; $j <= 2; $j++) {
+ $categorie->setVars('TitreCategorie', 'Catégorie ' . $j);
+ for ($i = 1; $i <= 3; $i++) {
+ $categorie->articles->titreArticle('Article ' . $i);
+ $categorie->articles->date(date('d/m/Y'));
+ $categorie->articles->merge();
+ }
+ for ($i = 1; $i <= 4; $i++) {
+ $categorie->commentaires->texteCommentaire('Commentaire ' . $i);
+ $categorie->commentaires->merge();
+ }
+ $categorie->merge();
+}
+$odf->mergeSegment($categorie);
+
+// We export the file. Note that the output will appear on stdout.
+// If you want to capture it, use something as " > output.odt."
+$odf->exportAsAttachedFile();
diff --git a/vendor/sboden/odtphp/examples/tutorial5.odt b/vendor/sboden/odtphp/examples/tutorial5.odt
new file mode 100755
index 0000000000..e8fea2c8fd
Binary files /dev/null and b/vendor/sboden/odtphp/examples/tutorial5.odt differ
diff --git a/vendor/sboden/odtphp/examples/tutorial5.php b/vendor/sboden/odtphp/examples/tutorial5.php
new file mode 100755
index 0000000000..05b07ae8ed
--- /dev/null
+++ b/vendor/sboden/odtphp/examples/tutorial5.php
@@ -0,0 +1,58 @@
+setVars('titre', 'Quelques articles de l\'encyclopédie Wikipédia');
+
+$message = "La force de cette encyclopédie en ligne réside dans son nombre important de
+contributeurs. Ce sont en effet des millions d'articles qui sont disponibles dans la langue
+de Shakespeare et des centaines de milliers d'autres dans de nombreuses langues dont
+le français, l'espagnol, l'italien, le turc ou encore l'allemand.";
+
+$odf->setVars('message', $message);
+
+$listeArticles = array(
+ array( 'titre' => 'PHP',
+ 'texte' => 'PHP (sigle de PHP: Hypertext Preprocessor), est un langage de scripts (...)',
+ 'image' => './images/php.gif'
+ ),
+ array( 'titre' => 'MySQL',
+ 'texte' => 'MySQL est un système de gestion de base de données (SGBD). Selon le (...)',
+ 'image' => './images/mysql.gif'
+ ),
+ array( 'titre' => 'Apache',
+ 'texte' => 'Apache HTTP Server, souvent appelé Apache, est un logiciel de serveur (...)',
+ 'image' => './images/apache.gif'
+ )
+);
+
+$article = $odf->setSegment('articles');
+foreach($listeArticles AS $element) {
+ $article->titreArticle($element['titre']);
+ $article->texteArticle($element['texte']);
+ $article->setImage('image', $element['image']);
+ $article->merge();
+}
+$odf->mergeSegment($article);
+
+// We export the file. Note that the output will appear on stdout.
+// If you want to capture it, use something as " > output.odt."
+$odf->exportAsAttachedFile();
diff --git a/vendor/sboden/odtphp/examples/tutorial6.odt b/vendor/sboden/odtphp/examples/tutorial6.odt
new file mode 100755
index 0000000000..db6808c18c
Binary files /dev/null and b/vendor/sboden/odtphp/examples/tutorial6.odt differ
diff --git a/vendor/sboden/odtphp/examples/tutorial6.php b/vendor/sboden/odtphp/examples/tutorial6.php
new file mode 100755
index 0000000000..3e3ee11f10
--- /dev/null
+++ b/vendor/sboden/odtphp/examples/tutorial6.php
@@ -0,0 +1,54 @@
+setVars('titre', 'Quelques articles de l\'encyclopédie Wikipédia');
+
+$message = "PHP (sigle de PHP: Hypertext Preprocessor), est un langage de scripts libre
+principalement utilisé pour produire des pages Web dynamiques via un serveur HTTP, mais
+pouvant également fonctionner comme n'importe quel langage interprété de façon locale,
+en exécutant les programmes en ligne de commande.";
+
+$odf->setVars('message', $message);
+
+$listeArticles = array(
+ array( 'titre' => 'PHP',
+ 'texte' => 'PHP (sigle de PHP: Hypertext Preprocessor), est un langage de scripts (...)',
+ ),
+ array( 'titre' => 'MySQL',
+ 'texte' => 'MySQL est un système de gestion de base de données (SGBD). Selon le (...)',
+ ),
+ array( 'titre' => 'Apache',
+ 'texte' => 'Apache HTTP Server, souvent appelé Apache, est un logiciel de serveur (...)',
+ ),
+);
+
+$article = $odf->setSegment('articles');
+foreach($listeArticles AS $element) {
+ $article->titreArticle($element['titre']);
+ $article->texteArticle($element['texte']);
+ $article->merge();
+}
+$odf->mergeSegment($article);
+
+// We export the file. Note that the output will appear on stdout.
+// If you want to capture it, use something as " > output.odt".
+$odf->exportAsAttachedFile();
diff --git a/vendor/sboden/odtphp/examples/tutorial7.odt b/vendor/sboden/odtphp/examples/tutorial7.odt
new file mode 100755
index 0000000000..8aa4edc6c4
Binary files /dev/null and b/vendor/sboden/odtphp/examples/tutorial7.odt differ
diff --git a/vendor/sboden/odtphp/examples/tutorial7.php b/vendor/sboden/odtphp/examples/tutorial7.php
new file mode 100755
index 0000000000..cbd501b7ba
--- /dev/null
+++ b/vendor/sboden/odtphp/examples/tutorial7.php
@@ -0,0 +1,40 @@
+ \Odtphp\Zip\PhpZipProxy::class, // Make sure you have Zip extension loaded.
+ 'DELIMITER_LEFT' => '#', // Yan can also change delimiters.
+ 'DELIMITER_RIGHT' => '#'
+);
+
+$odf = new Odf("tutorial7.odt", $config);
+
+$odf->setVars('titre', 'PHP: Hypertext Preprocessor');
+
+$message = "PHP (sigle de PHP: Hypertext Preprocessor), est un langage de scripts libre
+principalement utilisé pour produire des pages Web dynamiques via un serveur HTTP, mais
+pouvant également fonctionner comme n'importe quel langage interprété de façon locale,
+en exécutant les programmes en ligne de commande.";
+
+$odf->setVars('message', $message);
+
+// We export the file. Note that the output will appear on stdout.
+// If you want to capture it, use something as " > output.odt".
+$odf->exportAsAttachedFile();
diff --git a/vendor/sboden/odtphp/lib/pclzip.lib.php b/vendor/sboden/odtphp/lib/pclzip.lib.php
new file mode 100755
index 0000000000..6cb5dd369a
--- /dev/null
+++ b/vendor/sboden/odtphp/lib/pclzip.lib.php
@@ -0,0 +1,5464 @@
+zipname = $p_zipname;
+ $this->zip_fd = 0;
+ $this->magic_quotes_status = -1;
+
+ // ----- Return
+ return;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function :
+ // create($p_filelist, $p_add_dir="", $p_remove_dir="")
+ // create($p_filelist, $p_option, $p_option_value, ...)
+ // Description :
+ // This method supports two different synopsis. The first one is historical.
+ // This method creates a Zip Archive. The Zip file is created in the
+ // filesystem. The files and directories indicated in $p_filelist
+ // are added in the archive. See the parameters description for the
+ // supported format of $p_filelist.
+ // When a directory is in the list, the directory and its content is added
+ // in the archive.
+ // In this synopsis, the function takes an optional variable list of
+ // options. See bellow the supported options.
+ // Parameters :
+ // $p_filelist : An array containing file or directory names, or
+ // a string containing one filename or one directory name, or
+ // a string containing a list of filenames and/or directory
+ // names separated by spaces.
+ // $p_add_dir : A path to add before the real path of the archived file,
+ // in order to have it memorized in the archive.
+ // $p_remove_dir : A path to remove from the real path of the file to archive,
+ // in order to have a shorter path memorized in the archive.
+ // When $p_add_dir and $p_remove_dir are set, $p_remove_dir
+ // is removed first, before $p_add_dir is added.
+ // Options :
+ // PCLZIP_OPT_ADD_PATH :
+ // PCLZIP_OPT_REMOVE_PATH :
+ // PCLZIP_OPT_REMOVE_ALL_PATH :
+ // PCLZIP_OPT_COMMENT :
+ // PCLZIP_CB_PRE_ADD :
+ // PCLZIP_CB_POST_ADD :
+ // Return Values :
+ // 0 on failure,
+ // The list of the added files, with a status of the add action.
+ // (see PclZip::listContent() for list entry format)
+ // --------------------------------------------------------------------------------
+ public function create($p_filelist)
+ {
+ $v_result = 1;
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Set default values
+ $v_options = array();
+ $v_options[PCLZIP_OPT_NO_COMPRESSION] = false;
+
+ // ----- Look for variable options arguments
+ $v_size = func_num_args();
+
+ // ----- Look for arguments
+ if ($v_size > 1) {
+ // ----- Get the arguments
+ $v_arg_list = func_get_args();
+
+ // ----- Remove from the options list the first argument
+ array_shift($v_arg_list);
+ $v_size--;
+
+ // ----- Look for first arg
+ if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) {
+
+ // ----- Parse the options
+ $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, array(
+ PCLZIP_OPT_REMOVE_PATH => 'optional',
+ PCLZIP_OPT_REMOVE_ALL_PATH => 'optional',
+ PCLZIP_OPT_ADD_PATH => 'optional',
+ PCLZIP_CB_PRE_ADD => 'optional',
+ PCLZIP_CB_POST_ADD => 'optional',
+ PCLZIP_OPT_NO_COMPRESSION => 'optional',
+ PCLZIP_OPT_COMMENT => 'optional',
+ PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional',
+ PCLZIP_OPT_TEMP_FILE_ON => 'optional',
+ PCLZIP_OPT_TEMP_FILE_OFF => 'optional'
+ //, PCLZIP_OPT_CRYPT => 'optional'
+ ));
+ if ($v_result != 1) {
+ return 0;
+ }
+
+ // ----- Look for 2 args
+ // Here we need to support the first historic synopsis of the
+ // method.
+ } else {
+
+ // ----- Get the first argument
+ $v_options[PCLZIP_OPT_ADD_PATH] = $v_arg_list[0];
+
+ // ----- Look for the optional second argument
+ if ($v_size == 2) {
+ $v_options[PCLZIP_OPT_REMOVE_PATH] = $v_arg_list[1];
+ } elseif ($v_size > 2) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid number / type of arguments");
+
+ return 0;
+ }
+ }
+ }
+
+ // ----- Look for default option values
+ $this->privOptionDefaultThreshold($v_options);
+
+ // ----- Init
+ $v_string_list = array();
+ $v_att_list = array();
+ $v_filedescr_list = array();
+ $p_result_list = array();
+
+ // ----- Look if the $p_filelist is really an array
+ if (is_array($p_filelist)) {
+
+ // ----- Look if the first element is also an array
+ // This will mean that this is a file description entry
+ if (isset($p_filelist[0]) && is_array($p_filelist[0])) {
+ $v_att_list = $p_filelist;
+
+ // ----- The list is a list of string names
+ } else {
+ $v_string_list = $p_filelist;
+ }
+
+ // ----- Look if the $p_filelist is a string
+ } elseif (is_string($p_filelist)) {
+ // ----- Create a list from the string
+ $v_string_list = explode(PCLZIP_SEPARATOR, $p_filelist);
+
+ // ----- Invalid variable type for $p_filelist
+ } else {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type p_filelist");
+
+ return 0;
+ }
+
+ // ----- Reformat the string list
+ if (sizeof($v_string_list) != 0) {
+ foreach ($v_string_list as $v_string) {
+ if ($v_string != '') {
+ $v_att_list[][PCLZIP_ATT_FILE_NAME] = $v_string;
+ } else {
+ }
+ }
+ }
+
+ // ----- For each file in the list check the attributes
+ $v_supported_attributes = array(
+ PCLZIP_ATT_FILE_NAME => 'mandatory',
+ PCLZIP_ATT_FILE_NEW_SHORT_NAME => 'optional',
+ PCLZIP_ATT_FILE_NEW_FULL_NAME => 'optional',
+ PCLZIP_ATT_FILE_MTIME => 'optional',
+ PCLZIP_ATT_FILE_CONTENT => 'optional',
+ PCLZIP_ATT_FILE_COMMENT => 'optional'
+ );
+ foreach ($v_att_list as $v_entry) {
+ $v_result = $this->privFileDescrParseAtt($v_entry, $v_filedescr_list[], $v_options, $v_supported_attributes);
+ if ($v_result != 1) {
+ return 0;
+ }
+ }
+
+ // ----- Expand the filelist (expand directories)
+ $v_result = $this->privFileDescrExpand($v_filedescr_list, $v_options);
+ if ($v_result != 1) {
+ return 0;
+ }
+
+ // ----- Call the create fct
+ $v_result = $this->privCreate($v_filedescr_list, $p_result_list, $v_options);
+ if ($v_result != 1) {
+ return 0;
+ }
+
+ // ----- Return
+ return $p_result_list;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function :
+ // add($p_filelist, $p_add_dir="", $p_remove_dir="")
+ // add($p_filelist, $p_option, $p_option_value, ...)
+ // Description :
+ // This method supports two synopsis. The first one is historical.
+ // This methods add the list of files in an existing archive.
+ // If a file with the same name already exists, it is added at the end of the
+ // archive, the first one is still present.
+ // If the archive does not exist, it is created.
+ // Parameters :
+ // $p_filelist : An array containing file or directory names, or
+ // a string containing one filename or one directory name, or
+ // a string containing a list of filenames and/or directory
+ // names separated by spaces.
+ // $p_add_dir : A path to add before the real path of the archived file,
+ // in order to have it memorized in the archive.
+ // $p_remove_dir : A path to remove from the real path of the file to archive,
+ // in order to have a shorter path memorized in the archive.
+ // When $p_add_dir and $p_remove_dir are set, $p_remove_dir
+ // is removed first, before $p_add_dir is added.
+ // Options :
+ // PCLZIP_OPT_ADD_PATH :
+ // PCLZIP_OPT_REMOVE_PATH :
+ // PCLZIP_OPT_REMOVE_ALL_PATH :
+ // PCLZIP_OPT_COMMENT :
+ // PCLZIP_OPT_ADD_COMMENT :
+ // PCLZIP_OPT_PREPEND_COMMENT :
+ // PCLZIP_CB_PRE_ADD :
+ // PCLZIP_CB_POST_ADD :
+ // Return Values :
+ // 0 on failure,
+ // The list of the added files, with a status of the add action.
+ // (see PclZip::listContent() for list entry format)
+ // --------------------------------------------------------------------------------
+ public function add($p_filelist)
+ {
+ $v_result = 1;
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Set default values
+ $v_options = array();
+ $v_options[PCLZIP_OPT_NO_COMPRESSION] = false;
+
+ // ----- Look for variable options arguments
+ $v_size = func_num_args();
+
+ // ----- Look for arguments
+ if ($v_size > 1) {
+ // ----- Get the arguments
+ $v_arg_list = func_get_args();
+
+ // ----- Remove form the options list the first argument
+ array_shift($v_arg_list);
+ $v_size--;
+
+ // ----- Look for first arg
+ if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) {
+
+ // ----- Parse the options
+ $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, array(
+ PCLZIP_OPT_REMOVE_PATH => 'optional',
+ PCLZIP_OPT_REMOVE_ALL_PATH => 'optional',
+ PCLZIP_OPT_ADD_PATH => 'optional',
+ PCLZIP_CB_PRE_ADD => 'optional',
+ PCLZIP_CB_POST_ADD => 'optional',
+ PCLZIP_OPT_NO_COMPRESSION => 'optional',
+ PCLZIP_OPT_COMMENT => 'optional',
+ PCLZIP_OPT_ADD_COMMENT => 'optional',
+ PCLZIP_OPT_PREPEND_COMMENT => 'optional',
+ PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional',
+ PCLZIP_OPT_TEMP_FILE_ON => 'optional',
+ PCLZIP_OPT_TEMP_FILE_OFF => 'optional'
+ //, PCLZIP_OPT_CRYPT => 'optional'
+ ));
+ if ($v_result != 1) {
+ return 0;
+ }
+
+ // ----- Look for 2 args
+ // Here we need to support the first historic synopsis of the
+ // method.
+ } else {
+
+ // ----- Get the first argument
+ $v_options[PCLZIP_OPT_ADD_PATH] = $v_add_path = $v_arg_list[0];
+
+ // ----- Look for the optional second argument
+ if ($v_size == 2) {
+ $v_options[PCLZIP_OPT_REMOVE_PATH] = $v_arg_list[1];
+ } elseif ($v_size > 2) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid number / type of arguments");
+
+ // ----- Return
+ return 0;
+ }
+ }
+ }
+
+ // ----- Look for default option values
+ $this->privOptionDefaultThreshold($v_options);
+
+ // ----- Init
+ $v_string_list = array();
+ $v_att_list = array();
+ $v_filedescr_list = array();
+ $p_result_list = array();
+
+ // ----- Look if the $p_filelist is really an array
+ if (is_array($p_filelist)) {
+
+ // ----- Look if the first element is also an array
+ // This will mean that this is a file description entry
+ if (isset($p_filelist[0]) && is_array($p_filelist[0])) {
+ $v_att_list = $p_filelist;
+
+ // ----- The list is a list of string names
+ } else {
+ $v_string_list = $p_filelist;
+ }
+
+ // ----- Look if the $p_filelist is a string
+ } elseif (is_string($p_filelist)) {
+ // ----- Create a list from the string
+ $v_string_list = explode(PCLZIP_SEPARATOR, $p_filelist);
+
+ // ----- Invalid variable type for $p_filelist
+ } else {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type '" . gettype($p_filelist) . "' for p_filelist");
+
+ return 0;
+ }
+
+ // ----- Reformat the string list
+ if (sizeof($v_string_list) != 0) {
+ foreach ($v_string_list as $v_string) {
+ $v_att_list[][PCLZIP_ATT_FILE_NAME] = $v_string;
+ }
+ }
+
+ // ----- For each file in the list check the attributes
+ $v_supported_attributes = array(
+ PCLZIP_ATT_FILE_NAME => 'mandatory',
+ PCLZIP_ATT_FILE_NEW_SHORT_NAME => 'optional',
+ PCLZIP_ATT_FILE_NEW_FULL_NAME => 'optional',
+ PCLZIP_ATT_FILE_MTIME => 'optional',
+ PCLZIP_ATT_FILE_CONTENT => 'optional',
+ PCLZIP_ATT_FILE_COMMENT => 'optional'
+ );
+ foreach ($v_att_list as $v_entry) {
+ $v_result = $this->privFileDescrParseAtt($v_entry, $v_filedescr_list[], $v_options, $v_supported_attributes);
+ if ($v_result != 1) {
+ return 0;
+ }
+ }
+
+ // ----- Expand the filelist (expand directories)
+ $v_result = $this->privFileDescrExpand($v_filedescr_list, $v_options);
+ if ($v_result != 1) {
+ return 0;
+ }
+
+ // ----- Call the create fct
+ $v_result = $this->privAdd($v_filedescr_list, $p_result_list, $v_options);
+ if ($v_result != 1) {
+ return 0;
+ }
+
+ // ----- Return
+ return $p_result_list;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : listContent()
+ // Description :
+ // This public method, gives the list of the files and directories, with their
+ // properties.
+ // The properties of each entries in the list are (used also in other functions) :
+ // filename : Name of the file. For a create or add action it is the filename
+ // given by the user. For an extract function it is the filename
+ // of the extracted file.
+ // stored_filename : Name of the file / directory stored in the archive.
+ // size : Size of the stored file.
+ // compressed_size : Size of the file's data compressed in the archive
+ // (without the headers overhead)
+ // mtime : Last known modification date of the file (UNIX timestamp)
+ // comment : Comment associated with the file
+ // folder : true | false
+ // index : index of the file in the archive
+ // status : status of the action (depending of the action) :
+ // Values are :
+ // ok : OK !
+ // filtered : the file / dir is not extracted (filtered by user)
+ // already_a_directory : the file can not be extracted because a
+ // directory with the same name already exists
+ // write_protected : the file can not be extracted because a file
+ // with the same name already exists and is
+ // write protected
+ // newer_exist : the file was not extracted because a newer file exists
+ // path_creation_fail : the file is not extracted because the folder
+ // does not exist and can not be created
+ // write_error : the file was not extracted because there was a
+ // error while writing the file
+ // read_error : the file was not extracted because there was a error
+ // while reading the file
+ // invalid_header : the file was not extracted because of an archive
+ // format error (bad file header)
+ // Note that each time a method can continue operating when there
+ // is an action error on a file, the error is only logged in the file status.
+ // Return Values :
+ // 0 on an unrecoverable failure,
+ // The list of the files in the archive.
+ // --------------------------------------------------------------------------------
+ public function listContent()
+ {
+ $v_result = 1;
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Check archive
+ if (!$this->privCheckFormat()) {
+ return (0);
+ }
+
+ // ----- Call the extracting fct
+ $p_list = array();
+ if (($v_result = $this->privList($p_list)) != 1) {
+ unset($p_list);
+
+ return (0);
+ }
+
+ // ----- Return
+ return $p_list;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function :
+ // extract($p_path="./", $p_remove_path="")
+ // extract([$p_option, $p_option_value, ...])
+ // Description :
+ // This method supports two synopsis. The first one is historical.
+ // This method extract all the files / directories from the archive to the
+ // folder indicated in $p_path.
+ // If you want to ignore the 'root' part of path of the memorized files
+ // you can indicate this in the optional $p_remove_path parameter.
+ // By default, if a newer file with the same name already exists, the
+ // file is not extracted.
+ //
+ // If both PCLZIP_OPT_PATH and PCLZIP_OPT_ADD_PATH aoptions
+ // are used, the path indicated in PCLZIP_OPT_ADD_PATH is append
+ // at the end of the path value of PCLZIP_OPT_PATH.
+ // Parameters :
+ // $p_path : Path where the files and directories are to be extracted
+ // $p_remove_path : First part ('root' part) of the memorized path
+ // (if any similar) to remove while extracting.
+ // Options :
+ // PCLZIP_OPT_PATH :
+ // PCLZIP_OPT_ADD_PATH :
+ // PCLZIP_OPT_REMOVE_PATH :
+ // PCLZIP_OPT_REMOVE_ALL_PATH :
+ // PCLZIP_CB_PRE_EXTRACT :
+ // PCLZIP_CB_POST_EXTRACT :
+ // Return Values :
+ // 0 or a negative value on failure,
+ // The list of the extracted files, with a status of the action.
+ // (see PclZip::listContent() for list entry format)
+ // --------------------------------------------------------------------------------
+ public function extract()
+ {
+ $v_result = 1;
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Check archive
+ if (!$this->privCheckFormat()) {
+ return (0);
+ }
+
+ // ----- Set default values
+ $v_options = array();
+ // $v_path = "./";
+ $v_path = '';
+ $v_remove_path = "";
+ $v_remove_all_path = false;
+
+ // ----- Look for variable options arguments
+ $v_size = func_num_args();
+
+ // ----- Default values for option
+ $v_options[PCLZIP_OPT_EXTRACT_AS_STRING] = false;
+
+ // ----- Look for arguments
+ if ($v_size > 0) {
+ // ----- Get the arguments
+ $v_arg_list = func_get_args();
+
+ // ----- Look for first arg
+ if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) {
+
+ // ----- Parse the options
+ $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, array(
+ PCLZIP_OPT_PATH => 'optional',
+ PCLZIP_OPT_REMOVE_PATH => 'optional',
+ PCLZIP_OPT_REMOVE_ALL_PATH => 'optional',
+ PCLZIP_OPT_ADD_PATH => 'optional',
+ PCLZIP_CB_PRE_EXTRACT => 'optional',
+ PCLZIP_CB_POST_EXTRACT => 'optional',
+ PCLZIP_OPT_SET_CHMOD => 'optional',
+ PCLZIP_OPT_BY_NAME => 'optional',
+ PCLZIP_OPT_BY_EREG => 'optional',
+ PCLZIP_OPT_BY_PREG => 'optional',
+ PCLZIP_OPT_BY_INDEX => 'optional',
+ PCLZIP_OPT_EXTRACT_AS_STRING => 'optional',
+ PCLZIP_OPT_EXTRACT_IN_OUTPUT => 'optional',
+ PCLZIP_OPT_REPLACE_NEWER => 'optional',
+ PCLZIP_OPT_STOP_ON_ERROR => 'optional',
+ PCLZIP_OPT_EXTRACT_DIR_RESTRICTION => 'optional',
+ PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional',
+ PCLZIP_OPT_TEMP_FILE_ON => 'optional',
+ PCLZIP_OPT_TEMP_FILE_OFF => 'optional'
+ ));
+ if ($v_result != 1) {
+ return 0;
+ }
+
+ // ----- Set the arguments
+ if (isset($v_options[PCLZIP_OPT_PATH])) {
+ $v_path = $v_options[PCLZIP_OPT_PATH];
+ }
+ if (isset($v_options[PCLZIP_OPT_REMOVE_PATH])) {
+ $v_remove_path = $v_options[PCLZIP_OPT_REMOVE_PATH];
+ }
+ if (isset($v_options[PCLZIP_OPT_REMOVE_ALL_PATH])) {
+ $v_remove_all_path = $v_options[PCLZIP_OPT_REMOVE_ALL_PATH];
+ }
+ if (isset($v_options[PCLZIP_OPT_ADD_PATH])) {
+ // ----- Check for '/' in last path char
+ if ((strlen($v_path) > 0) && (substr($v_path, -1) != '/')) {
+ $v_path .= '/';
+ }
+ $v_path .= $v_options[PCLZIP_OPT_ADD_PATH];
+ }
+
+ // ----- Look for 2 args
+ // Here we need to support the first historic synopsis of the
+ // method.
+ } else {
+
+ // ----- Get the first argument
+ $v_path = $v_arg_list[0];
+
+ // ----- Look for the optional second argument
+ if ($v_size == 2) {
+ $v_remove_path = $v_arg_list[1];
+ } elseif ($v_size > 2) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid number / type of arguments");
+
+ // ----- Return
+ return 0;
+ }
+ }
+ }
+
+ // ----- Look for default option values
+ $this->privOptionDefaultThreshold($v_options);
+
+ // ----- Trace
+
+ // ----- Call the extracting fct
+ $p_list = array();
+ $v_result = $this->privExtractByRule($p_list, $v_path, $v_remove_path, $v_remove_all_path, $v_options);
+ if ($v_result < 1) {
+ unset($p_list);
+
+ return (0);
+ }
+
+ // ----- Return
+ return $p_list;
+ }
+ // --------------------------------------------------------------------------------
+
+
+ // --------------------------------------------------------------------------------
+ // Function :
+ // extractByIndex($p_index, $p_path="./", $p_remove_path="")
+ // extractByIndex($p_index, [$p_option, $p_option_value, ...])
+ // Description :
+ // This method supports two synopsis. The first one is historical.
+ // This method is doing a partial extract of the archive.
+ // The extracted files or folders are identified by their index in the
+ // archive (from 0 to n).
+ // Note that if the index identify a folder, only the folder entry is
+ // extracted, not all the files included in the archive.
+ // Parameters :
+ // $p_index : A single index (integer) or a string of indexes of files to
+ // extract. The form of the string is "0,4-6,8-12" with only numbers
+ // and '-' for range or ',' to separate ranges. No spaces or ';'
+ // are allowed.
+ // $p_path : Path where the files and directories are to be extracted
+ // $p_remove_path : First part ('root' part) of the memorized path
+ // (if any similar) to remove while extracting.
+ // Options :
+ // PCLZIP_OPT_PATH :
+ // PCLZIP_OPT_ADD_PATH :
+ // PCLZIP_OPT_REMOVE_PATH :
+ // PCLZIP_OPT_REMOVE_ALL_PATH :
+ // PCLZIP_OPT_EXTRACT_AS_STRING : The files are extracted as strings and
+ // not as files.
+ // The resulting content is in a new field 'content' in the file
+ // structure.
+ // This option must be used alone (any other options are ignored).
+ // PCLZIP_CB_PRE_EXTRACT :
+ // PCLZIP_CB_POST_EXTRACT :
+ // Return Values :
+ // 0 on failure,
+ // The list of the extracted files, with a status of the action.
+ // (see PclZip::listContent() for list entry format)
+ // --------------------------------------------------------------------------------
+ //function extractByIndex($p_index, options...)
+ public function extractByIndex($p_index)
+ {
+ $v_result = 1;
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Check archive
+ if (!$this->privCheckFormat()) {
+ return (0);
+ }
+
+ // ----- Set default values
+ $v_options = array();
+ // $v_path = "./";
+ $v_path = '';
+ $v_remove_path = "";
+ $v_remove_all_path = false;
+
+ // ----- Look for variable options arguments
+ $v_size = func_num_args();
+
+ // ----- Default values for option
+ $v_options[PCLZIP_OPT_EXTRACT_AS_STRING] = false;
+
+ // ----- Look for arguments
+ if ($v_size > 1) {
+ // ----- Get the arguments
+ $v_arg_list = func_get_args();
+
+ // ----- Remove form the options list the first argument
+ array_shift($v_arg_list);
+ $v_size--;
+
+ // ----- Look for first arg
+ if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) {
+
+ // ----- Parse the options
+ $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, array(
+ PCLZIP_OPT_PATH => 'optional',
+ PCLZIP_OPT_REMOVE_PATH => 'optional',
+ PCLZIP_OPT_REMOVE_ALL_PATH => 'optional',
+ PCLZIP_OPT_EXTRACT_AS_STRING => 'optional',
+ PCLZIP_OPT_ADD_PATH => 'optional',
+ PCLZIP_CB_PRE_EXTRACT => 'optional',
+ PCLZIP_CB_POST_EXTRACT => 'optional',
+ PCLZIP_OPT_SET_CHMOD => 'optional',
+ PCLZIP_OPT_REPLACE_NEWER => 'optional',
+ PCLZIP_OPT_STOP_ON_ERROR => 'optional',
+ PCLZIP_OPT_EXTRACT_DIR_RESTRICTION => 'optional',
+ PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional',
+ PCLZIP_OPT_TEMP_FILE_ON => 'optional',
+ PCLZIP_OPT_TEMP_FILE_OFF => 'optional'
+ ));
+ if ($v_result != 1) {
+ return 0;
+ }
+
+ // ----- Set the arguments
+ if (isset($v_options[PCLZIP_OPT_PATH])) {
+ $v_path = $v_options[PCLZIP_OPT_PATH];
+ }
+ if (isset($v_options[PCLZIP_OPT_REMOVE_PATH])) {
+ $v_remove_path = $v_options[PCLZIP_OPT_REMOVE_PATH];
+ }
+ if (isset($v_options[PCLZIP_OPT_REMOVE_ALL_PATH])) {
+ $v_remove_all_path = $v_options[PCLZIP_OPT_REMOVE_ALL_PATH];
+ }
+ if (isset($v_options[PCLZIP_OPT_ADD_PATH])) {
+ // ----- Check for '/' in last path char
+ if ((strlen($v_path) > 0) && (substr($v_path, -1) != '/')) {
+ $v_path .= '/';
+ }
+ $v_path .= $v_options[PCLZIP_OPT_ADD_PATH];
+ }
+ if (!isset($v_options[PCLZIP_OPT_EXTRACT_AS_STRING])) {
+ $v_options[PCLZIP_OPT_EXTRACT_AS_STRING] = false;
+ } else {
+ }
+
+ // ----- Look for 2 args
+ // Here we need to support the first historic synopsis of the
+ // method.
+ } else {
+
+ // ----- Get the first argument
+ $v_path = $v_arg_list[0];
+
+ // ----- Look for the optional second argument
+ if ($v_size == 2) {
+ $v_remove_path = $v_arg_list[1];
+ } elseif ($v_size > 2) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid number / type of arguments");
+
+ // ----- Return
+ return 0;
+ }
+ }
+ }
+
+ // ----- Trace
+
+ // ----- Trick
+ // Here I want to reuse extractByRule(), so I need to parse the $p_index
+ // with privParseOptions()
+ $v_arg_trick = array(
+ PCLZIP_OPT_BY_INDEX,
+ $p_index
+ );
+ $v_options_trick = array();
+ $v_result = $this->privParseOptions($v_arg_trick, sizeof($v_arg_trick), $v_options_trick, array(
+ PCLZIP_OPT_BY_INDEX => 'optional'
+ ));
+ if ($v_result != 1) {
+ return 0;
+ }
+ $v_options[PCLZIP_OPT_BY_INDEX] = $v_options_trick[PCLZIP_OPT_BY_INDEX];
+
+ // ----- Look for default option values
+ $this->privOptionDefaultThreshold($v_options);
+
+ // ----- Call the extracting fct
+ if (($v_result = $this->privExtractByRule($p_list, $v_path, $v_remove_path, $v_remove_all_path, $v_options)) < 1) {
+ return (0);
+ }
+
+ // ----- Return
+ return $p_list;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function :
+ // delete([$p_option, $p_option_value, ...])
+ // Description :
+ // This method removes files from the archive.
+ // If no parameters are given, then all the archive is emptied.
+ // Parameters :
+ // None or optional arguments.
+ // Options :
+ // PCLZIP_OPT_BY_INDEX :
+ // PCLZIP_OPT_BY_NAME :
+ // PCLZIP_OPT_BY_EREG :
+ // PCLZIP_OPT_BY_PREG :
+ // Return Values :
+ // 0 on failure,
+ // The list of the files which are still present in the archive.
+ // (see PclZip::listContent() for list entry format)
+ // --------------------------------------------------------------------------------
+ public function delete()
+ {
+ $v_result = 1;
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Check archive
+ if (!$this->privCheckFormat()) {
+ return (0);
+ }
+
+ // ----- Set default values
+ $v_options = array();
+
+ // ----- Look for variable options arguments
+ $v_size = func_num_args();
+
+ // ----- Look for arguments
+ if ($v_size > 0) {
+ // ----- Get the arguments
+ $v_arg_list = func_get_args();
+
+ // ----- Parse the options
+ $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, array(
+ PCLZIP_OPT_BY_NAME => 'optional',
+ PCLZIP_OPT_BY_EREG => 'optional',
+ PCLZIP_OPT_BY_PREG => 'optional',
+ PCLZIP_OPT_BY_INDEX => 'optional'
+ ));
+ if ($v_result != 1) {
+ return 0;
+ }
+ }
+
+ // ----- Magic quotes trick
+ $this->privDisableMagicQuotes();
+
+ // ----- Call the delete fct
+ $v_list = array();
+ if (($v_result = $this->privDeleteByRule($v_list, $v_options)) != 1) {
+ $this->privSwapBackMagicQuotes();
+ unset($v_list);
+
+ return (0);
+ }
+
+ // ----- Magic quotes trick
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Return
+ return $v_list;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : deleteByIndex()
+ // Description :
+ // ***** Deprecated *****
+ // delete(PCLZIP_OPT_BY_INDEX, $p_index) should be prefered.
+ // --------------------------------------------------------------------------------
+ public function deleteByIndex($p_index)
+ {
+
+ $p_list = $this->delete(PCLZIP_OPT_BY_INDEX, $p_index);
+
+ // ----- Return
+ return $p_list;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : properties()
+ // Description :
+ // This method gives the properties of the archive.
+ // The properties are :
+ // nb : Number of files in the archive
+ // comment : Comment associated with the archive file
+ // status : not_exist, ok
+ // Parameters :
+ // None
+ // Return Values :
+ // 0 on failure,
+ // An array with the archive properties.
+ // --------------------------------------------------------------------------------
+ public function properties()
+ {
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Magic quotes trick
+ $this->privDisableMagicQuotes();
+
+ // ----- Check archive
+ if (!$this->privCheckFormat()) {
+ $this->privSwapBackMagicQuotes();
+
+ return (0);
+ }
+
+ // ----- Default properties
+ $v_prop = array();
+ $v_prop['comment'] = '';
+ $v_prop['nb'] = 0;
+ $v_prop['status'] = 'not_exist';
+
+ // ----- Look if file exists
+ if (@is_file($this->zipname)) {
+ // ----- Open the zip file
+ if (($this->zip_fd = @fopen($this->zipname, 'rb')) == 0) {
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive \'' . $this->zipname . '\' in binary read mode');
+
+ // ----- Return
+ return 0;
+ }
+
+ // ----- Read the central directory informations
+ $v_central_dir = array();
+ if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) {
+ $this->privSwapBackMagicQuotes();
+
+ return 0;
+ }
+
+ // ----- Close the zip file
+ $this->privCloseFd();
+
+ // ----- Set the user attributes
+ $v_prop['comment'] = $v_central_dir['comment'];
+ $v_prop['nb'] = $v_central_dir['entries'];
+ $v_prop['status'] = 'ok';
+ }
+
+ // ----- Magic quotes trick
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Return
+ return $v_prop;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : duplicate()
+ // Description :
+ // This method creates an archive by copying the content of an other one. If
+ // the archive already exist, it is replaced by the new one without any warning.
+ // Parameters :
+ // $p_archive : The filename of a valid archive, or
+ // a valid PclZip object.
+ // Return Values :
+ // 1 on success.
+ // 0 or a negative value on error (error code).
+ // --------------------------------------------------------------------------------
+ public function duplicate($p_archive)
+ {
+ $v_result = 1;
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Look if the $p_archive is a PclZip object
+ if ((is_object($p_archive)) && (get_class($p_archive) == 'pclzip')) {
+
+ // ----- Duplicate the archive
+ $v_result = $this->privDuplicate($p_archive->zipname);
+
+ // ----- Look if the $p_archive is a string (so a filename)
+ } elseif (is_string($p_archive)) {
+
+ // ----- Check that $p_archive is a valid zip file
+ // TBC : Should also check the archive format
+ if (!is_file($p_archive)) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_FILE, "No file with filename '" . $p_archive . "'");
+ $v_result = PCLZIP_ERR_MISSING_FILE;
+ } else {
+ // ----- Duplicate the archive
+ $v_result = $this->privDuplicate($p_archive);
+ }
+
+ // ----- Invalid variable
+ } else {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type p_archive_to_add");
+ $v_result = PCLZIP_ERR_INVALID_PARAMETER;
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : merge()
+ // Description :
+ // This method merge the $p_archive_to_add archive at the end of the current
+ // one ($this).
+ // If the archive ($this) does not exist, the merge becomes a duplicate.
+ // If the $p_archive_to_add archive does not exist, the merge is a success.
+ // Parameters :
+ // $p_archive_to_add : It can be directly the filename of a valid zip archive,
+ // or a PclZip object archive.
+ // Return Values :
+ // 1 on success,
+ // 0 or negative values on error (see below).
+ // --------------------------------------------------------------------------------
+ public function merge($p_archive_to_add)
+ {
+ $v_result = 1;
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Check archive
+ if (!$this->privCheckFormat()) {
+ return (0);
+ }
+
+ // ----- Look if the $p_archive_to_add is a PclZip object
+ if ((is_object($p_archive_to_add)) && (get_class($p_archive_to_add) == 'pclzip')) {
+
+ // ----- Merge the archive
+ $v_result = $this->privMerge($p_archive_to_add);
+
+ // ----- Look if the $p_archive_to_add is a string (so a filename)
+ } elseif (is_string($p_archive_to_add)) {
+
+ // ----- Create a temporary archive
+ $v_object_archive = new PclZip($p_archive_to_add);
+
+ // ----- Merge the archive
+ $v_result = $this->privMerge($v_object_archive);
+
+ // ----- Invalid variable
+ } else {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type p_archive_to_add");
+ $v_result = PCLZIP_ERR_INVALID_PARAMETER;
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : errorCode()
+ // Description :
+ // Parameters :
+ // --------------------------------------------------------------------------------
+ public function errorCode()
+ {
+ if (PCLZIP_ERROR_EXTERNAL == 1) {
+ return (PclErrorCode());
+ } else {
+ return ($this->error_code);
+ }
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : errorName()
+ // Description :
+ // Parameters :
+ // --------------------------------------------------------------------------------
+ public function errorName($p_with_code = false)
+ {
+ $v_name = array(
+ PCLZIP_ERR_NO_ERROR => 'PCLZIP_ERR_NO_ERROR',
+ PCLZIP_ERR_WRITE_OPEN_FAIL => 'PCLZIP_ERR_WRITE_OPEN_FAIL',
+ PCLZIP_ERR_READ_OPEN_FAIL => 'PCLZIP_ERR_READ_OPEN_FAIL',
+ PCLZIP_ERR_INVALID_PARAMETER => 'PCLZIP_ERR_INVALID_PARAMETER',
+ PCLZIP_ERR_MISSING_FILE => 'PCLZIP_ERR_MISSING_FILE',
+ PCLZIP_ERR_FILENAME_TOO_LONG => 'PCLZIP_ERR_FILENAME_TOO_LONG',
+ PCLZIP_ERR_INVALID_ZIP => 'PCLZIP_ERR_INVALID_ZIP',
+ PCLZIP_ERR_BAD_EXTRACTED_FILE => 'PCLZIP_ERR_BAD_EXTRACTED_FILE',
+ PCLZIP_ERR_DIR_CREATE_FAIL => 'PCLZIP_ERR_DIR_CREATE_FAIL',
+ PCLZIP_ERR_BAD_EXTENSION => 'PCLZIP_ERR_BAD_EXTENSION',
+ PCLZIP_ERR_BAD_FORMAT => 'PCLZIP_ERR_BAD_FORMAT',
+ PCLZIP_ERR_DELETE_FILE_FAIL => 'PCLZIP_ERR_DELETE_FILE_FAIL',
+ PCLZIP_ERR_RENAME_FILE_FAIL => 'PCLZIP_ERR_RENAME_FILE_FAIL',
+ PCLZIP_ERR_BAD_CHECKSUM => 'PCLZIP_ERR_BAD_CHECKSUM',
+ PCLZIP_ERR_INVALID_ARCHIVE_ZIP => 'PCLZIP_ERR_INVALID_ARCHIVE_ZIP',
+ PCLZIP_ERR_MISSING_OPTION_VALUE => 'PCLZIP_ERR_MISSING_OPTION_VALUE',
+ PCLZIP_ERR_INVALID_OPTION_VALUE => 'PCLZIP_ERR_INVALID_OPTION_VALUE',
+ PCLZIP_ERR_UNSUPPORTED_COMPRESSION => 'PCLZIP_ERR_UNSUPPORTED_COMPRESSION',
+ PCLZIP_ERR_UNSUPPORTED_ENCRYPTION => 'PCLZIP_ERR_UNSUPPORTED_ENCRYPTION',
+ PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE => 'PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE',
+ PCLZIP_ERR_DIRECTORY_RESTRICTION => 'PCLZIP_ERR_DIRECTORY_RESTRICTION'
+ );
+
+ if (isset($v_name[$this->error_code])) {
+ $v_value = $v_name[$this->error_code];
+ } else {
+ $v_value = 'NoName';
+ }
+
+ if ($p_with_code) {
+ return ($v_value . ' (' . $this->error_code . ')');
+ } else {
+ return ($v_value);
+ }
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : errorInfo()
+ // Description :
+ // Parameters :
+ // --------------------------------------------------------------------------------
+ public function errorInfo($p_full = false)
+ {
+ if (PCLZIP_ERROR_EXTERNAL == 1) {
+ return (PclErrorString());
+ } else {
+ if ($p_full) {
+ return ($this->errorName(true) . " : " . $this->error_string);
+ } else {
+ return ($this->error_string . " [code " . $this->error_code . "]");
+ }
+ }
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // ***** UNDER THIS LINE ARE DEFINED PRIVATE INTERNAL FUNCTIONS *****
+ // ***** *****
+ // ***** THESES FUNCTIONS MUST NOT BE USED DIRECTLY *****
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privCheckFormat()
+ // Description :
+ // This method check that the archive exists and is a valid zip archive.
+ // Several level of check exists. (futur)
+ // Parameters :
+ // $p_level : Level of check. Default 0.
+ // 0 : Check the first bytes (magic codes) (default value))
+ // 1 : 0 + Check the central directory (futur)
+ // 2 : 1 + Check each file header (futur)
+ // Return Values :
+ // true on success,
+ // false on error, the error code is set.
+ // --------------------------------------------------------------------------------
+ public function privCheckFormat($p_level = 0)
+ {
+ $v_result = true;
+
+ // ----- Reset the file system cache
+ clearstatcache();
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Look if the file exits
+ if (!is_file($this->zipname)) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_FILE, "Missing archive file '" . $this->zipname . "'");
+
+ return (false);
+ }
+
+ // ----- Check that the file is readeable
+ if (!is_readable($this->zipname)) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, "Unable to read archive '" . $this->zipname . "'");
+
+ return (false);
+ }
+
+ // ----- Check the magic code
+ // TBC
+
+ // ----- Check the central header
+ // TBC
+
+ // ----- Check each file header
+ // TBC
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privParseOptions()
+ // Description :
+ // This internal methods reads the variable list of arguments ($p_options_list,
+ // $p_size) and generate an array with the options and values ($v_result_list).
+ // $v_requested_options contains the options that can be present and those that
+ // must be present.
+ // $v_requested_options is an array, with the option value as key, and 'optional',
+ // or 'mandatory' as value.
+ // Parameters :
+ // See above.
+ // Return Values :
+ // 1 on success.
+ // 0 on failure.
+ // --------------------------------------------------------------------------------
+ public function privParseOptions(&$p_options_list, $p_size, &$v_result_list, $v_requested_options = false)
+ {
+ $v_result = 1;
+
+ // ----- Read the options
+ $i = 0;
+ while ($i < $p_size) {
+
+ // ----- Check if the option is supported
+ if (!isset($v_requested_options[$p_options_list[$i]])) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid optional parameter '" . $p_options_list[$i] . "' for this method");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Look for next option
+ switch ($p_options_list[$i]) {
+ // ----- Look for options that request a path value
+ case PCLZIP_OPT_PATH:
+ case PCLZIP_OPT_REMOVE_PATH:
+ case PCLZIP_OPT_ADD_PATH:
+ // ----- Check the number of parameters
+ if (($i + 1) >= $p_size) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Get the value
+ $v_result_list[$p_options_list[$i]] = PclZipUtilTranslateWinPath($p_options_list[$i + 1], false);
+ $i++;
+ break;
+
+ case PCLZIP_OPT_TEMP_FILE_THRESHOLD:
+ // ----- Check the number of parameters
+ if (($i + 1) >= $p_size) {
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Check for incompatible options
+ if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_OFF])) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '" . PclZipUtilOptionText($p_options_list[$i]) . "' can not be used with option 'PCLZIP_OPT_TEMP_FILE_OFF'");
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Check the value
+ $v_value = $p_options_list[$i + 1];
+ if ((!is_integer($v_value)) || ($v_value < 0)) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Integer expected for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Get the value (and convert it in bytes)
+ $v_result_list[$p_options_list[$i]] = $v_value * 1048576;
+ $i++;
+ break;
+
+ case PCLZIP_OPT_TEMP_FILE_ON:
+ // ----- Check for incompatible options
+ if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_OFF])) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '" . PclZipUtilOptionText($p_options_list[$i]) . "' can not be used with option 'PCLZIP_OPT_TEMP_FILE_OFF'");
+
+ return PclZip::errorCode();
+ }
+
+ $v_result_list[$p_options_list[$i]] = true;
+ break;
+
+ case PCLZIP_OPT_TEMP_FILE_OFF:
+ // ----- Check for incompatible options
+ if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_ON])) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '" . PclZipUtilOptionText($p_options_list[$i]) . "' can not be used with option 'PCLZIP_OPT_TEMP_FILE_ON'");
+
+ return PclZip::errorCode();
+ }
+ // ----- Check for incompatible options
+ if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_THRESHOLD])) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '" . PclZipUtilOptionText($p_options_list[$i]) . "' can not be used with option 'PCLZIP_OPT_TEMP_FILE_THRESHOLD'");
+
+ return PclZip::errorCode();
+ }
+
+ $v_result_list[$p_options_list[$i]] = true;
+ break;
+
+ case PCLZIP_OPT_EXTRACT_DIR_RESTRICTION:
+ // ----- Check the number of parameters
+ if (($i + 1) >= $p_size) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Get the value
+ if (is_string($p_options_list[$i + 1]) && ($p_options_list[$i + 1] != '')) {
+ $v_result_list[$p_options_list[$i]] = PclZipUtilTranslateWinPath($p_options_list[$i + 1], false);
+ $i++;
+ } else {
+ }
+ break;
+
+ // ----- Look for options that request an array of string for value
+ case PCLZIP_OPT_BY_NAME:
+ // ----- Check the number of parameters
+ if (($i + 1) >= $p_size) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Get the value
+ if (is_string($p_options_list[$i + 1])) {
+ $v_result_list[$p_options_list[$i]][0] = $p_options_list[$i + 1];
+ } elseif (is_array($p_options_list[$i + 1])) {
+ $v_result_list[$p_options_list[$i]] = $p_options_list[$i + 1];
+ } else {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Wrong parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+ $i++;
+ break;
+
+ // ----- Look for options that request an EREG or PREG expression
+ case PCLZIP_OPT_BY_EREG:
+ $p_options_list[$i] = PCLZIP_OPT_BY_PREG;
+ // ereg() is deprecated starting with PHP 5.3. Move PCLZIP_OPT_BY_EREG
+ // to PCLZIP_OPT_BY_PREG
+ case PCLZIP_OPT_BY_PREG:
+ //case PCLZIP_OPT_CRYPT :
+ // ----- Check the number of parameters
+ if (($i + 1) >= $p_size) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Get the value
+ if (is_string($p_options_list[$i + 1])) {
+ $v_result_list[$p_options_list[$i]] = $p_options_list[$i + 1];
+ } else {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Wrong parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+ $i++;
+ break;
+
+ // ----- Look for options that takes a string
+ case PCLZIP_OPT_COMMENT:
+ case PCLZIP_OPT_ADD_COMMENT:
+ case PCLZIP_OPT_PREPEND_COMMENT:
+ // ----- Check the number of parameters
+ if (($i + 1) >= $p_size) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Get the value
+ if (is_string($p_options_list[$i + 1])) {
+ $v_result_list[$p_options_list[$i]] = $p_options_list[$i + 1];
+ } else {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Wrong parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+ $i++;
+ break;
+
+ // ----- Look for options that request an array of index
+ case PCLZIP_OPT_BY_INDEX:
+ // ----- Check the number of parameters
+ if (($i + 1) >= $p_size) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Get the value
+ $v_work_list = array();
+ if (is_string($p_options_list[$i + 1])) {
+
+ // ----- Remove spaces
+ $p_options_list[$i + 1] = strtr($p_options_list[$i + 1], ' ', '');
+
+ // ----- Parse items
+ $v_work_list = explode(",", $p_options_list[$i + 1]);
+ } elseif (is_integer($p_options_list[$i + 1])) {
+ $v_work_list[0] = $p_options_list[$i + 1] . '-' . $p_options_list[$i + 1];
+ } elseif (is_array($p_options_list[$i + 1])) {
+ $v_work_list = $p_options_list[$i + 1];
+ } else {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Value must be integer, string or array for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Reduce the index list
+ // each index item in the list must be a couple with a start and
+ // an end value : [0,3], [5-5], [8-10], ...
+ // ----- Check the format of each item
+ $v_sort_flag = false;
+ $v_sort_value = 0;
+ for ($j = 0; $j < sizeof($v_work_list); $j++) {
+ // ----- Explode the item
+ $v_item_list = explode("-", $v_work_list[$j]);
+ $v_size_item_list = sizeof($v_item_list);
+
+ // ----- TBC : Here we might check that each item is a
+ // real integer ...
+
+ // ----- Look for single value
+ if ($v_size_item_list == 1) {
+ // ----- Set the option value
+ $v_result_list[$p_options_list[$i]][$j]['start'] = $v_item_list[0];
+ $v_result_list[$p_options_list[$i]][$j]['end'] = $v_item_list[0];
+ } elseif ($v_size_item_list == 2) {
+ // ----- Set the option value
+ $v_result_list[$p_options_list[$i]][$j]['start'] = $v_item_list[0];
+ $v_result_list[$p_options_list[$i]][$j]['end'] = $v_item_list[1];
+ } else {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Too many values in index range for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Look for list sort
+ if ($v_result_list[$p_options_list[$i]][$j]['start'] < $v_sort_value) {
+ $v_sort_flag = true;
+
+ // ----- TBC : An automatic sort should be writen ...
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Invalid order of index range for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+ $v_sort_value = $v_result_list[$p_options_list[$i]][$j]['start'];
+ }
+
+ // ----- Sort the items
+ if ($v_sort_flag) {
+ // TBC : To Be Completed
+ }
+
+ // ----- Next option
+ $i++;
+ break;
+
+ // ----- Look for options that request no value
+ case PCLZIP_OPT_REMOVE_ALL_PATH:
+ case PCLZIP_OPT_EXTRACT_AS_STRING:
+ case PCLZIP_OPT_NO_COMPRESSION:
+ case PCLZIP_OPT_EXTRACT_IN_OUTPUT:
+ case PCLZIP_OPT_REPLACE_NEWER:
+ case PCLZIP_OPT_STOP_ON_ERROR:
+ $v_result_list[$p_options_list[$i]] = true;
+ break;
+
+ // ----- Look for options that request an octal value
+ case PCLZIP_OPT_SET_CHMOD:
+ // ----- Check the number of parameters
+ if (($i + 1) >= $p_size) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Get the value
+ $v_result_list[$p_options_list[$i]] = $p_options_list[$i + 1];
+ $i++;
+ break;
+
+ // ----- Look for options that request a call-back
+ case PCLZIP_CB_PRE_EXTRACT:
+ case PCLZIP_CB_POST_EXTRACT:
+ case PCLZIP_CB_PRE_ADD:
+ case PCLZIP_CB_POST_ADD:
+ /* for futur use
+ case PCLZIP_CB_PRE_DELETE :
+ case PCLZIP_CB_POST_DELETE :
+ case PCLZIP_CB_PRE_LIST :
+ case PCLZIP_CB_POST_LIST :
+ */
+ // ----- Check the number of parameters
+ if (($i + 1) >= $p_size) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Get the value
+ $v_function_name = $p_options_list[$i + 1];
+
+ // ----- Check that the value is a valid existing function
+ if (!function_exists($v_function_name)) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Function '" . $v_function_name . "()' is not an existing function for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Set the attribute
+ $v_result_list[$p_options_list[$i]] = $v_function_name;
+ $i++;
+ break;
+
+ default:
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Unknown parameter '" . $p_options_list[$i] . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Next options
+ $i++;
+ }
+
+ // ----- Look for mandatory options
+ if ($v_requested_options !== false) {
+ for ($key = reset($v_requested_options); $key = key($v_requested_options); $key = next($v_requested_options)) {
+ // ----- Look for mandatory option
+ if ($v_requested_options[$key] == 'mandatory') {
+ // ----- Look if present
+ if (!isset($v_result_list[$key])) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Missing mandatory parameter " . PclZipUtilOptionText($key) . "(" . $key . ")");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+ }
+ }
+ }
+
+ // ----- Look for default values
+ if (!isset($v_result_list[PCLZIP_OPT_TEMP_FILE_THRESHOLD])) {
+
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privOptionDefaultThreshold()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privOptionDefaultThreshold(&$p_options)
+ {
+ $v_result = 1;
+
+ if (isset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD]) || isset($p_options[PCLZIP_OPT_TEMP_FILE_OFF])) {
+ return $v_result;
+ }
+
+ // ----- Get 'memory_limit' configuration value
+ $v_memory_limit = ini_get('memory_limit');
+ $v_memory_limit = trim($v_memory_limit);
+ $last = strtolower(substr($v_memory_limit, -1));
+ $v_memory_limit = preg_replace('/\s*[KkMmGg]$/', '', $v_memory_limit);
+
+ if ($last == 'g') {
+ //$v_memory_limit = $v_memory_limit*1024*1024*1024;
+ $v_memory_limit = $v_memory_limit * 1073741824;
+ }
+ if ($last == 'm') {
+ //$v_memory_limit = $v_memory_limit*1024*1024;
+ $v_memory_limit = $v_memory_limit * 1048576;
+ }
+ if ($last == 'k') {
+ $v_memory_limit = $v_memory_limit * 1024;
+ }
+
+ $p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] = floor($v_memory_limit * PCLZIP_TEMPORARY_FILE_RATIO);
+
+ // ----- Sanity check : No threshold if value lower than 1M
+ if ($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] < 1048576) {
+ unset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD]);
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privFileDescrParseAtt()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // 1 on success.
+ // 0 on failure.
+ // --------------------------------------------------------------------------------
+ public function privFileDescrParseAtt(&$p_file_list, &$p_filedescr, $v_options, $v_requested_options = false)
+ {
+ $v_result = 1;
+
+ // ----- For each file in the list check the attributes
+ foreach ($p_file_list as $v_key => $v_value) {
+
+ // ----- Check if the option is supported
+ if (!isset($v_requested_options[$v_key])) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid file attribute '" . $v_key . "' for this file");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Look for attribute
+ switch ($v_key) {
+ case PCLZIP_ATT_FILE_NAME:
+ if (!is_string($v_value)) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type " . gettype($v_value) . ". String expected for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+ return PclZip::errorCode();
+ }
+
+ $p_filedescr['filename'] = PclZipUtilPathReduction($v_value);
+
+ if ($p_filedescr['filename'] == '') {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid empty filename for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+ return PclZip::errorCode();
+ }
+
+ break;
+
+ case PCLZIP_ATT_FILE_NEW_SHORT_NAME:
+ if (!is_string($v_value)) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type " . gettype($v_value) . ". String expected for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+ return PclZip::errorCode();
+ }
+
+ $p_filedescr['new_short_name'] = PclZipUtilPathReduction($v_value);
+
+ if ($p_filedescr['new_short_name'] == '') {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid empty short filename for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+ return PclZip::errorCode();
+ }
+ break;
+
+ case PCLZIP_ATT_FILE_NEW_FULL_NAME:
+ if (!is_string($v_value)) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type " . gettype($v_value) . ". String expected for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+ return PclZip::errorCode();
+ }
+
+ $p_filedescr['new_full_name'] = PclZipUtilPathReduction($v_value);
+
+ if ($p_filedescr['new_full_name'] == '') {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid empty full filename for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+ return PclZip::errorCode();
+ }
+ break;
+
+ // ----- Look for options that takes a string
+ case PCLZIP_ATT_FILE_COMMENT:
+ if (!is_string($v_value)) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type " . gettype($v_value) . ". String expected for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+ return PclZip::errorCode();
+ }
+
+ $p_filedescr['comment'] = $v_value;
+ break;
+
+ case PCLZIP_ATT_FILE_MTIME:
+ if (!is_integer($v_value)) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type " . gettype($v_value) . ". Integer expected for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+ return PclZip::errorCode();
+ }
+
+ $p_filedescr['mtime'] = $v_value;
+ break;
+
+ case PCLZIP_ATT_FILE_CONTENT:
+ $p_filedescr['content'] = $v_value;
+ break;
+
+ default:
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Unknown parameter '" . $v_key . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Look for mandatory options
+ if ($v_requested_options !== false) {
+ for ($key = reset($v_requested_options); $key = key($v_requested_options); $key = next($v_requested_options)) {
+ // ----- Look for mandatory option
+ if ($v_requested_options[$key] == 'mandatory') {
+ // ----- Look if present
+ if (!isset($p_file_list[$key])) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Missing mandatory parameter " . PclZipUtilOptionText($key) . "(" . $key . ")");
+
+ return PclZip::errorCode();
+ }
+ }
+ }
+ }
+
+ // end foreach
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privFileDescrExpand()
+ // Description :
+ // This method look for each item of the list to see if its a file, a folder
+ // or a string to be added as file. For any other type of files (link, other)
+ // just ignore the item.
+ // Then prepare the information that will be stored for that file.
+ // When its a folder, expand the folder with all the files that are in that
+ // folder (recursively).
+ // Parameters :
+ // Return Values :
+ // 1 on success.
+ // 0 on failure.
+ // --------------------------------------------------------------------------------
+ public function privFileDescrExpand(&$p_filedescr_list, &$p_options)
+ {
+ $v_result = 1;
+
+ // ----- Create a result list
+ $v_result_list = array();
+
+ // ----- Look each entry
+ for ($i = 0; $i < sizeof($p_filedescr_list); $i++) {
+
+ // ----- Get filedescr
+ $v_descr = $p_filedescr_list[$i];
+
+ // ----- Reduce the filename
+ $v_descr['filename'] = PclZipUtilTranslateWinPath($v_descr['filename'], false);
+ $v_descr['filename'] = PclZipUtilPathReduction($v_descr['filename']);
+
+ // ----- Look for real file or folder
+ if (file_exists($v_descr['filename'])) {
+ if (@is_file($v_descr['filename'])) {
+ $v_descr['type'] = 'file';
+ } elseif (@is_dir($v_descr['filename'])) {
+ $v_descr['type'] = 'folder';
+ } elseif (@is_link($v_descr['filename'])) {
+ // skip
+ continue;
+ } else {
+ // skip
+ continue;
+ }
+
+ // ----- Look for string added as file
+ } elseif (isset($v_descr['content'])) {
+ $v_descr['type'] = 'virtual_file';
+
+ // ----- Missing file
+ } else {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_FILE, "File '" . $v_descr['filename'] . "' does not exist");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Calculate the stored filename
+ $this->privCalculateStoredFilename($v_descr, $p_options);
+
+ // ----- Add the descriptor in result list
+ $v_result_list[sizeof($v_result_list)] = $v_descr;
+
+ // ----- Look for folder
+ if ($v_descr['type'] == 'folder') {
+ // ----- List of items in folder
+ $v_dirlist_descr = array();
+ $v_dirlist_nb = 0;
+ if ($v_folder_handler = @opendir($v_descr['filename'])) {
+ while (($v_item_handler = @readdir($v_folder_handler)) !== false) {
+
+ // ----- Skip '.' and '..'
+ if (($v_item_handler == '.') || ($v_item_handler == '..')) {
+ continue;
+ }
+
+ // ----- Compose the full filename
+ $v_dirlist_descr[$v_dirlist_nb]['filename'] = $v_descr['filename'] . '/' . $v_item_handler;
+
+ // ----- Look for different stored filename
+ // Because the name of the folder was changed, the name of the
+ // files/sub-folders also change
+ if (($v_descr['stored_filename'] != $v_descr['filename']) && (!isset($p_options[PCLZIP_OPT_REMOVE_ALL_PATH]))) {
+ if ($v_descr['stored_filename'] != '') {
+ $v_dirlist_descr[$v_dirlist_nb]['new_full_name'] = $v_descr['stored_filename'] . '/' . $v_item_handler;
+ } else {
+ $v_dirlist_descr[$v_dirlist_nb]['new_full_name'] = $v_item_handler;
+ }
+ }
+
+ $v_dirlist_nb++;
+ }
+
+ @closedir($v_folder_handler);
+ } else {
+ // TBC : unable to open folder in read mode
+ }
+
+ // ----- Expand each element of the list
+ if ($v_dirlist_nb != 0) {
+ // ----- Expand
+ if (($v_result = $this->privFileDescrExpand($v_dirlist_descr, $p_options)) != 1) {
+ return $v_result;
+ }
+
+ // ----- Concat the resulting list
+ $v_result_list = array_merge($v_result_list, $v_dirlist_descr);
+ } else {
+ }
+
+ // ----- Free local array
+ unset($v_dirlist_descr);
+ }
+ }
+
+ // ----- Get the result list
+ $p_filedescr_list = $v_result_list;
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privCreate()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privCreate($p_filedescr_list, &$p_result_list, &$p_options)
+ {
+ $v_result = 1;
+ $v_list_detail = array();
+
+ // ----- Magic quotes trick
+ $this->privDisableMagicQuotes();
+
+ // ----- Open the file in write mode
+ if (($v_result = $this->privOpenFd('wb')) != 1) {
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Add the list of files
+ $v_result = $this->privAddList($p_filedescr_list, $p_result_list, $p_options);
+
+ // ----- Close
+ $this->privCloseFd();
+
+ // ----- Magic quotes trick
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privAdd()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privAdd($p_filedescr_list, &$p_result_list, &$p_options)
+ {
+ $v_result = 1;
+ $v_list_detail = array();
+
+ // ----- Look if the archive exists or is empty
+ if ((!is_file($this->zipname)) || (filesize($this->zipname) == 0)) {
+
+ // ----- Do a create
+ $v_result = $this->privCreate($p_filedescr_list, $p_result_list, $p_options);
+
+ // ----- Return
+ return $v_result;
+ }
+ // ----- Magic quotes trick
+ $this->privDisableMagicQuotes();
+
+ // ----- Open the zip file
+ if (($v_result = $this->privOpenFd('rb')) != 1) {
+ // ----- Magic quotes trick
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Read the central directory information
+ $v_central_dir = array();
+ if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) {
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+
+ // ----- Go to beginning of File
+ @rewind($this->zip_fd);
+
+ // ----- Creates a temporary file
+ $v_zip_temp_name = PCLZIP_TEMPORARY_DIR . $this->createUniqueName('pclzip-') . '.tmp';
+
+ // ----- Open the temporary file in write mode
+ if (($v_zip_temp_fd = @fopen($v_zip_temp_name, 'wb')) == 0) {
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \'' . $v_zip_temp_name . '\' in binary write mode');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Copy the files from the archive to the temporary file
+ // TBC : Here I should better append the file and go back to erase the central dir
+ $v_size = $v_central_dir['offset'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = fread($this->zip_fd, $v_read_size);
+ @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Swap the file descriptor
+ // Here is a trick : I swap the temporary fd with the zip fd, in order to use
+ // the following methods on the temporary fil and not the real archive
+ $v_swap = $this->zip_fd;
+ $this->zip_fd = $v_zip_temp_fd;
+ $v_zip_temp_fd = $v_swap;
+
+ // ----- Add the files
+ $v_header_list = array();
+ if (($v_result = $this->privAddFileList($p_filedescr_list, $v_header_list, $p_options)) != 1) {
+ fclose($v_zip_temp_fd);
+ $this->privCloseFd();
+ @unlink($v_zip_temp_name);
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Store the offset of the central dir
+ $v_offset = @ftell($this->zip_fd);
+
+ // ----- Copy the block of file headers from the old archive
+ $v_size = $v_central_dir['size'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @fread($v_zip_temp_fd, $v_read_size);
+ @fwrite($this->zip_fd, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Create the Central Dir files header
+ for ($i = 0, $v_count = 0; $i < sizeof($v_header_list); $i++) {
+ // ----- Create the file header
+ if ($v_header_list[$i]['status'] == 'ok') {
+ if (($v_result = $this->privWriteCentralFileHeader($v_header_list[$i])) != 1) {
+ fclose($v_zip_temp_fd);
+ $this->privCloseFd();
+ @unlink($v_zip_temp_name);
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Return
+ return $v_result;
+ }
+ $v_count++;
+ }
+
+ // ----- Transform the header to a 'usable' info
+ $this->privConvertHeader2FileInfo($v_header_list[$i], $p_result_list[$i]);
+ }
+
+ // ----- Zip file comment
+ $v_comment = $v_central_dir['comment'];
+ if (isset($p_options[PCLZIP_OPT_COMMENT])) {
+ $v_comment = $p_options[PCLZIP_OPT_COMMENT];
+ }
+ if (isset($p_options[PCLZIP_OPT_ADD_COMMENT])) {
+ $v_comment = $v_comment . $p_options[PCLZIP_OPT_ADD_COMMENT];
+ }
+ if (isset($p_options[PCLZIP_OPT_PREPEND_COMMENT])) {
+ $v_comment = $p_options[PCLZIP_OPT_PREPEND_COMMENT] . $v_comment;
+ }
+
+ // ----- Calculate the size of the central header
+ $v_size = @ftell($this->zip_fd) - $v_offset;
+
+ // ----- Create the central dir footer
+ if (($v_result = $this->privWriteCentralHeader($v_count + $v_central_dir['entries'], $v_size, $v_offset, $v_comment)) != 1) {
+ // ----- Reset the file list
+ unset($v_header_list);
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Swap back the file descriptor
+ $v_swap = $this->zip_fd;
+ $this->zip_fd = $v_zip_temp_fd;
+ $v_zip_temp_fd = $v_swap;
+
+ // ----- Close
+ $this->privCloseFd();
+
+ // ----- Close the temporary file
+ @fclose($v_zip_temp_fd);
+
+ // ----- Magic quotes trick
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Delete the zip file
+ // TBC : I should test the result ...
+ @unlink($this->zipname);
+
+ // ----- Rename the temporary file
+ // TBC : I should test the result ...
+ //@rename($v_zip_temp_name, $this->zipname);
+ PclZipUtilRename($v_zip_temp_name, $this->zipname);
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privOpenFd()
+ // Description :
+ // Parameters :
+ // --------------------------------------------------------------------------------
+ public function privOpenFd($p_mode)
+ {
+ $v_result = 1;
+
+ // ----- Look if already open
+ if ($this->zip_fd != 0) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Zip file \'' . $this->zipname . '\' already open');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Open the zip file
+ if (($this->zip_fd = @fopen($this->zipname, $p_mode)) == 0) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive \'' . $this->zipname . '\' in ' . $p_mode . ' mode');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privCloseFd()
+ // Description :
+ // Parameters :
+ // --------------------------------------------------------------------------------
+ public function privCloseFd()
+ {
+ $v_result = 1;
+
+ if ($this->zip_fd != 0) {
+ @fclose($this->zip_fd);
+ }
+ $this->zip_fd = 0;
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privAddList()
+ // Description :
+ // $p_add_dir and $p_remove_dir will give the ability to memorize a path which is
+ // different from the real path of the file. This is usefull if you want to have PclTar
+ // running in any directory, and memorize relative path from an other directory.
+ // Parameters :
+ // $p_list : An array containing the file or directory names to add in the tar
+ // $p_result_list : list of added files with their properties (specially the status field)
+ // $p_add_dir : Path to add in the filename path archived
+ // $p_remove_dir : Path to remove in the filename path archived
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ // function privAddList($p_list, &$p_result_list, $p_add_dir, $p_remove_dir, $p_remove_all_dir, &$p_options)
+ public function privAddList($p_filedescr_list, &$p_result_list, &$p_options)
+ {
+ $v_result = 1;
+
+ // ----- Add the files
+ $v_header_list = array();
+ if (($v_result = $this->privAddFileList($p_filedescr_list, $v_header_list, $p_options)) != 1) {
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Store the offset of the central dir
+ $v_offset = @ftell($this->zip_fd);
+
+ // ----- Create the Central Dir files header
+ for ($i = 0, $v_count = 0; $i < sizeof($v_header_list); $i++) {
+ // ----- Create the file header
+ if ($v_header_list[$i]['status'] == 'ok') {
+ if (($v_result = $this->privWriteCentralFileHeader($v_header_list[$i])) != 1) {
+ // ----- Return
+ return $v_result;
+ }
+ $v_count++;
+ }
+
+ // ----- Transform the header to a 'usable' info
+ $this->privConvertHeader2FileInfo($v_header_list[$i], $p_result_list[$i]);
+ }
+
+ // ----- Zip file comment
+ $v_comment = '';
+ if (isset($p_options[PCLZIP_OPT_COMMENT])) {
+ $v_comment = $p_options[PCLZIP_OPT_COMMENT];
+ }
+
+ // ----- Calculate the size of the central header
+ $v_size = @ftell($this->zip_fd) - $v_offset;
+
+ // ----- Create the central dir footer
+ if (($v_result = $this->privWriteCentralHeader($v_count, $v_size, $v_offset, $v_comment)) != 1) {
+ // ----- Reset the file list
+ unset($v_header_list);
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privAddFileList()
+ // Description :
+ // Parameters :
+ // $p_filedescr_list : An array containing the file description
+ // or directory names to add in the zip
+ // $p_result_list : list of added files with their properties (specially the status field)
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privAddFileList($p_filedescr_list, &$p_result_list, &$p_options)
+ {
+ $v_result = 1;
+ $v_header = array();
+
+ // ----- Recuperate the current number of elt in list
+ $v_nb = sizeof($p_result_list);
+
+ // ----- Loop on the files
+ for ($j = 0; ($j < sizeof($p_filedescr_list)) && ($v_result == 1); $j++) {
+ // ----- Format the filename
+ $p_filedescr_list[$j]['filename'] = PclZipUtilTranslateWinPath($p_filedescr_list[$j]['filename'], false);
+
+ // ----- Skip empty file names
+ // TBC : Can this be possible ? not checked in DescrParseAtt ?
+ if ($p_filedescr_list[$j]['filename'] == "") {
+ continue;
+ }
+
+ // ----- Check the filename
+ if (($p_filedescr_list[$j]['type'] != 'virtual_file') && (!file_exists($p_filedescr_list[$j]['filename']))) {
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_FILE, "File '" . $p_filedescr_list[$j]['filename'] . "' does not exist");
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Look if it is a file or a dir with no all path remove option
+ // or a dir with all its path removed
+ // if ( (is_file($p_filedescr_list[$j]['filename']))
+ // || ( is_dir($p_filedescr_list[$j]['filename'])
+ if (($p_filedescr_list[$j]['type'] == 'file') || ($p_filedescr_list[$j]['type'] == 'virtual_file') || (($p_filedescr_list[$j]['type'] == 'folder') && (!isset($p_options[PCLZIP_OPT_REMOVE_ALL_PATH]) || !$p_options[PCLZIP_OPT_REMOVE_ALL_PATH]))) {
+
+ // ----- Add the file
+ $v_result = $this->privAddFile($p_filedescr_list[$j], $v_header, $p_options);
+ if ($v_result != 1) {
+ return $v_result;
+ }
+
+ // ----- Store the file infos
+ $p_result_list[$v_nb++] = $v_header;
+ }
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privAddFile()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privAddFile($p_filedescr, &$p_header, &$p_options)
+ {
+ $v_result = 1;
+
+ // ----- Working variable
+ $p_filename = $p_filedescr['filename'];
+
+ // TBC : Already done in the fileAtt check ... ?
+ if ($p_filename == "") {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid file list parameter (invalid or empty list)");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Look for a stored different filename
+ /* TBC : Removed
+ if (isset($p_filedescr['stored_filename'])) {
+ $v_stored_filename = $p_filedescr['stored_filename'];
+ } else {
+ $v_stored_filename = $p_filedescr['stored_filename'];
+ }
+ */
+
+ // ----- Set the file properties
+ clearstatcache();
+ $p_header['version'] = 20;
+ $p_header['version_extracted'] = 10;
+ $p_header['flag'] = 0;
+ $p_header['compression'] = 0;
+ $p_header['crc'] = 0;
+ $p_header['compressed_size'] = 0;
+ $p_header['filename_len'] = strlen($p_filename);
+ $p_header['extra_len'] = 0;
+ $p_header['disk'] = 0;
+ $p_header['internal'] = 0;
+ $p_header['offset'] = 0;
+ $p_header['filename'] = $p_filename;
+ // TBC : Removed $p_header['stored_filename'] = $v_stored_filename;
+ $p_header['stored_filename'] = $p_filedescr['stored_filename'];
+ $p_header['extra'] = '';
+ $p_header['status'] = 'ok';
+ $p_header['index'] = -1;
+
+ // ----- Look for regular file
+ if ($p_filedescr['type'] == 'file') {
+ $p_header['external'] = 0x00000000;
+ $p_header['size'] = filesize($p_filename);
+
+ // ----- Look for regular folder
+ } elseif ($p_filedescr['type'] == 'folder') {
+ $p_header['external'] = 0x00000010;
+ $p_header['mtime'] = filemtime($p_filename);
+ $p_header['size'] = filesize($p_filename);
+
+ // ----- Look for virtual file
+ } elseif ($p_filedescr['type'] == 'virtual_file') {
+ $p_header['external'] = 0x00000000;
+ $p_header['size'] = strlen($p_filedescr['content']);
+ }
+
+ // ----- Look for filetime
+ if (isset($p_filedescr['mtime'])) {
+ $p_header['mtime'] = $p_filedescr['mtime'];
+ } elseif ($p_filedescr['type'] == 'virtual_file') {
+ $p_header['mtime'] = time();
+ } else {
+ $p_header['mtime'] = filemtime($p_filename);
+ }
+
+ // ------ Look for file comment
+ if (isset($p_filedescr['comment'])) {
+ $p_header['comment_len'] = strlen($p_filedescr['comment']);
+ $p_header['comment'] = $p_filedescr['comment'];
+ } else {
+ $p_header['comment_len'] = 0;
+ $p_header['comment'] = '';
+ }
+
+ // ----- Look for pre-add callback
+ if (isset($p_options[PCLZIP_CB_PRE_ADD])) {
+
+ // ----- Generate a local information
+ $v_local_header = array();
+ $this->privConvertHeader2FileInfo($p_header, $v_local_header);
+
+ // ----- Call the callback
+ // Here I do not use call_user_func() because I need to send a reference to the
+ // header.
+ // eval('$v_result = '.$p_options[PCLZIP_CB_PRE_ADD].'(PCLZIP_CB_PRE_ADD, $v_local_header);');
+ $v_result = $p_options[PCLZIP_CB_PRE_ADD](PCLZIP_CB_PRE_ADD, $v_local_header);
+ if ($v_result == 0) {
+ // ----- Change the file status
+ $p_header['status'] = "skipped";
+ $v_result = 1;
+ }
+
+ // ----- Update the informations
+ // Only some fields can be modified
+ if ($p_header['stored_filename'] != $v_local_header['stored_filename']) {
+ $p_header['stored_filename'] = PclZipUtilPathReduction($v_local_header['stored_filename']);
+ }
+ }
+
+ // ----- Look for empty stored filename
+ if ($p_header['stored_filename'] == "") {
+ $p_header['status'] = "filtered";
+ }
+
+ // ----- Check the path length
+ if (strlen($p_header['stored_filename']) > 0xFF) {
+ $p_header['status'] = 'filename_too_long';
+ }
+
+ // ----- Look if no error, or file not skipped
+ if ($p_header['status'] == 'ok') {
+
+ // ----- Look for a file
+ if ($p_filedescr['type'] == 'file') {
+ // ----- Look for using temporary file to zip
+ if ((!isset($p_options[PCLZIP_OPT_TEMP_FILE_OFF])) && (isset($p_options[PCLZIP_OPT_TEMP_FILE_ON]) || (isset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD]) && ($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] <= $p_header['size'])))) {
+ $v_result = $this->privAddFileUsingTempFile($p_filedescr, $p_header, $p_options);
+ if ($v_result < PCLZIP_ERR_NO_ERROR) {
+ return $v_result;
+ }
+
+ // ----- Use "in memory" zip algo
+ } else {
+
+ // ----- Open the source file
+ if (($v_file = @fopen($p_filename, "rb")) == 0) {
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, "Unable to open file '$p_filename' in binary read mode");
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the file content
+ $v_content = @fread($v_file, $p_header['size']);
+
+ // ----- Close the file
+ @fclose($v_file);
+
+ // ----- Calculate the CRC
+ $p_header['crc'] = @crc32($v_content);
+
+ // ----- Look for no compression
+ if ($p_options[PCLZIP_OPT_NO_COMPRESSION]) {
+ // ----- Set header parameters
+ $p_header['compressed_size'] = $p_header['size'];
+ $p_header['compression'] = 0;
+
+ // ----- Look for normal compression
+ } else {
+ // ----- Compress the content
+ $v_content = @gzdeflate($v_content);
+
+ // ----- Set header parameters
+ $p_header['compressed_size'] = strlen($v_content);
+ $p_header['compression'] = 8;
+ }
+
+ // ----- Call the header generation
+ if (($v_result = $this->privWriteFileHeader($p_header)) != 1) {
+ @fclose($v_file);
+
+ return $v_result;
+ }
+
+ // ----- Write the compressed (or not) content
+ @fwrite($this->zip_fd, $v_content, $p_header['compressed_size']);
+
+ }
+
+ // ----- Look for a virtual file (a file from string)
+ } elseif ($p_filedescr['type'] == 'virtual_file') {
+
+ $v_content = $p_filedescr['content'];
+
+ // ----- Calculate the CRC
+ $p_header['crc'] = @crc32($v_content);
+
+ // ----- Look for no compression
+ if ($p_options[PCLZIP_OPT_NO_COMPRESSION]) {
+ // ----- Set header parameters
+ $p_header['compressed_size'] = $p_header['size'];
+ $p_header['compression'] = 0;
+
+ // ----- Look for normal compression
+ } else {
+ // ----- Compress the content
+ $v_content = @gzdeflate($v_content);
+
+ // ----- Set header parameters
+ $p_header['compressed_size'] = strlen($v_content);
+ $p_header['compression'] = 8;
+ }
+
+ // ----- Call the header generation
+ if (($v_result = $this->privWriteFileHeader($p_header)) != 1) {
+ @fclose($v_file);
+
+ return $v_result;
+ }
+
+ // ----- Write the compressed (or not) content
+ @fwrite($this->zip_fd, $v_content, $p_header['compressed_size']);
+
+ // ----- Look for a directory
+ } elseif ($p_filedescr['type'] == 'folder') {
+ // ----- Look for directory last '/'
+ if (@substr($p_header['stored_filename'], -1) != '/') {
+ $p_header['stored_filename'] .= '/';
+ }
+
+ // ----- Set the file properties
+ $p_header['size'] = 0;
+ //$p_header['external'] = 0x41FF0010; // Value for a folder : to be checked
+ $p_header['external'] = 0x00000010; // Value for a folder : to be checked
+
+ // ----- Call the header generation
+ if (($v_result = $this->privWriteFileHeader($p_header)) != 1) {
+ return $v_result;
+ }
+ }
+ }
+
+ // ----- Look for post-add callback
+ if (isset($p_options[PCLZIP_CB_POST_ADD])) {
+
+ // ----- Generate a local information
+ $v_local_header = array();
+ $this->privConvertHeader2FileInfo($p_header, $v_local_header);
+
+ // ----- Call the callback
+ // Here I do not use call_user_func() because I need to send a reference to the
+ // header.
+ // eval('$v_result = '.$p_options[PCLZIP_CB_POST_ADD].'(PCLZIP_CB_POST_ADD, $v_local_header);');
+ $v_result = $p_options[PCLZIP_CB_POST_ADD](PCLZIP_CB_POST_ADD, $v_local_header);
+ if ($v_result == 0) {
+ // ----- Ignored
+ $v_result = 1;
+ }
+
+ // ----- Update the informations
+ // Nothing can be modified
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privAddFileUsingTempFile()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privAddFileUsingTempFile($p_filedescr, &$p_header, &$p_options)
+ {
+ $v_result = PCLZIP_ERR_NO_ERROR;
+
+ // ----- Working variable
+ $p_filename = $p_filedescr['filename'];
+
+ // ----- Open the source file
+ if (($v_file = @fopen($p_filename, "rb")) == 0) {
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, "Unable to open file '$p_filename' in binary read mode");
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Creates a compressed temporary file
+ $v_gzip_temp_name = PCLZIP_TEMPORARY_DIR . $this->createUniqueName('pclzip-') . '.gz';
+ if (($v_file_compressed = @gzopen($v_gzip_temp_name, "wb")) == 0) {
+ fclose($v_file);
+ PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL, 'Unable to open temporary file \'' . $v_gzip_temp_name . '\' in binary write mode');
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
+ $v_size = filesize($p_filename);
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @fread($v_file, $v_read_size);
+ //$v_binary_data = pack('a'.$v_read_size, $v_buffer);
+ @gzputs($v_file_compressed, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Close the file
+ @fclose($v_file);
+ @gzclose($v_file_compressed);
+
+ // ----- Check the minimum file size
+ if (filesize($v_gzip_temp_name) < 18) {
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'gzip temporary file \'' . $v_gzip_temp_name . '\' has invalid filesize - should be minimum 18 bytes');
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Extract the compressed attributes
+ if (($v_file_compressed = @fopen($v_gzip_temp_name, "rb")) == 0) {
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \'' . $v_gzip_temp_name . '\' in binary read mode');
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the gzip file header
+ $v_binary_data = @fread($v_file_compressed, 10);
+ $v_data_header = unpack('a1id1/a1id2/a1cm/a1flag/Vmtime/a1xfl/a1os', $v_binary_data);
+
+ // ----- Check some parameters
+ $v_data_header['os'] = bin2hex($v_data_header['os']);
+
+ // ----- Read the gzip file footer
+ @fseek($v_file_compressed, filesize($v_gzip_temp_name) - 8);
+ $v_binary_data = @fread($v_file_compressed, 8);
+ $v_data_footer = unpack('Vcrc/Vcompressed_size', $v_binary_data);
+
+ // ----- Set the attributes
+ $p_header['compression'] = ord($v_data_header['cm']);
+ //$p_header['mtime'] = $v_data_header['mtime'];
+ $p_header['crc'] = $v_data_footer['crc'];
+ $p_header['compressed_size'] = filesize($v_gzip_temp_name) - 18;
+
+ // ----- Close the file
+ @fclose($v_file_compressed);
+
+ // ----- Call the header generation
+ if (($v_result = $this->privWriteFileHeader($p_header)) != 1) {
+ return $v_result;
+ }
+
+ // ----- Add the compressed data
+ if (($v_file_compressed = @fopen($v_gzip_temp_name, "rb")) == 0) {
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \'' . $v_gzip_temp_name . '\' in binary read mode');
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
+ fseek($v_file_compressed, 10);
+ $v_size = $p_header['compressed_size'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @fread($v_file_compressed, $v_read_size);
+ //$v_binary_data = pack('a'.$v_read_size, $v_buffer);
+ @fwrite($this->zip_fd, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Close the file
+ @fclose($v_file_compressed);
+
+ // ----- Unlink the temporary file
+ @unlink($v_gzip_temp_name);
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privCalculateStoredFilename()
+ // Description :
+ // Based on file descriptor properties and global options, this method
+ // calculate the filename that will be stored in the archive.
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privCalculateStoredFilename(&$p_filedescr, &$p_options)
+ {
+ $v_result = 1;
+
+ // ----- Working variables
+ $p_filename = $p_filedescr['filename'];
+ if (isset($p_options[PCLZIP_OPT_ADD_PATH])) {
+ $p_add_dir = $p_options[PCLZIP_OPT_ADD_PATH];
+ } else {
+ $p_add_dir = '';
+ }
+ if (isset($p_options[PCLZIP_OPT_REMOVE_PATH])) {
+ $p_remove_dir = $p_options[PCLZIP_OPT_REMOVE_PATH];
+ } else {
+ $p_remove_dir = '';
+ }
+ if (isset($p_options[PCLZIP_OPT_REMOVE_ALL_PATH])) {
+ $p_remove_all_dir = $p_options[PCLZIP_OPT_REMOVE_ALL_PATH];
+ } else {
+ $p_remove_all_dir = 0;
+ }
+
+ // ----- Look for full name change
+ if (isset($p_filedescr['new_full_name'])) {
+ // ----- Remove drive letter if any
+ $v_stored_filename = PclZipUtilTranslateWinPath($p_filedescr['new_full_name']);
+
+ // ----- Look for path and/or short name change
+ } else {
+
+ // ----- Look for short name change
+ // Its when we cahnge just the filename but not the path
+ if (isset($p_filedescr['new_short_name'])) {
+ $v_path_info = pathinfo($p_filename);
+ $v_dir = '';
+ if ($v_path_info['dirname'] != '') {
+ $v_dir = $v_path_info['dirname'] . '/';
+ }
+ $v_stored_filename = $v_dir . $p_filedescr['new_short_name'];
+ } else {
+ // ----- Calculate the stored filename
+ $v_stored_filename = $p_filename;
+ }
+
+ // ----- Look for all path to remove
+ if ($p_remove_all_dir) {
+ $v_stored_filename = basename($p_filename);
+
+ // ----- Look for partial path remove
+ } elseif ($p_remove_dir != "") {
+ if (substr($p_remove_dir, -1) != '/') {
+ $p_remove_dir .= "/";
+ }
+
+ if ((substr($p_filename, 0, 2) == "./") || (substr($p_remove_dir, 0, 2) == "./")) {
+
+ if ((substr($p_filename, 0, 2) == "./") && (substr($p_remove_dir, 0, 2) != "./")) {
+ $p_remove_dir = "./" . $p_remove_dir;
+ }
+ if ((substr($p_filename, 0, 2) != "./") && (substr($p_remove_dir, 0, 2) == "./")) {
+ $p_remove_dir = substr($p_remove_dir, 2);
+ }
+ }
+
+ $v_compare = PclZipUtilPathInclusion($p_remove_dir, $v_stored_filename);
+ if ($v_compare > 0) {
+ if ($v_compare == 2) {
+ $v_stored_filename = "";
+ } else {
+ $v_stored_filename = substr($v_stored_filename, strlen($p_remove_dir));
+ }
+ }
+ }
+
+ // ----- Remove drive letter if any
+ $v_stored_filename = PclZipUtilTranslateWinPath($v_stored_filename);
+
+ // ----- Look for path to add
+ if ($p_add_dir != "") {
+ if (substr($p_add_dir, -1) == "/") {
+ $v_stored_filename = $p_add_dir . $v_stored_filename;
+ } else {
+ $v_stored_filename = $p_add_dir . "/" . $v_stored_filename;
+ }
+ }
+ }
+
+ // ----- Filename (reduce the path of stored name)
+ $v_stored_filename = PclZipUtilPathReduction($v_stored_filename);
+ $p_filedescr['stored_filename'] = $v_stored_filename;
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privWriteFileHeader()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privWriteFileHeader(&$p_header)
+ {
+ $v_result = 1;
+
+ // ----- Store the offset position of the file
+ $p_header['offset'] = ftell($this->zip_fd);
+
+ // ----- Transform UNIX mtime to DOS format mdate/mtime
+ $v_date = getdate($p_header['mtime']);
+ $v_mtime = ($v_date['hours'] << 11) + ($v_date['minutes'] << 5) + $v_date['seconds'] / 2;
+ $v_mdate = (($v_date['year'] - 1980) << 9) + ($v_date['mon'] << 5) + $v_date['mday'];
+
+ // ----- Packed data
+ $v_binary_data = pack("VvvvvvVVVvv", 0x04034b50, $p_header['version_extracted'], $p_header['flag'], $p_header['compression'], $v_mtime, $v_mdate, $p_header['crc'], $p_header['compressed_size'], $p_header['size'], strlen($p_header['stored_filename']), $p_header['extra_len']);
+
+ // ----- Write the first 148 bytes of the header in the archive
+ fputs($this->zip_fd, $v_binary_data, 30);
+
+ // ----- Write the variable fields
+ if (strlen($p_header['stored_filename']) != 0) {
+ fputs($this->zip_fd, $p_header['stored_filename'], strlen($p_header['stored_filename']));
+ }
+ if ($p_header['extra_len'] != 0) {
+ fputs($this->zip_fd, $p_header['extra'], $p_header['extra_len']);
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privWriteCentralFileHeader()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privWriteCentralFileHeader(&$p_header)
+ {
+ $v_result = 1;
+
+ // TBC
+ //for (reset($p_header); $key = key($p_header); next($p_header)) {
+ //}
+
+ // ----- Transform UNIX mtime to DOS format mdate/mtime
+ $v_date = getdate($p_header['mtime']);
+ $v_mtime = ($v_date['hours'] << 11) + ($v_date['minutes'] << 5) + $v_date['seconds'] / 2;
+ $v_mdate = (($v_date['year'] - 1980) << 9) + ($v_date['mon'] << 5) + $v_date['mday'];
+
+ // ----- Packed data
+ $v_binary_data = pack("VvvvvvvVVVvvvvvVV", 0x02014b50, $p_header['version'], $p_header['version_extracted'], $p_header['flag'], $p_header['compression'], $v_mtime, $v_mdate, $p_header['crc'], $p_header['compressed_size'], $p_header['size'], strlen($p_header['stored_filename']), $p_header['extra_len'], $p_header['comment_len'], $p_header['disk'], $p_header['internal'], $p_header['external'], $p_header['offset']);
+
+ // ----- Write the 42 bytes of the header in the zip file
+ fputs($this->zip_fd, $v_binary_data, 46);
+
+ // ----- Write the variable fields
+ if (strlen($p_header['stored_filename']) != 0) {
+ fputs($this->zip_fd, $p_header['stored_filename'], strlen($p_header['stored_filename']));
+ }
+ if ($p_header['extra_len'] != 0) {
+ fputs($this->zip_fd, $p_header['extra'], $p_header['extra_len']);
+ }
+ if ($p_header['comment_len'] != 0) {
+ fputs($this->zip_fd, $p_header['comment'], $p_header['comment_len']);
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privWriteCentralHeader()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privWriteCentralHeader($p_nb_entries, $p_size, $p_offset, $p_comment)
+ {
+ $v_result = 1;
+
+ // ----- Packed data
+ $v_binary_data = pack("VvvvvVVv", 0x06054b50, 0, 0, $p_nb_entries, $p_nb_entries, $p_size, $p_offset, strlen($p_comment));
+
+ // ----- Write the 22 bytes of the header in the zip file
+ fputs($this->zip_fd, $v_binary_data, 22);
+
+ // ----- Write the variable fields
+ if (strlen($p_comment) != 0) {
+ fputs($this->zip_fd, $p_comment, strlen($p_comment));
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privList()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privList(&$p_list)
+ {
+ $v_result = 1;
+
+ // ----- Magic quotes trick
+ $this->privDisableMagicQuotes();
+
+ // ----- Open the zip file
+ if (($this->zip_fd = @fopen($this->zipname, 'rb')) == 0) {
+ // ----- Magic quotes trick
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive \'' . $this->zipname . '\' in binary read mode');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the central directory informations
+ $v_central_dir = array();
+ if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) {
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+
+ // ----- Go to beginning of Central Dir
+ @rewind($this->zip_fd);
+ if (@fseek($this->zip_fd, $v_central_dir['offset'])) {
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Read each entry
+ for ($i = 0; $i < $v_central_dir['entries']; $i++) {
+ // ----- Read the file header
+ if (($v_result = $this->privReadCentralFileHeader($v_header)) != 1) {
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+ $v_header['index'] = $i;
+
+ // ----- Get the only interesting attributes
+ $this->privConvertHeader2FileInfo($v_header, $p_list[$i]);
+ unset($v_header);
+ }
+
+ // ----- Close the zip file
+ $this->privCloseFd();
+
+ // ----- Magic quotes trick
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privConvertHeader2FileInfo()
+ // Description :
+ // This function takes the file informations from the central directory
+ // entries and extract the interesting parameters that will be given back.
+ // The resulting file infos are set in the array $p_info
+ // $p_info['filename'] : Filename with full path. Given by user (add),
+ // extracted in the filesystem (extract).
+ // $p_info['stored_filename'] : Stored filename in the archive.
+ // $p_info['size'] = Size of the file.
+ // $p_info['compressed_size'] = Compressed size of the file.
+ // $p_info['mtime'] = Last modification date of the file.
+ // $p_info['comment'] = Comment associated with the file.
+ // $p_info['folder'] = true/false : indicates if the entry is a folder or not.
+ // $p_info['status'] = status of the action on the file.
+ // $p_info['crc'] = CRC of the file content.
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privConvertHeader2FileInfo($p_header, &$p_info)
+ {
+ $v_result = 1;
+
+ // ----- Get the interesting attributes
+ $v_temp_path = PclZipUtilPathReduction($p_header['filename']);
+ $p_info['filename'] = $v_temp_path;
+ $v_temp_path = PclZipUtilPathReduction($p_header['stored_filename']);
+ $p_info['stored_filename'] = $v_temp_path;
+ $p_info['size'] = $p_header['size'];
+ $p_info['compressed_size'] = $p_header['compressed_size'];
+ $p_info['mtime'] = $p_header['mtime'];
+ $p_info['comment'] = $p_header['comment'];
+ $p_info['folder'] = (($p_header['external'] & 0x00000010) == 0x00000010);
+ $p_info['index'] = $p_header['index'];
+ $p_info['status'] = $p_header['status'];
+ $p_info['crc'] = $p_header['crc'];
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privExtractByRule()
+ // Description :
+ // Extract a file or directory depending of rules (by index, by name, ...)
+ // Parameters :
+ // $p_file_list : An array where will be placed the properties of each
+ // extracted file
+ // $p_path : Path to add while writing the extracted files
+ // $p_remove_path : Path to remove (from the file memorized path) while writing the
+ // extracted files. If the path does not match the file path,
+ // the file is extracted with its memorized path.
+ // $p_remove_path does not apply to 'list' mode.
+ // $p_path and $p_remove_path are commulative.
+ // Return Values :
+ // 1 on success,0 or less on error (see error code list)
+ // --------------------------------------------------------------------------------
+ public function privExtractByRule(&$p_file_list, $p_path, $p_remove_path, $p_remove_all_path, &$p_options)
+ {
+ $v_result = 1;
+
+ // ----- Magic quotes trick
+ $this->privDisableMagicQuotes();
+
+ // ----- Check the path
+ if (($p_path == "") || ((substr($p_path, 0, 1) != "/") && (substr($p_path, 0, 3) != "../") && (substr($p_path, 1, 2) != ":/"))) {
+ $p_path = "./" . $p_path;
+ }
+
+ // ----- Reduce the path last (and duplicated) '/'
+ if (($p_path != "./") && ($p_path != "/")) {
+ // ----- Look for the path end '/'
+ while (substr($p_path, -1) == "/") {
+ $p_path = substr($p_path, 0, strlen($p_path) - 1);
+ }
+ }
+
+ // ----- Look for path to remove format (should end by /)
+ if (($p_remove_path != "") && (substr($p_remove_path, -1) != '/')) {
+ $p_remove_path .= '/';
+ }
+ $p_remove_path_size = strlen($p_remove_path);
+
+ // ----- Open the zip file
+ if (($v_result = $this->privOpenFd('rb')) != 1) {
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+
+ // ----- Read the central directory informations
+ $v_central_dir = array();
+ if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+
+ // ----- Start at beginning of Central Dir
+ $v_pos_entry = $v_central_dir['offset'];
+
+ // ----- Read each entry
+ $j_start = 0;
+ for ($i = 0, $v_nb_extracted = 0; $i < $v_central_dir['entries']; $i++) {
+
+ // ----- Read next Central dir entry
+ @rewind($this->zip_fd);
+ if (@fseek($this->zip_fd, $v_pos_entry)) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the file header
+ $v_header = array();
+ if (($v_result = $this->privReadCentralFileHeader($v_header)) != 1) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+
+ // ----- Store the index
+ $v_header['index'] = $i;
+
+ // ----- Store the file position
+ $v_pos_entry = ftell($this->zip_fd);
+
+ // ----- Look for the specific extract rules
+ $v_extract = false;
+
+ // ----- Look for extract by name rule
+ if ((isset($p_options[PCLZIP_OPT_BY_NAME])) && ($p_options[PCLZIP_OPT_BY_NAME] != 0)) {
+
+ // ----- Look if the filename is in the list
+ for ($j = 0; ($j < sizeof($p_options[PCLZIP_OPT_BY_NAME])) && (!$v_extract); $j++) {
+
+ // ----- Look for a directory
+ if (substr($p_options[PCLZIP_OPT_BY_NAME][$j], -1) == "/") {
+
+ // ----- Look if the directory is in the filename path
+ if ((strlen($v_header['stored_filename']) > strlen($p_options[PCLZIP_OPT_BY_NAME][$j])) && (substr($v_header['stored_filename'], 0, strlen($p_options[PCLZIP_OPT_BY_NAME][$j])) == $p_options[PCLZIP_OPT_BY_NAME][$j])) {
+ $v_extract = true;
+ }
+
+ // ----- Look for a filename
+ } elseif ($v_header['stored_filename'] == $p_options[PCLZIP_OPT_BY_NAME][$j]) {
+ $v_extract = true;
+ }
+ }
+ // ----- Look for extract by ereg rule
+ // ereg() is deprecated with PHP 5.3
+ /*
+ elseif ( (isset($p_options[PCLZIP_OPT_BY_EREG]))
+ && ($p_options[PCLZIP_OPT_BY_EREG] != "")) {
+
+ if (ereg($p_options[PCLZIP_OPT_BY_EREG], $v_header['stored_filename'])) {
+ $v_extract = true;
+ }
+ }
+ */
+
+ // ----- Look for extract by preg rule
+ } elseif ((isset($p_options[PCLZIP_OPT_BY_PREG])) && ($p_options[PCLZIP_OPT_BY_PREG] != "")) {
+
+ if (preg_match($p_options[PCLZIP_OPT_BY_PREG], $v_header['stored_filename'])) {
+ $v_extract = true;
+ }
+
+ // ----- Look for extract by index rule
+ } elseif ((isset($p_options[PCLZIP_OPT_BY_INDEX])) && ($p_options[PCLZIP_OPT_BY_INDEX] != 0)) {
+
+ // ----- Look if the index is in the list
+ for ($j = $j_start; ($j < sizeof($p_options[PCLZIP_OPT_BY_INDEX])) && (!$v_extract); $j++) {
+
+ if (($i >= $p_options[PCLZIP_OPT_BY_INDEX][$j]['start']) && ($i <= $p_options[PCLZIP_OPT_BY_INDEX][$j]['end'])) {
+ $v_extract = true;
+ }
+ if ($i >= $p_options[PCLZIP_OPT_BY_INDEX][$j]['end']) {
+ $j_start = $j + 1;
+ }
+
+ if ($p_options[PCLZIP_OPT_BY_INDEX][$j]['start'] > $i) {
+ break;
+ }
+ }
+
+ // ----- Look for no rule, which means extract all the archive
+ } else {
+ $v_extract = true;
+ }
+
+ // ----- Check compression method
+ if (($v_extract) && (($v_header['compression'] != 8) && ($v_header['compression'] != 0))) {
+ $v_header['status'] = 'unsupported_compression';
+
+ // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
+ if ((isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) && ($p_options[PCLZIP_OPT_STOP_ON_ERROR] === true)) {
+
+ $this->privSwapBackMagicQuotes();
+
+ PclZip::privErrorLog(PCLZIP_ERR_UNSUPPORTED_COMPRESSION, "Filename '" . $v_header['stored_filename'] . "' is " . "compressed by an unsupported compression " . "method (" . $v_header['compression'] . ") ");
+
+ return PclZip::errorCode();
+ }
+ }
+
+ // ----- Check encrypted files
+ if (($v_extract) && (($v_header['flag'] & 1) == 1)) {
+ $v_header['status'] = 'unsupported_encryption';
+
+ // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
+ if ((isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) && ($p_options[PCLZIP_OPT_STOP_ON_ERROR] === true)) {
+
+ $this->privSwapBackMagicQuotes();
+
+ PclZip::privErrorLog(PCLZIP_ERR_UNSUPPORTED_ENCRYPTION, "Unsupported encryption for " . " filename '" . $v_header['stored_filename'] . "'");
+
+ return PclZip::errorCode();
+ }
+ }
+
+ // ----- Look for real extraction
+ if (($v_extract) && ($v_header['status'] != 'ok')) {
+ $v_result = $this->privConvertHeader2FileInfo($v_header, $p_file_list[$v_nb_extracted++]);
+ if ($v_result != 1) {
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+
+ $v_extract = false;
+ }
+
+ // ----- Look for real extraction
+ if ($v_extract) {
+
+ // ----- Go to the file position
+ @rewind($this->zip_fd);
+ if (@fseek($this->zip_fd, $v_header['offset'])) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Look for extraction as string
+ if ($p_options[PCLZIP_OPT_EXTRACT_AS_STRING]) {
+
+ $v_string = '';
+
+ // ----- Extracting the file
+ $v_result1 = $this->privExtractFileAsString($v_header, $v_string, $p_options);
+ if ($v_result1 < 1) {
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result1;
+ }
+
+ // ----- Get the only interesting attributes
+ if (($v_result = $this->privConvertHeader2FileInfo($v_header, $p_file_list[$v_nb_extracted])) != 1) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+
+ // ----- Set the file content
+ $p_file_list[$v_nb_extracted]['content'] = $v_string;
+
+ // ----- Next extracted file
+ $v_nb_extracted++;
+
+ // ----- Look for user callback abort
+ if ($v_result1 == 2) {
+ break;
+ }
+
+ // ----- Look for extraction in standard output
+ } elseif ((isset($p_options[PCLZIP_OPT_EXTRACT_IN_OUTPUT])) && ($p_options[PCLZIP_OPT_EXTRACT_IN_OUTPUT])) {
+ // ----- Extracting the file in standard output
+ $v_result1 = $this->privExtractFileInOutput($v_header, $p_options);
+ if ($v_result1 < 1) {
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result1;
+ }
+
+ // ----- Get the only interesting attributes
+ if (($v_result = $this->privConvertHeader2FileInfo($v_header, $p_file_list[$v_nb_extracted++])) != 1) {
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+
+ // ----- Look for user callback abort
+ if ($v_result1 == 2) {
+ break;
+ }
+
+ // ----- Look for normal extraction
+ } else {
+ // ----- Extracting the file
+ $v_result1 = $this->privExtractFile($v_header, $p_path, $p_remove_path, $p_remove_all_path, $p_options);
+ if ($v_result1 < 1) {
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result1;
+ }
+
+ // ----- Get the only interesting attributes
+ if (($v_result = $this->privConvertHeader2FileInfo($v_header, $p_file_list[$v_nb_extracted++])) != 1) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+
+ // ----- Look for user callback abort
+ if ($v_result1 == 2) {
+ break;
+ }
+ }
+ }
+ }
+
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privExtractFile()
+ // Description :
+ // Parameters :
+ // Return Values :
+ //
+ // 1 : ... ?
+ // PCLZIP_ERR_USER_ABORTED(2) : User ask for extraction stop in callback
+ // --------------------------------------------------------------------------------
+ public function privExtractFile(&$p_entry, $p_path, $p_remove_path, $p_remove_all_path, &$p_options)
+ {
+ $v_result = 1;
+
+ // ----- Read the file header
+ if (($v_result = $this->privReadFileHeader($v_header)) != 1) {
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Check that the file header is coherent with $p_entry info
+ if ($this->privCheckFileHeaders($v_header, $p_entry) != 1) {
+ // TBC
+ }
+
+ // ----- Look for all path to remove
+ if ($p_remove_all_path == true) {
+ // ----- Look for folder entry that not need to be extracted
+ if (($p_entry['external'] & 0x00000010) == 0x00000010) {
+
+ $p_entry['status'] = "filtered";
+
+ return $v_result;
+ }
+
+ // ----- Get the basename of the path
+ $p_entry['filename'] = basename($p_entry['filename']);
+
+ // ----- Look for path to remove
+ } elseif ($p_remove_path != "") {
+ if (PclZipUtilPathInclusion($p_remove_path, $p_entry['filename']) == 2) {
+
+ // ----- Change the file status
+ $p_entry['status'] = "filtered";
+
+ // ----- Return
+ return $v_result;
+ }
+
+ $p_remove_path_size = strlen($p_remove_path);
+ if (substr($p_entry['filename'], 0, $p_remove_path_size) == $p_remove_path) {
+
+ // ----- Remove the path
+ $p_entry['filename'] = substr($p_entry['filename'], $p_remove_path_size);
+
+ }
+ }
+
+ // Patch for Zip Traversal vulnerability
+ if (strpos($p_entry['stored_filename'], '../') !== false || strpos($p_entry['stored_filename'], '..\\') !== false) {
+ $p_entry['stored_filename'] = basename($p_entry['stored_filename']);
+ $p_entry['filename'] = basename($p_entry['stored_filename']);
+ }
+
+ // ----- Add the path
+ if ($p_path != '') {
+ $p_entry['filename'] = $p_path . "/" . $p_entry['filename'];
+ }
+
+ // ----- Check a base_dir_restriction
+ if (isset($p_options[PCLZIP_OPT_EXTRACT_DIR_RESTRICTION])) {
+ $v_inclusion = PclZipUtilPathInclusion($p_options[PCLZIP_OPT_EXTRACT_DIR_RESTRICTION], $p_entry['filename']);
+ if ($v_inclusion == 0) {
+
+ PclZip::privErrorLog(PCLZIP_ERR_DIRECTORY_RESTRICTION, "Filename '" . $p_entry['filename'] . "' is " . "outside PCLZIP_OPT_EXTRACT_DIR_RESTRICTION");
+
+ return PclZip::errorCode();
+ }
+ }
+
+ // ----- Look for pre-extract callback
+ if (isset($p_options[PCLZIP_CB_PRE_EXTRACT])) {
+
+ // ----- Generate a local information
+ $v_local_header = array();
+ $this->privConvertHeader2FileInfo($p_entry, $v_local_header);
+
+ // ----- Call the callback
+ // Here I do not use call_user_func() because I need to send a reference to the
+ // header.
+ // eval('$v_result = '.$p_options[PCLZIP_CB_PRE_EXTRACT].'(PCLZIP_CB_PRE_EXTRACT, $v_local_header);');
+ $v_result = $p_options[PCLZIP_CB_PRE_EXTRACT](PCLZIP_CB_PRE_EXTRACT, $v_local_header);
+ if ($v_result == 0) {
+ // ----- Change the file status
+ $p_entry['status'] = "skipped";
+ $v_result = 1;
+ }
+
+ // ----- Look for abort result
+ if ($v_result == 2) {
+ // ----- This status is internal and will be changed in 'skipped'
+ $p_entry['status'] = "aborted";
+ $v_result = PCLZIP_ERR_USER_ABORTED;
+ }
+
+ // ----- Update the informations
+ // Only some fields can be modified
+ $p_entry['filename'] = $v_local_header['filename'];
+ }
+
+ // ----- Look if extraction should be done
+ if ($p_entry['status'] == 'ok') {
+
+ // ----- Look for specific actions while the file exist
+ if (file_exists($p_entry['filename'])) {
+
+ // ----- Look if file is a directory
+ if (is_dir($p_entry['filename'])) {
+
+ // ----- Change the file status
+ $p_entry['status'] = "already_a_directory";
+
+ // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
+ // For historical reason first PclZip implementation does not stop
+ // when this kind of error occurs.
+ if ((isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) && ($p_options[PCLZIP_OPT_STOP_ON_ERROR] === true)) {
+
+ PclZip::privErrorLog(PCLZIP_ERR_ALREADY_A_DIRECTORY, "Filename '" . $p_entry['filename'] . "' is " . "already used by an existing directory");
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Look if file is write protected
+ } elseif (!is_writeable($p_entry['filename'])) {
+
+ // ----- Change the file status
+ $p_entry['status'] = "write_protected";
+
+ // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
+ // For historical reason first PclZip implementation does not stop
+ // when this kind of error occurs.
+ if ((isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) && ($p_options[PCLZIP_OPT_STOP_ON_ERROR] === true)) {
+
+ PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL, "Filename '" . $p_entry['filename'] . "' exists " . "and is write protected");
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Look if the extracted file is older
+ } elseif (filemtime($p_entry['filename']) > $p_entry['mtime']) {
+ // ----- Change the file status
+ if ((isset($p_options[PCLZIP_OPT_REPLACE_NEWER])) && ($p_options[PCLZIP_OPT_REPLACE_NEWER] === true)) {
+ } else {
+ $p_entry['status'] = "newer_exist";
+
+ // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
+ // For historical reason first PclZip implementation does not stop
+ // when this kind of error occurs.
+ if ((isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) && ($p_options[PCLZIP_OPT_STOP_ON_ERROR] === true)) {
+
+ PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL, "Newer version of '" . $p_entry['filename'] . "' exists " . "and option PCLZIP_OPT_REPLACE_NEWER is not selected");
+
+ return PclZip::errorCode();
+ }
+ }
+ } else {
+ }
+
+ // ----- Check the directory availability and create it if necessary
+ } else {
+ if ((($p_entry['external'] & 0x00000010) == 0x00000010) || (substr($p_entry['filename'], -1) == '/')) {
+ $v_dir_to_check = $p_entry['filename'];
+ } elseif (!strstr($p_entry['filename'], "/")) {
+ $v_dir_to_check = "";
+ } else {
+ $v_dir_to_check = dirname($p_entry['filename']);
+ }
+
+ if (($v_result = $this->privDirCheck($v_dir_to_check, (($p_entry['external'] & 0x00000010) == 0x00000010))) != 1) {
+
+ // ----- Change the file status
+ $p_entry['status'] = "path_creation_fail";
+
+ // ----- Return
+ //return $v_result;
+ $v_result = 1;
+ }
+ }
+ }
+
+ // ----- Look if extraction should be done
+ if ($p_entry['status'] == 'ok') {
+
+ // ----- Do the extraction (if not a folder)
+ if (!(($p_entry['external'] & 0x00000010) == 0x00000010)) {
+ // ----- Look for not compressed file
+ if ($p_entry['compression'] == 0) {
+
+ // ----- Opening destination file
+ if (($v_dest_file = @fopen($p_entry['filename'], 'wb')) == 0) {
+
+ // ----- Change the file status
+ $p_entry['status'] = "write_error";
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
+ $v_size = $p_entry['compressed_size'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @fread($this->zip_fd, $v_read_size);
+ /* Try to speed up the code
+ $v_binary_data = pack('a'.$v_read_size, $v_buffer);
+ @fwrite($v_dest_file, $v_binary_data, $v_read_size);
+ */
+ @fwrite($v_dest_file, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Closing the destination file
+ fclose($v_dest_file);
+
+ // ----- Change the file mtime
+ touch($p_entry['filename'], $p_entry['mtime']);
+
+ } else {
+ // ----- TBC
+ // Need to be finished
+ if (($p_entry['flag'] & 1) == 1) {
+ PclZip::privErrorLog(PCLZIP_ERR_UNSUPPORTED_ENCRYPTION, 'File \'' . $p_entry['filename'] . '\' is encrypted. Encrypted files are not supported.');
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Look for using temporary file to unzip
+ if ((!isset($p_options[PCLZIP_OPT_TEMP_FILE_OFF])) && (isset($p_options[PCLZIP_OPT_TEMP_FILE_ON]) || (isset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD]) && ($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] <= $p_entry['size'])))) {
+ $v_result = $this->privExtractFileUsingTempFile($p_entry, $p_options);
+ if ($v_result < PCLZIP_ERR_NO_ERROR) {
+ return $v_result;
+ }
+
+ // ----- Look for extract in memory
+ } else {
+
+ // ----- Read the compressed file in a buffer (one shot)
+ $v_buffer = @fread($this->zip_fd, $p_entry['compressed_size']);
+
+ // ----- Decompress the file
+ $v_file_content = @gzinflate($v_buffer);
+ unset($v_buffer);
+ if ($v_file_content === false) {
+
+ // ----- Change the file status
+ // TBC
+ $p_entry['status'] = "error";
+
+ return $v_result;
+ }
+
+ // ----- Opening destination file
+ if (($v_dest_file = @fopen($p_entry['filename'], 'wb')) == 0) {
+
+ // ----- Change the file status
+ $p_entry['status'] = "write_error";
+
+ return $v_result;
+ }
+
+ // ----- Write the uncompressed data
+ @fwrite($v_dest_file, $v_file_content, $p_entry['size']);
+ unset($v_file_content);
+
+ // ----- Closing the destination file
+ @fclose($v_dest_file);
+
+ }
+
+ // ----- Change the file mtime
+ @touch($p_entry['filename'], $p_entry['mtime']);
+ }
+
+ // ----- Look for chmod option
+ if (isset($p_options[PCLZIP_OPT_SET_CHMOD])) {
+
+ // ----- Change the mode of the file
+ @chmod($p_entry['filename'], $p_options[PCLZIP_OPT_SET_CHMOD]);
+ }
+
+ }
+ }
+
+ // ----- Change abort status
+ if ($p_entry['status'] == "aborted") {
+ $p_entry['status'] = "skipped";
+
+ // ----- Look for post-extract callback
+ } elseif (isset($p_options[PCLZIP_CB_POST_EXTRACT])) {
+
+ // ----- Generate a local information
+ $v_local_header = array();
+ $this->privConvertHeader2FileInfo($p_entry, $v_local_header);
+
+ // ----- Call the callback
+ // Here I do not use call_user_func() because I need to send a reference to the
+ // header.
+ // eval('$v_result = '.$p_options[PCLZIP_CB_POST_EXTRACT].'(PCLZIP_CB_POST_EXTRACT, $v_local_header);');
+ $v_result = $p_options[PCLZIP_CB_POST_EXTRACT](PCLZIP_CB_POST_EXTRACT, $v_local_header);
+
+ // ----- Look for abort result
+ if ($v_result == 2) {
+ $v_result = PCLZIP_ERR_USER_ABORTED;
+ }
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privExtractFileUsingTempFile()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privExtractFileUsingTempFile(&$p_entry, &$p_options)
+ {
+ $v_result = 1;
+
+ // ----- Creates a temporary file
+ $v_gzip_temp_name = PCLZIP_TEMPORARY_DIR . $this->createUniqueName('pclzip-') . '.gz';
+ if (($v_dest_file = @fopen($v_gzip_temp_name, "wb")) == 0) {
+ fclose($v_file);
+ PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL, 'Unable to open temporary file \'' . $v_gzip_temp_name . '\' in binary write mode');
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Write gz file format header
+ $v_binary_data = pack('va1a1Va1a1', 0x8b1f, Chr($p_entry['compression']), Chr(0x00), time(), Chr(0x00), Chr(3));
+ @fwrite($v_dest_file, $v_binary_data, 10);
+
+ // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
+ $v_size = $p_entry['compressed_size'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @fread($this->zip_fd, $v_read_size);
+ //$v_binary_data = pack('a'.$v_read_size, $v_buffer);
+ @fwrite($v_dest_file, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Write gz file format footer
+ $v_binary_data = pack('VV', $p_entry['crc'], $p_entry['size']);
+ @fwrite($v_dest_file, $v_binary_data, 8);
+
+ // ----- Close the temporary file
+ @fclose($v_dest_file);
+
+ // ----- Opening destination file
+ if (($v_dest_file = @fopen($p_entry['filename'], 'wb')) == 0) {
+ $p_entry['status'] = "write_error";
+
+ return $v_result;
+ }
+
+ // ----- Open the temporary gz file
+ if (($v_src_file = @gzopen($v_gzip_temp_name, 'rb')) == 0) {
+ @fclose($v_dest_file);
+ $p_entry['status'] = "read_error";
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \'' . $v_gzip_temp_name . '\' in binary read mode');
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
+ $v_size = $p_entry['size'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @gzread($v_src_file, $v_read_size);
+ //$v_binary_data = pack('a'.$v_read_size, $v_buffer);
+ @fwrite($v_dest_file, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+ @fclose($v_dest_file);
+ @gzclose($v_src_file);
+
+ // ----- Delete the temporary file
+ @unlink($v_gzip_temp_name);
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privExtractFileInOutput()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privExtractFileInOutput(&$p_entry, &$p_options)
+ {
+ $v_result = 1;
+
+ // ----- Read the file header
+ if (($v_result = $this->privReadFileHeader($v_header)) != 1) {
+ return $v_result;
+ }
+
+ // ----- Check that the file header is coherent with $p_entry info
+ if ($this->privCheckFileHeaders($v_header, $p_entry) != 1) {
+ // TBC
+ }
+
+ // ----- Look for pre-extract callback
+ if (isset($p_options[PCLZIP_CB_PRE_EXTRACT])) {
+
+ // ----- Generate a local information
+ $v_local_header = array();
+ $this->privConvertHeader2FileInfo($p_entry, $v_local_header);
+
+ // ----- Call the callback
+ // Here I do not use call_user_func() because I need to send a reference to the
+ // header.
+ // eval('$v_result = '.$p_options[PCLZIP_CB_PRE_EXTRACT].'(PCLZIP_CB_PRE_EXTRACT, $v_local_header);');
+ $v_result = $p_options[PCLZIP_CB_PRE_EXTRACT](PCLZIP_CB_PRE_EXTRACT, $v_local_header);
+ if ($v_result == 0) {
+ // ----- Change the file status
+ $p_entry['status'] = "skipped";
+ $v_result = 1;
+ }
+
+ // ----- Look for abort result
+ if ($v_result == 2) {
+ // ----- This status is internal and will be changed in 'skipped'
+ $p_entry['status'] = "aborted";
+ $v_result = PCLZIP_ERR_USER_ABORTED;
+ }
+
+ // ----- Update the informations
+ // Only some fields can be modified
+ $p_entry['filename'] = $v_local_header['filename'];
+ }
+
+ // ----- Trace
+
+ // ----- Look if extraction should be done
+ if ($p_entry['status'] == 'ok') {
+
+ // ----- Do the extraction (if not a folder)
+ if (!(($p_entry['external'] & 0x00000010) == 0x00000010)) {
+ // ----- Look for not compressed file
+ if ($p_entry['compressed_size'] == $p_entry['size']) {
+
+ // ----- Read the file in a buffer (one shot)
+ $v_buffer = @fread($this->zip_fd, $p_entry['compressed_size']);
+
+ // ----- Send the file to the output
+ echo $v_buffer;
+ unset($v_buffer);
+ } else {
+
+ // ----- Read the compressed file in a buffer (one shot)
+ $v_buffer = @fread($this->zip_fd, $p_entry['compressed_size']);
+
+ // ----- Decompress the file
+ $v_file_content = gzinflate($v_buffer);
+ unset($v_buffer);
+
+ // ----- Send the file to the output
+ echo $v_file_content;
+ unset($v_file_content);
+ }
+ }
+ }
+
+ // ----- Change abort status
+ if ($p_entry['status'] == "aborted") {
+ $p_entry['status'] = "skipped";
+
+ // ----- Look for post-extract callback
+ } elseif (isset($p_options[PCLZIP_CB_POST_EXTRACT])) {
+
+ // ----- Generate a local information
+ $v_local_header = array();
+ $this->privConvertHeader2FileInfo($p_entry, $v_local_header);
+
+ // ----- Call the callback
+ // Here I do not use call_user_func() because I need to send a reference to the
+ // header.
+ // eval('$v_result = '.$p_options[PCLZIP_CB_POST_EXTRACT].'(PCLZIP_CB_POST_EXTRACT, $v_local_header);');
+ $v_result = $p_options[PCLZIP_CB_POST_EXTRACT](PCLZIP_CB_POST_EXTRACT, $v_local_header);
+
+ // ----- Look for abort result
+ if ($v_result == 2) {
+ $v_result = PCLZIP_ERR_USER_ABORTED;
+ }
+ }
+
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privExtractFileAsString()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privExtractFileAsString(&$p_entry, &$p_string, &$p_options)
+ {
+ $v_result = 1;
+
+ // ----- Read the file header
+ $v_header = array();
+ if (($v_result = $this->privReadFileHeader($v_header)) != 1) {
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Check that the file header is coherent with $p_entry info
+ if ($this->privCheckFileHeaders($v_header, $p_entry) != 1) {
+ // TBC
+ }
+
+ // ----- Look for pre-extract callback
+ if (isset($p_options[PCLZIP_CB_PRE_EXTRACT])) {
+
+ // ----- Generate a local information
+ $v_local_header = array();
+ $this->privConvertHeader2FileInfo($p_entry, $v_local_header);
+
+ // ----- Call the callback
+ // Here I do not use call_user_func() because I need to send a reference to the
+ // header.
+ // eval('$v_result = '.$p_options[PCLZIP_CB_PRE_EXTRACT].'(PCLZIP_CB_PRE_EXTRACT, $v_local_header);');
+ $v_result = $p_options[PCLZIP_CB_PRE_EXTRACT](PCLZIP_CB_PRE_EXTRACT, $v_local_header);
+ if ($v_result == 0) {
+ // ----- Change the file status
+ $p_entry['status'] = "skipped";
+ $v_result = 1;
+ }
+
+ // ----- Look for abort result
+ if ($v_result == 2) {
+ // ----- This status is internal and will be changed in 'skipped'
+ $p_entry['status'] = "aborted";
+ $v_result = PCLZIP_ERR_USER_ABORTED;
+ }
+
+ // ----- Update the informations
+ // Only some fields can be modified
+ $p_entry['filename'] = $v_local_header['filename'];
+ }
+
+ // ----- Look if extraction should be done
+ if ($p_entry['status'] == 'ok') {
+
+ // ----- Do the extraction (if not a folder)
+ if (!(($p_entry['external'] & 0x00000010) == 0x00000010)) {
+ // ----- Look for not compressed file
+ // if ($p_entry['compressed_size'] == $p_entry['size'])
+ if ($p_entry['compression'] == 0) {
+
+ // ----- Reading the file
+ $p_string = @fread($this->zip_fd, $p_entry['compressed_size']);
+ } else {
+
+ // ----- Reading the file
+ $v_data = @fread($this->zip_fd, $p_entry['compressed_size']);
+
+ // ----- Decompress the file
+ if (($p_string = @gzinflate($v_data)) === false) {
+ // TBC
+ }
+ }
+
+ // ----- Trace
+ } else {
+ // TBC : error : can not extract a folder in a string
+ }
+
+ }
+
+ // ----- Change abort status
+ if ($p_entry['status'] == "aborted") {
+ $p_entry['status'] = "skipped";
+
+ // ----- Look for post-extract callback
+ } elseif (isset($p_options[PCLZIP_CB_POST_EXTRACT])) {
+
+ // ----- Generate a local information
+ $v_local_header = array();
+ $this->privConvertHeader2FileInfo($p_entry, $v_local_header);
+
+ // ----- Swap the content to header
+ $v_local_header['content'] = $p_string;
+ $p_string = '';
+
+ // ----- Call the callback
+ // Here I do not use call_user_func() because I need to send a reference to the
+ // header.
+ // eval('$v_result = '.$p_options[PCLZIP_CB_POST_EXTRACT].'(PCLZIP_CB_POST_EXTRACT, $v_local_header);');
+ $v_result = $p_options[PCLZIP_CB_POST_EXTRACT](PCLZIP_CB_POST_EXTRACT, $v_local_header);
+
+ // ----- Swap back the content to header
+ $p_string = $v_local_header['content'];
+ unset($v_local_header['content']);
+
+ // ----- Look for abort result
+ if ($v_result == 2) {
+ $v_result = PCLZIP_ERR_USER_ABORTED;
+ }
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privReadFileHeader()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privReadFileHeader(&$p_header)
+ {
+ $v_result = 1;
+
+ // ----- Read the 4 bytes signature
+ $v_binary_data = @fread($this->zip_fd, 4);
+ $v_data = unpack('Vid', $v_binary_data);
+
+ // ----- Check signature
+ if ($v_data['id'] != 0x04034b50) {
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Invalid archive structure');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the first 42 bytes of the header
+ $v_binary_data = fread($this->zip_fd, 26);
+
+ // ----- Look for invalid block size
+ if (strlen($v_binary_data) != 26) {
+ $p_header['filename'] = "";
+ $p_header['status'] = "invalid_header";
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Invalid block size : " . strlen($v_binary_data));
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Extract the values
+ $v_data = unpack('vversion/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len', $v_binary_data);
+
+ // ----- Get filename
+ $p_header['filename'] = fread($this->zip_fd, $v_data['filename_len']);
+
+ // ----- Get extra_fields
+ if ($v_data['extra_len'] != 0) {
+ $p_header['extra'] = fread($this->zip_fd, $v_data['extra_len']);
+ } else {
+ $p_header['extra'] = '';
+ }
+
+ // ----- Extract properties
+ $p_header['version_extracted'] = $v_data['version'];
+ $p_header['compression'] = $v_data['compression'];
+ $p_header['size'] = $v_data['size'];
+ $p_header['compressed_size'] = $v_data['compressed_size'];
+ $p_header['crc'] = $v_data['crc'];
+ $p_header['flag'] = $v_data['flag'];
+ $p_header['filename_len'] = $v_data['filename_len'];
+
+ // ----- Recuperate date in UNIX format
+ $p_header['mdate'] = $v_data['mdate'];
+ $p_header['mtime'] = $v_data['mtime'];
+ if ($p_header['mdate'] && $p_header['mtime']) {
+ // ----- Extract time
+ $v_hour = ($p_header['mtime'] & 0xF800) >> 11;
+ $v_minute = ($p_header['mtime'] & 0x07E0) >> 5;
+ $v_seconde = ($p_header['mtime'] & 0x001F) * 2;
+
+ // ----- Extract date
+ $v_year = (($p_header['mdate'] & 0xFE00) >> 9) + 1980;
+ $v_month = ($p_header['mdate'] & 0x01E0) >> 5;
+ $v_day = $p_header['mdate'] & 0x001F;
+
+ // ----- Get UNIX date format
+ $p_header['mtime'] = @mktime($v_hour, $v_minute, $v_seconde, $v_month, $v_day, $v_year);
+
+ } else {
+ $p_header['mtime'] = time();
+ }
+
+ // TBC
+ //for (reset($v_data); $key = key($v_data); next($v_data)) {
+ //}
+
+ // ----- Set the stored filename
+ $p_header['stored_filename'] = $p_header['filename'];
+
+ // ----- Set the status field
+ $p_header['status'] = "ok";
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privReadCentralFileHeader()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privReadCentralFileHeader(&$p_header)
+ {
+ $v_result = 1;
+
+ // ----- Read the 4 bytes signature
+ $v_binary_data = @fread($this->zip_fd, 4);
+ $v_data = unpack('Vid', $v_binary_data);
+
+ // ----- Check signature
+ if ($v_data['id'] != 0x02014b50) {
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Invalid archive structure');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the first 42 bytes of the header
+ $v_binary_data = fread($this->zip_fd, 42);
+
+ // ----- Look for invalid block size
+ if (strlen($v_binary_data) != 42) {
+ $p_header['filename'] = "";
+ $p_header['status'] = "invalid_header";
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Invalid block size : " . strlen($v_binary_data));
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Extract the values
+ $p_header = unpack('vversion/vversion_extracted/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len/vcomment_len/vdisk/vinternal/Vexternal/Voffset', $v_binary_data);
+
+ // ----- Get filename
+ if ($p_header['filename_len'] != 0) {
+ $p_header['filename'] = fread($this->zip_fd, $p_header['filename_len']);
+ } else {
+ $p_header['filename'] = '';
+ }
+
+ // ----- Get extra
+ if ($p_header['extra_len'] != 0) {
+ $p_header['extra'] = fread($this->zip_fd, $p_header['extra_len']);
+ } else {
+ $p_header['extra'] = '';
+ }
+
+ // ----- Get comment
+ if ($p_header['comment_len'] != 0) {
+ $p_header['comment'] = fread($this->zip_fd, $p_header['comment_len']);
+ } else {
+ $p_header['comment'] = '';
+ }
+
+ // ----- Extract properties
+
+ // ----- Recuperate date in UNIX format
+ //if ($p_header['mdate'] && $p_header['mtime'])
+ // TBC : bug : this was ignoring time with 0/0/0
+ if (1) {
+ // ----- Extract time
+ $v_hour = ($p_header['mtime'] & 0xF800) >> 11;
+ $v_minute = ($p_header['mtime'] & 0x07E0) >> 5;
+ $v_seconde = ($p_header['mtime'] & 0x001F) * 2;
+
+ // ----- Extract date
+ $v_year = (($p_header['mdate'] & 0xFE00) >> 9) + 1980;
+ $v_month = ($p_header['mdate'] & 0x01E0) >> 5;
+ $v_day = $p_header['mdate'] & 0x001F;
+
+ // ----- Get UNIX date format
+ $p_header['mtime'] = @mktime($v_hour, $v_minute, $v_seconde, $v_month, $v_day, $v_year);
+
+ } else {
+ $p_header['mtime'] = time();
+ }
+
+ // ----- Set the stored filename
+ $p_header['stored_filename'] = $p_header['filename'];
+
+ // ----- Set default status to ok
+ $p_header['status'] = 'ok';
+
+ // ----- Look if it is a directory
+ if (substr($p_header['filename'], -1) == '/') {
+ //$p_header['external'] = 0x41FF0010;
+ $p_header['external'] = 0x00000010;
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privCheckFileHeaders()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // 1 on success,
+ // 0 on error;
+ // --------------------------------------------------------------------------------
+ public function privCheckFileHeaders(&$p_local_header, &$p_central_header)
+ {
+ $v_result = 1;
+
+ // ----- Check the static values
+ // TBC
+ if ($p_local_header['filename'] != $p_central_header['filename']) {
+ }
+ if ($p_local_header['version_extracted'] != $p_central_header['version_extracted']) {
+ }
+ if ($p_local_header['flag'] != $p_central_header['flag']) {
+ }
+ if ($p_local_header['compression'] != $p_central_header['compression']) {
+ }
+ if ($p_local_header['mtime'] != $p_central_header['mtime']) {
+ }
+ if ($p_local_header['filename_len'] != $p_central_header['filename_len']) {
+ }
+
+ // ----- Look for flag bit 3
+ if (($p_local_header['flag'] & 8) == 8) {
+ $p_local_header['size'] = $p_central_header['size'];
+ $p_local_header['compressed_size'] = $p_central_header['compressed_size'];
+ $p_local_header['crc'] = $p_central_header['crc'];
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privReadEndCentralDir()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privReadEndCentralDir(&$p_central_dir)
+ {
+ $v_result = 1;
+
+ // ----- Go to the end of the zip file
+ $v_size = filesize($this->zipname);
+ @fseek($this->zip_fd, $v_size);
+ if (@ftell($this->zip_fd) != $v_size) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Unable to go to the end of the archive \'' . $this->zipname . '\'');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- First try : look if this is an archive with no commentaries (most of the time)
+ // in this case the end of central dir is at 22 bytes of the file end
+ $v_found = 0;
+ if ($v_size > 26) {
+ @fseek($this->zip_fd, $v_size - 22);
+ if (($v_pos = @ftell($this->zip_fd)) != ($v_size - 22)) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Unable to seek back to the middle of the archive \'' . $this->zipname . '\'');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Read for bytes
+ $v_binary_data = @fread($this->zip_fd, 4);
+ $v_data = @unpack('Vid', $v_binary_data);
+
+ // ----- Check signature
+ if ($v_data['id'] == 0x06054b50) {
+ $v_found = 1;
+ }
+
+ $v_pos = ftell($this->zip_fd);
+ }
+
+ // ----- Go back to the maximum possible size of the Central Dir End Record
+ if (!$v_found) {
+ $v_maximum_size = 65557; // 0xFFFF + 22;
+ if ($v_maximum_size > $v_size) {
+ $v_maximum_size = $v_size;
+ }
+ @fseek($this->zip_fd, $v_size - $v_maximum_size);
+ if (@ftell($this->zip_fd) != ($v_size - $v_maximum_size)) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Unable to seek back to the middle of the archive \'' . $this->zipname . '\'');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Read byte per byte in order to find the signature
+ $v_pos = ftell($this->zip_fd);
+ $v_bytes = 0x00000000;
+ while ($v_pos < $v_size) {
+ // ----- Read a byte
+ $v_byte = @fread($this->zip_fd, 1);
+
+ // ----- Add the byte
+ //$v_bytes = ($v_bytes << 8) | Ord($v_byte);
+ // Note we mask the old value down such that once shifted we can never end up with more than a 32bit number
+ // Otherwise on systems where we have 64bit integers the check below for the magic number will fail.
+ $v_bytes = (($v_bytes & 0xFFFFFF) << 8) | Ord($v_byte);
+
+ // ----- Compare the bytes
+ if ($v_bytes == 0x504b0506) {
+ $v_pos++;
+ break;
+ }
+
+ $v_pos++;
+ }
+
+ // ----- Look if not found end of central dir
+ if ($v_pos == $v_size) {
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Unable to find End of Central Dir Record signature");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+ }
+
+ // ----- Read the first 18 bytes of the header
+ $v_binary_data = fread($this->zip_fd, 18);
+
+ // ----- Look for invalid block size
+ if (strlen($v_binary_data) != 18) {
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Invalid End of Central Dir Record size : " . strlen($v_binary_data));
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Extract the values
+ $v_data = unpack('vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_size', $v_binary_data);
+
+ // ----- Check the global size
+ if (($v_pos + $v_data['comment_size'] + 18) != $v_size) {
+
+ // ----- Removed in release 2.2 see readme file
+ // The check of the file size is a little too strict.
+ // Some bugs where found when a zip is encrypted/decrypted with 'crypt'.
+ // While decrypted, zip has training 0 bytes
+ if (0) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'The central dir is not at the end of the archive.' . ' Some trailing bytes exists after the archive.');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+ }
+
+ // ----- Get comment
+ if ($v_data['comment_size'] != 0) {
+ $p_central_dir['comment'] = fread($this->zip_fd, $v_data['comment_size']);
+ } else {
+ $p_central_dir['comment'] = '';
+ }
+
+ $p_central_dir['entries'] = $v_data['entries'];
+ $p_central_dir['disk_entries'] = $v_data['disk_entries'];
+ $p_central_dir['offset'] = $v_data['offset'];
+ $p_central_dir['size'] = $v_data['size'];
+ $p_central_dir['disk'] = $v_data['disk'];
+ $p_central_dir['disk_start'] = $v_data['disk_start'];
+
+ // TBC
+ //for (reset($p_central_dir); $key = key($p_central_dir); next($p_central_dir)) {
+ //}
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privDeleteByRule()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privDeleteByRule(&$p_result_list, &$p_options)
+ {
+ $v_result = 1;
+ $v_list_detail = array();
+
+ // ----- Open the zip file
+ if (($v_result = $this->privOpenFd('rb')) != 1) {
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Read the central directory informations
+ $v_central_dir = array();
+ if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) {
+ $this->privCloseFd();
+
+ return $v_result;
+ }
+
+ // ----- Go to beginning of File
+ @rewind($this->zip_fd);
+
+ // ----- Scan all the files
+ // ----- Start at beginning of Central Dir
+ $v_pos_entry = $v_central_dir['offset'];
+ @rewind($this->zip_fd);
+ if (@fseek($this->zip_fd, $v_pos_entry)) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Read each entry
+ $v_header_list = array();
+ $j_start = 0;
+ for ($i = 0, $v_nb_extracted = 0; $i < $v_central_dir['entries']; $i++) {
+
+ // ----- Read the file header
+ $v_header_list[$v_nb_extracted] = array();
+ if (($v_result = $this->privReadCentralFileHeader($v_header_list[$v_nb_extracted])) != 1) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+
+ return $v_result;
+ }
+
+ // ----- Store the index
+ $v_header_list[$v_nb_extracted]['index'] = $i;
+
+ // ----- Look for the specific extract rules
+ $v_found = false;
+
+ // ----- Look for extract by name rule
+ if ((isset($p_options[PCLZIP_OPT_BY_NAME])) && ($p_options[PCLZIP_OPT_BY_NAME] != 0)) {
+
+ // ----- Look if the filename is in the list
+ for ($j = 0; ($j < sizeof($p_options[PCLZIP_OPT_BY_NAME])) && (!$v_found); $j++) {
+
+ // ----- Look for a directory
+ if (substr($p_options[PCLZIP_OPT_BY_NAME][$j], -1) == "/") {
+
+ // ----- Look if the directory is in the filename path
+ if ((strlen($v_header_list[$v_nb_extracted]['stored_filename']) > strlen($p_options[PCLZIP_OPT_BY_NAME][$j])) && (substr($v_header_list[$v_nb_extracted]['stored_filename'], 0, strlen($p_options[PCLZIP_OPT_BY_NAME][$j])) == $p_options[PCLZIP_OPT_BY_NAME][$j])) {
+ $v_found = true;
+ } elseif ((($v_header_list[$v_nb_extracted]['external'] & 0x00000010) == 0x00000010) /* Indicates a folder */ && ($v_header_list[$v_nb_extracted]['stored_filename'] . '/' == $p_options[PCLZIP_OPT_BY_NAME][$j])) {
+ $v_found = true;
+ }
+
+ // ----- Look for a filename
+ } elseif ($v_header_list[$v_nb_extracted]['stored_filename'] == $p_options[PCLZIP_OPT_BY_NAME][$j]) {
+ $v_found = true;
+ }
+ }
+
+ // ----- Look for extract by ereg rule
+ // ereg() is deprecated with PHP 5.3
+ /*
+ elseif ( (isset($p_options[PCLZIP_OPT_BY_EREG]))
+ && ($p_options[PCLZIP_OPT_BY_EREG] != "")) {
+
+ if (ereg($p_options[PCLZIP_OPT_BY_EREG], $v_header_list[$v_nb_extracted]['stored_filename'])) {
+ $v_found = true;
+ }
+ }
+ */
+
+ // ----- Look for extract by preg rule
+ } elseif ((isset($p_options[PCLZIP_OPT_BY_PREG])) && ($p_options[PCLZIP_OPT_BY_PREG] != "")) {
+
+ if (preg_match($p_options[PCLZIP_OPT_BY_PREG], $v_header_list[$v_nb_extracted]['stored_filename'])) {
+ $v_found = true;
+ }
+
+ // ----- Look for extract by index rule
+ } elseif ((isset($p_options[PCLZIP_OPT_BY_INDEX])) && ($p_options[PCLZIP_OPT_BY_INDEX] != 0)) {
+
+ // ----- Look if the index is in the list
+ for ($j = $j_start; ($j < sizeof($p_options[PCLZIP_OPT_BY_INDEX])) && (!$v_found); $j++) {
+
+ if (($i >= $p_options[PCLZIP_OPT_BY_INDEX][$j]['start']) && ($i <= $p_options[PCLZIP_OPT_BY_INDEX][$j]['end'])) {
+ $v_found = true;
+ }
+ if ($i >= $p_options[PCLZIP_OPT_BY_INDEX][$j]['end']) {
+ $j_start = $j + 1;
+ }
+
+ if ($p_options[PCLZIP_OPT_BY_INDEX][$j]['start'] > $i) {
+ break;
+ }
+ }
+ } else {
+ $v_found = true;
+ }
+
+ // ----- Look for deletion
+ if ($v_found) {
+ unset($v_header_list[$v_nb_extracted]);
+ } else {
+ $v_nb_extracted++;
+ }
+ }
+
+ // ----- Look if something need to be deleted
+ if ($v_nb_extracted > 0) {
+
+ // ----- Creates a temporary file
+ $v_zip_temp_name = PCLZIP_TEMPORARY_DIR . $this->createUniqueName('pclzip-') . '.tmp';
+
+ // ----- Creates a temporary zip archive
+ $v_temp_zip = new PclZip($v_zip_temp_name);
+
+ // ----- Open the temporary zip file in write mode
+ if (($v_result = $v_temp_zip->privOpenFd('wb')) != 1) {
+ $this->privCloseFd();
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Look which file need to be kept
+ for ($i = 0; $i < sizeof($v_header_list); $i++) {
+
+ // ----- Calculate the position of the header
+ @rewind($this->zip_fd);
+ if (@fseek($this->zip_fd, $v_header_list[$i]['offset'])) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $v_temp_zip->privCloseFd();
+ @unlink($v_zip_temp_name);
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the file header
+ $v_local_header = array();
+ if (($v_result = $this->privReadFileHeader($v_local_header)) != 1) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $v_temp_zip->privCloseFd();
+ @unlink($v_zip_temp_name);
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Check that local file header is same as central file header
+ if ($this->privCheckFileHeaders($v_local_header, $v_header_list[$i]) != 1) {
+ // TBC
+ }
+ unset($v_local_header);
+
+ // ----- Write the file header
+ if (($v_result = $v_temp_zip->privWriteFileHeader($v_header_list[$i])) != 1) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $v_temp_zip->privCloseFd();
+ @unlink($v_zip_temp_name);
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Read/write the data block
+ if (($v_result = PclZipUtilCopyBlock($this->zip_fd, $v_temp_zip->zip_fd, $v_header_list[$i]['compressed_size'])) != 1) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $v_temp_zip->privCloseFd();
+ @unlink($v_zip_temp_name);
+
+ // ----- Return
+ return $v_result;
+ }
+ }
+
+ // ----- Store the offset of the central dir
+ $v_offset = @ftell($v_temp_zip->zip_fd);
+
+ // ----- Re-Create the Central Dir files header
+ for ($i = 0; $i < sizeof($v_header_list); $i++) {
+ // ----- Create the file header
+ if (($v_result = $v_temp_zip->privWriteCentralFileHeader($v_header_list[$i])) != 1) {
+ $v_temp_zip->privCloseFd();
+ $this->privCloseFd();
+ @unlink($v_zip_temp_name);
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Transform the header to a 'usable' info
+ $v_temp_zip->privConvertHeader2FileInfo($v_header_list[$i], $p_result_list[$i]);
+ }
+
+ // ----- Zip file comment
+ $v_comment = '';
+ if (isset($p_options[PCLZIP_OPT_COMMENT])) {
+ $v_comment = $p_options[PCLZIP_OPT_COMMENT];
+ }
+
+ // ----- Calculate the size of the central header
+ $v_size = @ftell($v_temp_zip->zip_fd) - $v_offset;
+
+ // ----- Create the central dir footer
+ if (($v_result = $v_temp_zip->privWriteCentralHeader(sizeof($v_header_list), $v_size, $v_offset, $v_comment)) != 1) {
+ // ----- Reset the file list
+ unset($v_header_list);
+ $v_temp_zip->privCloseFd();
+ $this->privCloseFd();
+ @unlink($v_zip_temp_name);
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Close
+ $v_temp_zip->privCloseFd();
+ $this->privCloseFd();
+
+ // ----- Delete the zip file
+ // TBC : I should test the result ...
+ @unlink($this->zipname);
+
+ // ----- Rename the temporary file
+ // TBC : I should test the result ...
+ //@rename($v_zip_temp_name, $this->zipname);
+ PclZipUtilRename($v_zip_temp_name, $this->zipname);
+
+ // ----- Destroy the temporary archive
+ unset($v_temp_zip);
+
+ // ----- Remove every files : reset the file
+ } elseif ($v_central_dir['entries'] != 0) {
+ $this->privCloseFd();
+
+ if (($v_result = $this->privOpenFd('wb')) != 1) {
+ return $v_result;
+ }
+
+ if (($v_result = $this->privWriteCentralHeader(0, 0, 0, '')) != 1) {
+ return $v_result;
+ }
+
+ $this->privCloseFd();
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privDirCheck()
+ // Description :
+ // Check if a directory exists, if not it creates it and all the parents directory
+ // which may be useful.
+ // Parameters :
+ // $p_dir : Directory path to check.
+ // Return Values :
+ // 1 : OK
+ // -1 : Unable to create directory
+ // --------------------------------------------------------------------------------
+ public function privDirCheck($p_dir, $p_is_dir = false)
+ {
+ $v_result = 1;
+
+ // ----- Remove the final '/'
+ if (($p_is_dir) && (substr($p_dir, -1) == '/')) {
+ $p_dir = substr($p_dir, 0, strlen($p_dir) - 1);
+ }
+
+ // ----- Check the directory availability
+ if ((is_dir($p_dir)) || ($p_dir == "")) {
+ return 1;
+ }
+
+ // ----- Extract parent directory
+ $p_parent_dir = dirname($p_dir);
+
+ // ----- Just a check
+ if ($p_parent_dir != $p_dir) {
+ // ----- Look for parent directory
+ if ($p_parent_dir != "") {
+ if (($v_result = $this->privDirCheck($p_parent_dir)) != 1) {
+ return $v_result;
+ }
+ }
+ }
+
+ // ----- Create the directory
+ if (!@mkdir($p_dir, 0777)) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_DIR_CREATE_FAIL, "Unable to create directory '$p_dir'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privMerge()
+ // Description :
+ // If $p_archive_to_add does not exist, the function exit with a success result.
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privMerge(&$p_archive_to_add)
+ {
+ $v_result = 1;
+
+ // ----- Look if the archive_to_add exists
+ if (!is_file($p_archive_to_add->zipname)) {
+
+ // ----- Nothing to merge, so merge is a success
+ $v_result = 1;
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Look if the archive exists
+ if (!is_file($this->zipname)) {
+
+ // ----- Do a duplicate
+ $v_result = $this->privDuplicate($p_archive_to_add->zipname);
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Open the zip file
+ if (($v_result = $this->privOpenFd('rb')) != 1) {
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Read the central directory informations
+ $v_central_dir = array();
+ if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) {
+ $this->privCloseFd();
+
+ return $v_result;
+ }
+
+ // ----- Go to beginning of File
+ @rewind($this->zip_fd);
+
+ // ----- Open the archive_to_add file
+ if (($v_result = $p_archive_to_add->privOpenFd('rb')) != 1) {
+ $this->privCloseFd();
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Read the central directory informations
+ $v_central_dir_to_add = array();
+ if (($v_result = $p_archive_to_add->privReadEndCentralDir($v_central_dir_to_add)) != 1) {
+ $this->privCloseFd();
+ $p_archive_to_add->privCloseFd();
+
+ return $v_result;
+ }
+
+ // ----- Go to beginning of File
+ @rewind($p_archive_to_add->zip_fd);
+
+ // ----- Creates a temporary file
+ $v_zip_temp_name = PCLZIP_TEMPORARY_DIR . $this->createUniqueName('pclzip-') . '.tmp';
+
+ // ----- Open the temporary file in write mode
+ if (($v_zip_temp_fd = @fopen($v_zip_temp_name, 'wb')) == 0) {
+ $this->privCloseFd();
+ $p_archive_to_add->privCloseFd();
+
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \'' . $v_zip_temp_name . '\' in binary write mode');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Copy the files from the archive to the temporary file
+ // TBC : Here I should better append the file and go back to erase the central dir
+ $v_size = $v_central_dir['offset'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = fread($this->zip_fd, $v_read_size);
+ @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Copy the files from the archive_to_add into the temporary file
+ $v_size = $v_central_dir_to_add['offset'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = fread($p_archive_to_add->zip_fd, $v_read_size);
+ @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Store the offset of the central dir
+ $v_offset = @ftell($v_zip_temp_fd);
+
+ // ----- Copy the block of file headers from the old archive
+ $v_size = $v_central_dir['size'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @fread($this->zip_fd, $v_read_size);
+ @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Copy the block of file headers from the archive_to_add
+ $v_size = $v_central_dir_to_add['size'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @fread($p_archive_to_add->zip_fd, $v_read_size);
+ @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Merge the file comments
+ $v_comment = $v_central_dir['comment'] . ' ' . $v_central_dir_to_add['comment'];
+
+ // ----- Calculate the size of the (new) central header
+ $v_size = @ftell($v_zip_temp_fd) - $v_offset;
+
+ // ----- Swap the file descriptor
+ // Here is a trick : I swap the temporary fd with the zip fd, in order to use
+ // the following methods on the temporary fil and not the real archive fd
+ $v_swap = $this->zip_fd;
+ $this->zip_fd = $v_zip_temp_fd;
+ $v_zip_temp_fd = $v_swap;
+
+ // ----- Create the central dir footer
+ if (($v_result = $this->privWriteCentralHeader($v_central_dir['entries'] + $v_central_dir_to_add['entries'], $v_size, $v_offset, $v_comment)) != 1) {
+ $this->privCloseFd();
+ $p_archive_to_add->privCloseFd();
+ @fclose($v_zip_temp_fd);
+ $this->zip_fd = null;
+
+ // ----- Reset the file list
+ unset($v_header_list);
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Swap back the file descriptor
+ $v_swap = $this->zip_fd;
+ $this->zip_fd = $v_zip_temp_fd;
+ $v_zip_temp_fd = $v_swap;
+
+ // ----- Close
+ $this->privCloseFd();
+ $p_archive_to_add->privCloseFd();
+
+ // ----- Close the temporary file
+ @fclose($v_zip_temp_fd);
+
+ // ----- Delete the zip file
+ // TBC : I should test the result ...
+ @unlink($this->zipname);
+
+ // ----- Rename the temporary file
+ // TBC : I should test the result ...
+ //@rename($v_zip_temp_name, $this->zipname);
+ PclZipUtilRename($v_zip_temp_name, $this->zipname);
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privDuplicate()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privDuplicate($p_archive_filename)
+ {
+ $v_result = 1;
+
+ // ----- Look if the $p_archive_filename exists
+ if (!is_file($p_archive_filename)) {
+
+ // ----- Nothing to duplicate, so duplicate is a success.
+ $v_result = 1;
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Open the zip file
+ if (($v_result = $this->privOpenFd('wb')) != 1) {
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Open the temporary file in write mode
+ if (($v_zip_temp_fd = @fopen($p_archive_filename, 'rb')) == 0) {
+ $this->privCloseFd();
+
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive file \'' . $p_archive_filename . '\' in binary write mode');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Copy the files from the archive to the temporary file
+ // TBC : Here I should better append the file and go back to erase the central dir
+ $v_size = filesize($p_archive_filename);
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = fread($v_zip_temp_fd, $v_read_size);
+ @fwrite($this->zip_fd, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Close
+ $this->privCloseFd();
+
+ // ----- Close the temporary file
+ @fclose($v_zip_temp_fd);
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privErrorLog()
+ // Description :
+ // Parameters :
+ // --------------------------------------------------------------------------------
+ public function privErrorLog($p_error_code = 0, $p_error_string = '')
+ {
+ if (PCLZIP_ERROR_EXTERNAL == 1) {
+ PclError($p_error_code, $p_error_string);
+ } else {
+ $this->error_code = $p_error_code;
+ $this->error_string = $p_error_string;
+ }
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privErrorReset()
+ // Description :
+ // Parameters :
+ // --------------------------------------------------------------------------------
+ public function privErrorReset()
+ {
+ if (PCLZIP_ERROR_EXTERNAL == 1) {
+ PclErrorReset();
+ } else {
+ $this->error_code = 0;
+ $this->error_string = '';
+ }
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privDisableMagicQuotes()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privDisableMagicQuotes()
+ {
+ $v_result = 1;
+
+ // ----- Look if function exists
+ if ((!function_exists("get_magic_quotes_runtime")) || (!function_exists("set_magic_quotes_runtime"))) {
+ return $v_result;
+ }
+
+ // ----- Look if already done
+ if ($this->magic_quotes_status != -1) {
+ return $v_result;
+ }
+
+ // ----- Get and memorize the magic_quote value
+ $this->magic_quotes_status = @get_magic_quotes_runtime();
+
+ // ----- Disable magic_quotes
+ if ($this->magic_quotes_status == 1) {
+ @set_magic_quotes_runtime(0);
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privSwapBackMagicQuotes()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privSwapBackMagicQuotes()
+ {
+ $v_result = 1;
+
+ // ----- Look if function exists
+ if ((!function_exists("get_magic_quotes_runtime")) || (!function_exists("set_magic_quotes_runtime"))) {
+ return $v_result;
+ }
+
+ // ----- Look if something to do
+ if ($this->magic_quotes_status != -1) {
+ return $v_result;
+ }
+
+ // ----- Swap back magic_quotes
+ if ($this->magic_quotes_status == 1) {
+ @set_magic_quotes_runtime($this->magic_quotes_status);
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : createUniqueName()
+ // Description : Create a unique(sh) name.
+ // Parameters : $prefix: prefix to use.
+ // Return Values : A unique name starting with $prefix.
+ // --------------------------------------------------------------------------------
+ private function createUniqueName($prefix)
+ {
+ $pseudo_pid = NULL;
+
+ // ----- Try to get our own pid, else a random number
+ if (!function_exists("getmypid")) {
+ $pseudo_pid = mt_rand(1, 99999);
+ }
+ else {
+ $pseudo_pid = getmypid();
+ if (!$pseudo_pid) {
+ $pseudo_pid = mt_rand(1, 99999);
+ }
+ }
+
+ // The reasoning behind this: uniqid() does not return a unique id, even
+ // with 'more_entropy' set to true, uniqid() actually returns a hex version
+ // of the current system time. Adding process id and a couple of random
+ // numbers makes the chance of collisions much smaller when running in
+ // parallel: 4 random numbers 'feels' enough. This does not yet guarantee
+ // 100% uniqueness, but the probability of getting a unique id is much
+ // higher than only using uniqid().
+ //
+ // Downside: the resulting names will be longer.
+
+ // ----- Slap on some random numbers and the pseudo pid
+ $v_result = uniqid($prefix, TRUE) .
+ sprintf('%04X%04X%04X%04X%d',
+ mt_rand(0, 65535), mt_rand(0, 65535),
+ mt_rand(0, 65535), mt_rand(0, 65535), $pseudo_pid);
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+}
+
+// End of class
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// Function : PclZipUtilPathReduction()
+// Description :
+// Parameters :
+// Return Values :
+// --------------------------------------------------------------------------------
+function PclZipUtilPathReduction($p_dir)
+{
+ $v_result = "";
+
+ // ----- Look for not empty path
+ if ($p_dir != "") {
+ // ----- Explode path by directory names
+ $v_list = explode("/", $p_dir);
+
+ // ----- Study directories from last to first
+ $v_skip = 0;
+ for ($i = sizeof($v_list) - 1; $i >= 0; $i--) {
+ // ----- Look for current path
+ if ($v_list[$i] == ".") {
+ // ----- Ignore this directory
+ // Should be the first $i=0, but no check is done
+ } elseif ($v_list[$i] == "..") {
+ $v_skip++;
+ } elseif ($v_list[$i] == "") {
+ // ----- First '/' i.e. root slash
+ if ($i == 0) {
+ $v_result = "/" . $v_result;
+ if ($v_skip > 0) {
+ // ----- It is an invalid path, so the path is not modified
+ // TBC
+ $v_result = $p_dir;
+ $v_skip = 0;
+ }
+
+ // ----- Last '/' i.e. indicates a directory
+ } elseif ($i == (sizeof($v_list) - 1)) {
+ $v_result = $v_list[$i];
+
+ // ----- Double '/' inside the path
+ } else {
+ // ----- Ignore only the double '//' in path,
+ // but not the first and last '/'
+ }
+ } else {
+ // ----- Look for item to skip
+ if ($v_skip > 0) {
+ $v_skip--;
+ } else {
+ $v_result = $v_list[$i] . ($i != (sizeof($v_list) - 1) ? "/" . $v_result : "");
+ }
+ }
+ }
+
+ // ----- Look for skip
+ if ($v_skip > 0) {
+ while ($v_skip > 0) {
+ $v_result = '../' . $v_result;
+ $v_skip--;
+ }
+ }
+ }
+
+ // ----- Return
+ return $v_result;
+}
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// Function : PclZipUtilPathInclusion()
+// Description :
+// This function indicates if the path $p_path is under the $p_dir tree. Or,
+// said in an other way, if the file or sub-dir $p_path is inside the dir
+// $p_dir.
+// The function indicates also if the path is exactly the same as the dir.
+// This function supports path with duplicated '/' like '//', but does not
+// support '.' or '..' statements.
+// Parameters :
+// Return Values :
+// 0 if $p_path is not inside directory $p_dir
+// 1 if $p_path is inside directory $p_dir
+// 2 if $p_path is exactly the same as $p_dir
+// --------------------------------------------------------------------------------
+function PclZipUtilPathInclusion($p_dir, $p_path)
+{
+ $v_result = 1;
+
+ // ----- Look for path beginning by ./
+ if (($p_dir == '.') || ((strlen($p_dir) >= 2) && (substr($p_dir, 0, 2) == './'))) {
+ $p_dir = PclZipUtilTranslateWinPath(getcwd(), false) . '/' . substr($p_dir, 1);
+ }
+ if (($p_path == '.') || ((strlen($p_path) >= 2) && (substr($p_path, 0, 2) == './'))) {
+ $p_path = PclZipUtilTranslateWinPath(getcwd(), false) . '/' . substr($p_path, 1);
+ }
+
+ // ----- Explode dir and path by directory separator
+ $v_list_dir = explode("/", $p_dir);
+ $v_list_dir_size = sizeof($v_list_dir);
+ $v_list_path = explode("/", $p_path);
+ $v_list_path_size = sizeof($v_list_path);
+
+ // ----- Study directories paths
+ $i = 0;
+ $j = 0;
+ while (($i < $v_list_dir_size) && ($j < $v_list_path_size) && ($v_result)) {
+
+ // ----- Look for empty dir (path reduction)
+ if ($v_list_dir[$i] == '') {
+ $i++;
+ continue;
+ }
+ if ($v_list_path[$j] == '') {
+ $j++;
+ continue;
+ }
+
+ // ----- Compare the items
+ if (($v_list_dir[$i] != $v_list_path[$j]) && ($v_list_dir[$i] != '') && ($v_list_path[$j] != '')) {
+ $v_result = 0;
+ }
+
+ // ----- Next items
+ $i++;
+ $j++;
+ }
+
+ // ----- Look if everything seems to be the same
+ if ($v_result) {
+ // ----- Skip all the empty items
+ while (($j < $v_list_path_size) && ($v_list_path[$j] == '')) {
+ $j++;
+ }
+ while (($i < $v_list_dir_size) && ($v_list_dir[$i] == '')) {
+ $i++;
+ }
+
+ if (($i >= $v_list_dir_size) && ($j >= $v_list_path_size)) {
+ // ----- There are exactly the same
+ $v_result = 2;
+ } elseif ($i < $v_list_dir_size) {
+ // ----- The path is shorter than the dir
+ $v_result = 0;
+ }
+ }
+
+ // ----- Return
+ return $v_result;
+}
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// Function : PclZipUtilCopyBlock()
+// Description :
+// Parameters :
+// $p_mode : read/write compression mode
+// 0 : src & dest normal
+// 1 : src gzip, dest normal
+// 2 : src normal, dest gzip
+// 3 : src & dest gzip
+// Return Values :
+// --------------------------------------------------------------------------------
+function PclZipUtilCopyBlock($p_src, $p_dest, $p_size, $p_mode = 0)
+{
+ $v_result = 1;
+
+ if ($p_mode == 0) {
+ while ($p_size != 0) {
+ $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @fread($p_src, $v_read_size);
+ @fwrite($p_dest, $v_buffer, $v_read_size);
+ $p_size -= $v_read_size;
+ }
+ } elseif ($p_mode == 1) {
+ while ($p_size != 0) {
+ $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @gzread($p_src, $v_read_size);
+ @fwrite($p_dest, $v_buffer, $v_read_size);
+ $p_size -= $v_read_size;
+ }
+ } elseif ($p_mode == 2) {
+ while ($p_size != 0) {
+ $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @fread($p_src, $v_read_size);
+ @gzwrite($p_dest, $v_buffer, $v_read_size);
+ $p_size -= $v_read_size;
+ }
+ } elseif ($p_mode == 3) {
+ while ($p_size != 0) {
+ $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @gzread($p_src, $v_read_size);
+ @gzwrite($p_dest, $v_buffer, $v_read_size);
+ $p_size -= $v_read_size;
+ }
+ }
+
+ // ----- Return
+ return $v_result;
+}
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// Function : PclZipUtilRename()
+// Description :
+// This function tries to do a simple rename() function. If it fails, it
+// tries to copy the $p_src file in a new $p_dest file and then unlink the
+// first one.
+// Parameters :
+// $p_src : Old filename
+// $p_dest : New filename
+// Return Values :
+// 1 on success, 0 on failure.
+// --------------------------------------------------------------------------------
+function PclZipUtilRename($p_src, $p_dest)
+{
+ $v_result = 1;
+
+ // ----- Try to rename the files
+ if (!@rename($p_src, $p_dest)) {
+
+ // ----- Try to copy & unlink the src
+ if (!@copy($p_src, $p_dest)) {
+ $v_result = 0;
+ } elseif (!@unlink($p_src)) {
+ $v_result = 0;
+ }
+ }
+
+ // ----- Return
+ return $v_result;
+}
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// Function : PclZipUtilOptionText()
+// Description :
+// Translate option value in text. Mainly for debug purpose.
+// Parameters :
+// $p_option : the option value.
+// Return Values :
+// The option text value.
+// --------------------------------------------------------------------------------
+function PclZipUtilOptionText($p_option)
+{
+
+ $v_list = get_defined_constants();
+ for (reset($v_list); $v_key = key($v_list); next($v_list)) {
+ $v_prefix = substr($v_key, 0, 10);
+ if ((($v_prefix == 'PCLZIP_OPT') || ($v_prefix == 'PCLZIP_CB_') || ($v_prefix == 'PCLZIP_ATT')) && ($v_list[$v_key] == $p_option)) {
+ return $v_key;
+ }
+ }
+
+ $v_result = 'Unknown';
+
+ return $v_result;
+}
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// Function : PclZipUtilTranslateWinPath()
+// Description :
+// Translate windows path by replacing '\' by '/' and optionally removing
+// drive letter.
+// Parameters :
+// $p_path : path to translate.
+// $p_remove_disk_letter : true | false
+// Return Values :
+// The path translated.
+// --------------------------------------------------------------------------------
+function PclZipUtilTranslateWinPath($p_path, $p_remove_disk_letter = true)
+{
+ if (stristr(php_uname(), 'windows')) {
+ // ----- Look for potential disk letter
+ if (($p_remove_disk_letter) && (($v_position = strpos($p_path, ':')) != false)) {
+ $p_path = substr($p_path, $v_position + 1);
+ }
+ // ----- Change potential windows directory separator
+ if ((strpos($p_path, '\\') > 0) || (substr($p_path, 0, 1) == '\\')) {
+ $p_path = strtr($p_path, '\\', '/');
+ }
+ }
+
+ return $p_path;
+}
+// --------------------------------------------------------------------------------
diff --git a/vendor/sboden/odtphp/phpunit-runner.php b/vendor/sboden/odtphp/phpunit-runner.php
new file mode 100755
index 0000000000..6242d8d740
--- /dev/null
+++ b/vendor/sboden/odtphp/phpunit-runner.php
@@ -0,0 +1,36 @@
+load($arguments);
+$testRunner = new TestRunner();
+$result = $testRunner->run($configuration);
+
+exit($result->wasSuccessful() ? 0 : 1);
diff --git a/vendor/sboden/odtphp/phpunit.xml b/vendor/sboden/odtphp/phpunit.xml
new file mode 100755
index 0000000000..ddab5b05b3
--- /dev/null
+++ b/vendor/sboden/odtphp/phpunit.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+ tests/src
+
+
+
+
+ src
+
+
+ vendor
+
+
+
+
+
+
diff --git a/vendor/sboden/odtphp/run-tests.bat b/vendor/sboden/odtphp/run-tests.bat
new file mode 100755
index 0000000000..883007f2a7
--- /dev/null
+++ b/vendor/sboden/odtphp/run-tests.bat
@@ -0,0 +1,8 @@
+@echo off
+
+rem Assuming you're on Windows.
+rem Assuming you have php.exe in your path
+rem
+rem Add option "--debug" to get more output.
+
+php.exe phpunit-runner.php --configuration=phpunit.xml
diff --git a/vendor/sboden/odtphp/run-tests.sh b/vendor/sboden/odtphp/run-tests.sh
new file mode 100755
index 0000000000..53deed6340
--- /dev/null
+++ b/vendor/sboden/odtphp/run-tests.sh
@@ -0,0 +1,7 @@
+#/bin/bash
+
+#
+# Add option "--debug" to get more output.
+#
+
+vendor/bin/phpunit --configuration=phpunit.xml
diff --git a/vendor/sboden/odtphp/src/Attributes/AllowDynamicProperties.php b/vendor/sboden/odtphp/src/Attributes/AllowDynamicProperties.php
new file mode 100755
index 0000000000..d494290f45
--- /dev/null
+++ b/vendor/sboden/odtphp/src/Attributes/AllowDynamicProperties.php
@@ -0,0 +1,18 @@
+
+ */
+ protected array $config = [
+ 'ZIP_PROXY' => PhpZipProxy::class,
+ 'DELIMITER_LEFT' => '{',
+ 'DELIMITER_RIGHT' => '}',
+ 'PATH_TO_TMP' => NULL
+ ];
+
+ /**
+ * Content of the content.xml file.
+ */
+ protected string $contentXml = '';
+
+ /**
+ * Content of the manifest.xml file.
+ */
+ protected string $manifestXml = '';
+
+ /**
+ * Content of the styles.xml file.
+ */
+ protected string $stylesXml = '';
+
+ /**
+ * Content of the meta.xml file.
+ */
+ protected string $metaXml = '';
+
+ /**
+ * Temporary file path.
+ */
+ protected string $tmpfile = '';
+
+ /**
+ * Array of images used in the document.
+ *
+ * @var array
+ */
+ protected array $images = [];
+
+ /**
+ * Template variables.
+ *
+ * @var array
+ */
+ protected array $vars = [];
+
+ /**
+ * Manifest variables for image tracking.
+ *
+ * @var array
+ */
+ protected array $manifestVars = [];
+
+ /**
+ * Document segments.
+ *
+ * @var array
+ */
+ protected array $segments = [];
+
+ /**
+ * Initialize ODT document handling.
+ *
+ * @param string $filename
+ * Path to the ODT template file to process.
+ * @param array $config
+ * Configuration options for document processing.
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ * When the ODT file cannot be initialized or processed.
+ */
+ public function __construct(
+ protected readonly string $filename,
+ array $config = [],
+ ) {
+ // Merge and validate configuration.
+ $this->config = $this->mergeAndValidateConfig($config);
+
+ // Set default temporary directory if not provided.
+ if ($this->config['PATH_TO_TMP'] === NULL) {
+ $this->config['PATH_TO_TMP'] = sys_get_temp_dir();
+ }
+
+ // Validate configuration components.
+ $this->validateTemporaryDirectory();
+ $this->validateZipProxy();
+
+ // Initialize properties and process file.
+ $this->initializeProperties();
+ $this->processZipFile();
+ }
+
+ /**
+ * Merge and validate configuration options.
+ *
+ * @param array $config
+ * User-provided configuration.
+ *
+ * @return array
+ * Validated configuration array with default values merged.
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ * If configuration is invalid or cannot be processed.
+ */
+ private function mergeAndValidateConfig(array $config): array {
+ // Start with default configuration.
+ $mergedConfig = $this->config;
+
+ // Merge user configuration.
+ foreach ($config as $key => $value) {
+ $mergedConfig[$key] = $value;
+ }
+
+ return $mergedConfig;
+ }
+
+ /**
+ * Validate temporary directory configuration.
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ * If the temporary directory is invalid or inaccessible.
+ */
+ private function validateTemporaryDirectory(): void {
+ $path = $this->config['PATH_TO_TMP'];
+
+ if (!is_string($path)) {
+ throw new OdfException('Temporary directory path must be a string');
+ }
+
+ $path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
+
+ if (!is_dir($path)) {
+ throw new OdfException('Temporary directory does not exist');
+ }
+
+ if (!is_writable($path)) {
+ throw new OdfException('Temporary directory is not writable');
+ }
+
+ $this->config['PATH_TO_TMP'] = $path;
+ }
+
+ /**
+ * Validate ZIP proxy configuration.
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ * If the ZIP proxy class is invalid or does not implement the required interface.
+ *
+ * @return void
+ * Validates the ZIP proxy class configuration.
+ */
+ private function validateZipProxy(): void {
+ $zipProxyClass = $this->config['ZIP_PROXY'];
+
+ if (!class_exists($zipProxyClass)) {
+ throw new OdfException("ZIP proxy class does not exist: $zipProxyClass");
+ }
+
+ if (!is_subclass_of($zipProxyClass, ZipInterface::class)) {
+ throw new OdfException("$zipProxyClass must implement ZipInterface");
+ }
+ }
+
+ /**
+ * Initialize object properties with default values.
+ *
+ * @return void
+ * Initializes all internal object properties to their default values.
+ */
+ private function initializeProperties(): void {
+ $this->contentXml = '';
+ $this->manifestXml = '';
+ $this->stylesXml = '';
+ $this->metaXml = '';
+ $this->tmpfile = '';
+ $this->images = [];
+ $this->vars = [];
+ $this->manifestVars = [];
+ $this->segments = [];
+ }
+
+ /**
+ * Process the ZIP file and extract necessary XML contents.
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ * If the file cannot be processed or XML contents cannot be extracted.
+ *
+ * @return void
+ * Processes the ZIP file and extracts required XML contents.
+ */
+ private function processZipFile(): void {
+ // Validate file existence.
+ if (!file_exists($this->filename)) {
+ throw new OdfException("File '{$this->filename}' does not exist");
+ }
+
+ // Create ZIP handler.
+ $zipHandlerClass = $this->config['ZIP_PROXY'];
+ $this->file = new $zipHandlerClass($this->config['PATH_TO_TMP']);
+
+ // Open ZIP file.
+ if ($this->file->open($this->filename) !== TRUE) {
+ throw new OdfException("Error opening file '{$this->filename}'");
+ }
+
+ // Extract XML contents.
+ $this->extractXmlContents();
+
+ // Close the ZIP file.
+ $this->file->close();
+
+ // Create a temporary copy of the file.
+ $this->createTemporaryFileCopy();
+
+ // Process row segments.
+ $this->moveRowSegments();
+ }
+
+ /**
+ * Extract XML contents from the ZIP file.
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ * If XML content extraction fails or required files are missing.
+ *
+ * @return void
+ * Extracts and stores XML content from the ODT file.
+ */
+ private function extractXmlContents(): void {
+ // Extract content.xml.
+ $this->contentXml = $this->file->getFromName('content.xml');
+ if ($this->contentXml === FALSE) {
+ throw new OdfException("Error during content.xml extraction");
+ }
+
+ // Extract manifest.xml.
+ $this->manifestXml = $this->file->getFromName('META-INF/manifest.xml');
+ if ($this->manifestXml === FALSE) {
+ throw new OdfException("Error during manifest.xml extraction");
+ }
+
+ // Extract styles.xml.
+ $this->stylesXml = $this->file->getFromName('styles.xml');
+ if ($this->stylesXml === FALSE) {
+ throw new OdfException("Error during styles.xml extraction");
+ }
+
+ // Extract meta.xml.
+ $this->metaXml = $this->file->getFromName('meta.xml');
+ if ($this->metaXml === FALSE) {
+ throw new OdfException("Error during meta.xml extraction");
+ }
+ }
+
+ /**
+ * Create a temporary copy of the file for processing.
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ * If the temporary file cannot be created or copied.
+ *
+ * @return void
+ * Creates a temporary copy of the ODT file.
+ */
+ private function createTemporaryFileCopy(): void {
+ $this->tmpfile = tempnam($this->config['PATH_TO_TMP'], 'odtphp_');
+ if ($this->tmpfile === FALSE) {
+ throw new OdfException('Error creating temporary file');
+ }
+ copy($this->filename, $this->tmpfile);
+ }
+
+ /**
+ * Delete the temporary file when the object is destroyed.
+ *
+ * @return void
+ * Cleans up temporary files used during processing.
+ */
+ public function __destruct() {
+ if (file_exists($this->tmpfile)) {
+ unlink($this->tmpfile);
+ }
+ }
+
+ /**
+ * Assign a template variable.
+ *
+ * @param string $key
+ * Name of the variable within the template.
+ * @param string $value
+ * Replacement value for the variable.
+ * @param bool $encode
+ * Whether to encode special XML characters for safe XML output.
+ * @param string $charset
+ * Character set encoding of the input value (defaults to UTF-8).
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ * When the variable is not found in the document.
+ *
+ * @return $this
+ * The current ODT object for method chaining.
+ */
+ public function setVars($key, $value, $encode = TRUE, $charset = 'UTF-8'): self {
+ $tag = $this->config['DELIMITER_LEFT'] . $key . $this->config['DELIMITER_RIGHT'];
+ if (strpos($this->contentXml, $tag) === FALSE && strpos($this->stylesXml, $tag) === FALSE) {
+ throw new OdfException("var $key not found in the document");
+ }
+
+ // Handle encoding.
+ $value = $encode ? $this->recursiveHtmlspecialchars($value) : $value;
+
+ // Convert to UTF-8 if not already.
+ if ($charset !== 'UTF-8') {
+ $value = mb_convert_encoding($value, 'UTF-8', $charset);
+ }
+
+ $this->vars[$tag] = str_replace("\n", " ", $value);
+ return $this;
+ }
+
+ /**
+ * Set the value of a variable in a template.
+ *
+ * @param string $key
+ * Name of the variable within the template.
+ * @param string $value
+ * Replacement value for the variable.
+ * @param bool $encode
+ * Whether to encode special XML characters for safe XML output.
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ * When the variable is not found in the document.
+ *
+ * @return $this
+ * The current ODT object for method chaining.
+ */
+ public function setVariable($key, $value, $encode = TRUE): self {
+ return $this->setVars($key, $value, $encode);
+ }
+
+ /**
+ * Check if a variable exists in the template.
+ *
+ * @param string $key
+ * Name of the variable to check for in the template.
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ * When the variable check operation fails.
+ *
+ * @return bool
+ * TRUE if the variable exists in the document, FALSE otherwise.
+ */
+ public function variableExists($key): bool {
+ return strpos($this->contentXml, $this->config['DELIMITER_LEFT'] . $key . $this->config['DELIMITER_RIGHT']) !== FALSE;
+ }
+
+ /**
+ * Assign a template variable as a picture.
+ *
+ * @param string $key
+ * Name of the variable within the template.
+ * @param string $value
+ * Absolute or relative path to the picture file.
+ * @param int $page
+ * Page number to anchor the image to (-1 for as-char anchoring).
+ * @param int|null $width
+ * Width of the picture in cm (null to keep original).
+ * @param int|null $height
+ * Height of the picture in cm (null to keep original).
+ * @param int|null $offsetX
+ * Horizontal offset in cm (ignored if $page is -1).
+ * @param int|null $offsetY
+ * Vertical offset in cm (ignored if $page is -1).
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ * When the image cannot be added or processed.
+ *
+ * @return $this
+ * The current ODT object for method chaining.
+ */
+ public function setImage($key, $value, $page = -1, $width = NULL, $height = NULL, $offsetX = NULL, $offsetY = NULL): self {
+ $filename = strtok(strrchr($value, '/'), '/.');
+ $file = substr(strrchr($value, '/'), 1);
+ $size = @getimagesize($value);
+ if ($size === FALSE) {
+ throw new OdfException("Invalid image");
+ }
+ if (!$width && !$height) {
+ [$width, $height] = $size;
+ $width *= $this->getPixelToCm();
+ $height *= $this->getPixelToCm();
+ }
+ $anchor = $page == -1 ? 'text:anchor-type="as-char"' : "text:anchor-type=\"page\" text:anchor-page-number=\"{$page}\" svg:x=\"{$offsetX}cm\" svg:y=\"{$offsetY}cm\"";
+ $xml = <<
+IMG;
+
+ $this->images[$value] = $file;
+ $this->manifestVars[] = $file;
+ $this->setVars($key, $xml, FALSE);
+ return $this;
+ }
+
+ /**
+ * Assign a template variable as a picture using millimeter measurements.
+ *
+ * @param string $key
+ * Name of the variable within the template.
+ * @param string $value
+ * Path to the picture.
+ * @param int $page
+ * Page number to anchor the image to (-1 for as-char anchoring)
+ * @param float|null $width
+ * Width of the picture in millimeters (null to keep original)
+ * @param float|null $height
+ * Height of the picture in millimeters (null to keep original)
+ * @param float $offsetX
+ * Horizontal offset in millimeters (ignored if $page is -1)
+ * @param float $offsetY
+ * Vertical offset in millimeters (ignored if $page is -1)
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ *
+ * @return Odf
+ * The Odf document so you can chain functions.
+ */
+ public function setImageMm($key, $value, $page = -1, $width = NULL, $height = NULL, $offsetX = 0, $offsetY = 0): self {
+ $filename = strtok(strrchr($value, '/'), '/.');
+ $file = substr(strrchr($value, '/'), 1);
+ $size = @getimagesize($value);
+ if ($size === FALSE) {
+ throw new OdfException("Invalid image");
+ }
+
+ if (!$width && !$height) {
+ // Convert pixels to mm (1 inch = 25.4 mm, 1 inch = 96 pixels)
+ $mmPerPixel = 25.4 / 96;
+ $width = $size[0] * $mmPerPixel;
+ $height = $size[1] * $mmPerPixel;
+ }
+
+ // Format to 2 decimal places.
+ $width = number_format($width, 2, '.', '');
+ $height = number_format($height, 2, '.', '');
+ $offsetX = number_format($offsetX, 2, '.', '');
+ $offsetY = number_format($offsetY, 2, '.', '');
+
+ $anchor = $page == -1 ? 'text:anchor-type="as-char"' :
+ "text:anchor-type=\"page\" text:anchor-page-number=\"{$page}\" svg:x=\"{$offsetX}mm\" svg:y=\"{$offsetY}mm\"";
+
+ $xml = <<
+IMG;
+
+ $this->images[$value] = $file;
+ $this->manifestVars[] = $file;
+ $this->setVars($key, $xml, FALSE);
+ return $this;
+ }
+
+ /**
+ * Assign a template variable as a picture using pixel measurements.
+ *
+ * @param string $key
+ * Name of the variable within the template.
+ * @param string $value
+ * Path to the picture.
+ * @param int $page
+ * Page number to anchor the image to (-1 for as-char anchoring)
+ * @param int|null $width
+ * Width of the picture in pixels (null to keep original)
+ * @param int|null $height
+ * Height of the picture in pixels (null to keep original)
+ * @param int $offsetX
+ * Horizontal offset in pixels (ignored if $page is -1)
+ * @param int $offsetY
+ * Vertical offset in pixels (ignored if $page is -1)
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ *
+ * @return Odf
+ * The Odf document so you can chain functions.
+ */
+ public function setImagePixel($key, $value, $page = -1, $width = NULL, $height = NULL, $offsetX = 0, $offsetY = 0): self {
+ $filename = strtok(strrchr($value, '/'), '/.');
+ $file = substr(strrchr($value, '/'), 1);
+ $size = @getimagesize($value);
+ if ($size === FALSE) {
+ throw new OdfException("Invalid image");
+ }
+
+ if (!$width && !$height) {
+ [$width, $height] = $size;
+ }
+
+ // Convert pixels to mm (1 inch = 25.4 mm, 1 inch = 96 pixels)
+ $mmPerPixel = 25.4 / 96;
+ $widthMm = $width * $mmPerPixel;
+ $heightMm = $height * $mmPerPixel;
+ $offsetXMm = $offsetX * $mmPerPixel;
+ $offsetYMm = $offsetY * $mmPerPixel;
+
+ // Format to 2 decimal places.
+ $widthMm = number_format($widthMm, 2, '.', '');
+ $heightMm = number_format($heightMm, 2, '.', '');
+ $offsetXMm = number_format($offsetXMm, 2, '.', '');
+ $offsetYMm = number_format($offsetYMm, 2, '.', '');
+
+ $anchor = $page == -1 ? 'text:anchor-type="as-char"' :
+ "text:anchor-type=\"page\" text:anchor-page-number=\"{$page}\" svg:x=\"{$offsetXMm}mm\" svg:y=\"{$offsetYMm}mm\"";
+
+ $xml = <<
+IMG;
+
+ $this->images[$value] = $file;
+ $this->manifestVars[] = $file;
+ $this->setVars($key, $xml, FALSE);
+ return $this;
+ }
+
+ /**
+ * Move segment tags for lines of tables.
+ *
+ * Called automatically within the constructor.
+ *
+ * @return void
+ * Modifies the internal XML content by moving segment tags.
+ */
+ private function moveRowSegments(): void {
+ // Search all possible rows in the document.
+ $reg1 = "#]*>(.*) #smU";
+ preg_match_all($reg1, $this->contentXml, $matches);
+ for ($i = 0, $size = count($matches[0]); $i < $size; $i++) {
+ // Check if the current row contains a segment row.*.
+ $reg2 = '#\[!--\sBEGIN\s(row.[\S]*)\s--\](.*)\[!--\sEND\s\\1\s--\]#smU';
+ if (preg_match($reg2, $matches[0][$i], $matches2)) {
+ $balise = str_replace('row.', '', $matches2[1]);
+ // Move segment tags around the row.
+ $replace = [
+ '[!-- BEGIN ' . $matches2[1] . ' --]' => '',
+ '[!-- END ' . $matches2[1] . ' --]' => '',
+ ' '[!-- BEGIN ' . $balise . ' --]' => ' [!-- END ' . $balise . ' --]'
+ ];
+ $replacedXML = str_replace(array_keys($replace), array_values($replace), $matches[0][$i]);
+ $this->contentXml = str_replace($matches[0][$i], $replacedXML, $this->contentXml);
+ }
+ }
+ }
+
+ /**
+ * Merge template variables.
+ *
+ * Called automatically for a save operation.
+ *
+ * @return void
+ * Processes and updates the internal XML content with merged variables.
+ */
+ private function parse(): void {
+ $this->contentXml = str_replace(array_keys($this->vars), array_values($this->vars), $this->contentXml);
+ $this->stylesXml = str_replace(array_keys($this->vars), array_values($this->vars), $this->stylesXml);
+ }
+
+ /**
+ * Add the merged segment to the document.
+ *
+ * @param \Odtphp\Segment $segment
+ * The segment to merge.
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ * When the segment cannot be merged or has not been set.
+ *
+ * @return $this
+ * The current ODT object instance.
+ */
+ public function mergeSegment(Segment $segment): self {
+ if (!array_key_exists($segment->getName(), $this->segments)) {
+ throw new OdfException($segment->getName() . ' cannot be parsed, has it been set yet?');
+ }
+ $string = $segment->getName();
+ $reg = '@\[!--\sBEGIN\s' . $string . '\s--\](.*)\[!--.+END\s' . $string . '\s--\]@smU';
+ $this->contentXml = preg_replace($reg, $segment->getXmlParsed(), $this->contentXml);
+ foreach ($segment->manifestVars as $val) {
+ // Copy all segment image names into current array.
+ $this->manifestVars[] = $val;
+ }
+ return $this;
+ }
+
+ /**
+ * Display all the current template variables.
+ *
+ * @return string
+ * The formatted string containing all template variables.
+ */
+ public function printVars(): string {
+ return print_r('' . print_r($this->vars, TRUE) . ' ', TRUE);
+ }
+
+ /**
+ * Display the XML content of the file from ODT document as it is at the moment.
+ *
+ * @return string
+ * The XML content of the ODT document.
+ */
+ public function __toString(): string {
+ return $this->contentXml;
+ }
+
+ /**
+ * Display loop segments declared with setSegment().
+ *
+ * @return string
+ * Space-separated list of declared segments.
+ */
+ public function printDeclaredSegments(): string {
+ return '' . print_r(implode(' ', array_keys($this->segments)), TRUE) . ' ';
+ }
+
+ /**
+ * Check if the specified segment exists in the document.
+ *
+ * @param string $segment
+ * The name of the segment to check.
+ *
+ * @return bool
+ * TRUE when segment exists, FALSE otherwise.
+ */
+ public function segmentExists($segment): bool {
+ $reg = "#\[!--\sBEGIN\s$segment\s--](.*?)\[!--\sEND\s$segment\s--]#smU";
+ return preg_match($reg, html_entity_decode($this->contentXml), $m) != 0;
+ }
+
+ /**
+ * Declare a segment in order to use it in a loop.
+ *
+ * @param string $segment
+ * The name of the segment to declare.
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ * When the segment cannot be found in the document.
+ *
+ * @return \Odtphp\Segment
+ * The requested segment object for use in a loop.
+ */
+ public function setSegment($segment): Segment {
+ if (array_key_exists($segment, $this->segments)) {
+ return $this->segments[$segment];
+ }
+ $reg = "#\[!--\sBEGIN\s$segment\s--\](.*?)\[!--\sEND\s$segment\s--\]#smU";
+ if (preg_match($reg, html_entity_decode($this->contentXml), $m) == 0) {
+ throw new OdfException("'$segment' segment not found in the document");
+ }
+ $this->segments[$segment] = new Segment($segment, $m[1], $this);
+ return $this->segments[$segment];
+ }
+
+ /**
+ * Save the ODT file to disk.
+ *
+ * @param string|null $file
+ * Name of the desired file. If null, uses the original filename.
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ * When the file cannot be saved to disk.
+ *
+ * @return void
+ * Saves the ODT file to the specified location.
+ */
+ public function saveToDisk($file = NULL): void {
+ $this->saveInternal();
+ if ($file === NULL) {
+ $file = $this->filename;
+ }
+ copy($this->tmpfile, $file);
+ }
+
+ /**
+ * Export the file as an attached file via HTTP.
+ *
+ * @param string $name
+ * Optional name for the downloaded file.
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ * When the file cannot be exported or sent via HTTP.
+ *
+ * @return void
+ * Sends the ODT file as an HTTP attachment.
+ */
+ public function exportAsAttachedFile($name = ""): void {
+ $this->saveInternal();
+ if (empty($name)) {
+ $name = basename($this->filename);
+ }
+ header('Content-type: application/vnd.oasis.opendocument.text');
+ header('Content-Disposition: attachment; filename="' . $name . '"');
+ readfile($this->tmpfile);
+ }
+
+ /**
+ * Save internal ODT file state.
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ * When the internal file state cannot be saved.
+ *
+ * @return void
+ * Updates the internal ODT file state with current changes.
+ */
+ private function saveInternal(): void {
+ $this->file->open($this->tmpfile);
+ $this->parse();
+ if (!$this->file->addFromString('content.xml', $this->contentXml) || !$this->file->addFromString('styles.xml', $this->stylesXml)) {
+ throw new OdfException('Error during file export addFromString');
+ }
+ // Find second last newline in the manifest.xml file.
+ $lastpos = strrpos($this->manifestXml, "\n", -15);
+ $manifdata = "";
+
+ // Enter all images description in $manifdata variable.
+ foreach ($this->manifestVars as $val) {
+ $ext = substr(strrchr($val, '.'), 1);
+ $manifdata = $manifdata . ' ' . "\n";
+ }
+
+ // Place content of $manifdata variable in manifest.xml file at appropriate place.
+ $replace = ' ';
+ if ((strlen($manifdata) > 0) && (strpos($this->manifestXml, $replace) !== FALSE)) {
+ $this->manifestXml = str_replace($replace,
+ $replace . "\n" . $manifdata, $this->manifestXml);
+ }
+ else {
+ // This branch is a fail-safe but normally should not be used.
+ $this->manifestXml = substr_replace($this->manifestXml, "\n" . $manifdata, $lastpos + 1, 0);
+ }
+ if (!$this->file->addFromString('META-INF/manifest.xml', $this->manifestXml)) {
+ throw new OdfException('Error during manifest file export');
+ }
+ foreach ($this->images as $imageKey => $imageValue) {
+ $this->file->addFile($imageKey, 'Pictures/' . $imageValue);
+ }
+ // Seems to bug on windows CLI sometimes.
+ $this->file->close();
+ }
+
+ /**
+ * Returns a variable of configuration.
+ *
+ * @param string $configKey
+ * The name of the configuration variable to retrieve.
+ *
+ * @return string
+ * The requested configuration value.
+ */
+ public function getConfig($configKey): string {
+ if (array_key_exists($configKey, $this->config)) {
+ return $this->config[$configKey];
+ }
+ return FALSE;
+ }
+
+ /**
+ * Get the current configuration.
+ *
+ * @return array
+ * The complete configuration array.
+ */
+ public function getAllConfig(): array {
+ return $this->config;
+ }
+
+ /**
+ * Returns the temporary working file.
+ *
+ * @return string
+ * The path to the temporary working file.
+ */
+ public function getTmpfile(): string {
+ return $this->tmpfile;
+ }
+
+ /**
+ * Get the pixel to centimeter conversion ratio.
+ *
+ * @return float
+ * The pixel to centimeter conversion ratio.
+ */
+ public function getPixelToCm(): float {
+ return self::PIXEL_TO_CM;
+ }
+
+ /**
+ * Recursive htmlspecialchars.
+ *
+ * @param mixed $value
+ * The value to convert.
+ *
+ * @return mixed
+ * The converted value.
+ */
+ protected function recursiveHtmlspecialchars($value): mixed {
+ if (is_array($value)) {
+ return array_map([$this, 'recursiveHtmlspecialchars'], $value);
+ }
+ else {
+ return htmlspecialchars((string) ($value ?? ''));
+ }
+ }
+
+ /**
+ * Function to set custom properties in the ODT file's meta.xml.
+ *
+ * @param string $key
+ * Name of the variable within the template.
+ * @param string $value
+ * Replacement value for the variable.
+ * @param bool $encode
+ * Whether to encode special XML characters for safe XML output.
+ * @param string $charset
+ * Character set encoding of the input value (defaults to UTF-8).
+ */
+ public function setCustomProperty($key, $value, $encode = TRUE, $charset = 'UTF-8'): self {
+ if (!is_string($key) || !is_string($value)) {
+ throw new OdfException('Key and value must be strings');
+ }
+
+ if ($encode) {
+ $value = htmlspecialchars($value, ENT_QUOTES | ENT_XML1, $charset);
+ }
+
+ // Convert to UTF-8 if not already.
+ if ($charset !== 'UTF-8') {
+ $value = mb_convert_encoding($value, 'UTF-8', $charset);
+ }
+
+ try {
+ // Create the pattern to match the existing custom property.
+ $pattern = '/]*?)meta:name="' . preg_quote($key, '/') . '"([^>]*?)>.*?<\/meta:user-defined>/';
+
+ // Check if property exists and update it.
+ if (preg_match($pattern, $this->metaXml, $matches)) {
+ // Preserve the existing attributes.
+ $attributes = $matches[1] . 'meta:name="' . $key . '"' . $matches[2];
+
+ // Create the replacement custom property with preserved attributes.
+ $replacement = '' . $value . ' ';
+
+ // Replace the property.
+ $this->metaXml = preg_replace($pattern, $replacement, $this->metaXml);
+
+ // Open the temporary file and update meta.xml.
+ if ($this->file->open($this->tmpfile) !== TRUE) {
+ throw new OdfException('Error opening file');
+ }
+
+ if (!$this->file->addFromString('meta.xml', $this->metaXml)) {
+ throw new OdfException('Error updating meta.xml file');
+ }
+
+ $this->file->close();
+ }
+ else {
+ throw new OdfException("Custom property '$key' not found in meta.xml");
+ }
+
+ return $this;
+ }
+ catch (\ValueError $e) {
+ throw new OdfException('Error during metadata operation');
+ }
+ }
+
+ /**
+ * Get the ZIP file handler.
+ *
+ * @return \Odtphp\Zip\ZipInterface
+ * The ZIP file handler.
+ */
+ public function getFile(): ZipInterface {
+ return $this->file;
+ }
+
+ /**
+ * Get the meta XML content.
+ *
+ * @return string
+ * The meta XML content.
+ */
+ public function getMetaXml(): string {
+ return $this->metaXml;
+ }
+
+ /**
+ * Check if a custom property exists in the ODT file.
+ *
+ * @param string $key The name of the custom property to check
+ * @return bool TRUE if the property exists, FALSE otherwise
+ */
+ public function customPropertyExists(string $key): bool
+ {
+ // HTML encode the key for XML comparison
+ $encodedKey = htmlspecialchars($key, ENT_QUOTES | ENT_XML1);
+
+ // Pattern to match custom property with the given name
+ $pattern = '/]*?)meta:name="' . preg_quote($encodedKey, '/') . '"([^>]*?)>/';
+
+ // Check if the pattern exists in metaXml
+ return (bool) preg_match($pattern, $this->metaXml);
+ }
+
+}
diff --git a/vendor/sboden/odtphp/src/Segment.php b/vendor/sboden/odtphp/src/Segment.php
new file mode 100755
index 0000000000..7f2f60ed4e
--- /dev/null
+++ b/vendor/sboden/odtphp/src/Segment.php
@@ -0,0 +1,381 @@
+name = (string) $name;
+ $this->xml = (string) $xml;
+ $this->odf = $odf;
+ $zipHandler = $this->odf->getConfig('ZIP_PROXY');
+ $this->file = new $zipHandler();
+ $this->analyseChildren($this->xml);
+ }
+
+ /**
+ * Returns the name of the segment.
+ *
+ * @return string
+ * The name of the segment.
+ */
+ public function getName(): string {
+ return $this->name;
+ }
+
+ /**
+ * Checks if the segment has children.
+ *
+ * @return bool
+ * TRUE if the segment has children, FALSE otherwise.
+ */
+ public function hasChildren(): bool {
+ return !empty($this->children);
+ }
+
+ /**
+ * Implements the Countable interface.
+ *
+ * @return int
+ * Number of children in the segment.
+ */
+ #[\ReturnTypeWillChange]
+ public function count(): int {
+ return count($this->children);
+ }
+
+ /**
+ * Implements the IteratorAggregate interface.
+ *
+ * @return \RecursiveIteratorIterator
+ * Iterator for the segment's children.
+ */
+ #[\ReturnTypeWillChange]
+ public function getIterator(): \RecursiveIteratorIterator {
+ return new \RecursiveIteratorIterator(new SegmentIterator($this->children), 1);
+ }
+
+ /**
+ * Replace variables of the template in the XML code.
+ *
+ * All the children are also processed.
+ *
+ * @return string
+ * The merged XML content with variables replaced.
+ */
+ public function merge(): string {
+ $this->xmlParsed .= str_replace(array_keys($this->vars), array_values($this->vars), $this->xml);
+ if ($this->hasChildren()) {
+ foreach ($this->children as $child) {
+ $this->xmlParsed = str_replace($child->xml, ($child->xmlParsed == "") ? $child->merge() : $child->xmlParsed, $this->xmlParsed);
+ $child->xmlParsed = '';
+ // Store all image names used in child segments in current segment array.
+ foreach ($child->manifestVars as $file) {
+ $this->manifestVars[] = $file;
+ }
+
+ $child->manifestVars = [];
+ }
+ }
+ $reg = "/\[!--\sBEGIN\s$this->name\s--\](.*)\[!--\sEND\s$this->name\s--\]/smU";
+ $this->xmlParsed = preg_replace($reg, '$1', $this->xmlParsed);
+ $this->file->open($this->odf->getTmpfile());
+ foreach ($this->images as $imageKey => $imageValue) {
+ if ($this->file->getFromName('Pictures/' . $imageValue) === FALSE) {
+ $this->file->addFile($imageKey, 'Pictures/' . $imageValue);
+ }
+ }
+
+ $this->file->close();
+ return $this->xmlParsed;
+ }
+
+ /**
+ * Analyse the XML code to find children segments.
+ *
+ * @param string $xml
+ * XML content to analyse.
+ *
+ * @return $this
+ * The current segment instance.
+ */
+ protected function analyseChildren($xml): self {
+ $reg2 = "#\[!--\sBEGIN\s([\S]*)\s--\](.*)\[!--\sEND\s(\\1)\s--\]#smU";
+ preg_match_all($reg2, $xml, $matches);
+ for ($i = 0, $size = count($matches[0]); $i < $size; $i++) {
+ if ($matches[1][$i] != $this->name) {
+ $this->children[$matches[1][$i]] = new self($matches[1][$i], $matches[0][$i], $this->odf);
+ }
+ else {
+ $this->analyseChildren($matches[2][$i]);
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * Assign a template variable to replace.
+ *
+ * @param string $key
+ * The variable key to replace.
+ * @param string $value
+ * The value to replace the variable with.
+ * @param bool $encode
+ * Whether to HTML encode the value.
+ * @param string $charset
+ * Character set for encoding.
+ *
+ * @throws \Odtphp\Exceptions\SegmentException
+ * If the variable is not found in the segment.
+ *
+ * @return $this
+ * The current segment instance.
+ */
+ public function setVars($key, $value, $encode = TRUE, $charset = 'UTF-8'): self {
+ if (strpos($this->xml, $this->odf->getConfig('DELIMITER_LEFT') . $key . $this->odf->getConfig('DELIMITER_RIGHT')) === FALSE) {
+ throw new SegmentException("var $key not found in {$this->getName()}");
+ }
+ $value = $encode ? htmlspecialchars($value) : $value;
+ $value = ($charset != 'UTF-8') ? mb_convert_encoding($value, 'UTF-8', $charset) : $value;
+ $this->vars[$this->odf->getConfig('DELIMITER_LEFT') . $key . $this->odf->getConfig('DELIMITER_RIGHT')] = str_replace("\n", " ", $value);
+ return $this;
+ }
+
+ /**
+ * Assign a template variable as a picture.
+ *
+ * @param string $key
+ * Name of the variable within the template.
+ * @param string $value
+ * Path to the picture.
+ * @param int|null $page
+ * Anchor to page number (or -1 if anchor-type is aschar).
+ * @param string|null $width
+ * Width of picture (keep original if null).
+ * @param string|null $height
+ * Height of picture (keep original if null).
+ * @param string|null $offsetX
+ * Offset by horizontal (not used if $page = -1).
+ * @param string|null $offsetY
+ * Offset by vertical (not used if $page = -1).
+ *
+ * @throws \Odtphp\Exceptions\OdfException
+ * If the image is invalid.
+ *
+ * @return $this
+ * The current segment instance.
+ */
+ public function setImage($key, $value, $page = -1, $width = NULL, $height = NULL, $offsetX = NULL, $offsetY = NULL): self {
+ $filename = strtok(strrchr($value, '/'), '/.');
+ $file = substr(strrchr($value, '/'), 1);
+ $size = @getimagesize($value);
+ if ($size === FALSE) {
+ throw new OdfException("Invalid image");
+ }
+ if (!$width && !$height) {
+ [$width, $height] = $size;
+ $width *= $this->odf->getPixelToCm();
+ $height *= $this->odf->getPixelToCm();
+ }
+ $anchor = $page == -1 ? 'text:anchor-type="as-char"' : "text:anchor-type=\"page\" text:anchor-page-number=\"{$page}\" svg:x=\"{$offsetX}cm\" svg:y=\"{$offsetY}cm\"";
+ $xml = <<
+IMG;
+ $this->images[$value] = $file;
+ $this->manifestVars[] = $file;
+ $this->setVars($key, $xml, FALSE);
+ return $this;
+ }
+
+ /**
+ * Shortcut to retrieve a child.
+ *
+ * @param string $prop
+ * The name of the child segment to retrieve.
+ *
+ * @return Segment
+ * The child segment instance if it exists.
+ *
+ * @throws \Odtphp\Exceptions\SegmentException
+ * If the child segment does not exist.
+ */
+ public function __get($prop): Segment {
+ if (array_key_exists($prop, $this->children)) {
+ return $this->children[$prop];
+ }
+ else {
+ throw new SegmentException('child ' . $prop . ' does not exist');
+ }
+ }
+
+ /**
+ * Proxy for setVars.
+ *
+ * @param string $meth
+ * The method name being called.
+ * @param array $args
+ * The arguments passed to the method.
+ *
+ * @return Segment
+ * The current segment instance after setting variables.
+ *
+ * @throws \Odtphp\Exceptions\SegmentException
+ * If the method or variable does not exist.
+ */
+ public function __call($meth, $args): Segment {
+ try {
+ array_unshift($args, $meth);
+ return call_user_func_array([$this, 'setVars'], $args);
+ }
+ catch (SegmentException $e) {
+ throw new SegmentException("method $meth nor var $meth exist");
+ }
+ }
+
+ /**
+ * Retrieve the parsed XML content.
+ *
+ * @return string
+ * The parsed XML content of the segment.
+ */
+ public function getXmlParsed(): string {
+ return $this->xmlParsed;
+ }
+
+ /**
+ * Create a new child segment.
+ *
+ * @param string $name
+ * Name of the child segment.
+ *
+ * @return Segment
+ * The child segment with the specified name.
+ *
+ * @throws \Odtphp\Exceptions\SegmentException
+ * If the segment does not exist.
+ */
+ public function setSegment($name): Segment {
+ if (!isset($this->children[$name])) {
+ throw new SegmentException("Segment '$name' does not exist");
+ }
+ return $this->children[$name];
+ }
+
+ /**
+ * Retrieve the XML content of the segment.
+ *
+ * @return string
+ * The original XML content of the segment.
+ */
+ public function getXml(): string {
+ return $this->xml;
+ }
+
+ /**
+ * Retrieve the segment's children.
+ *
+ * @return Segment[]
+ * An array of child segments.
+ */
+ public function getChildren(): array {
+ return $this->children;
+ }
+
+ /**
+ * Retrieve the segment's variables.
+ *
+ * @return array
+ * An array of variables in the segment.
+ */
+ public function getVars(): array {
+ return $this->vars;
+ }
+
+}
diff --git a/vendor/sboden/odtphp/src/SegmentIterator.php b/vendor/sboden/odtphp/src/SegmentIterator.php
new file mode 100755
index 0000000000..2fec05ca2f
--- /dev/null
+++ b/vendor/sboden/odtphp/src/SegmentIterator.php
@@ -0,0 +1,73 @@
+
+ */
+class SegmentIterator implements \Iterator, \Countable {
+ /**
+ * Current position in the iterator.
+ */
+ private int $position = 0;
+
+ /**
+ * Initialize segment iterator.
+ *
+ * @param array $segments
+ * Array of segments to iterate over.
+ */
+ public function __construct(
+ private readonly array $segments,
+ ) {}
+
+ /**
+ * Reset iterator position.
+ */
+ public function rewind(): void {
+ $this->position = 0;
+ }
+
+ /**
+ * Get current segment.
+ */
+ public function current(): Segment {
+ return $this->segments[$this->position];
+ }
+
+ /**
+ * Get current position.
+ */
+ public function key(): int {
+ return $this->position;
+ }
+
+ /**
+ * Move to next segment.
+ */
+ public function next(): void {
+ ++$this->position;
+ }
+
+ /**
+ * Check if current position is valid.
+ */
+ public function valid(): bool {
+ return isset($this->segments[$this->position]);
+ }
+
+ /**
+ * Get total number of segments.
+ */
+ public function count(): int {
+ return count($this->segments);
+ }
+
+}
diff --git a/vendor/sboden/odtphp/src/Zip/PclZipProxy.php b/vendor/sboden/odtphp/src/Zip/PclZipProxy.php
new file mode 100755
index 0000000000..eadc7dc617
--- /dev/null
+++ b/vendor/sboden/odtphp/src/Zip/PclZipProxy.php
@@ -0,0 +1,249 @@
+tmpDir = sys_get_temp_dir() . '/tmpdir_odtphp_' . uniqid(sprintf('%04X%04X%04X%04X%d',
+ mt_rand(0, 65535), mt_rand(0, 65535),
+ mt_rand(0, 65535), mt_rand(0, 65535), $pseudo_pid),
+ TRUE);
+ }
+
+ /**
+ * Open a Zip archive.
+ *
+ * @param string $filename
+ * The name of the archive to open.
+ *
+ * @return bool
+ * TRUE if opening has succeeded.
+ */
+ public function open($filename) {
+ if (TRUE === $this->opened) {
+ $this->close();
+ }
+ $this->filename = $filename;
+ $this->pclzip = new \PclZip($this->filename);
+ if (!file_exists($this->tmpDir)) {
+ if (mkdir($this->tmpDir)) {
+ // Created a new directory.
+ $this->opened = TRUE;
+ return TRUE;
+ }
+ else {
+ // Failed to create a directory.
+ $this->opened = FALSE;
+ return FALSE;
+ }
+ }
+ else {
+ // Directory already existed.
+ $this->opened = FALSE;
+ return FALSE;
+ }
+ }
+
+ /**
+ * Retrieve the content of a file within the archive from its name.
+ *
+ * @param string $name
+ * The name of the file to extract.
+ *
+ * @return string|bool
+ * The content of the file as a string, or FALSE if retrieval fails.
+ */
+ public function getFromName($name) {
+ if (FALSE === $this->opened) {
+ return FALSE;
+ }
+ $name = preg_replace("/(?:\.|\/)*(.*)/", "\\1", $name);
+ $extraction = $this->pclzip->extract(PCLZIP_OPT_BY_NAME, $name, PCLZIP_OPT_EXTRACT_AS_STRING);
+ if (!empty($extraction)) {
+ return $extraction[0]['content'];
+ }
+ return FALSE;
+ }
+
+ /**
+ * Add a file within the archive from a string.
+ *
+ * @param string $localname
+ * The local path to the file in the archive.
+ * @param string $contents
+ * The content of the file.
+ *
+ * @return bool
+ * TRUE if the file has been successfully added.
+ */
+ public function addFromString($localname, $contents) {
+ if (FALSE === $this->opened) {
+ return FALSE;
+ }
+ if (file_exists($this->filename) && !is_writable($this->filename)) {
+ return FALSE;
+ }
+ $localname = preg_replace("/(?:\.|\/)*(.*)/", "\\1", $localname);
+ $localpath = dirname($localname);
+ $tmpfilename = $this->tmpDir . '/' . basename($localname);
+ if (FALSE !== file_put_contents($tmpfilename, $contents)) {
+ $this->pclzip->delete(PCLZIP_OPT_BY_NAME, $localname);
+ $add = $this->pclzip->add($tmpfilename, PCLZIP_OPT_REMOVE_PATH, $this->tmpDir, PCLZIP_OPT_ADD_PATH, $localpath);
+ unlink($tmpfilename);
+ if (!empty($add)) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * Add a file within the archive from a file.
+ *
+ * @param string $filename
+ * The path to the file we want to add.
+ * @param string|null $localname
+ * The local path to the file in the archive.
+ *
+ * @return bool
+ * TRUE if the file has been successfully added.
+ */
+ public function addFile($filename, $localname = NULL) {
+ if (FALSE === $this->opened) {
+ return FALSE;
+ }
+ if ((file_exists($this->filename) && !is_writable($this->filename))
+ || !file_exists($filename)) {
+ return FALSE;
+ }
+ if (isset($localname)) {
+ $localname = preg_replace("/(?:\.|\/)*(.*)/", "\\1", $localname);
+ $localpath = dirname($localname);
+ $tmpfilename = $this->tmpDir . '/' . basename($localname);
+ }
+ else {
+ $localname = basename($filename);
+ $tmpfilename = $this->tmpDir . '/' . $localname;
+ $localpath = '';
+ }
+ if (file_exists($filename)) {
+ copy($filename, $tmpfilename);
+ $this->pclzip->delete(PCLZIP_OPT_BY_NAME, $localname);
+ $this->pclzip->add($tmpfilename, PCLZIP_OPT_REMOVE_PATH, $this->tmpDir, PCLZIP_OPT_ADD_PATH, $localpath);
+ unlink($tmpfilename);
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Close the Zip archive.
+ *
+ * @return bool
+ * TRUE if the archive was closed successfully.
+ */
+ public function close() {
+ if (FALSE === $this->opened) {
+ return FALSE;
+ }
+ $this->pclzip = $this->filename = NULL;
+ $this->opened = FALSE;
+ if (file_exists($this->tmpDir)) {
+ $this->removeDir($this->tmpDir);
+ }
+ return TRUE;
+ }
+
+ /**
+ * Remove directory recursively.
+ *
+ * @param string $dir
+ * The directory to remove.
+ *
+ * @return bool
+ * TRUE if the directory was successfully removed.
+ */
+ private function removeDir($dir): bool {
+ if ($handle = opendir($dir)) {
+ while (FALSE !== ($file = readdir($handle))) {
+ if ($file != '.' && $file != '..') {
+ if (is_dir($dir . '/' . $file)) {
+ $this->removeDir($dir . '/' . $file);
+ rmdir($dir . '/' . $file);
+ }
+ else {
+ unlink($dir . '/' . $file);
+ }
+ }
+ }
+ closedir($handle);
+ }
+ return rmdir($dir);
+ }
+
+}
diff --git a/vendor/sboden/odtphp/src/Zip/PhpZipProxy.php b/vendor/sboden/odtphp/src/Zip/PhpZipProxy.php
new file mode 100755
index 0000000000..a80496c054
--- /dev/null
+++ b/vendor/sboden/odtphp/src/Zip/PhpZipProxy.php
@@ -0,0 +1,122 @@
+zipArchive = new \ZipArchive();
+ }
+
+ /**
+ * Open a Zip archive.
+ *
+ * @param string $filename
+ * The name of the archive to open.
+ *
+ * @return bool
+ * TRUE if opening has succeeded.
+ */
+ public function open($filename) {
+ $this->filename = $filename;
+ return $this->zipArchive->open($filename, \ZIPARCHIVE::CREATE);
+ }
+
+ /**
+ * Retrieve the content of a file within the archive from its name.
+ *
+ * @param string $name
+ * The name of the file to extract.
+ *
+ * @return string|bool
+ * The content of the file as a string, or FALSE if retrieval fails.
+ */
+ public function getFromName($name) {
+ return $this->zipArchive->getFromName($name);
+ }
+
+ /**
+ * Add a file within the archive from a string.
+ *
+ * @param string $localname
+ * The local path to the file in the archive.
+ * @param string $contents
+ * The content of the file.
+ *
+ * @return bool
+ * TRUE if the file has been successfully added.
+ */
+ public function addFromString($localname, $contents) {
+ if (file_exists($this->filename) && !is_writable($this->filename)) {
+ return FALSE;
+ }
+ return $this->zipArchive->addFromString($localname, $contents);
+ }
+
+ /**
+ * Add a file within the archive from a file.
+ *
+ * @param string $filename
+ * The path to the file we want to add.
+ * @param string|null $localname
+ * The local path to the file in the archive.
+ *
+ * @return bool
+ * TRUE if the file has been successfully added.
+ */
+ public function addFile($filename, $localname = NULL) {
+ if ((file_exists($this->filename) && !is_writable($this->filename))
+ || !file_exists($filename)) {
+ return FALSE;
+ }
+ return $this->zipArchive->addFile($filename, $localname);
+ }
+
+ /**
+ * Close the Zip archive.
+ *
+ * @return bool
+ * TRUE if the archive was closed successfully.
+ */
+ public function close() {
+ return $this->zipArchive->close();
+ }
+
+}
diff --git a/vendor/sboden/odtphp/src/Zip/ZipInterface.php b/vendor/sboden/odtphp/src/Zip/ZipInterface.php
new file mode 100755
index 0000000000..7d831de154
--- /dev/null
+++ b/vendor/sboden/odtphp/src/Zip/ZipInterface.php
@@ -0,0 +1,74 @@
+odfPhpZipConfig();
+ $gold_dir = 'gold_phpzip';
+ $type_name = 'PhpZip';
+ }
+ else {
+ $config = $this->odfPclZipConfig();
+ $gold_dir = 'gold_pclzip';
+ $type_name = 'PclZip';
+ }
+
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ $odf->setVars('titre',
+ 'PHP: Hypertext Preprocessor');
+
+ $message = "PHP (sigle de PHP: Hypertext Preprocessor), est un langage de scripts libre
+principalement utilisé pour produire des pages Web dynamiques via un serveur HTTP, mais
+pouvant également fonctionner comme n'importe quel langage interprété de façon locale,
+en exécutant les programmes en ligne de commande.";
+
+ $odf->setVars('message', $message, TRUE, 'UTF-8');
+
+ $output_name = __DIR__ . "/files/output/BasicTest1" . $type_name . "Output.odt";
+ // We export the file
+ $odf->saveToDisk($output_name);
+
+ // print("\nComparing files:\n $output_name\n files/$gold_dir/BasicTest1Gold.odt\n");
+ $this->assertTrue($this->odtFilesAreIdentical($output_name, __DIR__ . "/files/$gold_dir/BasicTest1Gold.odt"));
+
+ unlink($output_name);
+ }
+
+ public function testBasicPclZip1(): void {
+ $this->basic1(self::PCLZIP_TYPE);
+ }
+
+ public function testBasicPhpZip1(): void {
+ $this->basic1(self::PHPZIP_TYPE);
+ }
+
+}
diff --git a/vendor/sboden/odtphp/tests/src/Basic2Test.php b/vendor/sboden/odtphp/tests/src/Basic2Test.php
new file mode 100755
index 0000000000..5e957da507
--- /dev/null
+++ b/vendor/sboden/odtphp/tests/src/Basic2Test.php
@@ -0,0 +1,54 @@
+odfPhpZipConfig();
+ $gold_dir = 'gold_phpzip';
+ $type_name = 'PhpZip';
+ }
+ else {
+ $config = $this->odfPclZipConfig();
+ $gold_dir = 'gold_pclzip';
+ $type_name = 'PclZip';
+ }
+
+ $input_file = __DIR__ . '/files/input/BasicTest2.odt';
+ $odf = new Odf($input_file, $config);
+
+ $odf->setVars('titre', 'PHP: Hypertext Preprocessor');
+
+ $message = "PHP (sigle de PHP: Hypertext Preprocessor), est un langage de scripts libre
+principalement utilisé pour produire des pages Web dynamiques via un serveur HTTP, mais
+pouvant également fonctionner comme n'importe quel langage interprété de façon locale,
+en exécutant les programmes en ligne de commande.";
+
+ $odf->setVars('message', $message, TRUE, 'UTF-8');
+
+ $odf->setImage('image', __DIR__ . '/files/images/anaska.jpg');
+
+ $output_name = __DIR__ . "/files/output/BasicTest2" . $type_name . "Output.odt";
+ $odf->saveToDisk($output_name);
+
+ $this->assertTrue($this->odtFilesAreIdentical($output_name, __DIR__ . "/files/$gold_dir/BasicTest2Gold.odt"));
+
+ unlink($output_name);
+ }
+
+ public function testBasicPclZip2(): void {
+ $this->basic2(self::PCLZIP_TYPE);
+ }
+
+ public function testBasicPhpZip2(): void {
+ $this->basic2(self::PHPZIP_TYPE);
+ }
+}
diff --git a/vendor/sboden/odtphp/tests/src/BasicTest1.odt b/vendor/sboden/odtphp/tests/src/BasicTest1.odt
new file mode 100755
index 0000000000..05eab715a7
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/BasicTest1.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/ConfigTest.php b/vendor/sboden/odtphp/tests/src/ConfigTest.php
new file mode 100755
index 0000000000..97a232a37e
--- /dev/null
+++ b/vendor/sboden/odtphp/tests/src/ConfigTest.php
@@ -0,0 +1,45 @@
+expectException(\TypeError::class);
+ $this->expectExceptionMessage('Argument #2 ($config) must be of type array');
+ new Odf(__DIR__ . '/files/input/BasicTest1.odt', 'invalid');
+ }
+
+ public function testCustomDelimiters(): void {
+ $config = array_merge($this->odfPhpZipConfig(), [
+ 'DELIMITER_LEFT' => '[[',
+ 'DELIMITER_RIGHT' => ']]'
+ ]);
+
+ $odf = new Odf(__DIR__ . "/files/input/ConfigTest.odt", $config);
+ $odf->setVars('test_var', 'test value');
+
+ $output_name = __DIR__ . "/files/output/ConfigTestOutput.odt";
+ $odf->saveToDisk($output_name);
+
+ $this->assertTrue($this->odtFilesAreIdentical($output_name, __DIR__ . "/files/gold_phpzip/ConfigTestGold.odt"));
+ unlink($output_name);
+ }
+
+ public function testInvalidTempPath(): void {
+ $config = array_merge($this->odfPhpZipConfig(), [
+ 'PATH_TO_TMP' => '/nonexistent/path'
+ ]);
+
+ $this->expectException(OdfException::class);
+ new Odf(__DIR__ . "/files/input/BasicTest1.odt", $config);
+ }
+}
diff --git a/vendor/sboden/odtphp/tests/src/CustomPropertyExistsTest.php b/vendor/sboden/odtphp/tests/src/CustomPropertyExistsTest.php
new file mode 100644
index 0000000000..2ee263b6cc
--- /dev/null
+++ b/vendor/sboden/odtphp/tests/src/CustomPropertyExistsTest.php
@@ -0,0 +1,91 @@
+path = __DIR__ . '/files/';
+ }
+
+ /**
+ * Test checking for existing custom properties.
+ */
+ public function testExistingCustomProperties(): void {
+ $odf = new Odf($this->path . 'input/CustomPropertyExistsTest.odt');
+
+ // Test properties that should exist in the template.
+ $this->assertTrue($odf->customPropertyExists('Author'));
+ $this->assertTrue($odf->customPropertyExists('Version'));
+ $this->assertTrue($odf->customPropertyExists('Department'));
+ }
+
+ /**
+ * Test checking for non-existing custom properties.
+ */
+ public function testNonExistingCustomProperties(): void {
+ $odf = new Odf($this->path . 'input/CustomPropertyExistsTest.odt');
+
+ // Test properties that should not exist in the template.
+ $this->assertFalse($odf->customPropertyExists('NonExistentProperty'));
+ $this->assertFalse($odf->customPropertyExists(''));
+ $this->assertFalse($odf->customPropertyExists('Random Property'));
+ }
+
+ /**
+ * Test checking properties with special characters.
+ */
+ public function testSpecialCharacterProperties(): void {
+ $odf = new Odf($this->path . 'input/CustomPropertyExistsTest.odt');
+
+ // Test properties with special characters that should exist.
+ $this->assertTrue($odf->customPropertyExists('Special & Property'));
+ $this->assertTrue($odf->customPropertyExists('Property XML chars'));
+ }
+
+ /**
+ * Test checking properties after setting them.
+ */
+ public function testPropertyExistsAfterSet(): void {
+ $odf = new Odf($this->path . 'input/CustomPropertyExistsTest.odt');
+
+ // Verify property exists before modifying.
+ $this->assertTrue($odf->customPropertyExists('Author'));
+
+ // Modify the property.
+ $odf->setCustomProperty('Author', 'John Doe');
+
+ // Verify property still exists after modifying.
+ $this->assertTrue($odf->customPropertyExists('Author'));
+ }
+
+ /**
+ * Test checking properties with case sensitivity.
+ */
+ public function testCaseSensitiveProperties(): void {
+ $odf = new Odf($this->path . 'input/CustomPropertyExistsTest.odt');
+
+ // Test case sensitivity.
+ $this->assertTrue($odf->customPropertyExists('Author'));
+ $this->assertFalse($odf->customPropertyExists('author'));
+ $this->assertFalse($odf->customPropertyExists('AUTHOR'));
+ }
+}
\ No newline at end of file
diff --git a/vendor/sboden/odtphp/tests/src/EdgeCaseTest.php b/vendor/sboden/odtphp/tests/src/EdgeCaseTest.php
new file mode 100755
index 0000000000..ec09f0c887
--- /dev/null
+++ b/vendor/sboden/odtphp/tests/src/EdgeCaseTest.php
@@ -0,0 +1,177 @@
+odfPhpZipConfig();
+ $gold_dir = 'gold_phpzip';
+ $type_name = 'PhpZip';
+ }
+ else {
+ $config = $this->odfPclZipConfig();
+ $gold_dir = 'gold_pclzip';
+ $type_name = 'PclZip';
+ }
+
+ switch ($testName) {
+ case 'LargeVariableSubstitution':
+ $input_file = __DIR__ . '/files/input/LargeVariableTest.odt';
+ $output_name = __DIR__ . '/files/output/LargeVariableTest' . $type_name . 'Output.odt';
+ $gold_file = __DIR__ . '/files/' . $gold_dir . '/LargeVariableTestGold.odt';
+
+ $odf = new Odf($input_file, $config);
+
+ // Generate a large text block
+ $largeText = str_repeat("This is a large text block with many repetitions. ", 1000);
+
+ $odf->setVars('large_content', $largeText, true, 'UTF-8');
+
+ $odf->saveToDisk($output_name);
+
+ $this->assertTrue($this->odtFilesAreIdentical($output_name, $gold_file));
+
+ unlink($output_name);
+ break;
+
+ case 'ComplexSegmentMerging':
+ $input_file = __DIR__ . '/files/input/NestedSegmentTest.odt';
+ $output_name = __DIR__ . '/files/output/NestedSegmentTest' . $type_name . 'Output.odt';
+ $gold_file = __DIR__ . '/files/' . $gold_dir . '/NestedSegmentTestGold.odt';
+
+ $odf = new Odf($input_file, $config);
+
+ // Create a nested data structure
+ $departments = [
+ [
+ 'name' => 'Engineering',
+ 'employees' => [
+ ['name' => 'Alice', 'role' => 'Senior Developer'],
+ ['name' => 'Bob', 'role' => 'Junior Developer']
+ ]
+ ],
+ [
+ 'name' => 'Marketing',
+ 'employees' => [
+ ['name' => 'Charlie', 'role' => 'Marketing Manager'],
+ ['name' => 'David', 'role' => 'Content Strategist']
+ ]
+ ]
+ ];
+
+ $departmentSegment = $odf->setSegment('departments');
+
+ foreach ($departments as $department) {
+ $departmentSegment->setVars('department_name', $department['name']);
+
+ $employeeSegment = $departmentSegment->setSegment('employees');
+
+ foreach ($department['employees'] as $employee) {
+ $employeeSegment->setVars('employee_name', $employee['name']);
+ $employeeSegment->setVars('employee_role', $employee['role']);
+ $employeeSegment->merge();
+ }
+ $departmentSegment->merge();
+ }
+ $odf->mergeSegment($departmentSegment);
+ $odf->saveToDisk($output_name);
+
+ $this->assertTrue($this->odtFilesAreIdentical($output_name, $gold_file));
+
+ unlink($output_name);
+ break;
+
+ case 'SpecialCharacterEncoding':
+ $input_file = __DIR__ . '/files/input/EncodingTest.odt';
+ $output_name = __DIR__ . '/files/output/EncodingTest' . $type_name . 'Output.odt';
+ $gold_file = __DIR__ . '/files/' . $gold_dir . '/EncodingTestGold.odt';
+
+ $specialCharText = "Special characters: éèà€ñ¿ § ® © µ";
+
+ $odf = new Odf($input_file, $config);
+
+ $odf->setVars('special_chars', $specialCharText, true, 'UTF-8');
+
+ $odf->saveToDisk($output_name);
+
+ $this->assertTrue($this->odtFilesAreIdentical($output_name, $gold_file));
+
+ unlink($output_name);
+ break;
+
+ case 'AdvancedImageInsertion':
+ $input_file = __DIR__ . '/files/input/ImageTest.odt';
+ $output_name = __DIR__ . '/files/output/AdvancedImageTest' . $type_name . 'Output.odt';
+ $gold_file = __DIR__ . '/files/' . $gold_dir . '/AdvancedImageTestGold.odt';
+
+ $odf = new Odf($input_file, $config);
+
+ // Test different image types and sizes
+ $images = [
+ 'small_png' => __DIR__ . '/files/images/voronoi_sphere.png',
+ 'large_jpg' => __DIR__ . '/files/images/test.jpg',
+ 'transparent_gif' => __DIR__ . '/files/images/circle_transparent.gif'
+ ];
+
+ foreach ($images as $key => $imagePath) {
+ $odf->setImage($key, $imagePath);
+ }
+
+ $odf->saveToDisk($output_name);
+
+ $this->assertTrue($this->odtFilesAreIdentical($output_name, $gold_file));
+
+ unlink($output_name);
+ break;
+ }
+ }
+
+ public function testLargeVariableSubstitutionPclZip(): void {
+ $this->edgeCase(self::PCLZIP_TYPE, 'LargeVariableSubstitution');
+ }
+
+ public function testLargeVariableSubstitutionPhpZip(): void {
+ $this->edgeCase(self::PHPZIP_TYPE, 'LargeVariableSubstitution');
+ }
+
+ public function testComplexSegmentMergingPclZip(): void {
+ $this->edgeCase(self::PCLZIP_TYPE, 'ComplexSegmentMerging');
+ }
+
+ public function testComplexSegmentMergingPhpZip(): void {
+ $this->edgeCase(self::PHPZIP_TYPE, 'ComplexSegmentMerging');
+ }
+
+ public function testSpecialCharacterEncodingPclZip(): void {
+ $this->edgeCase(self::PCLZIP_TYPE, 'SpecialCharacterEncoding');
+ }
+
+ public function testSpecialCharacterEncodingPhpZip(): void {
+ $this->edgeCase(self::PHPZIP_TYPE, 'SpecialCharacterEncoding');
+ }
+
+ public function testAdvancedImageInsertionPclZip(): void {
+ $this->edgeCase(self::PCLZIP_TYPE, 'AdvancedImageInsertion');
+ }
+
+ public function testAdvancedImageInsertionPhpZip(): void {
+ $this->edgeCase(self::PHPZIP_TYPE, 'AdvancedImageInsertion');
+ }
+
+ /**
+ * Test handling of invalid template files.
+ */
+ public function testInvalidTemplateHandling(): void {
+ $this->expectException(\Odtphp\Exceptions\OdfException::class);
+ $this->expectExceptionMessage("File '" . __DIR__ . "/files/input/NonexistentTemplate.odt' does not exist");
+ new Odf(__DIR__ . '/files/input/NonexistentTemplate.odt');
+ }
+}
diff --git a/vendor/sboden/odtphp/tests/src/ImageTest.php b/vendor/sboden/odtphp/tests/src/ImageTest.php
new file mode 100755
index 0000000000..fde1ce8292
--- /dev/null
+++ b/vendor/sboden/odtphp/tests/src/ImageTest.php
@@ -0,0 +1,124 @@
+odfPclZipConfig();
+ $odf = new Odf(__DIR__ . "/files/input/ImageTest.odt", $config);
+ $odf->setImage('test_image', __DIR__ . "/files/images/voronoi_sphere.png");
+ $output_name = __DIR__ . "/files/output/ImageTestOutput.odt";
+ $odf->saveToDisk($output_name);
+ $this->assertFileExists($output_name);
+ $this->assertTrue($this->odtFilesAreIdentical($output_name, __DIR__ . "/files/gold_phpzip/ImageTestGold.odt"));
+ unlink($output_name);
+ }
+
+ public function testImageResizing(): void {
+ $config = $this->odfPclZipConfig();
+ $odf = new Odf(__DIR__ . "/files/input/ImageTest.odt", $config);
+ // This is actually 100cmx100cm of the original image.
+ $odf->setImage('test_image', __DIR__ . "/files/images/test.jpg", -1, 100, 100);
+ $output_name = __DIR__ . "/files/output/ImageTestResizedOutput.odt";
+ $odf->saveToDisk($output_name);
+ $this->assertFileExists($output_name);
+ $this->assertTrue($this->odtFilesAreIdentical($output_name, __DIR__ . "/files/gold_phpzip/ImageTestResizedGold.odt"));
+ unlink($output_name);
+ }
+
+ public function testImageResizingMm(): void {
+ $config = $this->odfPclZipConfig();
+ $odf = new Odf(__DIR__ . "/files/input/ImageTest.odt", $config);
+ $odf->setImageMm('test_image', __DIR__ . "/files/images/test.jpg", -1);
+ // 50mm x 50mm of the original image.
+ $odf->setImageMm('small_png', __DIR__ . "/files/images/test.jpg", -1, 50, 50);
+ // 100mm x 100mm of the original image.
+ $odf->setImageMm('large_jpg', __DIR__ . "/files/images/test.jpg", -1, 100, 100);
+ $output_name = __DIR__ . "/files/output/ImageTestResizedMmOutput.odt";
+ $odf->saveToDisk($output_name);
+ $this->assertFileExists($output_name);
+ $this->assertTrue($this->odtFilesAreIdentical($output_name, __DIR__ . "/files/gold_phpzip/ImageTestResizedMmGold.odt"));
+ unlink($output_name);
+ }
+
+ public function testImageResizingPixel(): void {
+ $config = $this->odfPclZipConfig();
+ $odf = new Odf(__DIR__ . "/files/input/ImageTest.odt", $config);
+ $odf->setImagePixel('test_image', __DIR__ . "/files/images/test.jpg", -1);
+ // About 50mm x 50mm of the original image in pixels.
+ $odf->setImagePixel('small_png', __DIR__ . "/files/images/test.jpg", -1, 188.97637795, 188.97637795);
+ // About 100mm x 100mm of the original image in pixels.
+ $odf->setImagePixel('large_jpg', __DIR__ . "/files/images/test.jpg", -1, 377.95275591, 377.95275591);
+ $output_name = __DIR__ . "/files/output/ImageTestResizedPixelOutput.odt";
+ $odf->saveToDisk($output_name);
+ $this->assertFileExists($output_name);
+ $this->assertTrue($this->odtFilesAreIdentical($output_name, __DIR__ . "/files/gold_phpzip/ImageTestResizedPixelGold.odt"));
+ unlink($output_name);
+ }
+
+ public function testImageResizingMmOffsetIgnored(): void {
+ $config = $this->odfPclZipConfig();
+ $odf = new Odf(__DIR__ . "/files/input/ImageTest.odt", $config);
+ $odf->setImageMm('test_image', __DIR__ . "/files/images/test.jpg", -1);
+ // 50mm x 50mm of the original image.
+ $odf->setImageMm('small_png', __DIR__ . "/files/images/test.jpg", -1, 50, 50, 50, 100);
+ // 100mm x 100mm of the original image.
+ $odf->setImageMm('large_jpg', __DIR__ . "/files/images/test.jpg", -1, 100, 100, 80, 70);
+ $output_name = __DIR__ . "/files/output/ImageTestResizedMmOffsetIgnoredOutput.odt";
+ $odf->saveToDisk($output_name);
+ $this->assertFileExists($output_name);
+ $this->assertTrue($this->odtFilesAreIdentical($output_name, __DIR__ . "/files/gold_phpzip/ImageTestResizedMmOffsetIgnoredGold.odt"));
+ unlink($output_name);
+ }
+
+ public function testImageResizingPixelOffsetIgnored(): void {
+ $config = $this->odfPclZipConfig();
+ $odf = new Odf(__DIR__ . "/files/input/ImageTest.odt", $config);
+ $odf->setImagePixel('test_image', __DIR__ . "/files/images/test.jpg", -1);
+ // About 50mm x 50mm of the original image in pixels.
+ $odf->setImagePixel('small_png', __DIR__ . "/files/images/test.jpg", -1, 188.97637795, 188.97637795, 50, 100);
+ // About 100mm x 100mm of the original image in pixels.
+ $odf->setImagePixel('large_jpg', __DIR__ . "/files/images/test.jpg", -1, 377.95275591, 377.95275591, 80, 70);
+ $output_name = __DIR__ . "/files/output/ImageTestResizedPixelOffsetIgnoredOutput.odt";
+ $odf->saveToDisk($output_name);
+ $this->assertFileExists($output_name);
+ $this->assertTrue($this->odtFilesAreIdentical($output_name, __DIR__ . "/files/gold_phpzip/ImageTestResizedPixelOffsetIgnoredGold.odt"));
+ unlink($output_name);
+ }
+
+ public function testImageResizingMmOffset(): void {
+ $config = $this->odfPclZipConfig();
+ $odf = new Odf(__DIR__ . "/files/input/ImageTest.odt", $config);
+ $odf->setImageMm('test_image', __DIR__ . "/files/images/test.jpg", 1, 100, 100, 80, 70);
+ $output_name = __DIR__ . "/files/output/ImageTestResizedMmOffsetOutput.odt";
+ $odf->saveToDisk($output_name);
+ $this->assertFileExists($output_name);
+ $this->assertTrue($this->odtFilesAreIdentical($output_name, __DIR__ . "/files/gold_phpzip/ImageTestResizedMmOffsetGold.odt"));
+ unlink($output_name);
+ }
+
+ public function testImageResizingPixelOffset(): void {
+ $config = $this->odfPclZipConfig();
+ $odf = new Odf(__DIR__ . "/files/input/ImageTest.odt", $config);
+ $odf->setImagePixel('test_image', __DIR__ . "/files/images/test.jpg", 1, 377.95275591, 377.95275591, 80, 70);
+ $output_name = __DIR__ . "/files/output/ImageTestResizedPixelOffsetOutput.odt";
+ $odf->saveToDisk($output_name);
+ $this->assertFileExists($output_name);
+ $this->assertTrue($this->odtFilesAreIdentical($output_name, __DIR__ . "/files/gold_phpzip/ImageTestResizedPixelOffsetGold.odt"));
+ unlink($output_name);
+ }
+
+
+ public function testInvalidImagePath(): void {
+ $config = $this->odfPclZipConfig();
+ $odf = new Odf(__DIR__ . "/files/input/ImageTest.odt", $config);
+ $this->expectException(OdfException::class);
+ $odf->setImage('test_image', __DIR__ . "/files/images/nonexistent.png");
+ }
+}
diff --git a/vendor/sboden/odtphp/tests/src/NullValueRegressionTest.php b/vendor/sboden/odtphp/tests/src/NullValueRegressionTest.php
new file mode 100644
index 0000000000..cc3ff73e17
--- /dev/null
+++ b/vendor/sboden/odtphp/tests/src/NullValueRegressionTest.php
@@ -0,0 +1,235 @@
+odfPhpZipConfig();
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ // Test the current behavior (after the fix)
+ $reflection = new ReflectionClass($odf);
+ $method = $reflection->getMethod('recursiveHtmlspecialchars');
+ $method->setAccessible(true);
+
+ // These calls should not throw any errors after the fix
+ $result = $method->invoke($odf, null);
+ $this->assertEquals('', $result);
+
+ $result = $method->invoke($odf, [null, 'test', null]);
+ $this->assertEquals(['', 'test', ''], $result);
+ }
+
+ /**
+ * Test that demonstrates the integration behavior after the fix.
+ * This shows that setVars and setVariable methods work correctly with null values.
+ */
+ public function testIntegrationBehaviorAfterFix(): void {
+ $config = $this->odfPhpZipConfig();
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ // Test that setVars works with null values and encoding enabled
+ $odf->setVars('titre', null, true, 'UTF-8');
+
+ // Test that setVariable works with null values
+ $odf->setVariable('message', null, true);
+
+ // Test that both variables were set (as empty strings after encoding)
+ $this->assertTrue($odf->variableExists('titre'));
+ $this->assertTrue($odf->variableExists('message'));
+ }
+
+ /**
+ * Test that demonstrates the behavior with various edge cases that were problematic before the fix.
+ */
+ public function testEdgeCaseHandlingAfterFix(): void {
+ $config = $this->odfPhpZipConfig();
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ $reflection = new ReflectionClass($odf);
+ $method = $reflection->getMethod('recursiveHtmlspecialchars');
+ $method->setAccessible(true);
+
+ // Test various edge cases that would have caused issues before the fix
+ $edgeCases = [
+ null,
+ [null],
+ [null, null, null],
+ ['test', null, 'value'],
+ [null, 'test', null],
+ [[null], 'test'],
+ ['test', [null]],
+ [[null, 'test'], null],
+ ];
+
+ foreach ($edgeCases as $case) {
+ $result = $method->invoke($odf, $case);
+ // Just verify it doesn't throw an error and returns something
+ $this->assertNotNull($result, "Failed for case: " . var_export($case, true));
+ }
+ }
+
+ /**
+ * Test that demonstrates the behavior with complex nested structures.
+ */
+ public function testComplexNestedStructuresAfterFix(): void {
+ $config = $this->odfPhpZipConfig();
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ $reflection = new ReflectionClass($odf);
+ $method = $reflection->getMethod('recursiveHtmlspecialchars');
+ $method->setAccessible(true);
+
+ // Test with complex nested structure containing nulls
+ $complexStructure = [
+ 'users' => [
+ [
+ 'name' => 'John',
+ 'email' => null,
+ 'preferences' => [
+ 'theme' => 'dark',
+ 'notifications' => null
+ ]
+ ],
+ [
+ 'name' => null,
+ 'email' => 'jane@example.com',
+ 'preferences' => null
+ ],
+ null, // This would have been problematic before the fix
+ [
+ 'name' => 'Bob',
+ 'email' => 'bob@example.com',
+ 'preferences' => [
+ 'theme' => null,
+ 'notifications' => true
+ ]
+ ]
+ ],
+ 'settings' => null,
+ 'metadata' => [
+ 'version' => '1.0',
+ 'author' => null,
+ 'tags' => [null, 'important', null]
+ ]
+ ];
+
+ $result = $method->invoke($odf, $complexStructure);
+
+ // Verify the structure is processed correctly
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('users', $result);
+ $this->assertArrayHasKey('settings', $result);
+ $this->assertArrayHasKey('metadata', $result);
+
+ // Verify null values are converted to empty strings
+ $this->assertEquals('', $result['settings']);
+ $this->assertEquals('', $result['metadata']['author']);
+ }
+
+ /**
+ * Test that demonstrates the behavior with different data types.
+ */
+ public function testDataTypeHandlingAfterFix(): void {
+ $config = $this->odfPhpZipConfig();
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ $reflection = new ReflectionClass($odf);
+ $method = $reflection->getMethod('recursiveHtmlspecialchars');
+ $method->setAccessible(true);
+
+ // Test with different data types including null
+ $mixedData = [
+ 'string' => 'test',
+ 'null' => null,
+ 'integer' => 123,
+ 'float' => 123.45,
+ 'boolean_true' => true,
+ 'boolean_false' => false,
+ 'empty_string' => '',
+ 'zero' => 0,
+ 'array_with_nulls' => [null, 'test', null],
+ 'nested_null' => [['deep' => null]]
+ ];
+
+ $result = $method->invoke($odf, $mixedData);
+
+ // Verify all values are processed without errors
+ $this->assertEquals('test', $result['string']);
+ $this->assertEquals('', $result['null']);
+ $this->assertEquals('123', $result['integer']);
+ $this->assertEquals('123.45', $result['float']);
+ $this->assertEquals('1', $result['boolean_true']);
+ $this->assertEquals('', $result['boolean_false']);
+ $this->assertEquals('', $result['empty_string']);
+ $this->assertEquals('0', $result['zero']);
+ $this->assertEquals(['', 'test', ''], $result['array_with_nulls']);
+ $this->assertEquals([['deep' => '']], $result['nested_null']);
+ }
+
+ /**
+ * Test that demonstrates the behavior with HTML special characters and null values mixed.
+ */
+ public function testHtmlSpecialCharsWithNullsAfterFix(): void {
+ $config = $this->odfPhpZipConfig();
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ $reflection = new ReflectionClass($odf);
+ $method = $reflection->getMethod('recursiveHtmlspecialchars');
+ $method->setAccessible(true);
+
+ // Test with HTML special characters mixed with null values
+ $htmlWithNulls = [
+ 'safe_text' => 'Normal text',
+ 'null_value' => null,
+ 'html_script' => '',
+ 'html_entities' => '<test>',
+ 'mixed_array' => [
+ 'safe' => 'Safe content',
+ 'null' => null,
+ 'html' => 'Content
',
+ 'empty' => ''
+ ],
+ 'nested_html' => [
+ 'level1' => [
+ 'level2' => [
+ 'content' => 'Paragraph
',
+ 'null_content' => null
+ ]
+ ]
+ ]
+ ];
+
+ $result = $method->invoke($odf, $htmlWithNulls);
+
+ // Verify HTML is properly encoded
+ $this->assertEquals('<script>alert("test")</script>', $result['html_script']);
+ $this->assertEquals('<test>', $result['html_entities']);
+ $this->assertEquals('<div>Content</div>', $result['mixed_array']['html']);
+ $this->assertEquals('<p>Paragraph</p>', $result['nested_html']['level1']['level2']['content']);
+
+ // Verify null values are converted to empty strings
+ $this->assertEquals('', $result['null_value']);
+ $this->assertEquals('', $result['mixed_array']['null']);
+ $this->assertEquals('', $result['nested_html']['level1']['level2']['null_content']);
+ }
+}
\ No newline at end of file
diff --git a/vendor/sboden/odtphp/tests/src/OdtTestCase.php b/vendor/sboden/odtphp/tests/src/OdtTestCase.php
new file mode 100755
index 0000000000..d6295e36ae
--- /dev/null
+++ b/vendor/sboden/odtphp/tests/src/OdtTestCase.php
@@ -0,0 +1,100 @@
+ \Odtphp\Zip\PhpZipProxy::class,
+ 'DELIMITER_LEFT' => '{',
+ 'DELIMITER_RIGHT' => '}',
+ ];
+ }
+
+ protected function odfPclZipConfig() {
+ return [
+ 'ZIP_PROXY' => \Odtphp\Zip\PclZipProxy::class,
+ 'DELIMITER_LEFT' => '{',
+ 'DELIMITER_RIGHT' => '}',
+ ];
+ }
+
+
+ protected function extractText($filename) {
+ $file_parts = explode('.', $filename);
+ $ext = end($file_parts);
+ if ($ext == 'docx') {
+ $dataFile = "word/document.xml";
+ }
+ else {
+ $dataFile = "content.xml";
+ }
+ $zip = new \ZipArchive;
+ if (TRUE === $zip->open($filename)) {
+ if (($index = $zip->locateName($dataFile)) !== FALSE) {
+ // Index found! Now read it to a string
+ $text = $zip->getFromIndex($index);
+ // Load XML from a string
+ // Ignore errors and warnings
+ $doc = new DOMDocument();
+ $rcode = $doc->loadXML($text,
+ LIBXML_NOENT | LIBXML_XINCLUDE | LIBXML_NOERROR | LIBXML_NOWARNING);
+ // Remove XML formatting tags and return the text
+ return strip_tags($doc->saveXML());
+ }
+ // Close the archive file
+ $zip->close();
+ }
+ // In case of failure return a message
+ return "File not found";
+ }
+
+
+ function odtFilesAreIdentical($file1, $file2)
+ {
+ // Ensure both files exist
+ if (!file_exists($file1)) {
+ throw new Exception("File $file1 does not exist.");
+ }
+
+ if (!file_exists($file2)) {
+ throw new Exception("File $file2 does not exist.");
+ }
+
+ // Open the first .odt file as a ZIP archive and extract content.xml
+ $zip1 = new \ZipArchive;
+ $zip2 = new \ZipArchive;
+
+ if ($zip1->open($file1) !== true) {
+ throw new Exception("Unable to open file: " . $file1);
+ }
+
+ if ($zip2->open($file2) !== true) {
+ $zip1->close();
+ throw new Exception("Unable to open file: " . $file2);
+ }
+
+ // Read the content.xml files from both .odt files
+ $content1 = $zip1->getFromName("content.xml");
+ $content2 = $zip2->getFromName("content.xml");
+
+ // Close the archives
+ $zip1->close();
+ $zip2->close();
+
+ // Compare the contents of content.xml
+ return $content1 === $content2;
+ }
+}
diff --git a/vendor/sboden/odtphp/tests/src/RecursiveHtmlspecialcharsTest.php b/vendor/sboden/odtphp/tests/src/RecursiveHtmlspecialcharsTest.php
new file mode 100644
index 0000000000..cc0f69ab04
--- /dev/null
+++ b/vendor/sboden/odtphp/tests/src/RecursiveHtmlspecialcharsTest.php
@@ -0,0 +1,315 @@
+odfPhpZipConfig();
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ // Use reflection to access the protected method
+ $reflection = new ReflectionClass($odf);
+ $method = $reflection->getMethod('recursiveHtmlspecialchars');
+ $method->setAccessible(true);
+
+ // Test with null value - should not throw an error and should return empty string
+ $result = $method->invoke($odf, null);
+ $this->assertEquals('', $result);
+ }
+
+ /**
+ * Test that recursiveHtmlspecialchars handles various null-like values correctly.
+ */
+ public function testRecursiveHtmlspecialcharsWithNullLikeValues(): void {
+ $config = $this->odfPhpZipConfig();
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ $reflection = new ReflectionClass($odf);
+ $method = $reflection->getMethod('recursiveHtmlspecialchars');
+ $method->setAccessible(true);
+
+ // Test with various null-like values (avoid duplicate keys)
+ $testCases = [
+ null => '',
+ '' => '',
+ '0' => '0', // string key only
+ true => '1',
+ ];
+
+ foreach ($testCases as $input => $expected) {
+ $result = $method->invoke($odf, $input);
+ $this->assertEquals($expected, $result, "Failed for input: " . var_export($input, true));
+ }
+ // Add explicit tests for integer 0 and boolean false
+ $result = $method->invoke($odf, 0);
+ $this->assertEquals('0', $result, "Failed for input: 0 (int)");
+ $result = $method->invoke($odf, false);
+ $this->assertEquals('', $result, "Failed for input: false (bool)");
+ }
+
+ /**
+ * Test that recursiveHtmlspecialchars handles arrays with null values correctly.
+ */
+ public function testRecursiveHtmlspecialcharsWithArrayContainingNull(): void {
+ $config = $this->odfPhpZipConfig();
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ $reflection = new ReflectionClass($odf);
+ $method = $reflection->getMethod('recursiveHtmlspecialchars');
+ $method->setAccessible(true);
+
+ // Test with array containing null values
+ $input = ['test', null, 'value', null];
+ $expected = ['test', '', 'value', ''];
+
+ $result = $method->invoke($odf, $input);
+ $this->assertEquals($expected, $result);
+ }
+
+ /**
+ * Test that recursiveHtmlspecialchars handles nested arrays with null values correctly.
+ */
+ public function testRecursiveHtmlspecialcharsWithNestedArrays(): void {
+ $config = $this->odfPhpZipConfig();
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ $reflection = new ReflectionClass($odf);
+ $method = $reflection->getMethod('recursiveHtmlspecialchars');
+ $method->setAccessible(true);
+
+ // Test with nested arrays containing null values
+ $input = [
+ 'level1' => [
+ 'level2' => [
+ 'value1' => 'test',
+ 'value2' => null,
+ 'value3' => 'another'
+ ],
+ 'level2b' => null
+ ],
+ 'top_level' => null
+ ];
+
+ $expected = [
+ 'level1' => [
+ 'level2' => [
+ 'value1' => 'test',
+ 'value2' => '',
+ 'value3' => 'another'
+ ],
+ 'level2b' => ''
+ ],
+ 'top_level' => ''
+ ];
+
+ $result = $method->invoke($odf, $input);
+ $this->assertEquals($expected, $result);
+ }
+
+ /**
+ * Test that recursiveHtmlspecialchars properly encodes HTML special characters.
+ */
+ public function testRecursiveHtmlspecialcharsWithSpecialCharacters(): void {
+ $config = $this->odfPhpZipConfig();
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ $reflection = new ReflectionClass($odf);
+ $method = $reflection->getMethod('recursiveHtmlspecialchars');
+ $method->setAccessible(true);
+
+ // Test with HTML special characters
+ $input = '';
+ $expected = '<script>alert("test")</script>';
+
+ $result = $method->invoke($odf, $input);
+ $this->assertEquals($expected, $result);
+ }
+
+ /**
+ * Test that setVars method handles null values correctly when encoding is enabled.
+ */
+ public function testSetVarsWithNullValueAndEncoding(): void {
+ $config = $this->odfPhpZipConfig();
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ // This should not throw an error even with null value and encoding enabled
+ $odf->setVars('titre', null, true, 'UTF-8');
+
+ // Verify the variable was set (should be empty string after encoding)
+ $this->assertTrue($odf->variableExists('titre'));
+ }
+
+ /**
+ * Test that setVars method handles null values correctly when encoding is disabled.
+ * Note: This test demonstrates that the setVars method needs additional null handling.
+ */
+ public function testSetVarsWithNullValueAndNoEncoding(): void {
+ $config = $this->odfPhpZipConfig();
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ // This test shows that setVars needs additional null handling when encoding is disabled
+ // because str_replace() receives null as the third parameter
+ $this->expectException(\TypeError::class);
+ $odf->setVars('titre', null, false, 'UTF-8');
+ }
+
+ /**
+ * Test that setVariable method (alias for setVars) handles null values correctly.
+ */
+ public function testSetVariableWithNullValue(): void {
+ $config = $this->odfPhpZipConfig();
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ // This should not throw an error
+ $odf->setVariable('titre', null, true);
+
+ // Verify the variable was set
+ $this->assertTrue($odf->variableExists('titre'));
+ }
+
+ /**
+ * Test integration with actual ODT file processing using null values.
+ */
+ public function testIntegrationWithNullValues(): void {
+ $config = $this->odfPhpZipConfig();
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ // Set various types of values including null
+ // Only use variables that exist in the BasicTest1.odt template
+ $odf->setVars('titre', null, true, 'UTF-8');
+ $odf->setVars('message', 'Normal message', true, 'UTF-8');
+
+ $output_name = __DIR__ . '/files/output/NullValueTestOutput.odt';
+
+ // This should not throw any errors
+ $odf->saveToDisk($output_name);
+
+ // Verify the file was created successfully
+ $this->assertFileExists($output_name);
+
+ // Clean up
+ unlink($output_name);
+ }
+
+ /**
+ * Test that the fix works with both PHPZip and PclZip configurations.
+ */
+ public function testNullValueHandlingWithPclZip(): void {
+ $config = $this->odfPclZipConfig();
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ $reflection = new ReflectionClass($odf);
+ $method = $reflection->getMethod('recursiveHtmlspecialchars');
+ $method->setAccessible(true);
+
+ // Test with null value using PclZip configuration
+ $result = $method->invoke($odf, null);
+ $this->assertEquals('', $result);
+ }
+
+ /**
+ * Test edge case with mixed data types in arrays.
+ */
+ public function testMixedDataTypesInArrays(): void {
+ $config = $this->odfPhpZipConfig();
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ $reflection = new ReflectionClass($odf);
+ $method = $reflection->getMethod('recursiveHtmlspecialchars');
+ $method->setAccessible(true);
+
+ // Test with mixed data types including null
+ $input = [
+ 'string' => 'test',
+ 'null' => null,
+ 'integer' => 123,
+ 'boolean' => true,
+ 'empty_string' => '',
+ 'zero' => 0,
+ 'false' => false
+ ];
+
+ $expected = [
+ 'string' => 'test',
+ 'null' => '',
+ 'integer' => '123',
+ 'boolean' => '1',
+ 'empty_string' => '',
+ 'zero' => '0',
+ 'false' => ''
+ ];
+
+ $result = $method->invoke($odf, $input);
+ $this->assertEquals($expected, $result);
+ }
+
+ /**
+ * Test that the method handles very deep nested arrays correctly.
+ */
+ public function testDeepNestedArrays(): void {
+ $config = $this->odfPhpZipConfig();
+ $input_file = __DIR__ . '/files/input/BasicTest1.odt';
+ $odf = new Odf($input_file, $config);
+
+ $reflection = new ReflectionClass($odf);
+ $method = $reflection->getMethod('recursiveHtmlspecialchars');
+ $method->setAccessible(true);
+
+ // Create a deeply nested array
+ $input = [
+ 'level1' => [
+ 'level2' => [
+ 'level3' => [
+ 'level4' => [
+ 'level5' => [
+ 'value' => null,
+ 'text' => ''
+ ]
+ ]
+ ]
+ ]
+ ]
+ ];
+
+ $expected = [
+ 'level1' => [
+ 'level2' => [
+ 'level3' => [
+ 'level4' => [
+ 'level5' => [
+ 'value' => '',
+ 'text' => '<script>alert("test")</script>'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ];
+
+ $result = $method->invoke($odf, $input);
+ $this->assertEquals($expected, $result);
+ }
+}
\ No newline at end of file
diff --git a/vendor/sboden/odtphp/tests/src/SetCustomPropertyTest.php b/vendor/sboden/odtphp/tests/src/SetCustomPropertyTest.php
new file mode 100644
index 0000000000..9659299584
--- /dev/null
+++ b/vendor/sboden/odtphp/tests/src/SetCustomPropertyTest.php
@@ -0,0 +1,168 @@
+filename = tempnam(sys_get_temp_dir(), 'odt_test_');
+ copy($sourcePath, $this->filename);
+ $this->odf = new Odf($this->filename);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+ if (file_exists($this->filename)) {
+ unlink($this->filename);
+ }
+ }
+
+ /**
+ * Test setting a basic custom property.
+ */
+ public function testSetBasicCustomProperty(): void
+ {
+ $this->odf->setCustomProperty('Name', 'Anja');
+ $metaXml = $this->odf->getMetaXml();
+
+ $this->assertStringContainsString('Anja ', $metaXml);
+ $this->assertStringNotContainsString('Sven Boden ', $metaXml);
+ }
+
+ /**
+ * Test setting a date custom property.
+ */
+ public function testSetDateCustomProperty(): void
+ {
+ $this->odf->setCustomProperty('Creation Date', '01/11/9999');
+ $metaXml = $this->odf->getMetaXml();
+
+ $this->assertStringContainsString('01/11/9999 ', $metaXml);
+ $this->assertStringNotContainsString('2025-03-20 ', $metaXml);
+ }
+
+ /**
+ * Test setting custom property with HTML encoding.
+ */
+ public function testSetCustomPropertyWithEncoding(): void
+ {
+ $this->odf->setCustomProperty('Name', '&"\'', TRUE);
+ $metaXml = $this->odf->getMetaXml();
+
+ $this->assertStringContainsString('<test>&"'', $metaXml);
+ }
+
+ /**
+ * Test setting custom property without HTML encoding.
+ */
+ public function testSetCustomPropertyWithoutEncoding(): void
+ {
+ $this->odf->setCustomProperty('Name', '&"\'', FALSE);
+ $metaXml = $this->odf->getMetaXml();
+
+ $this->assertStringContainsString('&"\'', $metaXml);
+ }
+
+ /**
+ * Test setting custom property with invalid input.
+ */
+ public function testSetCustomPropertyWithInvalidInput(): void
+ {
+ $this->expectException(OdfException::class);
+ $this->expectExceptionMessage('Key and value must be strings');
+ $this->odf->setCustomProperty(['invalid'], 'value');
+ }
+
+ /**
+ * Test setting non-existent custom property.
+ */
+ public function testSetNonExistentCustomProperty(): void
+ {
+ $this->expectException(OdfException::class);
+ $this->expectExceptionMessage("Custom property 'NonExistent' not found in meta.xml");
+ $this->odf->setCustomProperty('NonExistent', 'value');
+ }
+
+ /**
+ * Test error handling when meta.xml extraction fails.
+ */
+ public function testMetaXmlExtractionError(): void
+ {
+ // Create a mock zip file without meta.xml
+ $badFile = tempnam(sys_get_temp_dir(), 'bad_odt_');
+ file_put_contents($badFile, 'corrupted content');
+
+ $this->expectException(OdfException::class);
+ $this->expectExceptionMessage("Error opening file '$badFile'");
+
+ // Create a new ODT instance with the bad file - this should throw the exception
+ new Odf($badFile);
+
+ unlink($badFile);
+ }
+
+ /**
+ * Compare output with gold files.
+ */
+ public function goldFileComparison($type): void
+ {
+ if ($type === self::PHPZIP_TYPE) {
+ $config = $this->odfPhpZipConfig();
+ $gold_dir = 'gold_phpzip';
+ $type_name = 'PhpZip';
+ }
+ else {
+ $config = $this->odfPclZipConfig();
+ $gold_dir = 'gold_pclzip';
+ $type_name = 'PclZip';
+ }
+
+ $odf = new Odf(__DIR__ . '/files/input/CustomPropertiesTest.odt', $config);
+ $odf->setCustomProperty('Name', "Snow White");
+ $odf->setCustomProperty('Creation Date', '2100-01-01');
+
+ $output_name = __DIR__ . "/files/output/SetCustomPropertyTest" . $type_name . "Output.odt";
+ $odf->saveToDisk($output_name);
+
+ $this->assertTrue($this->odtFilesAreIdentical($output_name, __DIR__ . "/files/$gold_dir/SetCustomPropertyTestGold.odt"));
+ unlink($output_name);
+ }
+
+ /**
+ * Test with PclZip.
+ */
+ public function testSetCustomPropertyPclZip(): void
+ {
+ $this->goldFileComparison(self::PCLZIP_TYPE);
+ }
+
+ /**
+ * Test with PhpZip.
+ */
+ public function testSetCustomPropertyPhpZip(): void
+ {
+ $this->goldFileComparison(self::PHPZIP_TYPE);
+ }
+}
diff --git a/vendor/sboden/odtphp/tests/src/VariableTest.php b/vendor/sboden/odtphp/tests/src/VariableTest.php
new file mode 100755
index 0000000000..ffcbd9959e
--- /dev/null
+++ b/vendor/sboden/odtphp/tests/src/VariableTest.php
@@ -0,0 +1,46 @@
+odfPhpZipConfig();
+ $odf = new Odf(__DIR__ . "/files/input/VariableTest.odt", $config);
+
+ $this->assertTrue($odf->variableExists('test_var'));
+ $this->assertFalse($odf->variableExists('nonexistent_var'));
+ }
+
+ public function testSpecialCharacterEncoding(): void {
+ $config = $this->odfPhpZipConfig();
+ $odf = new Odf(__DIR__ . "/files/input/VariableTest.odt", $config);
+
+ $specialChars = "<>&'\"";
+ $odf->setVars('test_var', $specialChars, true);
+
+ $output_name = __DIR__ . "/files/output/VariableTestOutputSpecialChars.odt";
+ $odf->saveToDisk($output_name);
+
+ $this->assertTrue($this->odtFilesAreIdentical($output_name, __DIR__ . "/files/gold_phpzip/VariableTestSpecialCharsGold.odt"));
+ unlink($output_name);
+ }
+
+ public function testMultilineText(): void {
+ $config = $this->odfPhpZipConfig();
+ $odf = new Odf(__DIR__ . "/files/input/VariableTest.odt", $config);
+
+ $multiline = "Line 1\nLine 2\nLine 3";
+ $odf->setVars('test_var', $multiline, true);
+
+ $output_name = __DIR__ . "/files/output/VariableTestOutputMultiline.odt";
+ $odf->saveToDisk($output_name);
+
+ $this->assertTrue($this->odtFilesAreIdentical($output_name, __DIR__ . "/files/gold_phpzip/VariableTestMultilineGold.odt"));
+ unlink($output_name);
+ }
+}
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_pclzip/AdvancedImageTestGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/AdvancedImageTestGold.odt
new file mode 100755
index 0000000000..b0045f69a8
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/AdvancedImageTestGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_pclzip/BasicTest1Gold.odt b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/BasicTest1Gold.odt
new file mode 100755
index 0000000000..9cda417f2b
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/BasicTest1Gold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_pclzip/BasicTest2Gold.odt b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/BasicTest2Gold.odt
new file mode 100755
index 0000000000..af50d9c969
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/BasicTest2Gold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_pclzip/ConfigTestGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/ConfigTestGold.odt
new file mode 100755
index 0000000000..5fac55e7c4
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/ConfigTestGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_pclzip/EncodingTestGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/EncodingTestGold.odt
new file mode 100755
index 0000000000..7243f7a5eb
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/EncodingTestGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_pclzip/ImageTestGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/ImageTestGold.odt
new file mode 100755
index 0000000000..ad104898b7
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/ImageTestGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_pclzip/ImageTestOutputGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/ImageTestOutputGold.odt
new file mode 100755
index 0000000000..ad104898b7
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/ImageTestOutputGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_pclzip/ImageTestResizedGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/ImageTestResizedGold.odt
new file mode 100755
index 0000000000..1e3360f8fa
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/ImageTestResizedGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_pclzip/ImageTestResizedOutputGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/ImageTestResizedOutputGold.odt
new file mode 100644
index 0000000000..52aaaf2b79
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/ImageTestResizedOutputGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_pclzip/LargeVariableTestGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/LargeVariableTestGold.odt
new file mode 100755
index 0000000000..86562d47fa
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/LargeVariableTestGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_pclzip/NestedSegmentTestGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/NestedSegmentTestGold.odt
new file mode 100755
index 0000000000..a87b0072be
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/NestedSegmentTestGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_pclzip/SetCustomPropertyTestGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/SetCustomPropertyTestGold.odt
new file mode 100644
index 0000000000..0f4f54e842
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/SetCustomPropertyTestGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_pclzip/VariableTestGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/VariableTestGold.odt
new file mode 100755
index 0000000000..5100ff7ef8
--- /dev/null
+++ b/vendor/sboden/odtphp/tests/src/files/gold_pclzip/VariableTestGold.odt
@@ -0,0 +1,8 @@
+
+
+
+
+ <>&'"
+
+
+
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/AdvancedImageTestGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/AdvancedImageTestGold.odt
new file mode 100755
index 0000000000..b0045f69a8
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/AdvancedImageTestGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/BasicTest1Gold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/BasicTest1Gold.odt
new file mode 100755
index 0000000000..b8645415bd
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/BasicTest1Gold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/BasicTest2Gold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/BasicTest2Gold.odt
new file mode 100755
index 0000000000..af50d9c969
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/BasicTest2Gold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ConfigTestGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ConfigTestGold.odt
new file mode 100755
index 0000000000..5fac55e7c4
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ConfigTestGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/EncodingTestGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/EncodingTestGold.odt
new file mode 100755
index 0000000000..5a7b254df0
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/EncodingTestGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/EncodingTestPhpGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/EncodingTestPhpGold.odt
new file mode 100755
index 0000000000..7243f7a5eb
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/EncodingTestPhpGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestGold.odt
new file mode 100755
index 0000000000..bbd94b6f1d
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedGold.odt
new file mode 100755
index 0000000000..c91340a197
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedMmGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedMmGold.odt
new file mode 100644
index 0000000000..8386e43435
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedMmGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedMmOffsetGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedMmOffsetGold.odt
new file mode 100644
index 0000000000..1e38a38eec
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedMmOffsetGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedMmOffsetIgnoredGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedMmOffsetIgnoredGold.odt
new file mode 100644
index 0000000000..22bfae3d56
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedMmOffsetIgnoredGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedPixelGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedPixelGold.odt
new file mode 100644
index 0000000000..effb5940ba
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedPixelGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedPixelOffsetGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedPixelOffsetGold.odt
new file mode 100644
index 0000000000..1302c56382
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedPixelOffsetGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedPixelOffsetIgnoredGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedPixelOffsetIgnoredGold.odt
new file mode 100644
index 0000000000..22bfae3d56
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/ImageTestResizedPixelOffsetIgnoredGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/LargeVariableTestGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/LargeVariableTestGold.odt
new file mode 100755
index 0000000000..86562d47fa
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/LargeVariableTestGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/NestedSegmentTestGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/NestedSegmentTestGold.odt
new file mode 100755
index 0000000000..a87b0072be
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/NestedSegmentTestGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/SetCustomPropertyTestGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/SetCustomPropertyTestGold.odt
new file mode 100644
index 0000000000..a1dd562e02
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/SetCustomPropertyTestGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/VariableTestGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/VariableTestGold.odt
new file mode 100755
index 0000000000..5100ff7ef8
--- /dev/null
+++ b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/VariableTestGold.odt
@@ -0,0 +1,8 @@
+
+
+
+
+ <>&'"
+
+
+
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/VariableTestMultilineGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/VariableTestMultilineGold.odt
new file mode 100755
index 0000000000..adabf5af11
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/VariableTestMultilineGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/gold_phpzip/VariableTestSpecialCharsGold.odt b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/VariableTestSpecialCharsGold.odt
new file mode 100755
index 0000000000..0b65492a7f
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/gold_phpzip/VariableTestSpecialCharsGold.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/images/anaska.gif b/vendor/sboden/odtphp/tests/src/files/images/anaska.gif
new file mode 100755
index 0000000000..1ea3f0e68a
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/images/anaska.gif differ
diff --git a/vendor/sboden/odtphp/tests/src/files/images/anaska.jpg b/vendor/sboden/odtphp/tests/src/files/images/anaska.jpg
new file mode 100755
index 0000000000..b02b4c4eff
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/images/anaska.jpg differ
diff --git a/vendor/sboden/odtphp/tests/src/files/images/circle_transparent.gif b/vendor/sboden/odtphp/tests/src/files/images/circle_transparent.gif
new file mode 100755
index 0000000000..9a8eeed844
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/images/circle_transparent.gif differ
diff --git a/vendor/sboden/odtphp/tests/src/files/images/mysql.gif b/vendor/sboden/odtphp/tests/src/files/images/mysql.gif
new file mode 100755
index 0000000000..6520883e1d
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/images/mysql.gif differ
diff --git a/vendor/sboden/odtphp/tests/src/files/images/overschot.jpg b/vendor/sboden/odtphp/tests/src/files/images/overschot.jpg
new file mode 100755
index 0000000000..e5bb4ede0f
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/images/overschot.jpg differ
diff --git a/vendor/sboden/odtphp/tests/src/files/images/php.gif b/vendor/sboden/odtphp/tests/src/files/images/php.gif
new file mode 100755
index 0000000000..f352c7308f
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/images/php.gif differ
diff --git a/vendor/sboden/odtphp/tests/src/files/images/test.jpg b/vendor/sboden/odtphp/tests/src/files/images/test.jpg
new file mode 100755
index 0000000000..4e03043e69
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/images/test.jpg differ
diff --git a/vendor/sboden/odtphp/tests/src/files/images/voronoi_sphere.png b/vendor/sboden/odtphp/tests/src/files/images/voronoi_sphere.png
new file mode 100755
index 0000000000..1685f87466
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/images/voronoi_sphere.png differ
diff --git a/vendor/sboden/odtphp/tests/src/files/input/BasicTest1.odt b/vendor/sboden/odtphp/tests/src/files/input/BasicTest1.odt
new file mode 100755
index 0000000000..f1af2afd13
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/input/BasicTest1.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/input/BasicTest2.odt b/vendor/sboden/odtphp/tests/src/files/input/BasicTest2.odt
new file mode 100755
index 0000000000..68ae77677b
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/input/BasicTest2.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/input/ConfigTest.odt b/vendor/sboden/odtphp/tests/src/files/input/ConfigTest.odt
new file mode 100755
index 0000000000..480ce57de7
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/input/ConfigTest.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/input/CustomPropertiesTest.odt b/vendor/sboden/odtphp/tests/src/files/input/CustomPropertiesTest.odt
new file mode 100644
index 0000000000..e51af98a92
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/input/CustomPropertiesTest.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/input/CustomPropertyExistsTest.odt b/vendor/sboden/odtphp/tests/src/files/input/CustomPropertyExistsTest.odt
new file mode 100644
index 0000000000..6aa978b2c1
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/input/CustomPropertyExistsTest.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/input/EncodingTest.odt b/vendor/sboden/odtphp/tests/src/files/input/EncodingTest.odt
new file mode 100755
index 0000000000..985fcbcce3
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/input/EncodingTest.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/input/ImageTest.odt b/vendor/sboden/odtphp/tests/src/files/input/ImageTest.odt
new file mode 100755
index 0000000000..376df5009d
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/input/ImageTest.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/input/LargeVariableTest.odt b/vendor/sboden/odtphp/tests/src/files/input/LargeVariableTest.odt
new file mode 100755
index 0000000000..5196e2d6f5
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/input/LargeVariableTest.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/input/LargeVariableTest1.odt b/vendor/sboden/odtphp/tests/src/files/input/LargeVariableTest1.odt
new file mode 100755
index 0000000000..e87b3ca071
--- /dev/null
+++ b/vendor/sboden/odtphp/tests/src/files/input/LargeVariableTest1.odt
@@ -0,0 +1 @@
+{large_content}
diff --git a/vendor/sboden/odtphp/tests/src/files/input/NestedSegmentTest.odt b/vendor/sboden/odtphp/tests/src/files/input/NestedSegmentTest.odt
new file mode 100755
index 0000000000..7f477e5dae
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/input/NestedSegmentTest.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/input/VariableTest.odt b/vendor/sboden/odtphp/tests/src/files/input/VariableTest.odt
new file mode 100755
index 0000000000..d72bac6f82
Binary files /dev/null and b/vendor/sboden/odtphp/tests/src/files/input/VariableTest.odt differ
diff --git a/vendor/sboden/odtphp/tests/src/files/output/.gitkeep b/vendor/sboden/odtphp/tests/src/files/output/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2