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
, renders header + + // Each row = with 4 cells (label, field, label, field) + echo ""; + echo ""; + echo ""; + echo Html::input('name', ['id' => 'name', 'value' => $this->fields['name'] ?? '']); + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + + $this->showFormButtons($options); // renders Save/Add/Delete buttons + closes
+ 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} +
+ +
+ + + + + + + + + + + + + + + + + + + {$items_html} + + + + + + +
PozIlość/
Menge
Nazwa środka trwałego /
Bezeichnung
Nr seryjny /
Seriennummer
Nr środka trwałego /
Anlagennumm.
Wartość /
Betrag
Lokaliz. /
Standort
Cost
Center
OrderData włącz. do użytku /
Datum Inbetriebnahme
Data zdep. w magazynie /
Datum ins Lager
SUMA / Summe:{$total_formatted}
+ +
+ + + +
____________________________
czytelny podpis
+ +
+ + + + +
Uwagi:
 
 
+ +

+ + + + + +
+ Zmiany lokalizacji środków trwałych / (Verschiebung von Anlagenvermögen) +
+ + + + + + + + + + + + + + + + + +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 .= << +
PozIlość/
Menge
Nazwa środka trwałego /
Bezeichnung
Nr seryjny /
Seriennummer
Nr środka trwałego /
Anlagennumm.
Lokaliz. /
Standort
Cost
Center
OrderData przes. /
Datum von Verschiebung
Data włącz. do użytku /
Datum Inbetriebnahme
 
+ +
+ + + + + + +
____________________________
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 "\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 + + + + +
+

+ Mrs + Mr. +

+

+ + +

+

+ + +

+

+ + +

+

+ +

+
+ + +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('&lt;test&gt;', $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