diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 02672eef7..fc8e81946 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Install locales run: sudo apt-get update && sudo apt-get install -y language-pack-fr language-pack-de @@ -37,7 +37,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -68,7 +68,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 2 @@ -87,7 +87,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 2 @@ -105,7 +105,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 @@ -120,7 +120,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -136,7 +136,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 @@ -151,7 +151,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -167,7 +167,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 @@ -182,7 +182,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -198,7 +198,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 @@ -213,7 +213,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -231,7 +231,7 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: ${{ github.ref }} # Otherwise our annotated tag is not fetched and we cannot get correct version diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index e8d323405..29efca2d8 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -26,6 +26,8 @@ class Functions const RETURNDATE_PHP_DATETIME_OBJECT = 'O'; const RETURNDATE_EXCEL = 'E'; + public const NOT_YET_IMPLEMENTED = '#Not Yet Implemented'; + /** * Compatibility mode to use for error checking and responses. */ diff --git a/src/PhpSpreadsheet/Calculation/Web/Service.php b/src/PhpSpreadsheet/Calculation/Web/Service.php index fc849075c..656df4ca2 100644 --- a/src/PhpSpreadsheet/Calculation/Web/Service.php +++ b/src/PhpSpreadsheet/Calculation/Web/Service.php @@ -46,11 +46,12 @@ class Service } $host = $parsed['host'] ?? ''; if (!in_array($host, $domainWhiteList, true)) { - return ($cell === null) ? null : '#Not Yet Implemented'; // will be converted to oldCalculatedValue or null + return ($cell === null) ? null : Functions::NOT_YET_IMPLEMENTED; // will be converted to oldCalculatedValue or null } // Get results from the the webservice $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', ], ]; @@ -63,7 +64,7 @@ class Service return ExcelError::VALUE(); // Output not a string or too long } - return $output; + return ($output === '') ? Functions::NOT_YET_IMPLEMENTED : $output; } /** diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index e57a07f49..b350d18f8 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -58,6 +58,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. */ @@ -66,6 +74,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; } /** @@ -177,7 +199,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; } diff --git a/src/PhpSpreadsheet/Shared/OLERead.php b/src/PhpSpreadsheet/Shared/OLERead.php index 645dbf779..ff894a78e 100644 --- a/src/PhpSpreadsheet/Shared/OLERead.php +++ b/src/PhpSpreadsheet/Shared/OLERead.php @@ -60,6 +60,9 @@ class OLERead private array $props = []; + /** @var int[] */ + private array $possibleLoop = []; + /** * Read the file. */ @@ -136,7 +139,9 @@ class OLERead $sbdBlock = $this->sbdStartBlock; $this->smallBlockChain = ''; + $this->possibleLoop = []; while ($sbdBlock != -2) { + $this->catchLoop($sbdBlock); $pos = ($sbdBlock + 1) * self::BIG_BLOCK_SIZE; $this->smallBlockChain .= substr($this->data, $pos, 4 * $bbs); @@ -152,6 +157,14 @@ class OLERead $this->readPropertySets(); } + private function catchLoop(int $sbdBlock): void + { + if (in_array($sbdBlock, $this->possibleLoop, true)) { + throw new ReaderException('Detected loop while iterating blocks'); + } + $this->possibleLoop[] = $sbdBlock; + } + /** * Extract binary stream data. */ @@ -168,7 +181,9 @@ class OLERead $block = $this->props[$stream]['startBlock']; + $this->possibleLoop = []; while ($block != -2) { + $this->catchLoop($block); $pos = $block * self::SMALL_BLOCK_SIZE; $streamData .= substr($rootdata, $pos, self::SMALL_BLOCK_SIZE); @@ -188,7 +203,9 @@ class OLERead $block = $this->props[$stream]['startBlock']; + $this->possibleLoop = []; while ($block != -2) { + $this->catchLoop($block); $pos = ($block + 1) * self::BIG_BLOCK_SIZE; $streamData .= substr($this->data, $pos, self::BIG_BLOCK_SIZE); $block = self::getInt4d($this->bigBlockChain, $block * 4); @@ -208,7 +225,9 @@ class OLERead { $data = ''; + $this->possibleLoop = []; while ($block != -2) { + $this->catchLoop($block); $pos = ($block + 1) * self::BIG_BLOCK_SIZE; $data .= substr($this->data, $pos, self::BIG_BLOCK_SIZE); $block = self::getInt4d($this->bigBlockChain, $block * 4); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Web/ServerTestx.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Web/ServerTestx.php new file mode 100644 index 000000000..531888850 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Web/ServerTestx.php @@ -0,0 +1,69 @@ +start(); + while (!self::$httpServer->isRunning()) { + usleep(1000); + } + } + + public static function tearDownAfterClass(): void + { + self::$httpServer->stop(); + } + + public function testServer(): void + { + self::assertSame(self::NEWVALUE, file_get_contents(self::DIRECT)); + self::assertSame(self::NEWVALUE, file_get_contents(self::REDIRECT)); + } + + public function testReadFile(): void + { + $reader = new XlsxReader(); + $spreadsheet = $reader->load(__DIR__ . '/redirect.xlsx'); + $spreadsheet->setDomainWhiteList(['localhost']); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(self::OLDVALUE, $sheet->getCell('A1')->getOldCalculatedValue()); + self::assertSame(self::OLDVALUE, $sheet->getCell('A2')->getOldCalculatedValue()); + self::assertSame(self::NEWVALUE, $sheet->getCell('A1')->getCalculatedValue(), 'no redirect so recomputed'); + self::assertSame(self::OLDVALUE, $sheet->getCell('A2')->getCalculatedValue(), 'redirect so use old computed'); + } + + public static function testNew(): void + { + $spreadsheet = new Spreadsheet(); + $spreadsheet->setDomainWhiteList(['localhost']); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('G1')->setValue(self::DIRECT); + $sheet->getCell('A1')->setValue('=WEBSERVICE(G1)'); + self::assertSame(self::NEWVALUE, $sheet->getCell('A1')->getCalculatedValue(), 'no redirect so computed'); + $sheet->getCell('G2')->setValue(self::REDIRECT); + $sheet->getCell('A2')->setValue('=WEBSERVICE(G2)'); + self::assertNull( + $sheet->getCell('A2')->getCalculatedValue(), + 'redirect so not computed' + ); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Web/direct.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Web/direct.php new file mode 100644 index 000000000..002250d82 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Web/direct.php @@ -0,0 +1,5 @@ +canRead(self::FILENAME)); + } + + public function testNotEnoughMemoryCanRead(): void + { + $reader = new Gnumeric(); + $reader->setMaxLength(64 * 1024 * 1024); + self::assertFalse($reader->canRead(self::FILENAME)); + } + + public function testNotEnoughMemoryListNames(): void + { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('invalid Gnumeric file'); + $reader = new Gnumeric(); + $reader->setMaxLength(64 * 1024 * 1024); + $reader->listWorksheetNames(self::FILENAME); + } + + public function testNotEnoughMemoryListInfo(): void + { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('invalid Gnumeric file'); + $reader = new Gnumeric(); + $reader->setMaxLength(64 * 1024 * 1024); + $reader->listWorksheetInfo(self::FILENAME); + } + + public function testNotEnoughMemoryLoad(): void + { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('invalid Gnumeric file'); + $reader = new Gnumeric(); + $reader->setMaxLength(64 * 1024 * 1024); + $reader->load(self::FILENAME); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/OleLoopTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/OleLoopTest.php new file mode 100644 index 000000000..64c301f2d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/OleLoopTest.php @@ -0,0 +1,42 @@ +canRead(self::FILENAME)); + } + + public function testDetectLoopListNames(): void + { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('Detected loop'); + $reader = new XlsReader(); + $reader->listWorksheetNames(self::FILENAME); + } + + public function testDetectLoopListInfo(): void + { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('Detected loop'); + $reader = new XlsReader(); + $reader->listWorksheetInfo(self::FILENAME); + } + + public function testDetectLoopLoad(): void + { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('Detected loop'); + $reader = new XlsReader(); + $reader->load(self::FILENAME); + } +} diff --git a/tests/data/Reader/Gnumeric/gzbomb.gnumeric b/tests/data/Reader/Gnumeric/gzbomb.gnumeric new file mode 100644 index 000000000..c705f0c14 Binary files /dev/null and b/tests/data/Reader/Gnumeric/gzbomb.gnumeric differ diff --git a/tests/data/Reader/XLS/oleloop.xls b/tests/data/Reader/XLS/oleloop.xls new file mode 100644 index 000000000..4df845c18 Binary files /dev/null and b/tests/data/Reader/XLS/oleloop.xls differ