mapper = $mapper; $this->crypto = $crypto; $this->config = $config; $this->db = $db; $this->logger = $logger; $this->time = $time; $this->cache = $cacheFactory->isLocalCacheAvailable() ? $cacheFactory->createLocal('authtoken_') : $cacheFactory->createInMemory(); $this->hasher = $hasher; } /** * {@inheritDoc} */ public function generateToken(string $token, string $uid, string $loginName, ?string $password, string $name, int $type = OCPIToken::TEMPORARY_TOKEN, int $remember = OCPIToken::DO_NOT_REMEMBER, ?array $scope = null, ): OCPIToken { if (strlen($token) < self::TOKEN_MIN_LENGTH) { $exception = new InvalidTokenException('Token is too short, minimum of ' . self::TOKEN_MIN_LENGTH . ' characters is required, ' . strlen($token) . ' characters given'); $this->logger->error('Invalid token provided when generating new token', ['exception' => $exception]); throw $exception; } if (mb_strlen($name) > 128) { $name = mb_substr($name, 0, 120) . '…'; } // We need to check against one old token to see if there is a password // hash that we can reuse for detecting outdated passwords $randomOldToken = $this->mapper->getFirstTokenForUser($uid); $oldTokenMatches = $randomOldToken && $randomOldToken->getPasswordHash() && $password !== null && $this->hasher->verify(sha1($password) . $password, $randomOldToken->getPasswordHash()); $dbToken = $this->newToken($token, $uid, $loginName, $password, $name, $type, $remember); if ($oldTokenMatches) { $dbToken->setPasswordHash($randomOldToken->getPasswordHash()); } if ($scope !== null) { $dbToken->setScope($scope); } $this->mapper->insert($dbToken); if (!$oldTokenMatches && $password !== null) { $this->updatePasswords($uid, $password); } // Add the token to the cache $this->cacheToken($dbToken); return $dbToken; } public function getToken(string $tokenId): OCPIToken { /** * Token length: 72 * @see \OC\Core\Controller\ClientFlowLoginController::generateAppPassword * @see \OC\Core\Controller\AppPasswordController::getAppPassword * @see \OC\Core\Command\User\AddAppPassword::execute * @see \OC\Core\Service\LoginFlowV2Service::flowDone * @see \OCA\Talk\MatterbridgeManager::generatePassword * @see \OCA\Preferred_Providers\Controller\PasswordController::generateAppPassword * @see \OCA\GlobalSiteSelector\TokenHandler::generateAppPassword * * Token length: 22-256 - https://www.php.net/manual/en/session.configuration.php#ini.session.sid-length * @see \OC\User\Session::createSessionToken * * Token length: 29 * @see \OCA\Settings\Controller\AuthSettingsController::generateRandomDeviceToken * @see \OCA\Registration\Service\RegistrationService::generateAppPassword */ if (strlen($tokenId) < self::TOKEN_MIN_LENGTH) { throw new InvalidTokenException('Token is too short for a generated token, should be the password during basic auth'); } $tokenHash = $this->hashToken($tokenId); if ($token = $this->getTokenFromCache($tokenHash)) { $this->checkToken($token); return $token; } try { $token = $this->mapper->getToken($tokenHash); $this->cacheToken($token); } catch (DoesNotExistException $ex) { try { $token = $this->mapper->getToken($this->hashTokenWithEmptySecret($tokenId)); $this->rotate($token, $tokenId, $tokenId); } catch (DoesNotExistException) { $this->cacheInvalidHash($tokenHash); throw new InvalidTokenException('Token does not exist: ' . $ex->getMessage(), 0, $ex); } } $this->checkToken($token); return $token; } /** * @throws InvalidTokenException when token doesn't exist */ private function getTokenFromCache(string $tokenHash): ?PublicKeyToken { $serializedToken = $this->cache->get($tokenHash); if ($serializedToken === false) { return null; } if ($serializedToken === null) { return null; } $token = unserialize($serializedToken, [ 'allowed_classes' => [PublicKeyToken::class], ]); return $token instanceof PublicKeyToken ? $token : null; } private function cacheToken(PublicKeyToken $token): void { $this->cache->set($token->getToken(), serialize($token), self::TOKEN_CACHE_TTL); } private function cacheInvalidHash(string $tokenHash): void { // Invalid entries can be kept longer in cache since it’s unlikely to reuse them $this->cache->set($tokenHash, false, self::TOKEN_CACHE_TTL * 2); } public function getTokenById(int $tokenId): OCPIToken { try { $token = $this->mapper->getTokenById($tokenId); } catch (DoesNotExistException $ex) { throw new InvalidTokenException("Token with ID $tokenId does not exist: " . $ex->getMessage(), 0, $ex); } $this->checkToken($token); return $token; } private function checkToken($token): void { if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) { throw new ExpiredTokenException($token); } if ($token->getType() === OCPIToken::WIPE_TOKEN) { throw new WipeTokenException($token); } if ($token->getPasswordInvalid() === true) { //The password is invalid we should throw an TokenPasswordExpiredException throw new TokenPasswordExpiredException($token); } } public function renewSessionToken(string $oldSessionId, string $sessionId): OCPIToken { return $this->atomic(function () use ($oldSessionId, $sessionId) { $token = $this->getToken($oldSessionId); if (!($token instanceof PublicKeyToken)) { throw new InvalidTokenException('Invalid token type'); } $password = null; if (!is_null($token->getPassword())) { $privateKey = $this->decrypt($token->getPrivateKey(), $oldSessionId); $password = $this->decryptPassword($token->getPassword(), $privateKey); } $scope = $token->getScope() === '' ? null : $token->getScopeAsArray(); $newToken = $this->generateToken( $sessionId, $token->getUID(), $token->getLoginName(), $password, $token->getName(), OCPIToken::TEMPORARY_TOKEN, $token->getRemember(), $scope, ); $this->cacheToken($newToken); $this->cacheInvalidHash($token->getToken()); $this->mapper->delete($token); return $newToken; }, $this->db); } public function invalidateToken(string $token) { $tokenHash = $this->hashToken($token); $this->mapper->invalidate($this->hashToken($token)); $this->mapper->invalidate($this->hashTokenWithEmptySecret($token)); $this->cacheInvalidHash($tokenHash); } public function invalidateTokenById(string $uid, int $id) { $token = $this->mapper->getTokenById($id); if ($token->getUID() !== $uid) { return; } $this->mapper->invalidate($token->getToken()); $this->cacheInvalidHash($token->getToken()); } public function invalidateOldTokens() { $olderThan = $this->time->getTime() - $this->config->getSystemValueInt('session_lifetime', 60 * 60 * 24); $this->logger->debug('Invalidating session tokens older than ' . date('c', $olderThan), ['app' => 'cron']); $this->mapper->invalidateOld($olderThan, OCPIToken::TEMPORARY_TOKEN, OCPIToken::DO_NOT_REMEMBER); $rememberThreshold = $this->time->getTime() - $this->config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15); $this->logger->debug('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold), ['app' => 'cron']); $this->mapper->invalidateOld($rememberThreshold, OCPIToken::TEMPORARY_TOKEN, OCPIToken::REMEMBER); $wipeThreshold = $this->time->getTime() - $this->config->getSystemValueInt('token_auth_wipe_token_retention', 60 * 60 * 24 * 60); $this->logger->debug('Invalidating auth tokens marked for remote wipe older than ' . date('c', $wipeThreshold), ['app' => 'cron']); $this->mapper->invalidateOld($wipeThreshold, OCPIToken::WIPE_TOKEN); $authTokenThreshold = $this->time->getTime() - $this->config->getSystemValueInt('token_auth_token_retention', 60 * 60 * 24 * 365); $this->logger->debug('Invalidating auth tokens older than ' . date('c', $authTokenThreshold), ['app' => 'cron']); $this->mapper->invalidateOld($authTokenThreshold, OCPIToken::PERMANENT_TOKEN); } public function invalidateLastUsedBefore(string $uid, int $before): void { $this->mapper->invalidateLastUsedBefore($uid, $before); } public function updateToken(OCPIToken $token) { if (!($token instanceof PublicKeyToken)) { throw new InvalidTokenException('Invalid token type'); } $this->mapper->update($token); $this->cacheToken($token); } public function updateTokenActivity(OCPIToken $token) { if (!($token instanceof PublicKeyToken)) { throw new InvalidTokenException('Invalid token type'); } $activityInterval = $this->config->getSystemValueInt('token_auth_activity_update', 60); $activityInterval = min(max($activityInterval, 0), 300); /** @var PublicKeyToken $token */ $now = $this->time->getTime(); if ($token->getLastActivity() < ($now - $activityInterval)) { $token->setLastActivity($now); $this->mapper->updateActivity($token, $now); $this->cacheToken($token); } } public function getTokenByUser(string $uid): array { return $this->mapper->getTokenByUser($uid); } public function getPassword(OCPIToken $savedToken, string $tokenId): string { if (!($savedToken instanceof PublicKeyToken)) { throw new InvalidTokenException('Invalid token type'); } if ($savedToken->getPassword() === null) { throw new PasswordlessTokenException(); } // Decrypt private key with tokenId $privateKey = $this->decrypt($savedToken->getPrivateKey(), $tokenId); // Decrypt password with private key return $this->decryptPassword($savedToken->getPassword(), $privateKey); } public function setPassword(OCPIToken $token, string $tokenId, string $password) { if (!($token instanceof PublicKeyToken)) { throw new InvalidTokenException('Invalid token type'); } $this->atomic(function () use ($password, $token) { // When changing passwords all temp tokens are deleted $this->mapper->deleteTempToken($token); // Update the password for all tokens $tokens = $this->mapper->getTokenByUser($token->getUID()); $hashedPassword = $this->hashPassword($password); foreach ($tokens as $t) { $publicKey = $t->getPublicKey(); $t->setPassword($this->encryptPassword($password, $publicKey)); $t->setPasswordHash($hashedPassword); $this->updateToken($t); } }, $this->db); } private function hashPassword(string $password): string { return $this->hasher->hash(sha1($password) . $password); } public function rotate(OCPIToken $token, string $oldTokenId, string $newTokenId): OCPIToken { if (!($token instanceof PublicKeyToken)) { throw new InvalidTokenException('Invalid token type'); } // Decrypt private key with oldTokenId $privateKey = $this->decrypt($token->getPrivateKey(), $oldTokenId); // Encrypt with the new token $token->setPrivateKey($this->encrypt($privateKey, $newTokenId)); $token->setToken($this->hashToken($newTokenId)); $this->updateToken($token); return $token; } private function encrypt(string $plaintext, string $token): string { $secret = $this->config->getSystemValueString('secret'); return $this->crypto->encrypt($plaintext, $token . $secret); } /** * @throws InvalidTokenException */ private function decrypt(string $cipherText, string $token): string { $secret = $this->config->getSystemValueString('secret'); try { return $this->crypto->decrypt($cipherText, $token . $secret); } catch (\Exception $ex) { // Retry with empty secret as a fallback for instances where the secret might not have been set by accident try { return $this->crypto->decrypt($cipherText, $token); } catch (\Exception $ex2) { // Delete the invalid token $this->invalidateToken($token); throw new InvalidTokenException('Could not decrypt token password: ' . $ex->getMessage(), 0, $ex2); } } } private function encryptPassword(string $password, string $publicKey): string { openssl_public_encrypt($password, $encryptedPassword, $publicKey, OPENSSL_PKCS1_OAEP_PADDING); $encryptedPassword = base64_encode($encryptedPassword); return $encryptedPassword; } private function decryptPassword(string $encryptedPassword, string $privateKey): string { $encryptedPassword = base64_decode($encryptedPassword); openssl_private_decrypt($encryptedPassword, $password, $privateKey, OPENSSL_PKCS1_OAEP_PADDING); return $password; } private function hashToken(string $token): string { $secret = $this->config->getSystemValueString('secret'); return hash('sha512', $token . $secret); } /** * @deprecated 26.0.0 Fallback for instances where the secret might not have been set by accident */ private function hashTokenWithEmptySecret(string $token): string { return hash('sha512', $token); } /** * @throws \RuntimeException when OpenSSL reports a problem */ private function newToken(string $token, string $uid, string $loginName, $password, string $name, int $type, int $remember): PublicKeyToken { $dbToken = new PublicKeyToken(); $dbToken->setUid($uid); $dbToken->setLoginName($loginName); $config = array_merge([ 'digest_alg' => 'sha512', 'private_key_bits' => $password !== null && strlen($password) > 250 ? 4096 : 2048, ], $this->config->getSystemValue('openssl', [])); // Generate new key $res = openssl_pkey_new($config); if ($res === false) { $this->logOpensslError(); throw new \RuntimeException('OpenSSL reported a problem'); } if (openssl_pkey_export($res, $privateKey, null, $config) === false) { $this->logOpensslError(); throw new \RuntimeException('OpenSSL reported a problem'); } // Extract the public key from $res to $pubKey $publicKey = openssl_pkey_get_details($res); $publicKey = $publicKey['key']; $dbToken->setPublicKey($publicKey); $dbToken->setPrivateKey($this->encrypt($privateKey, $token)); if (!is_null($password) && $this->config->getSystemValueBool('auth.storeCryptedPassword', true)) { if (strlen($password) > IUserManager::MAX_PASSWORD_LENGTH) { throw new \RuntimeException('Trying to save a password with more than 469 characters is not supported. If you want to use big passwords, disable the auth.storeCryptedPassword option in config.php'); } $dbToken->setPassword($this->encryptPassword($password, $publicKey)); $dbToken->setPasswordHash($this->hashPassword($password)); } $dbToken->setName($name); $dbToken->setToken($this->hashToken($token)); $dbToken->setType($type); $dbToken->setRemember($remember); $dbToken->setLastActivity($this->time->getTime()); $dbToken->setLastCheck($this->time->getTime()); $dbToken->setVersion(PublicKeyToken::VERSION); return $dbToken; } public function markPasswordInvalid(OCPIToken $token, string $tokenId) { if (!($token instanceof PublicKeyToken)) { throw new InvalidTokenException('Invalid token type'); } $token->setPasswordInvalid(true); $this->mapper->update($token); $this->cacheToken($token); } public function updatePasswords(string $uid, string $password) { // prevent setting an empty pw as result of pw-less-login if ($password === '' || !$this->config->getSystemValueBool('auth.storeCryptedPassword', true)) { return; } $this->atomic(function () use ($password, $uid) { // Update the password for all tokens $tokens = $this->mapper->getTokenByUser($uid); $newPasswordHash = null; /** * - true: The password hash could not be verified anymore * and the token needs to be updated with the newly encrypted password * - false: The hash could still be verified * - missing: The hash needs to be verified */ $hashNeedsUpdate = []; foreach ($tokens as $t) { if (!isset($hashNeedsUpdate[$t->getPasswordHash()])) { if ($t->getPasswordHash() === null) { $hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true; } elseif (!$this->hasher->verify(sha1($password) . $password, $t->getPasswordHash())) { $hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true; } else { $hashNeedsUpdate[$t->getPasswordHash() ?: ''] = false; } } $needsUpdating = $hashNeedsUpdate[$t->getPasswordHash() ?: ''] ?? true; if ($needsUpdating) { if ($newPasswordHash === null) { $newPasswordHash = $this->hashPassword($password); } $publicKey = $t->getPublicKey(); $t->setPassword($this->encryptPassword($password, $publicKey)); $t->setPasswordHash($newPasswordHash); $t->setPasswordInvalid(false); $this->updateToken($t); } } // If password hashes are different we update them all to be equal so // that the next execution only needs to verify once if (count($hashNeedsUpdate) > 1) { $newPasswordHash = $this->hashPassword($password); $this->mapper->updateHashesForUser($uid, $newPasswordHash); } }, $this->db); } private function logOpensslError() { $errors = []; while ($error = openssl_error_string()) { $errors[] = $error; } $this->logger->critical('Something is wrong with your openssl setup: ' . implode(', ', $errors)); } } color: #555555 } /* Name.Decorator */ .highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */ .highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */ .highlight .nl { color: #336699; font-style: italic } /* Name.Label */ .highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */ .highlight .py { color: #336699; font-weight: bold } /* Name.Property */ .highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */ .highlight .nv { color: #336699 } /* Name.Variable */ .highlight .ow { color: #008800 } /* Operator.Word */ .highlight .w { color: #bbbbbb } /* Text.Whitespace */ .highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */ .highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */ .highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */ .highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ .highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */ .highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */ .highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */ .highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */ .highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */ .highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */ .highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */ .highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ .highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ .highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */ .highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ .highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- $Id$ -->
<testcase>
<info>
<p>
This test checks tables with collapse border model. Tables of two rows one column.
</p>
</info>
<fo>
<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:svg="http://www.w3.org/2000/svg">
<fo:layout-master-set>
<fo:simple-page-master master-name="normal" page-width="15cm" page-height="15cm" margin="20pt">
<fo:region-body margin="0pt"/>
</fo:simple-page-master>
</fo:layout-master-set>
<fo:page-sequence master-reference="normal">
<fo:flow flow-name="xsl-region-body">
<fo:block>Before the tables</fo:block>
<!-- table 1 -->
<fo:table table-layout="fixed" width="100pt" border-collapse="collapse">
<fo:table-column column-width="proportional-column-width(1)"/>
<fo:table-body>
<fo:table-row>
<fo:table-cell border="4pt solid black">
<fo:block>Cell 1.1</fo:block>
</fo:table-cell>
</fo:table-row>
<fo:table-row>
<fo:table-cell border="6pt solid blue">
<fo:block>Cell 2.1</fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-body>
</fo:table>
<fo:block>Between tables</fo:block>
<!-- table 2 -->
<fo:table table-layout="fixed" width="100pt" border-collapse="collapse">
<fo:table-column column-width="proportional-column-width(1)"/>
<fo:table-body>
<fo:table-row>
<fo:table-cell border="6pt solid black">
<fo:block>Cell 1.1</fo:block>
</fo:table-cell>
</fo:table-row>
<fo:table-row>
<fo:table-cell border="4pt solid blue">
<fo:block>Cell 2.1 Cell 2.1 Cell 2.1 Cell 2.1</fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-body>
</fo:table>
<fo:block>Between tables</fo:block>
<!-- table 3 -->
<fo:table table-layout="fixed" width="100pt" border-collapse="collapse">
<fo:table-column column-width="proportional-column-width(1)"/>
<fo:table-body>
<fo:table-row>
<fo:table-cell
border-top="2pt solid black"
border-bottom="6pt solid blue"
border-left="8pt solid red"
border-right="2pt solid orange">
<fo:block>Cell 1.1</fo:block>
</fo:table-cell>
</fo:table-row>
<fo:table-row>
<fo:table-cell
border-top="4pt solid gray"
border-bottom="12pt solid navy"
border-left="6pt solid purple"
border-right="10pt solid yellow">
<fo:block>cell 2.1</fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-body>
</fo:table>
<fo:block>Between tables</fo:block>
<!-- table 4 -->
<fo:table table-layout="fixed" width="100pt" border-collapse="collapse">
<fo:table-column column-width="proportional-column-width(1)"/>
<fo:table-body>
<fo:table-row>
<fo:table-cell
border-top="12pt solid black"
border-bottom="6pt solid blue"
border-left="2pt solid red"
border-right="10pt solid orange">
<fo:block>Cell 1.1 Cell 1.1 Cell 1.1 Cell 1.1</fo:block>
</fo:table-cell>
</fo:table-row>
<fo:table-row>
<fo:table-cell
border-top="8pt solid gray"
border-bottom="2pt solid navy"
border-left="8pt solid purple"
border-right="6pt solid yellow">
<fo:block>cell 2.1</fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-body>
</fo:table>
<fo:block>After the tables</fo:block>
</fo:flow>
</fo:page-sequence>
</fo:root>
</fo>
<checks>
<!-- table 1 -->
<eval expected="100000" xpath="//flow/block[2]/@ipd"/>
<eval expected="100000" xpath="//flow/block[2]/@ipda"/>
<eval expected="39800" xpath="//flow/block[2]/@bpd"/>
<eval expected="39800" xpath="//flow/block[2]/@bpda"/>
<!-- row 1 cell 1 -->
<eval expected="96000" xpath="//flow/block[2]/block[1]/@ipd"/>
<eval expected="104000" xpath="//flow/block[2]/block[1]/@ipda"/>
<eval expected="14400" xpath="//flow/block[2]/block[1]/@bpd"/>
<eval expected="24400" xpath="//flow/block[2]/block[1]/@bpda"/>
<eval expected="2000" xpath="//flow/block[2]/block[1]/@left-offset"/>
<eval expected="-2000" xpath="//flow/block[2]/block[1]/@top-offset"/>
<eval expected="(solid,#000000,4000,collapse-outer)" xpath="//flow/block[2]/block[1]/@border-before"/>
<eval expected="(solid,#0000ff,6000,collapse-inner)" xpath="//flow/block[2]/block[1]/@border-after"/>
<eval expected="(solid,#000000,4000,collapse-outer)" xpath="//flow/block[2]/block[1]/@border-start"/>
<eval expected="(solid,#000000,4000,collapse-outer)" xpath="//flow/block[2]/block[1]/@border-end"/>
<!-- row 2 cell 1 -->
<eval expected="94000" xpath="//flow/block[2]/block[2]/@ipd"/>
<eval expected="106000" xpath="//flow/block[2]/block[2]/@ipda"/>
<eval expected="14400" xpath="//flow/block[2]/block[2]/@bpd"/>
<eval expected="26400" xpath="//flow/block[2]/block[2]/@bpda"/>
<eval expected="3000" xpath="//flow/block[2]/block[2]/@left-offset"/>
<eval expected="16400" xpath="//flow/block[2]/block[2]/@top-offset"/>
<eval expected="(solid,#0000ff,6000,collapse-inner)" xpath="//flow/block[2]/block[2]/@border-before"/>
<eval expected="(solid,#0000ff,6000,collapse-outer)" xpath="//flow/block[2]/block[2]/@border-after"/>
<eval expected="(solid,#0000ff,6000,collapse-outer)" xpath="//flow/block[2]/block[2]/@border-start"/>
<eval expected="(solid,#0000ff,6000,collapse-outer)" xpath="//flow/block[2]/block[2]/@border-end"/>
<!-- table 2 -->
<eval expected="100000" xpath="//flow/block[4]/@ipd"/>
<eval expected="100000" xpath="//flow/block[4]/@ipda"/>
<eval expected="54200" xpath="//flow/block[4]/@bpd"/>
<eval expected="54200" xpath="//flow/block[4]/@bpda"/>
<!-- row 1 cell 1 -->
<eval expected="94000" xpath="//flow/block[4]/block[1]/@ipd"/>
<eval expected="106000" xpath="//flow/block[4]/block[1]/@ipda"/>
<eval expected="14400" xpath="//flow/block[4]/block[1]/@bpd"/>
<eval expected="26400" xpath="//flow/block[4]/block[1]/@bpda"/>
<eval expected="3000" xpath="//flow/block[4]/block[1]/@left-offset"/>
<eval expected="-3000" xpath="//flow/block[4]/block[1]/@top-offset"/>
<eval expected="(solid,#000000,6000,collapse-outer)" xpath="//flow/block[4]/block[1]/@border-before"/>
<eval expected="(solid,#000000,6000,collapse-inner)" xpath="//flow/block[4]/block[1]/@border-after"/>
<eval expected="(solid,#000000,6000,collapse-outer)" xpath="//flow/block[4]/block[1]/@border-start"/>
<eval expected="(solid,#000000,6000,collapse-outer)" xpath="//flow/block[4]/block[1]/@border-end"/>
<!-- row 2 cell 1 -->
<eval expected="96000" xpath="//flow/block[4]/block[2]/@ipd"/>
<eval expected="104000" xpath="//flow/block[4]/block[2]/@ipda"/>
<eval expected="28800" xpath="//flow/block[4]/block[2]/@bpd"/>
<eval expected="38800" xpath="//flow/block[4]/block[2]/@bpda"/>
<eval expected="2000" xpath="//flow/block[4]/block[2]/@left-offset"/>
<eval expected="17400" xpath="//flow/block[4]/block[2]/@top-offset"/>
<eval expected="(solid,#000000,6000,collapse-inner)" xpath="//flow/block[4]/block[2]/@border-before"/>
<eval expected="(solid,#0000ff,4000,collapse-outer)" xpath="//flow/block[4]/block[2]/@border-after"/>
<eval expected="(solid,#0000ff,4000,collapse-outer)" xpath="//flow/block[4]/block[2]/@border-start"/>
<eval expected="(solid,#0000ff,4000,collapse-outer)" xpath="//flow/block[4]/block[2]/@border-end"/>
<!-- table 3 -->
<eval expected="100000" xpath="//flow/block[6]/@ipd"/>
<eval expected="100000" xpath="//flow/block[6]/@ipda"/>
<eval expected="41800" xpath="//flow/block[6]/@bpd"/>
<eval expected="41800" xpath="//flow/block[6]/@bpda"/>
<!-- row 1 cell 1 -->
<eval expected="95000" xpath="//flow/block[6]/block[1]/@ipd"/>
<eval expected="105000" xpath="//flow/block[6]/block[1]/@ipda"/>
<eval expected="14400" xpath="//flow/block[6]/block[1]/@bpd"/>
<eval expected="22400" xpath="//flow/block[6]/block[1]/@bpda"/>
<eval expected="4000" xpath="//flow/block[6]/block[1]/@left-offset"/>
<eval expected="-1000" xpath="//flow/block[6]/block[1]/@top-offset"/>
<eval expected="(solid,#000000,2000,collapse-outer)" xpath="//flow/block[6]/block[1]/@border-before"/>
<eval expected="(solid,#0000ff,6000,collapse-inner)" xpath="//flow/block[6]/block[1]/@border-after"/>
<eval expected="(solid,#ff0000,8000,collapse-outer)" xpath="//flow/block[6]/block[1]/@border-start"/>
<eval expected="(solid,#ffa500,2000,collapse-outer)" xpath="//flow/block[6]/block[1]/@border-end"/>
<!-- row 2 cell 1 -->
<eval expected="92000" xpath="//flow/block[6]/block[2]/@ipd"/>
<eval expected="108000" xpath="//flow/block[6]/block[2]/@ipda"/>
<eval expected="14400" xpath="//flow/block[6]/block[2]/@bpd"/>
<eval expected="32400" xpath="//flow/block[6]/block[2]/@bpda"/>
<eval expected="3000" xpath="//flow/block[6]/block[2]/@left-offset"/>
<eval expected="15400" xpath="//flow/block[6]/block[2]/@top-offset"/>
<eval expected="(solid,#0000ff,6000,collapse-inner)" xpath="//flow/block[6]/block[2]/@border-before"/>
<eval expected="(solid,#000080,12000,collapse-outer)" xpath="//flow/block[6]/block[2]/@border-after"/>
<eval expected="(solid,#800080,6000,collapse-outer)" xpath="//flow/block[6]/block[2]/@border-start"/>
<eval expected="(solid,#ffff00,10000,collapse-outer)" xpath="//flow/block[6]/block[2]/@border-end"/>
<!-- table 4 -->
<eval expected="100000" xpath="//flow/block[8]/@ipd"/>
<eval expected="100000" xpath="//flow/block[8]/@ipda"/>
<eval expected="58200" xpath="//flow/block[8]/@bpd"/>
<eval expected="58200" xpath="//flow/block[8]/@bpda"/>
<!-- row 1 cell 1 -->
<eval expected="94000" xpath="//flow/block[8]/block[1]/@ipd"/>
<eval expected="106000" xpath="//flow/block[8]/block[1]/@ipda"/>
<eval expected="28800" xpath="//flow/block[8]/block[1]/@bpd"/>
<eval expected="48800" xpath="//flow/block[8]/block[1]/@bpda"/>
<eval expected="1000" xpath="//flow/block[8]/block[1]/@left-offset"/>
<eval expected="-6000" xpath="//flow/block[8]/block[1]/@top-offset"/>
<eval expected="(solid,#000000,12000,collapse-outer)" xpath="//flow/block[8]/block[1]/@border-before"/>
<eval expected="(solid,#808080,8000,collapse-inner)" xpath="//flow/block[8]/block[1]/@border-after"/>
<eval expected="(solid,#ff0000,2000,collapse-outer)" xpath="//flow/block[8]/block[1]/@border-start"/>
<eval expected="(solid,#ffa500,10000,collapse-outer)" xpath="//flow/block[8]/block[1]/@border-end"/>
<!-- row 2 cell 1 -->
<eval expected="93000" xpath="//flow/block[8]/block[2]/@ipd"/>
<eval expected="107000" xpath="//flow/block[8]/block[2]/@ipda"/>
<eval expected="14400" xpath="//flow/block[8]/block[2]/@bpd"/>
<eval expected="24400" xpath="//flow/block[8]/block[2]/@bpda"/>
<eval expected="4000" xpath="//flow/block[8]/block[2]/@left-offset"/>
<eval expected="34800" xpath="//flow/block[8]/block[2]/@top-offset"/>
<eval expected="(solid,#808080,8000,collapse-inner)" xpath="//flow/block[8]/block[2]/@border-before"/>
<eval expected="(solid,#000080,2000,collapse-outer)" xpath="//flow/block[8]/block[2]/@border-after"/>
<eval expected="(solid,#800080,8000,collapse-outer)" xpath="//flow/block[8]/block[2]/@border-start"/>
<eval expected="(solid,#ffff00,6000,collapse-outer)" xpath="//flow/block[8]/block[2]/@border-end"/>
</checks>
</testcase>