diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..89c1925
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/xml/settings.local.xml
+/.env
+/.tools/
+/tests/.local/
diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..9b0c667
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,10 @@
+Options -Indexes
+
";
- print_r($object);
- exit();
- }
-
- // ==============
- // Reset Password
- // ==============
+/** Reset page controller; account storage and email are shared with the other flows. */
+class Authentication extends Base {
public function resetPassword() {
-
- $username = filter_input(INPUT_POST, "user_name") ?: "";
-
- if (empty($username)) {
- return false;
- }
-
- $exists = $this->userExists($username);
-
- if (!$exists) {
- return false;
- }
-
- $user = $this->getUser($username);
-
- $password = $this->resetUsersPassword($user->record->user_serial);
-
- $this->emailUserPasswordResetLink($user, $password);
- $this->journalEntry("{$username} reset their password.");
-
- return true;
- }
-
- // ===========
- // User Exists
- // ===========
-
- private function userExists($username) {
-
- $SQL = 'select user_name from view_users where user_name = :username';
-
- $statement = $this->ghrconnection->prepare($SQL);
- $statement->bindValue(':username', $username, PDO::PARAM_STR);
- $statement->execute();
-
- $user = $statement->fetch(PDO::FETCH_ASSOC);
-
- return ($user !== false);
- }
-
- // =================
- // Get a User Object
- // =================
-
- private function getUser($username) {
-
- $SQL = 'select user_serial, user_name, user_email from view_users where user_name = :username';
-
- $statement = $this->ghrconnection->prepare($SQL);
- $statement->bindValue(':username', $username, PDO::PARAM_STR);
- $statement->execute();
-
- $user = $statement->fetch(PDO::FETCH_ASSOC);
-
- $xml = new SimpleXMLElement(' ');
- $record = $xml->addChild('record');
-
- $this->arrayToXml($user, $record);
-
- return $xml;
- }
-
- private function arrayToXml(array $data, SimpleXMLElement $xml): void {
- foreach ($data as $key => $value) {
- $key = is_numeric($key) ? 'item' : $key;
-
- if (is_array($value)) {
- $child = $xml->addChild($key);
- $this->arrayToXml($value, $child);
- } else {
- $xml->addChild($key, htmlspecialchars((string) $value));
- }
- }
- }
-
- // ========================
- // Reset the Users Password
- // ========================
-
- private function resetUsersPassword($user_serial) {
-
- $user_random_password = $this->generateRandomPassword();
- $user_temp_password = password_hash($user_random_password, PASSWORD_DEFAULT);
-
- $SQL = "update users
- set user_password = :user_password,
- user_change_password = :user_change_password
- where user_serial = :user_serial";
-
- $statement = $this->ghrconnection->prepare($SQL);
-
- $statement->bindValue(":user_serial", $user_serial);
- $statement->bindValue(":user_password", $user_temp_password);
- $statement->bindValue(":user_change_password", 1);
-
- $statement->execute();
-
- return $user_random_password;
- }
-
- // =================================
- // Email a users Password Reset Link
- // =================================
-
- public function emailUserPasswordResetLink($user, $password) {
-
- if (!$user instanceof SimpleXMLElement) {
- return;
- }
-
- // Prepare the email.
-
- $XSLParms['USER-TEMP-PASSWORD'] = $password;
-
- $to = $user->record->user_email;
- $subject = "DEC International DMS - Password Reset Request";
- $message = $this->applyXSL($user, $this->getXSL("emailPasswordResetLink"), $XSLParms);
-
- // Send the Email
- $this->sendEmail($to, $subject, $message);
- }
-
- // =======================================
- // Send an email with optional attachments
- // =======================================
-
- public function sendEmail($to = "", $subject = "", $message = "", $attachments = "", $attachment_names = "unknown") {
-
- if (empty($to) or empty($subject) or empty($message)) {
-
- return false;
- }
-
- $SmtpHost = $this->settings->SmtpServer;
- $SmtpUsername = $this->settings->SmtpUserName;
- $SmtpPassword = $this->settings->SmtpPassword;
- $SmtpPort = $this->settings->SmtpPort;
-
- $email = new PHPMailer(true);
-
- // Only for development
-
- $message = "" . $message . "";
-
- try {
-
- //Server settings
- $email->isSMTP();
- $email->Host = $SmtpHost;
- $email->SMTPAuth = true;
- $email->Username = $SmtpUsername; // Your Gmail address
- $email->Password = $SmtpPassword; // App password from Google
-// $email->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // or 'tls'
- $email->Port = $SmtpPort;
-
- $email->isHTML(true);
- $email->SetFrom("info@dec-international.com", "DEC-International");
-
- if (is_array($to)) {
- foreach ($to as $address) {
- $address = trim(str_replace(" ", "", $address));
- if (empty($address)) {
- continue;
+ $token = $_POST['token'] ?? $_GET['token'] ?? '';
+ $params = ['TOKEN' => $token, 'MESSAGE' => '', 'ERROR' => '', 'COMPLETE' => 'FALSE'];
+ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
+ $this->requireCsrf();
+ if (($_POST['step'] ?? '') === 'reset') {
+ try {
+ $auth = new AuthService();
+ if (!$auth->allowAttempt('reset-submit-ip', $_SERVER['REMOTE_ADDR'] ?? '', 20, 900)) {
+ throw new InvalidArgumentException('Too many attempts. Please try again later.');
}
- $email->addAddress($address);
- }
- } else {
- $email->addAddress($to);
- }
-
- $email->Subject = $subject;
- $email->Body = $message;
-
- if (is_array($attachments)) {
- foreach ($attachments as $index => $item) {
- if (!empty($item)) {
- $email->addStringAttachment($item, ($attachment_names[$index] ?? "unknown"));
+ if (!$auth->resetPassword($token, $_POST['user_password'] ?? '', $_POST['user_password_confirm'] ?? '')) {
+ throw new InvalidArgumentException('This reset link is invalid or expired. Request a new link.');
}
+ $this->logoutSession();
+ $_SESSION['password_reset_success'] = true;
+ header('Location: main.php?Xaction=resetpassword', true, 303);
+ exit;
+ } catch (InvalidArgumentException $e) {
+ $params['ERROR'] = $e->getMessage();
}
} else {
- if (!empty($attachments)) {
- $email->addStringAttachment($attachments, $attachment_names);
+ try {
+ (new AuthService())->requestReset(trim($_POST['user_email'] ?? ''));
+ } catch (Throwable $e) {
+ error_log('Password reset request failed: ' . $e->getMessage());
}
+ $_SESSION['password_reset_requested'] = true;
+ header('Location: main.php?Xaction=resetpassword', true, 303);
+ exit;
}
-
- $success = $email->Send();
- } catch (phpMailerException $e) {
-
- $this->journalEntry($e->errorMessage());
- $success = false;
- } catch (Exception $e) {
-
- $this->journalEntry($e->getMessage());
- $success = false;
}
-
- return $success;
- }
-
- // ======================
- // Create a Journal Entry
- // ======================
-
- public function journalEntry($journal_entry = "") {
-
- if (empty($journal_entry)) {
- return;
+ if (!empty($_SESSION['password_reset_success'])) {
+ $params['MESSAGE'] = 'Your password has been changed. Please sign in with your new password.';
+ $params['COMPLETE'] = 'TRUE';
+ unset($_SESSION['password_reset_success']);
+ } elseif (!empty($_SESSION['password_reset_requested'])) {
+ $params['MESSAGE'] = 'If an active account matches, you will receive a password reset link. The link expires in 30 minutes.';
+ unset($_SESSION['password_reset_requested']);
}
-
- $SQL = "insert into journal
-
- ( journal_origin,
- journal_ip,
- journal_entry )
-
- VALUES ( :journal_origin,
- :journal_ip,
- :journal_entry )";
-
- $statement = $this->ghrconnection->prepare($SQL);
-
- $statement->bindValue(":journal_origin", "Authentication Class");
- $statement->bindValue(":journal_ip", $_SERVER["REMOTE_ADDR"] ?? "");
- $statement->bindValue(":journal_entry", $journal_entry);
-
- $statement->execute();
- }
-
- // ==========================================
- // Apply an XSL Stylesheet to an XML Document
- // ==========================================
-
- public function applyXSL($XMLString = "", $XSLString = "", $XSLParms = "") {
-
- if ($XMLString instanceof SimpleXMLElement) {
- $XMLString = $XMLString->asXML();
- }
-
- if (empty($XSLString)) {
- trigger_error("No XSL Stylesheet provided.");
- }
-
- if (!is_array($XSLParms)) {
- $XSLParms = array();
- }
-
- $XSLParms["RANDOM_NUMBER"] = rand();
- $XSLParms["TODAY_YYYYMMDD"] = date("Y-m-d");
- $XSLParms["TODAY_MMDDYYYY"] = date("m/d/Y");
- $XSLParms["TODAY_HHMM"] = date("Hi");
- $XSLParms["COPYRIGHT"] = date("Y");
-
- $XML = new DOMDocument();
- $XSL = new DOMDocument();
- $XSLT = new XSLTProcessor();
-
- if ($XMLString) {
- $XML->loadXML($XMLString);
- }
-
- if ($XSLString) {
- $XSL->loadXML($XSLString);
- }
-
- $XSLT->importStylesheet($XSL);
-
- if ($XSLParms) {
- $XSLT->setParameter("", $XSLParms);
- }
-
- $document = $XSLT->transformToXml($XML);
-
- return $document;
- }
-
- // ========================
- // Return an XSL Stylesheet
- // ========================
-
- public function getXSL($document = "") {
-
- if (trim($document) == "") {
- return "";
- }
-
- $document = "xsl/" . trim($document) . ".xsl";
- $XSL = (file_exists($document) ? file_get_contents($document) : "");
-
- return $XSL;
+ echo $this->applyXSL(' ', $this->getXSL('resetPassword'), $params);
+ exit;
}
}
diff --git a/classes/Autoloader.php b/classes/Autoloader.php
index de353d0..99e2adf 100644
--- a/classes/Autoloader.php
+++ b/classes/Autoloader.php
@@ -13,35 +13,37 @@ class Base_Autoloader {
if (function_exists("__autoload")) {
spl_autoload_register("__autoload");
}
-
+
return spl_autoload_register(array("Base_Autoloader", "Load"), true, true);
-
+
}
public static function Load($pClassName) {
-
+
if (class_exists($pClassName, false)) {
return false;
}
+ if (!preg_match('/\A[A-Za-z_][A-Za-z0-9_]*\z/', $pClassName)) return false;
+
// Try for a "classes" Class first, then a "reports" Class.
-
- $pClassFilePath = "classes/{$pClassName}.php";
+
+ $pClassFilePath = __DIR__ . "/{$pClassName}.php";
if ((file_exists($pClassFilePath) === false) || (is_readable($pClassFilePath) === false)) {
-
- $pClassFilePath = "reports/{$pClassName}.php";
-
+
+ $pClassFilePath = __DIR__ . "/../reports/{$pClassName}.php";
+
if ((file_exists($pClassFilePath) === false) || (is_readable($pClassFilePath) === false)) {
return false;
}
-
+
}
require($pClassFilePath);
-
+
}
}
-?>
\ No newline at end of file
+?>
diff --git a/classes/Base.php b/classes/Base.php
index bf1f89f..3f68928 100644
--- a/classes/Base.php
+++ b/classes/Base.php
@@ -4,8 +4,8 @@
// Base Class
// ==========
-require_once "vendor/autoload.php";
-require_once "classes/Autoloader.php";
+require_once __DIR__ . "/../vendor/autoload.php";
+require_once __DIR__ . "/Autoloader.php";
class Base {
@@ -13,31 +13,30 @@ class Base {
// Constants
// =========
- const APPLICATION_NAME = "Hunting Club Roster";
+ const APPLICATION_NAME = "Hunting Club Roster";
const APPLICATION_MNEMONIC = "HUNTINGCLUBROSTER";
- const VERSION = "1.00";
- const TIMEZONE = "America/New_York";
-
+ const VERSION = "1.00";
+ const TIMEZONE = "America/New_York";
// =================
// Session Variables
// =================
- const ENVIRONMENT = "HUNTINGCLUBROSTER_ENVIRONMENT";
- const ACTIVE_SESSION = "HUNTINGCLUBROSTER_ACTIVE_SESSION";
- const USER_SERIAL = "HUNTINGCLUBROSTER_USER_SERIAL";
- const USER_NAME = "HUNTINGCLUBROSTER_USER_NAME";
- const USER_FULL_NAME = "HUNTINGCLUBROSTER_USER_FULL_NAME";
- const USER_EMAIL = "HUNTINGCLUBROSTER_USER_EMAIL";
- const USER_ROLE = "HUNTINGCLUBROSTER_USER_ROLE";
- const USER_AUTHENTICATED = "HUNTINGCLUBROSTER_USER_AUTHENTICATED";
- const REPORTS_GET = "HUNTINGCLUBROSTER_REPORTS_GET";
- const REPORTS_POST = "HUNTINGCLUBROSTER_REPORTS_POST";
- const THEME = "HUNTINGCLUBROSTER_THEME";
- const AJAX = "HUNTINGCLUBROSTER_AJAX";
- const TODAY = "HUNTINGCLUBROSTER_TODAY";
- const COPYRIGHT = "HUNTINGCLUBROSTER_COPYRIGHT";
- const PAGE = "HUNTINGCLUBROSTER_PAGE";
- const PAGINATION_PAGE = "HUNTINGCLUBROSTER_PAGINATION_PAGE";
+ const ENVIRONMENT = "HUNTINGCLUBROSTER_ENVIRONMENT";
+ const ACTIVE_SESSION = "HUNTINGCLUBROSTER_ACTIVE_SESSION";
+ const USER_SERIAL = "HUNTINGCLUBROSTER_USER_SERIAL";
+ const USER_NAME = "HUNTINGCLUBROSTER_USER_NAME";
+ const USER_FULL_NAME = "HUNTINGCLUBROSTER_USER_FULL_NAME";
+ const USER_EMAIL = "HUNTINGCLUBROSTER_USER_EMAIL";
+ const USER_ROLE = "HUNTINGCLUBROSTER_USER_ROLE";
+ const USER_AUTHENTICATED = "HUNTINGCLUBROSTER_USER_AUTHENTICATED";
+ const REPORTS_GET = "HUNTINGCLUBROSTER_REPORTS_GET";
+ const REPORTS_POST = "HUNTINGCLUBROSTER_REPORTS_POST";
+ const THEME = "HUNTINGCLUBROSTER_THEME";
+ const AJAX = "HUNTINGCLUBROSTER_AJAX";
+ const TODAY = "HUNTINGCLUBROSTER_TODAY";
+ const COPYRIGHT = "HUNTINGCLUBROSTER_COPYRIGHT";
+ const PAGE = "HUNTINGCLUBROSTER_PAGE";
+ const PAGINATION_PAGE = "HUNTINGCLUBROSTER_PAGINATION_PAGE";
// =================
// Class Constructor
@@ -56,7 +55,7 @@ class Base {
public function appendSXE(SimpleXMLElement $from, SimpleXMLElement $to) {
- $toDOM = dom_import_simplexml($to);
+ $toDOM = dom_import_simplexml($to);
$fromDOM = dom_import_simplexml($from);
$toDOM->appendChild($toDOM->ownerDocument->importNode($fromDOM, true));
@@ -80,30 +79,31 @@ class Base {
$XSLParms = array();
}
- $XSLParms["APPLICATION_NAME"] = self::APPLICATION_NAME;
+ $XSLParms["CSRF_TOKEN"] = $this->csrfToken();
+ $XSLParms["APPLICATION_NAME"] = self::APPLICATION_NAME;
$XSLParms["APPLICATION_MNEMONIC"] = self::APPLICATION_MNEMONIC;
- $XSLParms["VERSION"] = self::VERSION;
- $XSLParms["THEME"] = $_SESSION[self::THEME] ?? self::THEME;
- $XSLParms["ENVIRONMENT"] = $_SESSION[self::ENVIRONMENT] ?? "Production";
- $XSLParms["USER_NAME"] = $_SESSION[self::USER_NAME] ?? "";
- $XSLParms["USER_FULL_NAME"] = $_SESSION[self::USER_FULL_NAME] ?? "";
- $XSLParms["USER_ROLE"] = $_SESSION[self::USER_ROLE] ?? "Guest";
- $XSLParms["RANDOM_NUMBER"] = rand();
- $XSLParms["TODAY_YYYYMMDD"] = date("Y-m-d");
- $XSLParms["TODAY_MMDDYYYY"] = date("m/d/Y");
- $XSLParms["TODAY_HHMM"] = date("Hi");
- $XSLParms["COPYRIGHT"] = date("Y");
+ $XSLParms["VERSION"] = self::VERSION;
+ $XSLParms["THEME"] = $_SESSION[self::THEME] ?? self::THEME;
+ $XSLParms["ENVIRONMENT"] = $_SESSION[self::ENVIRONMENT] ?? "Production";
+ $XSLParms["USER_NAME"] = $XSLParms["USER_NAME"] ?? $_SESSION[self::USER_NAME] ?? "";
+ $XSLParms["USER_FULL_NAME"] = $_SESSION[self::USER_FULL_NAME] ?? "";
+ $XSLParms["USER_ROLE"] = $_SESSION[self::USER_ROLE] ?? "Guest";
+ $XSLParms["RANDOM_NUMBER"] = rand();
+ $XSLParms["TODAY_YYYYMMDD"] = date("Y-m-d");
+ $XSLParms["TODAY_MMDDYYYY"] = date("m/d/Y");
+ $XSLParms["TODAY_HHMM"] = date("Hi");
+ $XSLParms["COPYRIGHT"] = date("Y");
- $XML = new DOMDocument();
- $XSL = new DOMDocument();
+ $XML = new DOMDocument();
+ $XSL = new DOMDocument();
$XSLT = new XSLTProcessor();
if ($XMLString) {
- $XML->loadXML($XMLString);
+ $XML->loadXML($XMLString, LIBXML_NONET);
}
if ($XSLString) {
- $XSL->loadXML($XSLString);
+ $XSL->loadXML($XSLString, LIBXML_NONET);
}
$XSLT->importStylesheet($XSL);
@@ -112,6 +112,8 @@ class Base {
$XSLT->setParameter("", $XSLParms);
}
+ if (!$XML->documentElement)
+ $XML->loadXML(' ');
$document = $XSLT->transformToXml($XML);
return $document;
@@ -122,27 +124,26 @@ class Base {
// ============================
public function connect(): PDO {
-
+
$settings = $this->getSettings();
-
- $host = ((string) $settings->Environment === 'Production') ? 'localhost' : '192.168.1.190';
- $database = (string) $settings->DatabaseName;
- $username = (string) $settings->DatabaseUser;
- $password = (string) $settings->DatabasePassword;
+
+ $host = (string) $settings->DatabaseServer;
+ $database = (string) $settings->DatabaseName;
+ $username = (string) $settings->DatabaseUser;
+ $password = (string) $settings->DatabasePassword;
try {
- $dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $host, $database);
+ $dsn = sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', $host, (int) ($settings->DatabasePort ?? 3306), $database);
- return new PDO($dsn, $username, $password,
- [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
- PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
- PDO::ATTR_EMULATE_PREPARES => false,
- PDO::ATTR_PERSISTENT => false,]);
-
+ return new PDO($dsn, $username, $password,
+ [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ PDO::ATTR_EMULATE_PREPARES => false,
+ PDO::ATTR_PERSISTENT => false,]);
} catch (PDOException $e) {
- $this->logError("Database Connection Failed: " . $e->getMessage());
+ error_log("Database Connection Failed: " . $e->getMessage());
throw new Exception("Unable to connect to the database.", 0, $e);
}
@@ -182,32 +183,17 @@ class Base {
// Custom error handler to display informative error page
// ======================================================
- public function displayError($error_number = "", $error_message = "", $error_file = "", $error_line = "") {
-
- if (error_reporting() == 0) {
- return;
+ public function displayError($error_number = 0, $error_message = '', $error_file = '', $error_line = 0) {
+ if (!(error_reporting() & $error_number)) {
+ return false;
}
+ // Never log application failures through the failing database or SMTP service.
+ error_log("PHP {$error_number}: {$error_message} in {$error_file}:{$error_line}");
- $error_file = explode("/", $error_file);
- $error_file = end($error_file);
- $error_message = str_replace("'", '"', $error_message);
-
- (new Journal())->journalEntry("{$error_file} | {$error_line} | {$error_message}");
-
- if (($_SESSION[self::ENVIRONMENT] ?? "Production") == "Production") {
- (new Email())->sendErrorEmail($error_message, $error_file, $error_line);
+ if (in_array($error_number, [E_DEPRECATED, E_USER_DEPRECATED], true)) {
+ return true;
}
-
- $XSLParms["FILE_NAME"] = $error_file;
- $XSLParms["LINE_NUMBER"] = $error_line;
- $XSLParms["ERROR_MESSAGE"] = $error_message;
- $XSLParms["ERROR_NUMBER"] = $error_number;
-
- $content = $this->applyXSL("", $this->getXSL("error"), $XSLParms);
-
- $this->displayContent($content);
-
- exit(); // Exit so that only one error is presented.
+ throw new ErrorException($error_message, 0, $error_number, $error_file, $error_line);
}
// =====================
@@ -241,71 +227,139 @@ class Base {
public function do() {
- $_SESSION[self::TODAY] = date("Y-m-d");
-
- // Get the requested Action
-
- $action = filter_input(INPUT_POST, "action") ?: "";
- $Xaction = filter_input(INPUT_GET, "Xaction") ?: "";
-
- $action = (empty($Xaction) ? $action : $Xaction);
-
- if ($Xaction == 'resetpassword') {
- $this->resetpassword();
- }
-
- // Login
-
- if (($_SESSION[self::USER_AUTHENTICATED] ?? false) === false) {
-
- (new SignIn())->login();
- }
-
- // Determine if user needs a password change
- if ($this->userChangePassword() === true) {
- (new Users())->changePassword();
- exit();
- }
-
- // Determine if this is an AJAX request.
-
- $_SESSION[self::AJAX] = (isset($_SERVER["HTTP_X_REQUESTED_WITH"]) and strtolower($_SERVER["HTTP_X_REQUESTED_WITH"]) === "xmlhttprequest");
-
- // Get the requested Action
-
- $action = filter_input(INPUT_POST, "action") ?: "";
- $Xaction = filter_input(INPUT_GET, "Xaction") ?: "";
-
- $action = (empty($Xaction) ? $action : $Xaction);
-
- // Sign Out if requested.
-
- if (strtolower(trim($action)) === "signout") {
+ if (empty($_SESSION[self::ACTIVE_SESSION])) {
$this->initializeSession();
}
- // Process the requested Action.
+ $_SESSION[self::AJAX] = strtolower($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'xmlhttprequest';
- list($class, $method) = array_pad(explode('.', $action), 2, "");
+ $_SESSION[self::TODAY] = date('Y-m-d');
- if (empty($method)) {
+ $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
- $method = $class;
-
- if (method_exists(__CLASS__, $method)) {
- $this->$method();
- return;
- }
- } else {
-
- if (method_exists($class, $method)) {
- (new $class())->$method();
- return;
- }
+ if (!in_array($method, ['GET', 'POST'], true)) {
+ http_response_code(405);
+ exit('Method not allowed.');
}
- // Default to the Home Page.
- $this->displayHome();
+ // Reject arrays and oversized values before passing input to string operations.
+ foreach (array_merge($_GET, $_POST) as $value) {
+ if (!is_string($value) || strlen($value) > 8192) {
+ http_response_code(400);
+ exit('Invalid request.');
+ }
+ }
+ $action = strtolower(trim($_POST['action'] ?? $_GET['Xaction'] ?? $_GET['action'] ?? ''));
+ if ($action === 'signout') {
+ if ($method !== 'POST') {
+ http_response_code(405);
+ exit('Use POST to sign out.');
+ }
+ $this->requireCsrf();
+ $ajax = $_SESSION[self::AJAX];
+ $this->logoutSession();
+ if ($ajax) {
+ header('Content-Type: application/json');
+ echo json_encode(true);
+ exit;
+ }
+ header('Location: main.php', true, 303);
+ exit;
+ }
+ if (in_array($action, ['resetpassword', 'authentication.resetpassword'], true)) {
+ (new Authentication())->resetPassword();
+ }
+ if (in_array($action, ['signup', 'signup.createaccount'], true)) {
+ (new SignUp())->createAccount();
+ }
+ if (in_array($action, ['signin', 'signin.login', 'users.login'], true)) {
+ (new SignIn())->login();
+ }
+
+ $user = (new AuthService())->currentUser();
+
+ if (!$user) {
+ $ajax = $_SESSION[self::AJAX];
+ if (!empty($_SESSION[self::USER_AUTHENTICATED])) {
+ $this->logoutSession();
+ }
+ if ($ajax) {
+ http_response_code(401);
+ exit('Please sign in again.');
+ }
+ (new SignIn())->login();
+ }
+ if ($method === 'POST') {
+ $this->requireCsrf();
+ }
+ if ((bool) $user['user_change_password']) {
+ (new Users())->changePassword();
+ return;
+ }
+ // Only named, reviewed entry points are routable. Helpers are never endpoints.
+ $routes = [
+ '' => [Base::class, 'displayHome', false],
+ './' => [Base::class, 'displayHome', false],
+ 'displayhome' => [Base::class, 'displayHome', false],
+ 'toggletheme' => [Base::class, 'toggleTheme', false],
+ 'getzipcode' => [Base::class, 'getZipcode', false],
+ 'validatedate' => [Base::class, 'validateDate', false],
+ 'getage' => [Base::class, 'getAge', false],
+ 'users.changepassword' => [Users::class, 'changePassword', false],
+ 'print' => [Base::class, 'print', true],
+ 'print_all_permitsbybuilder.print' => ['Print_All_PermitsByBuilder', 'print', true],
+ 'export_allpermitsbybuildercsv.export' => ['Export_AllPermitsByBuilderCSV', 'export', true],
+ 'export_allpermitsbybuilderexcel.export' => ['Export_AllPermitsByBuilderExcel', 'export', true],
+ ];
+ $adminRoutes = [
+ 'Users' => ['setupUsers', 'addUser', 'editUser', 'deleteUser', 'resetPassword', 'userExists', 'getUser_AJAX', 'updateActiveStatus', 'userActive'],
+ 'Alerts' => ['setupAlerts', 'addAlert', 'editAlert', 'deleteAlert'],
+ 'Dropdowns' => ['setupDropdowns', 'setupDropdowntypes', 'addDropdown', 'editDropdown', 'deleteDropdown', 'addDropdowntype', 'editDropdowntype', 'deleteDropdowntype', 'dropdownExists', 'dropdowntypeExists', 'dropdowntypetitleExists', 'dropdownActive', 'dropdowntypeActive', 'updateDropdownActive', 'updateDropdownDefault'],
+ 'Popovers' => ['setupPopovers', 'addPopover', 'editPopover', 'deletePopover', 'popoverExists'],
+ 'Reports' => ['setupReports', 'addReport', 'editReport', 'deleteReport', 'selectReport', 'reportExists'],
+ 'Statuses' => ['setupStatuses', 'addStatus', 'editStatus', 'deleteStatus', 'statusExists', 'statusActive'],
+ 'Journal' => ['viewJournal', 'clearJournal'],
+ 'Notes' => ['addNote', 'editNote', 'deleteNote', 'addNoteQuick'],
+ ];
+ foreach ($adminRoutes as $class => $methods) {
+ foreach ($methods as $entry)
+ $routes[strtolower($class . '.' . $entry)] = [$class, $entry, true];
+ }
+ if (!isset($routes[$action])) {
+ http_response_code(404);
+ exit('Page not found.');
+ }
+ [$class, $entry, $adminOnly] = $routes[$action];
+ if ($adminOnly && !in_array($user['user_role'], ['Administrator', 'System Administrator'], true)) {
+ http_response_code(403);
+ exit('You do not have permission to perform this action.');
+ }
+ if ($method !== 'POST' && !in_array($action, ['', './', 'displayhome', 'users.changepassword'], true)) {
+ http_response_code(405);
+ exit('Use POST for this action.');
+ }
+ (new $class())->$entry();
+ }
+
+ public function csrfToken(): string {
+ if (empty($_SESSION['csrf_token']))
+ $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
+ return $_SESSION['csrf_token'];
+ }
+
+ public function requireCsrf(): void {
+ $token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
+ if (!is_string($token) || !hash_equals($this->csrfToken(), $token)) {
+ http_response_code(403);
+ exit('Your form expired. Reload the page and try again.');
+ }
+ }
+
+ public function logoutSession(): void {
+ $_SESSION = [];
+ session_regenerate_id(true);
+ $this->initializeSession();
+ $this->csrfToken();
}
// ==========================
@@ -314,12 +368,12 @@ class Base {
protected function encrypt($string = "", $key = "") {
- $key = (empty($key)) ? __CLASS__ : $key;
+ $key = (empty($key)) ? __CLASS__ : $key;
- $ivlen = openssl_cipher_iv_length($cipher = "AES-128-CBC");
- $iv = openssl_random_pseudo_bytes($ivlen);
- $raw = openssl_encrypt($string, $cipher, $key, $options = OPENSSL_RAW_DATA, $iv);
- $hmac = hash_hmac("sha256", $raw, $key, $as_binary = true);
+ $ivlen = openssl_cipher_iv_length($cipher = "AES-128-CBC");
+ $iv = openssl_random_pseudo_bytes($ivlen);
+ $raw = openssl_encrypt($string, $cipher, $key, $options = OPENSSL_RAW_DATA, $iv);
+ $hmac = hash_hmac("sha256", $raw, $key, $as_binary = true);
$encrypted = base64_encode($iv . $hmac . $raw);
return $encrypted;
@@ -366,7 +420,15 @@ class Base {
public function getSettings() {
- $settings = new SimpleXMLElement($this->getXML("settings"));
+ $path = getenv('HCR_SETTINGS_FILE') ?: (is_file(__DIR__ . '/../xml/settings.local.xml') ? __DIR__ . '/../xml/settings.local.xml' : __DIR__ . '/../xml/settings.xml');
+ $settings = simplexml_load_file($path, SimpleXMLElement::class, LIBXML_NONET);
+ if (!$settings)
+ throw new RuntimeException('Unable to load application settings.');
+ foreach (['Environment' => 'HCR_ENVIRONMENT', 'DatabaseServer' => 'HCR_DB_HOST', 'DatabasePort' => 'HCR_DB_PORT', 'DatabaseName' => 'HCR_DB_NAME', 'DatabaseUser' => 'HCR_DB_USER', 'DatabasePassword' => 'HCR_DB_PASSWORD', 'SmtpServer' => 'HCR_SMTP_HOST', 'SmtpUserName' => 'HCR_SMTP_USER', 'SmtpPassword' => 'HCR_SMTP_PASSWORD', 'SmtpPort' => 'HCR_SMTP_PORT', 'SmtpFromEmail' => 'HCR_SMTP_FROM', 'ApplicationUrl' => 'HCR_APPLICATION_URL'] as $name => $variable) {
+ $value = getenv($variable);
+ if ($value !== false)
+ $settings->$name = $value;
+ }
return $settings;
}
@@ -378,11 +440,11 @@ class Base {
public function getAge($birthdate = "", $date = "") {
$birthdate = filter_input(INPUT_POST, "birthdate", FILTER_UNSAFE_RAW) ?: $birthdate;
- $date = filter_input(INPUT_POST, "date", FILTER_UNSAFE_RAW) ?: $date;
+ $date = filter_input(INPUT_POST, "date", FILTER_UNSAFE_RAW) ?: $date;
- $age = 0;
+ $age = 0;
- $date = (empty($date)) ? new Datetime() : new DateTime($date);
+ $date = (empty($date)) ? new Datetime() : new DateTime($date);
$birthdate = (empty($birthdate)) ? new Datetime() : new DateTime($birthdate);
$age = $birthdate->diff($date)->format("%y");
@@ -529,8 +591,8 @@ class Base {
return "";
}
- $document = "xml/" . trim($document) . ".xml";
- $XML = (file_exists($document) ? file_get_contents($document) : "");
+ $document = __DIR__ . "/../xml/" . trim($document) . ".xml";
+ $XML = (file_exists($document) ? file_get_contents($document) : "");
return $XML;
}
@@ -545,8 +607,8 @@ class Base {
return "";
}
- $document = "xsl/" . trim($document) . ".xsl";
- $XSL = (file_exists($document) ? file_get_contents($document) : "");
+ $document = __DIR__ . "/../xsl/" . trim($document) . ".xsl";
+ $XSL = (file_exists($document) ? file_get_contents($document) : "");
return $XSL;
}
@@ -557,13 +619,13 @@ class Base {
public function getZipcode($zip_code = 0) {
- $zip_code = filter_input(INPUT_POST, "zip_code", FILTER_SANITIZE_NUMBER_INT) ?: 0;
- $zip_code = (int) substr((string) $zip_code, 0, 5);
+ $zip_code = filter_input(INPUT_POST, "zip_code", FILTER_SANITIZE_NUMBER_INT) ?: 0;
+ $zip_code = (int) substr((string) $zip_code, 0, 5);
$zipCodeDetail = null;
- $SQL = "select *
- from zipcodes
+ $SQL = "select *
+ from zipcodes
where zipcode_code = {$zip_code}";
$zipcodes = $this->getTable("zipcodes", $SQL);
@@ -571,9 +633,9 @@ class Base {
if ($zipcodes->count() > 0) {
$zipCodeDetail["ZipCode"] = $zip_code;
- $zipCodeDetail["City"] = (string) $zipcodes->record->zipcode_city;
- $zipCodeDetail["State"] = (string) $zipcodes->record->zipcode_state;
- $zipCodeDetail["County"] = (string) $zipcodes->record->zipcode_county;
+ $zipCodeDetail["City"] = (string) $zipcodes->record->zipcode_city;
+ $zipCodeDetail["State"] = (string) $zipcodes->record->zipcode_state;
+ $zipCodeDetail["County"] = (string) $zipcodes->record->zipcode_county;
$zipCodeDetail["Country"] = (string) $zipcodes->record->zipcode_country;
}
@@ -592,18 +654,18 @@ class Base {
$settings = $this->getSettings();
- $_SESSION[self::ACTIVE_SESSION] = true;
- $_SESSION[self::THEME] = "light";
- $_SESSION[self::ENVIRONMENT] = (string) ($settings->Environment ?? "Production");
+ $_SESSION[self::ACTIVE_SESSION] = true;
+ $_SESSION[self::THEME] = "light";
+ $_SESSION[self::ENVIRONMENT] = (string) ($settings->Environment ?? "Production");
$_SESSION[self::USER_AUTHENTICATED] = false;
- $_SESSION[self::USER_SERIAL] = 0;
- $_SESSION[self::USER_NAME] = "";
- $_SESSION[self::USER_FULL_NAME] = "";
- $_SESSION[self::USER_EMAIL] = "";
- $_SESSION[self::USER_ROLE] = "Guest";
- $_SESSION[self::REPORTS_GET] = "";
- $_SESSION[self::REPORTS_POST] = "";
- $_SESSION[self::AJAX] = false;
+ $_SESSION[self::USER_SERIAL] = 0;
+ $_SESSION[self::USER_NAME] = "";
+ $_SESSION[self::USER_FULL_NAME] = "";
+ $_SESSION[self::USER_EMAIL] = "";
+ $_SESSION[self::USER_ROLE] = "Guest";
+ $_SESSION[self::REPORTS_GET] = "";
+ $_SESSION[self::REPORTS_POST] = "";
+ $_SESSION[self::AJAX] = false;
}
// ============
@@ -647,7 +709,7 @@ class Base {
$thatDOM = new DOMDocument();
$thatDOM->loadXML($thatXML);
- $xpath = new DOMXpath($thisDOM);
+ $xpath = new DOMXpath($thisDOM);
$xpathQuery = $xpath->query("/*");
for ($i = 0; $i < $xpathQuery->length; $i++) {
@@ -665,11 +727,15 @@ class Base {
public function print() {
- $_SESSION[self::REPORTS_GET] = $_GET;
+ $_SESSION[self::REPORTS_GET] = $_GET;
$_SESSION[self::REPORTS_POST] = $_POST;
$report = filter_input(INPUT_POST, "report") ?: "";
+ if ($report !== 'Print_All_PermitsByBuilder') {
+ http_response_code(404);
+ exit('Report not found.');
+ }
(new $report())->print();
}
@@ -697,10 +763,10 @@ class Base {
public function validateDate($date = "", $format = "Y-m-d") {
- $date = filter_input(INPUT_POST, "date") ?: $date;
+ $date = filter_input(INPUT_POST, "date") ?: $date;
$datetime = DateTime::createFromFormat($format, $date);
- $valid = $datetime && $datetime->format($format) === $date;
+ $valid = $datetime && $datetime->format($format) === $date;
if ($_SESSION[self::AJAX]) {
echo json_encode($valid);
@@ -716,10 +782,10 @@ class Base {
public function generateRandomPassword($length = 12) {
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-_+=';
- $password = '';
+ $password = '';
for ($i = 0; $i < $length; $i++) {
- $index = rand(0, strlen($characters) - 1);
+ $index = random_int(0, strlen($characters) - 1);
$password .= $characters[$index];
}
@@ -759,7 +825,7 @@ class Base {
public function testing() {
- $to = "james.richie@onesourceits.com";
+ $to = "james.richie@onesourceits.com";
$subject = "Test Subject";
$message = "Test Message";
@@ -773,19 +839,4 @@ class Base {
// =====================
// Forgot/Reset Password
// =====================
-
- public function resetPassword() {
-
- require_once 'classes/Authentication.php';
-
- $XSLParms = [
- "BTN_SUBMIT" => "main.php?action=Users.signIn&step=authenticate",
- "RESET_PASSWORD_SUCCESSFUL" => (new Authentication())->resetPassword() ? "TRUE" : "FALSE"
- ];
-
- echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
- exit;
- }
}
-
-?>
diff --git a/classes/Email.php b/classes/Email.php
index 90bbcf7..11c653a 100644
--- a/classes/Email.php
+++ b/classes/Email.php
@@ -9,6 +9,13 @@ use PHPMailer\PHPMailer\Exception;
class Email extends Base {
+ public function sendPasswordReset(array $user, string $url): bool {
+ $xml = new SimpleXMLElement(' ');
+ $xml->record->user_name = $user['user_first_name'] ?: $user['user_name'];
+ $body = $this->applyXSL($xml, $this->getXSL('emailPasswordResetLink'), ['RESET_URL' => $url]);
+ return $this->sendEmail((string) $user['user_email'], self::APPLICATION_NAME . ' - Password reset', $body);
+ }
+
// Variables.
private $current_user_name = "";
@@ -32,45 +39,14 @@ class Email extends Base {
// Email a new users credentials
// =============================
- public function emailNewUserCredentials($user, $password) {
-
- if (!$user instanceof SimpleXMLElement) {
- return;
- }
-
- // Prepare the email
-
- $to = (string) $user->record->user_email;
-
- $XSLParms['USER-TEMP-PASSWORD'] = $password;
-
- $subject = "New User Credentials";
- $message = $this->applyXSL($user, $this->getXSL("emailNewUserCredentials"), $XSLParms);
-
- // Send the Email
- $this->sendEmail($to, $subject, $message);
+ public function emailNewUserCredentials($user, $password = null) {
+ $this->emailUserPasswordResetLink($user);
}
- // =================================
- // Email a users Password Reset Link
- // =================================
-
- public function emailUserPasswordResetLink($user, $password) {
-
- if (!$user instanceof SimpleXMLElement) {
- return;
+ public function emailUserPasswordResetLink($user, $password = null) {
+ if ($user instanceof SimpleXMLElement) {
+ (new AuthService())->requestReset((string) $user->record->user_email);
}
-
- // Prepare the email.
-
- $XSLParms['USER-TEMP-PASSWORD'] = $password;
-
- $to = (string) $user->record->user_email;
- $subject = "Password Reset Request";
- $message = $this->applyXSL($user, $this->getXSL("emailPasswordResetLink"), $XSLParms);
-
- // Send the Email
- $this->sendEmail($to, $subject, $message);
}
// =======================================
@@ -101,15 +77,17 @@ class Email extends Base {
//Server settings
$email->isSMTP();
- $email->Host = $SmtpHost;
+ $email->Host = (string) $SmtpHost;
$email->SMTPAuth = true;
- $email->Username = $SmtpUsername; // Your Gmail address
- $email->Password = $SmtpPassword; // App password from Google
-// $email->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // or 'tls'
- $email->Port = $SmtpPort;
+ $email->Username = (string) $SmtpUsername;
+ $email->Password = (string) $SmtpPassword;
+ $email->Port = (int) $SmtpPort;
+ $email->SMTPSecure = $email->Port === 465 ? PHPMailer::ENCRYPTION_SMTPS : PHPMailer::ENCRYPTION_STARTTLS;
+ $email->Timeout = 15;
+ $email->CharSet = 'UTF-8';
$email->isHTML(true);
- $email->SetFrom("info@onesourceits.com", "OneSource IT Solutions LLC");
+ $email->setFrom((string) ($settings->SmtpFromEmail ?? $settings->SmtpUserName), self::APPLICATION_NAME);
if (is_array($to)) {
foreach ($to as $address) {
@@ -125,6 +103,7 @@ class Email extends Base {
$email->Subject = $subject;
$email->Body = $message;
+ $email->AltBody = html_entity_decode(strip_tags(preg_replace('/]*>(.*?)<\/a>/is', '$2 ($1)', $message)), ENT_QUOTES | ENT_HTML5, 'UTF-8');
if (is_array($attachments)) {
foreach ($attachments as $index => $item) {
@@ -139,13 +118,9 @@ class Email extends Base {
}
$success = $email->Send();
- } catch (phpMailerException $e) {
+ } catch (\Throwable $e) {
- (new Journal())->journalEntry($e->errorMessage());
- $success = false;
- } catch (Exception $e) {
-
- (new Journal())->journalEntry($e->getMessage());
+ error_log('Email delivery failed: ' . $e->getMessage());
$success = false;
}
diff --git a/classes/SignIn.php b/classes/SignIn.php
index 4021933..45f655d 100644
--- a/classes/SignIn.php
+++ b/classes/SignIn.php
@@ -1,118 +1,16 @@
current_user_name = $_SESSION[self::USER_NAME] ?? "";
- $this->current_user_full_name = $_SESSION[self::USER_FULL_NAME] ?? "";
- $this->current_user_email = $_SESSION[self::USER_EMAIL] ?? "";
- }
-
- // ========================
- // Login to the application
- // ========================
-
public function login() {
-
- $step = filter_input(INPUT_POST, "step") ?: "prompt";
-
- switch ($step) {
-
- case "prompt":
-
- $XSLParms["BTN_SUBMIT"] = "main.php?action=SignIn.login&step=authenticate";
- $XSLParms["INVALID_SIGNIN"] = "FALSE";
-
- echo $this->applyXSL("", $this->getXSL("login"), $XSLParms);
- exit();
-
- break;
-
- case "authenticate":
-
- $user_name = trim(filter_input(INPUT_POST, "user_name") ?: "");
- $user_password = filter_input(INPUT_POST, "user_password") ?: "";
-
- $XSLParms["BTN_SUBMIT"] = "main.php?action=SignIn.login&step=authenticate";
- $XSLParms["INVALID_SIGNIN"] = "TRUE";
-
- $user = (new Users())->getUserByUserName($user_name);
-
- if ($user->count() == 0) {
- echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
- die();
- }
-
- if ((string) $user->record->user_status !== 'active') {
- echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
- die();
- }
-
- $stored_hashed_password = $user->record->user_password_hash;
-
- if (!password_verify($user_password, $stored_hashed_password)) {
-
- echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
- die();
- }
-
- session_regenerate_id(true);
-
- $_SESSION[self::USER_AUTHENTICATED] = true;
- $_SESSION[self::USER_SERIAL] = (int) $user->record->user_serial;
- $_SESSION[self::USER_NAME] = (string) $user->record->user_name;
- $_SESSION[self::USER_FULL_NAME] = (string) $user->record->user_full_name;
- $_SESSION[self::USER_EMAIL] = (string) $user->record->user_email;
- $_SESSION[self::USER_ROLE] = (string) $user->record->user_role;
-
- $this->current_user_serial = $_SESSION[self::USER_SERIAL] ?? 0;
- $this->current_user_name = $_SESSION[self::USER_NAME] ?? "";
- $this->current_user_email = $_SESSION[self::USER_EMAIL] ?? "";
-
- $system_admins = $this->getSettings()->xpath("//SystemAdministrator");
-
- foreach ($system_admins as $admin) {
- if ((string) $admin === $user_name) {
- $_SESSION[self::USER_ROLE] = "System Administrator";
- break;
- }
- }
-
- (new Journal())->journalEntry("{$_SESSION[self::USER_NAME]} logged in ({$_SESSION[self::USER_ROLE]}).");
-
- (new Users())->updateLogin();
-
- header("Location: ./");
- exit();
-
- break;
-
- default:
- // User NOT Validated.
- header("Location: ./");
- exit();
+ $params = ['BTN_SUBMIT' => 'main.php', 'INVALID_SIGNIN' => 'FALSE'];
+ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
+ $this->requireCsrf();
+ if ((new AuthService())->login(trim($_POST['user_email'] ?? $_POST['user_name'] ?? ''), $_POST['user_password'] ?? '')) {
+ header('Location: main.php', true, 303);
+ exit;
+ }
+ $params['INVALID_SIGNIN'] = 'TRUE';
}
+ echo $this->applyXSL(' ', $this->getXSL('login'), $params);
+ exit;
}
}
-
-?>
diff --git a/classes/SignUp.php b/classes/SignUp.php
index 8e59dfd..15368bd 100644
--- a/classes/SignUp.php
+++ b/classes/SignUp.php
@@ -1,103 +1,32 @@
applyXSL("", $this->getXSL("createAccount"), $XSLParms);
- exit();
-
- break;
-
- case "create":
-
- $user_name = trim(filter_input(INPUT_POST, "user_name") ?: "");
- $user_password = filter_input(INPUT_POST, "user_password") ?: "";
-
- $XSLParms["BTN_SUBMIT"] = "main.php?action=SignIn.login&step=authenticate";
- $XSLParms["INVALID_SIGNIN"] = "TRUE";
-
- $user = (new Users())->getUserByUserName($user_name);
-
- if ($user->count() == 0) {
- echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
- die();
+ $params = ['BTN_SUBMIT' => 'main.php', 'REGISTER_ACTION' => 'SignUp.createAccount'];
+ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
+ $this->requireCsrf();
+ try {
+ $auth = new AuthService();
+ if (!$auth->allowAttempt('signup-ip', $_SERVER['REMOTE_ADDR'] ?? '', 5, 3600)) {
+ throw new InvalidArgumentException('Too many account requests. Please try again later.');
}
-
- if ((string) $user->record->user_status !== 'active') {
- echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
- die();
+ $auth->register($_POST);
+ $_SESSION['signup_success'] = true;
+ header('Location: main.php?Xaction=signup', true, 303);
+ exit;
+ } catch (InvalidArgumentException $e) {
+ $params['SIGNUP_ERROR'] = $e->getMessage();
+ foreach (['user_first_name', 'user_last_name', 'user_email', 'user_name'] as $field) {
+ $params[strtoupper($field)] = $_POST[$field] ?? '';
}
-
- $stored_hashed_password = $user->record->user_password_hash;
-
- if (!password_verify($user_password, $stored_hashed_password)) {
-
- echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
- die();
- }
-
- session_regenerate_id(true);
-
- $_SESSION[self::USER_AUTHENTICATED] = true;
- $_SESSION[self::USER_SERIAL] = (int) $user->record->user_serial;
- $_SESSION[self::USER_NAME] = (string) $user->record->user_name;
- $_SESSION[self::USER_FULL_NAME] = (string) $user->record->user_full_name;
- $_SESSION[self::USER_EMAIL] = (string) $user->record->user_email;
- $_SESSION[self::USER_ROLE] = (string) $user->record->user_role;
-
- $this->current_user_serial = $_SESSION[self::USER_SERIAL] ?? 0;
- $this->current_user_name = $_SESSION[self::USER_NAME] ?? "";
- $this->current_user_email = $_SESSION[self::USER_EMAIL] ?? "";
-
- $system_admins = $this->getSettings()->xpath("//SystemAdministrator");
-
- foreach ($system_admins as $admin) {
- if ((string) $admin === $user_name) {
- $_SESSION[self::USER_ROLE] = "System Administrator";
- break;
- }
- }
-
- (new Journal())->journalEntry("{$_SESSION[self::USER_NAME]} logged in ({$_SESSION[self::USER_ROLE]}).");
-
- (new Users())->updateLogin();
-
- header("Location: ./");
- exit();
-
- break;
-
- default:
- // User NOT Validated.
- header("Location: ./");
- exit();
+ }
}
+ if (!empty($_SESSION['signup_success'])) {
+ $params['SIGNUP_SUCCESSFUL'] = 'TRUE';
+ unset($_SESSION['signup_success']);
+ }
+ echo $this->applyXSL(' ', $this->getXSL('createAccount'), $params);
+ exit;
}
}
-
-?>
diff --git a/classes/Users.php b/classes/Users.php
index 6525521..d107d95 100644
--- a/classes/Users.php
+++ b/classes/Users.php
@@ -25,6 +25,7 @@ class Users extends Base {
date_default_timezone_set(self::TIMEZONE);
+ $this->current_user_serial = (int) ($_SESSION[self::USER_SERIAL] ?? 0);
$this->current_user_name = $_SESSION[self::USER_NAME] ?? "";
$this->current_user_email = $_SESSION[self::USER_EMAIL] ?? "";
$this->pagination_page = $_SESSION[self::PAGINATION_PAGE] ?? 1;
@@ -100,9 +101,13 @@ class Users extends Base {
case "add":
$user_name = filter_input(INPUT_POST, "user_name") ?: "";
- $user_client_name = filter_input(INPUT_POST, "user_client_name") ?: "";
+ $user_first_name = filter_input(INPUT_POST, "user_first_name") ?: "";
$user_email = filter_input(INPUT_POST, "user_email") ?: "";
$user_role = filter_input(INPUT_POST, "user_role") ?: "";
+ if (!in_array($user_role, ['User', 'Guest', 'Administrator'], true) || !filter_var($user_email, FILTER_VALIDATE_EMAIL) || strlen($user_email) > 100 || !preg_match('/\A[A-Za-z0-9_.-]{3,64}\z/', $user_name)) {
+ http_response_code(422); exit('Enter a valid username, email address and role.');
+ }
+
$user_name = strtoupper(preg_replace("/\s+/", " ", $user_name));
@@ -110,31 +115,31 @@ class Users extends Base {
$user_temp_password = password_hash($user_random_password, PASSWORD_DEFAULT);
$user_change_password = (int) true;
- $user_active = (int) true;
+ $user_active = 'active';
// Add the User
$connection = $this->connect();
$SQL = "insert into users
-
+
( user_name,
user_email,
- user_client_name,
- user_password,
+ user_first_name,
+ user_password_hash,
user_role,
user_change_password,
- user_active,
+ user_status,
user_creator,
user_created )
values ( :user_name,
:user_email,
- :user_client_name,
- :user_password,
+ :user_first_name,
+ :user_password_hash,
:user_role,
:user_change_password,
- :user_active,
+ :user_status,
:user_creator,
:user_created )";
@@ -142,11 +147,11 @@ class Users extends Base {
$statement->bindValue(":user_name", $user_name);
$statement->bindValue(":user_email", $user_email);
- $statement->bindValue(":user_client_name", $user_client_name);
- $statement->bindValue(":user_password", $user_temp_password);
+ $statement->bindValue(":user_first_name", $user_first_name);
+ $statement->bindValue(":user_password_hash", $user_temp_password);
$statement->bindValue(":user_role", $user_role);
$statement->bindValue(":user_change_password", $user_change_password, PDO::PARAM_BOOL);
- $statement->bindValue(":user_active", $user_active, PDO::PARAM_BOOL);
+ $statement->bindValue(":user_status", $user_active);
$statement->bindValue(":user_creator", $this->current_user_name);
$statement->bindValue(":user_created", $this->getTimestamp());
@@ -158,7 +163,7 @@ class Users extends Base {
// Send User Credentials
- (new Email())->emailNewUserCredentials($user, $user_random_password);
+ (new AuthService())->requestReset($user_email);
break;
@@ -226,9 +231,13 @@ class Users extends Base {
$user_serial = filter_input(INPUT_POST, "user_serial", FILTER_SANITIZE_NUMBER_INT) ?: 0;
$user_name = filter_input(INPUT_POST, "user_name") ?: "";
$user_email = filter_input(INPUT_POST, "user_email") ?: "";
- $user_client_name = filter_input(INPUT_POST, "user_client_name") ?: "";
+ $user_first_name = filter_input(INPUT_POST, "user_first_name") ?: "";
$user_role = filter_input(INPUT_POST, "user_role") ?: "";
- $user_active = filter_input(INPUT_POST, "user_active", FILTER_VALIDATE_BOOLEAN) ?: false;
+ if (!in_array($user_role, ['User', 'Guest', 'Administrator'], true) || !filter_var($user_email, FILTER_VALIDATE_EMAIL) || strlen($user_email) > 100 || !preg_match('/\A[A-Za-z0-9_.-]{3,64}\z/', $user_name)) {
+ http_response_code(422); exit('Enter a valid username, email address and role.');
+ }
+
+ $user_active = filter_input(INPUT_POST, 'user_active', FILTER_VALIDATE_BOOLEAN) ? 'active' : 'inactive';
$users = $this->getUser($user_serial);
@@ -238,12 +247,13 @@ class Users extends Base {
// Update the User
- $SQL = "update users
+ $SQL = "update users
set user_name = :user_name,
user_email = :user_email,
- user_client_name = :user_client_name,
+ user_first_name = :user_first_name,
user_role = :user_role,
- user_active = :user_active,
+ user_auth_version = user_auth_version + 1,
+ user_status = :user_status,
user_changer = :user_changer,
user_changed = :user_changed
where user_serial = :user_serial";
@@ -253,9 +263,9 @@ class Users extends Base {
$statement->bindValue(":user_serial", $user_serial);
$statement->bindValue(":user_name", $user_name);
$statement->bindValue(":user_email", $user_email);
- $statement->bindValue(":user_client_name", $user_client_name);
+ $statement->bindValue(":user_first_name", $user_first_name);
$statement->bindValue(":user_role", $user_role);
- $statement->bindValue(":user_active", $user_active, PDO::PARAM_BOOL);
+ $statement->bindValue(":user_status", $user_active);
$statement->bindValue(":user_changer", $this->current_user_name);
$statement->bindValue(":user_changed", $this->getTimestamp());
@@ -274,6 +284,7 @@ class Users extends Base {
// ====================
public function getUser($user_serial = 0) {
+ $user_serial = (int) $user_serial;
$SQL = "select view_users.*
from view_users
@@ -287,12 +298,9 @@ class Users extends Base {
// ====================
public function getUserByUserName($user_name) {
-
- $SQL = "select view_users.*
- from view_users
- where user_name = '{$user_name}'";
-
- return $this->getTable("user", $SQL);
+ $stmt = $this->connect()->prepare('SELECT * FROM view_users WHERE user_name = :name');
+ $stmt->execute([':name' => $user_name]);
+ return $this->getTableXML('users', $stmt);
}
// =====================
@@ -337,14 +345,14 @@ class Users extends Base {
$SQL = "select count(*) as count
from view_users
where user_name regexp :user_name
- or user_client_name regexp :user_client_name
+ or user_first_name regexp :user_first_name
or user_email regexp :user_email
or user_role regexp :user_role";
$statement = $connection->prepare($SQL);
$statement->bindValue(":user_name", $find_words);
- $statement->bindValue(":user_client_name", $find_words);
+ $statement->bindValue(":user_first_name", $find_words);
$statement->bindValue(":user_email", $find_words);
$statement->bindValue(":user_role", $find_words);
@@ -379,7 +387,7 @@ class Users extends Base {
$SQL = "select view_users.*
from view_users
where user_name regexp :user_name
- or user_client_name regexp :user_client_name
+ or user_first_name regexp :user_first_name
or user_email regexp :user_email
or user_role regexp :user_role
limit {$this->pagination_size}
@@ -388,7 +396,7 @@ class Users extends Base {
$statement = $connection->prepare($SQL);
$statement->bindValue(":user_name", $find_words);
- $statement->bindValue(":user_client_name", $find_words);
+ $statement->bindValue(":user_first_name", $find_words);
$statement->bindValue(":user_email", $find_words);
$statement->bindValue(":user_role", $find_words);
@@ -485,111 +493,33 @@ class Users extends Base {
// ===============
public function changePassword() {
-
- $step = filter_input(INPUT_POST, "step") ?: "prompt";
-
- switch ($step) {
-
- case "prompt":
-
- $popovers = (new Popovers())->getPopovers();
- $XML = $this->mergeXML($popovers, " ");
-
- $content = $this->applyXSL($XML, $this->getXSL("changePassword"));
- $this->displayContent($content);
-
- break;
-
- case "change":
-
- $user_serial = $_SESSION[self::USER_SERIAL];
- $user_new_password = filter_input(INPUT_POST, "user_new_password") ?: "";
- $user_verify_password = filter_input(INPUT_POST, "user_verify_password") ?: "";
-
- if (!$user_new_password == $user_verify_password) {
- $this->initializeSession();
+ $params = ['PASSWORD_ERROR' => ''];
+ if (($_POST['step'] ?? '') === 'change') {
+ $this->requireCsrf();
+ try {
+ $auth = new AuthService();
+ if (!$auth->allowAttempt('change-password', (string) $_SESSION[self::USER_SERIAL], 10, 900)) {
+ throw new InvalidArgumentException('Too many attempts. Please try again later.');
}
-
- $user_new_hashed_password = password_hash($user_new_password, PASSWORD_DEFAULT);
-
- // Update User Password
-
- $connection = $this->connect();
-
- $SQL = "update users
- set user_password = :user_password,
- user_change_password = :user_change_password,
- user_changer = :user_changer,
- user_changed = :user_changed
- where user_serial = :user_serial";
-
- $statement = $connection->prepare($SQL);
-
- $statement->bindValue(":user_serial", $user_serial);
- $statement->bindValue(":user_password", $user_new_hashed_password);
- $statement->bindValue(":user_change_password", 0);
- $statement->bindValue(":user_changer", $this->current_user_name);
- $statement->bindValue(":user_changed", $this->getTimestamp());
-
- if (!$statement->execute()) {
- $this->debug($statement->errorInfo()[2]);
+ if (!$auth->changePassword((int) $_SESSION[self::USER_SERIAL], $_POST['user_current_password'] ?? '', $_POST['user_new_password'] ?? '', $_POST['user_verify_password'] ?? '')) {
+ throw new InvalidArgumentException('Your current password is incorrect or your session has expired.');
}
-
- (new Journal())->journalEntry("{$_SESSION[self::USER_NAME]} changed their password.");
-
- break;
-
- default:
-
- $this->displayHome();
+ $this->logoutSession();
+ header('Location: main.php', true, 303);
+ exit;
+ } catch (InvalidArgumentException $e) { $params['PASSWORD_ERROR'] = $e->getMessage(); }
}
+ $this->displayContent($this->applyXSL(' ', $this->getXSL('changePassword'), $params));
}
- // ==============
- // Reset Password
- // ==============
-
public function resetPassword() {
-
- $reset = false;
-
- $user_serial = filter_input(INPUT_POST, "user_serial") ?: "";
-
- $user = $this->getUser($user_serial);
-
- $user_name = $user->record->user_name;
-
- $user_random_password = $this->generateRandomPassword();
- $user_temp_password = password_hash($user_random_password, PASSWORD_DEFAULT);
-
- // Update User Password
-
- $connection = $this->connect();
-
- $SQL = "update users
- set user_password = :user_password,
- user_change_password = :user_change_password
- where user_serial = :user_serial";
-
- $statement = $connection->prepare($SQL);
-
- $statement->bindValue(":user_serial", $user_serial);
- $statement->bindValue(":user_password", $user_temp_password);
- $statement->bindValue(":user_change_password", 1);
-
- if (!$statement->execute()) {
- $this->debug($statement->errorInfo()[2]);
- } else {
- $reset = true;
- }
-
- // Send User Credentials
-
- (new Email())->emailUserPasswordResetLink($user, $user_random_password);
-
- (new Journal())->journalEntry("{$_SESSION[self::USER_NAME]} reset {$user_name}'s password.");
-
- echo json_encode($reset);
+ $this->requireCsrf();
+ $stmt = $this->connect()->prepare('SELECT user_email FROM users WHERE user_serial = :id');
+ $stmt->execute([':id' => (int) ($_POST['user_serial'] ?? 0)]);
+ $email = $stmt->fetchColumn();
+ if ($email) (new AuthService())->requestReset($email);
+ header('Content-Type: application/json');
+ echo json_encode(true);
}
// ===============================================
@@ -599,7 +529,7 @@ class Users extends Base {
public function updateLogin() {
$SQL = "update users
- set user_login = :user_login
+ set user_last_login = :user_login
where user_serial = :user_serial";
$statement = $this->connect()->prepare($SQL);
@@ -617,10 +547,11 @@ class Users extends Base {
public function updateActiveStatus() {
$user_serial = filter_input(INPUT_POST, "user_serial", FILTER_SANITIZE_NUMBER_INT) ?: 0;
- $user_active = filter_input(INPUT_POST, "user_active", FILTER_SANITIZE_NUMBER_INT) ?: 0;
+ $user_active = filter_input(INPUT_POST, 'user_active', FILTER_VALIDATE_BOOLEAN) ? 'active' : 'inactive';
$SQL = "update users
- set user_active = :user_active,
+ set user_status = :user_status,
+ user_auth_version = user_auth_version + 1,
user_changed = :user_changed,
user_changer = :user_changer
where user_serial = :user_serial";
@@ -628,7 +559,7 @@ class Users extends Base {
$statement = $this->connect()->prepare($SQL);
$statement->bindValue(":user_serial", $user_serial);
- $statement->bindValue(":user_active", $user_active);
+ $statement->bindValue(":user_status", $user_active);
$statement->bindValue(":user_changed", $this->getTimestamp());
$statement->bindValue(":user_changer", $this->current_user_name);
diff --git a/composer.json b/composer.json
index 70ebc1a..80f3586 100644
--- a/composer.json
+++ b/composer.json
@@ -1,7 +1,21 @@
{
"require": {
+ "php": "^8.3",
+ "php-64bit": "^8.3",
+ "ext-dom": "*",
+ "ext-filter": "*",
+ "ext-mbstring": "*",
+ "ext-openssl": "*",
+ "ext-pdo": "*",
+ "ext-pdo_mysql": "*",
+ "ext-session": "*",
+ "ext-simplexml": "*",
+ "ext-xsl": "*",
"tecnickcom/tcpdf": "^6.10",
"phpmailer/phpmailer": "^7.0",
"phpoffice/phpspreadsheet": "^5.2"
+ },
+ "scripts": {
+ "test:auth": "php tests/auth_integration.php"
}
}
diff --git a/composer.lock b/composer.lock
index 4ccbf57..5d53152 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,32 +4,33 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "6621356a0da1644f04f6f40bd0e0b948",
+ "content-hash": "0389de306dca31ea5873c1d9ed6a4c5d",
"packages": [
{
"name": "composer/pcre",
- "version": "3.3.2",
+ "version": "3.4.0",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
- "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
+ "reference": "d5a341b3fb61f3001970940afb1d332968a183ed"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
- "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+ "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed",
+ "reference": "d5a341b3fb61f3001970940afb1d332968a183ed",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"conflict": {
- "phpstan/phpstan": "<1.11.10"
+ "phpstan/phpstan": "<2.2.2"
},
"require-dev": {
- "phpstan/phpstan": "^1.12 || ^2",
- "phpstan/phpstan-strict-rules": "^1 || ^2",
- "phpunit/phpunit": "^8 || ^9"
+ "phpstan/phpstan": "^2",
+ "phpstan/phpstan-deprecation-rules": "^2",
+ "phpstan/phpstan-strict-rules": "^2",
+ "phpunit/phpunit": "^9"
},
"type": "library",
"extra": {
@@ -67,7 +68,7 @@
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
- "source": "https://github.com/composer/pcre/tree/3.3.2"
+ "source": "https://github.com/composer/pcre/tree/3.4.0"
},
"funding": [
{
@@ -77,26 +78,22 @@
{
"url": "https://github.com/composer",
"type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/composer/composer",
- "type": "tidelift"
}
],
- "time": "2024-11-12T16:29:46+00:00"
+ "time": "2026-06-07T11:47:49+00:00"
},
{
"name": "maennchen/zipstream-php",
- "version": "3.2.0",
+ "version": "3.2.2",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
- "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416"
+ "reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9712d8fa4cdf9240380b01eb4be55ad8dcf71416",
- "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416",
+ "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e",
+ "reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e",
"shasum": ""
},
"require": {
@@ -107,7 +104,7 @@
"require-dev": {
"brianium/paratest": "^7.7",
"ext-zip": "*",
- "friendsofphp/php-cs-fixer": "^3.16",
+ "friendsofphp/php-cs-fixer": "^3.86",
"guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
@@ -153,7 +150,7 @@
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
- "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.0"
+ "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.2"
},
"funding": [
{
@@ -161,7 +158,7 @@
"type": "github"
}
],
- "time": "2025-07-17T11:15:13+00:00"
+ "time": "2026-04-11T18:38:28+00:00"
},
{
"name": "markbaker/complex",
@@ -354,16 +351,16 @@
},
{
"name": "phpoffice/phpspreadsheet",
- "version": "5.2.0",
+ "version": "5.10.0",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
- "reference": "3b8994b3aac4b61018bc04fc8c441f4fd68c18eb"
+ "reference": "eb18727acf6b1f4cc67145a52ab04f9fd4c28d53"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/3b8994b3aac4b61018bc04fc8c441f4fd68c18eb",
- "reference": "3b8994b3aac4b61018bc04fc8c441f4fd68c18eb",
+ "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/eb18727acf6b1f4cc67145a52ab04f9fd4c28d53",
+ "reference": "eb18727acf6b1f4cc67145a52ab04f9fd4c28d53",
"shasum": ""
},
"require": {
@@ -371,6 +368,7 @@
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
+ "ext-filter": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-libxml": "*",
@@ -384,14 +382,14 @@
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
- "php": "^8.1",
- "psr/http-client": "^1.0",
- "psr/http-factory": "^1.0",
+ "php": "^8.2",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^2.0 || ^3.0",
+ "ext-intl": "*",
+ "ext-openssl": "*",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.5",
"mpdf/mpdf": "^8.1.1",
@@ -399,13 +397,14 @@
"phpstan/phpstan": "^1.1 || ^2.0",
"phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0",
"phpstan/phpstan-phpunit": "^1.0 || ^2.0",
- "phpunit/phpunit": "^10.5",
+ "phpunit/phpunit": "^10.5 || ^11.0",
"squizlabs/php_codesniffer": "^3.7",
- "tecnickcom/tcpdf": "^6.5"
+ "tecnickcom/tcpdf": ">=6.8.0 <7.0.0"
},
"suggest": {
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
- "ext-intl": "PHP Internationalization Functions, regquired for NumberFormat Wizard",
+ "ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard and StringHelper::setLocale()",
+ "ext-openssl": "Handline Agile-encrypted Xlsx spreadsheets",
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
@@ -438,6 +437,9 @@
},
{
"name": "Adrien Crivelli"
+ },
+ {
+ "name": "Owen Leibman"
}
],
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
@@ -454,169 +456,9 @@
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
- "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.2.0"
+ "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.10.0"
},
- "time": "2025-10-26T15:54:22+00:00"
- },
- {
- "name": "psr/http-client",
- "version": "1.0.3",
- "source": {
- "type": "git",
- "url": "https://github.com/php-fig/http-client.git",
- "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
- "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
- "shasum": ""
- },
- "require": {
- "php": "^7.0 || ^8.0",
- "psr/http-message": "^1.0 || ^2.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Psr\\Http\\Client\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "PHP-FIG",
- "homepage": "https://www.php-fig.org/"
- }
- ],
- "description": "Common interface for HTTP clients",
- "homepage": "https://github.com/php-fig/http-client",
- "keywords": [
- "http",
- "http-client",
- "psr",
- "psr-18"
- ],
- "support": {
- "source": "https://github.com/php-fig/http-client"
- },
- "time": "2023-09-23T14:17:50+00:00"
- },
- {
- "name": "psr/http-factory",
- "version": "1.1.0",
- "source": {
- "type": "git",
- "url": "https://github.com/php-fig/http-factory.git",
- "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
- "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
- "shasum": ""
- },
- "require": {
- "php": ">=7.1",
- "psr/http-message": "^1.0 || ^2.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Psr\\Http\\Message\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "PHP-FIG",
- "homepage": "https://www.php-fig.org/"
- }
- ],
- "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
- "keywords": [
- "factory",
- "http",
- "message",
- "psr",
- "psr-17",
- "psr-7",
- "request",
- "response"
- ],
- "support": {
- "source": "https://github.com/php-fig/http-factory"
- },
- "time": "2024-04-15T12:06:14+00:00"
- },
- {
- "name": "psr/http-message",
- "version": "2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/php-fig/http-message.git",
- "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
- "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
- "shasum": ""
- },
- "require": {
- "php": "^7.2 || ^8.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.0.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Psr\\Http\\Message\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "PHP-FIG",
- "homepage": "https://www.php-fig.org/"
- }
- ],
- "description": "Common interface for HTTP messages",
- "homepage": "https://github.com/php-fig/http-message",
- "keywords": [
- "http",
- "http-message",
- "psr",
- "psr-7",
- "request",
- "response"
- ],
- "support": {
- "source": "https://github.com/php-fig/http-message/tree/2.0"
- },
- "time": "2023-04-04T09:54:51+00:00"
+ "time": "2026-09-17T03:54:50+00:00"
},
{
"name": "psr/simple-cache",
@@ -747,7 +589,19 @@
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
- "platform": {},
+ "platform": {
+ "php": "^8.3",
+ "php-64bit": "^8.3",
+ "ext-dom": "*",
+ "ext-filter": "*",
+ "ext-mbstring": "*",
+ "ext-openssl": "*",
+ "ext-pdo": "*",
+ "ext-pdo_mysql": "*",
+ "ext-session": "*",
+ "ext-simplexml": "*",
+ "ext-xsl": "*"
+ },
"platform-dev": {},
- "plugin-api-version": "2.6.0"
+ "plugin-api-version": "2.9.0"
}
diff --git a/css/signin.css b/css/login.css
similarity index 100%
rename from css/signin.css
rename to css/login.css
diff --git a/js/addUser.js b/js/addUser.js
index a8b6dc4..6b96d16 100644
--- a/js/addUser.js
+++ b/js/addUser.js
@@ -83,7 +83,7 @@ $(document).ready(function () {
$("#addUser").submit(function (e) {
var user_name = $("#user_name"),
- user_client_name = $("#user_client_name"),
+ user_first_name = $("#user_first_name"),
user_email = $("#user_email"),
user_role = $("#user_role");
@@ -108,8 +108,8 @@ $(document).ready(function () {
return false;
}
- if (isBlank(user_client_name)) {
- createInputError(user_client_name, "Client Name is required");
+ if (isBlank(user_first_name)) {
+ createInputError(user_first_name, "Client Name is required");
return false;
}
diff --git a/js/base.js b/js/base.js
index 5410f3f..1285659 100644
--- a/js/base.js
+++ b/js/base.js
@@ -1,3 +1,17 @@
+// Attach the session CSRF token to existing forms and same-origin AJAX requests.
+$.ajaxPrefilter(function (options, original, xhr) {
+ if (!options.crossDomain && !/^(GET|HEAD|OPTIONS)$/i.test(options.type)) {
+ xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content') || '');
+ }
+});
+$(function () {
+ $('form[method="post" i]').each(function () {
+ if (!$(this).find('[name="csrf_token"]').length) {
+ $('', {type: 'hidden', name: 'csrf_token', value: $('meta[name="csrf-token"]').attr('content') || ''}).appendTo(this);
+ }
+ });
+});
+
// ==============
// Base Functions
diff --git a/js/editUser.js b/js/editUser.js
index 0b1b021..8151731 100644
--- a/js/editUser.js
+++ b/js/editUser.js
@@ -97,7 +97,7 @@ $(document).ready(function () {
var user_name = $("#user_name"),
user_email = $("#user_email"),
user_role = $("#user_role"),
- user_client_name = $("#user_client_name");
+ user_first_name = $("#user_first_name");
clearInputErrors();
@@ -114,8 +114,8 @@ $(document).ready(function () {
return false;
}
- if (isBlank(user_client_name)) {
- createInputError(user_client_name, "Client Name is required");
+ if (isBlank(user_first_name)) {
+ createInputError(user_first_name, "Client Name is required");
return false;
}
diff --git a/js/login.js b/js/login.js
index e009847..8f9b3fb 100644
--- a/js/login.js
+++ b/js/login.js
@@ -11,7 +11,7 @@ $(document).ready(function () {
// Use our Bootstrap validation messages.
$login.add($signUp).add($resetPassword).attr("novalidate", "novalidate");
- $(".btnSignUp").on("click", function (e) {
+ $("#login .btnSignUp").on("click", function (e) {
e.preventDefault();
window.location.href = "./?Xaction=signup";
});
diff --git a/main.php b/main.php
index cd08087..c86bffc 100644
--- a/main.php
+++ b/main.php
@@ -1,8 +1,29 @@
getSettings()->Environment === 'Production' && !$https) {
+ http_response_code(400);
+ exit('HTTPS is required. Open the secure application URL.');
+}
+ini_set('session.use_strict_mode', '1');
+ini_set('session.use_only_cookies', '1');
+session_name('HUNTINGCLUBROSTER');
+session_set_cookie_params([
+ 'lifetime' => 0, 'path' => '/',
+ 'secure' => $https,
+ 'httponly' => true, 'samesite' => 'Lax',
+]);
session_start();
-
-(new Base())->do();
-?>
\ No newline at end of file
+$application->do();
diff --git a/migrations/001_auth.sql b/migrations/001_auth.sql
new file mode 100644
index 0000000..5311f29
--- /dev/null
+++ b/migrations/001_auth.sql
@@ -0,0 +1,18 @@
+-- Run once on the supplied users schema. Take a database backup first.
+ALTER TABLE users ADD COLUMN user_auth_version INT UNSIGNED NOT NULL DEFAULT 0;
+ALTER TABLE users ENGINE=InnoDB;
+
+CREATE TABLE IF NOT EXISTS password_resets (
+ token_hash CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL PRIMARY KEY,
+ user_serial BIGINT UNSIGNED NOT NULL,
+ expires_at DATETIME NOT NULL,
+ INDEX (user_serial),
+ INDEX (expires_at)
+) ENGINE=InnoDB;
+
+CREATE TABLE IF NOT EXISTS auth_attempts (
+ attempt_key CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL PRIMARY KEY,
+ attempts INT UNSIGNED NOT NULL DEFAULT 0,
+ window_started DATETIME NOT NULL,
+ INDEX (window_started)
+) ENGINE=InnoDB;
diff --git a/reports/Print_All_PermitsByBuilder.php b/reports/Print_All_PermitsByBuilder.php
index a24e3de..111bddd 100644
--- a/reports/Print_All_PermitsByBuilder.php
+++ b/reports/Print_All_PermitsByBuilder.php
@@ -1,6 +1,6 @@
-
-
-
- Create System Tables
-
-
-
-
-
- 72) throw new RuntimeException('Set HCR_INITIAL_ADMIN_PASSWORD before rebuilding.');
+
// --------------------------
// Load Application Settings
// --------------------------
@@ -247,7 +217,9 @@
function seedInitialAdminUser(PDO $connection) {
- $password = password_hash("349Jamot@", PASSWORD_DEFAULT);
+ $initialPassword = getenv('HCR_INITIAL_ADMIN_PASSWORD');
+ if (!$initialPassword || strlen($initialPassword) < 12 || strlen($initialPassword) > 72) throw new RuntimeException('Set HCR_INITIAL_ADMIN_PASSWORD to a unique 12-72 byte password.');
+ $password = password_hash($initialPassword, PASSWORD_DEFAULT);
$SQL = "INSERT INTO users (
user_name,
diff --git a/tables/_createTable.php b/tables/_createTable.php
index b904a49..86b7242 100644
--- a/tables/_createTable.php
+++ b/tables/_createTable.php
@@ -1,14 +1,6 @@
-
-
diff --git a/tests/auth_http.py b/tests/auth_http.py
new file mode 100644
index 0000000..3b75943
--- /dev/null
+++ b/tests/auth_http.py
@@ -0,0 +1,127 @@
+"""End-to-end tests against a disposable local database and PHP development server.
+
+Set HCR_TEST_DSN, HCR_TEST_USER, HCR_TEST_PASSWORD, HCR_DB_HOST,
+HCR_DB_PORT, HCR_DB_USER, HCR_DB_PASSWORD and PHP_TEST_COMMAND (JSON argv).
+"""
+import http.cookiejar
+import json
+import os
+from pathlib import Path
+import re
+import socket
+import subprocess
+import tempfile
+import time
+import urllib.error
+import urllib.parse
+import urllib.request
+
+ROOT = Path(__file__).resolve().parents[1]
+PHP = json.loads(os.environ.get('PHP_TEST_COMMAND', '["php"]'))
+env = os.environ.copy()
+env['HCR_APPLICATION_URL'] = '' # Never send real email from the test suite.
+env['HCR_ENVIRONMENT'] = 'Development'
+checks = []
+
+def fixture(action, database=None):
+ args = PHP + ['tests/http_fixture.php', action] + ([database] if database else [])
+ result = subprocess.run(args, cwd=ROOT, env=env, check=True, text=True, capture_output=True)
+ return json.loads(result.stdout.splitlines()[-1]) if result.stdout.strip() else {}
+
+def check(condition, message):
+ if not condition:
+ raise AssertionError(message)
+ checks.append(message)
+
+jar = http.cookiejar.CookieJar()
+client = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
+
+def request(path='/', data=None, headers=None):
+ encoded = urllib.parse.urlencode(data).encode() if data is not None else None
+ req = urllib.request.Request(base + path, data=encoded, headers=headers or {})
+ try:
+ with client.open(req, timeout=20) as response:
+ return response.status, response.read().decode(), response.headers
+ except urllib.error.HTTPError as error:
+ return error.code, error.read().decode(), error.headers
+
+def csrf(html):
+ match = re.search(r'name="csrf_token"[^>]*value="([a-f0-9]+)"', html)
+ if not match:
+ match = re.search(r'name="csrf-token"[^>]*content="([a-f0-9]+)"', html)
+ if not match:
+ raise AssertionError('Missing CSRF token')
+ return match.group(1)
+
+database = fixture('create')['database']
+env['HCR_DB_NAME'] = database
+with socket.socket() as available:
+ available.bind(('127.0.0.1', 0))
+ port = available.getsockname()[1]
+base = f'http://127.0.0.1:{port}'
+server = None
+try:
+ result = subprocess.run(PHP + ['bin/migrate-auth.php'], cwd=ROOT, env=env, check=True, capture_output=True, text=True)
+ check('schema is ready' in result.stdout, 'Database migration can be rerun safely')
+ with tempfile.TemporaryFile(mode='w+b') as log:
+ server = subprocess.Popen(PHP + ['-S', f'127.0.0.1:{port}', 'bin/dev-router.php'], cwd=ROOT, env=env, stdout=log, stderr=log)
+ for _ in range(50):
+ try:
+ status, html, headers = request()
+ break
+ except urllib.error.URLError:
+ time.sleep(0.1)
+ else:
+ raise AssertionError('PHP development server did not start')
+ check(status == 200 and 'name="user_email"' in html, 'Login page renders over HTTP')
+ check('no-store' in headers.get('Cache-Control', ''), 'Authentication pages are not cached')
+ check('HttpOnly' in headers.get('Set-Cookie', '') and 'SameSite=Lax' in headers.get('Set-Cookie', ''), 'Session cookies have HttpOnly and SameSite protections')
+ check(request(data={'action': 'SignIn.login', 'user_email': 'http@example.test', 'user_password': 'wrong'})[0] == 403, 'Login rejects missing CSRF token')
+ check(request(data={'action[]': 'bad'})[0] == 400, 'Array-shaped request fields are rejected')
+ status, html, _ = request('/?Xaction=signup')
+ check(status == 200 and 'name="user_first_name"' in html, 'Signup is reachable before login')
+ fields = {'action': 'SignUp.createAccount', 'csrf_token': csrf(html), 'user_first_name': 'HTTP', 'user_last_name': 'Tester', 'user_email': 'http@example.test', 'user_name': 'httpuser', 'user_password': 'HTTP long password!', 'user_password_confirm': 'HTTP long password!', 'user_role': 'Administrator'}
+ status, html, _ = request(data=fields)
+ check(status == 200 and 'Your account has been created' in html, 'Signup form creates an account and redirects')
+ status, html, _ = request()
+ status, html, _ = request(data={'action': 'SignIn.login', 'csrf_token': csrf(html), 'user_email': 'http@example.test', 'user_password': 'wrong'})
+ check(status == 200 and 'Invalid email address or password' in html, 'Invalid login renders the existing login template')
+ status, html, _ = request(data={'action': 'SignIn.login', 'csrf_token': csrf(html), 'user_email': 'http@example.test', 'user_password': 'HTTP long password!'})
+ check(status == 200 and 'name="csrf-token"' in html, 'Email/password form signs in and reaches the authenticated shell')
+ token = csrf(html)
+ check(request(data={'action': 'Users.editUser', 'step': 'update', 'csrf_token': token, 'user_serial': '1', 'user_role': 'Administrator'})[0] == 403, 'Regular users cannot edit account privileges')
+ check(request(data={'action': 'print', 'report': 'Email', 'csrf_token': token})[0] == 403, 'Regular users cannot invoke administrative report actions')
+ check(request(data={'action': 'Base.executeSQL', 'csrf_token': token})[0] == 404, 'Arbitrary helper methods cannot be dispatched')
+ check(request(data={'action': 'signout'})[0] == 403, 'Logout rejects missing CSRF token')
+ status, html, _ = request(data={'action': 'signout', 'csrf_token': token})
+ check(status == 200 and 'id="login"' in html, 'Logout ends the authenticated session')
+ status, html, _ = request('/?Xaction=resetpassword')
+ check('Send reset link' in html, 'Password reset request page is reachable before login')
+ status, html, _ = request(data={'action': 'resetpassword', 'csrf_token': csrf(html), 'user_email': 'unknown@example.test'})
+ check(status == 200 and 'If an active account matches' in html, 'Unknown account reset returns a neutral response')
+ status, form, _ = request('/?Xaction=resetpassword')
+ status, html, _ = request(data={'action': 'resetpassword', 'csrf_token': csrf(form), 'user_email': 'http@example.test'})
+ check('If an active account matches' in html, 'Reset delivery/configuration failure returns the same neutral response')
+ reset_token = fixture('token', database)['token']
+ status, html, headers = request('/?Xaction=resetpassword&token=' + reset_token)
+ check(status == 200 and 'Set new password' in html and headers.get('Referrer-Policy') == 'no-referrer', 'Reset link renders a protected new-password form')
+ status, html, _ = request(data={'action': 'resetpassword', 'step': 'reset', 'token': reset_token, 'csrf_token': csrf(html), 'user_password': 'HTTP changed password!', 'user_password_confirm': 'HTTP changed password!'})
+ check(status == 200 and 'Your password has been changed' in html, 'Reset form changes the password and redirects')
+ status, html, _ = request('/?Xaction=resetpassword&token=' + reset_token)
+ status, html, _ = request(data={'action': 'resetpassword', 'step': 'reset', 'token': reset_token, 'csrf_token': csrf(html), 'user_password': 'HTTP changed password!', 'user_password_confirm': 'HTTP changed password!'})
+ check('invalid or expired' in html, 'Used reset link fails through the real HTTP handler')
+ status, html, _ = request()
+ status, html, _ = request(data={'action': 'SignIn.login', 'csrf_token': csrf(html), 'user_email': 'http@example.test', 'user_password': 'HTTP changed password!'})
+ check(status == 200 and 'name="csrf-token"' in html, 'Reset password works through the real login form')
+ fixture('revoke', database)
+ check(request(data={'action': 'toggleTheme', 'csrf_token': csrf(html)}, headers={'X-Requested-With': 'XMLHttpRequest'})[0] == 401, 'Revoked AJAX sessions return 401 instead of a login document')
+ for path in ['/xml/settings.local.xml', '/tables/_createSystemTable.php', '/bin/migrate-auth.php', '/.git/config']:
+ check(request(path)[0] == 404, 'Development router blocks ' + path)
+ for message in checks:
+ print('PASS:', message)
+ print(len(checks), 'HTTP checks passed.')
+finally:
+ if server:
+ server.terminate()
+ server.wait(timeout=10)
+ fixture('drop', database)
diff --git a/tests/auth_integration.php b/tests/auth_integration.php
new file mode 100644
index 0000000..ec904e6
--- /dev/null
+++ b/tests/auth_integration.php
@@ -0,0 +1,134 @@
+ PDO::ERRMODE_EXCEPTION]);
+$database = 'hcr_auth_test_' . bin2hex(random_bytes(6));
+$server->exec("CREATE DATABASE `{$database}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
+$server->exec("USE `{$database}`");
+$server->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
+$server->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
+session_start();
+
+class TestAuth extends AuthService {
+ public array $mailbox = [];
+ public bool $mailWorks = true;
+ public function __construct(private PDO $db) { parent::__construct(); }
+ public function connect(): PDO { return $this->db; }
+ public function getSettings() { return new SimpleXMLElement('https://roster.example.test/club '); }
+ protected function deliverResetEmail(array $user, string $url): bool {
+ $this->mailbox[] = ['email' => $user['user_email'], 'url' => $url];
+ return $this->mailWorks;
+ }
+}
+class CaptureMailer extends Email {
+ public array $message = [];
+ public function sendEmail($to = '', $subject = '', $message = '', $attachments = '', $attachment_names = 'unknown') {
+ $this->message = ['to' => $to, 'subject' => $subject, 'body' => $message];
+ return true;
+ }
+}
+$checks = [];
+function check(bool $condition, string $message): void {
+ global $checks;
+ if (!$condition) throw new RuntimeException('FAILED: ' . $message);
+ $checks[] = $message;
+}
+function rejects(callable $operation, string $message): void {
+ try { $operation(); } catch (InvalidArgumentException $e) { check(true, $message); return; }
+ check(false, $message);
+}
+function tokenFrom(array $email): string {
+ parse_str(parse_url($email['url'], PHP_URL_QUERY), $query);
+ return $query['token'];
+}
+try {
+ require __DIR__ . '/../tables/users.php';
+ $server->exec($SQL);
+ // Exercise the upgrade path from the supplied pre-change schema too.
+ $server->exec('ALTER TABLE users DROP COLUMN user_auth_version');
+ foreach (explode(';', file_get_contents(__DIR__ . '/../migrations/001_auth.sql')) as $sql) {
+ if (trim($sql)) $server->exec($sql);
+ }
+ require __DIR__ . '/../views/view_users.php';
+ $server->exec($SQL);
+ $auth = new TestAuth($server);
+ $auth->initializeSession();
+ $input = ['user_first_name' => 'Jane', 'user_last_name' => 'Hunter', 'user_email' => 'jane@example.test', 'user_name' => 'jane', 'user_password' => 'A long password 123!', 'user_password_confirm' => 'A long password 123!', 'user_role' => 'Administrator'];
+ $auth->register($input);
+ $user = $auth->findUser('jane@example.test');
+ $id = (int) $user['user_serial'];
+ check($user['user_role'] === 'User', 'Signup cannot assign administrator privileges');
+ check(password_verify($input['user_password'], $user['user_password_hash']), 'Signup stores a verifiable password hash');
+ check($user['user_password_hash'] !== $input['user_password'], 'Signup does not store plaintext passwords');
+ rejects(fn() => $auth->register($input), 'Duplicate account details are rejected');
+ rejects(fn() => $auth->register(array_replace($input, ['user_email' => 'invalid'])), 'Invalid email is rejected on the server');
+ rejects(fn() => $auth->register(array_replace($input, ['user_password_confirm' => 'different'])), 'Mismatched passwords are rejected on the server');
+ rejects(fn() => $auth->register(array_replace($input, ['user_password' => 'short', 'user_password_confirm' => 'short'])), 'Short passwords are rejected');
+ rejects(fn() => $auth->register(array_replace($input, ['user_password' => str_repeat('x', 73), 'user_password_confirm' => str_repeat('x', 73)])), 'Passwords above bcrypt byte limit are rejected');
+ check(!$auth->login("' OR 1=1 -- ", 'wrong'), 'SQL injection input cannot log in');
+ check(!$auth->login('jane@example.test', 'wrong'), 'Wrong password cannot log in');
+ $oldSession = session_id();
+ check($auth->login('jane@example.test', $input['user_password']), 'Email login succeeds');
+ check(session_id() !== $oldSession, 'Login rotates session ID');
+ check($auth->currentUser() !== null, 'Authenticated session is valid');
+ check($server->query('SELECT user_last_login FROM users')->fetchColumn() !== null, 'Login timestamp uses the correct column');
+ check($auth->login('jane', $input['user_password']), 'Username login remains supported');
+ $before = $auth->findUser('jane')['user_password_hash'];
+ $auth->requestReset('jane@example.test');
+ check(count($auth->mailbox) === 1, 'Reset request generates an email');
+ check($auth->findUser('jane')['user_password_hash'] === $before, 'Reset request leaves password unchanged');
+ $token = tokenFrom($auth->mailbox[0]);
+ check(strlen($token) === 64, 'Reset token contains 256 bits of randomness');
+ check($server->query('SELECT token_hash FROM password_resets')->fetchColumn() === hash('sha256', $token), 'Database stores only a hash of the reset token');
+ $auth->requestReset('missing@example.test');
+ check(count($auth->mailbox) === 1, 'Unknown accounts do not produce reset email');
+ check(!$auth->resetPassword(str_repeat('0', 64), 'Another long password!', 'Another long password!'), 'Unknown reset token is rejected');
+ $server->exec('UPDATE password_resets SET expires_at = DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 SECOND)');
+ check(!$auth->resetPassword($token, 'Another long password!', 'Another long password!'), 'Expired reset token is rejected');
+ $auth->requestReset('jane@example.test');
+ $token = tokenFrom($auth->mailbox[1]);
+ check($auth->resetPassword($token, 'Another long password!', 'Another long password!'), 'Valid reset token changes password');
+ check(!$auth->resetPassword($token, 'Another long password!', 'Another long password!'), 'Reset token cannot be reused');
+ check($auth->currentUser() === null, 'Password reset revokes an existing session');
+ check(!$auth->login('jane', $input['user_password']), 'Old password fails after reset');
+ check($auth->login('jane', 'Another long password!'), 'New password works after reset');
+ rejects(fn() => $auth->changePassword($id, 'Another long password!', 'New password 123!', 'mismatch'), 'Password change rejects mismatched confirmation');
+ check(!$auth->changePassword($id, 'wrong', 'New password 123!', 'New password 123!'), 'Password change requires the current password');
+ check($auth->changePassword($id, 'Another long password!', 'New password 123!', 'New password 123!'), 'Authenticated password change succeeds');
+ check($auth->currentUser() === null, 'Password change revokes previous sessions');
+ check($auth->login('jane', 'New password 123!'), 'Changed password works');
+ $_SESSION['last_activity'] = time() - 1801;
+ check($auth->currentUser() === null, 'Idle sessions expire on the server');
+ $server->exec("UPDATE users SET user_status = 'inactive'");
+ check(!$auth->login('jane', 'New password 123!'), 'Inactive users cannot log in');
+ $server->exec("UPDATE users SET user_status = 'active'");
+ $auth->mailWorks = false;
+ $beforeFailedEmail = $auth->findUser('jane')['user_password_hash'];
+ try { $auth->requestReset('jane@example.test'); check(false, 'Mail failure must be detected'); }
+ catch (RuntimeException $e) { check($e->getMessage() === 'Password reset email delivery failed.', 'Email failure is detected'); }
+ check((int) $server->query('SELECT COUNT(*) FROM password_resets')->fetchColumn() === 0, 'Failed email delivery removes its token');
+ check($auth->findUser('jane')['user_password_hash'] === $beforeFailedEmail, 'Email failure preserves the existing password');
+ $mailer = new CaptureMailer();
+ $mailer->sendPasswordReset(['user_name' => 'jane', 'user_first_name' => 'Jane & club', 'user_email' => 'jane@example.test'], 'https://app.myhuntingclubroster.com/main.php?Xaction=resetpassword&token=example');
+ check($mailer->message['to'] === 'jane@example.test', 'Reset email uses the stored account email');
+ check(str_contains($mailer->message['body'], 'https://app.myhuntingclubroster.com/main.php'), 'Reset email contains the configured application link');
+ check(str_contains($mailer->message['body'], '<b>Jane & club</b>'), 'Reset email escapes account names');
+ check(!str_contains($mailer->message['body'], 'New password 123!'), 'Reset email never includes an account password');
+ for ($i = 0; $i < 3; $i++) $allowed = $auth->allowAttempt('test', 'address', 2, 60);
+ check(!$allowed, 'Rate limits reject excessive attempts');
+ $server->exec('UPDATE auth_attempts SET window_started = DATE_SUB(UTC_TIMESTAMP(), INTERVAL 2 HOUR)');
+ check($auth->allowAttempt('test', 'address', 2, 60), 'Rate limits recover after their window');
+ $freshAuth = new TestAuth($server);
+ for ($i = 0; $i < 11; $i++) $allowed = $freshAuth->login('jane', 'wrong');
+ check(!$freshAuth->login('jane', 'New password 123!'), 'Login rate limits survive new service instances');
+ foreach (['login', 'createAccount', 'resetPassword', 'changePassword', 'content'] as $template) {
+ $html = $auth->applyXSL(' ', $auth->getXSL($template));
+ check(is_string($html) && strlen($html) > 100, $template . ' stylesheet renders');
+ }
+ foreach ($checks as $message) echo "PASS: {$message}\n";
+ echo count($checks) . " checks passed.\n";
+} finally {
+ $server->exec("DROP DATABASE `{$database}`");
+}
diff --git a/tests/http_fixture.php b/tests/http_fixture.php
new file mode 100644
index 0000000..8095cb9
--- /dev/null
+++ b/tests/http_fixture.php
@@ -0,0 +1,33 @@
+ PDO::ERRMODE_EXCEPTION]);
+$action = $argv[1] ?? '';
+$name = $argv[2] ?? ('hcr_http_test_' . bin2hex(random_bytes(6)));
+if (!preg_match('/\Ahcr_http_test_[a-f0-9]{12}\z/', $name)) throw new RuntimeException('Only generated test databases may be used.');
+if ($action === 'drop') { $db->exec("DROP DATABASE `{$name}`"); exit; }
+if ($action === 'create') {
+ $db->exec("CREATE DATABASE `{$name}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
+ $db->exec("USE `{$name}`");
+ require __DIR__ . '/../tables/users.php'; $db->exec($SQL);
+ // The CLI migration must also support being run against a fresh users table.
+ putenv('HCR_DB_NAME=' . $name);
+ require __DIR__ . '/../bin/migrate-auth.php';
+ // Idempotent migration is verified separately by the HTTP test runner.
+ echo json_encode(['database' => $name]);
+ exit;
+}
+$db->exec("USE `{$name}`");
+if ($action === 'revoke') {
+ $db->exec("UPDATE users SET user_auth_version = user_auth_version + 1 WHERE user_name = 'httpuser'");
+ exit;
+}
+if ($action === 'token') {
+ $id = $db->query("SELECT user_serial FROM users WHERE user_name = 'httpuser'")->fetchColumn();
+ $token = bin2hex(random_bytes(32));
+ $stmt = $db->prepare('INSERT INTO password_resets (token_hash, user_serial, expires_at) VALUES (?, ?, DATE_ADD(UTC_TIMESTAMP(), INTERVAL 30 MINUTE))');
+ $stmt->execute([hash('sha256', $token), $id]);
+ echo json_encode(['token' => $token]);
+}
diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php
index 8d3f8ac..2052022 100644
--- a/vendor/composer/InstalledVersions.php
+++ b/vendor/composer/InstalledVersions.php
@@ -277,7 +277,7 @@ class InstalledVersions
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' && is_file(__DIR__ . '/installed.php')) {
+ if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
@@ -378,7 +378,7 @@ class InstalledVersions
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' && is_file(__DIR__ . '/installed.php')) {
+ 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;
diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE
index 62ecfd8..f27399a 100644
--- a/vendor/composer/LICENSE
+++ b/vendor/composer/LICENSE
@@ -1,3 +1,4 @@
+
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -17,3 +18,4 @@ 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_psr4.php b/vendor/composer/autoload_psr4.php
index 28c70e9..7d6469c 100644
--- a/vendor/composer/autoload_psr4.php
+++ b/vendor/composer/autoload_psr4.php
@@ -8,25 +8,9 @@ $baseDir = dirname($vendorDir);
return array(
'ZipStream\\' => array($vendorDir . '/maennchen/zipstream-php/src'),
'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
- 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
- 'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
'PhpOffice\\PhpSpreadsheet\\' => array($vendorDir . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet'),
'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'),
'Matrix\\' => array($vendorDir . '/markbaker/matrix/classes/src'),
'Composer\\Pcre\\' => array($vendorDir . '/composer/pcre/src'),
'Complex\\' => array($vendorDir . '/markbaker/complex/classes/src'),
- 'Com\\Tecnick\\Unicode\\Data\\' => array($vendorDir . '/tecnickcom/tc-lib-unicode-data/src'),
- 'Com\\Tecnick\\Unicode\\' => array($vendorDir . '/tecnickcom/tc-lib-unicode/src'),
- 'Com\\Tecnick\\Pdf\\Sign\\' => array($vendorDir . '/tecnickcom/tc-lib-pdf-sign/src'),
- 'Com\\Tecnick\\Pdf\\Parser\\' => array($vendorDir . '/tecnickcom/tc-lib-pdf-parser/src'),
- 'Com\\Tecnick\\Pdf\\Page\\' => array($vendorDir . '/tecnickcom/tc-lib-pdf-page/src'),
- 'Com\\Tecnick\\Pdf\\Image\\' => array($vendorDir . '/tecnickcom/tc-lib-pdf-image/src'),
- 'Com\\Tecnick\\Pdf\\Graph\\' => array($vendorDir . '/tecnickcom/tc-lib-pdf-graph/src'),
- 'Com\\Tecnick\\Pdf\\Font\\' => array($vendorDir . '/tecnickcom/tc-lib-pdf-font/src'),
- 'Com\\Tecnick\\Pdf\\Filter\\' => array($vendorDir . '/tecnickcom/tc-lib-pdf-filter/src'),
- 'Com\\Tecnick\\Pdf\\Encrypt\\' => array($vendorDir . '/tecnickcom/tc-lib-pdf-encrypt/src'),
- 'Com\\Tecnick\\Pdf\\' => array($vendorDir . '/tecnickcom/tc-lib-pdf/src'),
- 'Com\\Tecnick\\File\\' => array($vendorDir . '/tecnickcom/tc-lib-file/src'),
- 'Com\\Tecnick\\Color\\' => array($vendorDir . '/tecnickcom/tc-lib-color/src'),
- 'Com\\Tecnick\\Barcode\\' => array($vendorDir . '/tecnickcom/tc-lib-barcode/src'),
);
diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php
index 6078577..1f49c16 100644
--- a/vendor/composer/autoload_static.php
+++ b/vendor/composer/autoload_static.php
@@ -14,8 +14,6 @@ class ComposerStaticInit6621356a0da1644f04f6f40bd0e0b948
'P' =>
array (
'Psr\\SimpleCache\\' => 16,
- 'Psr\\Http\\Message\\' => 17,
- 'Psr\\Http\\Client\\' => 16,
'PhpOffice\\PhpSpreadsheet\\' => 25,
'PHPMailer\\PHPMailer\\' => 20,
),
@@ -27,20 +25,6 @@ class ComposerStaticInit6621356a0da1644f04f6f40bd0e0b948
array (
'Composer\\Pcre\\' => 14,
'Complex\\' => 8,
- 'Com\\Tecnick\\Unicode\\Data\\' => 25,
- 'Com\\Tecnick\\Unicode\\' => 20,
- 'Com\\Tecnick\\Pdf\\Sign\\' => 21,
- 'Com\\Tecnick\\Pdf\\Parser\\' => 23,
- 'Com\\Tecnick\\Pdf\\Page\\' => 21,
- 'Com\\Tecnick\\Pdf\\Image\\' => 22,
- 'Com\\Tecnick\\Pdf\\Graph\\' => 22,
- 'Com\\Tecnick\\Pdf\\Font\\' => 21,
- 'Com\\Tecnick\\Pdf\\Filter\\' => 23,
- 'Com\\Tecnick\\Pdf\\Encrypt\\' => 24,
- 'Com\\Tecnick\\Pdf\\' => 16,
- 'Com\\Tecnick\\File\\' => 17,
- 'Com\\Tecnick\\Color\\' => 18,
- 'Com\\Tecnick\\Barcode\\' => 20,
),
);
@@ -53,15 +37,6 @@ class ComposerStaticInit6621356a0da1644f04f6f40bd0e0b948
array (
0 => __DIR__ . '/..' . '/psr/simple-cache/src',
),
- 'Psr\\Http\\Message\\' =>
- array (
- 0 => __DIR__ . '/..' . '/psr/http-factory/src',
- 1 => __DIR__ . '/..' . '/psr/http-message/src',
- ),
- 'Psr\\Http\\Client\\' =>
- array (
- 0 => __DIR__ . '/..' . '/psr/http-client/src',
- ),
'PhpOffice\\PhpSpreadsheet\\' =>
array (
0 => __DIR__ . '/..' . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet',
@@ -82,62 +57,6 @@ class ComposerStaticInit6621356a0da1644f04f6f40bd0e0b948
array (
0 => __DIR__ . '/..' . '/markbaker/complex/classes/src',
),
- 'Com\\Tecnick\\Unicode\\Data\\' =>
- array (
- 0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-unicode-data/src',
- ),
- 'Com\\Tecnick\\Unicode\\' =>
- array (
- 0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-unicode/src',
- ),
- 'Com\\Tecnick\\Pdf\\Sign\\' =>
- array (
- 0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-pdf-sign/src',
- ),
- 'Com\\Tecnick\\Pdf\\Parser\\' =>
- array (
- 0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-pdf-parser/src',
- ),
- 'Com\\Tecnick\\Pdf\\Page\\' =>
- array (
- 0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-pdf-page/src',
- ),
- 'Com\\Tecnick\\Pdf\\Image\\' =>
- array (
- 0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-pdf-image/src',
- ),
- 'Com\\Tecnick\\Pdf\\Graph\\' =>
- array (
- 0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-pdf-graph/src',
- ),
- 'Com\\Tecnick\\Pdf\\Font\\' =>
- array (
- 0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-pdf-font/src',
- ),
- 'Com\\Tecnick\\Pdf\\Filter\\' =>
- array (
- 0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-pdf-filter/src',
- ),
- 'Com\\Tecnick\\Pdf\\Encrypt\\' =>
- array (
- 0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-pdf-encrypt/src',
- ),
- 'Com\\Tecnick\\Pdf\\' =>
- array (
- 0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-pdf/src',
- ),
- 'Com\\Tecnick\\File\\' =>
- array (
- 0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-file/src',
- ),
- 'Com\\Tecnick\\Color\\' =>
- array (
- 0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-color/src',
- ),
- 'Com\\Tecnick\\Barcode\\' =>
- array (
- 0 => __DIR__ . '/..' . '/tecnickcom/tc-lib-barcode/src',
- ),
);
public static $classMap = array (
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
index 6f09eef..69458e6 100644
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -2,31 +2,32 @@
"packages": [
{
"name": "composer/pcre",
- "version": "3.3.2",
- "version_normalized": "3.3.2.0",
+ "version": "3.4.0",
+ "version_normalized": "3.4.0.0",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
- "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
+ "reference": "d5a341b3fb61f3001970940afb1d332968a183ed"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
- "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+ "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed",
+ "reference": "d5a341b3fb61f3001970940afb1d332968a183ed",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"conflict": {
- "phpstan/phpstan": "<1.11.10"
+ "phpstan/phpstan": "<2.2.2"
},
"require-dev": {
- "phpstan/phpstan": "^1.12 || ^2",
- "phpstan/phpstan-strict-rules": "^1 || ^2",
- "phpunit/phpunit": "^8 || ^9"
+ "phpstan/phpstan": "^2",
+ "phpstan/phpstan-deprecation-rules": "^2",
+ "phpstan/phpstan-strict-rules": "^2",
+ "phpunit/phpunit": "^9"
},
- "time": "2024-11-12T16:29:46+00:00",
+ "time": "2026-06-07T11:47:49+00:00",
"type": "library",
"extra": {
"phpstan": {
@@ -64,7 +65,7 @@
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
- "source": "https://github.com/composer/pcre/tree/3.3.2"
+ "source": "https://github.com/composer/pcre/tree/3.4.0"
},
"funding": [
{
@@ -74,27 +75,23 @@
{
"url": "https://github.com/composer",
"type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/composer/composer",
- "type": "tidelift"
}
],
"install-path": "./pcre"
},
{
"name": "maennchen/zipstream-php",
- "version": "3.2.0",
- "version_normalized": "3.2.0.0",
+ "version": "3.2.2",
+ "version_normalized": "3.2.2.0",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
- "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416"
+ "reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9712d8fa4cdf9240380b01eb4be55ad8dcf71416",
- "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416",
+ "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e",
+ "reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e",
"shasum": ""
},
"require": {
@@ -105,7 +102,7 @@
"require-dev": {
"brianium/paratest": "^7.7",
"ext-zip": "*",
- "friendsofphp/php-cs-fixer": "^3.16",
+ "friendsofphp/php-cs-fixer": "^3.86",
"guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
@@ -116,7 +113,7 @@
"guzzlehttp/psr7": "^2.4",
"psr/http-message": "^2.0"
},
- "time": "2025-07-17T11:15:13+00:00",
+ "time": "2026-04-11T18:38:28+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -153,7 +150,7 @@
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
- "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.0"
+ "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.2"
},
"funding": [
{
@@ -363,17 +360,17 @@
},
{
"name": "phpoffice/phpspreadsheet",
- "version": "5.2.0",
- "version_normalized": "5.2.0.0",
+ "version": "5.10.0",
+ "version_normalized": "5.10.0.0",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
- "reference": "3b8994b3aac4b61018bc04fc8c441f4fd68c18eb"
+ "reference": "eb18727acf6b1f4cc67145a52ab04f9fd4c28d53"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/3b8994b3aac4b61018bc04fc8c441f4fd68c18eb",
- "reference": "3b8994b3aac4b61018bc04fc8c441f4fd68c18eb",
+ "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/eb18727acf6b1f4cc67145a52ab04f9fd4c28d53",
+ "reference": "eb18727acf6b1f4cc67145a52ab04f9fd4c28d53",
"shasum": ""
},
"require": {
@@ -381,6 +378,7 @@
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
+ "ext-filter": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-libxml": "*",
@@ -394,14 +392,14 @@
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
- "php": "^8.1",
- "psr/http-client": "^1.0",
- "psr/http-factory": "^1.0",
+ "php": "^8.2",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^2.0 || ^3.0",
+ "ext-intl": "*",
+ "ext-openssl": "*",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.5",
"mpdf/mpdf": "^8.1.1",
@@ -409,18 +407,19 @@
"phpstan/phpstan": "^1.1 || ^2.0",
"phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0",
"phpstan/phpstan-phpunit": "^1.0 || ^2.0",
- "phpunit/phpunit": "^10.5",
+ "phpunit/phpunit": "^10.5 || ^11.0",
"squizlabs/php_codesniffer": "^3.7",
- "tecnickcom/tcpdf": "^6.5"
+ "tecnickcom/tcpdf": ">=6.8.0 <7.0.0"
},
"suggest": {
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
- "ext-intl": "PHP Internationalization Functions, regquired for NumberFormat Wizard",
+ "ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard and StringHelper::setLocale()",
+ "ext-openssl": "Handline Agile-encrypted Xlsx spreadsheets",
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
},
- "time": "2025-10-26T15:54:22+00:00",
+ "time": "2026-09-17T03:54:50+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -450,6 +449,9 @@
},
{
"name": "Adrien Crivelli"
+ },
+ {
+ "name": "Owen Leibman"
}
],
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
@@ -466,179 +468,10 @@
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
- "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.2.0"
+ "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.10.0"
},
"install-path": "../phpoffice/phpspreadsheet"
},
- {
- "name": "psr/http-client",
- "version": "1.0.3",
- "version_normalized": "1.0.3.0",
- "source": {
- "type": "git",
- "url": "https://github.com/php-fig/http-client.git",
- "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
- "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
- "shasum": ""
- },
- "require": {
- "php": "^7.0 || ^8.0",
- "psr/http-message": "^1.0 || ^2.0"
- },
- "time": "2023-09-23T14:17:50+00:00",
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "installation-source": "dist",
- "autoload": {
- "psr-4": {
- "Psr\\Http\\Client\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "PHP-FIG",
- "homepage": "https://www.php-fig.org/"
- }
- ],
- "description": "Common interface for HTTP clients",
- "homepage": "https://github.com/php-fig/http-client",
- "keywords": [
- "http",
- "http-client",
- "psr",
- "psr-18"
- ],
- "support": {
- "source": "https://github.com/php-fig/http-client"
- },
- "install-path": "../psr/http-client"
- },
- {
- "name": "psr/http-factory",
- "version": "1.1.0",
- "version_normalized": "1.1.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/php-fig/http-factory.git",
- "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
- "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
- "shasum": ""
- },
- "require": {
- "php": ">=7.1",
- "psr/http-message": "^1.0 || ^2.0"
- },
- "time": "2024-04-15T12:06:14+00:00",
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "installation-source": "dist",
- "autoload": {
- "psr-4": {
- "Psr\\Http\\Message\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "PHP-FIG",
- "homepage": "https://www.php-fig.org/"
- }
- ],
- "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
- "keywords": [
- "factory",
- "http",
- "message",
- "psr",
- "psr-17",
- "psr-7",
- "request",
- "response"
- ],
- "support": {
- "source": "https://github.com/php-fig/http-factory"
- },
- "install-path": "../psr/http-factory"
- },
- {
- "name": "psr/http-message",
- "version": "2.0",
- "version_normalized": "2.0.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/php-fig/http-message.git",
- "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
- "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
- "shasum": ""
- },
- "require": {
- "php": "^7.2 || ^8.0"
- },
- "time": "2023-04-04T09:54:51+00:00",
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.0.x-dev"
- }
- },
- "installation-source": "dist",
- "autoload": {
- "psr-4": {
- "Psr\\Http\\Message\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "PHP-FIG",
- "homepage": "https://www.php-fig.org/"
- }
- ],
- "description": "Common interface for HTTP messages",
- "homepage": "https://github.com/php-fig/http-message",
- "keywords": [
- "http",
- "http-message",
- "psr",
- "psr-7",
- "request",
- "response"
- ],
- "support": {
- "source": "https://github.com/php-fig/http-message/tree/2.0"
- },
- "install-path": "../psr/http-message"
- },
{
"name": "psr/simple-cache",
"version": "3.0.0",
@@ -693,1005 +526,6 @@
},
"install-path": "../psr/simple-cache"
},
- {
- "name": "tecnickcom/tc-lib-barcode",
- "version": "2.13.2",
- "version_normalized": "2.13.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/tecnickcom/tc-lib-barcode.git",
- "reference": "c7f3ddbc3df10259c59ddc3016ec421267e25e5d"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/tecnickcom/tc-lib-barcode/zipball/c7f3ddbc3df10259c59ddc3016ec421267e25e5d",
- "reference": "c7f3ddbc3df10259c59ddc3016ec421267e25e5d",
- "shasum": ""
- },
- "require": {
- "ext-ctype": "*",
- "ext-gd": "*",
- "ext-pcre": "*",
- "php": ">=8.2",
- "tecnickcom/tc-lib-color": "^2.13"
- },
- "require-dev": {
- "pdepend/pdepend": "^2.16",
- "phpunit/phpunit": "^11.5 || ^12.5 || ^13.2"
- },
- "suggest": {
- "ext-bcmath": "Speeds up the arbitrary precision arithmetic used by the IMB and PDF417 barcode types (a pure-PHP implementation is used when missing)",
- "ext-imagick": "Alternative PNG renderer used by getPngData() when the extension is loaded"
- },
- "time": "2026-08-07T08:07:20+00:00",
- "type": "library",
- "installation-source": "dist",
- "autoload": {
- "psr-4": {
- "Com\\Tecnick\\Barcode\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "LGPL-3.0-or-later"
- ],
- "authors": [
- {
- "name": "Nicola Asuni",
- "email": "info@tecnick.com",
- "role": "lead"
- }
- ],
- "description": "PHP library to generate linear and bidimensional barcodes",
- "homepage": "https://tcpdf.org",
- "keywords": [
- "3 of 9",
- "ANSI MH10.8M-1983",
- "CBC",
- "CODABAR",
- "CODE 11",
- "CODE 128 A B C",
- "CODE 39",
- "CODE 93",
- "EAN 13",
- "EAN 8",
- "ECC200",
- "ISO IEC 15438 2006",
- "ISO IEC 16022",
- "ISO IEC 24778 2008",
- "Intelligent Mail Barcode",
- "Interleaved 2 of 5",
- "KIX",
- "Klant",
- "MSI",
- "Onecode",
- "PHARMACODE",
- "PHARMACODE TWO-TRACKS",
- "POSTNET",
- "RMS4CC",
- "Standard 2 of 5",
- "UPC-A",
- "UPC-E",
- "USD-3",
- "USPS-B-3200",
- "USS-93",
- "aztec",
- "barcode",
- "datamatrix",
- "pdf417",
- "planet",
- "qr-code",
- "royal mail",
- "tc-lib-barcode",
- "upc"
- ],
- "support": {
- "issues": "https://github.com/tecnickcom/tc-lib-barcode/issues",
- "source": "https://github.com/tecnickcom/tc-lib-barcode"
- },
- "funding": [
- {
- "url": "https://github.com/sponsors/tecnickcom",
- "type": "github"
- }
- ],
- "install-path": "../tecnickcom/tc-lib-barcode"
- },
- {
- "name": "tecnickcom/tc-lib-color",
- "version": "2.13.3",
- "version_normalized": "2.13.3.0",
- "source": {
- "type": "git",
- "url": "https://github.com/tecnickcom/tc-lib-color.git",
- "reference": "36b0cb2215c446fed65842151d494f2b3081533c"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/tecnickcom/tc-lib-color/zipball/36b0cb2215c446fed65842151d494f2b3081533c",
- "reference": "36b0cb2215c446fed65842151d494f2b3081533c",
- "shasum": ""
- },
- "require": {
- "ext-pcre": "*",
- "php": ">=8.2"
- },
- "require-dev": {
- "pdepend/pdepend": "^2.16",
- "phpunit/phpunit": "^11.5 || ^12.5 || ^13.2"
- },
- "time": "2026-08-07T08:05:09+00:00",
- "type": "library",
- "installation-source": "dist",
- "autoload": {
- "psr-4": {
- "Com\\Tecnick\\Color\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "LGPL-3.0-or-later"
- ],
- "authors": [
- {
- "name": "Nicola Asuni",
- "email": "info@tecnick.com",
- "role": "lead"
- }
- ],
- "description": "PHP library to manipulate various color representations",
- "homepage": "https://tcpdf.org",
- "keywords": [
- "cmyk",
- "color",
- "colors",
- "colour",
- "colours",
- "hsl",
- "hsla",
- "javascript",
- "rgb",
- "rgba",
- "tc-lib-color",
- "web"
- ],
- "support": {
- "issues": "https://github.com/tecnickcom/tc-lib-color/issues",
- "source": "https://github.com/tecnickcom/tc-lib-color"
- },
- "funding": [
- {
- "url": "https://github.com/sponsors/tecnickcom",
- "type": "github"
- }
- ],
- "install-path": "../tecnickcom/tc-lib-color"
- },
- {
- "name": "tecnickcom/tc-lib-file",
- "version": "3.7.3",
- "version_normalized": "3.7.3.0",
- "source": {
- "type": "git",
- "url": "https://github.com/tecnickcom/tc-lib-file.git",
- "reference": "c582b6d493c26703e95809f6d3e938c79f88885f"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/tecnickcom/tc-lib-file/zipball/c582b6d493c26703e95809f6d3e938c79f88885f",
- "reference": "c582b6d493c26703e95809f6d3e938c79f88885f",
- "shasum": ""
- },
- "require": {
- "ext-curl": "*",
- "ext-pcre": "*",
- "php": ">=8.2"
- },
- "require-dev": {
- "pdepend/pdepend": "^2.16",
- "phpunit/phpunit": "^13.2 || ^12.5 || ^11.5"
- },
- "suggest": {
- "ext-intl": "Enables Unicode NFC normalization when matching paths against the allowlist"
- },
- "time": "2026-08-07T08:05:39+00:00",
- "type": "library",
- "installation-source": "dist",
- "autoload": {
- "psr-4": {
- "Com\\Tecnick\\File\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "LGPL-3.0-or-later"
- ],
- "authors": [
- {
- "name": "Nicola Asuni",
- "email": "info@tecnick.com",
- "role": "lead"
- }
- ],
- "description": "PHP library to read byte-level data from files",
- "homepage": "https://tcpdf.org",
- "keywords": [
- "Double",
- "bit",
- "byte",
- "file",
- "long",
- "read",
- "short",
- "tc-lib-file"
- ],
- "support": {
- "issues": "https://github.com/tecnickcom/tc-lib-file/issues",
- "source": "https://github.com/tecnickcom/tc-lib-file"
- },
- "funding": [
- {
- "url": "https://github.com/sponsors/tecnickcom",
- "type": "github"
- }
- ],
- "install-path": "../tecnickcom/tc-lib-file"
- },
- {
- "name": "tecnickcom/tc-lib-pdf",
- "version": "8.68.3",
- "version_normalized": "8.68.3.0",
- "source": {
- "type": "git",
- "url": "https://github.com/tecnickcom/tc-lib-pdf.git",
- "reference": "6b056239c0d9781319a4d009e8ca33d48e94e0ea"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/tecnickcom/tc-lib-pdf/zipball/6b056239c0d9781319a4d009e8ca33d48e94e0ea",
- "reference": "6b056239c0d9781319a4d009e8ca33d48e94e0ea",
- "shasum": ""
- },
- "require": {
- "ext-ctype": "*",
- "ext-filter": "*",
- "ext-hash": "*",
- "ext-json": "*",
- "ext-mbstring": "*",
- "ext-openssl": "*",
- "ext-pcre": "*",
- "ext-xml": "*",
- "ext-zlib": "*",
- "php": ">=8.2",
- "tecnickcom/tc-lib-barcode": "^2.13",
- "tecnickcom/tc-lib-color": "^2.13",
- "tecnickcom/tc-lib-file": "^3.7",
- "tecnickcom/tc-lib-pdf-encrypt": "^2.9",
- "tecnickcom/tc-lib-pdf-font": "^3.13",
- "tecnickcom/tc-lib-pdf-graph": "^2.15",
- "tecnickcom/tc-lib-pdf-image": "^3.12",
- "tecnickcom/tc-lib-pdf-page": "^4.14",
- "tecnickcom/tc-lib-pdf-parser": "^3.14",
- "tecnickcom/tc-lib-pdf-sign": "^1.1",
- "tecnickcom/tc-lib-unicode": "^3.0",
- "tecnickcom/tc-lib-unicode-data": "^3.0"
- },
- "require-dev": {
- "pdepend/pdepend": "^2.16",
- "phpunit/phpunit": "^11.5 || ^12.5 || ^13.2"
- },
- "suggest": {
- "ext-curl": "Required to contact a Timestamp Authority (RFC 3161) when timestamping signatures",
- "ext-intl": "Enables Unicode NFC normalization of the PDF file name"
- },
- "time": "2026-08-07T08:11:00+00:00",
- "type": "library",
- "installation-source": "dist",
- "autoload": {
- "psr-4": {
- "Com\\Tecnick\\Pdf\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "LGPL-3.0-or-later"
- ],
- "authors": [
- {
- "name": "Nicola Asuni",
- "email": "info@tecnick.com",
- "role": "lead"
- }
- ],
- "description": "PHP PDF Library",
- "homepage": "https://tcpdf.org",
- "keywords": [
- "PDFD32000-2008",
- "TCPDF",
- "document",
- "pdf",
- "tc-lib-pdf"
- ],
- "support": {
- "issues": "https://github.com/tecnickcom/tc-lib-pdf/issues",
- "source": "https://github.com/tecnickcom/tc-lib-pdf"
- },
- "funding": [
- {
- "url": "https://github.com/sponsors/tecnickcom",
- "type": "github"
- }
- ],
- "install-path": "../tecnickcom/tc-lib-pdf"
- },
- {
- "name": "tecnickcom/tc-lib-pdf-encrypt",
- "version": "2.9.3",
- "version_normalized": "2.9.3.0",
- "source": {
- "type": "git",
- "url": "https://github.com/tecnickcom/tc-lib-pdf-encrypt.git",
- "reference": "9d188937a673af8adc6da1f104c44cf434f7b198"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/tecnickcom/tc-lib-pdf-encrypt/zipball/9d188937a673af8adc6da1f104c44cf434f7b198",
- "reference": "9d188937a673af8adc6da1f104c44cf434f7b198",
- "shasum": ""
- },
- "require": {
- "ext-hash": "*",
- "ext-openssl": "*",
- "ext-pcre": "*",
- "php": ">=8.2"
- },
- "require-dev": {
- "pdepend/pdepend": "^2.16",
- "phpunit/phpunit": "^11.5 || ^12.5 || ^13.2"
- },
- "suggest": {
- "ext-posix": "Adds the process id to the random seed material"
- },
- "time": "2026-08-07T08:05:51+00:00",
- "type": "library",
- "installation-source": "dist",
- "autoload": {
- "psr-4": {
- "Com\\Tecnick\\Pdf\\Encrypt\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "LGPL-3.0-or-later"
- ],
- "authors": [
- {
- "name": "Nicola Asuni",
- "email": "info@tecnick.com",
- "role": "lead"
- }
- ],
- "description": "PHP library to encrypt data for PDF",
- "homepage": "https://tcpdf.org",
- "keywords": [
- "aes",
- "encrypt",
- "encryption",
- "pdf",
- "rc4",
- "tc-lib-pdf-encrypt"
- ],
- "support": {
- "issues": "https://github.com/tecnickcom/tc-lib-pdf-encrypt/issues",
- "source": "https://github.com/tecnickcom/tc-lib-pdf-encrypt"
- },
- "funding": [
- {
- "url": "https://github.com/sponsors/tecnickcom",
- "type": "github"
- }
- ],
- "install-path": "../tecnickcom/tc-lib-pdf-encrypt"
- },
- {
- "name": "tecnickcom/tc-lib-pdf-filter",
- "version": "2.10.3",
- "version_normalized": "2.10.3.0",
- "source": {
- "type": "git",
- "url": "https://github.com/tecnickcom/tc-lib-pdf-filter.git",
- "reference": "48270d50db69852ef58a22b5b034b8a08e5c8464"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/tecnickcom/tc-lib-pdf-filter/zipball/48270d50db69852ef58a22b5b034b8a08e5c8464",
- "reference": "48270d50db69852ef58a22b5b034b8a08e5c8464",
- "shasum": ""
- },
- "require": {
- "ext-pcre": "*",
- "ext-zlib": "*",
- "php": ">=8.2"
- },
- "require-dev": {
- "pdepend/pdepend": "^2.16",
- "phpunit/phpunit": "^11.5 || ^12.5 || ^13.2"
- },
- "suggest": {
- "ext-imagick": "Required for the JPXDecode (JPEG 2000) and CCITTFaxDecode filters"
- },
- "time": "2026-08-07T08:06:03+00:00",
- "type": "library",
- "installation-source": "dist",
- "autoload": {
- "psr-4": {
- "Com\\Tecnick\\Pdf\\Filter\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "LGPL-3.0-or-later"
- ],
- "authors": [
- {
- "name": "Nicola Asuni",
- "email": "info@tecnick.com",
- "role": "lead"
- }
- ],
- "description": "PHP library to decode PDF compression and encryption filters",
- "homepage": "https://tcpdf.org",
- "keywords": [
- "compression",
- "encryption",
- "filter",
- "pdf",
- "tc-lib-pdf-filter"
- ],
- "support": {
- "issues": "https://github.com/tecnickcom/tc-lib-pdf-filter/issues",
- "source": "https://github.com/tecnickcom/tc-lib-pdf-filter"
- },
- "funding": [
- {
- "url": "https://github.com/sponsors/tecnickcom",
- "type": "github"
- }
- ],
- "install-path": "../tecnickcom/tc-lib-pdf-filter"
- },
- {
- "name": "tecnickcom/tc-lib-pdf-font",
- "version": "3.13.2",
- "version_normalized": "3.13.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/tecnickcom/tc-lib-pdf-font.git",
- "reference": "314dfbe240057e1119d4830f35827455f8a76eba"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/tecnickcom/tc-lib-pdf-font/zipball/314dfbe240057e1119d4830f35827455f8a76eba",
- "reference": "314dfbe240057e1119d4830f35827455f8a76eba",
- "shasum": ""
- },
- "require": {
- "ext-hash": "*",
- "ext-json": "*",
- "ext-pcre": "*",
- "ext-zlib": "*",
- "php": ">=8.2",
- "tecnickcom/tc-lib-file": "^3.7",
- "tecnickcom/tc-lib-pdf-encrypt": "^2.9",
- "tecnickcom/tc-lib-unicode-data": "^3.0"
- },
- "require-dev": {
- "pdepend/pdepend": "^2.16",
- "phpunit/phpunit": "^11.5 || ^12.5 || ^13.2"
- },
- "suggest": {
- "ext-iconv": "Decodes legacy Macintosh (MacRoman) TrueType name strings",
- "ext-mbstring": "Decodes UTF-16BE and Windows-1252 TrueType name strings"
- },
- "time": "2026-08-07T08:09:37+00:00",
- "type": "library",
- "installation-source": "dist",
- "autoload": {
- "psr-4": {
- "Com\\Tecnick\\Pdf\\Font\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "LGPL-3.0-or-later"
- ],
- "authors": [
- {
- "name": "Nicola Asuni",
- "email": "info@tecnick.com",
- "role": "lead"
- }
- ],
- "description": "PHP library containing PDF page formats and definitions",
- "homepage": "https://tcpdf.org",
- "keywords": [
- "PFB",
- "afm",
- "font",
- "import",
- "pdf",
- "tc-lib-pdf-font",
- "ttf"
- ],
- "support": {
- "issues": "https://github.com/tecnickcom/tc-lib-pdf-font/issues",
- "source": "https://github.com/tecnickcom/tc-lib-pdf-font"
- },
- "funding": [
- {
- "url": "https://github.com/sponsors/tecnickcom",
- "type": "github"
- }
- ],
- "install-path": "../tecnickcom/tc-lib-pdf-font"
- },
- {
- "name": "tecnickcom/tc-lib-pdf-graph",
- "version": "2.15.2",
- "version_normalized": "2.15.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/tecnickcom/tc-lib-pdf-graph.git",
- "reference": "4a3408690c65e14b9c86abe97a12c3079eb8a0cd"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/tecnickcom/tc-lib-pdf-graph/zipball/4a3408690c65e14b9c86abe97a12c3079eb8a0cd",
- "reference": "4a3408690c65e14b9c86abe97a12c3079eb8a0cd",
- "shasum": ""
- },
- "require": {
- "ext-zlib": "*",
- "php": ">=8.2",
- "tecnickcom/tc-lib-color": "^2.13",
- "tecnickcom/tc-lib-pdf-encrypt": "^2.9"
- },
- "require-dev": {
- "pdepend/pdepend": "^2.16",
- "phpunit/phpunit": "^11.5 || ^12.5 || ^13.2"
- },
- "time": "2026-08-07T08:08:50+00:00",
- "type": "library",
- "installation-source": "dist",
- "autoload": {
- "psr-4": {
- "Com\\Tecnick\\Pdf\\Graph\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "LGPL-3.0-or-later"
- ],
- "authors": [
- {
- "name": "Nicola Asuni",
- "email": "info@tecnick.com",
- "role": "lead"
- }
- ],
- "description": "PHP library containing PDF graphic and geometric methods",
- "homepage": "https://tcpdf.org",
- "keywords": [
- "geometry",
- "graphic",
- "pdf",
- "tc-lib-pdf-graph",
- "transformation"
- ],
- "support": {
- "issues": "https://github.com/tecnickcom/tc-lib-pdf-graph/issues",
- "source": "https://github.com/tecnickcom/tc-lib-pdf-graph"
- },
- "funding": [
- {
- "url": "https://github.com/sponsors/tecnickcom",
- "type": "github"
- }
- ],
- "install-path": "../tecnickcom/tc-lib-pdf-graph"
- },
- {
- "name": "tecnickcom/tc-lib-pdf-image",
- "version": "3.12.2",
- "version_normalized": "3.12.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/tecnickcom/tc-lib-pdf-image.git",
- "reference": "d7052a7e9216e842bd77395f1dfb443e9bfbcef8"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/tecnickcom/tc-lib-pdf-image/zipball/d7052a7e9216e842bd77395f1dfb443e9bfbcef8",
- "reference": "d7052a7e9216e842bd77395f1dfb443e9bfbcef8",
- "shasum": ""
- },
- "require": {
- "ext-gd": "*",
- "ext-zlib": "*",
- "php": ">=8.2",
- "tecnickcom/tc-lib-color": "^2.13",
- "tecnickcom/tc-lib-file": "^3.7",
- "tecnickcom/tc-lib-pdf-encrypt": "^2.9"
- },
- "require-dev": {
- "pdepend/pdepend": "^2.16",
- "phpunit/phpunit": "^11.5 || ^12.5 || ^13.2"
- },
- "time": "2026-08-07T08:09:04+00:00",
- "type": "library",
- "installation-source": "dist",
- "autoload": {
- "psr-4": {
- "Com\\Tecnick\\Pdf\\Image\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "LGPL-3.0-or-later"
- ],
- "authors": [
- {
- "name": "Nicola Asuni",
- "email": "info@tecnick.com",
- "role": "lead"
- }
- ],
- "description": "PHP library containing PDF Image methods",
- "homepage": "https://tcpdf.org",
- "keywords": [
- "image",
- "jpeg",
- "jpg",
- "pdf",
- "png",
- "tc-lib-pdf-image"
- ],
- "support": {
- "issues": "https://github.com/tecnickcom/tc-lib-pdf-image/issues",
- "source": "https://github.com/tecnickcom/tc-lib-pdf-image"
- },
- "funding": [
- {
- "url": "https://github.com/sponsors/tecnickcom",
- "type": "github"
- }
- ],
- "install-path": "../tecnickcom/tc-lib-pdf-image"
- },
- {
- "name": "tecnickcom/tc-lib-pdf-page",
- "version": "4.14.2",
- "version_normalized": "4.14.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/tecnickcom/tc-lib-pdf-page.git",
- "reference": "ac9adaddf8624b5008da335914a42a318cd7d310"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/tecnickcom/tc-lib-pdf-page/zipball/ac9adaddf8624b5008da335914a42a318cd7d310",
- "reference": "ac9adaddf8624b5008da335914a42a318cd7d310",
- "shasum": ""
- },
- "require": {
- "ext-zlib": "*",
- "php": ">=8.2",
- "tecnickcom/tc-lib-color": "^2.13",
- "tecnickcom/tc-lib-pdf-encrypt": "^2.9"
- },
- "require-dev": {
- "pdepend/pdepend": "^2.16",
- "phpunit/phpunit": "^11.5 || ^12.5 || ^13.2"
- },
- "time": "2026-08-07T08:08:37+00:00",
- "type": "library",
- "installation-source": "dist",
- "autoload": {
- "psr-4": {
- "Com\\Tecnick\\Pdf\\Page\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "LGPL-3.0-or-later"
- ],
- "authors": [
- {
- "name": "Nicola Asuni",
- "email": "info@tecnick.com",
- "role": "lead"
- }
- ],
- "description": "PHP library containing PDF page formats and definitions",
- "homepage": "https://tcpdf.org",
- "keywords": [
- "format",
- "page",
- "pdf",
- "tc-lib-pdf-page"
- ],
- "support": {
- "issues": "https://github.com/tecnickcom/tc-lib-pdf-page/issues",
- "source": "https://github.com/tecnickcom/tc-lib-pdf-page"
- },
- "funding": [
- {
- "url": "https://github.com/sponsors/tecnickcom",
- "type": "github"
- }
- ],
- "install-path": "../tecnickcom/tc-lib-pdf-page"
- },
- {
- "name": "tecnickcom/tc-lib-pdf-parser",
- "version": "3.14.2",
- "version_normalized": "3.14.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/tecnickcom/tc-lib-pdf-parser.git",
- "reference": "db9c32046a3e8d57c8626e8d3ffc94caa10a0675"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/tecnickcom/tc-lib-pdf-parser/zipball/db9c32046a3e8d57c8626e8d3ffc94caa10a0675",
- "reference": "db9c32046a3e8d57c8626e8d3ffc94caa10a0675",
- "shasum": ""
- },
- "require": {
- "ext-pcre": "*",
- "php": ">=8.2",
- "tecnickcom/tc-lib-pdf-filter": "^2.10"
- },
- "require-dev": {
- "pdepend/pdepend": "^2.16",
- "phpunit/phpunit": "^11.5 || ^12.5 || ^13.2"
- },
- "time": "2026-08-07T08:08:23+00:00",
- "type": "library",
- "installation-source": "dist",
- "autoload": {
- "psr-4": {
- "Com\\Tecnick\\Pdf\\Parser\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "LGPL-3.0-or-later"
- ],
- "authors": [
- {
- "name": "Nicola Asuni",
- "email": "info@tecnick.com",
- "role": "lead"
- }
- ],
- "description": "PHP library to parse PDF documents",
- "homepage": "https://tcpdf.org",
- "keywords": [
- "document",
- "parser",
- "pdf",
- "tc-lib-pdf-parser"
- ],
- "support": {
- "issues": "https://github.com/tecnickcom/tc-lib-pdf-parser/issues",
- "source": "https://github.com/tecnickcom/tc-lib-pdf-parser"
- },
- "funding": [
- {
- "url": "https://github.com/sponsors/tecnickcom",
- "type": "github"
- }
- ],
- "install-path": "../tecnickcom/tc-lib-pdf-parser"
- },
- {
- "name": "tecnickcom/tc-lib-pdf-sign",
- "version": "1.1.3",
- "version_normalized": "1.1.3.0",
- "source": {
- "type": "git",
- "url": "https://github.com/tecnickcom/tc-lib-pdf-sign.git",
- "reference": "4132f1fc4b9c103b1a9ec879e2d7b7f9598918f7"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/tecnickcom/tc-lib-pdf-sign/zipball/4132f1fc4b9c103b1a9ec879e2d7b7f9598918f7",
- "reference": "4132f1fc4b9c103b1a9ec879e2d7b7f9598918f7",
- "shasum": ""
- },
- "require": {
- "ext-hash": "*",
- "ext-openssl": "*",
- "ext-pcre": "*",
- "php": ">=8.2"
- },
- "require-dev": {
- "pdepend/pdepend": "^2.16",
- "phpunit/phpunit": "^11.5 || ^12.5 || ^13.2"
- },
- "time": "2026-08-07T08:06:17+00:00",
- "type": "library",
- "installation-source": "dist",
- "autoload": {
- "psr-4": {
- "Com\\Tecnick\\Pdf\\Sign\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "LGPL-3.0-or-later"
- ],
- "authors": [
- {
- "name": "Nicola Asuni",
- "email": "info@tecnick.com",
- "role": "lead"
- }
- ],
- "description": "PHP library to create and embed digital signatures (PKCS#7 / CAdES / PAdES) in PDF documents",
- "homepage": "https://tcpdf.org",
- "keywords": [
- "cades",
- "digital-signature",
- "pades",
- "pdf",
- "pkcs7",
- "sign",
- "signature",
- "tc-lib-pdf-sign",
- "timestamp"
- ],
- "support": {
- "issues": "https://github.com/tecnickcom/tc-lib-pdf-sign/issues",
- "source": "https://github.com/tecnickcom/tc-lib-pdf-sign"
- },
- "funding": [
- {
- "url": "https://github.com/sponsors/tecnickcom",
- "type": "github"
- }
- ],
- "install-path": "../tecnickcom/tc-lib-pdf-sign"
- },
- {
- "name": "tecnickcom/tc-lib-unicode",
- "version": "3.0.2",
- "version_normalized": "3.0.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/tecnickcom/tc-lib-unicode.git",
- "reference": "e0549f1e0bee657a7a5c361d823409655a4ea2c3"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/tecnickcom/tc-lib-unicode/zipball/e0549f1e0bee657a7a5c361d823409655a4ea2c3",
- "reference": "e0549f1e0bee657a7a5c361d823409655a4ea2c3",
- "shasum": ""
- },
- "require": {
- "ext-ctype": "*",
- "ext-mbstring": "*",
- "ext-pcre": "*",
- "php": ">=8.2",
- "tecnickcom/tc-lib-unicode-data": "^3.0"
- },
- "require-dev": {
- "pdepend/pdepend": "^2.16",
- "phpunit/phpunit": "^11.5 || ^12.5 || ^13.2"
- },
- "time": "2026-08-07T08:08:11+00:00",
- "type": "library",
- "installation-source": "dist",
- "autoload": {
- "psr-4": {
- "Com\\Tecnick\\Unicode\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "LGPL-3.0-or-later"
- ],
- "authors": [
- {
- "name": "Nicola Asuni",
- "email": "info@tecnick.com",
- "role": "lead"
- }
- ],
- "description": "PHP library containing Unicode methods",
- "homepage": "https://tcpdf.org",
- "keywords": [
- "font",
- "pdf",
- "tc-lib-unicode",
- "unicode",
- "utf-8"
- ],
- "support": {
- "issues": "https://github.com/tecnickcom/tc-lib-unicode/issues",
- "source": "https://github.com/tecnickcom/tc-lib-unicode"
- },
- "funding": [
- {
- "url": "https://github.com/sponsors/tecnickcom",
- "type": "github"
- }
- ],
- "install-path": "../tecnickcom/tc-lib-unicode"
- },
- {
- "name": "tecnickcom/tc-lib-unicode-data",
- "version": "3.0.2",
- "version_normalized": "3.0.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/tecnickcom/tc-lib-unicode-data.git",
- "reference": "6b090f51613087c2ed6e576e9543c1feddf2ba51"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/tecnickcom/tc-lib-unicode-data/zipball/6b090f51613087c2ed6e576e9543c1feddf2ba51",
- "reference": "6b090f51613087c2ed6e576e9543c1feddf2ba51",
- "shasum": ""
- },
- "require": {
- "php": ">=8.2"
- },
- "require-dev": {
- "pdepend/pdepend": "^2.16",
- "phpunit/phpunit": "^11.5 || ^12.5 || ^13.2"
- },
- "time": "2026-08-07T08:06:30+00:00",
- "type": "library",
- "installation-source": "dist",
- "autoload": {
- "psr-4": {
- "Com\\Tecnick\\Unicode\\Data\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "LGPL-3.0-or-later"
- ],
- "authors": [
- {
- "name": "Nicola Asuni",
- "email": "info@tecnick.com",
- "role": "lead"
- }
- ],
- "description": "PHP library containing Unicode definitions",
- "homepage": "https://tcpdf.org",
- "keywords": [
- "font",
- "pdf",
- "tc-lib-unicode-data",
- "unicode",
- "utf-8"
- ],
- "support": {
- "issues": "https://github.com/tecnickcom/tc-lib-unicode-data/issues",
- "source": "https://github.com/tecnickcom/tc-lib-unicode-data"
- },
- "funding": [
- {
- "url": "https://github.com/sponsors/tecnickcom",
- "type": "github"
- }
- ],
- "install-path": "../tecnickcom/tc-lib-unicode-data"
- },
{
"name": "tecnickcom/tcpdf",
"version": "6.10.0",
diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php
index 7bb6e9a..d19329e 100644
--- a/vendor/composer/installed.php
+++ b/vendor/composer/installed.php
@@ -1,9 +1,9 @@
array(
'name' => '__root__',
- 'pretty_version' => '1.0.0+no-version-set',
- 'version' => '1.0.0.0',
- 'reference' => null,
+ 'pretty_version' => 'dev-development',
+ 'version' => 'dev-development',
+ 'reference' => 'a1643eb1b74d1dae0fda79a68b6f0672d4603a81',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -11,27 +11,27 @@
),
'versions' => array(
'__root__' => array(
- 'pretty_version' => '1.0.0+no-version-set',
- 'version' => '1.0.0.0',
- 'reference' => null,
+ 'pretty_version' => 'dev-development',
+ 'version' => 'dev-development',
+ 'reference' => 'a1643eb1b74d1dae0fda79a68b6f0672d4603a81',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'composer/pcre' => array(
- 'pretty_version' => '3.3.2',
- 'version' => '3.3.2.0',
- 'reference' => 'b2bed4734f0cc156ee1fe9c0da2550420d99a21e',
+ 'pretty_version' => '3.4.0',
+ 'version' => '3.4.0.0',
+ 'reference' => 'd5a341b3fb61f3001970940afb1d332968a183ed',
'type' => 'library',
'install_path' => __DIR__ . '/./pcre',
'aliases' => array(),
'dev_requirement' => false,
),
'maennchen/zipstream-php' => array(
- 'pretty_version' => '3.2.0',
- 'version' => '3.2.0.0',
- 'reference' => '9712d8fa4cdf9240380b01eb4be55ad8dcf71416',
+ 'pretty_version' => '3.2.2',
+ 'version' => '3.2.2.0',
+ 'reference' => '77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e',
'type' => 'library',
'install_path' => __DIR__ . '/../maennchen/zipstream-php',
'aliases' => array(),
@@ -65,41 +65,14 @@
'dev_requirement' => false,
),
'phpoffice/phpspreadsheet' => array(
- 'pretty_version' => '5.2.0',
- 'version' => '5.2.0.0',
- 'reference' => '3b8994b3aac4b61018bc04fc8c441f4fd68c18eb',
+ 'pretty_version' => '5.10.0',
+ 'version' => '5.10.0.0',
+ 'reference' => 'eb18727acf6b1f4cc67145a52ab04f9fd4c28d53',
'type' => 'library',
'install_path' => __DIR__ . '/../phpoffice/phpspreadsheet',
'aliases' => array(),
'dev_requirement' => false,
),
- 'psr/http-client' => array(
- 'pretty_version' => '1.0.3',
- 'version' => '1.0.3.0',
- 'reference' => 'bb5906edc1c324c9a05aa0873d40117941e5fa90',
- 'type' => 'library',
- 'install_path' => __DIR__ . '/../psr/http-client',
- 'aliases' => array(),
- 'dev_requirement' => false,
- ),
- 'psr/http-factory' => array(
- 'pretty_version' => '1.1.0',
- 'version' => '1.1.0.0',
- 'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a',
- 'type' => 'library',
- 'install_path' => __DIR__ . '/../psr/http-factory',
- 'aliases' => array(),
- 'dev_requirement' => false,
- ),
- 'psr/http-message' => array(
- 'pretty_version' => '2.0',
- 'version' => '2.0.0.0',
- 'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71',
- 'type' => 'library',
- 'install_path' => __DIR__ . '/../psr/http-message',
- 'aliases' => array(),
- 'dev_requirement' => false,
- ),
'psr/simple-cache' => array(
'pretty_version' => '3.0.0',
'version' => '3.0.0.0',
@@ -109,132 +82,6 @@
'aliases' => array(),
'dev_requirement' => false,
),
- 'tecnickcom/tc-lib-barcode' => array(
- 'pretty_version' => '2.13.2',
- 'version' => '2.13.2.0',
- 'reference' => 'c7f3ddbc3df10259c59ddc3016ec421267e25e5d',
- 'type' => 'library',
- 'install_path' => __DIR__ . '/../tecnickcom/tc-lib-barcode',
- 'aliases' => array(),
- 'dev_requirement' => false,
- ),
- 'tecnickcom/tc-lib-color' => array(
- 'pretty_version' => '2.13.3',
- 'version' => '2.13.3.0',
- 'reference' => '36b0cb2215c446fed65842151d494f2b3081533c',
- 'type' => 'library',
- 'install_path' => __DIR__ . '/../tecnickcom/tc-lib-color',
- 'aliases' => array(),
- 'dev_requirement' => false,
- ),
- 'tecnickcom/tc-lib-file' => array(
- 'pretty_version' => '3.7.3',
- 'version' => '3.7.3.0',
- 'reference' => 'c582b6d493c26703e95809f6d3e938c79f88885f',
- 'type' => 'library',
- 'install_path' => __DIR__ . '/../tecnickcom/tc-lib-file',
- 'aliases' => array(),
- 'dev_requirement' => false,
- ),
- 'tecnickcom/tc-lib-pdf' => array(
- 'pretty_version' => '8.68.3',
- 'version' => '8.68.3.0',
- 'reference' => '6b056239c0d9781319a4d009e8ca33d48e94e0ea',
- 'type' => 'library',
- 'install_path' => __DIR__ . '/../tecnickcom/tc-lib-pdf',
- 'aliases' => array(),
- 'dev_requirement' => false,
- ),
- 'tecnickcom/tc-lib-pdf-encrypt' => array(
- 'pretty_version' => '2.9.3',
- 'version' => '2.9.3.0',
- 'reference' => '9d188937a673af8adc6da1f104c44cf434f7b198',
- 'type' => 'library',
- 'install_path' => __DIR__ . '/../tecnickcom/tc-lib-pdf-encrypt',
- 'aliases' => array(),
- 'dev_requirement' => false,
- ),
- 'tecnickcom/tc-lib-pdf-filter' => array(
- 'pretty_version' => '2.10.3',
- 'version' => '2.10.3.0',
- 'reference' => '48270d50db69852ef58a22b5b034b8a08e5c8464',
- 'type' => 'library',
- 'install_path' => __DIR__ . '/../tecnickcom/tc-lib-pdf-filter',
- 'aliases' => array(),
- 'dev_requirement' => false,
- ),
- 'tecnickcom/tc-lib-pdf-font' => array(
- 'pretty_version' => '3.13.2',
- 'version' => '3.13.2.0',
- 'reference' => '314dfbe240057e1119d4830f35827455f8a76eba',
- 'type' => 'library',
- 'install_path' => __DIR__ . '/../tecnickcom/tc-lib-pdf-font',
- 'aliases' => array(),
- 'dev_requirement' => false,
- ),
- 'tecnickcom/tc-lib-pdf-graph' => array(
- 'pretty_version' => '2.15.2',
- 'version' => '2.15.2.0',
- 'reference' => '4a3408690c65e14b9c86abe97a12c3079eb8a0cd',
- 'type' => 'library',
- 'install_path' => __DIR__ . '/../tecnickcom/tc-lib-pdf-graph',
- 'aliases' => array(),
- 'dev_requirement' => false,
- ),
- 'tecnickcom/tc-lib-pdf-image' => array(
- 'pretty_version' => '3.12.2',
- 'version' => '3.12.2.0',
- 'reference' => 'd7052a7e9216e842bd77395f1dfb443e9bfbcef8',
- 'type' => 'library',
- 'install_path' => __DIR__ . '/../tecnickcom/tc-lib-pdf-image',
- 'aliases' => array(),
- 'dev_requirement' => false,
- ),
- 'tecnickcom/tc-lib-pdf-page' => array(
- 'pretty_version' => '4.14.2',
- 'version' => '4.14.2.0',
- 'reference' => 'ac9adaddf8624b5008da335914a42a318cd7d310',
- 'type' => 'library',
- 'install_path' => __DIR__ . '/../tecnickcom/tc-lib-pdf-page',
- 'aliases' => array(),
- 'dev_requirement' => false,
- ),
- 'tecnickcom/tc-lib-pdf-parser' => array(
- 'pretty_version' => '3.14.2',
- 'version' => '3.14.2.0',
- 'reference' => 'db9c32046a3e8d57c8626e8d3ffc94caa10a0675',
- 'type' => 'library',
- 'install_path' => __DIR__ . '/../tecnickcom/tc-lib-pdf-parser',
- 'aliases' => array(),
- 'dev_requirement' => false,
- ),
- 'tecnickcom/tc-lib-pdf-sign' => array(
- 'pretty_version' => '1.1.3',
- 'version' => '1.1.3.0',
- 'reference' => '4132f1fc4b9c103b1a9ec879e2d7b7f9598918f7',
- 'type' => 'library',
- 'install_path' => __DIR__ . '/../tecnickcom/tc-lib-pdf-sign',
- 'aliases' => array(),
- 'dev_requirement' => false,
- ),
- 'tecnickcom/tc-lib-unicode' => array(
- 'pretty_version' => '3.0.2',
- 'version' => '3.0.2.0',
- 'reference' => 'e0549f1e0bee657a7a5c361d823409655a4ea2c3',
- 'type' => 'library',
- 'install_path' => __DIR__ . '/../tecnickcom/tc-lib-unicode',
- 'aliases' => array(),
- 'dev_requirement' => false,
- ),
- 'tecnickcom/tc-lib-unicode-data' => array(
- 'pretty_version' => '3.0.2',
- 'version' => '3.0.2.0',
- 'reference' => '6b090f51613087c2ed6e576e9543c1feddf2ba51',
- 'type' => 'library',
- 'install_path' => __DIR__ . '/../tecnickcom/tc-lib-unicode-data',
- 'aliases' => array(),
- 'dev_requirement' => false,
- ),
'tecnickcom/tcpdf' => array(
'pretty_version' => '6.10.0',
'version' => '6.10.0.0',
diff --git a/vendor/composer/pcre/composer.json b/vendor/composer/pcre/composer.json
index d3a7e67..a6e72c3 100644
--- a/vendor/composer/pcre/composer.json
+++ b/vendor/composer/pcre/composer.json
@@ -20,12 +20,13 @@
"php": "^7.4 || ^8.0"
},
"require-dev": {
- "phpunit/phpunit": "^8 || ^9",
- "phpstan/phpstan": "^1.12 || ^2",
- "phpstan/phpstan-strict-rules": "^1 || ^2"
+ "phpunit/phpunit": "^9",
+ "phpstan/phpstan": "^2",
+ "phpstan/phpstan-strict-rules": "^2",
+ "phpstan/phpstan-deprecation-rules": "^2"
},
"conflict": {
- "phpstan/phpstan": "<1.11.10"
+ "phpstan/phpstan": "<2.2.2"
},
"autoload": {
"psr-4": {
@@ -48,7 +49,10 @@
}
},
"scripts": {
- "test": "@php vendor/bin/phpunit",
+ "test": [
+ "@php vendor/bin/phpunit",
+ "@php vendor/bin/phpunit --testsuite phpstan"
+ ],
"phpstan": "@php phpstan analyse"
}
}
diff --git a/vendor/composer/pcre/src/PHPStan/PregMatchTypeSpecifyingExtension.php b/vendor/composer/pcre/src/PHPStan/PregMatchTypeSpecifyingExtension.php
index 3db0ce0..e492c79 100644
--- a/vendor/composer/pcre/src/PHPStan/PregMatchTypeSpecifyingExtension.php
+++ b/vendor/composer/pcre/src/PHPStan/PregMatchTypeSpecifyingExtension.php
@@ -57,18 +57,38 @@ final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyi
{
$args = $node->getArgs();
$patternArg = $args[0] ?? null;
+ $subjectArg = $args[1] ?? null;
$matchesArg = $args[2] ?? null;
$flagsArg = $args[3] ?? null;
+ $subjectTypes = new SpecifiedTypes();
+ if ($patternArg === null) {
+ return $subjectTypes;
+ }
+
if (
- $patternArg === null || $matchesArg === null
+ $subjectArg !== null
+ && $context->true()
+ && $scope->getType($subjectArg->value)->isString()->yes()
) {
- return new SpecifiedTypes();
+ $subjectType = $this->regexShapeMatcher->matchSubjectExpr($patternArg->value, $scope);
+ if ($subjectType !== null) {
+ $subjectTypes = $this->typeSpecifier->create(
+ $subjectArg->value,
+ $subjectType,
+ $context,
+ $scope,
+ )->setRootExpr($node);
+ }
+ }
+
+ if ($matchesArg === null) {
+ return $subjectTypes;
}
$flagsType = PregMatchFlags::getType($flagsArg, $scope);
if ($flagsType === null) {
- return new SpecifiedTypes();
+ return $subjectTypes;
}
if (stripos($methodReflection->getName(), 'matchAll') !== false) {
@@ -78,7 +98,7 @@ final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyi
}
if ($matchedType === null) {
- return new SpecifiedTypes();
+ return $subjectTypes;
}
if (
@@ -93,27 +113,13 @@ final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyi
$context = $context->negate();
}
- // @phpstan-ignore function.alreadyNarrowedType
- if (method_exists('PHPStan\Analyser\SpecifiedTypes', 'setRootExpr')) {
- $typeSpecifier = $this->typeSpecifier->create(
- $matchesArg->value,
- $matchedType,
- $context,
- $scope
- )->setRootExpr($node);
-
- return $overwrite ? $typeSpecifier->setAlwaysOverwriteTypes() : $typeSpecifier;
- }
-
- // @phpstan-ignore arguments.count
- return $this->typeSpecifier->create(
+ $specifiedTypes = $this->typeSpecifier->create(
$matchesArg->value,
$matchedType,
$context,
- // @phpstan-ignore argument.type
- $overwrite,
- $scope,
- $node
- );
+ $scope
+ )->setRootExpr($node);
+
+ return $subjectTypes->unionWith($overwrite ? $specifiedTypes->setAlwaysOverwriteTypes() : $specifiedTypes);
}
}
diff --git a/vendor/composer/pcre/src/Preg.php b/vendor/composer/pcre/src/Preg.php
index 400abbf..98b4d29 100644
--- a/vendor/composer/pcre/src/Preg.php
+++ b/vendor/composer/pcre/src/Preg.php
@@ -395,7 +395,7 @@ class Preg
* @return array
* @throws UnexpectedNullMatchException
*/
- private static function enforceNonNullMatches(string $pattern, array $matches, string $variantMethod)
+ private static function enforceNonNullMatches(string $pattern, array $matches, string $variantMethod): array
{
foreach ($matches as $group => $match) {
if (is_string($match) || (is_array($match) && is_string($match[0]))) {
@@ -414,7 +414,7 @@ class Preg
* @return array>
* @throws UnexpectedNullMatchException
*/
- private static function enforceNonNullMatchAll(string $pattern, array $matches, string $variantMethod)
+ private static function enforceNonNullMatchAll(string $pattern, array $matches, string $variantMethod): array
{
foreach ($matches as $group => $groupMatches) {
foreach ($groupMatches as $match) {
diff --git a/vendor/maennchen/zipstream-php/.php-cs-fixer.dist.php b/vendor/maennchen/zipstream-php/.php-cs-fixer.dist.php
index 31c8f0c..af9423c 100644
--- a/vendor/maennchen/zipstream-php/.php-cs-fixer.dist.php
+++ b/vendor/maennchen/zipstream-php/.php-cs-fixer.dist.php
@@ -71,4 +71,5 @@ return $config->setRules([
])
->setFinder($finder)
->setRiskyAllowed(true)
+ ->setUnsupportedPhpVersionAllowed(true)
->setParallelConfig(Runner\Parallel\ParallelConfigFactory::detect());
diff --git a/vendor/maennchen/zipstream-php/.tool-versions b/vendor/maennchen/zipstream-php/.tool-versions
index abc068c..727f5c9 100644
--- a/vendor/maennchen/zipstream-php/.tool-versions
+++ b/vendor/maennchen/zipstream-php/.tool-versions
@@ -1 +1 @@
-php 8.4.10
+php 8.5.0
diff --git a/vendor/maennchen/zipstream-php/composer.json b/vendor/maennchen/zipstream-php/composer.json
index 529c244..f45acd4 100644
--- a/vendor/maennchen/zipstream-php/composer.json
+++ b/vendor/maennchen/zipstream-php/composer.json
@@ -32,7 +32,7 @@
"ext-zip": "*",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
- "friendsofphp/php-cs-fixer": "^3.16",
+ "friendsofphp/php-cs-fixer": "^3.86",
"vimeo/psalm": "^6.0",
"brianium/paratest": "^7.7"
},
diff --git a/vendor/maennchen/zipstream-php/guides/LargeFilesToS3.rst b/vendor/maennchen/zipstream-php/guides/LargeFilesToS3.rst
new file mode 100644
index 0000000..2330b85
--- /dev/null
+++ b/vendor/maennchen/zipstream-php/guides/LargeFilesToS3.rst
@@ -0,0 +1,245 @@
+Compressing large files to S3 compatible storages
+=============
+
+S3 compatible storages usually have a limitation of 5 GiB when using single upload. When generating larger zips, the
+solution is to use a `multi-part upload`_.
+
+.. _multi-part upload: `
+
+We can implement a `PSR-7 stream `_ that buffers ZipStream's output and uploads
+to S3 in chunks.
+
+MultipartUploadBufferStream example
+---------------
+
+.. code-block:: php
+ client->createMultipartUpload(
+ [
+ 'Bucket' => $bucket,
+ 'Key' => $destinationFileName,
+ 'StorageClass' => 'REDUCED_REDUNDANCY',
+ ]
+ );
+
+ $this->uploadId = $result['UploadId'];
+
+ $this->parts['Parts'] = [];
+ }
+
+ public function write($string): int
+ {
+ $chunkSize = strlen($string);
+
+ $this->buffer .= $string;
+ $this->bufferSize += $chunkSize;
+
+ if ($this->bufferSize >= self::PART_SIZE) {
+ $this->uploadPart();
+ }
+
+ return $chunkSize;
+ }
+
+ public function close(): void
+ {
+ // Upload remaining closing bytes from zip
+ $this->uploadPart();
+
+ $this->client->completeMultipartUpload([
+ 'Bucket' => $this->bucket,
+ 'Key' => $this->destinationFileName,
+ 'UploadId' => $this->uploadId,
+ 'MultipartUpload' => $this->parts,
+ ]);
+
+ $this->buffer = '';
+ $this->bufferSize = 0;
+ }
+
+ private function uploadPart(): void
+ {
+ $result = $this->client->uploadPart([
+ 'Bucket' => $this->bucket,
+ 'Key' => $this->destinationFileName,
+ 'UploadId' => $this->uploadId,
+ 'PartNumber' => $this->partNumber,
+ 'Body' => $this->buffer,
+ ]);
+
+ $this->buffer = '';
+ $this->bufferSize = 0;
+
+ $this->parts['Parts'][$this->partNumber] = [
+ 'PartNumber' => $this->partNumber,
+ 'ETag' => $result['ETag'],
+ ];
+
+ $this->partNumber++;
+
+ $result = null;
+ gc_collect_cycles(); // To avoid memory leaks. @see github.com/aws/aws-sdk-php/issues/1273
+ }
+
+ public function __toString(): string
+ {
+ return $this->getContents();
+ }
+
+ public function getContents(): string
+ {
+ $buffer = $this->buffer;
+ $this->buffer = '';
+ $this->bufferSize = 0;
+
+ return $buffer;
+ }
+
+ public function detach(): null
+ {
+ $this->close();
+
+ return null;
+ }
+
+ public function getSize(): int
+ {
+ return $this->bufferSize;
+ }
+
+ public function isReadable(): bool
+ {
+ return true;
+ }
+
+ public function isWritable(): bool
+ {
+ return true;
+ }
+
+ public function isSeekable(): bool
+ {
+ return false;
+ }
+
+ public function rewind(): void
+ {
+ $this->seek(0);
+ }
+
+ public function seek($offset, $whence = SEEK_SET): void
+ {
+ throw new \RuntimeException('Cannot seek a BufferStream');
+ }
+
+ public function eof(): bool
+ {
+ return $this->bufferSize === 0;
+ }
+
+ public function tell(): int
+ {
+ throw new \RuntimeException('Cannot determine the position of a BufferStream');
+ }
+
+ public function read($length): string
+ {
+ $currentLength = $this->bufferSize;
+
+ if ($length >= $currentLength) {
+ // No need to slice the buffer because we don't have enough data.
+ $result = $this->buffer;
+ $this->buffer = '';
+ $this->bufferSize = 0;
+ } else {
+ // Slice up the result to provide a subset of the buffer.
+ $result = substr($this->buffer, 0, $length);
+ $this->buffer = substr($this->buffer, $length);
+ $this->bufferSize -= $length;
+ }
+
+ return $result;
+ }
+
+ public function getMetadata($key = null)
+ {
+ return $key ? null : [];
+ }
+ }
+
+Then, we can use it with ZipStream to compress the files and upload the parts to the storage.
+
+ZipStream usage
+---------------
+
+.. code-block:: php
+ 'your region',
+ 'version' => 'latest',
+ 'bucketName' => $bucket,
+ 'credentials' => CredentialProvider::defaultProvider(),
+ ]);
+
+ $bufferStream = new MultipartUploadBufferStream(
+ 'destination-file.zip',
+ $bucket,
+ $client,
+ );
+
+ $zip = new ZipStream(
+ outputStream: $destination,
+ defaultCompressionMethod: CompressionMethod::STORE,
+ defaultEnableZeroHeader: true,
+ sendHttpHeaders: false,
+ );
+
+ $zip->addFile(
+ fileName: 'big-file-1.txt',
+ data: 'File1 data',
+ );
+ $zip->addFile(
+ fileName: 'big-file-2.txt',
+ data: 'File2 data',
+ );
+
+ $zip->finish();
+ $destination->close(); // Needed after $zip->finish() to upload the last remaining bytes to S3
+
+You can read more about the logic behind this implementation in the `discussion`_.
+
+.. _discussion: https://github.com/maennchen/ZipStream-PHP/discussions/402
diff --git a/vendor/maennchen/zipstream-php/guides/index.rst b/vendor/maennchen/zipstream-php/guides/index.rst
index 70211c0..8d429a4 100644
--- a/vendor/maennchen/zipstream-php/guides/index.rst
+++ b/vendor/maennchen/zipstream-php/guides/index.rst
@@ -16,6 +16,7 @@ user, which is much faster. It can work with S3 buckets or any PSR7 Stream.
Nginx
Varnish
ContentLength
+ LargeFilesToS3
Installation
---------------
diff --git a/vendor/maennchen/zipstream-php/src/File.php b/vendor/maennchen/zipstream-php/src/File.php
index 148fc55..d597e95 100644
--- a/vendor/maennchen/zipstream-php/src/File.php
+++ b/vendor/maennchen/zipstream-php/src/File.php
@@ -110,8 +110,8 @@ final class File
$detectedSize = $forecastSize ?? ($this->compressedSize > 0 ? $this->compressedSize : null);
if (
- $this->isSimulation() &&
- $detectedSize !== null
+ $this->isSimulation()
+ && $detectedSize !== null
) {
$this->uncompressedSize = $detectedSize;
$this->compressedSize = $detectedSize;
@@ -202,10 +202,10 @@ final class File
compressionMethod: $this->compressionMethod,
lastModificationDateTime: $this->lastModificationDateTime,
crc32UncompressedData: $this->crc,
- compressedSize: $zip64Enabled
+ compressedSize: ($forceEnableZip64 || $this->compressedSize > 0xFFFFFFFF)
? 0xFFFFFFFF
: $this->compressedSize,
- uncompressedSize: $zip64Enabled
+ uncompressedSize: ($forceEnableZip64 || $this->uncompressedSize > 0xFFFFFFFF)
? 0xFFFFFFFF
: $this->uncompressedSize,
fileName: $this->fileName,
@@ -238,8 +238,8 @@ final class File
// Sets Bit 11: Language encoding flag (EFS). If this bit is set,
// the filename and comment fields for this file
// MUST be encoded using UTF-8. (see APPENDIX D)
- if (mb_check_encoding($this->fileName, 'UTF-8') &&
- mb_check_encoding($this->comment, 'UTF-8')) {
+ if (mb_check_encoding($this->fileName, 'UTF-8')
+ && mb_check_encoding($this->comment, 'UTF-8')) {
$this->generalPurposeBitFlag |= GeneralPurposeBitFlag::EFS;
}
}
@@ -321,9 +321,9 @@ final class File
$deflate = $this->compressionInit();
while (
- !feof($this->unpackStream()) &&
- ($this->maxSize === null || $this->uncompressedSize < $this->maxSize) &&
- ($this->exactSize === null || $this->uncompressedSize < $this->exactSize)
+ !feof($this->unpackStream())
+ && ($this->maxSize === null || $this->uncompressedSize < $this->maxSize)
+ && ($this->exactSize === null || $this->uncompressedSize < $this->exactSize)
) {
$readLength = min(
($this->maxSize ?? PHP_INT_MAX) - $this->uncompressedSize,
diff --git a/vendor/maennchen/zipstream-php/src/Time.php b/vendor/maennchen/zipstream-php/src/Time.php
index 1b4121c..f8fd7b2 100644
--- a/vendor/maennchen/zipstream-php/src/Time.php
+++ b/vendor/maennchen/zipstream-php/src/Time.php
@@ -29,11 +29,11 @@ abstract class Time
[$year, $month, $day, $hour, $minute, $second] = explode(' ', $dateTime->format('Y n j G i s'));
return
- ((int) $year << 25) |
- ((int) $month << 21) |
- ((int) $day << 16) |
- ((int) $hour << 11) |
- ((int) $minute << 5) |
- ((int) $second >> 1);
+ ((int) $year << 25)
+ | ((int) $month << 21)
+ | ((int) $day << 16)
+ | ((int) $hour << 11)
+ | ((int) $minute << 5)
+ | ((int) $second >> 1);
}
}
diff --git a/vendor/maennchen/zipstream-php/src/Zip64/ExtendedInformationExtraField.php b/vendor/maennchen/zipstream-php/src/Zip64/ExtendedInformationExtraField.php
index aaac51c..e73a010 100644
--- a/vendor/maennchen/zipstream-php/src/Zip64/ExtendedInformationExtraField.php
+++ b/vendor/maennchen/zipstream-php/src/Zip64/ExtendedInformationExtraField.php
@@ -23,10 +23,10 @@ abstract class ExtendedInformationExtraField
new PackField(format: 'v', value: self::TAG),
new PackField(
format: 'v',
- value: ($originalSize === null ? 0 : 8) +
- ($compressedSize === null ? 0 : 8) +
- ($relativeHeaderOffset === null ? 0 : 8) +
- ($diskStartNumber === null ? 0 : 4)
+ value: ($originalSize === null ? 0 : 8)
+ + ($compressedSize === null ? 0 : 8)
+ + ($relativeHeaderOffset === null ? 0 : 8)
+ + ($diskStartNumber === null ? 0 : 4)
),
...($originalSize === null ? [] : [
new PackField(format: 'P', value: $originalSize),
diff --git a/vendor/maennchen/zipstream-php/src/ZipStream.php b/vendor/maennchen/zipstream-php/src/ZipStream.php
index d7b939d..c509a68 100644
--- a/vendor/maennchen/zipstream-php/src/ZipStream.php
+++ b/vendor/maennchen/zipstream-php/src/ZipStream.php
@@ -714,9 +714,9 @@ class ZipStream
}
// Add 64bit headers (if applicable)
- if (count($this->centralDirectoryRecords) >= 0xFFFF ||
- $centralDirectoryStartOffsetOnDisk > 0xFFFFFFFF ||
- $sizeOfCentralDirectory > 0xFFFFFFFF) {
+ if (count($this->centralDirectoryRecords) >= 0xFFFF
+ || $centralDirectoryStartOffsetOnDisk > 0xFFFFFFFF
+ || $sizeOfCentralDirectory > 0xFFFFFFFF) {
if (!$this->enableZip64) {
throw new OverflowException();
}
diff --git a/vendor/maennchen/zipstream-php/test/CallbackOutputTest.php b/vendor/maennchen/zipstream-php/test/CallbackOutputTest.php
index 1f185fe..23e3a4a 100644
--- a/vendor/maennchen/zipstream-php/test/CallbackOutputTest.php
+++ b/vendor/maennchen/zipstream-php/test/CallbackOutputTest.php
@@ -24,7 +24,9 @@ final class CallbackOutputTest extends TestCase
$buf = '';
$zip = new ZipStream(
outputStream: CallbackStreamWrapper::open(
- static function (string $chunk) use (&$buf): void { $buf .= $chunk; }
+ static function (string $chunk) use (&$buf): void {
+ $buf .= $chunk;
+ }
),
sendHttpHeaders: false
);
@@ -53,10 +55,14 @@ final class CallbackOutputTest extends TestCase
$buf2 = '';
$stream1 = CallbackStreamWrapper::open(
- static function (string $chunk) use (&$buf1): void { $buf1 .= $chunk; }
+ static function (string $chunk) use (&$buf1): void {
+ $buf1 .= $chunk;
+ }
);
$stream2 = CallbackStreamWrapper::open(
- static function (string $chunk) use (&$buf2): void { $buf2 .= $chunk; }
+ static function (string $chunk) use (&$buf2): void {
+ $buf2 .= $chunk;
+ }
);
$this->assertIsResource($stream1);
@@ -112,7 +118,8 @@ final class CallbackOutputTest extends TestCase
public function testStreamPositionTracking(): void
{
$stream = CallbackStreamWrapper::open(
- static function (string $chunk): void { /* no-op */ }
+ static function (string $chunk): void { // no-op
+ }
);
$this->assertSame(0, ftell($stream));
@@ -129,7 +136,8 @@ final class CallbackOutputTest extends TestCase
public function testInvalidModeRejection(): void
{
$stream = CallbackStreamWrapper::open(
- static function (string $chunk): void { /* no-op */ }
+ static function (string $chunk): void { // no-op
+ }
);
// Close the stream first
@@ -143,7 +151,8 @@ final class CallbackOutputTest extends TestCase
public function testStreamStatistics(): void
{
$stream = CallbackStreamWrapper::open(
- static function (string $chunk): void { /* no-op */ }
+ static function (string $chunk): void { // no-op
+ }
);
fwrite($stream, 'test data');
diff --git a/vendor/maennchen/zipstream-php/test/CentralDirectoryFileHeaderTest.php b/vendor/maennchen/zipstream-php/test/CentralDirectoryFileHeaderTest.php
index 5457b4f..3e9e385 100644
--- a/vendor/maennchen/zipstream-php/test/CentralDirectoryFileHeaderTest.php
+++ b/vendor/maennchen/zipstream-php/test/CentralDirectoryFileHeaderTest.php
@@ -35,26 +35,26 @@ class CentralDirectoryFileHeaderTest extends TestCase
$this->assertSame(
bin2hex($header),
- '504b0102' . // 4 bytes; central file header signature
- '0306' . // 2 bytes; version made by
- '2d00' . // 2 bytes; version needed to extract
- '2222' . // 2 bytes; general purpose bit flag
- '0800' . // 2 bytes; compression method
- '2008' . // 2 bytes; last mod file time
- '2154' . // 2 bytes; last mod file date
- '11111111' . // 4 bytes; crc-32
- '77777777' . // 4 bytes; compressed size
- '99999999' . // 4 bytes; uncompressed size
- '0800' . // 2 bytes; file name length (n)
- '0c00' . // 2 bytes; extra field length (m)
- '0c00' . // 2 bytes; file comment length (o)
- '0000' . // 2 bytes; disk number start
- '0000' . // 2 bytes; internal file attributes
- '20000000' . // 4 bytes; external file attributes
- '34120000' . // 4 bytes; relative offset of local header
- '746573742e706e67' . // n bytes; file name
- '736f6d6520636f6e74656e74' . // m bytes; extra field
- '736f6d6520636f6d6d656e74' // o bytes; file comment
+ '504b0102' // 4 bytes; central file header signature
+ . '0306' // 2 bytes; version made by
+ . '2d00' // 2 bytes; version needed to extract
+ . '2222' // 2 bytes; general purpose bit flag
+ . '0800' // 2 bytes; compression method
+ . '2008' // 2 bytes; last mod file time
+ . '2154' // 2 bytes; last mod file date
+ . '11111111' // 4 bytes; crc-32
+ . '77777777' // 4 bytes; compressed size
+ . '99999999' // 4 bytes; uncompressed size
+ . '0800' // 2 bytes; file name length (n)
+ . '0c00' // 2 bytes; extra field length (m)
+ . '0c00' // 2 bytes; file comment length (o)
+ . '0000' // 2 bytes; disk number start
+ . '0000' // 2 bytes; internal file attributes
+ . '20000000' // 4 bytes; external file attributes
+ . '34120000' // 4 bytes; relative offset of local header
+ . '746573742e706e67' // n bytes; file name
+ . '736f6d6520636f6e74656e74' // m bytes; extra field
+ . '736f6d6520636f6d6d656e74' // o bytes; file comment
);
}
}
diff --git a/vendor/maennchen/zipstream-php/test/DataDescriptorTest.php b/vendor/maennchen/zipstream-php/test/DataDescriptorTest.php
index cc886c7..349f87d 100644
--- a/vendor/maennchen/zipstream-php/test/DataDescriptorTest.php
+++ b/vendor/maennchen/zipstream-php/test/DataDescriptorTest.php
@@ -17,10 +17,10 @@ class DataDescriptorTest extends TestCase
compressedSize: 0x77777777,
uncompressedSize: 0x99999999,
)),
- '504b0708' . // 4 bytes; Optional data descriptor signature = 0x08074b50
- '11111111' . // 4 bytes; CRC-32 of uncompressed data
- '77777777' . // 4 bytes; Compressed size
- '99999999' // 4 bytes; Uncompressed size
+ '504b0708' // 4 bytes; Optional data descriptor signature = 0x08074b50
+ . '11111111' // 4 bytes; CRC-32 of uncompressed data
+ . '77777777' // 4 bytes; Compressed size
+ . '99999999' // 4 bytes; Uncompressed size
);
}
}
diff --git a/vendor/maennchen/zipstream-php/test/EndOfCentralDirectoryTest.php b/vendor/maennchen/zipstream-php/test/EndOfCentralDirectoryTest.php
index be0a907..0b808d8 100644
--- a/vendor/maennchen/zipstream-php/test/EndOfCentralDirectoryTest.php
+++ b/vendor/maennchen/zipstream-php/test/EndOfCentralDirectoryTest.php
@@ -21,15 +21,15 @@ class EndOfCentralDirectoryTest extends TestCase
centralDirectoryStartOffsetOnDisk: 0x33,
zipFileComment: 'foo',
)),
- '504b0506' . // 4 bytes; end of central dir signature 0x06054b50
- '0000' . // 2 bytes; number of this disk
- '0000' . // 2 bytes; number of the disk with the start of the central directory
- '1000' . // 2 bytes; total number of entries in the central directory on this disk
- '1000' . // 2 bytes; total number of entries in the central directory
- '22000000' . // 4 bytes; size of the central directory
- '33000000' . // 4 bytes; offset of start of central directory with respect to the starting disk number
- '0300' . // 2 bytes; .ZIP file comment length
- bin2hex('foo')
+ '504b0506' // 4 bytes; end of central dir signature 0x06054b50
+ . '0000' // 2 bytes; number of this disk
+ . '0000' // 2 bytes; number of the disk with the start of the central directory
+ . '1000' // 2 bytes; total number of entries in the central directory on this disk
+ . '1000' // 2 bytes; total number of entries in the central directory
+ . '22000000' // 4 bytes; size of the central directory
+ . '33000000' // 4 bytes; offset of start of central directory with respect to the starting disk number
+ . '0300' // 2 bytes; .ZIP file comment length
+ . bin2hex('foo')
);
}
}
diff --git a/vendor/maennchen/zipstream-php/test/EndlessCycleStream.php b/vendor/maennchen/zipstream-php/test/EndlessCycleStream.php
index d9e7df1..1020c02 100644
--- a/vendor/maennchen/zipstream-php/test/EndlessCycleStream.php
+++ b/vendor/maennchen/zipstream-php/test/EndlessCycleStream.php
@@ -97,7 +97,7 @@ class EndlessCycleStream implements StreamInterface
throw new RuntimeException('Infinite Stream!');
}
- public function getMetadata(?string $key = null): array|null
+ public function getMetadata(?string $key = null): ?array
{
return $key !== null ? null : [];
}
diff --git a/vendor/maennchen/zipstream-php/test/LocalFileHeaderTest.php b/vendor/maennchen/zipstream-php/test/LocalFileHeaderTest.php
index 196dd0f..6510e78 100644
--- a/vendor/maennchen/zipstream-php/test/LocalFileHeaderTest.php
+++ b/vendor/maennchen/zipstream-php/test/LocalFileHeaderTest.php
@@ -29,19 +29,19 @@ class LocalFileHeaderTest extends TestCase
$this->assertSame(
bin2hex((string) $header),
- '504b0304' . // 4 bytes; Local file header signature
- '2d00' . // 2 bytes; Version needed to extract (minimum)
- '2222' . // 2 bytes; General purpose bit flag
- '0800' . // 2 bytes; Compression method; e.g. none = 0, DEFLATE = 8
- '2008' . // 2 bytes; File last modification time
- '2154' . // 2 bytes; File last modification date
- '11111111' . // 4 bytes; CRC-32 of uncompressed data
- '77777777' . // 4 bytes; Compressed size (or 0xffffffff for ZIP64)
- '99999999' . // 4 bytes; Uncompressed size (or 0xffffffff for ZIP64)
- '0800' . // 2 bytes; File name length (n)
- '0c00' . // 2 bytes; Extra field length (m)
- '746573742e706e67' . // n bytes; File name
- '736f6d6520636f6e74656e74' // m bytes; Extra field
+ '504b0304' // 4 bytes; Local file header signature
+ . '2d00' // 2 bytes; Version needed to extract (minimum)
+ . '2222' // 2 bytes; General purpose bit flag
+ . '0800' // 2 bytes; Compression method; e.g. none = 0, DEFLATE = 8
+ . '2008' // 2 bytes; File last modification time
+ . '2154' // 2 bytes; File last modification date
+ . '11111111' // 4 bytes; CRC-32 of uncompressed data
+ . '77777777' // 4 bytes; Compressed size (or 0xffffffff for ZIP64)
+ . '99999999' // 4 bytes; Uncompressed size (or 0xffffffff for ZIP64)
+ . '0800' // 2 bytes; File name length (n)
+ . '0c00' // 2 bytes; Extra field length (m)
+ . '746573742e706e67' // n bytes; File name
+ . '736f6d6520636f6e74656e74' // m bytes; Extra field
);
}
}
diff --git a/vendor/maennchen/zipstream-php/test/Tempfile.php b/vendor/maennchen/zipstream-php/test/Tempfile.php
index 7ef9c61..f03ee6c 100644
--- a/vendor/maennchen/zipstream-php/test/Tempfile.php
+++ b/vendor/maennchen/zipstream-php/test/Tempfile.php
@@ -6,7 +6,7 @@ namespace ZipStream\Test;
trait Tempfile
{
- protected string|null $tempfile;
+ protected ?string $tempfile;
/**
* @var resource
diff --git a/vendor/maennchen/zipstream-php/test/Zip64/DataDescriptorTest.php b/vendor/maennchen/zipstream-php/test/Zip64/DataDescriptorTest.php
index 49fb2cc..8864b85 100644
--- a/vendor/maennchen/zipstream-php/test/Zip64/DataDescriptorTest.php
+++ b/vendor/maennchen/zipstream-php/test/Zip64/DataDescriptorTest.php
@@ -19,10 +19,10 @@ class DataDescriptorTest extends TestCase
$this->assertSame(
bin2hex($descriptor),
- '504b0708' . // 4 bytes; Optional data descriptor signature = 0x08074b50
- '11111111' . // 4 bytes; CRC-32 of uncompressed data
- '6666666677777777' . // 8 bytes; Compressed size
- '8888888899999999' // 8 bytes; Uncompressed size
+ '504b0708' // 4 bytes; Optional data descriptor signature = 0x08074b50
+ . '11111111' // 4 bytes; CRC-32 of uncompressed data
+ . '6666666677777777' // 8 bytes; Compressed size
+ . '8888888899999999' // 8 bytes; Uncompressed size
);
}
}
diff --git a/vendor/maennchen/zipstream-php/test/Zip64/EndOfCentralDirectoryLocatorTest.php b/vendor/maennchen/zipstream-php/test/Zip64/EndOfCentralDirectoryLocatorTest.php
index 271a298..0f905db 100644
--- a/vendor/maennchen/zipstream-php/test/Zip64/EndOfCentralDirectoryLocatorTest.php
+++ b/vendor/maennchen/zipstream-php/test/Zip64/EndOfCentralDirectoryLocatorTest.php
@@ -19,10 +19,10 @@ class EndOfCentralDirectoryLocatorTest extends TestCase
$this->assertSame(
bin2hex($descriptor),
- '504b0607' . // 4 bytes; zip64 end of central dir locator signature - 0x07064b50
- '11111111' . // 4 bytes; number of the disk with the start of the zip64 end of central directory
- '3333333322222222' . // 28 bytes; relative offset of the zip64 end of central directory record
- '44444444' // 4 bytes;total number of disks
+ '504b0607' // 4 bytes; zip64 end of central dir locator signature - 0x07064b50
+ . '11111111' // 4 bytes; number of the disk with the start of the zip64 end of central directory
+ . '3333333322222222' // 28 bytes; relative offset of the zip64 end of central directory record
+ . '44444444' // 4 bytes;total number of disks
);
}
}
diff --git a/vendor/maennchen/zipstream-php/test/Zip64/EndOfCentralDirectoryTest.php b/vendor/maennchen/zipstream-php/test/Zip64/EndOfCentralDirectoryTest.php
index b86fb17..ef046f9 100644
--- a/vendor/maennchen/zipstream-php/test/Zip64/EndOfCentralDirectoryTest.php
+++ b/vendor/maennchen/zipstream-php/test/Zip64/EndOfCentralDirectoryTest.php
@@ -25,17 +25,17 @@ class EndOfCentralDirectoryTest extends TestCase
$this->assertSame(
bin2hex($descriptor),
- '504b0606' . // 4 bytes;zip64 end of central dir signature - 0x06064b50
- '2f00000000000000' . // 8 bytes; size of zip64 end of central directory record
- '3333' . // 2 bytes; version made by
- '4444' . // 2 bytes; version needed to extract
- '55555555' . // 4 bytes; number of this disk
- '66666666' . // 4 bytes; number of the disk with the start of the central directory
- '8888888877777777' . // 8 bytes; total number of entries in the central directory on this disk
- 'aaaaaaaa99999999' . // 8 bytes; total number of entries in the central directory
- 'ccccccccbbbbbbbb' . // 8 bytes; size of the central directory
- 'eeeeeeeedddddddd' . // 8 bytes; offset of start of central directory with respect to the starting disk number
- bin2hex('foo')
+ '504b0606' // 4 bytes;zip64 end of central dir signature - 0x06064b50
+ . '2f00000000000000' // 8 bytes; size of zip64 end of central directory record
+ . '3333' // 2 bytes; version made by
+ . '4444' // 2 bytes; version needed to extract
+ . '55555555' // 4 bytes; number of this disk
+ . '66666666' // 4 bytes; number of the disk with the start of the central directory
+ . '8888888877777777' // 8 bytes; total number of entries in the central directory on this disk
+ . 'aaaaaaaa99999999' // 8 bytes; total number of entries in the central directory
+ . 'ccccccccbbbbbbbb' // 8 bytes; size of the central directory
+ . 'eeeeeeeedddddddd' // 8 bytes; offset of start of central directory with respect to the starting disk number
+ . bin2hex('foo')
);
}
}
diff --git a/vendor/maennchen/zipstream-php/test/Zip64/ExtendedInformationExtraFieldTest.php b/vendor/maennchen/zipstream-php/test/Zip64/ExtendedInformationExtraFieldTest.php
index 904783d..4dde274 100644
--- a/vendor/maennchen/zipstream-php/test/Zip64/ExtendedInformationExtraFieldTest.php
+++ b/vendor/maennchen/zipstream-php/test/Zip64/ExtendedInformationExtraFieldTest.php
@@ -20,12 +20,12 @@ class ExtendedInformationExtraFieldTest extends TestCase
$this->assertSame(
bin2hex($extraField),
- '0100' . // 2 bytes; Tag for this "extra" block type
- '1c00' . // 2 bytes; Size of this "extra" block
- '6666666677777777' . // 8 bytes; Original uncompressed file size
- '8888888899999999' . // 8 bytes; Size of compressed data
- '1111111122222222' . // 8 bytes; Offset of local header record
- '33333333' // 4 bytes; Number of the disk on which this file starts
+ '0100' // 2 bytes; Tag for this "extra" block type
+ . '1c00' // 2 bytes; Size of this "extra" block
+ . '6666666677777777' // 8 bytes; Original uncompressed file size
+ . '8888888899999999' // 8 bytes; Size of compressed data
+ . '1111111122222222' // 8 bytes; Offset of local header record
+ . '33333333' // 4 bytes; Number of the disk on which this file starts
);
}
@@ -35,8 +35,8 @@ class ExtendedInformationExtraFieldTest extends TestCase
$this->assertSame(
bin2hex($extraField),
- '0100' . // 2 bytes; Tag for this "extra" block type
- '0000' // 2 bytes; Size of this "extra" block
+ '0100' // 2 bytes; Tag for this "extra" block type
+ . '0000' // 2 bytes; Size of this "extra" block
);
}
}
diff --git a/vendor/maennchen/zipstream-php/test/ZipStreamTest.php b/vendor/maennchen/zipstream-php/test/ZipStreamTest.php
index f4ead1d..421b59e 100644
--- a/vendor/maennchen/zipstream-php/test/ZipStreamTest.php
+++ b/vendor/maennchen/zipstream-php/test/ZipStreamTest.php
@@ -62,10 +62,10 @@ class ZipStreamTest extends TestCase
$name = 'árvíztűrő tükörfúrógép.txt';
$content = 'Sample String Data';
- $comment =
- 'Filename has every special characters ' .
- 'from Hungarian language in lowercase. ' .
- 'In uppercase: ÁÍŰŐÜÖÚÓÉ';
+ $comment
+ = 'Filename has every special characters '
+ . 'from Hungarian language in lowercase. '
+ . 'In uppercase: ÁÍŰŐÜÖÚÓÉ';
$zip->addFile(fileName: $name, data: $content, comment: $comment);
$zip->finish();
@@ -327,11 +327,14 @@ class ZipStreamTest extends TestCase
defaultEnableZeroHeader: false,
);
- if (file_exists('/dev/null')) {
- $streamUnseekable = fopen('/dev/null', 'w+');
- } elseif (file_exists('NUL')) {
- $streamUnseekable = fopen('NUL', 'w+');
- } else {
+ foreach (['cat', 'more'] as $cmd) {
+ $streamUnseekable = @popen($cmd, 'w');
+ if ($streamUnseekable !== false) {
+ break;
+ }
+ }
+
+ if ($streamUnseekable === false) {
$this->markTestSkipped('Needs file /dev/null');
}
@@ -546,12 +549,24 @@ class ZipStreamTest extends TestCase
);
}
+ $zip->addDirectory('mydir/');
+
$zip->finish();
$tmpDir = $this->validateAndExtractZip($this->tempfile);
$files = $this->getRecursiveFileList($tmpDir);
$this->assertSame(['sample0', 'sample1', 'sample2', 'sample3'], $files);
+
+ $this->assertFileDoesNotContain(
+ $this->tempfile,
+ PackField::pack(
+ new PackField(format: 'V', value: 0x00000000), // CRC-32 = 0 (directory)
+ new PackField(format: 'V', value: 0xFFFFFFFF), // compressedSize — bug sentinel
+ new PackField(format: 'V', value: 0xFFFFFFFF), // uncompressedSize — bug sentinel
+ new PackField(format: 'v', value: 6), // filename length = strlen('mydir/')
+ )
+ );
}
#[Group('slow')]
diff --git a/vendor/maennchen/zipstream-php/test/Zs/ExtendedInformationExtraFieldTest.php b/vendor/maennchen/zipstream-php/test/Zs/ExtendedInformationExtraFieldTest.php
index 2b8dbed..36153a7 100644
--- a/vendor/maennchen/zipstream-php/test/Zs/ExtendedInformationExtraFieldTest.php
+++ b/vendor/maennchen/zipstream-php/test/Zs/ExtendedInformationExtraFieldTest.php
@@ -15,8 +15,8 @@ class ExtendedInformationExtraFieldTest extends TestCase
$this->assertSame(
bin2hex((string) $extraField),
- '5356' . // 2 bytes; Tag for this "extra" block type
- '0000' // 2 bytes; TODO: Document
+ '5356' // 2 bytes; Tag for this "extra" block type
+ . '0000' // 2 bytes; TODO: Document
);
}
}
diff --git a/vendor/phpoffice/phpspreadsheet/CHANGELOG.md b/vendor/phpoffice/phpspreadsheet/CHANGELOG.md
index 4e36a8d..049eefd 100644
--- a/vendor/phpoffice/phpspreadsheet/CHANGELOG.md
+++ b/vendor/phpoffice/phpspreadsheet/CHANGELOG.md
@@ -3,9 +3,13 @@
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com)
-and this project adheres to [Semantic Versioning](https://semver.org). Thia is always true of the master branch. Some earlier branches remain supported and security fixes are applied to them; if the security fix represents a breaking change, it may have to be applied as a minor or patch version.
+and this project adheres to [Semantic Versioning](https://semver.org). This is always true of the master branch.
+Dropping support for EOL versions of PHP will not be considered
+a breaking change.
-## TBD - 5.3.0
+Some earlier branches remain supported and security fixes are applied to them; if the security fix represents a breaking change, it may have to be applied as a minor or patch version.
+
+## TBD - 5.11.0
### Added
@@ -31,6 +35,210 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
- Nothing yet.
+## 2026-09-15 - 5.10.0
+
+### Added
+
+- XLSX password-to-open encryption and decryption using Office Agile encryption. [Issue #3878](https://github.com/PHPOffice/PhpSpreadsheet/issues/3878) [PR #4975](https://github.com/PHPOffice/PhpSpreadsheet/pull/4975) [PR #4988](https://github.com/PHPOffice/PhpSpreadsheet/pull/4988)
+- Support for Excel sparklines (line, column, and win/loss) in Xlsx reader and writer. [Issue #4941](https://github.com/PHPOffice/PhpSpreadsheet/issues/4941)
+- Read-only object model for Pivot Tables. Existing pivot tables in an Xlsx file are now parsed into `Worksheet\PivotTable\PivotTable` objects (name, location, source cache definition, and row/column/page/data field layout), accessible via `Worksheet::getPivotTableCollection()` / `getPivotTableByName()`. Pivot tables (their tables, caches and records) are now also preserved through an Xlsx load/save round-trip instead of being silently dropped. [Issue #4534](https://github.com/PHPOffice/PhpSpreadsheet/issues/4534)
+
+### Changed
+
+- Performance: avoid `Worksheet::getStyle()` on every `Cell::setValueExplicit()` unless quote-prefix must change. On a dense 40k-cell populate+save microbenchmark this cut wall time by ~5%. [PR #4958](https://github.com/PHPOffice/PhpSpreadsheet/pull/4958)
+- Infrastructure changes for documentation. [PR #4973](https://github.com/PHPOffice/PhpSpreadsheet/pull/4973)
+
+### Fixed
+
+- Security patches.
+- Xls Writer now creates conformant CFB/DIFAT metadata at FAT-sector boundaries. [Issue #4811](https://github.com/PHPOffice/PhpSpreadsheet/issues/4811) [PR #4983](https://github.com/PHPOffice/PhpSpreadsheet/pull/4983)
+- Ods Reader/Writer Drawings. [Issue #4809](https://github.com/PHPOffice/PhpSpreadsheet/issues/4809) [PR #4956](https://github.com/PHPOffice/PhpSpreadsheet/pull/4956)
+- Ods Writer no longer converts cell references or commas inside string literals, so `="THIS IS E1"` is written unchanged instead of as `="THIS IS [.E1]"`. [Issue #4454](https://github.com/PHPOffice/PhpSpreadsheet/issues/4454) [PR #4962](https://github.com/PHPOffice/PhpSpreadsheet/pull/4962)
+- Ods Reader/Writer slight improvement for Date styles. [PR #4960](https://github.com/PHPOffice/PhpSpreadsheet/pull/4960)
+- BETAINV/BETA.INV no longer abandons its search when the Beta CDF underflows to zero (wrong results for alpha above about 1080). [PR #4954](https://github.com/PHPOffice/PhpSpreadsheet/pull/4954)
+- GAMMA.INV, the GAMMA.DIST/CHISQ.DIST/F.DIST densities, and GAMMALN no longer fail or return wrong results for large shape parameters / degrees of freedom. [PR #4953](https://github.com/PHPOffice/PhpSpreadsheet/pull/4953)
+- GAMMAINV/GAMMA.INV no longer clamps upper-tail quantiles beyond alpha*beta*5. [PR #4946](https://github.com/PHPOffice/PhpSpreadsheet/pull/4946)
+- Correct incomplete gamma convergence for GAMMA.DIST/CHISQ.DIST family (wrong once the series argument reached ~32). [PR #4945](https://github.com/PHPOffice/PhpSpreadsheet/pull/4945)
+- Fix problem with VLOOKUP and whole-column ranges. [Issue #4969](https://github.com/PHPOffice/PhpSpreadsheet/issues/4969) [PR #4967](https://github.com/PHPOffice/PhpSpreadsheet/pull/4967)
+- Problem with Xlsx Writer and header-row table. [PR #4968](https://github.com/PHPOffice/PhpSpreadsheet/pull/4968)
+- Throw when Ods Reader encounters invalid Xml. [PR #4986](https://github.com/PHPOffice/PhpSpreadsheet/pull/4986)
+- Update Sheetname in Charts when Sheetname changes. [Issue #744](https://github.com/PHPOffice/PhpSpreadsheet/issues/744) [PR #4984](https://github.com/PHPOffice/PhpSpreadsheet/pull/4984)
+
+## 2026-07-12 - 5.9.0
+
+### Removed
+
+- **Drop support for PHP 8.1**, according to [our published guidelines](https://phpspreadsheet.readthedocs.io/en/latest/#php-version-support). [PR #4907](https://github.com/PHPOffice/PhpSpreadsheet/pull/4907)
+
+### Added
+
+- Navigate Cell as Cursor. [Issue #863](https://github.com/PHPOffice/PhpSpreadsheet/issues/863) [PR #4909](https://github.com/PHPOffice/PhpSpreadsheet/pull/4909)
+- Chart DataTables. [Issue #413](https://github.com/PHPOffice/PhpSpreadsheet/issues/413) [PR #4911](https://github.com/PHPOffice/PhpSpreadsheet/pull/4911)
+- Permit Ignoring "Misleading Format" Tag. [PR #4914](https://github.com/PHPOffice/PhpSpreadsheet/pull/4914)
+
+### Changed
+
+- Restructure parsing logic in Reader/Xlsx. [PR #4830](https://github.com/PHPOffice/PhpSpreadsheet/pull/4830)
+
+### Fixed
+
+- Small improvement for Radar Charts. [Issue #661](https://github.com/PHPOffice/PhpSpreadsheet/issues/661) [PR #4908](https://github.com/PHPOffice/PhpSpreadsheet/pull/4908)
+- Allow Use of Multiple Ranges When Setting Styles. [Issue #411](https://github.com/PHPOffice/PhpSpreadsheet/issues/411) [PR #4910](https://github.com/PHPOffice/PhpSpreadsheet/pull/4910)
+- Small change to Writer Html. [Issue #434](https://github.com/PHPOffice/PhpSpreadsheet/issues/434) [PR #4912](https://github.com/PHPOffice/PhpSpreadsheet/pull/4912)
+- Avoid hard-coding some chart Xml attributes. [PR #4915](https://github.com/PHPOffice/PhpSpreadsheet/pull/4915)
+- Readers should directly access readFilter. [PR #4919](https://github.com/PHPOffice/PhpSpreadsheet/pull/4919)
+- Very minor changes to Worksheet, Reader/Xlsx, and Worksheet/AutoFilter. [Issue #4917](https://github.com/PHPOffice/PhpSpreadsheet/issues/4917) [PR #4926](https://github.com/PHPOffice/PhpSpreadsheet/pull/4926)
+- Consistent HighestRow/Column after row/columnDelete. [Issue #943](https://github.com/PHPOffice/PhpSpreadsheet/issues/943) [PR #4925](https://github.com/PHPOffice/PhpSpreadsheet/pull/4925)
+
+## 2026-06-06 - 5.8.0
+
+### Security Note
+
+- File::prohibitWrappers and Drawing::setPath now reject phar paths with extra leading slashes (e.g. phar:///…) that escaped the prior parse_url-based filter. No security exploit was possible even with the extra slashes. [PR #4876](https://github.com/PHPOffice/PhpSpreadsheet/pull/4876)
+
+### Added
+
+- Optional method to increase Calculation Engine's parsing speed. [PR #4829](https://github.com/PHPOffice/PhpSpreadsheet/pull/4829)
+- Html Writer/Reader new data attributes. [PR #4858](https://github.com/PHPOffice/PhpSpreadsheet/pull/4858)
+- IReader2 interface extending IReader with listWorksheetInfo/Names. [Issue #4883](https://github.com/PHPOffice/PhpSpreadsheet/issues/4883) [PR #4886](https://github.com/PHPOffice/PhpSpreadsheet/pull/4886)
+
+### Changed
+
+- Helper/Handler is now internal and final. [PR #4896](https://github.com/PHPOffice/PhpSpreadsheet/pull/4896)
+
+### Fixed
+
+- Html Writer handle text colors a bit better. [PR #4855](https://github.com/PHPOffice/PhpSpreadsheet/pull/4855)
+- Html Writer apply rotation to images. [Issue #4875](https://github.com/PHPOffice/PhpSpreadsheet/issues/4875) [PR #4877](https://github.com/PHPOffice/PhpSpreadsheet/pull/4877)
+- Work around Php8.6 deprecation in mpdf/mpdf. [PR #4878](https://github.com/PHPOffice/PhpSpreadsheet/pull/4878)
+- Ability to reuse disconnected spreadsheet. [PR #4880](https://github.com/PHPOffice/PhpSpreadsheet/pull/4880)
+- Allow int parameter for getChartIndex. [PR #4896](https://github.com/PHPOffice/PhpSpreadsheet/pull/4896)
+
+## 2026-04-19 - 5.7.0
+
+### Fixed
+
+- Security patches.
+
+## 2026-04-09 - 5.6.0
+
+### Added
+
+- Make Reader/Csv Extendable, add new preferred Reader/CsvNoEscape class. [Issue #4836](https://github.com/PHPOffice/PhpSpreadsheet/issues/4836) [PR #4837](https://github.com/PHPOffice/PhpSpreadsheet/pull/4837) [PR #4845](https://github.com/PHPOffice/PhpSpreadsheet/pull/4845)
+- XLOOKUP function. [Issue #1453](https://github.com/PHPOffice/PhpSpreadsheet/issues/1453) [PR #4844](https://github.com/PHPOffice/PhpSpreadsheet/pull/4844)
+- Introduction of a benchmark test suite, independent of the default unit test suite. Users can use it as a template for experimenting and making decisions concerning performance. [PR #4824](https://github.com/PHPOffice/PhpSpreadsheet/pull/4824)
+
+### Deprecated
+
+- Collection/Cells::MAX_COLUMN_ID - use Cell/AddressRange::MAX_COLUMN_INT.
+- Writer/Xls/Worksheet constants MAX_XLS_COLUMN, MAX_XLS_COLUMN_STRING, MAX_XLS_ROW - use Cell/AddressRange MAX_COLUMN_INT_XLS, MAX_COLUMN_XLS, MAX_ROW_XLS
+
+### Fixed
+
+- Consistent handling of row and column limits. [PR #4820](https://github.com/PHPOffice/PhpSpreadsheet/pull/4820)
+- Problems with Html Conditional Formatting Colorscale. [Issue #4838](https://github.com/PHPOffice/PhpSpreadsheet/issues/4838) [PR #4839](https://github.com/PHPOffice/PhpSpreadsheet/pull/4839)
+- Fixed an issue where Date detection could misclassify invalid numeric values as dates. [PR #4841](https://github.com/PHPOffice/PhpSpreadsheet/pull/4841)
+- Ods Reader/Writer Integer Styles with Leading Zeros. [Issue #1606](https://github.com/PHPOffice/PhpSpreadsheet/issues/1606) [PR #4822](https://github.com/PHPOffice/PhpSpreadsheet/pull/4822)
+- Xlsx Writer Data URI for Images. [Issue #4823](https://github.com/PHPOffice/PhpSpreadsheet/issues/4823) [PR #4831](https://github.com/PHPOffice/PhpSpreadsheet/pull/4831)
+- Confusion Checking for Union Arguments. [Issue #4832](https://github.com/PHPOffice/PhpSpreadsheet/issues/4832) [PR #4835](https://github.com/PHPOffice/PhpSpreadsheet/pull/4835)
+- Reader Xlsx Hyperlink with Anchor. [Issue #4842](https://github.com/PHPOffice/PhpSpreadsheet/issues/4842) [PR #4843](https://github.com/PHPOffice/PhpSpreadsheet/pull/4843)
+
+## 2026-02-28 - 5.5.0
+
+### Added
+
+- Much improved handling of Styles by Ods Reader. The relevant changes are listed at the end of the "Fixed" section below.
+- Option to use OldCalculatedValue in ToArray and Relatives. [Issue #1810](https://github.com/PHPOffice/PhpSpreadsheet/issues/1810) [PR #4787](https://github.com/PHPOffice/PhpSpreadsheet/pull/4787)
+- Add checkbox style (Xlsx and Html). [PR #4781](https://github.com/PHPOffice/PhpSpreadsheet/pull/4781)
+- Option to whitelist external images. [PR #4793](https://github.com/PHPOffice/PhpSpreadsheet/pull/4793)
+- Writer/Html add ability to set line ending. [PR #4779](https://github.com/PHPOffice/PhpSpreadsheet/pull/4779)
+- Writer/Html optionally save formulas as data attributes. [PR #4783](https://github.com/PHPOffice/PhpSpreadsheet/pull/4783)
+- User-supplied headers and footers in PDF. [Issue #3159](https://github.com/PHPOffice/PhpSpreadsheet/issues/3159) [PR #4789](https://github.com/PHPOffice/PhpSpreadsheet/pull/4789)
+
+### Deprecated
+
+- Writer/Html constant BODY_LINE no longer makes sense with a configurable line ending. No replacement.
+- Calculation classes FormulaParser and FormulaToken are unused. No replacement.
+- Writer/Xls/Worksheet methods insertBitMap, positionImage, writeObjPicture, processBitmapGd, and processBitmap are unused. No replacement.
+
+### Fixed
+
+- Improve performance in value binders. [PR #4780](https://github.com/PHPOffice/PhpSpreadsheet/pull/4780)
+- Handle Unions as Function Arguments. [Issue #4656](https://github.com/PHPOffice/PhpSpreadsheet/issues/4656) [Issue #316](https://github.com/PHPOffice/PhpSpreadsheet/issues/316) [Issue #503](https://github.com/PHPOffice/PhpSpreadsheet/issues/503) [PR #4657](https://github.com/PHPOffice/PhpSpreadsheet/pull/4657)
+- Unexpected Behavior of CONCATENATE. [Issue #4061](https://github.com/PHPOffice/PhpSpreadsheet/issues/4061) [PR #4797](https://github.com/PHPOffice/PhpSpreadsheet/pull/4797)
+- Image Css size in millimeters. [Issue #4800](https://github.com/PHPOffice/PhpSpreadsheet/issues/4800) [PR #4801](https://github.com/PHPOffice/PhpSpreadsheet/pull/4801)
+- Ods Reader column misalignment. [Issue #4802](https://github.com/PHPOffice/PhpSpreadsheet/issues/4802) [PR #4803](https://github.com/PHPOffice/PhpSpreadsheet/pull/4803)
+- Ods improved handling of number formats. [Issue #3961](https://github.com/PHPOffice/PhpSpreadsheet/issues/3961) [Issue #4798](https://github.com/PHPOffice/PhpSpreadsheet/issues/4798) [PR #4806](https://github.com/PHPOffice/PhpSpreadsheet/pull/4806)
+- Ods Reader fonts and fills. [Issue #2622](https://github.com/PHPOffice/PhpSpreadsheet/issues/2622) [Issue #1191](https://github.com/PHPOffice/PhpSpreadsheet/issues/1191) [PR #4810](https://github.com/PHPOffice/PhpSpreadsheet/pull/4810)
+- Ods Reader alignment and cell protection. [PR #4813](https://github.com/PHPOffice/PhpSpreadsheet/pull/4813)
+- Ods Reader borders. [PR #4814](https://github.com/PHPOffice/PhpSpreadsheet/pull/4814)
+- Ods Reader column styles (partial). [PR #4815](https://github.com/PHPOffice/PhpSpreadsheet/pull/4815)
+
+## 2026-01-10 - 5.4.0
+
+### Added
+
+- Store image in cell (Xlsx only). [Issue #4014](https://github.com/PHPOffice/PhpSpreadsheet/issues/4014) [Issue #4034](https://github.com/PHPOffice/PhpSpreadsheet/issues/4034) [Issue #913](https://github.com/PHPOffice/PhpSpreadsheet/issues/913) [PR #4677](https://github.com/PHPOffice/PhpSpreadsheet/pull/4677)
+- Passthrough support (with some restrictions) for otherwise unsupported drawing elements. [Issue #4037](https://github.com/PHPOffice/PhpSpreadsheet/issues/4037) [Issue #4704](https://github.com/PHPOffice/PhpSpreadsheet/issues/4704) [PR #4712](https://github.com/PHPOffice/PhpSpreadsheet/pull/4712)
+- Set all locale variables at once in a threadsafe manner. [Issue #954](https://github.com/PHPOffice/PhpSpreadsheet/issues/954) [PR #4760](https://github.com/PHPOffice/PhpSpreadsheet/pull/4760)
+- Reader/Html add ability to suppress warning messages from loadhtml. [Issue #647](https://github.com/PHPOffice/PhpSpreadsheet/issues/647) [Issue #849](https://github.com/PHPOffice/PhpSpreadsheet/issues/849) [PR #4761](https://github.com/PHPOffice/PhpSpreadsheet/pull/4761)
+
+### Changed
+
+- Evaluation of WEBSERVICE no longer requires external client, but will use oldCalculatedValue unless the request is for a domain in a user-supplied whitelist. [PR #4751](https://github.com/PHPOffice/PhpSpreadsheet/pull/4751)
+
+### Moved
+
+- Code to merge cell base style with table and conditional styles moved from Html Writer to its own class. [Issue #1058](https://github.com/PHPOffice/PhpSpreadsheet/issues/1058) [PR #4763](https://github.com/PHPOffice/PhpSpreadsheet/pull/4763)
+
+### Deprecated
+
+- Settings methods setHttpClient, unsetHttpClient, getHttpClient, and getRequestFactory are no longer used. No replacement.
+- Reader/Html protected property dataArray, described as used only for testing, is not used for testing. No replacement.
+
+### Fixed
+
+- Slightly better support for escaped characters in Xlsx Reader/Writer. [Discussion #4724](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4724) [PR #4726](https://github.com/PHPOffice/PhpSpreadsheet/pull/4726)
+- CODE/UNICODE and CHAR/UNICHAR. [PR #4727](https://github.com/PHPOffice/PhpSpreadsheet/pull/4727)
+- Minor changes to TextGrid. [PR #4735](https://github.com/PHPOffice/PhpSpreadsheet/pull/4735) [PR #4743](https://github.com/PHPOffice/PhpSpreadsheet/pull/4743)
+- Single-character table names. [Issue #4739](https://github.com/PHPOffice/PhpSpreadsheet/issues/4739) [PR #4740](https://github.com/PHPOffice/PhpSpreadsheet/pull/4740)
+- Improvements to SORT and SORTBY. [PR #4743](https://github.com/PHPOffice/PhpSpreadsheet/pull/4743)
+- Coverage-related changes in Shared. [PR #4745](https://github.com/PHPOffice/PhpSpreadsheet/pull/4745)
+- ListWorksheetInfo improvements for Xlsx and Ods. [Issue #3255](https://github.com/PHPOffice/PhpSpreadsheet/issues/3255) [PR #4746](https://github.com/PHPOffice/PhpSpreadsheet/pull/4746)
+- Fix functions related to Student-T distribution. [Issue #4167](https://github.com/PHPOffice/PhpSpreadsheet/issues/4167) [PR #4748](https://github.com/PHPOffice/PhpSpreadsheet/pull/4748)
+- Fix drawing hyperlinks. [Issue #993](https://github.com/PHPOffice/PhpSpreadsheet/issues/993) [PR #4764](https://github.com/PHPOffice/PhpSpreadsheet/pull/4764)
+- Fix clone spreadsheet with defined names. [PR #4753](https://github.com/PHPOffice/PhpSpreadsheet/pull/4753)
+- Changes to WEBSERVICE. [PR #4751](https://github.com/PHPOffice/PhpSpreadsheet/pull/4751)
+- More consistent handling of unsupported functions. [Issue #606](https://github.com/PHPOffice/PhpSpreadsheet/issues/606) [PR #4772](https://github.com/PHPOffice/PhpSpreadsheet/pull/4772)
+- Chart shadow `kx` and `ky`. [PR #4770](https://github.com/PHPOffice/PhpSpreadsheet/pull/4770)
+- SUBTOTAL and hidden rows. [Issue #820](https://github.com/PHPOffice/PhpSpreadsheet/issues/820) [PR #4765](https://github.com/PHPOffice/PhpSpreadsheet/pull/4765)
+- Fix some hyperlink problems. [Issue #3889](https://github.com/PHPOffice/PhpSpreadsheet/issues/3889) [Issue #2464](https://github.com/PHPOffice/PhpSpreadsheet/issues/2464) [PR #4771](https://github.com/PHPOffice/PhpSpreadsheet/pull/4771)
+- Xls Writer and empty RichText. [Issue #918](https://github.com/PHPOffice/PhpSpreadsheet/issues/918) [PR #4769](https://github.com/PHPOffice/PhpSpreadsheet/pull/4769)
+- Strings that look like huge floating-point numbers. [Issue #4766](https://github.com/PHPOffice/PhpSpreadsheet/issues/4766) [PR #4768](https://github.com/PHPOffice/PhpSpreadsheet/pull/4768)
+- Rowspan in Html. [Issue #1319](https://github.com/PHPOffice/PhpSpreadsheet/issues/1319) [PR #4767](https://github.com/PHPOffice/PhpSpreadsheet/pull/4767)
+- Mpdf styling of multi-line strings. [Issue #4773](https://github.com/PHPOffice/PhpSpreadsheet/issues/4773) [PR #4775](https://github.com/PHPOffice/PhpSpreadsheet/pull/4775)
+
+## 2025-11-24 - 5.3.0
+
+### Added
+
+- Formal support for Php8.5.
+- Limited Printarea support for Html/Pdf. [Issue #3941](https://github.com/PHPOffice/PhpSpreadsheet/issues/3941) [PR #4711](https://github.com/PHPOffice/PhpSpreadsheet/pull/4711)
+- Implement missing `INFO` function. [PR #4709](https://github.com/PHPOffice/PhpSpreadsheet/pull/4709)
+- Implement missing `BAHTTEXT` function. [PR #4715](https://github.com/PHPOffice/PhpSpreadsheet/pull/4715)
+
+### Deprecated
+
+- $dataType, the second parameter of Cell::setValueExplicit, is currently optional. Omitting it is deprecated, and it will be required in a future release.
+
+### Fixed
+
+- Protected ranges and insert/delete rows/columns. [Issue #4695](https://github.com/PHPOffice/PhpSpreadsheet/issues/4695) [PR #4702](https://github.com/PHPOffice/PhpSpreadsheet/pull/4702)
+- Unexpected Exception in Php DateTime. [Issue #4696](https://github.com/PHPOffice/PhpSpreadsheet/issues/4696) [Issue #917](https://github.com/PHPOffice/PhpSpreadsheet/issues/917) [PR #4697](https://github.com/PHPOffice/PhpSpreadsheet/pull/4697)
+- Add missing Dutch translation to translation file. [PR #4707](https://github.com/PHPOffice/PhpSpreadsheet/pull/4707)
+- Fix lots of typos throughout codebase. [PR #4705](https://github.com/PHPOffice/PhpSpreadsheet/pull/4705)
+- Apply small code style improvements. [PR #4708](https://github.com/PHPOffice/PhpSpreadsheet/pull/4708)
+
## 2025-10-25 - 5.2.0
### Added
@@ -250,18 +458,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
- Worksheet::getProtectedCells - use getProtectedCellRanges instead.
- Writer/Html::isMpdf property - use instanceof Mpdf instead.
-### Changed
-
-- Nothing yet.
-
-### Moved
-
-- Nothing yet.
-
-### Deprecated
-
-- Nothing yet.
-
### Fixed
- Xls writer Parser Mishandling True/False Argument. [Issue #4331](https://github.com/PHPOffice/PhpSpreadsheet/issues/4331) [PR #4333](https://github.com/PHPOffice/PhpSpreadsheet/pull/4333)
@@ -314,22 +510,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
## 2024-12-08 - 3.6.0
-### Added
-
-- Nothing yet.
-
-### Changed
-
-- Nothing yet.
-
-### Moved
-
-- Nothing yet.
-
-### Deprecated
-
-- Nothing yet.
-
### Fixed
- Html Reader/Writer Better Handling of Booleans. [PR #4257](https://github.com/PHPOffice/PhpSpreadsheet/pull/4257)
@@ -342,18 +522,10 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
## 2024-11-22 - 3.5.0
-### Added
-
-- Nothing yet.
-
### Changed
- Settings::libXmlLoaderOptions is ignored. [PR #4233](https://github.com/PHPOffice/PhpSpreadsheet/pull/4233)
-### Moved
-
-- Nothing yet.
-
### Deprecated
- Settings::setLibXmlLoaderOptions() and Settings::getLibXmlLoaderOptions() are no longer needed - no replacement.
@@ -386,10 +558,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
- Worksheet::getProtectedCells was deprecated in release 2, but was not properly documented, and not removed in release 3. Use getProtectedCellRanges instead.
- Writer/Html::isMpdf property was deprecated in release 2, but was not properly documented, and not removed in release 3. Use instanceof Mpdf instead.
-### Moved
-
-- Nothing yet.
-
### Fixed
- Xls Writer Condtional Rules Applied to Whole Rows or Columns. [Issue #3185](https://github.com/PHPOffice/PhpSpreadsheet/issues/3185) [PR #4152](https://github.com/PHPOffice/PhpSpreadsheet/pull/4152)
@@ -421,10 +589,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
- Currency and Accounting Wizards are changed to act like Excel, and a new CurrencyBase Wizard is added for for non-Excel formats. [Issue #4125](https://github.com/PHPOffice/PhpSpreadsheet/issues/4125) [Issue #4124](https://github.com/PHPOffice/PhpSpreadsheet/issues/4124) [PR #4127](https://github.com/PHPOffice/PhpSpreadsheet/pull/4127)
- Images will not be added to spreadsheet if they cannot be validated as images.
-### Deprecated
-
-- Nothing yet.
-
### Removed
- The following items were deprecated in release 2 and are now removed.
@@ -433,10 +597,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
- Calculation\Calculation::_translateFormulaToLocale (use method name translateFormulaToLocale without leading underscore).
- Calculation\Calculation::_translateFormulaToEnglish (use method name translateFormulaToEnglish without leading underscore).
-### Moved
-
-- Nothing yet.
-
### Fixed
- Several security patches.
@@ -446,22 +606,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
## 2024-08-07 - 2.2.2
-### Added
-
-- Nothing yet.
-
-### Changed
-
-- Nothing yet.
-
-### Deprecated
-
-- Nothing yet.
-
-### Moved
-
-- Nothing yet.
-
### Fixed
- Html Reader Preserve Unicode Whitespace. [Issue #1284](https://github.com/PHPOffice/PhpSpreadsheet/issues/1284) [PR #4106](https://github.com/PHPOffice/PhpSpreadsheet/pull/4106)
@@ -500,10 +644,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
- Writer\Xls\Style\ColorMap is no longer needed.
-### Moved
-
-- Nothing
-
### Fixed
- Incorrect Reader CSV with BOM. [Issue #4028](https://github.com/PHPOffice/PhpSpreadsheet/issues/4028) [PR #4029](https://github.com/PHPOffice/PhpSpreadsheet/pull/4029)
@@ -538,18 +678,10 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
- Default Style Alignment Property (workaround for bug in non-Excel spreadsheet apps) [Issue #3918](https://github.com/PHPOffice/PhpSpreadsheet/issues/3918) [PR #3924](https://github.com/PHPOffice/PhpSpreadsheet/pull/3924)
- Additional Support for Date/Time Styles [PR #3939](https://github.com/PHPOffice/PhpSpreadsheet/pull/3939)
-### Changed
-
-- Nothing
-
### Deprecated
- Reader/Xml trySimpleXMLLoadString should not have had public visibility, and will be removed.
-### Removed
-
-- Nothing
-
### Fixed
- IF Empty Arguments. [Issue #3875](https://github.com/PHPOffice/PhpSpreadsheet/issues/3875) [Issue #2146](https://github.com/PHPOffice/PhpSpreadsheet/issues/2146) [PR #3879](https://github.com/PHPOffice/PhpSpreadsheet/pull/3879)
@@ -611,10 +743,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
- Functions `_translateFormulaToLocale` and `_translateFormulaEnglish` are replaced by versions without leading underscore. [PR #3828](https://github.com/PHPOffice/PhpSpreadsheet/pull/3828)
-### Removed
-
-- Nothing
-
### Fixed
- Take advantage of mitoteam/jpgraph Extended mode to enable rendering of more graphs. [PR #3603](https://github.com/PHPOffice/PhpSpreadsheet/pull/3603)
@@ -682,14 +810,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
- Allow `CellRange` and `CellAddress` objects for the `range` argument in the `rangeToArray()` method. [PR #3494](https://github.com/PHPOffice/PhpSpreadsheet/pull/3494)
- Stock charts will now read and reproduce `upDownBars` and subsidiary tags; these were previously ignored on read and hard-coded on write. [PR #3515](https://github.com/PHPOffice/PhpSpreadsheet/pull/3515)
-### Deprecated
-
-- Nothing
-
-### Removed
-
-- Nothing
-
### Fixed
- Updates Cell formula absolute ranges/references, and Defined Name absolute ranges/references when inserting/deleting rows/columns. [Issue #3368](https://github.com/PHPOffice/PhpSpreadsheet/issues/3368) [PR #3402](https://github.com/PHPOffice/PhpSpreadsheet/pull/3402)
@@ -747,10 +867,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
- Rationalisation of Pre-defined Currency Format Masks [PR #3377](https://github.com/PHPOffice/PhpSpreadsheet/pull/3377)
-### Removed
-
-- Nothing
-
### Fixed
- Calculation Engine doesn't evaluate Defined Name when default cell A1 is quote-prefixed [Issue #3335](https://github.com/PHPOffice/PhpSpreadsheet/issues/3335) [PR #3336](https://github.com/PHPOffice/PhpSpreadsheet/pull/3336)
@@ -764,22 +880,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
## 1.27.1 - 2023-02-08
-### Added
-
-- Nothing
-
-### Changed
-
-- Nothing
-
-### Deprecated
-
-- Nothing
-
-### Removed
-
-- Nothing
-
### Fixed
- Fix Composer --dev dependency issue with dealerdirect/phpcodesniffer-composer-installer renaming their `master` branch to `main`
@@ -794,14 +894,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
- Support for Structured References in the Calculation Engine [PR #3261](https://github.com/PHPOffice/PhpSpreadsheet/pull/3261)
- Limited Support for Form Controls [PR #3130](https://github.com/PHPOffice/PhpSpreadsheet/pull/3130) [Issue #2396](https://github.com/PHPOffice/PhpSpreadsheet/issues/2396) [Issue #1770](https://github.com/PHPOffice/PhpSpreadsheet/issues/1770) [Issue #2388](https://github.com/PHPOffice/PhpSpreadsheet/issues/2388) [Issue #2904](https://github.com/PHPOffice/PhpSpreadsheet/issues/2904) [Issue #2661](https://github.com/PHPOffice/PhpSpreadsheet/issues/2661)
-### Changed
-
-- Nothing
-
-### Deprecated
-
-- Nothing
-
### Removed
- Shared/JAMA is removed. [PR #3260](https://github.com/PHPOffice/PhpSpreadsheet/pull/3260)
@@ -827,10 +919,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
- Xlsx Reader support for Pivot Tables [PR #2829](https://github.com/PHPOffice/PhpSpreadsheet/pull/2829)
- Permit Date/Time Entered on Spreadsheet to be calculated as Float [Issue #1416](https://github.com/PHPOffice/PhpSpreadsheet/issues/1416) [PR #3121](https://github.com/PHPOffice/PhpSpreadsheet/pull/3121)
-### Changed
-
-- Nothing
-
### Deprecated
- Direct update of Calculation::suppressFormulaErrors is replaced with setter.
@@ -838,10 +926,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
- ExcelError public static variable errorCodes replaced with constant ERROR_CODES.
- NumberFormat constant FORMAT_DATE_YYYYMMDD2 replaced with existing identical FORMAT_DATE_YYYYMMDD.
-### Removed
-
-- Nothing
-
### Fixed
- Fixed handling for `_xlws` prefixed functions from Office365 [Issue #3245](https://github.com/PHPOffice/PhpSpreadsheet/issues/3245) [PR #3247](https://github.com/PHPOffice/PhpSpreadsheet/pull/3247)
@@ -870,22 +954,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
## 1.25.2 - 2022-09-25
-### Added
-
-- Nothing
-
-### Changed
-
-- Nothing
-
-### Deprecated
-
-- Nothing
-
-### Removed
-
-- Nothing
-
### Fixed
- Composer dependency clash with ezyang/htmlpurifier
@@ -918,10 +986,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
- Misspelled Properties::LINE_STYLE_DASH_SQUERE_DOT deprecated in favor of LINE_STYLE_DASH_SQUARE_DOT.
- Clone not permitted for Spreadsheet. Spreadsheet->copy() can be used instead.
-### Removed
-
-- Nothing
-
### Fixed
- Fix update to defined names when inserting/deleting rows/columns [Issue #3076](https://github.com/PHPOffice/PhpSpreadsheet/issues/3076) [PR #3077](https://github.com/PHPOffice/PhpSpreadsheet/pull/3077)
@@ -950,18 +1014,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
- Support for SimpleCache Interface versions 1.0, 2.0 and 3.0
- Add Chart Axis Option textRotation [Issue #2705](https://github.com/PHPOffice/PhpSpreadsheet/issues/2705) [PR #2940](https://github.com/PHPOffice/PhpSpreadsheet/pull/2940)
-### Changed
-
-- Nothing
-
-### Deprecated
-
-- Nothing
-
-### Removed
-
-- Nothing
-
### Fixed
- Fix Encoding issue with Html reader (PHP 8.2 deprecation for mb_convert_encoding) [Issue #2942](https://github.com/PHPOffice/PhpSpreadsheet/issues/2942) [PR #2943](https://github.com/PHPOffice/PhpSpreadsheet/pull/2943)
@@ -1002,14 +1054,6 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
See [the Discussion section on github](https://github.com/PHPOffice/PhpSpreadsheet/discussions/2821) for details of performance across versions
- Improved performance for removing rows/columns from a worksheet
-### Deprecated
-
-- Nothing
-
-### Removed
-
-- Nothing
-
### Fixed
- Xls Reader resolving absolute named ranges to relative ranges [Issue #2826](https://github.com/PHPOffice/PhpSpreadsheet/issues/2826) [PR #2827](https://github.com/PHPOffice/PhpSpreadsheet/pull/2827)
@@ -1089,10 +1133,6 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
This change provides more consistency in the methods (not every "by cell address" method has an equivalent "byColumnAndRow" method);
and the "by cell address" methods often provide more flexibility, such as allowing a range of cells, or referencing them by passing the defined name of a named range as the argument.
-### Removed
-
-- Nothing
-
### Fixed
- Make allowance for the AutoFilter dropdown icon in the first row of an Autofilter range when using Autosize columns. [Issue #2413](https://github.com/PHPOffice/PhpSpreadsheet/issues/2413) [PR #2754](https://github.com/PHPOffice/PhpSpreadsheet/pull/2754)
@@ -1160,14 +1200,6 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
- Extract some methods from the Calculation Engine into dedicated classes [#2537](https://github.com/PHPOffice/PhpSpreadsheet/issues/2537)
- Eliminate calls to `flattenSingleValue()` that are no longer required when we're checking for array values as arguments [#2590](https://github.com/PHPOffice/PhpSpreadsheet/issues/2590)
-### Deprecated
-
-- Nothing
-
-### Removed
-
-- Nothing
-
### Fixed
- Fixed `ReferenceHelper@insertNewBefore` behavior when removing column before last column with null value [PR #2541](https://github.com/PHPOffice/PhpSpreadsheet/pull/2541)
@@ -1202,14 +1234,6 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
- Xlsx Writer will evaluate AutoFilter only if it is as yet unevaluated, or has changed since it was last evaluated [PR #2414](https://github.com/PHPOffice/PhpSpreadsheet/pull/2414)
-### Deprecated
-
-- Nothing
-
-### Removed
-
-- Nothing
-
### Fixed
- Rounding in `NumberFormatter` [Issue #2385](https://github.com/PHPOffice/PhpSpreadsheet/issues/2385) [PR #2399](https://github.com/PHPOffice/PhpSpreadsheet/pull/2399)
@@ -1240,14 +1264,6 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
- Drop support for PHP 7.2, according to https://phpspreadsheet.readthedocs.io/en/latest/#php-version-support
- Use native typing for objects that were already documented as such
-### Deprecated
-
-- Nothing
-
-### Removed
-
-- Nothing
-
### Fixed
- Fixed null conversation for strToUpper [#2292](https://github.com/PHPOffice/PhpSpreadsheet/issues/2292)
@@ -1281,18 +1297,10 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
- Ability to stream to an Amazon S3 bucket [Issue #2249](https://github.com/PHPOffice/PhpSpreadsheet/issues/2249)
- Provided a Size Helper class to validate size values (pt, px, em) [PR #1694](https://github.com/PHPOffice/PhpSpreadsheet/pull/1694)
-### Changed
-
-- Nothing.
-
### Deprecated
- PHP 8.1 will deprecate auto_detect_line_endings. As a result of this change, Csv Reader using some release after PHP8.1 will no longer be able to handle a Csv with Mac line endings.
-### Removed
-
-- Nothing.
-
### Fixed
- Unexpected format in Xlsx Timestamp [Issue #2331](https://github.com/PHPOffice/PhpSpreadsheet/issues/2331) [PR #2332](https://github.com/PHPOffice/PhpSpreadsheet/pull/2332)
@@ -1415,10 +1423,6 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
This makes the logic in these classes easier to maintain; and will reduce the memory footprint required to execute formulae when calling these functions.
-### Removed
-
-- Nothing.
-
### Fixed
- Avoid Duplicate Titles When Reading Multiple HTML Files.[Issue #1823](https://github.com/PHPOffice/PhpSpreadsheet/issues/1823) [PR #1829](https://github.com/PHPOffice/PhpSpreadsheet/pull/1829)
@@ -1448,10 +1452,6 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
- All Excel Function implementations in `Calculation\Database`, `Calculation\DateTime`, `Calculation\Engineering`, `Calculation\Financial`, `Calculation\Logical`, `Calculation\LookupRef`, `Calculation\MathTrig`, `Calculation\Statistical`, `Calculation\TextData` and `Calculation\Web` have been moved to dedicated classes for individual functions or groups of related functions. See the docblocks against all the deprecated methods for details of the new methods to call instead. At some point, these old classes will be deleted.
-### Removed
-
-- Nothing.
-
### Fixed
- Fixed issue with absolute path in worksheets' Target [PR #1769](https://github.com/PHPOffice/PhpSpreadsheet/pull/1769)
@@ -1511,10 +1511,6 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
- **IMPORTANT NOTE:** This Introduces a **BC break** in the handling of named ranges. Previously, a named range cell reference of `B2` would be treated identically to a named range cell reference of `$B2` or `B$2` or `$B$2` because the calculation engine treated then all as absolute references. These changes "fix" that, so the calculation engine now handles relative references in named ranges correctly.
This change that resolves previously incorrect behaviour in the calculation may affect users who have dynamically defined named ranges using relative references when they should have used absolute references.
-### Removed
-
-- Nothing.
-
### Fixed
- PrintArea causes exception [#1544](https://github.com/phpoffice/phpspreadsheet/pull/1544)
@@ -1525,10 +1521,6 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
## 1.14.1 - 2020-07-19
-### Added
-
-- nothing
-
### Fixed
- WEBSERVICE is HTTP client agnostic and must be configured via `Settings::setHttpClient()` [#1562](https://github.com/PHPOffice/PhpSpreadsheet/issues/1562)
diff --git a/vendor/phpoffice/phpspreadsheet/CONTRIBUTING.md b/vendor/phpoffice/phpspreadsheet/CONTRIBUTING.md
index 6cbf9fb..a8d9237 100644
--- a/vendor/phpoffice/phpspreadsheet/CONTRIBUTING.md
+++ b/vendor/phpoffice/phpspreadsheet/CONTRIBUTING.md
@@ -43,7 +43,7 @@ This makes it easier to see exactly what is being tested when reviewing the PR.
4. By default, Github removes markdown headings in the Release Notes. You can either edit to restore these, or, probably preferably, change the default comment character on your system - `git config core.commentChar ";"`.
> **Note:** Tagged releases are made from the `master` branch. Only in an emergency should a tagged release be made from the `release` branch. (i.e. cherry-picked hot-fixes.) However, there are 4 branches which have been updated to apply security patches, and those may be tagged if future security updates are needed.
-- release1291 (no further updates aside from security patches, including code changes needed for Php 8.5 compatibility)
-- release210 (no further updates aside from security patches, including code changes needed for Php 8.5 compatibility)
-- release222
-- release390
+- 1.30.x (no further updates aside from security patches, including code changes needed for Php 8.5 compatibility)
+- 2.1.x (no further updates aside from security patches, including code changes needed for Php 8.5 compatibility)
+- 2.4.x (no further updates aside from security patches)
+- 3.10.x
diff --git a/vendor/phpoffice/phpspreadsheet/LICENSE b/vendor/phpoffice/phpspreadsheet/LICENSE
index 04a90f0..e4bc4a3 100644
--- a/vendor/phpoffice/phpspreadsheet/LICENSE
+++ b/vendor/phpoffice/phpspreadsheet/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2019-2025 PhpSpreadsheet Authors
+Copyright (c) 2019-2026 PhpSpreadsheet Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/vendor/phpoffice/phpspreadsheet/README.md b/vendor/phpoffice/phpspreadsheet/README.md
index 8286e3a..1a7488f 100644
--- a/vendor/phpoffice/phpspreadsheet/README.md
+++ b/vendor/phpoffice/phpspreadsheet/README.md
@@ -4,7 +4,7 @@
[](https://coveralls.io/github/PHPOffice/PhpSpreadsheet?branch=master)
[](https://packagist.org/packages/phpoffice/phpspreadsheet)
[](https://packagist.org/packages/phpoffice/phpspreadsheet)
-[](https://packagist.org/packages/phpoffice/phpspreadsheet)
+[](https://packagist.org/packages/phpoffice/phpspreadsheet)
[](https://gitter.im/PHPOffice/PhpSpreadsheet)
PhpSpreadsheet is a library written in pure PHP and offers a set of classes that
@@ -16,8 +16,9 @@ This is the master branch, and is maintained for security and bug fixes.
LTS: For maintained branches, support for PHP versions will only be maintained for a period of six months beyond the
[end of life](https://www.php.net/supported-versions) of that PHP version.
+Dropping support for versions after that date will not be considered a breaking change.
-Currently the required PHP minimum version is PHP __8.1__, and we [will support that version](https://www.php.net/supported-versions.php) until 30th June 2026.
+Currently the required PHP minimum version is PHP __8.2__, and we [will support that version](https://www.php.net/supported-versions.php) until 30th June 2027.
See the `composer.json` for other requirements.
diff --git a/vendor/phpoffice/phpspreadsheet/composer.json b/vendor/phpoffice/phpspreadsheet/composer.json
index d26157e..19cbcc0 100644
--- a/vendor/phpoffice/phpspreadsheet/composer.json
+++ b/vendor/phpoffice/phpspreadsheet/composer.json
@@ -13,7 +13,7 @@
],
"config": {
"platform": {
- "php" : "8.1.99"
+ "php": "8.2.99"
},
"process-timeout": 600,
"sort-packages": true,
@@ -42,6 +42,9 @@
},
{
"name": "Adrien Crivelli"
+ },
+ {
+ "name": "Owen Leibman"
}
],
"scripts": {
@@ -66,10 +69,11 @@
]
},
"require": {
- "php": "^8.1",
+ "php": "^8.2",
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
+ "ext-filter": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-libxml": "*",
@@ -84,11 +88,11 @@
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
- "psr/http-client": "^1.0",
- "psr/http-factory": "^1.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
+ "ext-intl": "*",
+ "ext-openssl": "*",
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^2.0 || ^3.0",
"friendsofphp/php-cs-fixer": "^3.2",
@@ -98,12 +102,13 @@
"phpstan/phpstan": "^1.1 || ^2.0",
"phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0",
"phpstan/phpstan-phpunit": "^1.0 || ^2.0",
- "phpunit/phpunit": "^10.5",
+ "phpunit/phpunit": "^10.5 || ^11.0",
"squizlabs/php_codesniffer": "^3.7",
- "tecnickcom/tcpdf": "^6.5"
+ "tecnickcom/tcpdf": ">=6.8.0 <7.0.0"
},
"suggest": {
- "ext-intl": "PHP Internationalization Functions, regquired for NumberFormat Wizard",
+ "ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard and StringHelper::setLocale()",
+ "ext-openssl": "Handline Agile-encrypted Xlsx spreadsheets",
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer",
@@ -117,7 +122,8 @@
"autoload-dev": {
"psr-4": {
"PhpOffice\\PhpSpreadsheetTests\\": "tests/PhpSpreadsheetTests",
- "PhpOffice\\PhpSpreadsheetInfra\\": "infra"
+ "PhpOffice\\PhpSpreadsheetInfra\\": "infra",
+ "PhpOffice\\PhpSpreadsheetBenchmarks\\": "tests/Benchmark"
}
}
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/BinaryComparison.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/BinaryComparison.php
index 1f69794..38c298f 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/BinaryComparison.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/BinaryComparison.php
@@ -2,6 +2,7 @@
namespace PhpOffice\PhpSpreadsheet\Calculation;
+use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class BinaryComparison
@@ -41,17 +42,25 @@ class BinaryComparison
return strcmp($str1, $str2);
}
- public static function compare(mixed $operand1, mixed $operand2, string $operator): bool
+ public static function compare(mixed $operand1, mixed $operand2, string $operator): bool|string
{
// Simple validate the two operands if they are string values
if (is_string($operand1) && $operand1 > '' && $operand1[0] == Calculation::FORMULA_STRING_QUOTE) {
$operand1 = Calculation::unwrapResult($operand1);
}
+ if (ErrorValue::isError($operand1, true)) {
+ /** @var string $operand1 */
+ return $operand1;
+ }
if (is_string($operand2) && $operand2 > '' && $operand2[0] == Calculation::FORMULA_STRING_QUOTE) {
$operand2 = Calculation::unwrapResult($operand2);
}
+ if (ErrorValue::isError($operand2, true)) {
+ /** @var string $operand2 */
+ return $operand2;
+ }
- // Use case insensitive comparaison if not OpenOffice mode
+ // Use case-insensitive comparison if not OpenOffice mode
if (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) {
if (is_string($operand1)) {
$operand1 = StringHelper::strToUpper($operand1);
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Calculation.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Calculation.php
index abdd4c8..faaf440 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Calculation.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Calculation.php
@@ -2,6 +2,7 @@
namespace PhpOffice\PhpSpreadsheet\Calculation;
+use Composer\Pcre\Preg; // many pregs in this program use u modifier, which has side-effects which make it unsuitable for this
use PhpOffice\PhpSpreadsheet\Calculation\Engine\BranchPruner;
use PhpOffice\PhpSpreadsheet\Calculation\Engine\CyclicReferenceStack;
use PhpOffice\PhpSpreadsheet\Calculation\Engine\Logger;
@@ -13,6 +14,7 @@ use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\DefinedName;
+use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
use PhpOffice\PhpSpreadsheet\NamedRange;
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
@@ -36,7 +38,7 @@ class Calculation extends CalculationLocale
const CALCULATION_REGEXP_OPENBRACE = '\(';
// Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it)
const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?(?:_xlws\.)?((?:__xludf\.)?[\p{L}][\p{L}\p{N}\.]*)[\s]*\(';
- // Cell reference (cell or range of cells, with or without a sheet reference)
+ // Cell reference, with or without a sheet reference)
const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])';
// Used only to detect spill operator #
const CALCULATION_REGEXP_CELLREF_SPILL = '/' . self::CALCULATION_REGEXP_CELLREF . '#/i';
@@ -88,9 +90,22 @@ class Calculation extends CalculationLocale
*/
private bool $calculationCacheEnabled = true;
+ /**
+ * Maximum number of entries in the formula token cache.
+ * Default 0 (disabled). Set via setFormulaTokenCacheMaxSize() to enable.
+ */
+ private int $formulaTokenCacheMaxSize = 0;
+
+ /**
+ * Cache of parsed formula tokens, keyed by the raw formula string.
+ *
+ * @var array|bool>
+ */
+ private array $formulaTokenCache = [];
+
private BranchPruner $branchPruner;
- private bool $branchPruningEnabled = true;
+ protected bool $branchPruningEnabled = true;
/**
* List of operators that can be used within formulae
@@ -239,6 +254,7 @@ class Calculation extends CalculationLocale
{
$this->clearCalculationCache();
$this->branchPruner->clearBranchStore();
+ $this->formulaTokenCache = [];
}
/**
@@ -364,6 +380,44 @@ class Calculation extends CalculationLocale
$this->calculationCache = [];
}
+ /**
+ * Clear the formula token cache.
+ */
+ public function clearFormulaTokenCache(): void
+ {
+ $this->formulaTokenCache = [];
+ }
+
+ /**
+ * Get the current number of entries in the formula token cache.
+ */
+ public function getFormulaTokenCacheSize(): int
+ {
+ return count($this->formulaTokenCache);
+ }
+
+ /**
+ * Set the maximum number of entries in the formula token cache.
+ * Set to 0 to disable caching (default), or a positive integer to enable.
+ */
+ public function setFormulaTokenCacheMaxSize(int $size): self
+ {
+ $this->formulaTokenCacheMaxSize = max(0, $size);
+ if ($this->formulaTokenCacheMaxSize === 0) {
+ $this->formulaTokenCache = [];
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get the maximum number of entries allowed in the formula token cache.
+ */
+ public function getFormulaTokenCacheMaxSize(): int
+ {
+ return $this->formulaTokenCacheMaxSize;
+ }
+
/**
* Clear calculation cache for a specified worksheet.
*/
@@ -415,7 +469,7 @@ class Calculation extends CalculationLocale
{
if (is_string($value)) {
// Error values cannot be "wrapped"
- if (preg_match('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) {
+ if (Preg::isMatch('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) {
// Return Excel errors "as is"
return $value;
}
@@ -494,13 +548,13 @@ class Calculation extends CalculationLocale
try {
$value = $cell->getValue();
if (is_string($value) && $cell->getDataType() === DataType::TYPE_FORMULA) {
- $value = preg_replace_callback(
+ $value = Preg::replaceCallback(
self::CALCULATION_REGEXP_CELLREF_SPILL,
fn (array $matches) => 'ANCHORARRAY(' . substr($matches[0], 0, -1) . ')',
$value
);
}
- $result = self::unwrapResult($this->_calculateFormulaValue($value, $cell->getCoordinate(), $cell)); //* @phpstan-ignore-line
+ $result = self::unwrapResult($this->_calculateFormulaValue($value, $cell->getCoordinate(), $cell)); //* @phpstan-ignore argument.type ($value can be mixed not string)
if ($this->spreadsheet === null) {
throw new Exception('null spreadsheet in calculateCellValue');
}
@@ -520,7 +574,7 @@ class Calculation extends CalculationLocale
$cellAddress = array_pop($this->cellStack);
}
if ($this->spreadsheet !== null && is_array($cellAddress) && array_key_exists('sheet', $cellAddress)) {
- $sheetName = $cellAddress['sheet'] ?? null;
+ $sheetName = $cellAddress['sheet'];
$testSheet = is_string($sheetName) ? $this->spreadsheet->getSheetByName($sheetName) : null;
if ($testSheet !== null && array_key_exists('cell', $cellAddress)) {
/** @var array{cell: string} $cellAddress */
@@ -557,11 +611,17 @@ class Calculation extends CalculationLocale
*/
public function parseFormula(string $formula): array|bool
{
- $formula = preg_replace_callback(
+ // Check the formula token cache first (only when caching is enabled)
+ if ($this->formulaTokenCacheMaxSize > 0 && isset($this->formulaTokenCache[$formula])) {
+ return $this->formulaTokenCache[$formula];
+ }
+
+ $originalFormula = $formula;
+ $formula = Preg::replaceCallback(
self::CALCULATION_REGEXP_CELLREF_SPILL,
fn (array $matches) => 'ANCHORARRAY(' . substr($matches[0], 0, -1) . ')',
$formula
- ) ?? $formula;
+ );
// Basic validation that this is indeed a formula
// We return an empty array if not
$formula = trim($formula);
@@ -574,7 +634,21 @@ class Calculation extends CalculationLocale
}
// Parse the formula and return the token stack
- return $this->internalParseFormula($formula);
+ $result = $this->internalParseFormula($formula);
+
+ // Cache the result when caching is enabled (clear cache if it exceeds the maximum size)
+ if ($this->formulaTokenCacheMaxSize > 0) {
+ // Phpstan says if condition is always false,
+ // but coverage report says next statement is covered.
+ if (count($this->formulaTokenCache) >= $this->formulaTokenCacheMaxSize) {
+ $this->formulaTokenCache = [];
+ }
+ // Cache key is the original formula string (before ANCHORARRAY transformation)
+ // to ensure consistent lookup regardless of internal transformations.
+ $this->formulaTokenCache[$originalFormula] = $result;
+ }
+
+ return $result;
}
/**
@@ -657,8 +731,9 @@ class Calculation extends CalculationLocale
return self::wrapResult((string) $formula);
}
+ // https://www.reddit.com/r/excel/comments/chr41y/cmd_formula_stopped_working_since_last_update/
if (preg_match('/^=\s*cmd\s*\|/miu', $formula) !== 0) {
- return self::wrapResult($formula);
+ return ExcelError::REF(); // returns #BLOCKED in newer versions
}
// Basic validation that this is indeed a formula
@@ -914,13 +989,14 @@ class Calculation extends CalculationLocale
$pad = $rpad = ', ';
foreach ($value as $row) {
if (is_array($row)) {
- $returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row));
+ $returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row)); // @phpstan-ignore argument.type (array_map can theoretically return array not array)
$rpad = '; ';
} else {
$returnMatrix[] = $this->showValue($row);
}
}
+ /** @var string[] $returnMatrix */
return '{ ' . implode($rpad, $returnMatrix) . ' }';
} elseif (is_string($value) && (trim($value, self::FORMULA_STRING_QUOTE) == $value)) {
return self::FORMULA_STRING_QUOTE . $value . self::FORMULA_STRING_QUOTE;
@@ -1052,6 +1128,50 @@ class Calculation extends CalculationLocale
'>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, // Comparison
];
+ /** @param array $matches */
+ private function unionForComma(array $matches): string
+ {
+ $matches5 = (string) $matches[5];
+ // Weirdly, the regexp which get us here for issue 4832
+ // only gets us here for Php8.4+. 8.4 introduced
+ // major changes for PCRE, but I cannot identify
+ // the exact change which caused this discrepancy.
+ // I do plan to update coverage to 8.4 at some point,
+ // and I can remove the coverage annotations after that.
+ // @codeCoverageIgnoreStart
+ if (str_contains($matches5, '(') && !str_contains($matches5, ')')) {
+ if ($this->spreadsheet !== null) {
+ if ($this->spreadsheet->getSheetByName($matches5) === null) {
+ $matches0 = (string) $matches[0];
+ $this->debugLog->writeDebugLog('Not Reformulating %s', $matches0);
+
+ return $matches0;
+ }
+ }
+ }
+ // @codeCoverageIgnoreEnd
+ $matches1 = (string) $matches[1];
+ $matches2 = (string) $matches[2];
+
+ return $matches1 . str_replace(',', '∪', $matches2);
+ }
+
+ private const CELL_OR_CELLRANGE_OR_DEFINED_NAME
+ = '(?:'
+ . self::CALCULATION_REGEXP_CELLREF // cell address
+ . '(?::' . self::CALCULATION_REGEXP_CELLREF . ')?' // optional range address, non-capturing
+ . '|' . self::CALCULATION_REGEXP_DEFINEDNAME
+ . ')'
+ ;
+
+ public const UNIONABLE_COMMAS = '/((?:[,(]|^)\s*)' // comma or open paren or start of string, followed by optional whitespace
+ . '([(]' // open paren
+ . self::CELL_OR_CELLRANGE_OR_DEFINED_NAME // cell address
+ . '(?:\s*,\s*' // optioonal whitespace, comma, optional whitespace, non-capturing
+ . self::CELL_OR_CELLRANGE_OR_DEFINED_NAME // cell address
+ . ')+' // one or more occurrences
+ . '\s*[)])/i'; // optional whitespace, end paren
+
/**
* @return array|false
*/
@@ -1060,6 +1180,12 @@ class Calculation extends CalculationLocale
if (($formula = $this->convertMatrixReferences(trim($formula))) === false) {
return false;
}
+
+ $oldFormula = $formula;
+ $formula = Preg::replaceCallback(self::UNIONABLE_COMMAS, $this->unionForComma(...), $formula);
+ if ($oldFormula !== $formula) {
+ $this->debugLog->writeDebugLog('Reformulated as %s', $formula);
+ }
$phpSpreadsheetFunctions = &self::getFunctionsAddress();
// If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent worksheet),
@@ -1179,7 +1305,7 @@ class Calculation extends CalculationLocale
}
}
} elseif (is_string($expectedArgumentCount) && $expectedArgumentCount !== '*') {
- if (1 !== preg_match('/(\d*)([-+,])(\d*)/', $expectedArgumentCount, $argMatch)) {
+ if (!Preg::isMatch('/(\d*)([-+,])(\d*)/', $expectedArgumentCount, $argMatch)) {
$argMatch = ['', '', '', ''];
}
switch ($argMatch[2]) {
@@ -1236,16 +1362,14 @@ class Calculation extends CalculationLocale
// because at least the braces are paired up (at this stage in the formula)
// MS Excel allows this if the content is cell references; but doesn't allow actual values,
// but at this point, we can't differentiate (so allow both)
- return $this->raiseFormulaError('Formula Error: Unexpected ,');
- /* The following code may be a better choice, but, with
- the other changes for this PR, I can no longer come up
- with a test case that gets here
+ //return $this->raiseFormulaError('Formula Error: Unexpected ,');
+
$stack->push('Binary Operator', '∪');
++$index;
$expectingOperator = false;
- continue;*/
+ continue;
}
/** @var array $d */
@@ -1267,11 +1391,12 @@ class Calculation extends CalculationLocale
// do we now have a function/variable/number?
$expectingOperator = true;
$expectingOperand = false;
- $val = $match[1] ?? ''; //* @phpstan-ignore-line
+ $val = $match[1] ?? '';
$length = strlen($val);
if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $val, $matches)) {
- $val = (string) preg_replace('/\s/u', '', $val);
+ // $val is known to be valid unicode from statement above, so Preg::replace is okay even with u modifier
+ $val = Preg::replace('/\s/u', '', $val);
if (isset($phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function
$valToUpper = strtoupper($val);
} else {
@@ -1410,7 +1535,7 @@ class Calculation extends CalculationLocale
$refSheet = $pCellParent->getParentOrThrow()->getSheetByName($rangeSheetRef);
}
- if (ctype_digit($val) && $val <= 1048576) {
+ if (ctype_digit($val) && $val <= AddressRange::MAX_ROW) {
// Row range
$stackItemType = 'Row Reference';
$valx = $val;
@@ -1419,7 +1544,7 @@ class Calculation extends CalculationLocale
} elseif (ctype_alpha($val) && strlen($val) <= 3) {
// Column range
$stackItemType = 'Column Reference';
- $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow($val) : AddressRange::MAX_ROW; // Max 1,048,576 rows for Excel2007
+ $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow() : AddressRange::MAX_ROW; // Max 1,048,576 rows for Excel2007
$val = "{$rangeWS2}{$val}{$endRowColRef}";
}
$stackItemReference = $val;
@@ -1445,7 +1570,7 @@ class Calculation extends CalculationLocale
// unescape any apostrophes or double quotes in worksheet name
$val = str_replace(["''", '""'], ["'", '"'], $val);
$column = 'A';
- if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) {
+ if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) { // @phpstan-ignore booleanAnd.alwaysFalse (testPrevop must be non-null), booleanAnd.alwaysFalse (ditto), identical.alwaysFalse (ditto)
$column = $pCellParent->getHighestDataColumn($val);
}
$val = "{$rowRangeReference[2]}{$column}{$rowRangeReference[7]}";
@@ -1459,7 +1584,7 @@ class Calculation extends CalculationLocale
// unescape any apostrophes or double quotes in worksheet name
$val = str_replace(["''", '""'], ["'", '"'], $val);
$row = '1';
- if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) {
+ if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) { // @phpstan-ignore booleanAnd.alwaysFalse (testPrevOp must be non-null?), booleanAnd.alwaysFalse (ditto), identical.alwaysFalse (ditto)
$row = $pCellParent->getHighestDataRow($val);
}
$val = "{$val}{$row}";
@@ -1610,7 +1735,7 @@ class Calculation extends CalculationLocale
foreach ($tokens as $tokenIdx => $tokenData) {
/** @var mixed[] $tokenData */
$this->processingAnchorArray = false;
- if ($tokenData['type'] === 'Cell Reference' && isset($tokens[$tokenIdx + 1]) && $tokens[$tokenIdx + 1]['type'] === 'Operand Count for Function ANCHORARRAY()') { //* @phpstan-ignore-line
+ if ($tokenData['type'] === 'Cell Reference' && isset($tokens[$tokenIdx + 1]) && $tokens[$tokenIdx + 1]['type'] === 'Operand Count for Function ANCHORARRAY()') { //* @phpstan-ignore offsetAccess.nonOffsetAccessible ($tokens might be mixed not array)
$this->processingAnchorArray = true;
}
$token = $tokenData['value'];
@@ -1712,7 +1837,7 @@ class Calculation extends CalculationLocale
return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e);
}
}
- } elseif (!is_numeric($token) && !is_object($token) && isset($token, self::BINARY_OPERATORS[$token])) { //* @phpstan-ignore-line
+ } elseif (!is_numeric($token) && !is_object($token) && isset($token, self::BINARY_OPERATORS[$token])) { //* @phpstan-ignore offsetAccess.invalidOffset ($token is mixed)
// if the token is a binary operator, pop the top two values off the stack, do the operation, and push the result back on the stack
// We must have two operands, error if we don't
$operand2Data = $stack->pop();
@@ -1830,7 +1955,7 @@ class Calculation extends CalculationLocale
if ($breakNeeded) {
break;
}
- $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); // @phpstan-ignore-line
+ $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
if ($pCellParent !== null && $this->spreadsheet !== null) {
$cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($sheet1), false);
} else {
@@ -1901,7 +2026,7 @@ class Calculation extends CalculationLocale
} elseif (Information\ErrorValue::isError($operand2)) {
$result = $operand2;
} else {
- $result = str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)); //* @phpstan-ignore-line
+ $result = str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)); //* @phpstan-ignore binaryOp.invalid (unwrapresult can return mixed rather than string)
$result = StringHelper::substring(
$result,
0,
@@ -1934,8 +2059,8 @@ class Calculation extends CalculationLocale
$this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellIntersect));
$stack->push('Error', ExcelError::null(), null);
} else {
- $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' // @phpstan-ignore-line
- . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); // @phpstan-ignore-line
+ $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' // @phpstan-ignore argument.type ($oCol or $oRow might be empty), argument.type (ditto)
+ . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); // @phpstan-ignore argument.type ($oCol or $oRow might be empty), argument.type (ditto)
$this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellIntersect));
$stack->push('Value', $cellIntersect, $cellRef);
}
@@ -1950,13 +2075,13 @@ class Calculation extends CalculationLocale
break;
}
- } elseif (($token === '~') || ($token === '%')) {
+ } elseif (($token === '~') || ($token === '%')) { // @phpstan-ignore booleanOr.alwaysFalse (phpstan says token can't be plain string), identical.alwaysFalse (ditto), identical.alwaysFalse (ditto)
// if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
if (($arg = $stack->pop()) === null) {
return $this->raiseFormulaError('Internal error - Operand value missing from stack');
}
$arg = $arg['value'];
- if ($token === '~') {
+ if ($token === '~') { // @phpstan-ignore identical.alwaysFalse (phpstan says token can't be plain string)
$this->debugLog->writeDebugLog('Evaluating Negation of %s', $this->showValue($arg));
$multiplier = -1;
} else {
@@ -1988,20 +2113,27 @@ class Calculation extends CalculationLocale
} else {
$this->executeNumericBinaryOperation($multiplier, $arg, '*', $stack);
}
- } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', StringHelper::convertToString($token ?? ''), $matches)) {
+ } elseif (Preg::isMatch('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', StringHelper::convertToString($token ?? ''), $matches)) {
$cellRef = null;
/* Phpstan says matches[8/9/10] is never set,
and code coverage report seems to confirm.
- Appease PhpStan for now;
- probably delete this block later.
+ regex101.com confirms - only 7 capturing groups.
+ My theory is that this code expected regexp to
+ match cell *or* cellRange, but it does not
+ match the latter. Retain the code for now in case
+ we do want to add the range match later.
+ Probably delete this block later.
+ Until delete happens, turn code coverage off.
*/
if (isset($matches[self::$matchIndex8])) {
+ // @codeCoverageIgnoreStart
if ($cell === null) {
// We can't access the range, so return a REF error
$cellValue = ExcelError::REF();
} else {
$cellRef = $matches[6] . $matches[7] . ':' . $matches[self::$matchIndex9] . $matches[self::$matchIndex10];
+ $matches[2] = (string) $matches[2];
if ($matches[2] > '') {
$matches[2] = trim($matches[2], "\"'");
if ((str_contains($matches[2], '[')) || (str_contains($matches[2], ']'))) {
@@ -2026,12 +2158,14 @@ class Calculation extends CalculationLocale
$this->debugLog->writeDebugLog('Evaluation Result for cells %s is %s', $cellRef, $this->showTypeDetails($cellValue));
}
}
+ // @codeCoverageIgnoreEnd
} else {
if ($cell === null) {
// We can't access the cell, so return a REF error
$cellValue = ExcelError::REF();
} else {
$cellRef = $matches[6] . $matches[7];
+ $matches[2] = (string) $matches[2];
if ($matches[2] > '') {
$matches[2] = trim($matches[2], "\"'");
if ((str_contains($matches[2], '[')) || (str_contains($matches[2], ']'))) {
@@ -2042,7 +2176,11 @@ class Calculation extends CalculationLocale
if ($pCellParent !== null && $this->spreadsheet !== null) {
$cellSheet = $this->spreadsheet->getSheetByName($matches[2]);
if ($cellSheet && !$cellSheet->cellExists($cellRef)) {
- $cellSheet->setCellValue($cellRef, null);
+ try {
+ $cellSheet->setCellValue($cellRef, null);
+ } catch (SpreadsheetException) {
+ // do nothing
+ }
}
if ($cellSheet && $cellSheet->cellExists($cellRef)) {
$cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
@@ -2073,7 +2211,7 @@ class Calculation extends CalculationLocale
$cellValue = array_shift($cellValue);
}
if (is_string($cellValue)) {
- $cellValue = preg_replace('/"/', '""', $cellValue);
+ $cellValue = Preg::replace('/"/', '""', $cellValue);
}
$this->debugLog->writeDebugLog('Scalar Result for cell %s is %s', $cellRef, $this->showTypeDetails($cellValue));
}
@@ -2117,7 +2255,7 @@ class Calculation extends CalculationLocale
$a = $argCount - $i - 1;
if (
($passByReference)
- && (isset($phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) //* @phpstan-ignore-line
+ && (isset($phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) //* @phpstan-ignore offsetAccess.nonOffsetAccessible (possibly need to pass 2 arguments to isset)
&& ($phpSpreadsheetFunctions[$functionName]['passByReference'][$a])
) {
/** @var mixed[] $arg */
@@ -2184,7 +2322,7 @@ class Calculation extends CalculationLocale
if ($functionName !== 'MKMATRIX') {
if ($this->debugLog->getWriteDebugLog()) {
krsort($argArrayVals);
- $this->debugLog->writeDebugLog('Evaluating %s ( %s )', self::localeFunc($functionName), implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals)));
+ $this->debugLog->writeDebugLog('Evaluating %s ( %s )', self::localeFunc($functionName), implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals))); // @phpstan-ignore argument.type (flattenArray returns array rather than array)
}
}
@@ -2221,7 +2359,7 @@ class Calculation extends CalculationLocale
}
} else {
// if the token is a number, boolean, string or an Excel error, push it onto the stack
- /** @var ?string $token */
+ /** @var null|numeric-string $token */
if (isset(self::EXCEL_CONSTANTS[strtoupper($token ?? '')])) {
$excelConstant = strtoupper("$token");
$stack->push('Constant Value', self::EXCEL_CONSTANTS[$excelConstant]);
@@ -2229,7 +2367,7 @@ class Calculation extends CalculationLocale
$branchStore[$storeKey] = self::EXCEL_CONSTANTS[$excelConstant];
}
$this->debugLog->writeDebugLog('Evaluating Constant %s as %s', $excelConstant, $this->showTypeDetails(self::EXCEL_CONSTANTS[$excelConstant]));
- } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == self::FORMULA_STRING_QUOTE) || ($token[0] == '#')) { //* @phpstan-ignore-line
+ } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == self::FORMULA_STRING_QUOTE) || ($token[0] == '#')) { //* @phpstan-ignore function.alreadyNarrowedType (phpstan says is_bool has argument of *NEVER*?), equal.alwaysFalse (ditto)
/** @var array{type: string, reference: ?string} $tokenData */
$stack->push($tokenData['type'], $token, $tokenData['reference']);
if (isset($storeKey)) {
@@ -2355,7 +2493,7 @@ class Calculation extends CalculationLocale
$r = $stack->pop();
$result[$x] = $r['value'];
}
- } elseif (is_array($operand2) && is_array($operand1)) {
+ } elseif (is_array($operand2) /*&& is_array($operand1)*/) {
// Operand 1 and Operand 2 are both arrays
if (!$recursingArrays) {
self::checkMatrixOperands($operand1, $operand2, 2);
@@ -2368,7 +2506,7 @@ class Calculation extends CalculationLocale
$result[$x] = $r['value'];
}
} else {
- throw new Exception('Neither operand is an arra');
+ throw new Exception('Neither operand is an array');
}
// Log the result details
$this->debugLog->writeDebugLog('Comparison Evaluation Result is %s', $this->showTypeDetails($result));
@@ -2378,8 +2516,8 @@ class Calculation extends CalculationLocale
return $result;
}
- /** @return bool|mixed[] */
- private function executeBinaryComparisonOperation(mixed $operand1, mixed $operand2, string $operation, Stack &$stack, bool $recursingArrays = false): array|bool
+ /** @return array|bool|string */
+ private function executeBinaryComparisonOperation(mixed $operand1, mixed $operand2, string $operation, Stack &$stack, bool $recursingArrays = false): array|bool|string
{
// If we're dealing with matrix operations, we want a matrix result
if ((is_array($operand1)) || (is_array($operand2))) {
@@ -2581,6 +2719,8 @@ class Calculation extends CalculationLocale
if (!isset($aReferences[1])) {
// Single cell in range
sscanf($aReferences[0], '%[A-Z]%d', $currentCol, $currentRow);
+ /** @var string $currentCol */
+ /** @var int $currentRow */
if ($createCell && $worksheet !== null && !$worksheet->cellExists($aReferences[0])) {
$worksheet->setCellValue($aReferences[0], null);
}
@@ -2600,6 +2740,8 @@ class Calculation extends CalculationLocale
foreach ($aReferences as $reference) {
// Extract range
sscanf($reference, '%[A-Z]%d', $currentCol, $currentRow);
+ /** @var string $currentCol */
+ /** @var int $currentRow */
if ($createCell && $worksheet !== null && !$worksheet->cellExists($reference)) {
$worksheet->setCellValue($reference, null);
}
@@ -2760,6 +2902,7 @@ class Calculation extends CalculationLocale
// read constant value
if (str_contains($constantName, '::')) {
[$className, $constantName] = explode('::', $constantName);
+ /** @var class-string $className */
$constantReflector = new ReflectionClassConstant($className, $constantName);
return $constantReflector->getValue();
@@ -2814,10 +2957,12 @@ class Calculation extends CalculationLocale
$definedNameValue = $namedRange->getValue();
$definedNameType = $namedRange->isFormula() ? 'Formula' : 'Range';
if ($definedNameType === 'Range') {
- if (preg_match('/^(.*!)?(.*)$/', $definedNameValue, $matches) === 1) {
- $matches2 = trim($matches[2]);
- $matches2 = preg_replace('/ +/', ' ∩ ', $matches2) ?? $matches2;
- $matches2 = preg_replace('/,/', ' ∪ ', $matches2) ?? $matches2;
+ if (Preg::isMatch('/^(.*!)?(.*)$/', $definedNameValue, $matches)) {
+ $matches2 = Preg::replace(
+ ['/ +/', '/,/'],
+ [' ∩ ', ' ∪ '],
+ trim($matches[2])
+ );
$definedNameValue = $matches[1] . $matches2;
}
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/CalculationParserOnly.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/CalculationParserOnly.php
new file mode 100644
index 0000000..cfbc928
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/CalculationParserOnly.php
@@ -0,0 +1,43 @@
+= count($fieldNames)) {
@@ -143,7 +143,7 @@ abstract class DatabaseAbstract
}
$rowQuery = array_map(
- fn ($rowValue): string => (count($rowValue) > 1) ? 'AND(' . implode(',', $rowValue) . ')' : ($rowValue[0] ?? ''), // @phpstan-ignore-line
+ fn ($rowValue): string => (count($rowValue) > 1) ? 'AND(' . implode(',', $rowValue) . ')' : ($rowValue[0] ?? ''), // @phpstan-ignore nullCoalesce.offset ($rowValue[0] always exists?)
$baseQuery
);
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Date.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Date.php
index 9a6faf3..6ee9d7e 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Date.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Date.php
@@ -24,7 +24,7 @@ class Date
* Excel Function:
* DATE(year,month,day)
*
- * PhpSpreadsheet is a lot more forgiving than MS Excel when passing non numeric values to this function.
+ * PhpSpreadsheet is a lot more forgiving than MS Excel when passing non-numeric values to this function.
* A Month name or abbreviation (English only at this point) such as 'January' or 'Jan' will still be accepted,
* as will a day value with a suffix (e.g. '21st' rather than simply 21); again only English language.
*
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php
index d29ed6b..74f189e 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php
@@ -46,13 +46,22 @@ class DateValue
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue);
}
+ return self::fromString2($dateValue, null);
+ }
+
+ /**
+ * @return array|DateTime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object,
+ * depending on the value of the ReturnDateType flag
+ */
+ public static function fromString2(null|string|int|bool|float $dateValue, ?int $calendar = null): array|string|float|int|DateTime
+ {
// try to parse as date iff there is at least one digit
if (is_string($dateValue) && preg_match('/\d/', $dateValue) !== 1) {
return ExcelError::VALUE();
}
$dti = new DateTimeImmutable();
- $baseYear = SharedDateHelper::getExcelCalendar();
+ $baseYear = $calendar ?? SharedDateHelper::getExcelCalendar();
$dateValue = trim((string) $dateValue, '"');
// Strip any ordinals because they're allowed in Excel (English only)
$dateValue = (string) preg_replace('/(\d)(st|nd|rd|th)([ -\/])/Ui', '$1$3', $dateValue);
@@ -86,7 +95,7 @@ class DateValue
return self::finalResults($PHPDateArray, $dti, $baseYear);
}
- /** @param mixed[] $t1 */
+ /** @param list $t1 */
private static function t1ToString(array $t1, DateTimeImmutable $dti, bool $yearFound): string
{
if (count($t1) == 2) {
@@ -118,7 +127,7 @@ class DateValue
if (!Helpers::dateParseSucceeded($PHPDateArray)) {
// If original count was 1, we've already returned.
// If it was 2, we added another.
- // Therefore, neither of the first 2 stroks below can fail.
+ // Therefore, neither of the first 2 strtoks below can fail.
$testVal1 = strtok($dateValue, '- ');
$testVal2 = strtok('- ');
$testVal3 = strtok('- ') ?: $dti->format('Y');
@@ -160,9 +169,9 @@ class DateValue
$day = self::getInt($PHPDateArray, 'day');
$year = self::getInt($PHPDateArray, 'year');
if (!checkdate($month, $day, $year)) {
- return ($year === 1900 && $month === 2 && $day === 29) ? Helpers::returnIn3FormatsFloat(60.0) : ExcelError::VALUE();
+ return ($year === 1900 && $month === 2 && $day === 29) ? Helpers::returnIn3FormatsFloat(60.0, calendar: $baseYear) : ExcelError::VALUE();
}
- $retValue = Helpers::returnIn3FormatsArray($PHPDateArray, true);
+ $retValue = Helpers::returnIn3FormatsArray($PHPDateArray, true, calendar: $baseYear);
}
return $retValue;
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days.php
index a3e5058..43a96b0 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days.php
@@ -50,11 +50,9 @@ class Days
$days = ExcelError::VALUE();
$diff = $PHPStartDateObject->diff($PHPEndDateObject);
- if (!is_bool($diff->days)) {
- $days = $diff->days;
- if ($diff->invert) {
- $days = -$days;
- }
+ $days = $diff->days;
+ if ($diff->invert) {
+ $days = -$days;
}
return $days;
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php
index 2ccadf5..58eb68d 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php
@@ -76,7 +76,7 @@ class Days360
}
/**
- * Return the number of days between two dates based on a 360 day calendar.
+ * Return the number of days between two dates based on a 360-day calendar.
*/
private static function dateDiff360(int $startDay, int $startMonth, int $startYear, int $endDay, int $endMonth, int $endYear, bool $methodUS): int
{
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php
index ee1d486..5fc951c 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php
@@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
+use Throwable;
class Helpers
{
@@ -29,10 +30,10 @@ class Helpers
*
* @return float Excel date/time serial value
*/
- public static function getDateValue(mixed $dateValue, bool $allowBool = true): float
+ public static function getDateValue(mixed $dateValue, bool $allowBool = true, ?int $calendar = null): float
{
if (is_object($dateValue)) {
- $retval = SharedDateHelper::PHPToExcel($dateValue);
+ $retval = SharedDateHelper::PHPToExcel($dateValue, calendar: $calendar);
if (is_bool($retval)) {
throw new Exception(ExcelError::VALUE());
}
@@ -40,7 +41,7 @@ class Helpers
return $retval;
}
- self::nullFalseTrueToNumber($dateValue, $allowBool);
+ self::nullFalseTrueToNumber($dateValue, $allowBool, $calendar);
if (!is_numeric($dateValue)) {
$saveReturnDateType = Functions::getReturnDateType();
Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
@@ -56,6 +57,12 @@ class Helpers
throw new Exception(ExcelError::NAN());
}
+ try {
+ SharedDateHelper::excelToDateTimeObject((float) $dateValue, calendar: $calendar);
+ } catch (Throwable) {
+ throw new Exception(ExcelError::NAN());
+ }
+
return (float) $dateValue;
}
@@ -80,10 +87,10 @@ class Helpers
*
* @param float|int $dateValue date to be adjusted
*/
- public static function adjustDateByMonths($dateValue = 0, float $adjustmentMonths = 0): DateTime
+ public static function adjustDateByMonths($dateValue = 0, float $adjustmentMonths = 0, ?int $calendar = null): DateTime
{
// Execute function
- $PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue);
+ $PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue, calendar: $calendar);
$oMonth = (int) $PHPDateObject->format('m');
$oYear = (int) $PHPDateObject->format('Y');
@@ -134,7 +141,7 @@ class Helpers
*
* @param array{year: int, month: int, day: int, hour: int, minute: int, second: int} $dateArray
*/
- public static function returnIn3FormatsArray(array $dateArray, bool $noFrac = false): DateTime|float|int
+ public static function returnIn3FormatsArray(array $dateArray, bool $noFrac = false, ?int $calendar = null): DateTime|float|int
{
$retType = Functions::getReturnDateType();
if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) {
@@ -154,58 +161,60 @@ class Helpers
$dateArray['day'],
$dateArray['hour'],
$dateArray['minute'],
- $dateArray['second']
+ $dateArray['second'],
+ calendar: $calendar
);
if ($retType === Functions::RETURNDATE_EXCEL) {
return $noFrac ? floor($excelDateValue) : $excelDateValue;
}
// RETURNDATE_UNIX_TIMESTAMP)
- return SharedDateHelper::excelToTimestamp($excelDateValue);
+ return SharedDateHelper::excelToTimestamp($excelDateValue, calendar: $calendar);
}
/**
* Return result in one of three formats.
*/
- public static function returnIn3FormatsFloat(float $excelDateValue): float|int|DateTime
+ public static function returnIn3FormatsFloat(float $excelDateValue, ?int $calendar = null): float|int|DateTime
{
$retType = Functions::getReturnDateType();
if ($retType === Functions::RETURNDATE_EXCEL) {
return $excelDateValue;
}
if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) {
- return SharedDateHelper::excelToTimestamp($excelDateValue);
+ return SharedDateHelper::excelToTimestamp($excelDateValue, calendar: $calendar);
}
// RETURNDATE_PHP_DATETIME_OBJECT
- return SharedDateHelper::excelToDateTimeObject($excelDateValue);
+ return SharedDateHelper::excelToDateTimeObject($excelDateValue, calendar: $calendar);
}
/**
* Return result in one of three formats.
*/
- public static function returnIn3FormatsObject(DateTime $PHPDateObject): DateTime|float|int
+ public static function returnIn3FormatsObject(DateTime $PHPDateObject, ?int $calendar = null): DateTime|float|int
{
$retType = Functions::getReturnDateType();
if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) {
return $PHPDateObject;
}
if ($retType === Functions::RETURNDATE_EXCEL) {
- return (float) SharedDateHelper::PHPToExcel($PHPDateObject);
+ return (float) SharedDateHelper::PHPToExcel($PHPDateObject, calendar: $calendar);
}
// RETURNDATE_UNIX_TIMESTAMP
- $stamp = SharedDateHelper::PHPToExcel($PHPDateObject);
+ $stamp = SharedDateHelper::PHPToExcel($PHPDateObject, calendar: $calendar);
$stamp = is_bool($stamp) ? ((int) $stamp) : $stamp;
- return SharedDateHelper::excelToTimestamp($stamp);
+ return SharedDateHelper::excelToTimestamp($stamp, calendar: $calendar);
}
- private static function baseDate(): int
+ private static function baseDate(?int $calendar): int
{
if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
return 0;
}
- if (SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_MAC_1904) {
+ $calendar ??= SharedDateHelper::getExcelCalendar();
+ if ($calendar === SharedDateHelper::CALENDAR_MAC_1904) {
return 0;
}
@@ -215,10 +224,10 @@ class Helpers
/**
* Many functions accept null/false/true argument treated as 0/0/1.
*/
- public static function nullFalseTrueToNumber(mixed &$number, bool $allowBool = true): void
+ public static function nullFalseTrueToNumber(mixed &$number, bool $allowBool = true, ?int $calendar = null): void
{
$number = Functions::flattenSingleValue($number);
- $nullVal = self::baseDate();
+ $nullVal = self::baseDate($calendar);
if ($number === null) {
$number = $nullVal;
} elseif ($allowBool && is_bool($number)) {
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php
index a7f415d..86007d4 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php
@@ -4,7 +4,9 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
+use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
+use Throwable;
class TimeParts
{
@@ -44,6 +46,11 @@ class TimeParts
}
// Execute function
+ try {
+ SharedDateHelper::excelToDateTimeObject($timeValue);
+ } catch (Throwable) {
+ return ExcelError::NAN();
+ }
$timeValue = fmod($timeValue, 1);
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue);
SharedDateHelper::roundMicroseconds($timeValue);
@@ -85,6 +92,11 @@ class TimeParts
}
// Execute function
+ try {
+ SharedDateHelper::excelToDateTimeObject($timeValue);
+ } catch (Throwable) {
+ return ExcelError::NAN();
+ }
$timeValue = fmod($timeValue, 1);
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue);
SharedDateHelper::roundMicroseconds($timeValue);
@@ -126,6 +138,11 @@ class TimeParts
}
// Execute function
+ try {
+ SharedDateHelper::excelToDateTimeObject($timeValue);
+ } catch (Throwable) {
+ return ExcelError::NAN();
+ }
$timeValue = fmod($timeValue, 1);
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue);
SharedDateHelper::roundMicroseconds($timeValue);
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php
index fc1f72c..f710f51 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php
@@ -3,7 +3,7 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use Composer\Pcre\Preg;
-use Datetime;
+use DateTime;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
@@ -45,12 +45,12 @@ class TimeValue
* Date information in time_text is ignored.
* Or can be an array of date/time values
*
- * @return array|Datetime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object,
+ * @return array|DateTime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
* If an array of numbers is passed as the argument, then the returned result will also be an array
* with the same dimensions
*/
- public static function fromString(null|array|string|int|bool|float $timeValue): array|string|Datetime|int|float
+ public static function fromString(null|array|string|int|bool|float $timeValue): array|string|DateTime|int|float
{
if (is_array($timeValue)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue);
@@ -88,7 +88,7 @@ class TimeValue
} elseif ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) {
$retValue = (int) SharedDateHelper::excelToTimestamp($excelDateValue + 25569) - 3600;
} else {
- $retValue = new Datetime('1900-01-01 ' . $PHPDateArray['hour'] . ':' . $PHPDateArray['minute'] . ':' . $PHPDateArray['second']);
+ $retValue = new DateTime('1900-01-01 ' . $PHPDateArray['hour'] . ':' . $PHPDateArray['minute'] . ':' . $PHPDateArray['second']);
}
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Logger.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Logger.php
index e82faf5..d917320 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Logger.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Logger.php
@@ -78,8 +78,8 @@ class Logger
{
// Only write the debug log if logging is enabled
if ($this->writeDebugLog) {
- $message = sprintf($message, ...$args); //* @phpstan-ignore-line
- $cellReference = implode(' -> ', $this->cellStack->showStack());
+ $message = sprintf($message, ...$args); //* @phpstan-ignore argument.type (...$args could contain items that sprintf can't handle)
+ $cellReference = implode(' -> ', $this->cellStack->showStack()); //* @phpstan-ignore argument.type (showStack can return array not array)
if ($this->echoDebugLog) {
echo $cellReference,
($this->cellStack->count() > 0 ? ' => ' : ''),
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Operands/StructuredReference.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Operands/StructuredReference.php
index 9c4cf72..0b1f0eb 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Operands/StructuredReference.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Operands/StructuredReference.php
@@ -204,8 +204,9 @@ final class StructuredReference implements Operand, Stringable
/** @var string $reference */
$reference = str_replace('[' . self::ITEM_SPECIFIER_THIS_ROW . '],', '', $reference);
- foreach ($this->columns as $columnId => $columnName) {
- $columnName = str_replace("\u{a0}", ' ', $columnName); //* @phpstan-ignore-line
+ foreach ($this->columns as $columnId => $columnNamex) {
+ /** @var string $columnNamex */
+ $columnName = str_replace("\u{a0}", ' ', $columnNamex);
$reference = $this->adjustRowReference($columnName, $reference, $cell, $columnId);
}
@@ -338,8 +339,9 @@ final class StructuredReference implements Operand, Stringable
private function getColumnsForColumnReference(string $reference, int $startRow, int $endRow): string
{
$columnsSelected = false;
- foreach ($this->columns as $columnId => $columnName) {
- $columnName = str_replace("\u{a0}", ' ', $columnName ?? ''); //* @phpstan-ignore-line
+ foreach ($this->columns as $columnId => $columnNamex) {
+ /** @var ?string $columnNamex */
+ $columnName = str_replace("\u{a0}", ' ', $columnNamex ?? '');
$cellFrom = "{$columnId}{$startRow}";
$cellTo = "{$columnId}{$endRow}";
$cellReference = ($cellFrom === $cellTo) ? $cellFrom : "{$cellFrom}:{$cellTo}";
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertDecimal.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertDecimal.php
index 9834bcc..3513840 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertDecimal.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertDecimal.php
@@ -150,7 +150,7 @@ class ConvertDecimal extends ConvertBase
/**
* toOctal.
*
- * Return an decimal value as octal.
+ * Return a decimal value as octal.
*
* Excel Function:
* DEC2OCT(x[,places])
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ErfC.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ErfC.php
index 4cca2d6..db104bf 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ErfC.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ErfC.php
@@ -18,7 +18,7 @@ class ErfC
* Note: In Excel 2007 or earlier, if you input a negative value for the lower bound argument,
* the function would return a #NUM! error. However, in Excel 2010, the function algorithm was
* improved, so that it can now calculate the function for both positive and negative x values.
- * PhpSpreadsheet follows Excel 2010 behaviour, and accepts nagative arguments.
+ * PhpSpreadsheet follows Excel 2010 behaviour, and accepts negative arguments.
*
* Excel Function:
* ERFC(x)
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Interest.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Interest.php
index 68ac57b..33031cc 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Interest.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Interest.php
@@ -83,7 +83,7 @@ class Interest
* =ISPMT(interest_rate, period, number_payments, pv)
*
* @param mixed $interestRate is the interest rate for the investment
- * @param mixed $period is the period to calculate the interest rate. It must be betweeen 1 and number_payments.
+ * @param mixed $period is the period to calculate the interest rate. It must be between 1 and number_payments.
* @param mixed $numberOfPeriods is the number of payments for the annuity
* @param mixed $principleRemaining is the loan amount or present value of the payments
*/
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/InterestAndPrincipal.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/InterestAndPrincipal.php
deleted file mode 100644
index ea9abb9..0000000
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/InterestAndPrincipal.php
+++ /dev/null
@@ -1,44 +0,0 @@
-interest = $interest;
- $this->principal = $principal;
- }
-
- public function interest(): float
- {
- return $this->interest;
- }
-
- public function principal(): float
- {
- return $this->principal;
- }
-}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php
index 53668d1..130e4c1 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php
@@ -166,7 +166,7 @@ class NonPeriodic
$foundneg = false;
for ($i = 0; $i < $valCount; ++$i) {
$fld = $values[$i];
- if (!is_numeric($fld)) { //* @phpstan-ignore-line
+ if (!is_numeric($fld)) {
return ExcelError::VALUE();
} elseif ($fld > 0) {
$foundpos = true;
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaParser.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaParser.php
index f07b5b5..eda4787 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaParser.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaParser.php
@@ -21,8 +21,11 @@ namespace PhpOffice\PhpSpreadsheet\Calculation;
* 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.
*
+ * The following links are no longer valid.
* https://ewbi.blogs.com/develops/2007/03/excel_formula_p.html
* https://ewbi.blogs.com/develops/2004/12/excel_formula_p.html
+ *
+ * @deprecated 5.5.0 No replacement.
*/
class FormulaParser
{
@@ -463,16 +466,8 @@ class FormulaParser
$tokenCount = count($tokens1);
for ($i = 0; $i < $tokenCount; ++$i) {
$token = $tokens1[$i];
- if (isset($tokens1[$i - 1])) {
- $previousToken = $tokens1[$i - 1];
- } else {
- $previousToken = null;
- }
- if (isset($tokens1[$i + 1])) {
- $nextToken = $tokens1[$i + 1];
- } else {
- $nextToken = null;
- }
+ $previousToken = $tokens1[$i - 1] ?? null;
+ $nextToken = $tokens1[$i + 1] ?? null;
if ($token->getTokenType() != FormulaToken::TOKEN_TYPE_WHITESPACE) {
$tokens2[] = $token;
@@ -518,11 +513,7 @@ class FormulaParser
$tokenCount = count($tokens2);
for ($i = 0; $i < $tokenCount; ++$i) {
$token = $tokens2[$i];
- if (isset($tokens2[$i - 1])) {
- $previousToken = $tokens2[$i - 1];
- } else {
- $previousToken = null;
- }
+ $previousToken = $tokens2[$i - 1] ?? null;
if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORINFIX && $token->getValue() == '-') {
if ($i == 0) {
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaToken.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaToken.php
index cc7d48f..b46e846 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaToken.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaToken.php
@@ -21,8 +21,11 @@ namespace PhpOffice\PhpSpreadsheet\Calculation;
* 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.
*
+ * The following links are no longer valid.
* https://ewbi.blogs.com/develops/2007/03/excel_formula_p.html
* https://ewbi.blogs.com/develops/2004/12/excel_formula_p.html
+ *
+ * @deprecated 5.5.0 No replacement.
*/
class FormulaToken
{
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FunctionArray.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FunctionArray.php
index fd2d5a0..8f1de75 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FunctionArray.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FunctionArray.php
@@ -9,7 +9,7 @@ class FunctionArray extends CalculationBase
*
* @var array
*/
- protected static array $phpSpreadsheetFunctions = [
+ protected static array $phpSpreadsheetFunctions = [ // @phpstan-ignore property.defaultValue (Phpstan is wrong)
'ABS' => [
'category' => Category::CATEGORY_MATH_AND_TRIG,
'functionCall' => [MathTrig\Absolute::class, 'evaluate'],
@@ -149,7 +149,7 @@ class FunctionArray extends CalculationBase
],
'BAHTTEXT' => [
'category' => Category::CATEGORY_TEXT_AND_DATA,
- 'functionCall' => [Functions::class, 'DUMMY'],
+ 'functionCall' => [TextData\Thai::class, 'getBahtText'],
'argumentCount' => '1',
],
'BASE' => [
@@ -405,6 +405,7 @@ class FunctionArray extends CalculationBase
'category' => Category::CATEGORY_TEXT_AND_DATA,
'functionCall' => [TextData\Concatenate::class, 'actualCONCATENATE'],
'argumentCount' => '1+',
+ 'passCellReference' => true,
],
'CONFIDENCE' => [
'category' => Category::CATEGORY_STATISTICAL,
@@ -919,7 +920,7 @@ class FunctionArray extends CalculationBase
'FLOOR' => [
'category' => Category::CATEGORY_MATH_AND_TRIG,
'functionCall' => [MathTrig\Floor::class, 'floor'],
- 'argumentCount' => '1-2', // Excel requries 2, Ods/Gnumeric 1-2
+ 'argumentCount' => '1-2', // Excel requires 2, Ods/Gnumeric 1-2
],
'FLOOR.MATH' => [
'category' => Category::CATEGORY_MATH_AND_TRIG,
@@ -1284,8 +1285,9 @@ class FunctionArray extends CalculationBase
],
'INFO' => [
'category' => Category::CATEGORY_INFORMATION,
- 'functionCall' => [Functions::class, 'DUMMY'],
+ 'functionCall' => [Information\Info::class, 'getInfo'],
'argumentCount' => '1',
+ 'passCellReference' => true,
],
'INT' => [
'category' => Category::CATEGORY_MATH_AND_TRIG,
@@ -2335,17 +2337,17 @@ class FunctionArray extends CalculationBase
],
'T.DIST' => [
'category' => Category::CATEGORY_STATISTICAL,
- 'functionCall' => [Functions::class, 'DUMMY'],
+ 'functionCall' => [Statistical\Distributions\StudentT::class, 'tDotDist'],
'argumentCount' => '3',
],
'T.DIST.2T' => [
'category' => Category::CATEGORY_STATISTICAL,
- 'functionCall' => [Functions::class, 'DUMMY'],
+ 'functionCall' => [Statistical\Distributions\StudentT::class, 'tDotDistDot2T'],
'argumentCount' => '2',
],
'T.DIST.RT' => [
'category' => Category::CATEGORY_STATISTICAL,
- 'functionCall' => [Functions::class, 'DUMMY'],
+ 'functionCall' => [Statistical\Distributions\StudentT::class, 'tDotDistDotRT'],
'argumentCount' => '2',
],
'TEXT' => [
@@ -2425,12 +2427,12 @@ class FunctionArray extends CalculationBase
],
'T.INV' => [
'category' => Category::CATEGORY_STATISTICAL,
- 'functionCall' => [Statistical\Distributions\StudentT::class, 'inverse'],
+ 'functionCall' => [Statistical\Distributions\StudentT::class, 'tDotInv'],
'argumentCount' => '2',
],
'T.INV.2T' => [
'category' => Category::CATEGORY_STATISTICAL,
- 'functionCall' => [Functions::class, 'DUMMY'],
+ 'functionCall' => [Statistical\Distributions\StudentT::class, 'inverse'],
'argumentCount' => '2',
],
'TODAY' => [
@@ -2495,12 +2497,12 @@ class FunctionArray extends CalculationBase
],
'UNICHAR' => [
'category' => Category::CATEGORY_TEXT_AND_DATA,
- 'functionCall' => [TextData\CharacterConvert::class, 'character'],
+ 'functionCall' => [TextData\CharacterConvert::class, 'characterUnicode'],
'argumentCount' => '1',
],
'UNICODE' => [
'category' => Category::CATEGORY_TEXT_AND_DATA,
- 'functionCall' => [TextData\CharacterConvert::class, 'code'],
+ 'functionCall' => [TextData\CharacterConvert::class, 'codeUnicode'],
'argumentCount' => '1',
],
'UNIQUE' => [
@@ -2577,6 +2579,7 @@ class FunctionArray extends CalculationBase
'category' => Category::CATEGORY_WEB,
'functionCall' => [Web\Service::class, 'webService'],
'argumentCount' => '1',
+ 'passCellReference' => true,
],
'WEEKDAY' => [
'category' => Category::CATEGORY_DATE_AND_TIME,
@@ -2625,7 +2628,7 @@ class FunctionArray extends CalculationBase
],
'XLOOKUP' => [
'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
- 'functionCall' => [Functions::class, 'DUMMY'],
+ 'functionCall' => [LookupRef\XLookup::class, 'lookup'],
'argumentCount' => '3-6',
],
'XNPV' => [
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Functions.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Functions.php
index 16f6a3d..5004f52 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Functions.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Functions.php
@@ -3,6 +3,7 @@
namespace PhpOffice\PhpSpreadsheet\Calculation;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
@@ -360,4 +361,47 @@ class Functions
return $coordinate;
}
+
+ /** @param mixed[] $array */
+ public static function convertArrayToCellRange(array $array): string
+ {
+ $retVal = '';
+ $lastRow = $lastColumn = $firstRow = $firstColumn = 0;
+ foreach ($array as $rowkey => $row) {
+ if (!is_array($row) || !is_int($rowkey) || $rowkey < 1) {
+ $firstRow = 0;
+
+ break;
+ }
+ if ($firstRow > $rowkey || $firstRow === 0) {
+ $firstRow = $rowkey;
+ }
+ if ($lastRow < $rowkey) {
+ $lastRow = $rowkey;
+ }
+ foreach ($row as $colkey => $cellValue) {
+ if (!preg_match('/^[A-Z]{1,3}$/', $colkey)) {
+ $firstRow = 0;
+
+ break 2;
+ }
+ $column = Coordinate::columnIndexFromString($colkey);
+ if ($firstColumn > $column || $firstColumn === 0) {
+ $firstColumn = $column;
+ }
+ if ($lastColumn < $column) {
+ $lastColumn = $column;
+ }
+ }
+ }
+ if ($firstRow > 0 && $firstColumn > 0 && ($firstRow !== $lastRow || $firstColumn !== $lastColumn)) {
+ $retVal = Coordinate::stringFromColumnIndex($firstColumn)
+ . $firstRow
+ . ':'
+ . Coordinate::stringFromColumnIndex($lastColumn)
+ . $lastRow;
+ }
+
+ return $retVal;
+ }
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Information/Info.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Information/Info.php
new file mode 100644
index 0000000..61138fe
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Information/Info.php
@@ -0,0 +1,44 @@
+ '/',
+ 'numfile' => $cell?->getWorksheetOrNull()?->getParent()?->getSheetCount() ?? 1,
+ 'origin' => '$A:$A$1',
+ 'osversion' => 'PHP ' . PHP_VERSION,
+ 'recalc' => 'Automatic',
+ 'release' => PHP_VERSION,
+ 'system' => 'PHP',
+ 'memavail', 'memused', 'totmem' => ExcelError::NA(),
+ default => ExcelError::VALUE(),
+ };
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Information/Value.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Information/Value.php
index 57a6704..5662085 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Information/Value.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Information/Value.php
@@ -52,8 +52,10 @@ class Value
if (!empty($worksheet) && $cell->getWorksheet()->getParentOrThrow()->getSheetByName($worksheet) === null) {
return false;
}
- [$column, $row] = Coordinate::indexesFromString($cellValue ?? '');
- if ($column > 16384 || $row > 1048576) {
+
+ try {
+ [$column, $row] = Coordinate::indexesFromString($cellValue ?? ''); //* @phpstan-ignore nullCoalesce.variable (Not sure whether Phpstan is correct)
+ } catch (SpreadsheetException) {
return false;
}
@@ -265,7 +267,7 @@ class Value
if (is_bool($value)) {
return (int) $value;
}
- if (is_string($value) && substr($value, 0, 1) === '#') {
+ if (is_string($value) && str_starts_with($value, '#')) {
return $value;
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Logical/Conditional.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Logical/Conditional.php
index 9ac36c5..2dd3829 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Logical/Conditional.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Logical/Conditional.php
@@ -50,7 +50,7 @@ class Conditional
{
$condition = ($condition === null) ? true : Functions::flattenSingleValue($condition);
- if (ErrorValue::isError($condition)) {
+ if (ErrorValue::isError($condition, true)) {
return $condition;
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/ChooseRowsEtc.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/ChooseRowsEtc.php
index 944675e..b268de8 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/ChooseRowsEtc.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/ChooseRowsEtc.php
@@ -18,7 +18,7 @@ class ChooseRowsEtc
*/
public static function transpose(array $array): array
{
- return empty($array) ? [] : (array_map((count($array) === 1) ? (fn ($x) => [$x]) : null, ...$array)); // @phpstan-ignore-line
+ return empty($array) ? [] : (array_map((count($array) === 1) ? (fn ($x) => [$x]) : null, ...$array)); // @phpstan-ignore argument.type (phstan is wrong), argument.type (phpstan is wrong)
}
/** @return mixed[] */
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php
index 3a1a6c4..4346037 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php
@@ -58,7 +58,7 @@ class ExcelMatch
return $e->getMessage();
}
- // MATCH() is not case sensitive, so we convert lookup value to be lower cased if it's a string type.
+ // MATCH() is not case-sensitive, so we convert lookup value to be lower cased if it's a string type.
if (is_string($lookupValue)) {
$lookupValue = StringHelper::strToLower($lookupValue);
}
@@ -70,7 +70,7 @@ class ExcelMatch
};
if ($valueKey !== null) {
- return ++$valueKey; //* @phpstan-ignore-line
+ return ++$valueKey; //* @phpstan-ignore preInc.type ($valueKey can be mixed not numeric)
}
// Unsuccessful in finding a match, return #N/A error value
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php
index 486194e..3106d4e 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php
@@ -58,7 +58,7 @@ class Filter
*/
private static function filterByRow(array $lookupArray, array $matchArray): array
{
- $matchArray = array_values(array_column($matchArray, 0)); // @phpstan-ignore-line
+ $matchArray = array_values(array_column($matchArray, 0));
return array_filter(
array_values($lookupArray),
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Hstack.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Hstack.php
index 9be77e5..5d4938b 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Hstack.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Hstack.php
@@ -37,7 +37,7 @@ class Hstack
}
}
- $transpose = array_map(null, ...$inputData); //* @phpstan-ignore-line
+ $transpose = array_map(null, ...$inputData); //* @phpstan-ignore argument.type (inputData is mixed not array)
$returnMatrix = [];
foreach ($transpose as $array) {
$returnMatrix[] = Functions::flattenArray($array);
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php
index e7752aa..ab35ae3 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php
@@ -23,6 +23,13 @@ class Hyperlink
*/
public static function set(mixed $linkURL = '', mixed $displayName = null, ?Cell $cell = null): string
{
+ $worksheet = null;
+ $coordinate = '';
+ if ($cell !== null) {
+ $coordinate = $cell->getCoordinate();
+ $worksheet = $cell->getWorksheetOrNull();
+ }
+
$linkURL = ($linkURL === null) ? '' : StringHelper::convertToString(Functions::flattenSingleValue($linkURL));
$displayName = ($displayName === null) ? '' : Functions::flattenSingleValue($displayName);
@@ -30,17 +37,16 @@ class Hyperlink
return ExcelError::REF();
}
- if (is_object($displayName)) {
- $displayName = $linkURL;
- }
- $displayName = StringHelper::convertToString($displayName);
+ $displayName = StringHelper::convertToString($displayName, false);
if (trim($displayName) === '') {
$displayName = $linkURL;
}
- $cell->getHyperlink()
- ->setUrl($linkURL);
- $cell->getHyperlink()->setTooltip($displayName);
+ $worksheet?->getCell($coordinate)
+ ->getHyperlink()
+ ->setUrl($linkURL)
+ ->setTooltip($displayName)
+ ->setDisplay('');
return $displayName;
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php
index 756478c..c0e4b28 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php
@@ -106,14 +106,14 @@ class Indirect
*/
private static function extractRequiredCells(?Worksheet $worksheet, string $cellAddress): array
{
- return Calculation::getInstance($worksheet !== null ? $worksheet->getParent() : null)
+ return Calculation::getInstance($worksheet?->getParent())
->extractCellRange($cellAddress, $worksheet, false, createCell: true);
}
private static function handleRowColumnRanges(?Worksheet $worksheet, string $start, string $end): string
{
// Being lazy, we're only checking a single row/column to get the max
- if (ctype_digit($start) && $start <= 1048576) {
+ if (ctype_digit($start) && $start <= AddressRange::MAX_ROW) {
// Max 16,384 columns for Excel2007
$endColRef = ($worksheet !== null) ? $worksheet->getHighestDataColumn((int) $start) : AddressRange::MAX_COLUMN;
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php
index 9d3eea4..f3b6ad9 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php
@@ -40,15 +40,15 @@ class Lookup
$lookupColumns = self::columnCount($lookupVector);
}
- $resultVector = self::verifyResultVector($resultVector ?? $lookupVector); //* @phpstan-ignore-line
+ $resultVector = self::verifyResultVector($resultVector ?? $lookupVector); //* @phpstan-ignore argument.type (resultVector can be mixed not array>)
if ($lookupRows === 2 && !$hasResultVector) {
$resultVector = array_pop($lookupVector);
$lookupVector = array_shift($lookupVector);
}
- /** @var mixed[] $lookupVector */
- /** @var mixed[] $resultVector */
+ /** @var array $lookupVector */
+ /** @var array $resultVector */
if ($lookupColumns !== 2) {
$lookupVector = self::verifyLookupValues($lookupVector, $resultVector);
}
@@ -57,8 +57,8 @@ class Lookup
}
/**
- * @param mixed[] $lookupVector
- * @param mixed[] $resultVector
+ * @param array $lookupVector
+ * @param array $resultVector
*
* @return mixed[]
*/
@@ -80,6 +80,7 @@ class Lookup
if (is_array($dataValue2)) {
$dataValue2 = array_shift($dataValue2);
}
+ /** @var int $key2 */
$value = [$key1 => $dataValue1, $key2 => $dataValue2];
}
unset($value);
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php
index 620705e..9101559 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php
@@ -11,7 +11,7 @@ class LookupRefValidations
public static function validateInt(mixed $value): int
{
if (!is_numeric($value)) {
- if (is_string($value) && ErrorValue::isError($value)) {
+ if (is_string($value) && ErrorValue::isError($value, true)) {
throw new Exception($value);
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php
index 287e841..a916af5 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php
@@ -125,7 +125,7 @@ class Matrix
}
$rowKeys = array_keys($matrix);
- $columnKeys = @array_keys($matrix[$rowKeys[0]]); //* @phpstan-ignore-line
+ $columnKeys = @array_keys($matrix[$rowKeys[0]]); //* @phpstan-ignore argument.type (argument 1 of array_keys may be mixed not array)
if ($columnNum > count($columnKeys)) {
return ExcelError::REF();
@@ -135,14 +135,14 @@ class Matrix
return self::extractRowValue($matrix, $rowKeys, $rowNum);
}
- $columnNum = $columnKeys[--$columnNum]; //* @phpstan-ignore-line
+ $columnNum = $columnKeys[--$columnNum]; //* @phpstan-ignore offsetAccess.notFound (perhaps index --$columnNum does not exist)
if ($rowNum === 0) {
return array_map(
fn ($value): array => [$value],
array_column($matrix, $columnNum)
);
}
- $rowNum = $rowKeys[--$rowNum]; //* @phpstan-ignore-line
+ $rowNum = $rowKeys[--$rowNum]; //* @phpstan-ignore offsetAccess.notFound (perhaps index --$rowNum does not exist)
/** @var mixed[][] $matrix */
return $matrix[$rowNum][$columnNum];
@@ -150,7 +150,7 @@ class Matrix
/**
* @param mixed[] $matrix
- * @param mixed[] $rowKeys
+ * @param array $rowKeys
*/
private static function extractRowValue(array $matrix, array $rowKeys, int $rowNum): mixed
{
@@ -159,7 +159,7 @@ class Matrix
}
$rowNum = $rowKeys[--$rowNum];
- $row = $matrix[$rowNum]; //* @phpstan-ignore-line
+ $row = $matrix[$rowNum];
if (is_array($row)) {
return [$rowNum => $row];
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php
index 6b07262..bc6537d 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php
@@ -84,7 +84,7 @@ class Offset
$endCellColumn = self::adjustEndCellColumnForWidth($endCellColumn, $width, $startCellColumn, $columns);
$startCellColumn = Coordinate::stringFromColumnIndex($startCellColumn + 1);
- $endCellRow = self::adustEndCellRowForHeight($height, $startCellRow, $rows, $endCellRow);
+ $endCellRow = self::adjustEndCellRowForHeight($height, $startCellRow, $rows, $endCellRow);
if (($endCellRow <= 0) || ($endCellColumn < 0)) {
return ExcelError::REF();
@@ -102,7 +102,7 @@ class Offset
/** @return mixed[] */
private static function extractRequiredCells(?Worksheet $worksheet, string $cellAddress): array
{
- return Calculation::getInstance($worksheet !== null ? $worksheet->getParent() : null)
+ return Calculation::getInstance($worksheet?->getParent())
->extractCellRange($cellAddress, $worksheet, false);
}
@@ -152,7 +152,7 @@ class Offset
* @param null|object|scalar $height
* @param scalar $rows
*/
- private static function adustEndCellRowForHeight($height, int $startCellRow, $rows, int $endCellRow): int
+ private static function adjustEndCellRowForHeight($height, int $startCellRow, $rows, int $endCellRow): int
{
if (($height !== null) && (!is_object($height))) {
$endCellRow = $startCellRow + (int) $height - 1;
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php
index caf0983..59d5875 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php
@@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
+use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
@@ -106,7 +107,7 @@ class RowColumnInformation
if (self::cellAddressNullOrWhitespace($cellAddress)) {
return 1;
}
- if (is_string($cellAddress) && ErrorValue::isError($cellAddress)) {
+ if (is_string($cellAddress) && ErrorValue::isError($cellAddress, true)) {
return $cellAddress;
}
if (!is_array($cellAddress)) {
@@ -131,7 +132,7 @@ class RowColumnInformation
private static function convert0ToName(int|string $result): int|string
{
- if (is_int($result) && ($result <= 0 || $result > 1048576)) {
+ if (is_int($result) && ($result <= 0 || $result > AddressRange::MAX_ROW)) {
return ExcelError::NAME();
}
@@ -211,7 +212,7 @@ class RowColumnInformation
if (self::cellAddressNullOrWhitespace($cellAddress)) {
return 1;
}
- if (is_string($cellAddress) && ErrorValue::isError($cellAddress)) {
+ if (is_string($cellAddress) && ErrorValue::isError($cellAddress, true)) {
return $cellAddress;
}
if (!is_array($cellAddress)) {
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Selection.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Selection.php
index 53396c9..8d8daee 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Selection.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Selection.php
@@ -37,7 +37,7 @@ class Selection
} else {
return ExcelError::VALUE();
}
- $chosenEntry = floor($chosenEntry);
+ $chosenEntry = (int) floor($chosenEntry);
if (($chosenEntry < 0) || ($chosenEntry > $entryCount)) {
return ExcelError::VALUE();
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php
index 78a30a4..e013da7 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php
@@ -2,7 +2,6 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
-use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
@@ -20,6 +19,12 @@ class Sort extends LookupRefValidations
* The returned array is the same shape as the provided array argument.
* Both $sortIndex and $sortOrder can be arrays, to provide multi-level sorting.
*
+ * NOTE: If $sortArray contains a mixture of data types
+ * (string/int/bool), the results may be unexpected.
+ * This is also true if the array consists of string
+ * representations of numbers, especially if there are
+ * both positive and negative numbers in the mix.
+ *
* @param mixed $sortArray The range of cells being sorted
* @param mixed $sortIndex The column or row number within the sortArray to sort on
* @param mixed $sortOrder Flag indicating whether to sort ascending or descending
@@ -32,8 +37,7 @@ class Sort extends LookupRefValidations
public static function sort(mixed $sortArray, mixed $sortIndex = 1, mixed $sortOrder = self::ORDER_ASCENDING, mixed $byColumn = false): mixed
{
if (!is_array($sortArray)) {
- // Scalars are always returned "as is"
- return $sortArray;
+ $sortArray = [[$sortArray]];
}
/** @var mixed[][] */
@@ -55,7 +59,7 @@ class Sort extends LookupRefValidations
return $e->getMessage();
}
- // We want a simple, enumrated array of arrays where we can reference column by its index number.
+ // We want a simple, enumerated array of arrays where we can reference column by its index number.
/** @var callable(mixed): mixed */
$temp = 'array_values';
/** @var array $sortOrder */
@@ -72,6 +76,18 @@ class Sort extends LookupRefValidations
* The SORTBY function sorts the contents of a range or array based on the values in a corresponding range or array.
* The returned array is the same shape as the provided array argument.
* Both $sortIndex and $sortOrder can be arrays, to provide multi-level sorting.
+ * Microsoft doesn't even bother documenting that a column sort
+ * is possible. However, it is. According to:
+ * https://exceljet.net/functions/sortby-function
+ * When by_array is a horizontal range, SORTBY sorts horizontally by columns.
+ * My interpretation of this is that by_array must be an
+ * array which contains exactly one row.
+ *
+ * NOTE: If the "byArray" contains a mixture of data types
+ * (string/int/bool), the results may be unexpected.
+ * This is also true if the array consists of string
+ * representations of numbers, especially if there are
+ * both positive and negative numbers in the mix.
*
* @param mixed $sortArray The range of cells being sorted
* @param mixed $args
@@ -87,8 +103,16 @@ class Sort extends LookupRefValidations
public static function sortBy(mixed $sortArray, mixed ...$args): mixed
{
if (!is_array($sortArray)) {
- // Scalars are always returned "as is"
- return $sortArray;
+ $sortArray = [[$sortArray]];
+ }
+ $transpose = false;
+ $args0 = $args[0] ?? null;
+ if (is_array($args0) && count($args0) === 1) {
+ $args0 = reset($args0);
+ if (is_array($args0) && count($args0) > 1) {
+ $transpose = true;
+ $sortArray = Matrix::transpose($sortArray);
+ }
}
$sortArray = self::enumerateArrayKeys($sortArray);
@@ -99,14 +123,23 @@ class Sort extends LookupRefValidations
try {
$sortBy = $sortOrder = [];
for ($i = 0; $i < $argumentCount; $i += 2) {
- $sortBy[] = self::validateSortVector($args[$i], $lookupArraySize);
+ $argsI = $args[$i];
+ if (!is_array($argsI)) {
+ $argsI = [[$argsI]];
+ }
+ $sortBy[] = self::validateSortVector($argsI, $lookupArraySize);
$sortOrder[] = self::validateSortOrder($args[$i + 1] ?? self::ORDER_ASCENDING);
}
} catch (Exception $e) {
return $e->getMessage();
}
- return self::processSortBy($sortArray, $sortBy, $sortOrder);
+ $temp = self::processSortBy($sortArray, $sortBy, $sortOrder);
+ if ($transpose) {
+ $temp = Matrix::transpose($temp);
+ }
+
+ return $temp;
}
/**
@@ -130,10 +163,6 @@ class Sort extends LookupRefValidations
private static function validateScalarArgumentsForSort(mixed &$sortIndex, mixed &$sortOrder, int $sortArraySize): void
{
- if (is_array($sortIndex) || is_array($sortOrder)) {
- throw new Exception(ExcelError::VALUE());
- }
-
$sortIndex = self::validatePositiveInt($sortIndex, false);
if ($sortIndex > $sortArraySize) {
@@ -143,13 +172,13 @@ class Sort extends LookupRefValidations
$sortOrder = self::validateSortOrder($sortOrder);
}
- /** @return mixed[] */
- private static function validateSortVector(mixed $sortVector, int $sortArraySize): array
+ /**
+ * @param mixed[] $sortVector
+ *
+ * @return mixed[]
+ */
+ private static function validateSortVector(array $sortVector, int $sortArraySize): array
{
- if (!is_array($sortVector)) {
- throw new Exception(ExcelError::VALUE());
- }
-
// It doesn't matter if it's a row or a column vectors, it works either way
$sortVector = Functions::flattenArray($sortVector);
if (count($sortVector) !== $sortArraySize) {
@@ -203,12 +232,19 @@ class Sort extends LookupRefValidations
*/
private static function prepareSortVectorValues(array $sortVector): array
{
- // Strings should be sorted case-insensitive; with booleans converted to locale-strings
+ // Strings should be sorted case-insensitive.
+ // Booleans are a complete mess. Excel always seems to sort
+ // booleans in a mixed vector at either the top or the bottom,
+ // so converting them to string or int doesn't really work.
+ // Best advice is to use them in a boolean-only vector.
+ // Code below chooses int conversion, which is sensible,
+ // and, as a bonus, compatible with LibreOffice.
return array_map(
function ($value) {
if (is_bool($value)) {
- return ($value) ? Calculation::getTRUE() : Calculation::getFALSE();
- } elseif (is_string($value)) {
+ return (int) $value;
+ }
+ if (is_string($value)) {
return StringHelper::strToLower($value);
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/TorowTocol.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/TorowTocol.php
index 60b40d9..60945a2 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/TorowTocol.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/TorowTocol.php
@@ -80,7 +80,7 @@ class TorowTocol
continue;
}
$cell = 0;
- } elseif (ErrorValue::isError($cell)) {
+ } elseif (ErrorValue::isError($cell, true)) {
if ($ignore === 2 || $ignore === 3) {
continue;
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php
index 40a9df7..aec06a0 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php
@@ -40,7 +40,7 @@ class Unique
// so implode each row into a single string value
array_walk(
$lookupVector,
- //* @phpstan-ignore-next-line
+ // @phpstan-ignore argument.type (not sure what is needed)
function (array &$value): void {
$valuex = '';
$separator = '';
@@ -79,7 +79,7 @@ class Unique
if (str_ends_with($stringValue, "\x01")) {
// x01 should only end a string which is otherwise a float or int,
// so phpstan is technically correct but what it fears should not happen.
- $stringValue = 0 + substr($stringValue, 0, -1); //@phpstan-ignore-line
+ $stringValue = 0 + substr($stringValue, 0, -1); //@phpstan-ignore binaryOp.invalid (stringValue might not be numeric?)
}
}
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Vstack.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Vstack.php
index 919c262..ce2fe63 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Vstack.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Vstack.php
@@ -20,7 +20,7 @@ class Vstack
if (!is_array($matrix)) {
$count = 1;
} else {
- $count = count(reset($matrix)); //* @phpstan-ignore-line
+ $count = count(reset($matrix)); //* @phpstan-ignore argument.type (reset can theoretically return mixed not array)
}
$columns = max($columns, $count);
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/XLookup.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/XLookup.php
new file mode 100644
index 0000000..8e074a3
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/XLookup.php
@@ -0,0 +1,275 @@
+getMessage();
+ }
+
+ if (!in_array($matchMode, [0, -1, 1, 2], true)) {
+ return ExcelError::VALUE();
+ }
+
+ if (count($lookupArray) !== count($returnArray)) {
+ return ExcelError::VALUE();
+ }
+
+ try {
+ $index = match ($searchMode) {
+ 1 => self::searchLinear($lookupValue, $lookupArray, $matchMode, false),
+ -1 => self::searchLinear($lookupValue, $lookupArray, $matchMode, true),
+ 2 => self::searchBinary($lookupValue, $lookupArray, $matchMode, true),
+ -2 => self::searchBinary($lookupValue, $lookupArray, $matchMode, false),
+ };
+ } catch (UnhandledMatchError) {
+ return ExcelError::VALUE();
+ }
+
+ return ($index === null) ? $ifNotFound : $returnArray[$index];
+ }
+
+ // ---------------------------------------------------------------------------
+ // Search strategies
+ // ---------------------------------------------------------------------------
+
+ /**
+ * Linear search (searchMode 1 and -1).
+ *
+ * @param mixed[] $lookupArray
+ */
+ private static function searchLinear(
+ mixed $lookupValue,
+ array $lookupArray,
+ int $matchMode,
+ bool $reverse
+ ): ?int {
+
+ $keys = array_keys($lookupArray);
+ if ($reverse) {
+ $keys = array_reverse($keys);
+ }
+
+ $bestIdx = null;
+ $bestVal = null;
+
+ foreach ($keys as $i) {
+ /** @var scalar */
+ $candidate = $lookupArray[$i];
+
+ if ($matchMode === 2) {
+ // Wildcard: convert Excel wildcards to PHP regex
+ /** @var scalar $lookupValue */
+ if (self::wildcardMatch((string) $lookupValue, (string) $candidate)) {
+ return $i;
+ }
+
+ continue;
+ }
+
+ $cmp = self::compareValues($candidate, $lookupValue);
+
+ if ($cmp === 0) {
+ return $i; // Exact match — return immediately
+ }
+
+ if ($matchMode === -1 && $cmp < 0) {
+ // Next smaller: track largest value still below lookupValue
+ if ($bestVal === null || self::compareValues($candidate, $bestVal) > 0) {
+ $bestVal = $candidate;
+ $bestIdx = $i;
+ }
+ }
+
+ if ($matchMode === 1 && $cmp > 0) {
+ // Next larger: track smallest value still above lookupValue
+ if ($bestVal === null || self::compareValues($candidate, $bestVal) < 0) {
+ $bestVal = $candidate;
+ $bestIdx = $i;
+ }
+ }
+ }
+ /** @var ?int $bestIdx */
+
+ return $bestIdx;
+ }
+
+ /**
+ * Binary search (searchMode 2 and -2)
+ * Assumes array is sorted ascending (searchMode 2) or descending (searchMode -2).
+ *
+ * @param mixed[] $lookupArray
+ */
+ private static function searchBinary(
+ mixed $lookupValue,
+ array $lookupArray,
+ int $matchMode,
+ bool $ascending
+ ): ?int {
+
+ $values = array_values($lookupArray);
+ $keys = array_keys($lookupArray);
+ $lo = 0;
+ $hi = count($values) - 1;
+ $bestIdx = null;
+
+ while ($lo <= $hi) {
+ $mid = intdiv($lo + $hi, 2);
+ $cmp = self::compareValues($values[$mid], $lookupValue);
+
+ if (!$ascending) {
+ $cmp = -$cmp; // Flip for descending
+ }
+
+ if ($cmp === 0) {
+ return $keys[$mid]; // Exact match
+ }
+
+ if ($cmp < 0) {
+ if ($matchMode === -1) {
+ $bestIdx = $keys[$mid]; // Candidate for next smaller
+ }
+ $lo = $mid + 1;
+ } else {
+ if ($matchMode === 1) {
+ $bestIdx = $keys[$mid]; // Candidate for next larger
+ }
+ $hi = $mid - 1;
+ }
+ }
+ /** @var int $bestIdx */
+
+ return ($matchMode !== 0) ? $bestIdx : null;
+ }
+
+ /**
+ * Compare two values with type coercion matching Excel's behaviour:
+ * numbers < strings < booleans
+ */
+ private static function compareValues(mixed $a, mixed $b): int
+ {
+ // Numeric comparison
+ if (is_numeric($a) && is_numeric($b)) {
+ return $a <=> $b;
+ }
+ // String comparison (case-insensitive, like Excel)
+ if (is_string($a) && is_string($b)) {
+ return strcasecmp($a, $b);
+ }
+ // Bool comparison
+ if (is_bool($a) && is_bool($b)) {
+ return $a <=> $b;
+ }
+ // Cross-type: number < string < bool
+ $typeOrder = fn ($v) => match (true) {
+ is_numeric($v) => 0,
+ is_string($v) => 1,
+ is_bool($v) => 2,
+ default => 3,
+ };
+
+ return $typeOrder($a) <=> $typeOrder($b);
+ }
+
+ /**
+ * Wildcard match (matchMode 2)
+ * Supports Excel wildcards: * (any sequence), ? (any single char), ~ (escape).
+ */
+ private static function wildcardMatch(string $pattern, string $subject): bool
+ {
+ // Handle ~* and ~? escapes first
+ $regex = '';
+ $len = strlen($pattern);
+ for ($i = 0; $i < $len; ++$i) {
+ $ch = $pattern[$i];
+ if ($ch === '~' && $i + 1 < $len) {
+ $next = $pattern[++$i];
+ $regex .= preg_quote($next, '/');
+ } elseif ($ch === '*') {
+ $regex .= '.*';
+ } elseif ($ch === '?') {
+ $regex .= '.';
+ } else {
+ $regex .= preg_quote($ch, '/');
+ }
+ }
+
+ return (bool) preg_match('/^' . $regex . '$/i', $subject);
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Arabic.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Arabic.php
index 47c2c98..04549a1 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Arabic.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Arabic.php
@@ -57,7 +57,7 @@ class Arabic
*
* @param string|string[] $roman Should be a string, or can be an array of strings
*
- * @return array|int|string the arabic numberal contrived from the roman numeral
+ * @return array|int|string the arabic numeral contrived from the roman numeral
* If an array of numbers is passed as the argument, then the returned result will also be an array
* with the same dimensions
*/
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Round.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Round.php
index d8e338c..130636f 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Round.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Round.php
@@ -73,7 +73,7 @@ class Round
return round(
(float) (string) $number,
$digits,
- RoundingMode::AwayFromZero //* @phpstan-ignore-line
+ RoundingMode::AwayFromZero //* @phpstan-ignore argument.type (phpstan error till we no longer support Php < 8.4)
);
}
@@ -119,7 +119,7 @@ class Round
return round(
(float) (string) $number,
$digits,
- RoundingMode::TowardsZero //* @phpstan-ignore-line
+ RoundingMode::TowardsZero
);
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php
index 09f9cfa..cec1ff1 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php
@@ -32,6 +32,25 @@ class Subtotal
);
}
+ /**
+ * @param mixed[] $args
+ *
+ * @return mixed[]
+ */
+ protected static function filterFilteredArgs(Cell $cellReference, array $args): array
+ {
+ return array_filter(
+ $args,
+ function ($index) use ($cellReference) {
+ $explodeArray = explode('.', $index);
+ $row = $explodeArray[1] ?? '';
+
+ return is_numeric($row) ? ($cellReference->getWorksheet()->getRowDimension((int) $row)->getVisibleAfterFilter()) : true;
+ },
+ ARRAY_FILTER_USE_KEY
+ );
+ }
+
/**
* @param mixed[] $args
*
@@ -125,13 +144,18 @@ class Subtotal
if ($subtotal > 100) {
$aArgs = self::filterHiddenArgs($cellReference, $aArgs);
$subtotal -= 100;
+ } else {
+ $aArgs = self::filterFilteredArgs($cellReference, $aArgs);
}
$aArgs = self::filterFormulaArgs($cellReference, $aArgs);
if (array_key_exists($subtotal, self::CALL_FUNCTIONS)) {
$call = self::CALL_FUNCTIONS[$subtotal];
- return call_user_func_array($call, $aArgs); //* @phpstan-ignore-line
+ /** @var float|int|string */
+ $temp = call_user_func_array($call, $aArgs);
+
+ return $temp;
}
return ExcelError::VALUE();
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php
index b5d0339..0bbff73 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php
@@ -59,7 +59,7 @@ class Sum
$returnValue += $arg;
} elseif (is_bool($arg)) {
$returnValue += (int) $arg;
- } elseif (ErrorValue::isError($arg)) {
+ } elseif (ErrorValue::isError($arg, true)) {
/** @var string $arg */
return $arg;
} elseif ($arg !== null && !Functions::isCellValue($k)) {
@@ -110,6 +110,7 @@ class Sum
}
}
+ /** @var array $wrkArray */
return array_sum($wrkArray);
}
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Averages.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Averages.php
index 5880982..e9e7bd2 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Averages.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Averages.php
@@ -163,7 +163,8 @@ class Averages extends AggregateBase
sort($aArgs, SORT_NUMERIC);
$valueCount = $valueCount / 2;
if ($valueCount == floor($valueCount)) {
- $returnValue = ($aArgs[$valueCount--] + $aArgs[$valueCount]) / 2; //* @phpstan-ignore-line
+ $valueCount = (int) $valueCount;
+ $returnValue = ($aArgs[$valueCount--] + $aArgs[$valueCount]) / 2;
} else {
$valueCount = (int) floor($valueCount);
$returnValue = $aArgs[$valueCount];
@@ -243,7 +244,7 @@ class Averages extends AggregateBase
$maxfreqkey = $key;
$maxfreqdatum = $datum;
} elseif ($freq == $maxfreq) {
- if ($frequencyArray[$key]['index'] < $frequencyArray[$maxfreqkey]['index']) { //* @phpstan-ignore-line
+ if ($frequencyArray[$key]['index'] < $frequencyArray[$maxfreqkey]['index']) { //* @phpstan-ignore offsetAccess.notFound (not sure what phpstan wants)
$maxfreqkey = $key;
$maxfreqdatum = $datum;
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php
index 818d9db..7ac5ec1 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php
@@ -10,6 +10,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Database\DSum;
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcException;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
+use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class Conditional
{
@@ -26,11 +27,14 @@ class Conditional
* AVERAGEIF(range,condition[, average_range])
*
* @param mixed $range Data values, expect array
- * @param null|mixed[]|string $condition the criteria that defines which cells will be checked
+ * @param mixed $condition the criteria that defines which cells will be checked, expect null|mixed[]|string
* @param mixed $averageRange Data values
*/
- public static function AVERAGEIF(mixed $range, null|array|string $condition, mixed $averageRange = []): null|int|float|string
+ public static function AVERAGEIF(mixed $range, mixed $condition, mixed $averageRange = []): null|int|float|string
{
+ if ($condition !== null && !is_array($condition)) {
+ $condition = StringHelper::convertToString($condition);
+ }
if (!is_array($range) || !is_array($averageRange) || array_key_exists(0, $range) || array_key_exists(0, $averageRange)) {
$refError = ExcelError::REF();
if (in_array($refError, [$range, $averageRange], true)) {
@@ -62,7 +66,7 @@ class Conditional
return 0.0;
}
if (count($args) === 3) {
- return self::AVERAGEIF($args[1], $args[2], $args[0]); //* @phpstan-ignore-line
+ return self::AVERAGEIF($args[1], $args[2], $args[0]);
}
foreach ($args as $arg) {
if (is_array($arg) && array_key_exists(0, $arg)) {
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php
index 378ce8a..dbe64d3 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php
@@ -28,9 +28,9 @@ class Beta
* Or can be an array of values
* @param mixed $beta Parameter to the distribution as a float
* Or can be an array of values
- * @param mixed $rMin as an float
+ * @param mixed $rMin as a float
* Or can be an array of values
- * @param mixed $rMax as an float
+ * @param mixed $rMax as a float
* Or can be an array of values
*
* @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
@@ -116,6 +116,11 @@ class Beta
if (($alpha <= 0) || ($beta <= 0) || ($rMin == $rMax) || ($probability <= 0.0)) {
return ExcelError::NAN();
}
+ if (($alpha + $beta) > self::LOG_GAMMA_X_MAX_VALUE) {
+ // incompleteBeta declines to evaluate here and returns 0 for every x,
+ // so there is no quantile to search for.
+ return ExcelError::NAN();
+ }
return self::calculateInverse($probability, $alpha, $beta, $rMin, $rMax);
}
@@ -130,7 +135,7 @@ class Beta
while ((($b - $a) > Functions::PRECISION) && (++$i <= self::MAX_ITERATIONS)) {
$guess = ($a + $b) / 2;
$result = self::distribution($guess, $alpha, $beta);
- if (($result === $probability) || ($result === 0.0)) {
+ if ($result === $probability) {
$b = $a;
} elseif ($result > $probability) {
$b = $guess;
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php
index 67cf402..948e640 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php
@@ -65,7 +65,7 @@ class Binomial
/**
* BINOM.DIST.RANGE.
*
- * Returns returns the Binomial Distribution probability for the number of successes from a specified number
+ * Returns the Binomial Distribution probability for the number of successes from a specified number
* of trials falling into a specified range.
*
* @param mixed $trials Integer number of trials
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php
index b132484..0ce1547 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php
@@ -50,7 +50,7 @@ class ChiSquared
return ExcelError::NAN();
}
- return 1 - (Gamma::incompleteGamma($degrees / 2, $value / 2) / Gamma::gammaValue($degrees / 2));
+ return Gamma::regularizedGammaQ($degrees / 2, $value / 2);
}
/**
@@ -99,8 +99,16 @@ class ChiSquared
return 1 - (is_numeric($temp) ? $temp : 0);
}
- return ($value ** (($degrees / 2) - 1) * exp(-$value / 2))
- / ((2 ** ($degrees / 2)) * Gamma::gammaValue($degrees / 2));
+ if ($value == 0.0) {
+ if ($degrees === 2) {
+ return 0.5;
+ }
+
+ return ($degrees === 1) ? INF : 0.0;
+ }
+
+ // Log domain, so large degrees of freedom cannot overflow Gamma(d/2).
+ return exp((($degrees / 2) - 1) * log($value) - $value / 2 - ($degrees / 2) * M_LN2 - Gamma::logGamma($degrees / 2));
}
/**
@@ -133,8 +141,7 @@ class ChiSquared
return ExcelError::NAN();
}
- $callback = fn (float $value): float => 1 - (Gamma::incompleteGamma($degrees / 2, $value / 2)
- / Gamma::gammaValue($degrees / 2));
+ $callback = fn (float $value): float => Gamma::regularizedGammaQ($degrees / 2, $value / 2);
$newtonRaphson = new NewtonRaphson($callback);
@@ -258,75 +265,6 @@ class ChiSquared
private static function pchisq(float $chi2, int $degrees): float
{
- return self::gammp($degrees, 0.5 * $chi2);
- }
-
- private static function gammp(int $n, float $x): float
- {
- if ($x < 0.5 * $n + 1) {
- return self::gser($n, $x);
- }
-
- return 1 - self::gcf($n, $x);
- }
-
- // Return the incomplete gamma function P(n/2,x) evaluated by
- // series representation. Algorithm from numerical recipe.
- // Assume that n is a positive integer and x>0, won't check arguments.
- // Relative error controlled by the eps parameter
- private static function gser(int $n, float $x): float
- {
- /** @var float $gln */
- $gln = Gamma::ln($n / 2);
- $a = 0.5 * $n;
- $ap = $a;
- $sum = 1.0 / $a;
- $del = $sum;
- for ($i = 1; $i < 101; ++$i) {
- ++$ap;
- $del = $del * $x / $ap;
- $sum += $del;
- if ($del < $sum * self::EPS) {
- break;
- }
- }
-
- return $sum * exp(-$x + $a * log($x) - $gln);
- }
-
- // Return the incomplete gamma function Q(n/2,x) evaluated by
- // its continued fraction representation. Algorithm from numerical recipe.
- // Assume that n is a postive integer and x>0, won't check arguments.
- // Relative error controlled by the eps parameter
- private static function gcf(int $n, float $x): float
- {
- /** @var float $gln */
- $gln = Gamma::ln($n / 2);
- $a = 0.5 * $n;
- $b = $x + 1 - $a;
- $fpmin = 1.e-300;
- $c = 1 / $fpmin;
- $d = 1 / $b;
- $h = $d;
- for ($i = 1; $i < 101; ++$i) {
- $an = -$i * ($i - $a);
- $b += 2;
- $d = $an * $d + $b;
- if (abs($d) < $fpmin) {
- $d = $fpmin;
- }
- $c = $b + $an / $c;
- if (abs($c) < $fpmin) {
- $c = $fpmin;
- }
- $d = 1 / $d;
- $del = $d * $c;
- $h = $h * $del;
- if (abs($del - 1) < self::EPS) {
- break;
- }
- }
-
- return $h * exp(-$x + $a * log($x) - $gln);
+ return Gamma::regularizedGammaP($degrees / 2, 0.5 * $chi2);
}
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php
index 0127565..e615355 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php
@@ -55,9 +55,20 @@ class F
return Beta::incompleteBeta($adjustedValue, $u / 2, $v / 2);
}
- return (Gamma::gammaValue(($v + $u) / 2)
- / (Gamma::gammaValue($u / 2) * Gamma::gammaValue($v / 2)))
- * (($u / $v) ** ($u / 2))
- * (($value ** (($u - 2) / 2)) / ((1 + ($u / $v) * $value) ** (($u + $v) / 2)));
+ if ($value == 0.0) {
+ if ($u === 2) {
+ return 1.0;
+ }
+
+ return ($u === 1) ? INF : 0.0;
+ }
+
+ // Log domain, so large degrees of freedom cannot overflow the Gamma ratio.
+ return exp(
+ Gamma::logGamma(($v + $u) / 2) - Gamma::logGamma($u / 2) - Gamma::logGamma($v / 2)
+ + ($u / 2) * log($u / $v)
+ + (($u - 2) / 2) * log($value)
+ - (($u + $v) / 2) * log(1 + ($u / $v) * $value)
+ );
}
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php
index 60f5309..284c89d 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php
@@ -85,6 +85,10 @@ class Gamma extends GammaBase
*
* Returns the inverse of the Gamma distribution.
*
+ * For a probability so far into the upper tail that the forward-CDF series
+ * approximation plateaus below it, the root cannot be bracketed; the result
+ * is capped at the alpha*beta*5 search ceiling rather than diverging.
+ *
* @param mixed $probability Float probability at which you want to evaluate the distribution
* Or can be an array of values
* @param mixed $alpha Parameter to the distribution as a float
@@ -143,6 +147,9 @@ class Gamma extends GammaBase
return ExcelError::NAN();
}
- return log(self::gammaValue($value));
+ $result = self::logGamma($value);
+
+ // logGamma returns its MAX_VALUE sentinel when the result would overflow.
+ return ($result >= self::MAX_VALUE) ? ExcelError::NAN() : $result;
}
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php
index 4ae6075..e1d5539 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php
@@ -11,7 +11,7 @@ abstract class GammaBase
private const EPS = 2.22e-16;
- private const MAX_VALUE = 1.2e308;
+ protected const MAX_VALUE = 1.2e308;
private const SQRT2PI = 2.5066282746310005024157652848110452530069867406099;
@@ -20,10 +20,19 @@ abstract class GammaBase
protected static function calculateDistribution(float $value, float $a, float $b, bool $cumulative): float
{
if ($cumulative) {
- return self::incompleteGamma($a, $value / $b) / self::gammaValue($a);
+ return self::regularizedGammaP($a, $value / $b);
}
- return (1 / ($b ** $a * self::gammaValue($a))) * $value ** ($a - 1) * exp(0 - ($value / $b));
+ if ($value == 0.0) {
+ if ($a == 1.0) {
+ return 1.0 / $b;
+ }
+
+ return ($a < 1.0) ? INF : 0.0;
+ }
+
+ // Log domain, so large shape parameters cannot overflow Gamma(a).
+ return exp(($a - 1.0) * log($value) - $value / $b - $a * log($b) - self::logGamma($a));
}
/** @return float|string */
@@ -32,6 +41,24 @@ abstract class GammaBase
$xLo = 0;
$xHi = $alpha * $beta * 5;
+ // Extend the upper bound while it does not yet bracket the root, so a
+ // tail quantile beyond alpha*beta*5 is no longer clamped to it. Stop if
+ // the CDF stops increasing (series approximation past its usable range)
+ // and keep the original bound rather than expanding into that region.
+ $xHiBase = $xHi;
+ $cdfHi = self::calculateDistribution($xHi, $alpha, $beta, true);
+ while ($cdfHi < $probability) {
+ $xHiNext = $xHi * 2;
+ $cdfNext = self::calculateDistribution($xHiNext, $alpha, $beta, true);
+ if ($cdfNext <= $cdfHi) {
+ $xHi = $xHiBase;
+
+ break;
+ }
+ $xHi = $xHiNext;
+ $cdfHi = $cdfNext;
+ }
+
$dx = 1024;
$x = $xNew = 1;
$i = 0;
@@ -78,17 +105,98 @@ abstract class GammaBase
//
public static function incompleteGamma(float $a, float $x): float
{
- static $max = 32;
- $summer = 0;
- for ($n = 0; $n <= $max; ++$n) {
- $divisor = $a;
- for ($i = 1; $i <= $n; ++$i) {
- $divisor *= ($a + $i);
- }
- $summer += ($x ** $n / $divisor);
+ // Unregularized lower incomplete gamma; kept for backward compatibility.
+ return self::regularizedGammaP($a, $x) * self::gammaValue($a);
+ }
+
+ /**
+ * Regularized lower incomplete gamma P(a,x) = gamma(a,x) / Gamma(a).
+ * Series for x < a+1, else the complement of the continued fraction.
+ */
+ public static function regularizedGammaP(float $a, float $x): float
+ {
+ if ($x <= 0.0 || $a <= 0.0) {
+ return 0.0;
+ }
+ if ($x < $a + 1.0) {
+ return self::gammaSeries($a, $x);
}
- return $x ** $a * exp(0 - $x) * $summer;
+ return 1.0 - self::gammaContinuedFraction($a, $x);
+ }
+
+ /**
+ * Regularized upper incomplete gamma Q(a,x) = 1 - P(a,x).
+ * Continued fraction for x >= a+1 keeps the right tail free of cancellation.
+ */
+ public static function regularizedGammaQ(float $a, float $x): float
+ {
+ if ($x <= 0.0 || $a <= 0.0) {
+ return 1.0;
+ }
+ if ($x < $a + 1.0) {
+ return 1.0 - self::gammaSeries($a, $x);
+ }
+
+ return self::gammaContinuedFraction($a, $x);
+ }
+
+ // Near x ~ a both expansions need O(sqrt(a)) terms to reach EPS.
+ private static function incompleteGammaIterations(float $a): int
+ {
+ return max(self::MAX_ITERATIONS, (int) ceil(10.0 * sqrt($a)));
+ }
+
+ // P(a,x) by its series representation (Numerical Recipes gser).
+ private static function gammaSeries(float $a, float $x): float
+ {
+ $maxIterations = self::incompleteGammaIterations($a);
+ $gln = self::logGamma($a);
+ $ap = $a;
+ $sum = 1.0 / $a;
+ $del = $sum;
+ for ($i = 1; $i <= $maxIterations; ++$i) {
+ ++$ap;
+ $del *= $x / $ap;
+ $sum += $del;
+ if (abs($del) < abs($sum) * self::EPS) {
+ break;
+ }
+ }
+
+ return $sum * exp(-$x + $a * log($x) - $gln);
+ }
+
+ // Q(a,x) by its continued fraction representation (Numerical Recipes gcf).
+ private static function gammaContinuedFraction(float $a, float $x): float
+ {
+ $maxIterations = self::incompleteGammaIterations($a);
+ $fpMin = 1.0e-300;
+ $gln = self::logGamma($a);
+ $b = $x + 1.0 - $a;
+ $c = 1.0 / $fpMin;
+ $d = 1.0 / $b;
+ $h = $d;
+ for ($i = 1; $i <= $maxIterations; ++$i) {
+ $an = -$i * ($i - $a);
+ $b += 2.0;
+ $d = $an * $d + $b;
+ if (abs($d) < $fpMin) {
+ $d = $fpMin;
+ }
+ $c = $b + $an / $c;
+ if (abs($c) < $fpMin) {
+ $c = $fpMin;
+ }
+ $d = 1.0 / $d;
+ $del = $d * $c;
+ $h *= $del;
+ if (abs($del - 1.0) < self::EPS) {
+ break;
+ }
+ }
+
+ return $h * exp(-$x + $a * log($x) - $gln);
}
private const GAMMA_VALUE_P0 = 1.000000000190015;
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php
index 7e2ee92..5a0a7eb 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php
@@ -116,7 +116,7 @@ class Normal
// whatever purpose you want, but please show common courtesy and give credit
// where credit is due.
- // Input paramater is $p - probability - where 0 < p < 1.
+ // Input parameter is $p - probability - where 0 < p < 1.
// Coefficients in rational approximations
/** @var array */
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php
index cbe7d84..db1af8f 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php
@@ -4,9 +4,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
-use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
+/**
+ * Some of this code is drived from Perl CPAN Statistical::Distributions.
+ * Its copyright statement is:
+ * Copyright 2003 Michael Kospach. All rights reserved.
+ *
+ * This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
+ */
class StudentT
{
use ArrayEnabled;
@@ -28,9 +34,39 @@ class StudentT
* with the same dimensions
*/
public static function distribution(mixed $value, mixed $degrees, mixed $tails)
+ {
+ return self::calcDistribution($value, $degrees, $tails, self::distribution(...));
+ }
+
+ /**
+ * T.DIST.2T.
+ * Returns the two-tailed Student's t distribution.
+ *
+ * @return array|float|string The result, or a string containing an error
+ */
+ public static function tDotDistDot2T(mixed $value, mixed $degrees)
+ {
+ return self::calcDistribution($value, $degrees, 2, self::distribution(...));
+ }
+
+ /**
+ * T.DIST.RT.
+ * Returns the right-tailed Student's t distribution.
+ *
+ * @return array|float|string The result, or a string containing an error
+ */
+ public static function tDotDistDotRT(mixed $value, mixed $degrees)
+ {
+ return self::calcDistribution($value, $degrees, 1, self::distribution(...));
+ }
+
+ /**
+ * @return array|float|string The result, or a string containing an error
+ */
+ private static function calcDistribution(mixed $value, mixed $degrees, mixed $tails, callable $callback)
{
if (is_array($value) || is_array($degrees) || is_array($tails)) {
- return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $degrees, $tails);
+ return self::evaluateArrayArguments($callback, $value, $degrees, $tails);
}
try {
@@ -45,13 +81,105 @@ class StudentT
return ExcelError::NAN();
}
- return self::calculateDistribution($value, $degrees, $tails);
+ return self::subTProb($value, $degrees, $tails);
}
/**
- * TINV.
+ * Based on code from Perl CPAN Statistical::Distributions.
+ */
+ private static function subTProb(float $x, int $n, int $tails): float
+ {
+ $w = atan2($x / sqrt($n), 1);
+ $z = cos($w) ** 2;
+ $y = 1;
+
+ for ($i = $n - 2; $i >= 2; $i -= 2) {
+ $y = 1 + ($i - 1) / $i * $z * $y;
+ }
+
+ if ($n % 2 == 0) {
+ $a = sin($w) / 2;
+ $b = 0.5;
+ } else {
+ $a = ($n == 1) ? 0 : (sin($w) * cos($w) / M_PI);
+ $b = 0.5 + $w / M_PI;
+ }
+
+ return $tails * max(0, 1 - $b - $a * $y);
+ }
+
+ /**
+ * T.DIST.
+ * Returns the Student's left-tailed t distribution,
+ * either as a cumulative distribution function (cdf) (TRUE)
+ * or as a probability density function (pdf) (FALSE),
+ * where TRUE/FALSE are the value of $cumulative parameter.
*
- * Returns the one-tailed probability of the chi-squared distribution.
+ * "True" algoritm adapted from java.
+ * org.apache.commons.math3.distribution.TDistribution.
+ * "False" algorithm comes from:
+ * https://statproofbook.github.io/P/t-pdf.html
+ *
+ * @param mixed $cumulative Expecting bool. See above for explanation.
+ *
+ * @return array|float|string The result, or a string containing an error
+ */
+ public static function tDotDist(mixed $value, mixed $degrees, mixed $cumulative)
+ {
+ if (is_array($value) || is_array($degrees) || is_array($cumulative)) {
+ return self::evaluateArrayArguments(self::tDotDist(...), $value, $degrees, $cumulative);
+ }
+
+ try {
+ $value = DistributionValidations::validateFloat($value);
+ $degrees = DistributionValidations::validateInt($degrees);
+ $cumulative = DistributionValidations::validateBool($cumulative);
+ } catch (Exception $e) {
+ return $e->getMessage();
+ }
+
+ /** @var int $degrees */
+ if (($degrees < 1)) {
+ return ExcelError::NAN();
+ }
+ /** @var float $value */
+ if (!$cumulative) {
+ return self::tDotDistFalse($value, $degrees);
+ }
+
+ $f16 = $degrees / ($degrees + $value * $value);
+ $g16 = 0.5 * $degrees;
+ $h16 = 0.5;
+ $result = Beta::distribution($f16, $g16, $h16);
+ if (is_numeric($result)) {
+ $result = ($value < 0) ? (0.5 * $result) : (1 - 0.5 * $result);
+ }
+
+ return $result;
+ }
+
+ private static function tDotDistFalse(float $value, int $degrees): float|string
+ {
+ $result = $k15 = Gamma::gamma(($degrees + 1) / 2);
+ if (is_numeric($k15)) {
+ $result = $k16 = Gamma::gamma($degrees / 2);
+ if (is_numeric($k16)) {
+ $k17 = sqrt(M_PI * $degrees);
+ $k18 = $k15 / ($k16 * $k17);
+ $k19 = $value * $value / $degrees + 1;
+ $k20 = -($degrees + 1) / 2;
+ $k21 = $k19 ** $k20;
+ $result = $k18 * $k21;
+ }
+ }
+
+ /** @var float|string $result */
+ return $result;
+ }
+
+ /**
+ * TINV and T.INV.2T.
+ * Returns the two-tailed inverse of the Student t distribution.
*
* @param mixed $probability Float probability for the function
* Or can be an array of values
@@ -63,9 +191,17 @@ class StudentT
* with the same dimensions
*/
public static function inverse(mixed $probability, mixed $degrees)
+ {
+ return self::calcInverse($probability, $degrees, 2, self::inverse(...));
+ }
+
+ /**
+ * @return array|float|string The result, or a string containing an error
+ */
+ private static function calcInverse(mixed $probability, mixed $degrees, int $tails, callable $callback2)
{
if (is_array($probability) || is_array($degrees)) {
- return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $degrees);
+ return self::evaluateArrayArguments($callback2, $probability, $degrees, $tails);
}
try {
@@ -79,54 +215,102 @@ class StudentT
return ExcelError::NAN();
}
- $callback = fn ($value) => self::distribution($value, $degrees, 2);
+ $callback = fn ($value) => self::distribution($value, $degrees, $tails);
$newtonRaphson = new NewtonRaphson($callback);
- return $newtonRaphson->execute($probability);
+ $result = $newtonRaphson->execute($probability);
+ if (is_numeric($result) && $tails === 1) {
+ $result = -$result; // @codeCoverageIgnore
+ }
+
+ return $result;
}
- private static function calculateDistribution(float $value, int $degrees, int $tails): float
+ /**
+ * T.INV.
+ * Returns the left-tailed inverse of the Student's t distribution.
+ *
+ * Based on code from Perl CPAN Statistical::Distributions.
+ *
+ * @return array|float|string The result, or a string containing an error
+ */
+ public static function tDotInv(mixed $probability, mixed $degrees)
{
- // tdist, which finds the probability that corresponds to a given value
- // of t with k degrees of freedom. This algorithm is translated from a
- // pascal function on p81 of "Statistical Computing in Pascal" by D
- // Cooke, A H Craven & G M Clark (1985: Edward Arnold (Pubs.) Ltd:
- // London). The above Pascal algorithm is itself a translation of the
- // fortran algoritm "AS 3" by B E Cooper of the Atlas Computer
- // Laboratory as reported in (among other places) "Applied Statistics
- // Algorithms", editied by P Griffiths and I D Hill (1985; Ellis
- // Horwood Ltd.; W. Sussex, England).
- $tterm = $degrees;
- $ttheta = atan2($value, sqrt($tterm));
- $tc = cos($ttheta);
- $ts = sin($ttheta);
-
- if (($degrees % 2) === 1) {
- $ti = 3;
- $tterm = $tc;
- } else {
- $ti = 2;
- $tterm = 1;
+ if (is_array($probability) || is_array($degrees)) {
+ return self::evaluateArrayArguments(self::tDotInv(...), $probability, $degrees);
}
- $tsum = $tterm;
- while ($ti < $degrees) {
- $tterm *= $tc * $tc * ($ti - 1) / $ti;
- $tsum += $tterm;
- $ti += 2;
+ try {
+ $probability = DistributionValidations::validateProbability($probability);
+ $degrees = DistributionValidations::validateInt($degrees);
+ } catch (Exception $e) {
+ return $e->getMessage();
}
- $tsum *= $ts;
- if (($degrees % 2) == 1) {
- $tsum = Functions::M_2DIVPI * ($tsum + $ttheta);
+ if ($degrees < 1) {
+ return ExcelError::NAN();
+ }
+ if ($probability == 0.5) {
+ return 0.0;
+ }
+ if ($probability < 0.5) {
+ $result = self::tDotInv(1.0 - $probability, $degrees);
+
+ return is_numeric($result) ? -$result : $result;
+ }
+ $p = $probability;
+ $n = $degrees;
+ $u = self::subU($p);
+ $u2 = $u ** 2;
+
+ $a = ($u2 + 1) / 4;
+ $b = ((5 * $u2 + 16) * $u2 + 3) / 96;
+ $c = (((3 * $u2 + 19) * $u2 + 17) * $u2 - 15) / 384;
+ $d = ((((79 * $u2 + 776) * $u2 + 1482) * $u2 - 1920) * $u2 - 945) / 92160;
+ $e = (((((27 * $u2 + 339) * $u2 + 930) * $u2 - 1782) * $u2 - 765) * $u2 + 17955) / 368640;
+
+ $x = $u * (1 + ($a + ($b + ($c + ($d + $e / $n) / $n) / $n) / $n) / $n);
+
+ if ($n <= log10($p) ** 2 + 3) {
+ do {
+ $p1 = self::subTProb($x, $n, 1);
+ $n1 = $n + 1;
+ $delta = ($p1 - $p)
+ / exp(($n1 * log($n1 / ($n + $x * $x))
+ + log($n / $n1 / 2 / M_PI) - 1
+ + (1 / $n1 - 1 / $n) / 6) / 2);
+ $x += $delta;
+ $round = sprintf('%.' . abs((int) (log10(abs($x)) - 4)) . 'F', $delta);
+ } while (($x) && ($round != 0));
}
- $tValue = 0.5 * (1 + $tsum);
- if ($tails == 1) {
- return 1 - abs($tValue);
+ return -$x;
+ }
+
+ /**
+ * Based on code from Perl CPAN Statistical::Distributions.
+ */
+ private static function subU(float $p): float
+ {
+ $y = -log(4 * $p * (1 - $p));
+ $x = sqrt(
+ $y * (1.570796288
+ + $y * (.03706987906
+ + $y * (-.8364353589E-3
+ + $y * (-.2250947176E-3
+ + $y * (.6841218299E-5
+ + $y * (0.5824238515E-5
+ + $y * (-.104527497E-5
+ + $y * (.8360937017E-7
+ + $y * (-.3231081277E-8
+ + $y * (.3657763036E-10
+ + $y * .6936233982E-12))))))))))
+ );
+ if ($p > 0.5) {
+ $x = -$x;
}
- return 1 - abs((1 - $tValue) - $tValue);
+ return $x;
}
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Maximum.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Maximum.php
index 88531a5..3a4f9eb 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Maximum.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Maximum.php
@@ -25,7 +25,7 @@ class Maximum extends MaxMinBase
// Loop through arguments
$aArgs = Functions::flattenArray($args);
foreach ($aArgs as $arg) {
- if (ErrorValue::isError($arg)) {
+ if (ErrorValue::isError($arg, true)) {
$returnValue = $arg;
break;
@@ -63,7 +63,7 @@ class Maximum extends MaxMinBase
// Loop through arguments
$aArgs = Functions::flattenArray($args);
foreach ($aArgs as $arg) {
- if (ErrorValue::isError($arg)) {
+ if (ErrorValue::isError($arg, true)) {
$returnValue = $arg;
break;
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Minimum.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Minimum.php
index 874edc3..0a1a0c0 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Minimum.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Minimum.php
@@ -25,7 +25,7 @@ class Minimum extends MaxMinBase
// Loop through arguments
$aArgs = Functions::flattenArray($args);
foreach ($aArgs as $arg) {
- if (ErrorValue::isError($arg)) {
+ if (ErrorValue::isError($arg, true)) {
$returnValue = $arg;
break;
@@ -63,7 +63,7 @@ class Minimum extends MaxMinBase
// Loop through arguments
$aArgs = Functions::flattenArray($args);
foreach ($aArgs as $arg) {
- if (ErrorValue::isError($arg)) {
+ if (ErrorValue::isError($arg, true)) {
$returnValue = $arg;
break;
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Percentiles.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Percentiles.php
index f2ee15f..0fb0749 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Percentiles.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Percentiles.php
@@ -89,6 +89,7 @@ class Percentiles
return $e->getMessage();
}
+ /** @var array */
$valueSet = self::rankFilterValues($valueSet);
$valueCount = count($valueSet);
if ($valueCount == 0) {
@@ -172,6 +173,7 @@ class Percentiles
return $e->getMessage();
}
+ /** @var array */
$valueSet = self::rankFilterValues($valueSet);
if ($order === self::RANK_SORT_DESCENDING) {
rsort($valueSet, SORT_NUMERIC);
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php
index 2f15bfc..305f1b7 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php
@@ -6,11 +6,14 @@ use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
+use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class CharacterConvert
{
use ArrayEnabled;
+ private static string $oneByteCharacterSet = 'Windows-1252';
+
/**
* CHAR.
*
@@ -27,19 +30,45 @@ class CharacterConvert
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $character);
}
+ return self::characterBoth($character, true);
+ }
+
+ /** @return array|string */
+ public static function characterUnicode(mixed $character): array|string
+ {
+ if (is_array($character)) {
+ return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $character);
+ }
+
+ return self::characterBoth($character, false);
+ }
+
+ private static function characterBoth(mixed $character, bool $ansi = true): string
+ {
try {
$character = Helpers::validateInt($character, true);
} catch (CalcExp $e) {
return $e->getMessage();
}
+ if ($ansi && $character === 219 && self::$oneByteCharacterSet[0] === 'M') {
+ return '€';
+ }
+
$min = Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE ? 0 : 1;
- if ($character < $min || $character > 255) {
+ if ($character < $min || ($ansi && $character > 255) || $character > 0x10FFFF) {
return ExcelError::VALUE();
}
- $result = iconv('UCS-4LE', 'UTF-8', pack('V', $character));
+ if ($character > 0x10FFFD) { // last assigned
+ return ExcelError::NA();
+ }
+ if ($ansi) {
+ $result = chr($character);
- return ($result === false) ? '' : $result;
+ return (string) iconv(self::$oneByteCharacterSet, 'UTF-8//IGNORE', $result);
+ }
+
+ return mb_chr($character, 'UTF-8');
}
/**
@@ -57,7 +86,28 @@ class CharacterConvert
if (is_array($characters)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $characters);
}
+ if (is_bool($characters) && Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
+ $characters = $characters ? '1' : '0';
+ }
+ return self::codeBoth(StringHelper::convertToString($characters, convertBool: true), true);
+ }
+
+ /** @return array|int|string */
+ public static function codeUnicode(mixed $characters): array|string|int
+ {
+ if (is_array($characters)) {
+ return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $characters);
+ }
+ if (is_bool($characters) && Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
+ $characters = $characters ? '1' : '0';
+ }
+
+ return self::codeBoth(StringHelper::convertToString($characters, convertBool: true), false);
+ }
+
+ private static function codeBoth(string $characters, bool $ansi = true): int|string
+ {
try {
$characters = Helpers::extractString($characters, true);
} catch (CalcExp $e) {
@@ -72,22 +122,27 @@ class CharacterConvert
if (mb_strlen($characters, 'UTF-8') > 1) {
$character = mb_substr($characters, 0, 1, 'UTF-8');
}
-
- return self::unicodeToOrd($character);
- }
-
- private static function unicodeToOrd(string $character): int
- {
- $retVal = 0;
- $iconv = iconv('UTF-8', 'UCS-4LE', $character);
- if ($iconv !== false) {
- /** @var false|int[] */
- $result = unpack('V', $iconv);
- if (is_array($result) && isset($result[1])) {
- $retVal = $result[1];
- }
+ if ($ansi && $character === '€' && self::$oneByteCharacterSet[0] === 'M') {
+ return 219;
}
- return $retVal;
+ $result = mb_ord($character, 'UTF-8');
+ if ($ansi) {
+ $result = iconv('UTF-8', self::$oneByteCharacterSet . '//IGNORE', $character);
+
+ return ($result !== '') ? ord("$result") : 63; // question mark
+ }
+
+ return $result;
+ }
+
+ public static function setWindowsCharacterSet(): void
+ {
+ self::$oneByteCharacterSet = 'Windows-1252';
+ }
+
+ public static function setMacCharacterSet(): void
+ {
+ self::$oneByteCharacterSet = 'MAC';
}
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php
index d51d44b..f377821 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php
@@ -7,6 +7,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
+use PhpOffice\PhpSpreadsheet\Calculation\Internal\ExcelArrayPseudoFunctions;
+use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
@@ -17,7 +19,7 @@ class Concatenate
/**
* This implements the CONCAT function, *not* CONCATENATE.
*
- * @param mixed[] $args
+ * @param mixed $args data to be concatenated
*/
public static function CONCATENATE(...$args): string
{
@@ -47,17 +49,33 @@ class Concatenate
/**
* This implements the CONCATENATE function.
*
- * @param mixed[] $args data to be concatenated
+ * @param mixed $args data to be concatenated
*
* @return array|string
*/
public static function actualCONCATENATE(...$args): array|string
{
+ $useSingle = false;
+ $cell = null;
+ $count = count($args);
+ if ($args[$count - 1] instanceof Cell) {
+ /** @var Cell */
+ $cell = array_pop($args);
+ $type = $cell->getWorksheet()->getParent()?->getCalculationEngine()->getInstanceArrayReturnType() ?? Calculation::getArrayReturnType();
+ $useSingle = $type === Calculation::RETURN_ARRAY_AS_VALUE;
+ }
if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_GNUMERIC) {
return self::CONCATENATE(...$args);
}
$result = '';
foreach ($args as $operand2) {
+ if ($useSingle && $cell instanceof Cell && is_array($operand2)) {
+ $temp = Functions::convertArrayToCellRange($operand2);
+ if ($temp !== '') {
+ $operand2 = ExcelArrayPseudoFunctions::single($temp, $cell);
+ }
+ }
+ /** @var null|array|bool|float|int|string $operand2 */
$result = self::concatenate2Args($result, $operand2);
if (ErrorValue::isError($result, true) === true) {
break;
@@ -142,6 +160,7 @@ class Concatenate
$aArgs = Functions::flattenArray($args);
$returnValue = self::evaluateTextJoinArray($ignoreEmpty, $aArgs);
+ /** @var string[] $aArgs */
$returnValue ??= implode($delimiter, $aArgs);
if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) {
$returnValue = ExcelError::CALC();
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Format.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Format.php
index 23bf74f..c6e37f4 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Format.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Format.php
@@ -96,16 +96,13 @@ class Format
if ($decimals < 0) {
$decimals = 0;
}
- if ($noCommas === false) {
- $valueResult = number_format(
- $valueResult,
- $decimals,
- StringHelper::getDecimalSeparator(),
- StringHelper::getThousandsSeparator()
- );
- }
- return (string) $valueResult;
+ return number_format(
+ $valueResult,
+ $decimals,
+ StringHelper::getDecimalSeparator(),
+ $noCommas ? '' : StringHelper::getThousandsSeparator()
+ );
}
/**
@@ -209,18 +206,20 @@ class Format
Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
if (str_contains($value, ':')) {
+ /** @var DateTimeInterface|float|int|string */
$timeValue = Functions::scalar(DateTimeExcel\TimeValue::fromString($value));
if ($timeValue !== ExcelError::VALUE()) {
Functions::setReturnDateType($dateSetting);
- return $timeValue; //* @phpstan-ignore-line
+ return $timeValue;
}
}
+ /** @var DateTimeInterface|float|int|string */
$dateValue = Functions::scalar(DateTimeExcel\DateValue::fromString($value));
if ($dateValue !== ExcelError::VALUE()) {
Functions::setReturnDateType($dateSetting);
- return $dateValue; //* @phpstan-ignore-line
+ return $dateValue;
}
Functions::setReturnDateType($dateSetting);
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Search.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Search.php
index 0c4b977..0aad5ed 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Search.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Search.php
@@ -12,7 +12,7 @@ class Search
use ArrayEnabled;
/**
- * FIND (case sensitive search).
+ * FIND (case-sensitive search).
*
* @param mixed $needle The string to look for
* Or can be an array of values
@@ -54,7 +54,7 @@ class Search
}
/**
- * SEARCH (case insensitive search).
+ * SEARCH (case-insensitive search).
*
* @param mixed $needle The string to look for
* Or can be an array of values
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Text.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Text.php
index a4cc14f..40e3a17 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Text.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Text.php
@@ -171,12 +171,12 @@ class Text
{
$columnCount = array_reduce(
$rows,
- fn (int $counter, array $row): int => max($counter, count($row)), //* @phpstan-ignore-line
+ fn (int $counter, array $row): int => max($counter, count($row)), //* @phpstan-ignore argument.type (I don't know)
0
);
return array_map(
- fn (array $row): array => (count($row) < $columnCount) //* @phpstan-ignore-line
+ fn (array $row): array => (count($row) < $columnCount) //* @phpstan-ignore argument.type (I don't know)
? array_merge($row, array_fill(0, $columnCount - count($row), $padding))
: $row,
$rows
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Thai.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Thai.php
new file mode 100644
index 0000000..0d08db0
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Thai.php
@@ -0,0 +1,139 @@
+ 'ศูนย์',
+ 1 => 'หนึ่ง',
+ 2 => 'สอง',
+ 3 => 'สาม',
+ 4 => 'สี่',
+ 5 => 'ห้า',
+ 6 => 'หก',
+ 7 => 'เจ็ด',
+ 8 => 'แปด',
+ 9 => 'เก้า',
+ ];
+
+ private const THAI_UNITS = [
+ 1 => 'สิบ',
+ 2 => 'ร้อย',
+ 3 => 'พัน',
+ 4 => 'หมื่น',
+ 5 => 'แสน',
+ 6 => 'ล้าน',
+ ];
+
+ private const THAI_COMPOUND_ONE = 'เอ็ด';
+ private const THAI_COMPOUND_TWO = 'ยี่';
+ private const THAI_INTEGER = 'ถ้วน';
+ private const THAI_MINUS = 'ลบ';
+ private const THAI_BAHT = 'บาท';
+ private const THAI_SATANG = 'สตางค์';
+
+ /**
+ * BAHTTEXT.
+ *
+ * @param mixed $number The number or array of numbers to convert
+ *
+ * @return array|string If an array of values is passed as the argument, then the returned result will also be an array with the same dimensions
+ */
+ public static function getBahtText(mixed $number): array|string
+ {
+ if (is_array($number)) {
+ return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number);
+ }
+
+ if (is_string($number) && preg_match('/^-?\d+$/', $number)) {
+ $isNegative = str_starts_with($number, '-');
+ $baht = ltrim($number, '-0') ?: '0';
+ $satang = '00';
+ } elseif (is_bool($number) || is_numeric($number)) {
+ $number += 0;
+ $isNegative = $number < 0;
+ [$baht, $satang] = explode('.', number_format(abs($number), 2, '.', ''));
+ } else {
+ return ExcelError::VALUE();
+ }
+
+ $hasWhole = $baht !== '0';
+ $hasFraction = $satang !== '00';
+
+ if (!$hasWhole && !$hasFraction) {
+ return self::THAI_DIGITS[0] . self::THAI_BAHT . self::THAI_INTEGER;
+ }
+
+ $text = $isNegative
+ ? self::THAI_MINUS
+ : '';
+
+ if ($hasWhole) {
+ $text .= self::convertLarge($baht) . self::THAI_BAHT;
+ }
+
+ $text .= $hasFraction
+ ? self::convertBlock($satang) . self::THAI_SATANG
+ : self::THAI_INTEGER;
+
+ return $text;
+ }
+
+ private static function convertLarge(string $digits): string
+ {
+ $length = strlen($digits) % 6 ?: 6;
+
+ $chunks = [
+ substr($digits, 0, $length),
+ ...str_split(substr($digits, $length), 6),
+ ];
+
+ $chunks = array_filter($chunks, fn (string $chunk): bool => $chunk !== '');
+
+ return implode(
+ self::THAI_UNITS[6],
+ array_map(self::convertBlock(...), $chunks)
+ );
+ }
+
+ private static function convertBlock(string $block): string
+ {
+ $out = '';
+ $length = strlen($block);
+ $i = 0;
+
+ // Hundreds and higher powers
+ for ($power = $length - 1; $power >= 2; --$power) {
+ $digit = $block[$i++];
+ if ($digit !== '0') {
+ $out .= self::THAI_DIGITS[$digit] . self::THAI_UNITS[$power];
+ }
+ }
+
+ // Tens
+ $ten = $length > 1 ? $block[$i++] : '0';
+ if ($ten !== '0') {
+ $out .= match ($ten) {
+ '1' => '',
+ '2' => self::THAI_COMPOUND_TWO,
+ default => self::THAI_DIGITS[$ten],
+ } . self::THAI_UNITS[1];
+ }
+
+ // Ones
+ $one = $block[$i] ?? '0';
+ if ($one !== '0') {
+ $out .= $ten !== '0' && $one === '1'
+ ? self::THAI_COMPOUND_ONE
+ : self::THAI_DIGITS[$one];
+ }
+
+ return $out;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Web/Service.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Web/Service.php
index ad06d39..0268400 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Web/Service.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Web/Service.php
@@ -2,9 +2,10 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\Web;
+use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
-use PhpOffice\PhpSpreadsheet\Settings;
-use Psr\Http\Client\ClientExceptionInterface;
+use PhpOffice\PhpSpreadsheet\Cell\Cell;
+use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class Service
{
@@ -18,38 +19,43 @@ class Service
*
* @return string the output resulting from a call to the webservice
*/
- public static function webService(string $url): string
+ public static function webService(mixed $url, ?Cell $cell = null): ?string
{
- $url = trim($url);
- if (strlen($url) > 2048) {
+ if (is_array($url)) {
+ $url = Functions::flattenSingleValue($url);
+ }
+ $url = trim(StringHelper::convertToString($url, false));
+ if (mb_strlen($url) > 2048) {
return ExcelError::VALUE(); // Invalid URL length
}
-
- if (!preg_match('/^http[s]?:\/\//', $url)) {
+ $parsed = parse_url($url);
+ $scheme = $parsed['scheme'] ?? '';
+ if ($scheme !== 'http' && $scheme !== 'https') {
return ExcelError::VALUE(); // Invalid protocol
}
+ $domainWhiteList = $cell?->getWorksheet()->getParent()?->getDomainWhiteList() ?? [];
+ $host = $parsed['host'] ?? '';
+ if (!in_array($host, $domainWhiteList, true)) {
+ return ($cell === null) ? null : Functions::NOT_YET_IMPLEMENTED; // will be converted to oldCalculatedValue or null
+ }
// Get results from the webservice
- $client = Settings::getHttpClient();
- $requestFactory = Settings::getRequestFactory();
- $request = $requestFactory->createRequest('GET', $url);
-
- try {
- $response = $client->sendRequest($request);
- } catch (ClientExceptionInterface) {
- return ExcelError::VALUE(); // cURL error
+ $ctxArray = [
+ 'http' => [
+ 'follow_location' => 0,
+ 'user_agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
+ ],
+ ];
+ if ($scheme === 'https') {
+ $ctxArray['ssl'] = ['crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT];
}
-
- if ($response->getStatusCode() != 200) {
- return ExcelError::VALUE(); // cURL error
- }
-
- $output = $response->getBody()->getContents();
- if (strlen($output) > 32767) {
+ $ctx = stream_context_create($ctxArray);
+ $output = @file_get_contents($url, false, $ctx);
+ if ($output === false || mb_strlen($output) > 32767) {
return ExcelError::VALUE(); // Output not a string or too long
}
- return $output;
+ return ($output === '') ? Functions::NOT_YET_IMPLEMENTED : $output;
}
/**
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx
index 080d5e7..78a67d0 100644
Binary files a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx and b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx differ
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/nl/functions b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/nl/functions
index ce0b30c..7fd661b 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/nl/functions
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/nl/functions
@@ -38,6 +38,7 @@ DVARP = DBVARP
## Datum- en tijdfuncties (Date & Time Functions)
##
DATE = DATUM
+DATEDIF = DATUMVERSCHIL
DATESTRING = DATUMNOTATIE
DATEVALUE = DATUMWAARDE
DAY = DAG
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AddressHelper.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AddressHelper.php
index 9b48f59..15920f5 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AddressHelper.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AddressHelper.php
@@ -58,7 +58,7 @@ class AddressHelper
$columnReference = (string) $currentColumnNumber;
}
// Bracketed C references are relative to the current column
- if (is_string($columnReference) && $columnReference[0] === '[') { // @phpstan-ignore-line
+ if ($columnReference[0] === '[') {
$columnReference = $currentColumnNumber + (int) trim($columnReference, '[]');
}
$columnReference = (int) $columnReference;
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AddressRange.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AddressRange.php
index 22e9d11..12977c1 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AddressRange.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AddressRange.php
@@ -7,11 +7,14 @@ namespace PhpOffice\PhpSpreadsheet\Cell;
*/
interface AddressRange
{
- public const MAX_ROW = 1048576;
-
+ public const MAX_ROW = 1_048_576;
public const MAX_COLUMN = 'XFD';
+ public const MAX_COLUMN_INT = 16_384;
- public const MAX_COLUMN_INT = 16384;
+ public const MAX_ROW_XLS_OLD = 16_384;
+ public const MAX_ROW_XLS = 65_536;
+ public const MAX_COLUMN_XLS = 'IV';
+ public const MAX_COLUMN_INT_XLS = 256;
/**
* @return T
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AdvancedValueBinder.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AdvancedValueBinder.php
index 1db0c0f..a0edb8a 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AdvancedValueBinder.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AdvancedValueBinder.php
@@ -60,7 +60,7 @@ class AdvancedValueBinder extends DefaultValueBinder implements IValueBinder
// Check for currency
if (preg_match(FormattedNumber::currencyMatcherRegexp(), (string) preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value), $matches, PREG_UNMATCHED_AS_NULL)) {
// Convert value to number
- $sign = ($matches['PrefixedSign'] ?? $matches['PrefixedSign2'] ?? $matches['PostfixedSign']) ?? null;
+ $sign = ($matches['PrefixedSign'] ?? $matches['PrefixedSign2'] ?? $matches['PostfixedSign']) ?? null; //* @phpstan-ignore nullCoalesce.unnecessary (Not sure whether Phpstan is correct)
$currencyCode = $matches['PrefixedCurrency'] ?? $matches['PostfixedCurrency'] ?? '';
/** @var string */
$temp = str_replace([$decimalSeparatorNoPreg, $currencyCode, ' ', '-'], ['.', '', '', ''], (string) preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value));
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Cell.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Cell.php
index e9fbabb..dd9b533 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Cell.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Cell.php
@@ -15,6 +15,7 @@ use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\CellStyleAssessor;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Style\Protection;
use PhpOffice\PhpSpreadsheet\Style\Style;
+use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Stringable;
@@ -190,12 +191,22 @@ class Cell implements Stringable
public function getFormattedValue(): string
{
$currentCalendar = SharedDate::getExcelCalendar();
- SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar());
- $formattedValue = (string) NumberFormat::toFormattedString(
- $this->getCalculatedValueString(),
- (string) $this->getStyle()->getNumberFormat()->getFormatCode(true)
+ SharedDate::setExcelCalendar(
+ $this->getWorksheet()
+ ->getParent()
+ ?->getExcelCalendar()
);
- SharedDate::setExcelCalendar($currentCalendar);
+
+ try {
+ $formattedValue = NumberFormat::toFormattedString(
+ $this->getCalculatedValueString(),
+ (string) $this->getStyle()
+ ->getNumberFormat()
+ ->getFormatCode(true)
+ );
+ } finally {
+ SharedDate::setExcelCalendar($currentCalendar);
+ }
return $formattedValue;
}
@@ -233,6 +244,9 @@ class Cell implements Stringable
*/
public function setValue(mixed $value, ?IValueBinder $binder = null): self
{
+ if ($this->hadHyperlink) {
+ $this->clearHyperlink();
+ }
// Cells?->Worksheet?->Spreadsheet
$binder ??= $this->parent?->getParent()?->getParent()?->getValueBinder() ?? self::getValueBinder();
if (!$binder->bindValue($this, $value)) {
@@ -242,11 +256,31 @@ class Cell implements Stringable
return $this;
}
+ private bool $hadHyperlink = false;
+
+ /** @internal */
+ public function setHadHyperlink(bool $hadHyperlink): void
+ {
+ $this->hadHyperlink = $hadHyperlink;
+ }
+
+ private function clearHyperlink(): void
+ {
+ $worksheet = $this->getWorksheetOrNull();
+ if ($worksheet !== null) {
+ $coordinate = $this->getCoordinate();
+ $worksheet->setHyperlink($coordinate, null);
+ }
+ $this->hadHyperlink = false;
+ }
+
/**
* Set the value for a cell, with the explicit data type passed to the method (bypassing any use of the value binder).
*
* @param mixed $value Value
* @param string $dataType Explicit data type, see DataType::TYPE_*
+ * This parameter is currently optional (default = string).
+ * Omitting it is ***DEPRECATED***, and the default will be removed in a future release.
* Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
* method, then it is your responsibility as an end-user developer to validate that the value and
* the datatype match.
@@ -257,6 +291,9 @@ class Cell implements Stringable
*/
public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE_STRING): self
{
+ if ($this->hadHyperlink) {
+ $this->clearHyperlink();
+ }
$oldValue = $this->value;
$quotePrefix = false;
@@ -307,6 +344,14 @@ class Cell implements Stringable
$this->value = SharedDate::convertIsoDate($value);
$dataType = DataType::TYPE_NUMERIC;
+ break;
+ case DataType::TYPE_DRAWING_IN_CELL:
+ if ($value instanceof BaseDrawing) {
+ $this->value = $value;
+ } else {
+ throw new SpreadsheetException('Item is not a drawing');
+ }
+
break;
case DataType::TYPE_ERROR:
$this->value = DataType::checkErrorCode($value);
@@ -325,16 +370,16 @@ class Cell implements Stringable
$worksheet = $this->getWorksheet();
$spreadsheet = $worksheet->getParent();
if (isset($spreadsheet) && $spreadsheet->getIndex($worksheet, true) >= 0) {
- $originalSelected = $worksheet->getSelectedCells();
- $activeSheetIndex = $spreadsheet->getActiveSheetIndex();
- $style = $this->getStyle();
- $oldQuotePrefix = $style->getQuotePrefix();
+ // Avoid Worksheet::getStyle() (selection + validation) unless quotePrefix must change.
+ $oldQuotePrefix = $spreadsheet->getCellXfByIndex($this->getXfIndex())->getQuotePrefix();
if ($oldQuotePrefix !== $quotePrefix) {
- $style->setQuotePrefix($quotePrefix);
- }
- $worksheet->setSelectedCells($originalSelected);
- if ($activeSheetIndex >= 0) {
- $spreadsheet->setActiveSheetIndex($activeSheetIndex);
+ $originalSelected = $worksheet->getSelectedCells();
+ $activeSheetIndex = $spreadsheet->getActiveSheetIndex();
+ $this->getStyle()->setQuotePrefix($quotePrefix);
+ $worksheet->setSelectedCells($originalSelected);
+ if ($activeSheetIndex >= 0) {
+ $spreadsheet->setActiveSheetIndex($activeSheetIndex);
+ }
}
}
@@ -391,7 +436,7 @@ class Cell implements Stringable
$value = array_shift($value);
}
- return StringHelper::convertToString($value, false);
+ return StringHelper::convertToString($value, false, convertBool: true);
}
/**
@@ -503,7 +548,7 @@ class Cell implements Stringable
$coordinate = $this->getCoordinate();
$ref = $oldAttributesRef;
if (preg_match('/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/', $ref, $matches) === 1) {
- if (isset($matches[3])) {
+ if (isset($matches[5])) {
$minCol = $matches[1];
$minRow = (int) $matches[2];
$maxCol = $matches[4];
@@ -746,7 +791,8 @@ class Cell implements Stringable
throw new SpreadsheetException('Cannot get hyperlink for cell that is not bound to a worksheet');
}
- return $this->getWorksheet()->getHyperlink($this->getCoordinate());
+ return $this->getWorksheet()
+ ->getHyperlink($this->getCoordinate());
}
/**
@@ -760,7 +806,8 @@ class Cell implements Stringable
throw new SpreadsheetException('Cannot set hyperlink for cell that is not bound to a worksheet');
}
- $this->getWorksheet()->setHyperlink($this->getCoordinate(), $hyperlink);
+ $this->getWorksheet()
+ ->setHyperlink($this->getCoordinate(), $hyperlink);
return $this->updateInCollection();
}
@@ -973,6 +1020,19 @@ class Cell implements Stringable
return $this->updateInCollection();
}
+ /**
+ * Set the XF index without triggering updateInCollection().
+ *
+ * This is intended for use by readers that will immediately follow with
+ * setValueExplicit(), avoiding a redundant cache write.
+ *
+ * @internal
+ */
+ public function setXfIndexNoUpdate(int $indexValue): void
+ {
+ $this->xfIndex = $indexValue;
+ }
+
/**
* Set the formula attributes.
*
@@ -1034,4 +1094,96 @@ class Cell implements Stringable
return $hidden !== Protection::PROTECTION_UNPROTECTED;
}
+
+ /**
+ * Return cell $right positions to the right of this one.
+ */
+ public function cursorRight(int $right = 1): self
+ {
+ $row = $this->getRow();
+ $col = $this->getColumn();
+ $colIndex = Coordinate::columnIndexFromString($col);
+ $newCol = max(
+ 1,
+ min($colIndex + $right, AddressRange::MAX_COLUMN_INT)
+ );
+ $newColStr = Coordinate::stringFromColumnIndex($newCol);
+
+ return $this->getWorksheet()->getCell("$newColStr$row");
+ }
+
+ /**
+ * Return cell $left positions to the left of this one.
+ */
+ public function cursorLeft(int $left = 1): self
+ {
+ return $this->cursorRight(-$left);
+ }
+
+ /**
+ * Return cell $down positions below this one.
+ */
+ public function cursorDown(int $down = 1): self
+ {
+ $row = $this->getRow();
+ $col = $this->getColumn();
+ $newRow = max(
+ 1,
+ min($row + $down, AddressRange::MAX_ROW)
+ );
+
+ return $this->getWorksheet()->getCell("$col$newRow");
+ }
+
+ /**
+ * Return cell $up positions above this one.
+ */
+ public function cursorUp(int $up = 1): self
+ {
+ return $this->cursorDown(-$up);
+ }
+
+ /**
+ * Return cell at row $row in current column.
+ */
+ public function cursorRow(int $row = 1): self
+ {
+ $col = $this->getColumn();
+ $newRow = max(
+ 1,
+ min($row, AddressRange::MAX_ROW)
+ );
+
+ return $this->getWorksheet()->getCell("$col$newRow");
+ }
+
+ /**
+ * Return cell at column $column in current row.
+ */
+ public function cursorColumn(string $column = 'A'): self
+ {
+ $row = $this->getRow();
+ $colIndex = Coordinate::columnIndexFromString($column);
+ $newCol = max(
+ 1,
+ min($colIndex, AddressRange::MAX_COLUMN_INT)
+ );
+ $newColStr = Coordinate::stringFromColumnIndex($newCol);
+
+ return $this->getWorksheet()->getCell("$newColStr$row");
+ }
+
+ /**
+ * Return cell adjusted for Xls limits if applicable.
+ */
+ public function cursorXlsLimits(): self
+ {
+ $row = min($this->getRow(), AddressRange::MAX_ROW_XLS);
+ $column = $this->getColumn();
+ $colIndex = Coordinate::columnIndexFromString($column);
+ $newCol = min($colIndex, AddressRange::MAX_COLUMN_INT_XLS);
+ $newColStr = Coordinate::stringFromColumnIndex($newCol);
+
+ return $this->getWorksheet()->getCell("$newColStr$row");
+ }
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Coordinate.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Coordinate.php
index 227f2c1..02e1d9e 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Coordinate.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Coordinate.php
@@ -35,7 +35,11 @@ abstract class Coordinate
public static function coordinateFromString(string $cellAddress): array
{
if (preg_match(self::A1_COORDINATE_REGEX, $cellAddress, $matches)) {
- return [$matches['col'], $matches['row']];
+ $row = (int) ltrim($matches['row'], '$');
+ // reluctantly allow row 0 due to regression problems
+ if (/*$row > 0 &&*/ $row <= AddressRange::MAX_ROW) {
+ return [$matches['col'], $matches['row']];
+ }
} elseif (self::coordinateIsRange($cellAddress)) {
throw new Exception('Cell coordinate string can not be a range of cells');
} elseif ($cellAddress == '') {
@@ -186,7 +190,7 @@ abstract class Coordinate
/**
* Build range from coordinate strings.
*
- * @param mixed[] $range Array containing one or more arrays containing one or two coordinate strings
+ * @param array> $range Array containing one or more arrays containing one or two coordinate strings
*
* @return string String representation of $pRange
*/
@@ -206,6 +210,7 @@ abstract class Coordinate
$range[$i] = implode(':', $range[$i]);
}
+ /** @var array $range */
return implode(',', $range);
}
@@ -316,7 +321,7 @@ abstract class Coordinate
$worksheet = $matches['worksheet'];
if ($worksheet !== '') {
- if (substr($worksheet, 0, 1) === "'" && substr($worksheet, -1, 1) === "'") {
+ if (str_starts_with($worksheet, "'") && str_ends_with($worksheet, "'")) {
$worksheet = substr($worksheet, 1, -1);
}
$data['worksheet'] = strtolower($worksheet);
@@ -401,7 +406,7 @@ abstract class Coordinate
return $indexCache[$columnAddress];
}
// It's surprising how costly the strtoupper() and ord() calls actually are, so we use a lookup array
- // rather than use ord() and make it case insensitive to get rid of the strtoupper() as well.
+ // rather than use ord() and make it case-insensitive to get rid of the strtoupper() as well.
// Because it's a static, there's no significant memory overhead either.
/** @var array */
static $columnLookup = [
@@ -420,22 +425,28 @@ abstract class Coordinate
$indexCache[$columnAddress] = $columnLookup[$columnAddress];
return $indexCache[$columnAddress];
- } elseif (!isset($columnAddress[2])) {
+ }
+ if (!isset($columnAddress[2])) {
$indexCache[$columnAddress] = $columnLookup[$columnAddress[0]] * 26
+ $columnLookup[$columnAddress[1]];
return $indexCache[$columnAddress];
- } elseif (!isset($columnAddress[3])) {
- $indexCache[$columnAddress] = $columnLookup[$columnAddress[0]] * 676
+ }
+ if (!isset($columnAddress[3])) {
+ $temp = $columnLookup[$columnAddress[0]] * 676
+ $columnLookup[$columnAddress[1]] * 26
+ $columnLookup[$columnAddress[2]];
- return $indexCache[$columnAddress];
+ if ($temp <= AddressRange::MAX_COLUMN_INT) {
+ $indexCache[$columnAddress] = $temp;
+
+ return $temp;
+ }
}
}
throw new Exception(
- 'Column string index can not be ' . ((isset($columnAddress[0])) ? 'longer than 3 characters' : 'empty')
+ 'Column string index can not be ' . ((isset($columnAddress[0])) ? ('beyond ' . AddressRange::MAX_COLUMN) : 'empty')
);
}
@@ -446,11 +457,19 @@ abstract class Coordinate
*
* @param int|numeric-string $columnIndex Column index (A = 1)
*/
- public static function stringFromColumnIndex(int|string $columnIndex): string
+ public static function stringFromColumnIndex(int|string $columnIndex, bool $tolerateZero = false): string
{
/** @var string[] */
static $indexCache = [];
+ $columnIndex2 = (int) $columnIndex;
+ if ($columnIndex2 === 0 && $tolerateZero) {
+ return '';
+ }
+ if ($columnIndex2 < 1 || $columnIndex2 > AddressRange::MAX_COLUMN_INT) {
+ throw new Exception("Invalid column index $columnIndex");
+ }
+ $columnIndex = $columnIndex2;
if (!isset($indexCache[$columnIndex])) {
$indexValue = $columnIndex;
$base26 = '';
@@ -503,8 +522,7 @@ abstract class Coordinate
}
/** @var string[] */
- $cellList = array_merge(...$cells); //* @phpstan-ignore-line
- // Unsure how to satisfy phpstan in line above
+ $cellList = array_merge(...$cells); //* @phpstan-ignore argument.type (Unsure how to satisfy phpstan)
$retVal = array_map(
fn (string $cellAddress) => ($worksheet !== '') ? "{$quoted}{$worksheet}{$quoted}!{$cellAddress}" : $cellAddress,
@@ -516,7 +534,7 @@ abstract class Coordinate
/**
* @param mixed[] $operators
- * @param mixed[][] $cells
+ * @param string[][] $cells
*
* @return mixed[]
*/
@@ -554,7 +572,7 @@ abstract class Coordinate
$row = 0;
sscanf($coordinate, '%[A-Z]%d', $column, $row);
/** @var int $row */
- $key = (--$row * 16384) + self::columnIndexFromString((string) $column);
+ $key = (--$row * AddressRange::MAX_COLUMN_INT) + self::columnIndexFromString((string) $column);
$sortKeys[$key] = $coordinate;
}
ksort($sortKeys);
@@ -628,10 +646,8 @@ abstract class Coordinate
// Range...
[$rangeStart, $rangeEnd] = $range;
- [$startColumn, $startRow] = self::coordinateFromString($rangeStart);
- [$endColumn, $endRow] = self::coordinateFromString($rangeEnd);
- $startColumnIndex = self::columnIndexFromString($startColumn);
- $endColumnIndex = self::columnIndexFromString($endColumn);
+ [$startColumnIndex, $startRow, $startColumn] = self::indexesFromString($rangeStart);
+ [$endColumnIndex, $endRow, $endColumn] = self::indexesFromString($rangeEnd);
++$endColumnIndex;
// Current data
@@ -642,8 +658,6 @@ abstract class Coordinate
// Loop cells
while ($currentColumnIndex < $endColumnIndex) {
- /** @var int $currentRow */
- /** @var int $endRow */
while ($currentRow <= $endRow) {
$returnValue[] = self::stringFromColumnIndex($currentColumnIndex) . $currentRow;
++$currentRow;
@@ -671,7 +685,7 @@ abstract class Coordinate
*
* @param array $coordinateCollection associative array mapping coordinates to values
*
- * @return array associative array mapping coordinate ranges to valuea
+ * @return array associative array mapping coordinate ranges to values
*/
public static function mergeRangesInCollection(array $coordinateCollection): array
{
@@ -726,7 +740,7 @@ abstract class Coordinate
}
}
- if ($rowStart !== null) { // @phpstan-ignore-line
+ if ($rowStart !== null) {
if ($rowStart == $rowEnd) {
$ranges[] = $hashedValue->col . $rowStart;
} else {
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DataType.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DataType.php
index 774a57b..71cde4a 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DataType.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DataType.php
@@ -17,6 +17,7 @@ class DataType
const TYPE_INLINE = 'inlineStr';
const TYPE_ERROR = 'e';
const TYPE_ISO_DATE = 'd';
+ const TYPE_DRAWING_IN_CELL = 'drawingCell';
/**
* List of error codes.
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DefaultValueBinder.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DefaultValueBinder.php
index 42a45a9..49b2238 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DefaultValueBinder.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DefaultValueBinder.php
@@ -4,11 +4,12 @@ namespace PhpOffice\PhpSpreadsheet\Cell;
use Composer\Pcre\Preg;
use DateTimeInterface;
-use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
+use PhpOffice\PhpSpreadsheet\Calculation\CalculationParserOnly;
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
+use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
use Stringable;
class DefaultValueBinder implements IValueBinder
@@ -33,6 +34,11 @@ class DefaultValueBinder implements IValueBinder
$value = $value->format('Y-m-d H:i:s');
} elseif ($value instanceof Stringable) {
$value = (string) $value;
+ } elseif ($value instanceof BaseDrawing) {
+ $value->setCoordinates($cell->getCoordinate());
+ $value->setResizeProportional(false);
+ $value->setInCell(true);
+ $value->setWorksheet($cell->getWorksheet(), true);
} else {
throw new SpreadsheetException('Unable to bind unstringable ' . gettype($value));
}
@@ -68,6 +74,9 @@ class DefaultValueBinder implements IValueBinder
if ($value instanceof RichText) {
return DataType::TYPE_INLINE;
}
+ if ($value instanceof BaseDrawing) {
+ return DataType::TYPE_DRAWING_IN_CELL;
+ }
if ($value instanceof Stringable) {
$value = (string) $value;
}
@@ -77,8 +86,7 @@ class DefaultValueBinder implements IValueBinder
throw new SpreadsheetException("unusable type $gettype");
}
if (strlen($value) > 1 && $value[0] === '=') {
- $calculation = new Calculation();
- $calculation->disableBranchPruning();
+ $calculation = CalculationParserOnly::getParserInstance();
try {
if (empty($calculation->parseFormula($value))) {
@@ -107,7 +115,7 @@ class DefaultValueBinder implements IValueBinder
return DataType::TYPE_STRING;
}
}
- if (!is_numeric($value)) {
+ if (!is_numeric($value) || !is_finite((float) $value)) {
return DataType::TYPE_STRING;
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Hyperlink.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Hyperlink.php
index 5f1c521..2acc743 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Hyperlink.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Hyperlink.php
@@ -14,6 +14,8 @@ class Hyperlink
*/
private string $tooltip;
+ private string $display = '';
+
/**
* Create a new Hyperlink.
*
@@ -80,6 +82,23 @@ class Hyperlink
return $this->isInternal() ? '' : 'External';
}
+ public function getDisplay(): string
+ {
+ return $this->display;
+ }
+
+ /**
+ * This can be displayed in cell rather than actual cell contents.
+ * It seems to be ignored by Excel.
+ * It may be used by Google Sheets.
+ */
+ public function setDisplay(string $display): self
+ {
+ $this->display = $display;
+
+ return $this;
+ }
+
/**
* Get hash code.
*
@@ -89,7 +108,11 @@ class Hyperlink
{
return md5(
$this->url
+ . ','
. $this->tooltip
+ . ','
+ . $this->display
+ . ','
. __CLASS__
);
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/IgnoredErrors.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/IgnoredErrors.php
index 0d88fe3..37b4de4 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/IgnoredErrors.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/IgnoredErrors.php
@@ -14,6 +14,8 @@ class IgnoredErrors
private bool $evalError = false;
+ private bool $misleadingFormat = false;
+
public function setNumberStoredAsText(bool $value): self
{
$this->numberStoredAsText = $value;
@@ -73,4 +75,16 @@ class IgnoredErrors
{
return $this->evalError;
}
+
+ public function setMisleadingFormat(bool $value): self
+ {
+ $this->misleadingFormat = $value;
+
+ return $this;
+ }
+
+ public function getMisleadingFormat(): bool
+ {
+ return $this->misleadingFormat;
+ }
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Axis.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Axis.php
index dc1fca4..243c11f 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Axis.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Axis.php
@@ -336,7 +336,7 @@ class Axis extends Properties
{
parent::__clone();
$this->majorGridlines = ($this->majorGridlines === null) ? null : clone $this->majorGridlines;
- $this->majorGridlines = ($this->minorGridlines === null) ? null : clone $this->minorGridlines;
+ $this->minorGridlines = ($this->minorGridlines === null) ? null : clone $this->minorGridlines;
$this->axisText = ($this->axisText === null) ? null : clone $this->axisText;
$this->dispUnitsTitle = ($this->dispUnitsTitle === null) ? null : clone $this->dispUnitsTitle;
$this->fillColor = clone $this->fillColor;
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Chart.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Chart.php
index e750edc..6288ea1 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Chart.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Chart.php
@@ -110,6 +110,37 @@ class Chart
private bool $roundedCorners = false;
+ private bool $date1904 = false;
+
+ private string $lang = 'en-GB';
+
+ /** @var array{
+ * b?: numeric-string,
+ * l?: numeric-string,
+ * r?: numeric-string,
+ * t?: numeric-string,
+ * header?: numeric-string,
+ * footer?: numeric-string,
+ * }
+ */
+ private array $pageMargins = [
+ 'b' => '0.75',
+ 'l' => '0.7',
+ 'r' => '0.7',
+ 't' => '0.75',
+ 'header' => '0.3',
+ 'footer' => '0.3',
+ ];
+
+ /** @var array{
+ * paperSize?: string,
+ * orientation?: string,
+ * }
+ */
+ private array $pageSetup = [
+ 'orientation' => 'portrait',
+ ];
+
private GridLines $borderLines;
private ChartColor $fillColor;
@@ -680,9 +711,11 @@ class Chart
return $this->autoTitleDeleted;
}
- public function setAutoTitleDeleted(bool $autoTitleDeleted): self
+ public function setAutoTitleDeleted(?bool $autoTitleDeleted): self
{
- $this->autoTitleDeleted = $autoTitleDeleted;
+ if (is_bool($autoTitleDeleted)) {
+ $this->autoTitleDeleted = $autoTitleDeleted;
+ }
return $this;
}
@@ -725,6 +758,34 @@ class Chart
return $this;
}
+ public function getDate1904(): bool
+ {
+ return $this->date1904;
+ }
+
+ public function setDate1904(?bool $date1904): self
+ {
+ if ($date1904 !== null) {
+ $this->date1904 = $date1904;
+ }
+
+ return $this;
+ }
+
+ public function getLang(): string
+ {
+ return $this->lang;
+ }
+
+ public function setLang(?string $lang): self
+ {
+ if ($lang !== null && $lang !== '') {
+ $this->lang = $lang;
+ }
+
+ return $this;
+ }
+
public function getBorderLines(): GridLines
{
return $this->borderLines;
@@ -782,4 +843,58 @@ class Chart
$this->borderLines = clone $this->borderLines;
$this->fillColor = clone $this->fillColor;
}
+
+ /** @return array{
+ * b?: numeric-string,
+ * l?: numeric-string,
+ * r?: numeric-string,
+ * t?: numeric-string,
+ * header?: numeric-string,
+ * footer?: numeric-string,
+ * }
+ */
+ public function getPageMargins(): array
+ {
+ return $this->pageMargins;
+ }
+
+ /** @param mixed $pageMargins expecting array matching $this->pageMargins */
+ public function setPageMargins(mixed $pageMargins): self
+ {
+ if (is_array($pageMargins)) {
+ foreach (['b', 'l', 'r', 't', 'header', 'footer'] as $key) {
+ $value = $pageMargins[$key] ?? null;
+ if (is_string($value) && is_numeric($value)) {
+ $this->pageMargins[$key] = "$value";
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /** @return array{
+ * paperSize?: string,
+ * orientation?: string,
+ * }
+ */
+ public function getPageSetup(): array
+ {
+ return $this->pageSetup;
+ }
+
+ /** @param mixed $pageSetup expecting array matching $this->pageSetup */
+ public function setPageSetup(mixed $pageSetup): self
+ {
+ if (is_array($pageSetup)) {
+ foreach (['paperSize', 'orientation'] as $key) {
+ $value = $pageSetup[$key] ?? null;
+ if (is_string($value)) {
+ $this->pageSetup[$key] = "$value";
+ }
+ }
+ }
+
+ return $this;
+ }
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/ChartColor.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/ChartColor.php
index 6ac4b5a..a138163 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/ChartColor.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/ChartColor.php
@@ -114,7 +114,7 @@ class ChartColor
return $this;
}
- /** @param array{value: ?string, alpha: null|int|string, brightness?: null|int|string, type: ?string} $color */
+ /** @param array{value: ?string, alpha?: null|int|string, brightness?: null|int|string, type?: null|string} $color */
public function setColorPropertiesArray(array $color): self
{
return $this->setColorProperties(
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/DataSeries.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/DataSeries.php
index 06f1c3c..972a385 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/DataSeries.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/DataSeries.php
@@ -387,7 +387,7 @@ class DataSeries
$plotLabels = $this->plotLabel;
$this->plotLabel = [];
foreach ($plotLabels as $plotLabel) {
- $this->plotLabel[] = $plotLabel;
+ $this->plotLabel[] = clone $plotLabel;
}
$plotCategories = $this->plotCategory;
$this->plotCategory = [];
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/DataTable.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/DataTable.php
new file mode 100644
index 0000000..71c18fe
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/DataTable.php
@@ -0,0 +1,62 @@
+showHorizontalBorder;
+ }
+
+ public function getShowVerticalBorder(): bool
+ {
+ return $this->showVerticalBorder;
+ }
+
+ public function getShowOutline(): bool
+ {
+ return $this->showOutline;
+ }
+
+ public function getShowKeys(): bool
+ {
+ return $this->showKeys;
+ }
+
+ public function setShowHorizontalBorder(bool $showHorizontalBorder): self
+ {
+ $this->showHorizontalBorder = $showHorizontalBorder;
+
+ return $this;
+ }
+
+ public function setShowVerticalBorder(bool $showVerticalBorder): self
+ {
+ $this->showVerticalBorder = $showVerticalBorder;
+
+ return $this;
+ }
+
+ public function setShowOutline(bool $showOutline): self
+ {
+ $this->showOutline = $showOutline;
+
+ return $this;
+ }
+
+ public function setShowKeys(bool $showKeys): self
+ {
+ $this->showKeys = $showKeys;
+
+ return $this;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Layout.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Layout.php
index 95e113d..dbf9f75 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Layout.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Layout.php
@@ -99,6 +99,26 @@ class Layout
private ?Properties $labelEffects = null;
+ /** @var array{
+ * vertOverflow?: string,
+ * horzOverflow?: string,
+ * wrap?: string,
+ * lIns?: numeric-string,
+ * tIns?: numeric-string,
+ * rIns?: numeric-string,
+ * bIns?: numeric-string,
+ * anchor?: string,
+ * }
+ */
+ private array $bodyPr = [
+ 'wrap' => 'square',
+ 'lIns' => '38100',
+ 'tIns' => '19050',
+ 'rIns' => '38100',
+ 'bIns' => '19050',
+ 'anchor' => 'ctr',
+ ];
+
/**
* Create a new Layout.
*
@@ -106,7 +126,22 @@ class Layout
*/
public function __construct(array $layout = [])
{
- /** @var array{layoutTarget?: string, xMode?: string, yMode?: string, x?: float, y?: float, w?:float, h?:float, dLblPos?: string, labelFont?: ?mixed, labelFontColor?: ?mixed, labelEffects?: ?mixed, numFmtCode?: string} $layout */
+ /** @var array{
+ * layoutTarget?: string,
+ * xMode?: string,
+ * yMode?: string,
+ * x?: float,
+ * y?: float,
+ * w?:float,
+ * h?:float,
+ * dLblPos?: string,
+ * labelFont?: ?mixed,
+ * labelFontColor?: ?mixed,
+ * labelEffects?: ?mixed,
+ * numFmtCode?: string,
+ * bodyPr?: mixed,
+ * } $layout
+ */
if (isset($layout['layoutTarget'])) {
$this->layoutTarget = $layout['layoutTarget'];
}
@@ -156,6 +191,10 @@ class Layout
if ($labelEffects instanceof Properties) {
$this->labelEffects = $labelEffects;
}
+ $bodyPr = $layout['bodyPr'] ?? null;
+ if (is_array($bodyPr)) {
+ $this->setBodyPr($bodyPr);
+ }
}
/** @param mixed[] $layout */
@@ -469,11 +508,7 @@ class Layout
public function getLabelFontColor(): ?ChartColor
{
- if ($this->labelFont === null) {
- return null;
- }
-
- return $this->labelFont->getChartColor();
+ return $this->labelFont?->getChartColor();
}
public function setLabelFontColor(?ChartColor $chartColor): self
@@ -523,6 +558,45 @@ class Layout
return $this;
}
+ /** @return array{
+ * vertOverflow?: string,
+ * horzOverflow?: string,
+ * wrap?: string,
+ * lIns?: numeric-string,
+ * tIns?: numeric-string,
+ * rIns?: numeric-string,
+ * bIns?: numeric-string,
+ * anchor?: string,
+ * }
+ */
+ public function getBodyPr(): array
+ {
+ return $this->bodyPr;
+ }
+
+ /**
+ * @param mixed $bodyPr expect array matching $this->bodyPr
+ */
+ public function setBodyPr(mixed $bodyPr): self
+ {
+ if (is_array($bodyPr)) {
+ foreach (['vertOverflow', 'horzOverflow', 'wrap', 'anchor'] as $key) {
+ $value = $bodyPr[$key] ?? null;
+ if (is_string($value)) {
+ $this->bodyPr[$key] = "$value";
+ }
+ }
+ foreach (['lIns', 'tIns', 'rIns', 'bIns'] as $key) {
+ $value = $bodyPr[$key] ?? null;
+ if (is_string($value) && is_numeric($value)) {
+ $this->bodyPr[$key] = "$value";
+ }
+ }
+ }
+
+ return $this;
+ }
+
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Legend.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Legend.php
index 7736fb7..f10642d 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Legend.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Legend.php
@@ -66,7 +66,7 @@ class Legend
}
/**
- * Get legend position as an excel string value.
+ * Get legend position as an Excel string value.
*/
public function getPosition(): string
{
@@ -74,7 +74,7 @@ class Legend
}
/**
- * Get legend position using an excel string value.
+ * Get legend position using an Excel string value.
*
* @param string $position see self::POSITION_*
*/
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/PlotArea.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/PlotArea.php
index e5e3907..f23febc 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/PlotArea.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/PlotArea.php
@@ -38,6 +38,8 @@ class PlotArea
*/
private array $plotSeries;
+ private ?DataTable $dataTable = null;
+
/**
* Create a new PlotArea.
*
@@ -195,16 +197,34 @@ class PlotArea
return $this;
}
+ public function setDataTable(DataTable $dataTable): self
+ {
+ $this->dataTable = $dataTable;
+
+ return $this;
+ }
+
+ public function getDataTable(): ?DataTable
+ {
+ return $this->dataTable;
+ }
+
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
$this->layout = ($this->layout === null) ? null : clone $this->layout;
+ $this->dataTable = ($this->dataTable === null) ? null : clone $this->dataTable;
$plotSeries = $this->plotSeries;
$this->plotSeries = [];
foreach ($plotSeries as $series) {
$this->plotSeries[] = clone $series;
}
+ $gradientFillStops = $this->gradientFillStops;
+ $this->gradientFillStops = [];
+ foreach ($gradientFillStops as $gradientFillStop) {
+ $this->gradientFillStops[] = [$gradientFillStop[0], clone $gradientFillStop[1]];
+ }
}
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Properties.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Properties.php
index 700ccd1..9bf1599 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Properties.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Properties.php
@@ -99,9 +99,11 @@ abstract class Properties
const SHADOW_PRESETS_PERSPECTIVE_LOWER_RIGHT = 22;
const SHADOW_PRESETS_PERSPECTIVE_LOWER_LEFT = 23;
- const POINTS_WIDTH_MULTIPLIER = 12700;
- const ANGLE_MULTIPLIER = 60000; // direction and size-kx size-ky
- const PERCENTAGE_MULTIPLIER = 100000; // size sx and sy
+ const POINTS_WIDTH_MULTIPLIER = 12_700;
+ const ANGLE_MULTIPLIER = 60_000; // direction and size-kx size-ky
+ const PERCENTAGE_MULTIPLIER = 100_000; // size sx and sy, and gradient pos
+ const MAX_SKEW_ANGLE_XML = 90 * self::ANGLE_MULTIPLIER - 1;
+ const MAX_SKEW_ANGLE_DEGREES = self::MAX_SKEW_ANGLE_XML / self::ANGLE_MULTIPLIER; // max for size-kx size-ky
protected bool $objectState = false; // used only for minor gridlines
@@ -417,7 +419,7 @@ abstract class Properties
}
foreach ($elements as $keys) {
- $reference = &$reference[$keys]; //* @phpstan-ignore-line
+ $reference = &$reference[$keys]; //* @phpstan-ignore offsetAccess.nonOffsetAccessible (I'm not sure what this is doing)
}
return $reference;
@@ -519,7 +521,7 @@ abstract class Properties
return $this->softEdges['size'];
}
- /** @param null|array{value?: ?string, alpha?: null|int|string, brightness?: null|int|string, type?: ?string}|float|string $value */
+ /** @param null|array{value?: ?string, alpha?: null|int|string, brightness?: null|int|string, type?: ?string, sx?: ?float, sy?: ?float, ky?: ?float, kx?: ?float}|float|string $value */
public function setShadowProperty(string $propertyName, mixed $value): self
{
$this->activateObject();
@@ -853,7 +855,7 @@ abstract class Properties
public function setLineStyleProperty(string $propertyName, mixed $value): self
{
$this->activateObject();
- $this->lineStyleProperties[$propertyName] = $value; //* @phpstan-ignore-line
+ $this->lineStyleProperties[$propertyName] = $value; //* @phpstan-ignore assign.propertyType (Too complicated)
return $this;
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php
index faadd9a..ba30a35 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php
@@ -3,7 +3,7 @@
namespace PhpOffice\PhpSpreadsheet\Chart\Renderer;
/**
- * Jpgraph is not oficially maintained in Composer, so the version there
+ * Jpgraph is not officially maintained in Composer, so the version there
* could be out of date. For that reason, all unit test requiring Jpgraph
* are skipped. So, do not measure code coverage for this class till that
* is fixed.
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php
index 151d5b5..ce08ee0 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php
@@ -81,7 +81,7 @@ abstract class JpGraphRendererBase implements IRenderer
}
/**
- * This method should be overriden in descendants to do real JpGraph library initialization.
+ * This method should be overridden in descendants to do real JpGraph library initialization.
*/
abstract protected static function init(): void;
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Cells.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Cells.php
index 0661867..af1d5c8 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Cells.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Cells.php
@@ -2,6 +2,7 @@
namespace PhpOffice\PhpSpreadsheet\Collection;
+use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
@@ -11,7 +12,8 @@ use Psr\SimpleCache\CacheInterface;
class Cells
{
- protected const MAX_COLUMN_ID = 16384;
+ /** @deprecated 5.6.0 use AddressRange::MAX_COLUMN_INT */
+ protected const MAX_COLUMN_ID = AddressRange::MAX_COLUMN_INT;
private CacheInterface $cache;
@@ -242,9 +244,9 @@ class Cells
// Lookup highest column and highest row
$maxRow = $maxColumn = 1;
foreach ($this->index as $coordinate) {
- $row = (int) floor(($coordinate - 1) / self::MAX_COLUMN_ID) + 1;
+ $row = (int) floor(($coordinate - 1) / AddressRange::MAX_COLUMN_INT) + 1;
$maxRow = ($maxRow > $row) ? $maxRow : $row;
- $column = ($coordinate % self::MAX_COLUMN_ID) ?: self::MAX_COLUMN_ID;
+ $column = ($coordinate % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT;
$maxColumn = ($maxColumn > $column) ? $maxColumn : $column;
}
@@ -274,14 +276,14 @@ class Cells
}
$maxColumn = 1;
- $toRow = $row * self::MAX_COLUMN_ID;
- $fromRow = --$row * self::MAX_COLUMN_ID;
+ $toRow = $row * AddressRange::MAX_COLUMN_INT;
+ $fromRow = --$row * AddressRange::MAX_COLUMN_INT;
foreach ($this->index as $coordinate) {
if ($coordinate < $fromRow || $coordinate >= $toRow) {
continue;
}
- $column = ($coordinate % self::MAX_COLUMN_ID) ?: self::MAX_COLUMN_ID;
- $maxColumn = $maxColumn > $column ? $maxColumn : $column;
+ $column = ($coordinate % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT;
+ $maxColumn = max($column, $maxColumn);
}
return Coordinate::stringFromColumnIndex($maxColumn);
@@ -304,10 +306,10 @@ class Cells
$maxRow = 1;
$columnIndex = Coordinate::columnIndexFromString($column);
foreach ($this->index as $coordinate) {
- if ($coordinate % self::MAX_COLUMN_ID !== $columnIndex) {
+ if ($coordinate % AddressRange::MAX_COLUMN_INT !== $columnIndex) {
continue;
}
- $row = (int) floor($coordinate / self::MAX_COLUMN_ID) + 1;
+ $row = (int) floor($coordinate / AddressRange::MAX_COLUMN_INT) + 1;
$maxRow = ($maxRow > $row) ? $maxRow : $row;
}
@@ -371,12 +373,12 @@ class Cells
throw new PhpSpreadsheetException('Row number must be a positive integer');
}
- $toRow = $row * self::MAX_COLUMN_ID;
- $fromRow = --$row * self::MAX_COLUMN_ID;
+ $toRow = $row * AddressRange::MAX_COLUMN_INT;
+ $fromRow = --$row * AddressRange::MAX_COLUMN_INT;
foreach ($this->index as $coordinate) {
if ($coordinate >= $fromRow && $coordinate < $toRow) {
- $row = (int) floor($coordinate / self::MAX_COLUMN_ID) + 1;
- $column = Coordinate::stringFromColumnIndex($coordinate % self::MAX_COLUMN_ID);
+ $row = (int) floor($coordinate / AddressRange::MAX_COLUMN_INT) + 1;
+ $column = Coordinate::stringFromColumnIndex($coordinate % AddressRange::MAX_COLUMN_INT);
$this->delete("{$column}{$row}");
}
}
@@ -393,9 +395,9 @@ class Cells
$columnIndex = Coordinate::columnIndexFromString($column);
foreach ($this->index as $coordinate) {
- if ($coordinate % self::MAX_COLUMN_ID === $columnIndex) {
- $row = (int) floor($coordinate / self::MAX_COLUMN_ID) + 1;
- $column = Coordinate::stringFromColumnIndex($coordinate % self::MAX_COLUMN_ID);
+ if ($coordinate % AddressRange::MAX_COLUMN_INT === $columnIndex) {
+ $row = (int) floor($coordinate / AddressRange::MAX_COLUMN_INT) + 1;
+ $column = Coordinate::stringFromColumnIndex($coordinate % AddressRange::MAX_COLUMN_INT);
$this->delete("{$column}{$row}");
}
}
@@ -443,7 +445,7 @@ class Cells
$row = '';
sscanf($cellCoordinate, '%[A-Z]%d', $column, $row);
/** @var int $row */
- $this->index[$cellCoordinate] = (--$row * self::MAX_COLUMN_ID) + Coordinate::columnIndexFromString((string) $column);
+ $this->index[$cellCoordinate] = (--$row * AddressRange::MAX_COLUMN_INT) + Coordinate::columnIndexFromString((string) $column);
// Clear index sorted flag and index caches
$this->indexSorted = false;
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Downloader.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Downloader.php
index 0c39efe..7aa1494 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Downloader.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Downloader.php
@@ -2,6 +2,8 @@
namespace PhpOffice\PhpSpreadsheet\Helper;
+use DateTimeImmutable;
+use DateTimeZone;
use PhpOffice\PhpSpreadsheet\Exception;
/**
@@ -29,19 +31,16 @@ class Downloader
public function __construct(string $folder, string $filename, ?string $filetype = null)
{
- if ((is_dir($folder) === false) || (is_readable($folder) === false)) {
- throw new Exception('Folder is not accessible');
- }
- $filepath = "{$folder}/{$filename}";
- $this->filepath = (string) realpath($filepath);
- $this->filename = basename($filepath);
clearstatcache();
- if ((is_file($this->filepath) === false) || (is_readable($this->filepath) === false)) {
- throw new Exception('File not found, or not a regular file, or cannot be read');
+ $filepath = realpath("{$folder}/{$filename}");
+ if ($filepath === false || !is_file($filepath) || !is_readable($filepath)) {
+ throw new Exception('File not found, or cannot be read');
}
+ $this->filepath = $filepath;
+ $this->filename = basename($this->filepath);
- $filetype ??= pathinfo($filename, PATHINFO_EXTENSION);
- if (array_key_exists(strtolower($filetype), self::CONTENT_TYPES) === false) {
+ $filetype ??= pathinfo($this->filename, PATHINFO_EXTENSION);
+ if (!array_key_exists(strtolower($filetype), self::CONTENT_TYPES)) {
throw new Exception('Invalid filetype: file cannot be downloaded');
}
$this->filetype = strtolower($filetype);
@@ -90,7 +89,8 @@ class Downloader
// If you're serving to IE over SSL, then the following may be needed
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); // Date in the past
- header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); // always modified
+ $dt = new DateTimeImmutable(timezone: new DateTimeZone('UTC'));
+ header('Last-Modified: ' . $dt->format('D, d M Y H:i:s') . ' GMT'); // always modified
header('Cache-Control: cache, must-revalidate'); // HTTP/1.1
header('Pragma: public'); // HTTP/1.0
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Handler.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Handler.php
index fb1b9a0..514b7d7 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Handler.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Handler.php
@@ -2,13 +2,14 @@
namespace PhpOffice\PhpSpreadsheet\Helper;
-class Handler
+/** @internal */
+final class Handler
{
private static string $invalidHex = 'Y';
// A bunch of methods to show that we continue
// to capture messages even using PhpUnit 10.
- public static function suppressed(): bool
+ public static function suppressed(): true
{
return @trigger_error('hello');
}
@@ -28,17 +29,17 @@ class Handler
return file_get_contents(__FILE__ . 'noexist') !== false;
}
- public static function userDeprecated(): bool
+ public static function userDeprecated(): true
{
return trigger_error('hello', E_USER_DEPRECATED);
}
- public static function userNotice(): bool
+ public static function userNotice(): true
{
return trigger_error('userNotice', E_USER_NOTICE);
}
- public static function userWarning(): bool
+ public static function userWarning(): true
{
return trigger_error('userWarning', E_USER_WARNING);
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Sample.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Sample.php
index 729d5d7..8cdfe41 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Sample.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Sample.php
@@ -131,16 +131,21 @@ class Sample
$callStartTime = microtime(true);
$writer->save($path);
$this->logWrite($writer, $path, $callStartTime);
- if ($this->isCli() === false) {
- // @codeCoverageIgnoreStart
- echo 'Download ' . basename($path) . '
';
- // @codeCoverageIgnoreEnd
- }
+ $this->addDownloadLink($path);
}
$this->logEndingNotes();
}
+ public function addDownloadLink(string $path): void
+ {
+ if ($this->isCli() === false) {
+ // @codeCoverageIgnoreStart
+ echo 'Download ' . basename($path) . '
';
+ // @codeCoverageIgnoreEnd
+ }
+ }
+
protected function isDirOrMkdir(string $folder): bool
{
return \is_dir($folder) || \mkdir($folder);
@@ -242,9 +247,15 @@ class Sample
}
/** @param mixed[][] $matrix */
- public function displayGrid(array $matrix): void
+ public function displayGrid(array $matrix, null|bool|TextGridRightAlign $numbersRight = null): void
{
$renderer = new TextGrid($matrix, $this->isCli());
+ if (is_bool($numbersRight)) {
+ $numbersRight = $numbersRight ? TextGridRightAlign::numeric : TextGridRightAlign::none;
+ }
+ if ($numbersRight !== null) {
+ $renderer->setNumbersRight($numbersRight);
+ }
echo $renderer->render();
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/TextGrid.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/TextGrid.php
index 9c64c61..cd6fb2f 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/TextGrid.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/TextGrid.php
@@ -6,7 +6,7 @@ use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class TextGrid
{
- private bool $isCli;
+ protected bool $isCli;
/** @var mixed[][] */
protected array $matrix;
@@ -17,16 +17,18 @@ class TextGrid
/** @var string[] */
protected array $columns;
- private string $gridDisplay;
+ protected string $gridDisplay;
- private bool $rowDividers = false;
+ protected bool $rowDividers = false;
- private bool $rowHeaders = true;
+ protected bool $rowHeaders = true;
- private bool $columnHeaders = true;
+ protected bool $columnHeaders = true;
+
+ protected TextGridRightAlign $numbersRight = TextGridRightAlign::none;
/** @param mixed[][] $matrix */
- public function __construct(array $matrix, bool $isCli = true, bool $rowDividers = false, bool $rowHeaders = true, bool $columnHeaders = true)
+ public function __construct(array $matrix, bool $isCli = true, bool $rowDividers = false, bool $rowHeaders = true, bool $columnHeaders = true, TextGridRightAlign $numbersRight = TextGridRightAlign::none)
{
$this->rows = array_keys($matrix);
$this->columns = array_keys($matrix[$this->rows[0]]);
@@ -44,6 +46,12 @@ class TextGrid
$this->rowDividers = $rowDividers;
$this->rowHeaders = $rowHeaders;
$this->columnHeaders = $columnHeaders;
+ $this->numbersRight = $numbersRight;
+ }
+
+ public function setNumbersRight(TextGridRightAlign $numbersRight): void
+ {
+ $this->numbersRight = $numbersRight;
}
public function render(): string
@@ -52,7 +60,7 @@ class TextGrid
if (!empty($this->rows)) {
$maxRow = max($this->rows);
- $maxRowLength = strlen((string) $maxRow) + 1;
+ $maxRowLength = $this->strlen((string) $maxRow) + 1;
$columnWidths = $this->getColumnWidths();
$this->renderColumnHeader($maxRowLength, $columnWidths);
@@ -68,7 +76,7 @@ class TextGrid
}
/** @param int[] $columnWidths */
- private function renderRows(int $maxRowLength, array $columnWidths): void
+ protected function renderRows(int $maxRowLength, array $columnWidths): void
{
foreach ($this->matrix as $row => $rowData) {
if ($this->rowHeaders) {
@@ -86,18 +94,27 @@ class TextGrid
* @param mixed[] $rowData
* @param int[] $columnWidths
*/
- private function renderCells(array $rowData, array $columnWidths): void
+ protected function renderCells(array $rowData, array $columnWidths): void
{
foreach ($rowData as $column => $cell) {
$valueForLength = $this->getString($cell);
$displayCell = $this->isCli ? $valueForLength : htmlentities($valueForLength);
$this->gridDisplay .= '| ';
- $this->gridDisplay .= $displayCell . str_repeat(' ', $columnWidths[$column] - $this->strlen($valueForLength) + 1);
+ if ($this->rightAlign($displayCell, $cell)) {
+ $this->gridDisplay .= str_repeat(' ', $columnWidths[$column] - $this->strlen($valueForLength)) . $displayCell . ' ';
+ } else {
+ $this->gridDisplay .= $displayCell . str_repeat(' ', $columnWidths[$column] - $this->strlen($valueForLength) + 1);
+ }
}
}
+ protected function rightAlign(string $displayCell, mixed $cell = null): bool
+ {
+ return ($this->numbersRight === TextGridRightAlign::numeric && is_numeric($displayCell)) || ($this->numbersRight === TextGridRightAlign::floatOrInt && (is_int($cell) || is_float($cell)));
+ }
+
/** @param int[] $columnWidths */
- private function renderColumnHeader(int $maxRowLength, array &$columnWidths): void
+ protected function renderColumnHeader(int $maxRowLength, array &$columnWidths): void
{
if (!$this->columnHeaders) {
$this->renderFooter($maxRowLength, $columnWidths);
@@ -129,7 +146,7 @@ class TextGrid
}
/** @param int[] $columnWidths */
- private function renderFooter(int $maxRowLength, array $columnWidths): void
+ protected function renderFooter(int $maxRowLength, array $columnWidths): void
{
if ($this->rowHeaders) {
$this->gridDisplay .= '+' . str_repeat('-', $maxRowLength + 1);
@@ -142,7 +159,7 @@ class TextGrid
}
/** @return int[] */
- private function getColumnWidths(): array
+ protected function getColumnWidths(): array
{
$columnCount = count($this->matrix, COUNT_RECURSIVE) / count($this->matrix);
$columnWidths = [];
@@ -154,7 +171,7 @@ class TextGrid
}
/** @param mixed[] $columnData */
- private function getColumnWidth(array $columnData): int
+ protected function getColumnWidth(array $columnData): int
{
$columnWidth = 0;
$columnData = array_values($columnData);
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/TextGridRightAlign.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/TextGridRightAlign.php
new file mode 100644
index 0000000..2921e5c
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/TextGridRightAlign.php
@@ -0,0 +1,10 @@
+ CsvNoEscape::class];
+
public const WRITER_XLSX = 'Xlsx';
public const WRITER_XLS = 'Xls';
public const WRITER_ODS = 'Ods';
@@ -54,6 +57,31 @@ abstract class IOFactory
'Mpdf' => Writer\Pdf\Mpdf::class,
];
+ /** @internal */
+ public static function restoreDefaultReadersAndWriters(): void
+ {
+ self::$readers = [
+ self::READER_XLSX => Reader\Xlsx::class,
+ self::READER_XLS => Reader\Xls::class,
+ self::READER_XML => Reader\Xml::class,
+ self::READER_ODS => Reader\Ods::class,
+ self::READER_SLK => Reader\Slk::class,
+ self::READER_GNUMERIC => Reader\Gnumeric::class,
+ self::READER_HTML => Reader\Html::class,
+ self::READER_CSV => Reader\Csv::class,
+ ];
+ self::$writers = [
+ self::WRITER_XLS => Writer\Xls::class,
+ self::WRITER_XLSX => Writer\Xlsx::class,
+ self::WRITER_ODS => Writer\Ods::class,
+ self::WRITER_CSV => Writer\Csv::class,
+ self::WRITER_HTML => Writer\Html::class,
+ 'Tcpdf' => Writer\Pdf\Tcpdf::class,
+ 'Dompdf' => Writer\Pdf\Dompdf::class,
+ 'Mpdf' => Writer\Pdf\Mpdf::class,
+ ];
+ }
+
/**
* Create Writer\IWriter.
*/
@@ -75,18 +103,22 @@ abstract class IOFactory
/**
* Create IReader.
+ *
+ * @param array> $mergeArray
+ * Array to use to find reader, self::$readers will be used if $readers is empty.
*/
- public static function createReader(string $readerType): IReader
+ public static function createReader(string $readerType, array $mergeArray = []): IReader
{
/** @var class-string */
$className = $readerType;
- if (!in_array($readerType, self::$readers, true)) {
- if (!isset(self::$readers[$readerType])) {
+ $readers = empty($mergeArray) ? self::$readers : $mergeArray;
+ if (!in_array($readerType, $readers, true)) {
+ if (!isset($readers[$readerType])) {
throw new Reader\Exception("No reader found for type $readerType");
}
// Instantiate reader
- $className = self::$readers[$readerType];
+ $className = $readers[$readerType];
}
return new $className();
@@ -106,10 +138,11 @@ abstract class IOFactory
* list of Readers so it will only try the subset that you specify here.
* Values in this list can be any of the constant values defined in the set
* IOFactory::READER_*.
+ * @param array> $mergeArray
*/
- public static function load(string $filename, int $flags = 0, ?array $readers = null): Spreadsheet
+ public static function load(string $filename, int $flags = 0, ?array $readers = null, array $mergeArray = []): Spreadsheet
{
- $reader = self::createReaderForFile($filename, $readers);
+ $reader = self::createReaderForFile($filename, $readers, $mergeArray);
return $reader->load($filename, $flags);
}
@@ -118,10 +151,11 @@ abstract class IOFactory
* Identify file type using automatic IReader resolution.
*
* @param string[] $readers
+ * @param array> $mergeArray
*/
- public static function identify(string $filename, ?array $readers = null, bool $fullClassName = false): string
+ public static function identify(string $filename, ?array $readers = null, bool $fullClassName = false, array $mergeArray = []): string
{
- $reader = self::createReaderForFile($filename, $readers);
+ $reader = self::createReaderForFile($filename, $readers, $mergeArray);
$className = $reader::class;
if ($fullClassName) {
return $className;
@@ -139,12 +173,16 @@ abstract class IOFactory
* list of Readers so it will only try the subset that you specify here.
* Values in this list can be any of the constant values defined in the set
* IOFactory::READER_*.
+ * @param array> $mergeArray supplied readers will be merged with
+ * default readers, allowing specification of a partial list.
+ *
+ * @throws Reader\Exception
*/
- public static function createReaderForFile(string $filename, ?array $readers = null): IReader
+ public static function createReaderForFile(string $filename, ?array $readers = null, array $mergeArray = []): IReader
{
File::assertFile($filename);
- $testReaders = self::$readers;
+ $testReaders = array_merge(self::$readers, $mergeArray);
if ($readers !== null) {
$readers = array_map('strtoupper', $readers);
$testReaders = array_filter(
@@ -157,7 +195,7 @@ abstract class IOFactory
// First, lucky guess by inspecting file extension
$guessedReader = self::getReaderTypeFromExtension($filename);
if (($guessedReader !== null) && array_key_exists($guessedReader, $testReaders)) {
- $reader = self::createReader($guessedReader);
+ $reader = self::createReader($guessedReader, $testReaders);
// Let's see if we are lucky
if ($reader->canRead($filename)) {
@@ -170,7 +208,7 @@ abstract class IOFactory
foreach ($testReaders as $readerType => $class) {
// Ignore our original guess, we know that won't work
if ($readerType !== $guessedReader) {
- $reader = self::createReader($readerType);
+ $reader = self::createReader($readerType, $testReaders);
if ($reader->canRead($filename)) {
return $reader;
}
@@ -227,8 +265,7 @@ abstract class IOFactory
*/
public static function registerWriter(string $writerType, string $writerClass): void
{
- // We want phpstan to validate caller, but still need this test
- if (!is_a($writerClass, IWriter::class, true)) { //* @phpstan-ignore-line
+ if (!is_a($writerClass, IWriter::class, true)) {
throw new Writer\Exception('Registered writers must implement ' . IWriter::class);
}
@@ -242,11 +279,22 @@ abstract class IOFactory
*/
public static function registerReader(string $readerType, string $readerClass): void
{
- // We want phpstan to validate caller, but still need this test
- if (!is_a($readerClass, IReader::class, true)) { //* @phpstan-ignore-line
+ if (!is_a($readerClass, IReader::class, true)) {
throw new Reader\Exception('Registered readers must implement ' . IReader::class);
}
self::$readers[$readerType] = $readerClass;
}
+
+ /**
+ * @return array>
+ *
+ * @internal
+ *
+ * @codeCoverageIgnore
+ */
+ public static function getReaders(): array
+ {
+ return self::$readers;
+ }
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/BaseReader.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/BaseReader.php
index c4184bd..da18657 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/BaseReader.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/BaseReader.php
@@ -9,7 +9,7 @@ use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
-abstract class BaseReader implements IReader
+abstract class BaseReader implements IReader2
{
/**
* Read data only?
@@ -60,6 +60,14 @@ abstract class BaseReader implements IReader
*/
protected bool $createBlankSheetIfNoneRead = false;
+ /**
+ * Enable drawing pass-through?
+ * Identifies whether the Reader should preserve unsupported drawing elements (shapes, grouped images, etc.)
+ * by storing the original XML for pass-through during write operations.
+ * When enabled, drawings cannot be modified programmatically but are preserved exactly.
+ */
+ protected bool $enableDrawingPassThrough = false;
+
/**
* IReadFilter instance.
*/
@@ -72,6 +80,9 @@ abstract class BaseReader implements IReader
protected ?IValueBinder $valueBinder = null;
+ /** @var null|callable(string):bool function to return whether image path is okay */
+ protected $isWhitelisted;
+
public function __construct()
{
$this->readFilter = new DefaultReadFilter();
@@ -82,7 +93,7 @@ abstract class BaseReader implements IReader
return $this->readDataOnly;
}
- public function setReadDataOnly(bool $readCellValuesOnly): self
+ public function setReadDataOnly(bool $readCellValuesOnly): static
{
$this->readDataOnly = $readCellValuesOnly;
@@ -94,7 +105,7 @@ abstract class BaseReader implements IReader
return $this->readEmptyCells;
}
- public function setReadEmptyCells(bool $readEmptyCells): self
+ public function setReadEmptyCells(bool $readEmptyCells): static
{
$this->readEmptyCells = $readEmptyCells;
@@ -106,7 +117,7 @@ abstract class BaseReader implements IReader
return $this->ignoreRowsWithNoCells;
}
- public function setIgnoreRowsWithNoCells(bool $ignoreRowsWithNoCells): self
+ public function setIgnoreRowsWithNoCells(bool $ignoreRowsWithNoCells): static
{
$this->ignoreRowsWithNoCells = $ignoreRowsWithNoCells;
@@ -118,13 +129,25 @@ abstract class BaseReader implements IReader
return $this->includeCharts;
}
- public function setIncludeCharts(bool $includeCharts): self
+ public function setIncludeCharts(bool $includeCharts): static
{
$this->includeCharts = $includeCharts;
return $this;
}
+ public function getEnableDrawingPassThrough(): bool
+ {
+ return $this->enableDrawingPassThrough;
+ }
+
+ public function setEnableDrawingPassThrough(bool $enableDrawingPassThrough): static
+ {
+ $this->enableDrawingPassThrough = $enableDrawingPassThrough;
+
+ return $this;
+ }
+
/** @return null|string[] */
public function getLoadSheetsOnly(): ?array
{
@@ -132,7 +155,7 @@ abstract class BaseReader implements IReader
}
/** @param null|string|string[] $sheetList */
- public function setLoadSheetsOnly(string|array|null $sheetList): self
+ public function setLoadSheetsOnly(string|array|null $sheetList): static
{
if ($sheetList === null) {
return $this->setLoadAllSheets();
@@ -143,7 +166,7 @@ abstract class BaseReader implements IReader
return $this;
}
- public function setLoadAllSheets(): self
+ public function setLoadAllSheets(): static
{
$this->loadSheetsOnly = null;
@@ -155,7 +178,7 @@ abstract class BaseReader implements IReader
return $this->readFilter;
}
- public function setReadFilter(IReadFilter $readFilter): self
+ public function setReadFilter(IReadFilter $readFilter): static
{
$this->readFilter = $readFilter;
@@ -163,11 +186,12 @@ abstract class BaseReader implements IReader
}
/**
- * Allow external images. Use with caution.
- * Improper specification of these within a spreadsheet
- * can subject the caller to security exploits.
+ * USE WITH CAUTION (and in conjunction with setIsWhiteListed)!
+ * Allow external images;
+ * these can be specified within a spreadsheet
+ * in a way that can subject the caller to security exploits.
*/
- public function setAllowExternalImages(bool $allowExternalImages): self
+ public function setAllowExternalImages(bool $allowExternalImages): static
{
$this->allowExternalImages = $allowExternalImages;
@@ -179,11 +203,27 @@ abstract class BaseReader implements IReader
return $this->allowExternalImages;
}
+ /**
+ * USE WITH CAUTION!
+ * Supply a callback to determine whether a path should be whitelisted,
+ * used in conjunction with setAllowExternalImages;
+ * supplying a method which might return true
+ * can subject the caller to security exploits.
+ *
+ * @param callable(string):bool $isWhitelisted
+ */
+ public function setIsWhitelisted(callable $isWhitelisted): static
+ {
+ $this->isWhitelisted = $isWhitelisted;
+
+ return $this;
+ }
+
/**
* Create a blank sheet if none are read,
* possibly due to a typo when using LoadSheetsOnly.
*/
- public function setCreateBlankSheetIfNoneRead(bool $createBlankSheetIfNoneRead): self
+ public function setCreateBlankSheetIfNoneRead(bool $createBlankSheetIfNoneRead): static
{
$this->createBlankSheetIfNoneRead = $createBlankSheetIfNoneRead;
@@ -305,7 +345,7 @@ abstract class BaseReader implements IReader
return $this->valueBinder;
}
- public function setValueBinder(?IValueBinder $valueBinder): self
+ public function setValueBinder(?IValueBinder $valueBinder): static
{
$this->valueBinder = $valueBinder;
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv.php
index 55fe08a..e20b0fb 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv.php
@@ -34,47 +34,48 @@ class Csv extends BaseReader
/**
* Input encoding.
*/
- private string $inputEncoding = 'UTF-8';
+ protected string $inputEncoding = 'UTF-8';
/**
* Fallback encoding if guess strikes out.
*/
- private string $fallbackEncoding = self::DEFAULT_FALLBACK_ENCODING;
+ protected string $fallbackEncoding = self::DEFAULT_FALLBACK_ENCODING;
/**
* Delimiter.
*/
- private ?string $delimiter = null;
+ protected ?string $delimiter = null;
/**
* Enclosure.
*/
- private string $enclosure = '"';
+ protected string $enclosure = '"';
/**
* Sheet index to read.
*/
- private int $sheetIndex = 0;
+ protected int $sheetIndex = 0;
/**
* Load rows contiguously.
*/
- private bool $contiguous = false;
+ protected bool $contiguous = false;
/**
* The character that can escape the enclosure.
* This will probably become unsupported in Php 9.
* Not yet ready to mark deprecated in order to give users
* a migration path.
+ * Strongly encourage that this value be set to null-string.
*/
- private ?string $escapeCharacter = null;
+ protected ?string $escapeCharacter = null;
/**
* Callback for setting defaults in construction.
*
* @var ?callable
*/
- private static $constructorCallback;
+ protected static $constructorCallback;
/** Changed from true to false in release 4.0.0 */
public const DEFAULT_TEST_AUTODETECT = false;
@@ -82,23 +83,23 @@ class Csv extends BaseReader
/**
* Attempt autodetect line endings (deprecated after PHP8.1)?
*/
- private bool $testAutodetect = self::DEFAULT_TEST_AUTODETECT;
+ protected bool $testAutodetect = self::DEFAULT_TEST_AUTODETECT;
protected bool $castFormattedNumberToNumeric = false;
protected bool $preserveNumericFormatting = false;
- private bool $preserveNullString = false;
+ protected bool $preserveNullString = false;
- private bool $sheetNameIsFileName = false;
+ protected bool $sheetNameIsFileName = false;
- private string $getTrue = 'true';
+ protected ?string $getTrue = null;
- private string $getFalse = 'false';
+ protected ?string $getFalse = null;
- private string $thousandsSeparator = ',';
+ protected ?string $thousandsSeparator = null;
- private string $decimalSeparator = '.';
+ protected ?string $decimalSeparator = null;
/**
* Create a new CSV Reader instance.
@@ -106,7 +107,7 @@ class Csv extends BaseReader
public function __construct()
{
parent::__construct();
- $callback = self::$constructorCallback;
+ $callback = static::$constructorCallback;
if ($callback !== null) {
$callback($this);
}
@@ -120,12 +121,12 @@ class Csv extends BaseReader
*/
public static function setConstructorCallback(?callable $callback): void
{
- self::$constructorCallback = $callback;
+ static::$constructorCallback = $callback;
}
public static function getConstructorCallback(): ?callable
{
- return self::$constructorCallback;
+ return static::$constructorCallback;
}
public function setInputEncoding(string $encoding): self
@@ -248,7 +249,7 @@ class Csv extends BaseReader
$rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
}
- $worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1);
+ $worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1, true);
$worksheetInfo[0]['totalColumns'] = $worksheetInfo[0]['lastColumnIndex'] + 1;
$worksheetInfo[0]['sheetState'] = Worksheet::SHEETSTATE_VISIBLE;
@@ -300,18 +301,27 @@ class Csv extends BaseReader
}
$this->openFile($filename);
if ($this->inputEncoding !== 'UTF-8') {
- fclose($this->fileHandle);
- $entireFile = file_get_contents($filename);
- $fileHandle = fopen('php://memory', 'r+b');
- if ($fileHandle !== false && $entireFile !== false) {
- $this->fileHandle = $fileHandle;
- $data = StringHelper::convertEncoding($entireFile, 'UTF-8', $this->inputEncoding);
- fwrite($this->fileHandle, $data);
- $this->skipBOM();
- }
+ $this->convertNonUtf8($filename);
}
}
+ protected function convertNonUtf8(string $filename): void
+ {
+ fclose($this->fileHandle);
+ $entireFile = file_get_contents($filename);
+ if ($entireFile === false) {
+ throw new ReaderException("Unable to get contents of $filename"); // @codeCoverageIgnore
+ }
+ $fileHandle = fopen('php://memory', 'r+b');
+ if ($fileHandle === false) {
+ throw new ReaderException('Unable to open php://memory'); // @codeCoverageIgnore
+ }
+ $this->fileHandle = $fileHandle;
+ $data = StringHelper::convertEncoding($entireFile, 'UTF-8', $this->inputEncoding);
+ fwrite($this->fileHandle, $data);
+ $this->skipBOM();
+ }
+
public function setTestAutoDetect(bool $value): self
{
$this->testAutodetect = $value;
@@ -417,10 +427,10 @@ class Csv extends BaseReader
$rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
$valueBinder = $this->valueBinder ?? Cell::getValueBinder();
$preserveBooleanString = method_exists($valueBinder, 'getBooleanConversion') && $valueBinder->getBooleanConversion();
- $this->getTrue = Calculation::getTRUE();
- $this->getFalse = Calculation::getFALSE();
- $this->thousandsSeparator = StringHelper::getThousandsSeparator();
- $this->decimalSeparator = StringHelper::getDecimalSeparator();
+ $this->getTrue ??= Calculation::getTRUE();
+ $this->getFalse ??= Calculation::getFALSE();
+ $this->thousandsSeparator ??= StringHelper::getThousandsSeparator();
+ $this->decimalSeparator ??= StringHelper::getDecimalSeparator();
while (is_array($rowData)) {
$noOutputYet = true;
$columnLetter = 'A';
@@ -465,9 +475,9 @@ class Csv extends BaseReader
private function convertBoolean(mixed &$rowDatum): void
{
if (is_string($rowDatum)) {
- if (strcasecmp($this->getTrue, $rowDatum) === 0 || strcasecmp('true', $rowDatum) === 0) {
+ if (strcasecmp((string) $this->getTrue, $rowDatum) === 0 || strcasecmp('true', $rowDatum) === 0) {
$rowDatum = true;
- } elseif (strcasecmp($this->getFalse, $rowDatum) === 0 || strcasecmp('false', $rowDatum) === 0) {
+ } elseif (strcasecmp((string) $this->getFalse, $rowDatum) === 0 || strcasecmp('false', $rowDatum) === 0) {
$rowDatum = false;
}
} else {
@@ -483,15 +493,15 @@ class Csv extends BaseReader
$numberFormatMask = '';
if ($this->castFormattedNumberToNumeric === true && is_string($rowDatum)) {
$numeric = str_replace(
- [$this->thousandsSeparator, $this->decimalSeparator],
+ [(string) $this->thousandsSeparator, (string) $this->decimalSeparator],
['', '.'],
$rowDatum
);
if (is_numeric($numeric)) {
- $decimalPos = strpos($rowDatum, $this->decimalSeparator);
+ $decimalPos = strpos($rowDatum, (string) $this->decimalSeparator);
if ($this->preserveNumericFormatting === true) {
- $numberFormatMask = (str_contains($rowDatum, $this->thousandsSeparator))
+ $numberFormatMask = (str_contains($rowDatum, (string) $this->thousandsSeparator))
? '#,##0' : '0';
if ($decimalPos !== false) {
$decimals = strlen($rowDatum) - $decimalPos - 1;
@@ -759,4 +769,32 @@ class Csv extends BaseReader
{
return $version < 90000 ? '\\' : '';
}
+
+ public function setGetTrue(?string $getTrue): self
+ {
+ $this->getTrue = $getTrue;
+
+ return $this;
+ }
+
+ public function setGetFalse(?string $getFalse): self
+ {
+ $this->getFalse = $getFalse;
+
+ return $this;
+ }
+
+ public function setDecimalSeparator(?string $decimalSeparator): self
+ {
+ $this->decimalSeparator = $decimalSeparator;
+
+ return $this;
+ }
+
+ public function setThousandsSeparator(?string $thousandsSeparator): self
+ {
+ $this->thousandsSeparator = $thousandsSeparator;
+
+ return $this;
+ }
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv/Delimiter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv/Delimiter.php
index 05b898f..a3370e0 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv/Delimiter.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv/Delimiter.php
@@ -4,7 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Csv;
class Delimiter
{
- protected const POTENTIAL_DELIMETERS = [',', ';', "\t", '|', ':', ' ', '~'];
+ protected const POTENTIAL_DELIMITERS = [',', ';', "\t", '|', ':', ' ', '~'];
/** @var resource */
protected $fileHandle;
@@ -34,7 +34,7 @@ class Delimiter
public function getDefaultDelimiter(): string
{
- return self::POTENTIAL_DELIMETERS[0];
+ return self::POTENTIAL_DELIMITERS[0];
}
public function linesCounted(): int
@@ -44,8 +44,8 @@ class Delimiter
protected function countPotentialDelimiters(): void
{
- $this->counts = array_fill_keys(self::POTENTIAL_DELIMETERS, []);
- $delimiterKeys = array_flip(self::POTENTIAL_DELIMETERS);
+ $this->counts = array_fill_keys(self::POTENTIAL_DELIMITERS, []);
+ $delimiterKeys = array_flip(self::POTENTIAL_DELIMITERS);
// Count how many times each of the potential delimiters appears in each line
$this->numberLines = 0;
@@ -61,7 +61,7 @@ class Delimiter
$distribution = array_count_values($splitString);
$countLine = array_intersect_key($distribution, $delimiterKeys);
- foreach (self::POTENTIAL_DELIMETERS as $delimiter) {
+ foreach (self::POTENTIAL_DELIMITERS as $delimiter) {
$this->counts[$delimiter][] = $countLine[$delimiter] ?? 0;
}
}
@@ -73,7 +73,7 @@ class Delimiter
$meanSquareDeviations = [];
$middleIdx = (int) floor(($this->numberLines - 1) / 2);
- foreach (self::POTENTIAL_DELIMETERS as $delimiter) {
+ foreach (self::POTENTIAL_DELIMITERS as $delimiter) {
$series = $this->counts[$delimiter];
sort($series);
@@ -94,7 +94,7 @@ class Delimiter
// ... and pick the delimiter with the smallest mean square deviation
// (in case of ties, the order in potentialDelimiters is respected)
$min = INF;
- foreach (self::POTENTIAL_DELIMETERS as $delimiter) {
+ foreach (self::POTENTIAL_DELIMITERS as $delimiter) {
if (!isset($meanSquareDeviations[$delimiter])) {
continue;
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/CsvNoEscape.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/CsvNoEscape.php
new file mode 100644
index 0000000..9eea2b0
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/CsvNoEscape.php
@@ -0,0 +1,51 @@
+escapeCharacter = $escapeCharacter;
+
+ return $this;
+ }
+
+ /**
+ * Changing auto-detect not allowed in this class.
+ */
+ public function setTestAutoDetect(bool $value): self
+ {
+ if ($value !== false) {
+ throw new ReaderException('This class requires that testAutoDetect be false');
+ }
+ $this->testAutodetect = $value;
+
+ return $this;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/EmptyWorksheetInfo.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/EmptyWorksheetInfo.php
new file mode 100644
index 0000000..ec5718b
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/EmptyWorksheetInfo.php
@@ -0,0 +1,31 @@
+
+ */
+ public function listWorksheetInfo(string $filename): array
+ {
+ return [];
+ }
+
+ /**
+ * Return empty array as names of the worksheets from a file,
+ * possibly without parsing the whole file to a Spreadsheet object.
+ *
+ * @return string[]
+ */
+ public function listWorksheetNames(string $filename): array
+ {
+ return [];
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric.php
index 5e68360..67cbba9 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric.php
@@ -50,8 +50,14 @@ class Gnumeric extends BaseReader
private ReferenceHelper $referenceHelper;
- /** @var array{'dataType': string[]} */
- public static array $mappings = [
+ /**
+ * @deprecated 5.10.0 No longer used, replaced by const which is not user-accessible.
+ *
+ * @var array{'dataType': string[]}
+ */
+ public static array $mappings = self::MAPPINGS;
+
+ private const MAPPINGS = [
'dataType' => [
'10' => DataType::TYPE_NULL,
'20' => DataType::TYPE_BOOL,
@@ -64,6 +70,14 @@ class Gnumeric extends BaseReader
],
];
+ protected int $maxLength;
+
+ private const LENGTH_MULTIPLIER = [
+ 'G' => 1024 * 1024 * 1024,
+ 'M' => 1024 * 1024,
+ 'K' => 1024,
+ ];
+
/**
* Create a new Gnumeric.
*/
@@ -72,6 +86,20 @@ class Gnumeric extends BaseReader
parent::__construct();
$this->referenceHelper = ReferenceHelper::getInstance();
$this->securityScanner = XmlScanner::getInstance($this);
+ $limit = ini_get('memory_limit') ?: '128M';
+ $limit = trim(str_replace('-1', '128M', $limit));
+ $unit = strtoupper(substr($limit, -1));
+ $limit = (int) $limit;
+ $multiplier = self::LENGTH_MULTIPLIER[$unit] ?? 1;
+ $limit *= $multiplier;
+ $this->maxLength = intdiv($limit, 4);
+ }
+
+ public function setMaxLength(int $maxLength): self
+ {
+ $this->maxLength = $maxLength;
+
+ return $this;
}
/**
@@ -176,7 +204,7 @@ class Gnumeric extends BaseReader
break;
}
}
- $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
+ $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1, true);
$worksheetInfo[] = $tmpInfo;
}
}
@@ -192,7 +220,7 @@ class Gnumeric extends BaseReader
if (str_starts_with($contents, "\x1f\x8b")) {
// Check if gzlib functions are available
if (function_exists('gzdecode')) {
- $contents = @gzdecode($contents);
+ $contents = @gzdecode($contents, $this->maxLength);
if ($contents !== false) {
$data = $contents;
}
@@ -208,10 +236,14 @@ class Gnumeric extends BaseReader
return $data;
}
- /** @return mixed[] */
+ /**
+ * @return mixed[]
+ *
+ * @internal
+ */
public static function gnumericMappings(): array
{
- return array_merge(self::$mappings, Styles::$mappings);
+ return array_merge(self::MAPPINGS, Styles::MAPPINGS);
}
private function processComments(SimpleXMLElement $sheet): void
@@ -260,8 +292,7 @@ class Gnumeric extends BaseReader
$gFileData = $this->gzfileGetContents($filename);
- /** @var XmlScanner */
- $securityScanner = $this->securityScanner;
+ $securityScanner = $this->getSecurityScannerOrThrow();
$xml2 = simplexml_load_string($securityScanner->scan($gFileData));
$xml = self::testSimpleXml($xml2);
@@ -311,7 +342,7 @@ class Gnumeric extends BaseReader
$column = Coordinate::stringFromColumnIndex($column + 1);
// Read cell?
- if (!$this->getReadFilter()->readCell($column, $row, $worksheetName)) {
+ if (!$this->readFilter->readCell($column, $row, $worksheetName)) {
continue;
}
@@ -589,8 +620,8 @@ class Gnumeric extends BaseReader
$type = DataType::TYPE_FORMULA;
} elseif ($isArrayFormula === false) {
$vtype = (string) $ValueType;
- if (array_key_exists($vtype, self::$mappings['dataType'])) {
- $type = self::$mappings['dataType'][$vtype];
+ if (array_key_exists($vtype, self::MAPPINGS['dataType'])) {
+ $type = self::MAPPINGS['dataType'][$vtype];
}
if ($vtype === '20') { // Boolean
$cell = $cell == 'TRUE';
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php
index e947ae2..07f8a70 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php
@@ -18,8 +18,15 @@ class Styles
protected bool $readDataOnly;
- /** @var array */
- public static array $mappings = [
+ /**
+ * @deprecated 5.10.0 No longer used, replaced by const which is not user-accessible.
+ *
+ * @var array
+ */
+ public static array $mappings = self::MAPPINGS;
+
+ /** @internal */
+ public const MAPPINGS = [
'borderStyle' => [
'0' => Border::BORDER_NONE,
'1' => Border::BORDER_THIN,
@@ -47,7 +54,7 @@ class Styles
'8' => Fill::FILL_PATTERN_DARKVERTICAL, // vertical stripe
'9' => Fill::FILL_PATTERN_DARKDOWN, // diagonal stripe
'10' => Fill::FILL_PATTERN_DARKUP, // reverse diagonal stripe
- '11' => Fill::FILL_PATTERN_DARKGRID, // diagoanl crosshatch
+ '11' => Fill::FILL_PATTERN_DARKGRID, // diagonal crosshatch
'12' => Fill::FILL_PATTERN_DARKTRELLIS, // thick diagonal crosshatch
'13' => Fill::FILL_PATTERN_LIGHTHORIZONTAL,
'14' => Fill::FILL_PATTERN_LIGHTVERTICAL,
@@ -143,6 +150,7 @@ class Styles
{
$ucDirection = ucfirst($direction);
if (isset($srssb->$ucDirection)) {
+ // https://github.com/phpstan/phpstan/issues/14925
/** @var SimpleXMLElement */
$temp = $srssb->$ucDirection;
$styleArray['borders'][$direction] = self::parseBorderAttributes($temp->attributes());
@@ -160,19 +168,19 @@ class Styles
return $rotation;
}
- /** @param mixed[][] $styleArray */
+ /** @param array|string> $styleArray */
private static function addStyle(array &$styleArray, string $key, string $value): void
{
- if (array_key_exists($value, self::$mappings[$key])) {
- $styleArray[$key] = self::$mappings[$key][$value]; //* @phpstan-ignore-line
+ if (array_key_exists($value, self::MAPPINGS[$key])) {
+ $styleArray[$key] = self::MAPPINGS[$key][$value];
}
}
/** @param mixed[][] $styleArray */
private static function addStyle2(array &$styleArray, string $key1, string $key, string $value): void
{
- if (array_key_exists($value, self::$mappings[$key])) {
- $styleArray[$key1][$key] = self::$mappings[$key][$value];
+ if (array_key_exists($value, self::MAPPINGS[$key])) {
+ $styleArray[$key1][$key] = self::MAPPINGS[$key][$value];
}
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Html.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Html.php
index 579f389..5135ba7 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Html.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Html.php
@@ -2,11 +2,13 @@
namespace PhpOffice\PhpSpreadsheet\Reader;
+use Composer\Pcre\Preg;
use DOMAttr;
use DOMDocument;
use DOMElement;
use DOMNode;
use DOMText;
+use LibXMLError;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
@@ -132,6 +134,32 @@ class Html extends BaseReader
/** @var array */
protected array $rowspan = [];
+ /**
+ * Default setting uses current setting of libxml_use_internal_errors.
+ * It will probably change to 'true' in a future release.
+ */
+ protected ?bool $suppressLoadWarnings = null;
+
+ /** @var LibXMLError[] */
+ protected array $libxmlMessages = [];
+
+ /**
+ * Suppress load warning messages, keeping them available
+ * in $this->libxmlMessages().
+ */
+ public function setSuppressLoadWarnings(?bool $suppressLoadWarnings): self
+ {
+ $this->suppressLoadWarnings = $suppressLoadWarnings;
+
+ return $this;
+ }
+
+ /** @return LibXMLError[] */
+ public function getLibxmlMessages(): array
+ {
+ return $this->libxmlMessages;
+ }
+
/**
* Create a new HTML Reader instance.
*/
@@ -153,7 +181,7 @@ class Html extends BaseReader
return false;
}
- $beginning = preg_replace(self::STARTS_WITH_BOM, '', $this->readBeginning()) ?? '';
+ $beginning = Preg::replace(self::STARTS_WITH_BOM, '', $this->readBeginning());
$startWithTag = self::startsWithTag($beginning);
$containsTags = self::containsTags($beginning);
@@ -175,7 +203,7 @@ class Html extends BaseReader
{
$meta = stream_get_meta_data($this->fileHandle);
// Phpstan incorrectly flags following line for Php8.2-, corrected in 8.3
- $filename = $meta['uri']; //@phpstan-ignore-line
+ $filename = $meta['uri']; //@phpstan-ignore offsetAccess.notFound (fixable when Php < 8.3 is no longer possible)
clearstatcache(true, $filename);
$size = (int) filesize($filename);
@@ -224,6 +252,8 @@ class Html extends BaseReader
* Data Array used for testing only, should write to
* Spreadsheet object on completion of tests.
*
+ * @deprecated 5.4.0 No replacement.
+ *
* @var mixed[][]
*/
protected array $dataArray = [];
@@ -261,10 +291,11 @@ class Html extends BaseReader
*
* @param string[] $attributeArray
*
- * @param-out string $cellContent In one case, it can be bool
+ * @param-out string $cellContentx
*/
- protected function flushCell(Worksheet $sheet, string $column, int|string $row, mixed &$cellContent, array $attributeArray): void
+ protected function flushCell(Worksheet $sheet, string $column, int|string $row, mixed &$cellContentx, array $attributeArray): void
{
+ $cellContent = $cellContentx;
if (is_string($cellContent)) {
// Simple String content
if (trim($cellContent) > '') {
@@ -273,6 +304,10 @@ class Html extends BaseReader
// ... we return the cell, so we can mess about with styles more easily
// Set cell value explicitly if there is data-type attribute
+ if (isset($attributeArray['data-checkbox'])) {
+ $sheet->getStyle($column . $row)
+ ->setCheckBox(true);
+ }
if (isset($attributeArray['data-type'])) {
$datatype = $attributeArray['data-type'];
if (in_array($datatype, [DataType::TYPE_STRING, DataType::TYPE_STRING2, DataType::TYPE_INLINE])) {
@@ -285,29 +320,65 @@ class Html extends BaseReader
}
if ($datatype === DataType::TYPE_BOOL) {
// This is the case where we can set cellContent to bool rather than string
- $cellContent = self::convertBoolean($cellContent); //* @phpstan-ignore-line
- if (!is_bool($cellContent)) {
- $attributeArray['data-type'] = DataType::TYPE_STRING;
+ if ($cellContent === '☑') {
+ $cellContent = true;
+ $sheet->getStyle($column . $row)
+ ->setCheckBox(true);
+ } elseif ($cellContent === '☐') {
+ $cellContent = false;
+ $sheet->getStyle($column . $row)
+ ->setCheckBox(true);
+ } else {
+ $cellContent = self::convertBoolean($cellContent);
+ if (!is_bool($cellContent)) {
+ $attributeArray['data-type'] = DataType::TYPE_STRING;
+ }
}
}
//catching the Exception and ignoring the invalid data types
+ $hyperlink = $sheet->hyperlinkExists($column . $row) ? $sheet->getHyperlink($column . $row) : null;
+
try {
- $sheet->setCellValueExplicit($column . $row, $cellContent, $attributeArray['data-type']);
+ if (isset($attributeArray['data-formula'])) {
+ $sheet->setCellValueExplicit(
+ $column . $row,
+ $attributeArray['data-formula'],
+ DataType::TYPE_FORMULA
+ );
+ $sheet->getCell($column . $row)
+ ->setCalculatedValue(
+ $cellContent
+ );
+ } else {
+ $sheet->setCellValueExplicit(
+ $column . $row,
+ $attributeArray['data-value'] ?? $cellContent,
+ $attributeArray['data-type']
+ );
+ }
} catch (SpreadsheetException) {
$sheet->setCellValue($column . $row, $cellContent);
}
+ $sheet->setHyperlink($column . $row, $hyperlink);
} else {
- $sheet->setCellValue($column . $row, $cellContent);
+ $hyperlink = null;
+ if ($sheet->hyperlinkExists($column . $row)) {
+ $hyperlink = $sheet->getHyperlink($column . $row);
+ }
+ $sheet->setCellValue($column . $row, $attributeArray['data-value'] ?? $cellContent);
+ $sheet->setHyperlink($column . $row, $hyperlink);
}
- $this->dataArray[$row][$column] = $cellContent;
+ $this->dataArray[$row][$column] = $cellContent; // @phpstan-ignore property.deprecated (fixable when $this.data is removed)
}
} else {
- // We have a Rich Text run
+ // We have a Rich Text run.
+ // I don't actually see any way to reach this line.
// TODO
- $this->dataArray[$row][$column] = 'RICH TEXT: ' . StringHelper::convertToString($cellContent);
+ // @phpstan-ignore property.deprecated (fixable when $this->data is removed)
+ $this->dataArray[$row][$column] = 'RICH TEXT: ' . StringHelper::convertToString($cellContent); // @codeCoverageIgnore
}
- $cellContent = (string) '';
+ $cellContentx = '';
}
/** @var array> */
@@ -387,7 +458,7 @@ class Html extends BaseReader
}
if (isset($attributeArray['style'])) {
$alignStyle = $attributeArray['style'];
- if (preg_match('/\btext-align:\s*(left|right|center|justify)\b/', (string) $alignStyle, $matches) === 1) {
+ if (Preg::isMatch('/\btext-align:\s*(left|right|center|justify)\b/', (string) $alignStyle, $matches)) {
$sheet->getComment($column . $row)->setAlignment($matches[1]);
}
}
@@ -533,6 +604,10 @@ class Html extends BaseReader
$sheet->setShowGridlines(in_array('gridlines', $classes, true));
$sheet->setPrintGridlines(in_array('gridlinesp', $classes, true));
}
+ if (isset($attributeArray['data-printarea'])) {
+ $sheet->getPageSetup()
+ ->setPrintArea($attributeArray['data-printarea']);
+ }
if ('rtl' === ($attributeArray['dir'] ?? '')) {
$sheet->setRightToLeft(true);
}
@@ -698,7 +773,7 @@ class Html extends BaseReader
{
foreach ($element->childNodes as $child) {
if ($child instanceof DOMText) {
- $domText = (string) preg_replace('/\s+/', ' ', trim($child->nodeValue ?? ''));
+ $domText = Preg::replace('/\s+/', ' ', trim($child->nodeValue ?? ''));
if ($domText === "\u{a0}") {
$domText = '';
}
@@ -726,12 +801,23 @@ class Html extends BaseReader
$dom = new DOMDocument();
// Reload the HTML file into the DOM object
+ if (is_bool($this->suppressLoadWarnings)) {
+ $useErrors = libxml_use_internal_errors($this->suppressLoadWarnings);
+ } else {
+ $useErrors = null;
+ }
+
try {
$convert = $this->getSecurityScannerOrThrow()->scanFile($filename);
$convert = static::replaceNonAsciiIfNeeded($convert);
$loaded = ($convert === null) ? false : $dom->loadHTML($convert);
} catch (Throwable $e) {
$loaded = false;
+ } finally {
+ $this->libxmlMessages = libxml_get_errors();
+ if (is_bool($useErrors)) {
+ libxml_use_internal_errors($useErrors);
+ }
}
if ($loaded === false) {
throw new Exception('Failed to load file ' . $filename . ' as a DOM Document', 0, $e ?? null);
@@ -798,7 +884,7 @@ class Html extends BaseReader
break;
default:
- if (preg_match('/^custom[.](bool|date|float|int|string)[.](.+)$/', $metaName, $matches) === 1) {
+ if (Preg::isMatch('/^custom[.](bool|date|float|int|string)[.](.+)$/', $metaName, $matches)) {
match ($matches[1]) {
'bool' => $properties->setCustomProperty($matches[2], (bool) $metaContent, Properties::PROPERTY_TYPE_BOOLEAN),
'float' => $properties->setCustomProperty($matches[2], (float) $metaContent, Properties::PROPERTY_TYPE_FLOAT),
@@ -822,15 +908,15 @@ class Html extends BaseReader
return '' . mb_ord($matches[0], 'UTF-8') . ';';
}
+ /** @internal */
protected static function replaceNonAsciiIfNeeded(string $convert): ?string
{
- if (preg_match(self::STARTS_WITH_BOM, $convert) !== 1 && preg_match(self::DECLARES_CHARSET, $convert) !== 1) {
+ if (!Preg::isMatch(self::STARTS_WITH_BOM, $convert) && !Preg::isMatch(self::DECLARES_CHARSET, $convert)) {
$lowend = "\u{80}";
$highend = "\u{10ffff}";
$regexp = "/[$lowend-$highend]/u";
- /** @var callable $callback */
- $callback = [self::class, 'replaceNonAscii'];
- $convert = preg_replace_callback($regexp, $callback, $convert);
+ // use native preg because of "u" modifier
+ $convert = preg_replace_callback($regexp, self::replaceNonAscii(...), $convert);
}
return $convert;
@@ -845,12 +931,23 @@ class Html extends BaseReader
$dom = new DOMDocument();
// Reload the HTML file into the DOM object
+ if (is_bool($this->suppressLoadWarnings)) {
+ $useErrors = libxml_use_internal_errors($this->suppressLoadWarnings);
+ } else {
+ $useErrors = null;
+ }
+
try {
$convert = $this->getSecurityScannerOrThrow()->scan($content);
$convert = static::replaceNonAsciiIfNeeded($convert);
$loaded = ($convert === null) ? false : $dom->loadHTML($convert);
} catch (Throwable $e) {
$loaded = false;
+ } finally {
+ $this->libxmlMessages = libxml_get_errors();
+ if (is_bool($useErrors)) {
+ libxml_use_internal_errors($useErrors);
+ }
}
if ($loaded === false) {
throw new Exception('Failed to load content as a DOM Document', 0, $e ?? null);
@@ -915,7 +1012,7 @@ class Html extends BaseReader
* and only takes 'background-color' and 'color'; property with HEX color
*
* TODO :
- * - Implement to other propertie, such as border
+ * - Implement to other properties, such as border
*
* @param string[] $attributeArray
*/
@@ -1129,7 +1226,7 @@ class Html extends BaseReader
$styleArray = self::getStyleArray($attributes);
$src = $attributes['src'];
- if (substr($src, 0, 5) !== 'data:') {
+ if (!str_starts_with($src, 'data:')) {
$src = urldecode($src);
}
$width = isset($attributes['width']) ? (float) $attributes['width'] : ($styleArray['width'] ?? null);
@@ -1137,7 +1234,7 @@ class Html extends BaseReader
$name = $attributes['alt'] ?? null;
$drawing = new Drawing();
- $drawing->setPath($src, false, allowExternal: $this->allowExternalImages);
+ $drawing->setPath($src, false, allowExternal: $this->allowExternalImages, isWhitelisted: $this->isWhitelisted);
if ($drawing->getPath() === '') {
return;
}
@@ -1177,6 +1274,11 @@ class Html extends BaseReader
$drawing->setOpacity((int) ($opacity * 100000));
}
}
+ /** @var string */
+ $transform = $styleArray['transform'] ?? '';
+ if (Preg::isMatch('/rotate[(](-?\d{1,3})deg[)]$/', $transform, $matches)) {
+ $drawing->setRotation((int) $matches[1]);
+ }
}
/**
@@ -1195,13 +1297,13 @@ class Html extends BaseReader
$arrayKey = trim($value[0]);
$arrayValue = trim($value[1]);
if ($arrayKey === 'width') {
- if (substr($arrayValue, -2) === 'px') {
+ if (str_ends_with($arrayValue, 'px')) {
$arrayValue = (string) (((float) substr($arrayValue, 0, -2)));
} else {
$arrayValue = (new CssDimension($arrayValue))->toUnit(CssDimension::UOM_PIXELS);
}
} elseif ($arrayKey === 'height') {
- if (substr($arrayValue, -2) === 'px') {
+ if (str_ends_with($arrayValue, 'px')) {
$arrayValue = substr($arrayValue, 0, -2);
} else {
$arrayValue = (new CssDimension($arrayValue))->toUnit(CssDimension::UOM_PIXELS);
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/IReader2.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/IReader2.php
new file mode 100644
index 0000000..065e7d6
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/IReader2.php
@@ -0,0 +1,21 @@
+
+ */
+ public function listWorksheetInfo(string $filename): array;
+
+ /**
+ * Returns names of the worksheets from a file,
+ * possibly without parsing the whole file to a Spreadsheet object.
+ *
+ * @return string[]
+ */
+ public function listWorksheetNames(string $filename): array;
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods.php
index d243f4d..bb4b7b1 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods.php
@@ -2,11 +2,15 @@
namespace PhpOffice\PhpSpreadsheet\Reader;
+use Composer\Pcre\Preg;
+use DateTime;
+use DateTimeZone;
use DOMAttr;
use DOMDocument;
use DOMElement;
use DOMNode;
use DOMText;
+use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Helper\Dimension as HelperDimension;
@@ -21,7 +25,13 @@ use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Style\Alignment;
+use PhpOffice\PhpSpreadsheet\Style\Border;
+use PhpOffice\PhpSpreadsheet\Style\Borders;
+use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
+use PhpOffice\PhpSpreadsheet\Style\Protection;
+use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Throwable;
use XMLReader;
@@ -31,6 +41,10 @@ class Ods extends BaseReader
{
const INITIAL_FILE = 'content.xml';
+ private ZipArchive $zip;
+
+ private string $filename;
+
/**
* Create a new Ods Reader instance.
*/
@@ -141,7 +155,14 @@ class Ods extends BaseReader
/**
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
*
- * @return array
+ * @return array
*/
public function listWorksheetInfo(string $filename): array
{
@@ -187,28 +208,41 @@ class Ods extends BaseReader
];
// Loop through each child node of the table:table element reading
- $currCells = 0;
+ $currRow = 0;
do {
$xml->read();
if ($xml->name == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) {
$rowspan = $xml->getAttribute('table:number-rows-repeated');
$rowspan = empty($rowspan) ? 1 : (int) $rowspan;
- $tmpInfo['totalRows'] += $rowspan;
- $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
- $currCells = 0;
+ self::checkRowsRepeated($currRow, $rowspan);
+ $currRow += $rowspan;
+ $currCol = 0;
// Step into the row
$xml->read();
do {
$doread = true;
if ($xml->name == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
+ $mergeSize = $xml->getAttribute('table:number-columns-repeated');
+ $mergeSize = empty($mergeSize) ? 1 : (int) $mergeSize;
+ self::checkColumnsRepeatedInt(
+ $currCol,
+ $mergeSize
+ );
+ $currCol += $mergeSize;
if (!$xml->isEmptyElement) {
- ++$currCells;
+ $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCol);
+ $tmpInfo['totalRows'] = $currRow;
$xml->next();
$doread = false;
}
} elseif ($xml->name == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
$mergeSize = $xml->getAttribute('table:number-columns-repeated');
- $currCells += (int) $mergeSize;
+ $mergeSize = empty($mergeSize) ? 1 : (int) $mergeSize;
+ self::checkColumnsRepeatedInt(
+ $currCol,
+ $mergeSize
+ );
+ $currCol += (int) $mergeSize;
}
if ($doread) {
$xml->read();
@@ -217,9 +251,8 @@ class Ods extends BaseReader
}
} while ($xml->name != 'table:table');
- $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
$tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1;
- $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
+ $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1, true);
$worksheetInfo[] = $tmpInfo;
}
}
@@ -240,6 +273,53 @@ class Ods extends BaseReader
return $this->loadIntoExisting($filename, $spreadsheet);
}
+ /** @var array
+ */
+ private array $allStyles;
+
+ /** @var string[] */
+ private array $numberFormats;
+
+ private int $highestDataIndex;
+
/**
* Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
*/
@@ -247,7 +327,8 @@ class Ods extends BaseReader
{
File::assertFile($filename, self::INITIAL_FILE);
- $zip = new ZipArchive();
+ $this->zip = $zip = new ZipArchive();
+ $this->filename = $filename;
$zip->open($filename);
// Meta
@@ -257,7 +338,7 @@ class Ods extends BaseReader
->scan($zip->getFromName('meta.xml'))
);
if ($xml === false) {
- throw new Exception('Unable to read data from {$pFilename}');
+ throw new Exception("Unable to read data from {$filename}");
}
/** @var array{meta?: string, office?: string, dc?: string} */
@@ -267,34 +348,82 @@ class Ods extends BaseReader
// Styles
- $dom = new DOMDocument('1.01', 'UTF-8');
- $dom->loadXML(
- $this->getSecurityScannerOrThrow()
- ->scan($zip->getFromName('styles.xml'))
- );
+ $this->allStyles = $this->numberFormats = [];
+ $dom = $this->loadDom('styles.xml', $zip);
+ $officeNs = (string) $dom->lookupNamespaceUri('office');
+ $styleNs = (string) $dom->lookupNamespaceUri('style');
+ $fontNs = (string) $dom->lookupNamespaceUri('fo');
+ $numberNs = (string) $dom->lookupNamespaceUri('number');
+ $tableNs = (string) $dom->lookupNamespaceUri('table');
+ $textNs = (string) $dom->lookupNamespaceUri('text');
+ $xlinkNs = (string) $dom->lookupNamespaceUri('xlink');
+
+ $automaticStyle0 = $this->readDataOnly ? null : $dom->getElementsByTagNameNS($officeNs, 'styles')->item(0);
+ $this->processSomeNumberFormats($automaticStyle0, $numberNs, $styleNs);
+ $automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'default-style');
+ foreach ($automaticStyles as $automaticStyle) {
+ $styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family');
+ if ($styleFamily === 'table-cell') {
+ $fonts = [];
+ foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'text-properties') as $textProperty) {
+ $fonts = $this->getFontStyles($textProperty, $styleNs, $fontNs);
+ }
+ if (!empty($fonts)) {
+ $spreadsheet->getDefaultStyle()
+ ->getFont()
+ ->applyFromArray($fonts);
+ }
+ }
+ }
+ $automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'style');
+ foreach ($automaticStyles as $automaticStyle) {
+ $styleName = $automaticStyle->getAttributeNS($styleNs, 'name');
+ $styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family');
+ if ($styleFamily === 'table-cell') {
+ $fills = $fonts = [];
+ foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'text-properties') as $textProperty) {
+ $fonts = $this->getFontStyles($textProperty, $styleNs, $fontNs);
+ }
+ foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'table-cell-properties') as $tableCellProperty) {
+ $fills = $this->getFillStyles($tableCellProperty, $fontNs);
+ }
+ if ($styleName !== '') {
+ if (!empty($fonts)) {
+ $this->allStyles[$styleName]['font'] = $fonts;
+ if ($styleName === 'Default') {
+ $spreadsheet->getDefaultStyle()
+ ->getFont()
+ ->applyFromArray($fonts);
+ }
+ }
+ if (!empty($fills)) {
+ $this->allStyles[$styleName]['fill'] = $fills;
+ if ($styleName === 'Default') {
+ $spreadsheet->getDefaultStyle()
+ ->getFill()
+ ->applyFromArray($fills);
+ }
+ }
+ }
+ }
+ }
+
+ $automaticStyle0 = $this->readDataOnly ? null : $dom->getElementsByTagNameNS($officeNs, 'automatic-styles')->item(0);
+ $this->processSomeNumberFormats($automaticStyle0, $numberNs, $styleNs);
$pageSettings = new PageSettings($dom);
// Main Content
- $dom = new DOMDocument('1.01', 'UTF-8');
- $dom->loadXML(
- $this->getSecurityScannerOrThrow()
- ->scan($zip->getFromName(self::INITIAL_FILE))
- );
-
- $officeNs = (string) $dom->lookupNamespaceUri('office');
- $tableNs = (string) $dom->lookupNamespaceUri('table');
- $textNs = (string) $dom->lookupNamespaceUri('text');
- $xlinkNs = (string) $dom->lookupNamespaceUri('xlink');
- $styleNs = (string) $dom->lookupNamespaceUri('style');
+ $dom = $this->loadDom(self::INITIAL_FILE, $zip);
$pageSettings->readStyleCrossReferences($dom);
$autoFilterReader = new AutoFilter($spreadsheet, $tableNs);
$definedNameReader = new DefinedNames($spreadsheet, $tableNs);
$columnWidths = [];
- $automaticStyle0 = $dom->getElementsByTagNameNS($officeNs, 'automatic-styles')->item(0);
+ $automaticStyle0 = $this->readDataOnly ? null : $dom->getElementsByTagNameNS($officeNs, 'automatic-styles')->item(0);
+ $this->processSomeNumberFormats($automaticStyle0, $numberNs, $styleNs);
$automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'style');
foreach ($automaticStyles as $automaticStyle) {
$styleName = $automaticStyle->getAttributeNS($styleNs, 'name');
@@ -307,6 +436,46 @@ class Ods extends BaseReader
$columnWidths[$styleName] = $columnWidth;
}
}
+ if ($styleFamily === 'table-cell') {
+ $fonts = $fills = $alignment1 = $alignment2 = $protection = $borders = [];
+ $numberFormatName = $automaticStyle->getAttributeNS($styleNs, 'data-style-name');
+ $numberFormat = $this->numberFormats[$numberFormatName] ?? '';
+ foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'text-properties') as $textProperty) {
+ $fonts = $this->getFontStyles($textProperty, $styleNs, $fontNs);
+ }
+ foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'table-cell-properties') as $tableCellProperty) {
+ $fills = $this->getFillStyles($tableCellProperty, $fontNs);
+ $borders = $this->getBorderStyles($tableCellProperty, $fontNs, $styleNs);
+ $protection = $this->getProtectionStyles($tableCellProperty, $styleNs);
+ }
+ foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'table-cell-properties') as $tableCellProperty) {
+ $alignment1 = $this->getAlignment1Styles($tableCellProperty, $styleNs, $fontNs);
+ }
+ foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'paragraph-properties') as $paragraphProperty) {
+ $alignment2 = $this->getAlignment2Styles($paragraphProperty, $styleNs, $fontNs);
+ }
+ if ($styleName !== '') {
+ if (!empty($fonts)) {
+ $this->allStyles[$styleName]['font'] = $fonts;
+ }
+ if (!empty($fills)) {
+ $this->allStyles[$styleName]['fill'] = $fills;
+ }
+ $alignment = array_merge($alignment1, $alignment2);
+ if (!empty($alignment)) {
+ $this->allStyles[$styleName]['alignment'] = $alignment;
+ }
+ if (!empty($protection)) {
+ $this->allStyles[$styleName]['protection'] = $protection;
+ }
+ if (!empty($borders)) {
+ $this->allStyles[$styleName]['borders'] = $borders;
+ }
+ if ($numberFormat !== '') {
+ $this->allStyles[$styleName]['numberFormat']['formatCode'] = $numberFormat;
+ }
+ }
+ }
}
// Content
@@ -343,12 +512,14 @@ class Ods extends BaseReader
// Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in
// formula cells... during the load, all formulae should be correct, and we're simply
// bringing the worksheet name in line with the formula, not the reverse
- $spreadsheet->getActiveSheet()->setTitle((string) $worksheetName, false, false);
+ $spreadsheet->getActiveSheet()
+ ->setTitle((string) $worksheetName, false, false);
}
// Go through every child of table element
$rowID = 1;
$tableColumnIndex = 1;
+ $this->highestDataIndex = AddressRange::MAX_COLUMN_INT;
foreach ($worksheetDataSet->childNodes as $childNode) {
/** @var DOMElement $childNode */
@@ -386,37 +557,6 @@ class Ods extends BaseReader
$spreadsheet
);
- break;
- case 'table-header-columns':
- case 'table-columns':
- $this->processTableHeaderColumns(
- $childNode,
- $tableNs,
- $columnWidths,
- $tableColumnIndex,
- $spreadsheet
- );
-
- break;
- case 'table-column-group':
- $this->processTableColumnGroup(
- $childNode,
- $tableNs,
- $columnWidths,
- $tableColumnIndex,
- $spreadsheet
- );
-
- break;
- case 'table-column':
- $this->processTableColumn(
- $childNode,
- $tableNs,
- $columnWidths,
- $tableColumnIndex,
- $spreadsheet
- );
-
break;
case 'table-row':
$this->processTableRow(
@@ -430,6 +570,43 @@ class Ods extends BaseReader
$spreadsheet
);
+ break;
+ case 'table-header-columns':
+ case 'table-columns':
+ $this->processTableColumnHeader(
+ $childNode,
+ $tableNs,
+ $columnWidths,
+ $tableColumnIndex,
+ $spreadsheet,
+ $this->readEmptyCells,
+ true
+ );
+
+ break;
+ case 'table-column-group':
+ $this->processTableColumnGroup(
+ $childNode,
+ $tableNs,
+ $columnWidths,
+ $tableColumnIndex,
+ $spreadsheet,
+ $this->readEmptyCells,
+ true
+ );
+
+ break;
+ case 'table-column':
+ $this->processTableColumn(
+ $childNode,
+ $tableNs,
+ $columnWidths,
+ $tableColumnIndex,
+ $spreadsheet,
+ $this->readEmptyCells,
+ true
+ );
+
break;
}
}
@@ -446,10 +623,94 @@ class Ods extends BaseReader
if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
$spreadsheet->createSheet();
}
+ }
+
+ foreach ($spreadsheets as $workbookData) {
+ /** @var DOMElement $workbookData */
+ $tables = $workbookData->getElementsByTagNameNS($tableNs, 'table');
+
+ $worksheetID = 0;
+ foreach ($tables as $worksheetDataSet) {
+ /** @var DOMElement $worksheetDataSet */
+ $worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name');
+
+ // Check loadSheetsOnly
+ if (
+ $this->loadSheetsOnly !== null
+ && $worksheetName
+ && !in_array($worksheetName, $this->loadSheetsOnly)
+ ) {
+ continue;
+ }
+
+ // Create sheet
+ $spreadsheet->setActiveSheetIndex($worksheetID);
+ $highestDataColumn = $spreadsheet->getActiveSheet()->getHighestDataColumn();
+ $this->highestDataIndex = Coordinate::columnIndexFromString($highestDataColumn);
+
+ // Go through every child of table element processing column widths
+ $rowID = 1;
+ $tableColumnIndex = 1;
+ foreach ($worksheetDataSet->childNodes as $childNode) {
+ /** @var DOMElement $childNode */
+ if (empty($columnWidths) || $this->readEmptyCells) {
+ break;
+ }
+
+ // Filter elements which are not under the "table" ns
+ if ($childNode->namespaceURI != $tableNs) {
+ continue;
+ }
+
+ $key = self::extractNodeName($childNode->nodeName);
+
+ switch ($key) {
+ case 'table-header-columns':
+ case 'table-columns':
+ $this->processTableColumnHeader(
+ $childNode,
+ $tableNs,
+ $columnWidths,
+ $tableColumnIndex,
+ $spreadsheet,
+ true,
+ false
+ );
+
+ break;
+ case 'table-column-group':
+ $this->processTableColumnGroup(
+ $childNode,
+ $tableNs,
+ $columnWidths,
+ $tableColumnIndex,
+ $spreadsheet,
+ true,
+ false
+ );
+
+ break;
+ case 'table-column':
+ $this->processTableColumn(
+ $childNode,
+ $tableNs,
+ $columnWidths,
+ $tableColumnIndex,
+ $spreadsheet,
+ true,
+ false
+ );
+
+ break;
+ }
+ }
+ ++$worksheetID;
+ }
$autoFilterReader->read($workbookData);
$definedNameReader->read($workbookData);
}
+
$spreadsheet->setActiveSheetIndex(0);
if ($zip->locateName('settings.xml') !== false) {
@@ -564,6 +825,8 @@ class Ods extends BaseReader
} else {
$rowRepeats = 1;
}
+ self::checkRowsRepeated($rowID, $rowRepeats);
+ $worksheet = $spreadsheet->getSheetByName($worksheetName);
$columnID = 'A';
/** @var DOMElement|DOMText $cellData */
@@ -571,18 +834,94 @@ class Ods extends BaseReader
if ($cellData instanceof DOMText) {
continue; // should just be whitespace
}
- if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
- if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
- $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
+ if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
+ $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
+ } else {
+ $colRepeats = 1;
+ }
+ $columnIndex = Coordinate::columnIndexFromString($columnID);
+ self::checkColumnsRepeated($columnID, $colRepeats);
+ $styleName = $cellData->getAttributeNS($tableNs, 'style-name');
+ if ($styleName === '') {
+ if ($worksheet === null || !$worksheet->columnDimensionExists($columnID)) {
+ $assignedNumberFormat = '';
} else {
- $colRepeats = 1;
+ $colStyle = $worksheet->getColumnDimension($columnID)->getXfIndex() ?? 0;
+ $assignedNumberFormat = $spreadsheet
+ ->getCellXfByIndex($colStyle)
+ ->getNumberFormat()->getFormatCode();
+ if ($assignedNumberFormat === NumberFormat::FORMAT_GENERAL) {
+ $assignedNumberFormat = '';
+ }
}
+ } else {
+ $assignedNumberFormat = $this->allStyles[$styleName]['numberFormat']['formatCode'] ?? '';
+ }
- for ($i = 0; $i < $colRepeats; ++$i) {
+ // When a cell has number-columns-repeated, check if ANY column in the
+ // repeated range passes the read filter. If not, skip the entire group.
+ // If some columns pass, we need to fall through to the processing block
+ // which will handle per-column filtering.
+ if (!$this->readFilter->readCell($columnID, $rowID, $worksheetName)) {
+ if ($colRepeats <= 1) {
StringHelper::stringIncrement($columnID);
+
+ continue;
}
- continue;
+ // Check if any column within this repeated group passes the filter
+ $anyColumnPasses = false;
+ $tempCol = $columnID;
+ for ($i = 0; $i < $colRepeats; ++$i) {
+ if ($i > 0) {
+ StringHelper::stringIncrement($tempCol);
+ }
+ if ($this->readFilter->readCell($tempCol, $rowID, $worksheetName)) {
+ $anyColumnPasses = true;
+
+ break;
+ }
+ }
+
+ if (!$anyColumnPasses) {
+ for ($i = 0; $i < $colRepeats; ++$i) {
+ StringHelper::stringIncrement($columnID);
+ }
+
+ continue;
+ }
+ // Fall through to process the cell, with per-column filter checks
+ }
+ $tempSpannedRange = "$columnID$rowID";
+ $spannedRange = '';
+ if ($worksheet !== null && ($cellData->hasChildNodes() || ($cellData->nextSibling !== null)) && isset($this->allStyles[$styleName])) {
+ $spannedRange = "$columnID$rowID";
+ // the following is sufficient for ods,
+ // and does no harm for xlsx/xls.
+ $worksheet->getStyle($spannedRange)
+ ->applyFromArray($this->allStyles[$styleName]);
+ // the rest of this block is needed for xlsx/xls,
+ // and does no harm for ods.
+ if (isset($this->allStyles[$styleName]['borders'])) {
+ $spannedRows = $cellData->getAttributeNS($tableNs, 'number-columns-spanned');
+ $spannedColumns = $cellData->getAttributeNS($tableNs, 'number-rows-spanned');
+ $spannedRows = max((int) $spannedRows, 1);
+ $spannedColumns = max((int) $spannedColumns, 1);
+ if ($spannedRows > 1 || $spannedColumns > 1) {
+ $endRow = $rowID + $spannedRows - 1;
+ $endCol = $columnID;
+ while ($spannedColumns > 1) {
+ StringHelper::stringIncrement($endCol);
+ --$spannedColumns;
+ }
+ $spannedRange .= ":$endCol$endRow";
+ $worksheet->getStyle($spannedRange)
+ ->getBorders()
+ ->applyFromArray(
+ $this->allStyles[$styleName]['borders']
+ );
+ }
+ }
}
// Initialize variables
@@ -651,10 +990,13 @@ class Ods extends BaseReader
// Filter text:p elements
if ($item->nodeName == 'text:p') {
$paragraphs[] = $item;
+ } elseif ($item->nodeName === 'draw:frame' && $worksheet !== null) {
+ $this->processDrawFrame($spannedRange ?: $tempSpannedRange, $item, $worksheet);
}
}
if (count($paragraphs) > 0) {
+ $dataValue = null;
// Consolidate if there are multiple p records (maybe with spans as well)
$dataArray = [];
@@ -669,6 +1011,19 @@ class Ods extends BaseReader
$allCellDataText = implode("\n", $dataArray);
$type = $cellData->getAttributeNS($officeNs, 'value-type');
+ $symbol = '';
+ $leftHandCurrency = Preg::isMatch('/\$|£|¥/', $allCellDataText, $matches);
+ if ($leftHandCurrency) {
+ $type = str_replace('float', 'currency', $type);
+ $symbol = (string) $matches[0];
+ }
+ $customFormatting = '';
+ if ($this->formatCallback !== null) {
+ $temp = ($this->formatCallback)($type, $allCellDataText);
+ if ($temp !== '') {
+ $customFormatting = $temp;
+ }
+ }
switch ($type) {
case 'string':
@@ -689,30 +1044,47 @@ class Ods extends BaseReader
break;
case 'percentage':
+ if (!str_contains($allCellDataText, '.')) {
+ $formatting = NumberFormat::FORMAT_PERCENTAGE;
+ } elseif (substr($allCellDataText, -3, 1) === '.') {
+ $formatting = NumberFormat::FORMAT_PERCENTAGE_0;
+ } else {
+ $formatting = NumberFormat::FORMAT_PERCENTAGE_00;
+ }
$type = DataType::TYPE_NUMERIC;
$dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
- // percentage should always be float
- //if (floor($dataValue) == $dataValue) {
- // $dataValue = (int) $dataValue;
- //}
- $formatting = NumberFormat::FORMAT_PERCENTAGE_00;
-
break;
case 'currency':
$type = DataType::TYPE_NUMERIC;
$dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
- if (floor($dataValue) == $dataValue) {
- $dataValue = (int) $dataValue;
+ $currency = $cellData->getAttributeNS($officeNs, 'currency');
+ if ($leftHandCurrency) {
+ $typeValue = 'currency';
+ $formatting = str_contains($allCellDataText, '.') ? NumberFormat::FORMAT_CURRENCY_USD : NumberFormat::FORMAT_CURRENCY_USD_INTEGER;
+ if ($symbol !== '$') {
+ $formatting = str_replace('$', $symbol, $formatting);
+ }
+ } elseif (str_contains($allCellDataText, '€')) {
+ $typeValue = 'currency';
+ $formatting = str_contains($allCellDataText, '.') ? NumberFormat::FORMAT_CURRENCY_EUR : NumberFormat::FORMAT_CURRENCY_EUR_INTEGER;
}
- $formatting = NumberFormat::FORMAT_CURRENCY_USD_INTEGER;
break;
case 'float':
$type = DataType::TYPE_NUMERIC;
$dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
+ if ($assignedNumberFormat !== '') {
+ $formatting = $assignedNumberFormat;
+ } elseif ($dataValue !== floor($dataValue)) {
+ // do nothing
+ } elseif (substr($allCellDataText, -2, 1) === '.') {
+ $formatting = NumberFormat::FORMAT_NUMBER_0;
+ } elseif (substr($allCellDataText, -3, 1) === '.') {
+ $formatting = NumberFormat::FORMAT_NUMBER_00;
+ }
if (floor($dataValue) == $dataValue) {
if ($dataValue == (int) $dataValue) {
$dataValue = (int) $dataValue;
@@ -724,13 +1096,20 @@ class Ods extends BaseReader
$type = DataType::TYPE_NUMERIC;
$value = $cellData->getAttributeNS($officeNs, 'date-value');
$dataValue = Date::convertIsoDate($value);
+ $format15 = Preg::isMatch('/\d\d\d\d/', $allCellDataText) ? NumberFormat::FORMAT_DATE_XLSX15_YYYY : NumberFormat::FORMAT_DATE_XLSX15;
- if ($dataValue != floor($dataValue)) {
- $formatting = NumberFormat::FORMAT_DATE_XLSX15
+ if (Preg::isMatch('/^\d\d\d\d-\d\d-\d\d$/', $allCellDataText)) {
+ $formatting = 'yyyy-mm-dd';
+ } elseif (Preg::isMatch('/^\d\d\d\d-\d\d-\d\d \d\d:\d\d(:\d\d)?$/', $allCellDataText)) {
+ $formatting = NumberFormat::FORMAT_DATE_DATETIME_BETTER;
+ } elseif (Preg::isMatch('/^\d\d?-[a-zA-Z]+-\d\d\d\d$/', $allCellDataText)) {
+ $formatting = 'd-mmm-yyyy';
+ } elseif ($dataValue != floor($dataValue) || str_contains($allCellDataText, ':')) {
+ $formatting = $format15
. ' '
. NumberFormat::FORMAT_DATE_TIME4;
} else {
- $formatting = NumberFormat::FORMAT_DATE_XLSX15;
+ $formatting = $format15;
}
break;
@@ -738,18 +1117,34 @@ class Ods extends BaseReader
$type = DataType::TYPE_NUMERIC;
$timeValue = $cellData->getAttributeNS($officeNs, 'time-value');
-
- $dataValue = Date::PHPToExcel(
- strtotime(
- '01-01-1970 ' . implode(':', sscanf($timeValue, 'PT%dH%dM%dS') ?? [])
- )
- );
- $formatting = NumberFormat::FORMAT_DATE_TIME4;
+ $minus = '';
+ if (str_starts_with($timeValue, '-')) {
+ $minus = '-';
+ $timeValue = substr($timeValue, 1);
+ }
+ $timeArray = sscanf($timeValue, 'PT%dH%dM%dS');
+ if (is_array($timeArray)) {
+ /** @var array{int, int, int} $timeArray */
+ $days = intdiv($timeArray[0], 24);
+ $hours = $timeArray[0] % 24;
+ $dt = new DateTime("1899-12-30 $hours:{$timeArray[1]}:{$timeArray[2]}", new DateTimeZone('UTC'));
+ $dt->modify("+$days days");
+ $dataValue = Date::PHPToExcel($dt);
+ if ($minus === '-') {
+ $dataValue *= -1;
+ $formatting = '[hh]:mm:ss';
+ } else {
+ $formatting = NumberFormat::FORMAT_DATE_TIME4;
+ }
+ }
break;
default:
$dataValue = null;
}
+ if ($customFormatting !== '') {
+ $formatting = $customFormatting;
+ }
} else {
$type = DataType::TYPE_NULL;
$dataValue = null;
@@ -761,59 +1156,55 @@ class Ods extends BaseReader
$cellDataFormula = FormulaTranslator::convertToExcelFormulaValue($cellDataFormula);
}
- if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
- $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
- } else {
- $colRepeats = 1;
- }
+ for ($i = 0; $i < $colRepeats; ++$i) {
+ if ($i > 0) {
+ StringHelper::stringIncrement($columnID);
+ }
- if ($type !== null) { // @phpstan-ignore-line
- for ($i = 0; $i < $colRepeats; ++$i) {
- if ($i > 0) {
- StringHelper::stringIncrement($columnID);
- }
+ if (!$this->readFilter->readCell($columnID, $rowID, $worksheetName)) {
+ continue;
+ }
- if ($type !== DataType::TYPE_NULL) {
- for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) {
- $rID = $rowID + $rowAdjust;
+ if ($type !== DataType::TYPE_NULL) {
+ for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) {
+ $rID = $rowID + $rowAdjust;
- $cell = $spreadsheet->getActiveSheet()
- ->getCell($columnID . $rID);
+ $cell = $spreadsheet->getActiveSheet()
+ ->getCell($columnID . $rID);
- // Set value
- if ($hasCalculatedValue) {
- $cell->setValueExplicit($cellDataFormula, $type);
- if ($cellDataType === 'array') {
- $cell->setFormulaAttributes(['t' => 'array', 'ref' => $cellDataRef]);
- }
- } elseif ($type !== '' || $dataValue !== null) {
- $cell->setValueExplicit($dataValue, $type);
+ // Set value
+ if ($hasCalculatedValue) {
+ $cell->setValueExplicit($cellDataFormula, $type);
+ if ($cellDataType === 'array') {
+ $cell->setFormulaAttributes(['t' => 'array', 'ref' => $cellDataRef]);
}
+ } elseif ($type !== '' || $dataValue !== null) {
+ $cell->setValueExplicit($dataValue, $type);
+ }
- if ($hasCalculatedValue) {
- $cell->setCalculatedValue($dataValue, $type === DataType::TYPE_NUMERIC);
- }
+ if ($hasCalculatedValue) {
+ $cell->setCalculatedValue($dataValue, $type === DataType::TYPE_NUMERIC);
+ }
- // Set other properties
- if ($formatting !== null) {
- $spreadsheet->getActiveSheet()
- ->getStyle($columnID . $rID)
- ->getNumberFormat()
- ->setFormatCode($formatting);
- } else {
- $spreadsheet->getActiveSheet()
- ->getStyle($columnID . $rID)
- ->getNumberFormat()
- ->setFormatCode(NumberFormat::FORMAT_GENERAL);
- }
+ // Set other properties
+ if ($formatting !== null) {
+ $spreadsheet->getActiveSheet()
+ ->getStyle($columnID . $rID)
+ ->getNumberFormat()
+ ->setFormatCode($formatting);
+ } else {
+ $spreadsheet->getActiveSheet()
+ ->getStyle($columnID . $rID)
+ ->getNumberFormat()
+ ->setFormatCode(NumberFormat::FORMAT_GENERAL);
+ }
- if ($hyperlink !== null) {
- if ($hyperlink[0] === '#') {
- $hyperlink = 'sheet://' . substr($hyperlink, 1);
- }
- $cell->getHyperlink()
- ->setUrl($hyperlink);
+ if ($hyperlink !== null) {
+ if ($hyperlink[0] === '#') {
+ $hyperlink = 'sheet://' . substr($hyperlink, 1);
}
+ $cell->getHyperlink()
+ ->setUrl($hyperlink);
}
}
}
@@ -827,6 +1218,61 @@ class Ods extends BaseReader
$rowID += $rowRepeats;
}
+ private function processDrawFrame(string $spannedRange, DOMElement $item, Worksheet $worksheet): void
+ {
+ $drawName = $item->getAttribute('draw:name');
+ $svgWidth = $item->getAttribute('svg:width');
+ $svgHeight = $item->getAttribute('svg:height');
+ $styleName = $item->getAttribute('draw:style-name');
+ $drawImage = null;
+ foreach ($item->childNodes as $node) {
+ // Check if the node is a standard element tag
+ if ($node->nodeType === XML_ELEMENT_NODE && $node->nodeName === 'draw:image') {
+ /** @var DOMElement */
+ $drawImage = $node;
+
+ break;
+ }
+ }
+
+ $xlinkHref = $xlinkType = $xlinkShow = '';
+ if ($drawImage !== null) {
+ $xlinkHref = $drawImage->getAttribute('xlink:href');
+ $xlinkType = $drawImage->getAttribute('xlink:type');
+ $xlinkShow = $drawImage->getAttribute('xlink:show');
+ }
+ if (
+ $drawName !== ''
+ && Preg::isMatch('/(\d+([.]\d+)?)(cm|in)/', $svgWidth, $matchWidth)
+ && Preg::isMatch('/(\d+([.]\d+)?)(cm|in)/', $svgHeight, $matchHeight)
+ //&& $styleName === 'gr1'
+ && (str_starts_with($xlinkHref, 'Pictures/') || str_starts_with($xlinkHref, 'media/'))
+ && $xlinkType === 'simple'
+ && $xlinkShow === 'embed'
+ ) {
+ $drawing = new Drawing();
+ $drawing->setPath(
+ "zip://{$this->filename}#$xlinkHref",
+ true,
+ $this->zip,
+ false
+ );
+ $unit = [
+ 'cm' => HelperDimension::ABSOLUTE_UNITS[HelperDimension::UOM_CENTIMETERS],
+ 'in' => HelperDimension::ABSOLUTE_UNITS[HelperDimension::UOM_INCHES],
+ ];
+ $width = ((float) $matchWidth[1]) * $unit[$matchWidth[3]];
+ $height = ((float) $matchHeight[1]) * $unit[$matchHeight[3]];
+ if ($drawing->getPath()) {
+ $drawing->setCoordinates($spannedRange)
+ ->setWidth((int) $width)
+ ->setHeight((int) $height)
+ ->setName($drawName)
+ ->setWorksheet($worksheet);
+ }
+ }
+ }
+
private static function extractNodeName(string $key): string
{
// Remove ns from node name
@@ -841,12 +1287,14 @@ class Ods extends BaseReader
/**
* @param string[] $columnWidths
*/
- private function processTableHeaderColumns(
+ private function processTableColumnHeader(
DOMElement $childNode,
string $tableNs,
array $columnWidths,
int &$tableColumnIndex,
- Spreadsheet $spreadsheet
+ Spreadsheet $spreadsheet,
+ bool $processWidths = true,
+ bool $processStyles = true
): void {
foreach ($childNode->childNodes as $grandchildNode) {
/** @var DOMElement $grandchildNode */
@@ -858,7 +1306,9 @@ class Ods extends BaseReader
$tableNs,
$columnWidths,
$tableColumnIndex,
- $spreadsheet
+ $spreadsheet,
+ $processWidths,
+ $processStyles
);
break;
@@ -874,7 +1324,9 @@ class Ods extends BaseReader
string $tableNs,
array $columnWidths,
int &$tableColumnIndex,
- Spreadsheet $spreadsheet
+ Spreadsheet $spreadsheet,
+ bool $processWidths = true,
+ bool $processStyles = true
): void {
foreach ($childNode->childNodes as $grandchildNode) {
/** @var DOMElement $grandchildNode */
@@ -886,18 +1338,22 @@ class Ods extends BaseReader
$tableNs,
$columnWidths,
$tableColumnIndex,
- $spreadsheet
+ $spreadsheet,
+ $processWidths,
+ $processStyles
);
break;
case 'table-header-columns':
case 'table-columns':
- $this->processTableHeaderColumns(
+ $this->processTableColumnHeader(
$grandchildNode,
$tableNs,
$columnWidths,
$tableColumnIndex,
- $spreadsheet
+ $spreadsheet,
+ $processWidths,
+ $processStyles
);
break;
@@ -907,7 +1363,9 @@ class Ods extends BaseReader
$tableNs,
$columnWidths,
$tableColumnIndex,
- $spreadsheet
+ $spreadsheet,
+ $processWidths,
+ $processStyles
);
break;
@@ -923,35 +1381,61 @@ class Ods extends BaseReader
string $tableNs,
array $columnWidths,
int &$tableColumnIndex,
- Spreadsheet $spreadsheet
+ Spreadsheet $spreadsheet,
+ bool $processWidths = true,
+ bool $processStyles = true
): void {
if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) {
- $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated');
+ $colRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated');
} else {
- $rowRepeats = 1;
+ $colRepeats = 1;
}
+ // called routine expects index to be 1 less than it is
+ self::checkColumnsRepeatedInt($tableColumnIndex - 1, $colRepeats);
$tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name');
- if (isset($columnWidths[$tableStyleName])) {
- $columnWidth = new HelperDimension($columnWidths[$tableStyleName]);
- $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex);
- for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0; --$rowRepeats2) {
- /** @var string $tableColumnString */
- $spreadsheet->getActiveSheet()
- ->getColumnDimension($tableColumnString)
- ->setWidth($columnWidth->toUnit('cm'), 'cm');
- StringHelper::stringIncrement($tableColumnString);
+ if ($processWidths) {
+ if (isset($columnWidths[$tableStyleName])) {
+ $columnWidth = new HelperDimension($columnWidths[$tableStyleName]);
+ $tableColumnIndex2 = $tableColumnIndex;
+ $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex2);
+ for ($colRepeats2 = $colRepeats; $colRepeats2 > 0 && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT; --$colRepeats2) {
+ if (!$this->readEmptyCells && $tableColumnIndex2 > $this->highestDataIndex) {
+ break;
+ }
+ $spreadsheet->getActiveSheet()
+ ->getColumnDimension($tableColumnString)
+ ->setWidth($columnWidth->toUnit('cm'), 'cm');
+ StringHelper::stringIncrement(
+ $tableColumnString
+ );
+ ++$tableColumnIndex2;
+ }
}
}
- $tableColumnIndex += $rowRepeats;
+ if ($processStyles) {
+ $defaultStyleName = $childNode->getAttributeNS($tableNs, 'default-cell-style-name');
+ if ($defaultStyleName !== 'Default' && isset($this->allStyles[$defaultStyleName])) {
+ $tableColumnIndex2 = $tableColumnIndex;
+ $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex2);
+ for ($colRepeats2 = $colRepeats; $colRepeats2 > 0 && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT; --$colRepeats2) {
+ $spreadsheet->getActiveSheet()
+ ->getStyle($tableColumnString)
+ ->applyFromArray(
+ $this->allStyles[$defaultStyleName]
+ );
+ StringHelper::stringIncrement(
+ $tableColumnString
+ );
+ ++$tableColumnIndex2;
+ }
+ }
+ }
+ $tableColumnIndex += $colRepeats;
}
private function processSettings(ZipArchive $zip, Spreadsheet $spreadsheet): void
{
- $dom = new DOMDocument('1.01', 'UTF-8');
- $dom->loadXML(
- $this->getSecurityScannerOrThrow()
- ->scan($zip->getFromName('settings.xml'))
- );
+ $dom = $this->loadDom('settings.xml', $zip);
$configNs = (string) $dom->lookupNamespaceUri('config');
$officeNs = (string) $dom->lookupNamespaceUri('office');
$settings = $dom->getElementsByTagNameNS($officeNs, 'settings')
@@ -1097,4 +1581,346 @@ class Ods extends BaseReader
}
}
}
+
+ /** @var null|callable(string, string):string format callback routine */
+ private $formatCallback;
+
+ /** @param callable(string, string):string $formatCallback format callback routine */
+ public function setFormatCallback(callable $formatCallback): void
+ {
+ $this->formatCallback = $formatCallback;
+ }
+
+ /** @return array{
+ * autoColor?: true,
+ * bold?: true,
+ * color?: array{rgb: string},
+ * italic?: true,
+ * name?: non-empty-string,
+ * size?: float|int,
+ * strikethrough?: true,
+ * underline?: 'double'|'single',
+ * }
+ */
+ protected function getFontStyles(DOMElement $textProperty, string $styleNs, string $fontNs): array
+ {
+ $fonts = [];
+ $temp = $textProperty->getAttributeNs($styleNs, 'font-name') ?: $textProperty->getAttributeNs($fontNs, 'font-family');
+ if ($temp !== '') {
+ $fonts['name'] = $temp;
+ }
+ $temp = $textProperty->getAttributeNs($fontNs, 'font-size');
+ if ($temp !== '' && str_ends_with($temp, 'pt')) {
+ $fonts['size'] = (float) substr($temp, 0, -2);
+ }
+ $temp = $textProperty->getAttributeNs($fontNs, 'font-style');
+ if ($temp === 'italic') {
+ $fonts['italic'] = true;
+ }
+ $temp = $textProperty->getAttributeNs($fontNs, 'font-weight');
+ if ($temp === 'bold') {
+ $fonts['bold'] = true;
+ }
+ $temp = $textProperty->getAttributeNs($fontNs, 'color');
+ if (Preg::isMatch('/^#[a-f0-9]{6}$/i', $temp)) {
+ $fonts['color'] = ['rgb' => substr($temp, 1)];
+ }
+ $temp = $textProperty->getAttributeNs($styleNs, 'use-window-font-color');
+ if ($temp === 'true') {
+ $fonts['autoColor'] = true;
+ }
+ $temp = $textProperty->getAttributeNs($styleNs, 'text-underline-type');
+ if ($temp === '') {
+ $temp = $textProperty->getAttributeNs($styleNs, 'text-underline-style');
+ if ($temp !== '' && $temp !== 'none') {
+ $temp = 'single';
+ }
+ }
+ if ($temp === 'single' || $temp === 'double') {
+ $fonts['underline'] = $temp;
+ }
+ $temp = $textProperty->getAttributeNs($styleNs, 'text-line-through-type');
+ if ($temp !== '' && $temp !== 'none') {
+ $fonts['strikethrough'] = true;
+ }
+
+ return $fonts;
+ }
+
+ /** @return array{
+ * fillType?: string,
+ * startColor?: array{rgb: string},
+ * }
+ */
+ protected function getFillStyles(DOMElement $tableCellProperties, string $fontNs): array
+ {
+ $fills = [];
+ $temp = $tableCellProperties->getAttributeNs($fontNs, 'background-color');
+ if (Preg::isMatch('/^#[a-f0-9]{6}$/i', $temp)) {
+ $fills['fillType'] = Fill::FILL_SOLID;
+ $fills['startColor'] = ['rgb' => substr($temp, 1)];
+ } elseif ($temp === 'transparent') {
+ $fills['fillType'] = Fill::FILL_NONE;
+ }
+
+ return $fills;
+ }
+
+ private const MAP_VERTICAL = [
+ 'top' => Alignment::VERTICAL_TOP,
+ 'middle' => Alignment::VERTICAL_CENTER,
+ 'automatic' => Alignment::VERTICAL_JUSTIFY,
+ 'bottom' => Alignment::VERTICAL_BOTTOM,
+ ];
+ private const MAP_HORIZONTAL = [
+ 'center' => Alignment::HORIZONTAL_CENTER,
+ 'end' => Alignment::HORIZONTAL_RIGHT,
+ 'justify' => Alignment::HORIZONTAL_FILL,
+ 'start' => Alignment::HORIZONTAL_LEFT,
+ ];
+
+ /** @return array{
+ * shrinkToFit?: bool,
+ * textRotation?: int,
+ * vertical?: string,
+ * wrapText?: bool,
+ * }
+ */
+ protected function getAlignment1Styles(DOMElement $tableCellProperties, string $styleNs, string $fontNs): array
+ {
+ $alignment1 = [];
+ $temp = $tableCellProperties->getAttributeNs($styleNs, 'rotation-angle');
+ if (is_numeric($temp)) {
+ $temp2 = (int) $temp;
+ if ($temp2 > 90) {
+ $temp2 -= 360;
+ }
+ if ($temp2 >= -90 && $temp2 <= 90) {
+ $alignment1['textRotation'] = (int) $temp2;
+ }
+ }
+ $temp = $tableCellProperties->getAttributeNs($styleNs, 'vertical-align');
+ $temp2 = self::MAP_VERTICAL[$temp] ?? '';
+ if ($temp2 !== '') {
+ $alignment1['vertical'] = $temp2;
+ }
+ $temp = $tableCellProperties->getAttributeNs($fontNs, 'wrap-option');
+ if ($temp === 'wrap') {
+ $alignment1['wrapText'] = true;
+ } elseif ($temp === 'no-wrap') {
+ $alignment1['wrapText'] = false;
+ }
+ $temp = $tableCellProperties->getAttributeNs($styleNs, 'shrink-to-fit');
+ if ($temp === 'true' || $temp === 'false') {
+ $alignment1['shrinkToFit'] = $temp === 'true';
+ }
+
+ return $alignment1;
+ }
+
+ /** @return array{
+ * horizontal?: string,
+ * readOrder?: int,
+ * indent?: int,
+ * }
+ */
+ protected function getAlignment2Styles(DOMElement $paragraphProperties, string $styleNs, string $fontNs): array
+ {
+ $alignment2 = [];
+ $temp = $paragraphProperties->getAttributeNs($fontNs, 'text-align');
+ $temp2 = self::MAP_HORIZONTAL[$temp] ?? '';
+ if ($temp2 !== '') {
+ $alignment2['horizontal'] = $temp2;
+ }
+ $temp = $paragraphProperties->getAttributeNs($fontNs, 'margin-left') ?: $paragraphProperties->getAttributeNs($fontNs, 'margin-right');
+ if (Preg::isMatch('/^\d+([.]\d+)?(cm|in|mm|pt)$/', $temp)) {
+ $dimension = new HelperDimension($temp);
+ $alignment2['indent'] = (int) round($dimension->toUnit('px') / Alignment::INDENT_UNITS_TO_PIXELS);
+ }
+
+ $temp = $paragraphProperties->getAttributeNs($styleNs, 'writing-mode');
+ if ($temp === 'rl-tb') {
+ $alignment2['readOrder'] = Alignment::READORDER_RTL;
+ } elseif ($temp === 'lr-tb') {
+ $alignment2['readOrder'] = Alignment::READORDER_LTR;
+ }
+
+ return $alignment2;
+ }
+
+ /** @return array{
+ * locked?: string,
+ * hidden?: string,
+ * }
+ */
+ protected function getProtectionStyles(DOMElement $tableCellProperties, string $styleNs): array
+ {
+ $protection = [];
+ $temp = $tableCellProperties->getAttributeNs($styleNs, 'cell-protect');
+ switch ($temp) {
+ case 'protected formula-hidden':
+ $protection['locked'] = Protection::PROTECTION_PROTECTED;
+ $protection['hidden'] = Protection::PROTECTION_PROTECTED;
+
+ break;
+ case 'formula-hidden':
+ $protection['locked'] = Protection::PROTECTION_UNPROTECTED;
+ $protection['hidden'] = Protection::PROTECTION_PROTECTED;
+
+ break;
+ case 'protected':
+ $protection['locked'] = Protection::PROTECTION_PROTECTED;
+ $protection['hidden'] = Protection::PROTECTION_UNPROTECTED;
+
+ break;
+ case 'none':
+ $protection['locked'] = Protection::PROTECTION_UNPROTECTED;
+ $protection['hidden'] = Protection::PROTECTION_UNPROTECTED;
+
+ break;
+ }
+
+ return $protection;
+ }
+
+ private const MAP_BORDER_STYLE = [ // default BORDER_THIN
+ 'none' => Border::BORDER_NONE,
+ 'hidden' => Border::BORDER_NONE,
+ 'dotted' => Border::BORDER_DOTTED,
+ 'dash-dot' => Border::BORDER_DASHDOT,
+ 'dash-dot-dot' => Border::BORDER_DASHDOTDOT,
+ 'dashed' => Border::BORDER_DASHED,
+ 'double' => Border::BORDER_DOUBLE,
+ ];
+
+ private const MAP_BORDER_MEDIUM = [
+ Border::BORDER_THIN => Border::BORDER_MEDIUM,
+ Border::BORDER_DASHDOT => Border::BORDER_MEDIUMDASHDOT,
+ Border::BORDER_DASHDOTDOT => Border::BORDER_MEDIUMDASHDOTDOT,
+ Border::BORDER_DASHED => Border::BORDER_MEDIUMDASHED,
+ ];
+
+ private const MAP_BORDER_THICK = [
+ Border::BORDER_THIN => Border::BORDER_THICK,
+ Border::BORDER_DASHDOT => Border::BORDER_MEDIUMDASHDOT,
+ Border::BORDER_DASHDOTDOT => Border::BORDER_MEDIUMDASHDOTDOT,
+ Border::BORDER_DASHED => Border::BORDER_MEDIUMDASHED,
+ ];
+
+ /** @return array{
+ * bottom?: array{borderStyle:string, color:array{rgb: string}},
+ * top?: array{borderStyle:string, color:array{rgb: string}},
+ * left?: array{borderStyle:string, color:array{rgb: string}},
+ * right?: array{borderStyle:string, color:array{rgb: string}},
+ * diagonal?: array{borderStyle:string, color:array{rgb: string}},
+ * diagonalDirection?: int,
+ * }
+ */
+ protected function getBorderStyles(DOMElement $tableCellProperties, string $fontNs, string $styleNs): array
+ {
+ $borders = [];
+ $temp = $tableCellProperties->getAttributeNs($fontNs, 'border');
+ $diagonalIndex = Borders::DIAGONAL_NONE;
+ foreach (['bottom', 'left', 'right', 'top', 'diagonal-tl-br', 'diagonal-bl-tr'] as $direction) {
+ if ($direction === 'diagonal-tl-br' || $direction === 'diagonal-bl-tr') {
+ $directionIndex = 'diagonal';
+ $temp = $tableCellProperties->getAttributeNs($styleNs, $direction);
+ } else {
+ $directionIndex = $direction;
+ $temp = $tableCellProperties->getAttributeNs($fontNs, "border-$direction");
+ }
+ if (Preg::isMatch('/^(\d+(?:[.]\d+)?)pt\s+([-\w]+)\s+#([0-9a-fA-F]{6})$/', $temp, $matches)) {
+ $style = self::MAP_BORDER_STYLE[$matches[2]] ?? Border::BORDER_THIN;
+ $width = (float) $matches[1];
+ if ($width >= 2.5) {
+ $style = self::MAP_BORDER_THICK[$style] ?? $style;
+ } elseif ($width >= 1.75) {
+ $style = self::MAP_BORDER_MEDIUM[$style] ?? $style;
+ }
+ $color = $matches[3];
+ $borders[$directionIndex] = ['borderStyle' => $style, 'color' => ['rgb' => $matches[3]]];
+ if ($direction === 'diagonal-tl-br') {
+ $diagonalIndex = Borders::DIAGONAL_DOWN;
+ } elseif ($direction === 'diagonal-bl-tr') {
+ $diagonalIndex = ($diagonalIndex === Borders::DIAGONAL_NONE) ? Borders::DIAGONAL_UP : Borders::DIAGONAL_BOTH;
+ }
+ }
+ }
+ if ($diagonalIndex !== Borders::DIAGONAL_NONE) {
+ $borders['diagonalDirection'] = $diagonalIndex;
+ }
+
+ return $borders;
+ }
+
+ protected function processSomeNumberFormats(?DOMElement $automaticStyle0, string $numberNs, string $styleNs): void
+ {
+ $automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($numberNs, 'number-style');
+ foreach ($automaticStyles as $automaticStyle) {
+ $this->processNumberNumber($automaticStyle, $numberNs, $styleNs);
+ }
+ }
+
+ protected function processNumberNumber(DOMElement $automaticStyle, string $numberNs, string $styleNs): void
+ {
+ $styleName = $automaticStyle->getAttributeNS($styleNs, 'name');
+ foreach ($automaticStyle->getElementsByTagNameNS($numberNs, 'number') as $numberNumber) {
+ $decimalPlaces = $numberNumber->getAttributeNs($numberNs, 'decimal-places');
+ $minIntegerDigits = (int) $numberNumber->getAttributeNs($numberNs, 'min-integer-digits');
+ if ($decimalPlaces === '0' && $minIntegerDigits > 1) {
+ $this->numberFormats[$styleName] = str_repeat('0', $minIntegerDigits);
+ }
+ }
+ }
+
+ private static function checkRowsRepeated(int $rowID, int $rowRepeats): void
+ {
+ if ($rowRepeats < 1 || $rowID + $rowRepeats - 1 > AddressRange::MAX_ROW) {
+ throw new Exception("Invalid number-rows-repeated $rowRepeats following row $rowID");
+ }
+ }
+
+ private static function checkColumnsRepeated(string $colID, int $colRepeats): void
+ {
+ $colIndex = Coordinate::columnIndexFromString($colID);
+ if ($colRepeats < 1 || $colIndex + $colRepeats - 1 > AddressRange::MAX_COLUMN_INT) {
+ throw new Exception("Invalid number-columns-repeated $colRepeats following column $colID");
+ }
+ }
+
+ private static function checkColumnsRepeatedInt(int $colIndex, int $colRepeats): void
+ {
+ // We don't have column string at this point,
+ // and colIndex is actually 1 less than it should be.
+ if ($colRepeats < 1 || $colIndex + $colRepeats > AddressRange::MAX_COLUMN_INT) {
+ throw new Exception("Invalid number-columns-repeated $colRepeats following column index $colIndex");
+ }
+ }
+
+ private function loadDom(string $file, ZipArchive $zip): DOMDocument
+ {
+ $dom = new DOMDocument('1.01', 'UTF-8');
+ $orig = false;
+
+ try {
+ $orig = libxml_use_internal_errors(true);
+ $result = $dom->loadXML(
+ $this->getSecurityScannerOrThrow()
+ ->scan($zip->getFromName($file))
+ );
+ if ($result === false) {
+ $fatal = false;
+ foreach (libxml_get_errors() as $err) {
+ if ($err->level === LIBXML_ERR_FATAL) {
+ throw new Exception($err->message);
+ }
+ }
+ }
+ } finally {
+ libxml_clear_errors();
+ libxml_use_internal_errors($orig);
+ }
+
+ return $dom;
+ }
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Security/XmlScanner.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Security/XmlScanner.php
index e4da44f..5d238a8 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Security/XmlScanner.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Security/XmlScanner.php
@@ -56,6 +56,11 @@ class XmlScanner
throw new Reader\Exception('UTF-7 encoding not permitted');
}
if (substr($xml, 0, Reader\Csv::UTF8_BOM_LEN) === Reader\Csv::UTF8_BOM) {
+ if (preg_match(self::ENCODING_PATTERN, $xml, $matches) === 1) {
+ if (strtolower($matches[2]) !== 'utf-8') {
+ throw new Reader\Exception("BOM says UTF-8 but encoding says {$matches[2]}");
+ }
+ }
$xml = substr($xml, Reader\Csv::UTF8_BOM_LEN);
}
@@ -64,7 +69,7 @@ class XmlScanner
private function findCharSet(string $xml): string
{
- if (substr($xml, 0, 4) === "\x4c\x6f\xa7\x94") {
+ if (str_starts_with($xml, "\x4c\x6f\xa7\x94")) {
throw new Reader\Exception('EBCDIC encoding not permitted');
}
$encoding = Reader\Csv::guessEncodingBom('', $xml);
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Slk.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Slk.php
index 73d3738..f0e8189 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Slk.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Slk.php
@@ -134,7 +134,7 @@ class Slk extends BaseReader
$worksheetInfo[0]['lastColumnIndex'] = $columnIndex;
$worksheetInfo[0]['totalRows'] = $rowIndex;
- $worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1);
+ $worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1, true);
$worksheetInfo[0]['totalColumns'] = $worksheetInfo[0]['lastColumnIndex'] + 1;
$worksheetInfo[0]['sheetState'] = Worksheet::SHEETSTATE_VISIBLE;
@@ -371,7 +371,7 @@ class Slk extends BaseReader
if (array_key_exists($char, self::STYLE_SETTINGS_FONT)) {
$styleData['font'][self::STYLE_SETTINGS_FONT[$char]] = true;
} elseif (array_key_exists($char, self::STYLE_SETTINGS_BORDER)) {
- $styleData['borders'][self::STYLE_SETTINGS_BORDER[$char]]['borderStyle'] = Border::BORDER_THIN; //* @phpstan-ignore-line
+ $styleData['borders'][self::STYLE_SETTINGS_BORDER[$char]]['borderStyle'] = Border::BORDER_THIN; //* @phpstan-ignore offsetAccess.nonOffsetAccessible (I don't know how to fix this)
} elseif ($char == 'S') {
$styleData['fill']['fillType'] = Fill::FILL_PATTERN_GRAY125;
} elseif ($char == 'M') {
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls.php
index 09a2b77..9c9397d 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls.php
@@ -2,6 +2,8 @@
namespace PhpOffice\PhpSpreadsheet\Reader;
+use Composer\Pcre\Preg;
+use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
@@ -97,6 +99,12 @@ class Xls extends XlsBase
*/
protected Worksheet $phpSheet;
+ /**
+ * Cached sheet title for the current sheet being parsed.
+ * Avoids repeated getTitle() calls in per-cell read filter checks.
+ */
+ protected string $phpSheetTitle = '';
+
/**
* BIFF version.
*/
@@ -154,7 +162,7 @@ class Xls extends XlsBase
/**
* Defined names.
*
- * @var array{isBuiltInName: int, name: string, formula: string, scope: int}
+ * @var array
*/
protected array $definedname;
@@ -378,7 +386,7 @@ class Xls extends XlsBase
// get excel data,
$ole->read($filename);
// Get workbook data: workbook stream + sheet streams
- $this->data = $ole->getStream($ole->wrkbook); // @phpstan-ignore-line
+ $this->data = $ole->getStream($ole->wrkbook) ?? '';
// Get summary information data
$this->summaryInformation = $ole->getStream($ole->summaryInformation);
// Get additional document summary information data
@@ -709,7 +717,7 @@ class Xls extends XlsBase
];
} else {
$extension = false;
- if ($cellAddress == '$B$65536') {
+ if ($cellAddress === '$B$' . AddressRange::MAX_ROW_XLS) {
// If the address row is -1 and the column is 0, (which translates as $B$65536) then this is a continuation
// note from the previous cell annotation. We're not yet handling this, so annotations longer than the
// max 2048 bytes will probably throw a wobbly.
@@ -1942,7 +1950,7 @@ class Xls extends XlsBase
$nm = self::getInt4d($recordData, 4);
$pos += 4;
- // look up limit position
+ // look up limit position (last splice offset where pos fits)
foreach ($spliceOffsets as $spliceOffset) {
// it can happen that the string is empty, therefore we need
// <= and not just <
@@ -1990,7 +1998,7 @@ class Xls extends XlsBase
// expected byte length of character array if not split
$len = ($isCompressed) ? $numChars : $numChars * 2;
- // look up limit position - Check it again to be sure that no error occurs when parsing SST structure
+ // look up limit position - find the first splice offset at or beyond current pos
$limitpos = null;
foreach ($spliceOffsets as $spliceOffset) {
// it can happen that the string is empty, therefore we need
@@ -2023,7 +2031,7 @@ class Xls extends XlsBase
// keep reading the characters
while ($charsLeft > 0) {
- // look up next limit position, in case the string span more than one continue record
+ // look up next limit position, in case the string spans more than one continue record
foreach ($spliceOffsets as $spliceOffset) {
if ($pos < $spliceOffset) {
$limitpos = $spliceOffset;
@@ -2055,25 +2063,19 @@ class Xls extends XlsBase
$retstr .= substr($recordData, $pos, $len);
$charsLeft -= $len / 2;
$isCompressed = false;
- } elseif (!$isCompressed && ($option == 0)) {
+ } elseif (!$isCompressed /*&& ($option == 0)*/) {
// 1st fragment uncompressed
// this fragment compressed
- $len = min($charsLeft, $limitpos - $pos);
- for ($j = 0; $j < $len; ++$j) {
- $retstr .= $recordData[$pos + $j]
- . chr(0);
- }
+ $len = (int) min($charsLeft, $limitpos - $pos);
+ // Pad each byte with a null byte to expand to UTF-16LE
+ $retstr .= chunk_split(substr($recordData, $pos, $len), 1, "\x00");
$charsLeft -= $len;
$isCompressed = false;
} else {
// 1st fragment compressed
// this fragment uncompressed
- $newstr = '';
- $jMax = strlen($retstr);
- for ($j = 0; $j < $jMax; ++$j) {
- $newstr .= $retstr[$j] . chr(0);
- }
- $retstr = $newstr;
+ // Pad existing compressed string bytes with null bytes
+ $retstr = chunk_split($retstr, 1, "\x00");
/** @var int */
$len = min($charsLeft * 2, $limitpos - $pos);
$retstr .= substr($recordData, $pos, $len);
@@ -2608,7 +2610,7 @@ class Xls extends XlsBase
// offset: 10; size: 2; not used
for ($i = $firstColumnIndex + 1; $i <= $lastColumnIndex + 1; ++$i) {
- if ($lastColumnIndex == 255 || $lastColumnIndex == 256) {
+ if ($lastColumnIndex == AddressRange::MAX_COLUMN_INT_XLS - 1 || $lastColumnIndex == AddressRange::MAX_COLUMN_INT) {
$this->phpSheet->getDefaultColumnDimension()->setWidth($width / 256);
break;
@@ -2724,9 +2726,10 @@ class Xls extends XlsBase
// offset: 2; size: 2; index to column
$column = self::getUInt2d($recordData, 2);
$columnString = Coordinate::stringFromColumnIndex($column + 1);
+ $cellCoordinate = $columnString . ($row + 1);
// Read cell?
- if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
+ if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
// offset: 4; size: 2; index to XF record
$xfIndex = self::getUInt2d($recordData, 4);
@@ -2734,10 +2737,10 @@ class Xls extends XlsBase
$rknum = self::getInt4d($recordData, 6);
$numValue = self::getIEEE754($rknum);
- $cell = $this->phpSheet->getCell($columnString . ($row + 1));
+ $cell = $this->phpSheet->getCell($cellCoordinate);
if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
// add style information
- $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
+ $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
}
// add cell
@@ -2768,22 +2771,28 @@ class Xls extends XlsBase
// offset: 2; size: 2; index to column
$column = self::getUInt2d($recordData, 2);
$columnString = Coordinate::stringFromColumnIndex($column + 1);
+ $cellCoordinate = $columnString . ($row + 1);
- $cell = null;
// Read cell?
- if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
+ if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
// offset: 4; size: 2; index to XF record
$xfIndex = self::getUInt2d($recordData, 4);
// offset: 6; size: 4; index to SST record
$index = self::getInt4d($recordData, 6);
+ // cache SST entry locally to avoid repeated array lookups
+ $sstValue = $this->sst[$index]['value'];
+ $fmtRuns = $this->sst[$index]['fmtRuns'];
+
// add cell
- if (($fmtRuns = $this->sst[$index]['fmtRuns']) && !$this->readDataOnly) {
+ if ($fmtRuns && !$this->readDataOnly) {
// then we should treat as rich text
$richText = new RichText();
$charPos = 0;
- $sstCount = count($this->sst[$index]['fmtRuns']);
+ $sstCount = count($fmtRuns);
+ $sstValueLength = StringHelper::countCharacters($sstValue);
+ $lastFontIndex = count($this->objFonts) - 1;
for ($i = 0; $i <= $sstCount; ++$i) {
/** @var mixed[][] $fmtRuns */
if (isset($fmtRuns[$i])) {
@@ -2791,10 +2800,10 @@ class Xls extends XlsBase
$temp = $fmtRuns[$i];
$temp = $temp['charPos'];
/** @var int $charPos */
- $text = StringHelper::substring($this->sst[$index]['value'], $charPos, $temp - $charPos);
+ $text = StringHelper::substring($sstValue, $charPos, $temp - $charPos);
$charPos = $temp;
} else {
- $text = StringHelper::substring($this->sst[$index]['value'], $charPos, StringHelper::countCharacters($this->sst[$index]['value']));
+ $text = StringHelper::substring($sstValue, $charPos, $sstValueLength);
}
if (StringHelper::countCharacters($text) > 0) {
@@ -2813,8 +2822,8 @@ class Xls extends XlsBase
$temp = $fmtRuns[$i - 1]['fontIndex'];
$fontIndex = $temp - 1;
}
- if (array_key_exists($fontIndex, $this->objFonts) === false) {
- $fontIndex = count($this->objFonts) - 1;
+ if ($fontIndex > $lastFontIndex) {
+ $fontIndex = $lastFontIndex;
}
$textRun->setFont(clone $this->objFonts[$fontIndex]);
}
@@ -2822,20 +2831,21 @@ class Xls extends XlsBase
}
}
if ($this->readEmptyCells || trim($richText->getPlainText()) !== '') {
- $cell = $this->phpSheet->getCell($columnString . ($row + 1));
+ $cell = $this->phpSheet->getCell($cellCoordinate);
+ if (isset($this->mapCellXfIndex[$xfIndex])) {
+ $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
+ }
$cell->setValueExplicit($richText, DataType::TYPE_STRING);
}
} else {
- if ($this->readEmptyCells || trim($this->sst[$index]['value']) !== '') {
- $cell = $this->phpSheet->getCell($columnString . ($row + 1));
- $cell->setValueExplicit($this->sst[$index]['value'], DataType::TYPE_STRING);
+ if ($this->readEmptyCells || trim($sstValue) !== '') {
+ $cell = $this->phpSheet->getCell($cellCoordinate);
+ if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
+ $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
+ }
+ $cell->setValueExplicit($sstValue, DataType::TYPE_STRING);
}
}
-
- if (!$this->readDataOnly && $cell !== null && isset($this->mapCellXfIndex[$xfIndex])) {
- // add style information
- $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
- }
}
}
@@ -2868,20 +2878,21 @@ class Xls extends XlsBase
// offset within record data
$offset = 4;
+ $rowIndex = $row + 1;
for ($i = 1; $i <= $columns; ++$i) {
$columnString = Coordinate::stringFromColumnIndex($colFirst + $i);
// Read cell?
- if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
+ if ($this->readFilter->readCell($columnString, $rowIndex, $this->phpSheetTitle)) {
// offset: var; size: 2; index to XF record
$xfIndex = self::getUInt2d($recordData, $offset);
// offset: var; size: 4; RK value
$numValue = self::getIEEE754(self::getInt4d($recordData, $offset + 2));
- $cell = $this->phpSheet->getCell($columnString . ($row + 1));
+ $cell = $this->phpSheet->getCell($columnString . $rowIndex);
if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
// add style
- $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
+ $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
}
// add cell value
@@ -2914,18 +2925,19 @@ class Xls extends XlsBase
// offset: 2; size 2; index to column
$column = self::getUInt2d($recordData, 2);
$columnString = Coordinate::stringFromColumnIndex($column + 1);
+ $cellCoordinate = $columnString . ($row + 1);
// Read cell?
- if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
+ if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
// offset 4; size: 2; index to XF record
$xfIndex = self::getUInt2d($recordData, 4);
$numValue = self::extractNumber(substr($recordData, 6, 8));
- $cell = $this->phpSheet->getCell($columnString . ($row + 1));
+ $cell = $this->phpSheet->getCell($cellCoordinate);
if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
// add cell style
- $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
+ $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
}
// add cell value
@@ -2955,6 +2967,7 @@ class Xls extends XlsBase
// offset: 2; size: 2; col index
$column = self::getUInt2d($recordData, 2);
$columnString = Coordinate::stringFromColumnIndex($column + 1);
+ $cellCoordinate = $columnString . ($row + 1);
// offset: 20: size: variable; formula structure
$formulaStructure = substr($recordData, 20);
@@ -2982,10 +2995,10 @@ class Xls extends XlsBase
}
// Read cell?
- if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
+ if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
if ($isPartOfSharedFormula) {
// formula is added to this cell after the sheet has been read
- $this->sharedFormulaParts[$columnString . ($row + 1)] = $this->baseCell;
+ $this->sharedFormulaParts[$cellCoordinate] = $this->baseCell;
}
// offset: 16: size: 4; not used
@@ -2994,7 +3007,9 @@ class Xls extends XlsBase
$xfIndex = self::getUInt2d($recordData, 4);
// offset: 6; size: 8; result of the formula
- if ((ord($recordData[6]) == 0) && (ord($recordData[12]) == 255) && (ord($recordData[13]) == 255)) {
+ $resultType = ord($recordData[6]);
+ $isSpecialResult = (ord($recordData[12]) == 255) && (ord($recordData[13]) == 255);
+ if (($resultType == 0) && $isSpecialResult) {
// String formula. Result follows in appended STRING record
$dataType = DataType::TYPE_STRING;
@@ -3006,27 +3021,15 @@ class Xls extends XlsBase
// read STRING record
$value = $this->readString();
- } elseif (
- (ord($recordData[6]) == 1)
- && (ord($recordData[12]) == 255)
- && (ord($recordData[13]) == 255)
- ) {
+ } elseif (($resultType == 1) && $isSpecialResult) {
// Boolean formula. Result is in +2; 0=false, 1=true
$dataType = DataType::TYPE_BOOL;
$value = (bool) ord($recordData[8]);
- } elseif (
- (ord($recordData[6]) == 2)
- && (ord($recordData[12]) == 255)
- && (ord($recordData[13]) == 255)
- ) {
+ } elseif (($resultType == 2) && $isSpecialResult) {
// Error formula. Error code is in +2
$dataType = DataType::TYPE_ERROR;
$value = Xls\ErrorCode::lookup(ord($recordData[8]));
- } elseif (
- (ord($recordData[6]) == 3)
- && (ord($recordData[12]) == 255)
- && (ord($recordData[13]) == 255)
- ) {
+ } elseif (($resultType == 3) && $isSpecialResult) {
// Formula result is a null string
$dataType = DataType::TYPE_NULL;
$value = '';
@@ -3036,10 +3039,12 @@ class Xls extends XlsBase
$value = self::extractNumber(substr($recordData, 6, 8));
}
- $cell = $this->phpSheet->getCell($columnString . ($row + 1));
+ $cell = $this->phpSheet->getCell($cellCoordinate);
if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
- // add cell style
- $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
+ // add cell style; skipping the collection update is safe here
+ // because every path below ends in setCalculatedValue(), which
+ // performs the update
+ $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
}
// store the formula
@@ -3146,9 +3151,10 @@ class Xls extends XlsBase
// offset: 2; size: 2; column index
$column = self::getUInt2d($recordData, 2);
$columnString = Coordinate::stringFromColumnIndex($column + 1);
+ $cellCoordinate = $columnString . ($row + 1);
// Read cell?
- if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
+ if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
// offset: 4; size: 2; index to XF record
$xfIndex = self::getUInt2d($recordData, 4);
@@ -3158,7 +3164,12 @@ class Xls extends XlsBase
// offset: 7; size: 1; 0=boolean; 1=error
$isError = ord($recordData[7]);
- $cell = $this->phpSheet->getCell($columnString . ($row + 1));
+ $cell = $this->phpSheet->getCell($cellCoordinate);
+ if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
+ // add cell style; a value write does not follow on every path,
+ // so use the updating setter to guarantee persistence
+ $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
+ }
switch ($isError) {
case 0: // boolean
$value = (bool) $boolErr;
@@ -3175,11 +3186,6 @@ class Xls extends XlsBase
break;
}
-
- if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
- // add cell style
- $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
- }
}
}
@@ -3208,14 +3214,17 @@ class Xls extends XlsBase
// offset: 4; size: 2 x nc; list of indexes to XF records
// add style information
if (!$this->readDataOnly && $this->readEmptyCells) {
+ $rowIndex = $row + 1;
for ($i = 0; $i < $length / 2 - 3; ++$i) {
$columnString = Coordinate::stringFromColumnIndex($fc + $i + 1);
// Read cell?
- if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
+ if ($this->readFilter->readCell($columnString, $rowIndex, $this->phpSheetTitle)) {
$xfIndex = self::getUInt2d($recordData, 4 + 2 * $i);
if (isset($this->mapCellXfIndex[$xfIndex])) {
- $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]);
+ // blank cells never receive a value write, so use the
+ // updating setter to guarantee persistence
+ $this->phpSheet->getCell($columnString . $rowIndex)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
}
}
}
@@ -3248,9 +3257,10 @@ class Xls extends XlsBase
// offset: 2; size: 2; index to column
$column = self::getUInt2d($recordData, 2);
$columnString = Coordinate::stringFromColumnIndex($column + 1);
+ $cellCoordinate = $columnString . ($row + 1);
// Read cell?
- if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
+ if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
// offset: 4; size: 2; XF index
$xfIndex = self::getUInt2d($recordData, 4);
@@ -3265,13 +3275,12 @@ class Xls extends XlsBase
}
/** @var string $value */
if ($this->readEmptyCells || trim($value) !== '') {
- $cell = $this->phpSheet->getCell($columnString . ($row + 1));
- $cell->setValueExplicit($value, DataType::TYPE_STRING);
-
+ $cell = $this->phpSheet->getCell($cellCoordinate);
if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
// add cell style
- $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
+ $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
}
+ $cell->setValueExplicit($value, DataType::TYPE_STRING);
}
}
}
@@ -3294,14 +3303,17 @@ class Xls extends XlsBase
$col = self::getUInt2d($recordData, 2);
$columnString = Coordinate::stringFromColumnIndex($col + 1);
+ $rowIndex = $row + 1;
+
// Read cell?
- if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
+ if ($this->readFilter->readCell($columnString, $rowIndex, $this->phpSheetTitle)) {
// offset: 4; size: 2; XF index
$xfIndex = self::getUInt2d($recordData, 4);
- // add style information
+ // add style information; blank cells never receive a value write,
+ // so use the updating setter to guarantee persistence
if (!$this->readDataOnly && $this->readEmptyCells && isset($this->mapCellXfIndex[$xfIndex])) {
- $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]);
+ $this->phpSheet->getCell($columnString . $rowIndex)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
}
}
}
@@ -3537,6 +3549,17 @@ class Xls extends XlsBase
}
}
+ private const REGEX_WHOLE_COLUMN = '/^([A-Z]+1\:[A-Z]+)'
+ . '(' . AddressRange::MAX_ROW_XLS_OLD . '|' . AddressRange::MAX_ROW_XLS . ')'
+ . '$/';
+ private const REGEX_WHOLE_COLUMN_REPLACE = '${1}' . AddressRange::MAX_ROW;
+ private const REGEX_WHOLE_ROW = '/^(A\d+\:)'
+ . AddressRange::MAX_COLUMN_XLS
+ . '(\d+)$/';
+ private const REGEX_WHOLE_ROW_REPLACE = '${1}'
+ . AddressRange::MAX_COLUMN
+ . '${2}';
+
/**
* Read SELECTION record. There is one such record for each pane in the sheet.
*/
@@ -3569,19 +3592,14 @@ class Xls extends XlsBase
$selectedCells = $cellRangeAddressList['cellRangeAddresses'][0];
- // first row '1' + last row '16384' indicates that full column is selected (apparently also in BIFF8!)
- if (preg_match('/^([A-Z]+1\:[A-Z]+)16384$/', $selectedCells)) {
- $selectedCells = (string) preg_replace('/^([A-Z]+1\:[A-Z]+)16384$/', '${1}1048576', $selectedCells);
- }
-
- // first row '1' + last row '65536' indicates that full column is selected
- if (preg_match('/^([A-Z]+1\:[A-Z]+)65536$/', $selectedCells)) {
- $selectedCells = (string) preg_replace('/^([A-Z]+1\:[A-Z]+)65536$/', '${1}1048576', $selectedCells);
+ // first row '1' + last row '16384' or '65536' indicates that full column is selected (apparently also in BIFF8!)
+ if (Preg::isMatch(self::REGEX_WHOLE_COLUMN, $selectedCells)) {
+ $selectedCells = Preg::replace(self::REGEX_WHOLE_COLUMN, self::REGEX_WHOLE_COLUMN_REPLACE, $selectedCells);
}
// first column 'A' + last column 'IV' indicates that full row is selected
- if (preg_match('/^(A\d+\:)IV(\d+)$/', $selectedCells)) {
- $selectedCells = (string) preg_replace('/^(A\d+\:)IV(\d+)$/', '${1}XFD${2}', $selectedCells);
+ if (Preg::isMatch(self::REGEX_WHOLE_ROW, $selectedCells)) {
+ $selectedCells = Preg::replace(self::REGEX_WHOLE_ROW, self::REGEX_WHOLE_ROW_REPLACE, $selectedCells);
}
$this->phpSheet->setSelectedCells($selectedCells);
@@ -3597,7 +3615,7 @@ class Xls extends XlsBase
StringHelper::stringIncrement($rangeBoundaries[1][0]);
for ($row = $rangeBoundaries[0][1]; $row <= $rangeBoundaries[1][1]; ++$row) {
for ($column = $rangeBoundaries[0][0]; $column != $rangeBoundaries[1][0]; StringHelper::stringIncrement($column)) {
- if ($this->getReadFilter()->readCell($column, $row, $this->phpSheet->getTitle())) {
+ if ($this->readFilter->readCell($column, $row, $this->phpSheetTitle)) {
$includeCellRange = true;
break 2;
@@ -4633,7 +4651,7 @@ class Xls extends XlsBase
$definedNameIndex = self::getUInt2d($formulaData, 1) - 1;
// offset: 2; size: 2; not used
/** @var string[] */
- $data = $this->definedname[$definedNameIndex]['name'] ?? ''; //* @phpstan-ignore-line
+ $data = $this->definedname[$definedNameIndex]['name'] ?? '';
break;
case 0x24: // single cell reference e.g. A5
@@ -4800,6 +4818,7 @@ class Xls extends XlsBase
// todo: check if we have identified the whole set of special characters
// it seems that the following characters are not accepted for sheet names
// and we may assume that they are not present: []*/:\?
+ // 'u' qualifier makes it risky to use Preg::isMatch here
if (preg_match("/[ !\"@#£$%&{()}<>=+'|^,;-]/u", $sheetRange)) {
$sheetRange = "'$sheetRange'";
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/Biff8.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/Biff8.php
index 2de9fb8..6bb8472 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/Biff8.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/Biff8.php
@@ -241,7 +241,7 @@ class Biff8 extends Xls
*/
protected static function readBIFF8CellRangeAddress(string $subData): string
{
- // todo: if cell range is just a single cell, should this funciton
+ // todo: if cell range is just a single cell, should this function
// not just return e.g. 'A1' and not 'A1:A1' ?
// offset: 0; size: 2; index to first row (0... 65535) (or offset (-32768... 32767))
@@ -297,7 +297,7 @@ class Biff8 extends Xls
[$baseCol, $baseRow] = Coordinate::indexesFromString($baseCell);
$baseCol = $baseCol - 1;
- // TODO: if cell range is just a single cell, should this funciton
+ // TODO: if cell range is just a single cell, should this function
// not just return e.g. 'A1' and not 'A1:A1' ?
// offset: 0; size: 2; first row
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php
index b5a9454..55d94af 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php
@@ -6,7 +6,6 @@ use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Reader\Xls;
-use PhpOffice\PhpSpreadsheet\Writer\Xls\Worksheet as XlsWorksheet;
class DataValidationHelper extends Xls
{
@@ -180,8 +179,8 @@ class DataValidationHelper extends Xls
$cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses'];
$maxRow = (string) AddressRange::MAX_ROW;
$maxCol = AddressRange::MAX_COLUMN;
- $maxXlsRow = (string) XlsWorksheet::MAX_XLS_ROW;
- $maxXlsColumnString = (string) XlsWorksheet::MAX_XLS_COLUMN_STRING;
+ $maxXlsRow = (string) AddressRange::MAX_ROW_XLS;
+ $maxXlsColumnString = AddressRange::MAX_COLUMN_XLS;
foreach ($cellRangeAddresses as $cellRange) {
$cellRange = preg_replace(
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/ListFunctions.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/ListFunctions.php
index 9856989..2bc472f 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/ListFunctions.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/ListFunctions.php
@@ -157,7 +157,7 @@ class ListFunctions extends Xls
}
}
- $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
+ $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1, true);
$tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1;
$worksheetInfo[] = $tmpInfo;
@@ -246,7 +246,7 @@ class ListFunctions extends Xls
$tmpInfo['dimensionsMaxR'] = $data['rwMac'];
$tmpInfo['dimensionsMinC'] = $data['colMic'];
$tmpInfo['dimensionsMaxC'] = $data['colMac'];
- $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['dimensionsMaxC']);
+ $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['dimensionsMaxC'], true);
}
}
$xls->readDefault();
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php
index 5c27b0c..10a2e5e 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php
@@ -2,6 +2,7 @@
namespace PhpOffice\PhpSpreadsheet\Reader\Xls;
+use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\NamedRange;
@@ -34,10 +35,10 @@ class LoadSpreadsheet extends Xls
$xls->spreadsheet->removeCellXfByIndex(0); // remove the default style
}
- // Read the summary information stream (containing meta data)
+ // Read the summary information stream (containing metadata)
$xls->readSummaryInformation();
- // Read the Additional document summary information stream (containing application-specific meta data)
+ // Read the Additional document summary information stream (containing application-specific metadata)
$xls->readDocumentSummaryInformation();
// total byte size of Excel data (workbook global substream + sheet substreams)
@@ -52,7 +53,7 @@ class LoadSpreadsheet extends Xls
$xls->sheets = [];
$xls->externalBooks = [];
$xls->ref = [];
- $xls->definedname = []; //* @phpstan-ignore-line
+ $xls->definedname = [];
$xls->sst = [];
$xls->drawingGroupData = '';
$xls->xfIndex = 0;
@@ -174,6 +175,7 @@ class LoadSpreadsheet extends Xls
// name in line with the formula, not the reverse
$xls->phpSheet->setTitle($sheet['name'], false, false);
$xls->phpSheet->setSheetState($sheet['sheetState']);
+ $xls->phpSheetTitle = $sheet['name'];
$xls->pos = $sheet['offset'];
@@ -450,10 +452,8 @@ class LoadSpreadsheet extends Xls
}
// calculate the width and height of the shape
- /** @var int $startRow */
- [$startColumn, $startRow] = Coordinate::coordinateFromString($spContainer->getStartCoordinates());
- /** @var int $endRow */
- [$endColumn, $endRow] = Coordinate::coordinateFromString($spContainer->getEndCoordinates());
+ [, $startRow, $startColumn] = Coordinate::indexesFromString($spContainer->getStartCoordinates());
+ [, $endRow, $endColumn] = Coordinate::indexesFromString($spContainer->getEndCoordinates());
$startOffsetX = $spContainer->getStartOffsetX();
$startOffsetY = $spContainer->getStartOffsetY();
@@ -476,7 +476,7 @@ class LoadSpreadsheet extends Xls
if (isset($xls->textObjects[$obj['idObjID']])) {
$textObject = $xls->textObjects[$obj['idObjID']];
- $xls->cellNotes[$obj['idObjID']]['objTextData'] = $textObject; //* @phpstan-ignore-line
+ $xls->cellNotes[$obj['idObjID']]['objTextData'] = $textObject; //* @phpstan-ignore offsetAccess.nonOffsetAccessible (I don't know to fix this)
}
}
@@ -490,7 +490,7 @@ class LoadSpreadsheet extends Xls
// If there is no BSE Index, we will fail here and other fields are not read.
// Fix by checking here.
// TODO: Why is there no BSE Index? Is this a new Office Version? Password protected field?
- // More likely : a uncompatible picture
+ // More likely: an incompatible picture
if (!$BSEindex) {
continue 2;
}
@@ -547,10 +547,9 @@ class LoadSpreadsheet extends Xls
// treat SHAREDFMLA records
if ($xls->version == self::XLS_BIFF8) {
foreach ($xls->sharedFormulaParts as $cell => $baseCell) {
- /** @var int $row */
- [$column, $row] = Coordinate::coordinateFromString($cell);
+ [, $row, $column] = Coordinate::indexesFromString($cell);
/** @var string $baseCell */
- if ($xls->getReadFilter()->readCell($column, $row, $xls->phpSheet->getTitle())) {
+ if ($xls->readFilter->readCell($column, $row, $xls->phpSheet->getTitle())) {
/** @var string */
$temp = $xls->sharedFormulas[$baseCell];
$formula = $xls->getFormulaFromStructure($temp, $cell);
@@ -654,7 +653,7 @@ class LoadSpreadsheet extends Xls
if ($firstColumn == 'A' && $lastColumn == 'IV') {
// then we have repeating rows
$docSheet->getPageSetup()->setRowsToRepeatAtTop([$firstRow, $lastRow]);
- } elseif ($firstRow == 1 && $lastRow == 65536) {
+ } elseif ($firstRow === 1 && $lastRow === AddressRange::MAX_ROW_XLS) {
// then we have repeating columns
$docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$firstColumn, $lastColumn]);
}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/RC4.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/RC4.php
index 663f367..a880eee 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/RC4.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/RC4.php
@@ -12,7 +12,7 @@ class RC4
protected int $j = 0;
/**
- * RC4 stream decryption/encryption constrcutor.
+ * RC4 stream decryption/encryption constructor.
*
* @param string $key Encryption key/passphrase
*/
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/XlsBase.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/XlsBase.php
index eedd185..454f0f2 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/XlsBase.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/XlsBase.php
@@ -392,7 +392,7 @@ class XlsBase extends BaseReader
{
self::confirmPos($data, $pos + 1);
- return unpack('s', $data[$pos] . $data[$pos + 1])[1]; // @phpstan-ignore-line
+ return unpack('s', $data[$pos] . $data[$pos + 1])[1]; // @phpstan-ignore offsetAccess.nonOffsetAccessible (I don't know how this statement works), return.type (ditto)
}
/**
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx.php
index 829f81f..b1de05a 100644
--- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx.php
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx.php
@@ -2,6 +2,8 @@
namespace PhpOffice\PhpSpreadsheet\Reader;
+use Composer\Pcre\Preg;
+use InvalidArgumentException;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
@@ -17,10 +19,12 @@ use PhpOffice\PhpSpreadsheet\Reader\Xlsx\DataValidations;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Hyperlinks;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\PageSetup;
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx\PivotTableReader;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Properties as PropertyReader;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SharedFormula;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViewOptions;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViews;
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Sparklines;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\TableReader;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Theme;
@@ -31,7 +35,9 @@ use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Shared\Drawing;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Shared\Font;
+use PhpOffice\PhpSpreadsheet\Shared\OLE;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
+use PhpOffice\PhpSpreadsheet\Shared\Xlsx\AgileEncryption;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Color;
use PhpOffice\PhpSpreadsheet\Style\Font as StyleFont;
@@ -52,16 +58,37 @@ class Xlsx extends BaseReader
/**
* ReferenceHelper instance.
*/
- private ReferenceHelper $referenceHelper;
+ protected ReferenceHelper $referenceHelper;
- private ZipArchive $zip;
+ protected ZipArchive $zip;
private Styles $styleReader;
/** @var SharedFormula[] */
- private array $sharedFormulae = [];
+ protected array $sharedFormulae = [];
- private bool $parseHuge = false;
+ protected bool $parseHuge = false;
+
+ private string $encryptionPassword = '';
+
+ private int $maxEncryptionSpinCount = AgileEncryption::MAX_SPIN_COUNT;
+
+ public function setEncryptionPassword(string $encryptionPassword): self
+ {
+ $this->encryptionPassword = $encryptionPassword;
+
+ return $this;
+ }
+
+ public function setMaxEncryptionSpinCount(int $maxEncryptionSpinCount): self
+ {
+ if ($maxEncryptionSpinCount < 0 || $maxEncryptionSpinCount > AgileEncryption::MAX_SPIN_COUNT) {
+ throw new InvalidArgumentException('Maximum encryption spin count must be between 0 and ' . AgileEncryption::MAX_SPIN_COUNT . '.');
+ }
+ $this->maxEncryptionSpinCount = $maxEncryptionSpinCount;
+
+ return $this;
+ }
/**
* Allow use of LIBXML_PARSEHUGE.
@@ -90,7 +117,7 @@ class Xlsx extends BaseReader
public function canRead(string $filename): bool
{
if (!File::testFileNoThrow($filename, self::INITIAL_FILE)) {
- return false;
+ return $this->hasEncryptedPackage($filename);
}
$result = false;
@@ -106,6 +133,75 @@ class Xlsx extends BaseReader
return $result;
}
+ public function load(string $filename, int $flags = 0): Spreadsheet
+ {
+ $temporaryFilename = $this->decryptToTemporaryFile($filename);
+ if ($temporaryFilename === null) {
+ return parent::load($filename, $flags);
+ }
+
+ try {
+ return parent::load($temporaryFilename, $flags);
+ } finally {
+ @unlink($temporaryFilename);
+ }
+ }
+
+ private function hasEncryptedPackage(string $filename): bool
+ {
+ try {
+ $ole = new OLE();
+ $ole->read($filename);
+
+ return $ole->hasDataByName('EncryptionInfo') && $ole->hasDataByName('EncryptedPackage');
+ } catch (Throwable) {
+ return false;
+ }
+ }
+
+ private function decryptToTemporaryFile(string $filename): ?string
+ {
+ if (File::testFileNoThrow($filename, self::INITIAL_FILE)) {
+ return null;
+ }
+
+ try {
+ $ole = new OLE();
+ $ole->read($filename);
+ $encryptionInfo = $ole->getDataByName('EncryptionInfo');
+ } catch (Throwable) {
+ return null;
+ }
+
+ $temporaryFilename = File::temporaryFilename();
+ $encryptedPackageFilename = File::temporaryFilename();
+ $encryptedPackage = fopen($encryptedPackageFilename, 'wb');
+ if ($encryptedPackage === false) {
+ @unlink($temporaryFilename);
+ @unlink($encryptedPackageFilename);
+
+ throw new Exception('Could not create decrypted XLSX package.');
+ }
+
+ try {
+ $ole->copyDataByName('EncryptedPackage', $encryptedPackage);
+ fclose($encryptedPackage);
+ $encryptedPackage = null;
+ AgileEncryption::decryptFile(AgileEncryption::parse($encryptionInfo, $this->maxEncryptionSpinCount), $encryptedPackageFilename, $temporaryFilename, $this->encryptionPassword);
+ } catch (Throwable $e) {
+ if ($encryptedPackage !== null) {
+ fclose($encryptedPackage);
+ }
+ @unlink($temporaryFilename);
+
+ throw $e;
+ } finally {
+ @unlink($encryptedPackageFilename);
+ }
+
+ return $temporaryFilename;
+ }
+
public static function testSimpleXml(mixed $value): SimpleXMLElement
{
return ($value instanceof SimpleXMLElement) ? $value : new SimpleXMLElement(' ');
@@ -179,6 +275,21 @@ class Xlsx extends BaseReader
* @return string[]
*/
public function listWorksheetNames(string $filename): array
+ {
+ $temporaryFilename = $this->decryptToTemporaryFile($filename);
+ if ($temporaryFilename === null) {
+ return $this->listWorksheetNamesFromFile($filename);
+ }
+
+ try {
+ return $this->listWorksheetNamesFromFile($temporaryFilename);
+ } finally {
+ @unlink($temporaryFilename);
+ }
+ }
+
+ /** @return string[] */
+ private function listWorksheetNamesFromFile(string $filename): array
{
File::assertFile($filename, self::INITIAL_FILE);
@@ -216,6 +327,23 @@ class Xlsx extends BaseReader
* @return array
*/
public function listWorksheetInfo(string $filename): array
+ {
+ $temporaryFilename = $this->decryptToTemporaryFile($filename);
+ if ($temporaryFilename === null) {
+ return $this->listWorksheetInfoFromFile($filename);
+ }
+
+ try {
+ return $this->listWorksheetInfoFromFile($temporaryFilename);
+ } finally {
+ @unlink($temporaryFilename);
+ }
+ }
+
+ /**
+ * @return array
+ */
+ private function listWorksheetInfoFromFile(string $filename): array
{
File::assertFile($filename, self::INITIAL_FILE);
@@ -279,22 +407,33 @@ class Xlsx extends BaseReader
$xml->setParserProperty(2, true);
$currCells = 0;
+ $currRow = 0;
while ($xml->read()) {
if ($xml->localName == 'row' && $xml->nodeType == XMLReader::ELEMENT && $xml->namespaceURI === $mainNS) {
$row = (int) $xml->getAttribute('r');
- $tmpInfo['totalRows'] = $row;
+ if ($this->readEmptyCells) {
+ $tmpInfo['totalRows'] = $row;
+ } else {
+ $currRow = $row;
+ }
$tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
$currCells = 0;
} elseif ($xml->localName == 'c' && $xml->nodeType == XMLReader::ELEMENT && $xml->namespaceURI === $mainNS) {
- $cell = $xml->getAttribute('r');
- $currCells = $cell ? max($currCells, Coordinate::indexesFromString($cell)[0]) : ($currCells + 1);
+ if ($this->readEmptyCells || !$xml->isEmptyElement) {
+ if ($currRow !== 0) {
+ $tmpInfo['totalRows'] = $currRow;
+ $currRow = 0;
+ }
+ $cell = $xml->getAttribute('r');
+ $currCells = $cell ? max($currCells, Coordinate::indexesFromString($cell)[0]) : ($currCells + 1);
+ }
}
}
$tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
$xml->close();
$tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1;
- $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
+ $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1, true);
$worksheetInfo[] = $tmpInfo;
}
@@ -307,7 +446,7 @@ class Xlsx extends BaseReader
return $worksheetInfo;
}
- private static function castToBoolean(SimpleXMLElement $c): bool
+ protected static function castToBoolean(SimpleXMLElement $c): bool
{
$value = isset($c->v) ? (string) $c->v : null;
if ($value == '0') {
@@ -319,12 +458,12 @@ class Xlsx extends BaseReader
return (bool) $c->v;
}
- private static function castToError(?SimpleXMLElement $c): ?string
+ protected static function castToError(?SimpleXMLElement $c): ?string
{
return isset($c, $c->v) ? (string) $c->v : null;
}
- private static function castToString(?SimpleXMLElement $c): ?string
+ protected static function castToString(?SimpleXMLElement $c): ?string
{
return isset($c, $c->v) ? (string) $c->v : null;
}
@@ -334,7 +473,7 @@ class Xlsx extends BaseReader
return str_replace(['_xlfn.', '_xlws.'], '', $formula);
}
- private function castToFormula(?SimpleXMLElement $c, string $r, string &$cellDataType, mixed &$value, mixed &$calculatedValue, string $castBaseType, bool $updateSharedCells = true): void
+ protected function castToFormula(?SimpleXMLElement $c, string $r, string &$cellDataType, mixed &$value, mixed &$calculatedValue, string $castBaseType, bool $updateSharedCells = true): void
{
if ($c === null) {
return;
@@ -386,7 +525,7 @@ class Xlsx extends BaseReader
return $contents !== false;
}
- private function getFromZipArchive(ZipArchive $archive, string $fileName = ''): string
+ protected function getFromZipArchive(ZipArchive $archive, string $fileName = ''): string
{
// Root-relative paths
if (str_contains($fileName, '//')) {
@@ -394,7 +533,7 @@ class Xlsx extends BaseReader
}
// Relative paths generated by dirname($filename) when $filename
// has no path (i.e.files in root of the zip archive)
- $fileName = (string) preg_replace('/^\.\//', '', $fileName);
+ $fileName = Preg::replace('/^\.\//', '', $fileName);
$fileName = File::realpath($fileName);
// Sadly, some 3rd party xlsx generators don't use consistent case for filenaming
@@ -497,12 +636,12 @@ class Xlsx extends BaseReader
$majorFonts = [];
$minorFonts = [];
$fontScheme = $xmlTheme->themeElements->fontScheme->children($drawingNS);
- $majorLatin = self::getAttributes($fontScheme->majorFont->latin)['typeface'] ?? '';
- $majorEastAsian = self::getAttributes($fontScheme->majorFont->ea)['typeface'] ?? '';
- $majorComplexScript = self::getAttributes($fontScheme->majorFont->cs)['typeface'] ?? '';
- $minorLatin = self::getAttributes($fontScheme->minorFont->latin)['typeface'] ?? '';
- $minorEastAsian = self::getAttributes($fontScheme->minorFont->ea)['typeface'] ?? '';
- $minorComplexScript = self::getAttributes($fontScheme->minorFont->cs)['typeface'] ?? '';
+ $majorLatin = (string) (self::getAttributes($fontScheme->majorFont->latin)['typeface'] ?? '');
+ $majorEastAsian = (string) (self::getAttributes($fontScheme->majorFont->ea)['typeface'] ?? '');
+ $majorComplexScript = (string) (self::getAttributes($fontScheme->majorFont->cs)['typeface'] ?? '');
+ $minorLatin = (string) (self::getAttributes($fontScheme->minorFont->latin)['typeface'] ?? '');
+ $minorEastAsian = (string) (self::getAttributes($fontScheme->minorFont->ea)['typeface'] ?? '');
+ $minorComplexScript = (string) (self::getAttributes($fontScheme->minorFont->cs)['typeface'] ?? '');
foreach ($fontScheme->majorFont->font as $xmlFont) {
$fontAttributes = self::getAttributes($xmlFont);
@@ -567,6 +706,7 @@ class Xlsx extends BaseReader
$relsWorkbook->registerXPathNamespace('rel', Namespaces::RELATIONSHIPS);
$worksheets = [];
+ $pivotCacheRels = [];
$macros = $customUI = null;
foreach ($relsWorkbook->Relationship as $elex) {
$ele = self::getAttributes($elex);
@@ -581,6 +721,10 @@ class Xlsx extends BaseReader
$worksheets[(string) $ele['Id']] = $ele['Target'];
}
+ break;
+ case Namespaces::RELATIONSHIPS_PIVOT_CACHE_DEFINITION:
+ $pivotCacheRels[(string) $ele['Id']] = File::realpath("$dir/" . (string) $ele['Target']);
+
break;
// a vbaProject ? (: some macros)
case Namespaces::VBA:
@@ -678,6 +822,16 @@ class Xlsx extends BaseReader
$objStyle = new Style();
$this->styleReader
->readStyle($objStyle, $style);
+ if (isset($xfTag->extLst)) {
+ foreach ($xfTag->extLst->ext as $extTag) {
+ $attributes = $extTag->attributes();
+ if (isset($attributes['uri'])) {
+ if ((string) $attributes['uri'] === Namespaces::STYLE_CHECKBOX_URI) {
+ $objStyle->setCheckBox(true);
+ }
+ }
+ }
+ }
foreach ($this->styleReader->getFontCharsets() as $fontName => $charset) {
$excel->addFontCharset($fontName, $charset);
}
@@ -784,6 +938,19 @@ class Xlsx extends BaseReader
$charts = $chartDetails = [];
+ // Add richData (contains relation of in-cell images)
+ $richData = [];
+ $relationsFileName = $dir . '/richData/_rels/richValueRel.xml.rels';
+ if ($zip->locateName($relationsFileName)) {
+ $relsWorksheet = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS);
+ foreach ($relsWorksheet->Relationship as $elex) {
+ $ele = self::getAttributes($elex);
+ if ($ele['Type'] == Namespaces::IMAGE) {
+ $richData['image'][(string) $ele['Id']] = (string) $ele['Target'];
+ }
+ }
+ }
+
$sheetCreated = false;
if ($xmlWorkbookNS->sheets) {
foreach ($xmlWorkbookNS->sheets->sheet as $eleSheet) {
@@ -855,167 +1022,26 @@ class Xlsx extends BaseReader
$sheetViewOptions->load($this->readDataOnly, $this->styleReader);
(new ColumnAndRowAttributes($docSheet, $xmlSheetNS))
- ->load($this->getReadFilter(), $this->readDataOnly, $this->ignoreRowsWithNoCells);
+ ->load($this->readFilter, $this->readDataOnly, $this->ignoreRowsWithNoCells);
}
$holdSelectedCells = $docSheet->getSelectedCells();
- if ($xmlSheetNS && $xmlSheetNS->sheetData && $xmlSheetNS->sheetData->row) {
- $cIndex = 1; // Cell Start from 1
- foreach ($xmlSheetNS->sheetData->row as $row) {
- $rowIndex = 1;
- foreach ($row->c as $c) {
- $cAttr = self::getAttributes($c);
- $r = (string) $cAttr['r'];
- if ($r == '') {
- $r = Coordinate::stringFromColumnIndex($rowIndex) . $cIndex;
- }
- $cellDataType = (string) $cAttr['t'];
- $originalCellDataTypeNumeric = $cellDataType === '';
- $value = null;
- $calculatedValue = null;
+ /** @var array