From 13a1c9f24f99402edc574bbade7961dc8a524a67 Mon Sep 17 00:00:00 2001 From: Sebastian Schwaiger Date: Wed, 29 Jul 2015 11:16:04 +0200 Subject: [PATCH 01/44] A chart object can also be added to a table cell --- src/PhpWord/Element/AbstractContainer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpWord/Element/AbstractContainer.php b/src/PhpWord/Element/AbstractContainer.php index 57d646a01b..13512694f9 100644 --- a/src/PhpWord/Element/AbstractContainer.php +++ b/src/PhpWord/Element/AbstractContainer.php @@ -211,7 +211,7 @@ private function checkValidity($method) 'Title' => array('Section'), 'TOC' => array('Section'), 'PageBreak' => array('Section'), - 'Chart' => array('Section'), + 'Chart' => array('Section', 'Cell'), ); // Special condition, e.g. preservetext can only exists in cell when From d11b2467aa0d36bea5c1febfb1fd05b605338d36 Mon Sep 17 00:00:00 2001 From: Russ Date: Thu, 6 Aug 2015 15:57:18 -0500 Subject: [PATCH 02/44] Fix Empty Dropdown Entry Pad any empty dropdown entries we find so that xmlWriter doesn't generate an error. --- src/PhpWord/Writer/Word2007/Element/FormField.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PhpWord/Writer/Word2007/Element/FormField.php b/src/PhpWord/Writer/Word2007/Element/FormField.php index 432dc9c23b..f8a6a12863 100644 --- a/src/PhpWord/Writer/Word2007/Element/FormField.php +++ b/src/PhpWord/Writer/Word2007/Element/FormField.php @@ -165,6 +165,9 @@ private function writeDropDown(XMLWriter $xmlWriter, FormFieldElement $element) $xmlWriter->writeElementBlock('w:result', 'w:val', $value); $xmlWriter->writeElementBlock('w:default', 'w:val', $default); foreach ($entries as $entry) { + if ($entry == null || $entry == '') { + $entry = str_repeat(' ', self::FILLER_LENGTH); + } $xmlWriter->writeElementBlock('w:listEntry', 'w:val', $entry); } $xmlWriter->endElement(); From f7afdebb0331b393a157bb0874c7cf5209a9fbe3 Mon Sep 17 00:00:00 2001 From: Henri MEDOT Date: Thu, 12 Nov 2015 18:51:04 +0100 Subject: [PATCH 03/44] Added support for linebreaks
in Shared\Html::addHtml() --- src/PhpWord/Shared/Html.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index cdc88b4396..d9a3f40ed3 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -128,6 +128,7 @@ protected static function parseNode($node, $element, $styles = array(), $data = 'ul' => array('List', null, null, $styles, $data, 3, null), 'ol' => array('List', null, null, $styles, $data, 7, null), 'li' => array('ListItem', $node, $element, $styles, $data, null, null), + 'br' => array('LineBreak', null, $element, $styles, null, null, null), ); $newElement = null; @@ -374,4 +375,17 @@ private static function parseStyle($attribute, $styles) return $styles; } + + /** + * Parse line break + * + * @param \PhpOffice\PhpWord\Element\AbstractContainer $element + * @return null + */ + private static function parseLineBreak($element) + { + $element->addTextBreak(); + + return null; + } } From 0857f36572269aa12c33485384c6ccbfb938e7a7 Mon Sep 17 00:00:00 2001 From: Denis Solovyov Date: Fri, 12 Feb 2016 17:37:44 +0200 Subject: [PATCH 04/44] Add TextRun as container for CheckBox --- src/PhpWord/Element/AbstractContainer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpWord/Element/AbstractContainer.php b/src/PhpWord/Element/AbstractContainer.php index d211ae07b7..260d23000a 100644 --- a/src/PhpWord/Element/AbstractContainer.php +++ b/src/PhpWord/Element/AbstractContainer.php @@ -207,7 +207,7 @@ private function checkValidity($method) 'ListItem' => array('Section', 'Header', 'Footer', 'Cell', 'TextBox'), 'ListItemRun' => array('Section', 'Header', 'Footer', 'Cell', 'TextBox'), 'Table' => array('Section', 'Header', 'Footer', 'Cell', 'TextBox'), - 'CheckBox' => array('Section', 'Header', 'Footer', 'Cell'), + 'CheckBox' => array('Section', 'Header', 'Footer', 'Cell', 'TextRun'), 'TextBox' => array('Section', 'Header', 'Footer', 'Cell'), 'Footnote' => array('Section', 'TextRun', 'Cell'), 'Endnote' => array('Section', 'TextRun', 'Cell'), From 05387fac0903969fb88c832042991bd34f0154b0 Mon Sep 17 00:00:00 2001 From: Maria Haubner Date: Fri, 10 Mar 2017 13:28:32 +0100 Subject: [PATCH 05/44] enable password setting in word --- src/PhpWord/Metadata/Protection.php | 224 +++++++++++++++++- src/PhpWord/Writer/Word2007/Part/Settings.php | 28 ++- 2 files changed, 244 insertions(+), 8 deletions(-) diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index 0e2ee7c140..bcc0d65279 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -22,18 +22,77 @@ * * @since 0.12.0 * @link http://www.datypic.com/sc/ooxml/t-w_CT_DocProtect.html - * @todo Password! */ class Protection { + static $algorithmMapping = [ + 1 => 'md2', + 2 => 'md4', + 3 => 'md5', + 4 => 'sha1', + 5 => '', // 'mac' -> not possible with hash() + 6 => 'ripemd', + 7 => 'ripemd160', + 8 => '', + 9 => '', //'hmac' -> not possible with hash() + 10 => '', + 11 => '', + 12 => 'sha256', + 13 => 'sha384', + 14 => 'sha512', + ]; + static $initialCodeArray = [ + 0xE1F0, + 0x1D0F, + 0xCC9C, + 0x84C0, + 0x110C, + 0x0E10, + 0xF1CE, + 0x313E, + 0x1872, + 0xE139, + 0xD40F, + 0x84F9, + 0x280C, + 0xA96A, + 0x4EC3 + ]; + static $encryptionMatrix = + [ + [0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09], + [0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF], + [0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0], + [0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40], + [0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5], + [0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A], + [0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9], + [0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0], + [0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC], + [0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10], + [0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168], + [0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C], + [0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD], + [0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC], + [0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4] + ]; + /** - * Editing restriction readOnly|comments|trackedChanges|forms + * Editing restriction none|readOnly|comments|trackedChanges|forms * * @var string * @link http://www.datypic.com/sc/ooxml/a-w_edit-1.html */ private $editing; + private $password; + + private $spinCount = 100000; + + private $algorithmSid = 4; + + private $salt; + /** * Create a new instance * @@ -66,4 +125,165 @@ public function setEditing($editing = null) return $this; } + + public function getPassword() + { + return $this->password; + } + + public function setPassword($password) + { + $this->password = $this->getPasswordHash($password); + + return $this; + } + + public function getSpinCount() + { + return $this->spinCount; + } + + public function setSpinCount($spinCount) + { + $this->spinCount = $spinCount; + + return $this; + } + + public function getAlgorithmSid() + { + return $this->algorithmSid; + } + + public function setAlgorithmSid($algorithmSid) + { + $this->algorithmSid = $algorithmSid; + + return $this; + } + + public function setSalt($salt) + { + $this->salt = $salt; + } + + public function getSalt() + { + return $this->salt; + } + + private function getAlgorithm() + { + $algorithm = self::$algorithmMapping[$this->algorithmSid]; + if ($algorithm == '') { + $algorithm = 'sha1'; + } + + return $algorithm; + } + + private function getPasswordHash($password) + { + if (empty($password)) { + return ''; + } + $passwordMaxLength = 15; + + // Truncate the password to $passwordMaxLength characters + $password = mb_substr($password, 0, min($passwordMaxLength, mb_strlen($password))); + + $byteChars = []; + + echo "password: '{$password}'(".mb_strlen($password).")"; + + $pass_utf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); + for ($i = 0; $i < mb_strlen($password); $i++) { + $byteChars[$i] = ord(substr($pass_utf8, $i*2, 1)); + if ($byteChars[$i] == 0) { + echo "hi!$i"; + $byteChars[$i] = ord(substr($pass_utf8, $i*2+1, 1)); + } + } + + // Compute the high-order word + $highOrderWord = self::$initialCodeArray[sizeof($byteChars) - 1]; + for ($i = 0; $i < sizeof($byteChars); $i++) { + $tmp = $passwordMaxLength - sizeof($byteChars) + $i; + $matrixRow = self::$encryptionMatrix[$tmp]; + + for ($intBit = 0; $intBit < 7; $intBit++) { + if (($byteChars[$i] & (0x0001 << $intBit)) != 0) { + $highOrderWord = ($highOrderWord ^ $matrixRow[$intBit]); + } + } + } + + // Compute low-order word + $lowOrderWord = 0; + for ($i = sizeof($byteChars) - 1; $i >= 0; $i--) { + $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteChars[$i]); + } + $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ sizeof($byteChars) ^ 0xCE4B); + + $combinedKey = $this->int32(($highOrderWord << 16) + $lowOrderWord); + $generatedKey = [ + 0 => (($combinedKey & 0x000000FF) >> 0), + 1 => (($combinedKey & 0x0000FF00) >> 8), + 2 => (($combinedKey & 0x00FF0000) >> 16), + 3 => (($combinedKey & 0xFF000000) >> 24), + ]; + + $tmpStr = ''; + for ($i = 0; $i < 4; $i++) { + $tmpStr .= strtoupper(dechex($generatedKey[$i])); + } + $generatedKey = []; + $tmpStr = mb_convert_encoding($tmpStr, 'UCS-2LE', 'UTF-8'); + for ($i = 0; $i < strlen($tmpStr); $i++) { + $generatedKey[] = ord(substr($tmpStr, $i, 1)); + } + + $salt = unpack('C*', base64_decode($this->getSalt())); + $algorithm = $this->getAlgorithm(); + + $tmpArray1 = $generatedKey; + $tmpArray2 = $salt; + $generatedKey = array_merge($tmpArray2, $tmpArray1); + + $generatedKey = $this->hashByteArray($algorithm, $generatedKey); + + for ($i = 0; $i < $this->getSpinCount(); $i++) { + $iterator = [ + 0 => (($i & 0x000000FF) >> 0), + 1 => (($i & 0x0000FF00) >> 8), + 2 => (($i & 0x00FF0000) >> 16), + 3 => (($i & 0xFF000000) >> 24), + ]; + $generatedKey = array_merge($generatedKey, $iterator); + $generatedKey = $this->hashByteArray($algorithm, $generatedKey); + } + + $hash = implode(array_map("chr", $generatedKey)); + + return base64_encode($hash); + } + + private function int32($value) + { + $value = ($value & 0xFFFFFFFF); + + if ($value & 0x80000000) { + $value = -((~$value & 0xFFFFFFFF) + 1); + } + + return $value; + } + + private function hashByteArray($algorithm, $array) + { + $string = implode(array_map("chr", $array)); + $string = hash($algorithm, $string, true); + + return unpack('C*', $string); + } } diff --git a/src/PhpWord/Writer/Word2007/Part/Settings.php b/src/PhpWord/Writer/Word2007/Part/Settings.php index d881e13af2..11549e0873 100644 --- a/src/PhpWord/Writer/Word2007/Part/Settings.php +++ b/src/PhpWord/Writer/Word2007/Part/Settings.php @@ -152,12 +152,28 @@ private function getProtection() { $protection = $this->getParentWriter()->getPhpWord()->getProtection(); if ($protection->getEditing() !== null) { - $this->settings['w:documentProtection'] = array( - '@attributes' => array( - 'w:enforcement' => 1, - 'w:edit' => $protection->getEditing(), - ) - ); + if (empty($protection->getPassword())) { + $this->settings['w:documentProtection'] = array( + '@attributes' => array( + 'w:enforcement' => 1, + 'w:edit' => $protection->getEditing(), + ) + ); + } else { + $this->settings['w:documentProtection'] = array( + '@attributes' => array( + 'w:enforcement' => 1, + 'w:edit' => $protection->getEditing(), + 'w:cryptProviderType' => 'rsaFull', + 'w:cryptAlgorithmClass' => 'hash', + 'w:cryptAlgorithmType' => 'typeAny', + 'w:cryptAlgorithmSid' => $protection->getAlgorithmSid(), + 'w:cryptSpinCount' => $protection->getSpinCount(), + 'w:hash' => $protection->getPassword(), + 'w:salt' => $protection->getSalt(), + ) + ); + } } } From 483a167500a008d4895a79f594658dc3b5fc2769 Mon Sep 17 00:00:00 2001 From: Maria Haubner Date: Fri, 10 Mar 2017 15:44:13 +0100 Subject: [PATCH 06/44] refactoring of hash function --- src/PhpWord/Metadata/Protection.php | 206 +++++++++++++++++++--------- 1 file changed, 141 insertions(+), 65 deletions(-) diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index bcc0d65279..5427f57092 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -76,6 +76,7 @@ class Protection [0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC], [0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4] ]; + static $passwordMaxLength = 15; /** * Editing restriction none|readOnly|comments|trackedChanges|forms @@ -85,12 +86,32 @@ class Protection */ private $editing; + /** + * Hashed password + * + * @var string + */ private $password; + /** + * Number of hashing iterations + * + * @var int + */ private $spinCount = 100000; + /** + * Algorithm-SID according to self::$algorithmMapping + * + * @var int + */ private $algorithmSid = 4; + /** + * Hashed salt + * + * @var string + */ private $salt; /** @@ -126,11 +147,22 @@ public function setEditing($editing = null) return $this; } + /** + * Get password hash + * + * @return string + */ public function getPassword() { return $this->password; } + /** + * Set password + * + * @param $password + * @return self + */ public function setPassword($password) { $this->password = $this->getPasswordHash($password); @@ -138,11 +170,22 @@ public function setPassword($password) return $this; } + /** + * Get count for hash iterations + * + * @return int + */ public function getSpinCount() { return $this->spinCount; } + /** + * Set count for hash iterations + * + * @param $spinCount + * @return self + */ public function setSpinCount($spinCount) { $this->spinCount = $spinCount; @@ -150,11 +193,22 @@ public function setSpinCount($spinCount) return $this; } + /** + * Get algorithm-sid + * + * @return int + */ public function getAlgorithmSid() { return $this->algorithmSid; } + /** + * Set algorithm-sid (see self::$algorithmMapping) + * + * @param $algorithmSid + * @return self + */ public function setAlgorithmSid($algorithmSid) { $this->algorithmSid = $algorithmSid; @@ -162,16 +216,34 @@ public function setAlgorithmSid($algorithmSid) return $this; } - public function setSalt($salt) + /** + * Get salt hash + * + * @return string + */ + public function getSalt() { - $this->salt = $salt; + return $this->salt; } - public function getSalt() + /** + * Set salt hash + * + * @param $salt + * @return self + */ + public function setSalt($salt) { - return $this->salt; + $this->salt = $salt; + + return $this; } + /** + * Get algorithm from self::$algorithmMapping + * + * @return string + */ private function getAlgorithm() { $algorithm = self::$algorithmMapping[$this->algorithmSid]; @@ -182,35 +254,76 @@ private function getAlgorithm() return $algorithm; } + /** + * Create a hashed password that MS Word will be able to work with + * + * @param string $password + * @return string + */ private function getPasswordHash($password) { + $orig_encoding = mb_internal_encoding(); + mb_internal_encoding("UTF-8"); + if (empty($password)) { return ''; } - $passwordMaxLength = 15; - // Truncate the password to $passwordMaxLength characters - $password = mb_substr($password, 0, min($passwordMaxLength, mb_strlen($password))); + $password = mb_substr($password, 0, min(self::$passwordMaxLength, mb_strlen($password))); + // Construct a new NULL-terminated string consisting of single-byte characters: + // Get the single-byte values by iterating through the Unicode characters of the truncated password. + // For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte. + $pass_utf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); $byteChars = []; - - echo "password: '{$password}'(".mb_strlen($password).")"; - - $pass_utf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); for ($i = 0; $i < mb_strlen($password); $i++) { - $byteChars[$i] = ord(substr($pass_utf8, $i*2, 1)); + $byteChars[$i] = ord(substr($pass_utf8, $i * 2, 1)); if ($byteChars[$i] == 0) { - echo "hi!$i"; - $byteChars[$i] = ord(substr($pass_utf8, $i*2+1, 1)); + $byteChars[$i] = ord(substr($pass_utf8, $i * 2 + 1, 1)); } } + // build low-order word and hig-order word and combine them + $combinedKey = $this->buildCombinedKey($byteChars); + // build reversed hexadecimal string + $hex = strtoupper(dechex($combinedKey & 0xFFFFFFFF)); + $reversedHex = $hex[6].$hex[7].$hex[4].$hex[5].$hex[2].$hex[3].$hex[0].$hex[1]; + + $generatedKey = mb_convert_encoding($reversedHex, 'UCS-2LE', 'UTF-8'); + + // Implementation Notes List: + // Word requires that the initial hash of the password with the salt not be considered in the count. + // The initial hash of salt + key is not included in the iteration count. + $generatedKey = hash($this->getAlgorithm(), base64_decode($this->getSalt()) . $generatedKey, true); + for ($i = 0; $i < $this->getSpinCount(); $i++) { + $generatedKey = hash($this->getAlgorithm(), $generatedKey . pack("CCCC", $i, $i>>8, $i>>16, $i>>24), true); + } + $generatedKey = base64_encode($generatedKey); + + mb_internal_encoding($orig_encoding); + + return $generatedKey; + } + + /** + * Build combined key from low-order word and high-order word + * + * @param array $byteChars -> byte array representation of password + * @return int + */ + private function buildCombinedKey($byteChars) + { // Compute the high-order word + // Initialize from the initial code array (see above), depending on the passwords length. $highOrderWord = self::$initialCodeArray[sizeof($byteChars) - 1]; + + // For each character in the password: + // For every bit in the character, starting with the least significant and progressing to (but excluding) + // the most significant, if the bit is set, XOR the key’s high-order word with the corresponding word from + // the Encryption Matrix for ($i = 0; $i < sizeof($byteChars); $i++) { - $tmp = $passwordMaxLength - sizeof($byteChars) + $i; + $tmp = self::$passwordMaxLength - sizeof($byteChars) + $i; $matrixRow = self::$encryptionMatrix[$tmp]; - for ($intBit = 0; $intBit < 7; $intBit++) { if (($byteChars[$i] & (0x0001 << $intBit)) != 0) { $highOrderWord = ($highOrderWord ^ $matrixRow[$intBit]); @@ -219,55 +332,26 @@ private function getPasswordHash($password) } // Compute low-order word + // Initialize with 0 $lowOrderWord = 0; + // For each character in the password, going backwards for ($i = sizeof($byteChars) - 1; $i >= 0; $i--) { + // low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR character $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteChars[$i]); } + // Lastly, low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR strPassword length XOR 0xCE4B. $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ sizeof($byteChars) ^ 0xCE4B); - $combinedKey = $this->int32(($highOrderWord << 16) + $lowOrderWord); - $generatedKey = [ - 0 => (($combinedKey & 0x000000FF) >> 0), - 1 => (($combinedKey & 0x0000FF00) >> 8), - 2 => (($combinedKey & 0x00FF0000) >> 16), - 3 => (($combinedKey & 0xFF000000) >> 24), - ]; - - $tmpStr = ''; - for ($i = 0; $i < 4; $i++) { - $tmpStr .= strtoupper(dechex($generatedKey[$i])); - } - $generatedKey = []; - $tmpStr = mb_convert_encoding($tmpStr, 'UCS-2LE', 'UTF-8'); - for ($i = 0; $i < strlen($tmpStr); $i++) { - $generatedKey[] = ord(substr($tmpStr, $i, 1)); - } - - $salt = unpack('C*', base64_decode($this->getSalt())); - $algorithm = $this->getAlgorithm(); - - $tmpArray1 = $generatedKey; - $tmpArray2 = $salt; - $generatedKey = array_merge($tmpArray2, $tmpArray1); - - $generatedKey = $this->hashByteArray($algorithm, $generatedKey); - - for ($i = 0; $i < $this->getSpinCount(); $i++) { - $iterator = [ - 0 => (($i & 0x000000FF) >> 0), - 1 => (($i & 0x0000FF00) >> 8), - 2 => (($i & 0x00FF0000) >> 16), - 3 => (($i & 0xFF000000) >> 24), - ]; - $generatedKey = array_merge($generatedKey, $iterator); - $generatedKey = $this->hashByteArray($algorithm, $generatedKey); - } - - $hash = implode(array_map("chr", $generatedKey)); - - return base64_encode($hash); + // Combine the Low and High Order Word + return $this->int32(($highOrderWord << 16) + $lowOrderWord); } + /** + * simulate behaviour of int32 + * + * @param int $value + * @return int + */ private function int32($value) { $value = ($value & 0xFFFFFFFF); @@ -278,12 +362,4 @@ private function int32($value) return $value; } - - private function hashByteArray($algorithm, $array) - { - $string = implode(array_map("chr", $array)); - $string = hash($algorithm, $string, true); - - return unpack('C*', $string); - } } From 703e34137b4fa9147d864fbef09442aa4370509c Mon Sep 17 00:00:00 2001 From: Maria Haubner Date: Fri, 10 Mar 2017 16:24:52 +0100 Subject: [PATCH 07/44] refactored hash function to word settings --- src/PhpWord/Metadata/Protection.php | 191 +---------------- src/PhpWord/Writer/Word2007/Part/Settings.php | 199 +++++++++++++++++- 2 files changed, 204 insertions(+), 186 deletions(-) diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index 5427f57092..511503e4be 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -25,59 +25,6 @@ */ class Protection { - static $algorithmMapping = [ - 1 => 'md2', - 2 => 'md4', - 3 => 'md5', - 4 => 'sha1', - 5 => '', // 'mac' -> not possible with hash() - 6 => 'ripemd', - 7 => 'ripemd160', - 8 => '', - 9 => '', //'hmac' -> not possible with hash() - 10 => '', - 11 => '', - 12 => 'sha256', - 13 => 'sha384', - 14 => 'sha512', - ]; - static $initialCodeArray = [ - 0xE1F0, - 0x1D0F, - 0xCC9C, - 0x84C0, - 0x110C, - 0x0E10, - 0xF1CE, - 0x313E, - 0x1872, - 0xE139, - 0xD40F, - 0x84F9, - 0x280C, - 0xA96A, - 0x4EC3 - ]; - static $encryptionMatrix = - [ - [0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09], - [0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF], - [0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0], - [0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40], - [0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5], - [0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A], - [0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9], - [0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0], - [0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC], - [0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10], - [0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168], - [0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C], - [0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD], - [0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC], - [0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4] - ]; - static $passwordMaxLength = 15; - /** * Editing restriction none|readOnly|comments|trackedChanges|forms * @@ -91,28 +38,28 @@ class Protection * * @var string */ - private $password; + private $password = ''; /** * Number of hashing iterations * * @var int */ - private $spinCount = 100000; + private $spinCount = 0; /** - * Algorithm-SID according to self::$algorithmMapping + * Algorithm-SID (see to \PhpOffice\PhpWord\Writer\Word2007\Part\Settings::$algorithmMapping) * * @var int */ - private $algorithmSid = 4; + private $algorithmSid = 0; /** * Hashed salt * * @var string */ - private $salt; + private $salt = ''; /** * Create a new instance @@ -165,7 +112,7 @@ public function getPassword() */ public function setPassword($password) { - $this->password = $this->getPasswordHash($password); + $this->password = $password; return $this; } @@ -204,7 +151,7 @@ public function getAlgorithmSid() } /** - * Set algorithm-sid (see self::$algorithmMapping) + * Set algorithm-sid (see \PhpOffice\PhpWord\Writer\Word2007\Part\Settings::$algorithmMapping) * * @param $algorithmSid * @return self @@ -238,128 +185,4 @@ public function setSalt($salt) return $this; } - - /** - * Get algorithm from self::$algorithmMapping - * - * @return string - */ - private function getAlgorithm() - { - $algorithm = self::$algorithmMapping[$this->algorithmSid]; - if ($algorithm == '') { - $algorithm = 'sha1'; - } - - return $algorithm; - } - - /** - * Create a hashed password that MS Word will be able to work with - * - * @param string $password - * @return string - */ - private function getPasswordHash($password) - { - $orig_encoding = mb_internal_encoding(); - mb_internal_encoding("UTF-8"); - - if (empty($password)) { - return ''; - } - - $password = mb_substr($password, 0, min(self::$passwordMaxLength, mb_strlen($password))); - - // Construct a new NULL-terminated string consisting of single-byte characters: - // Get the single-byte values by iterating through the Unicode characters of the truncated password. - // For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte. - $pass_utf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); - $byteChars = []; - for ($i = 0; $i < mb_strlen($password); $i++) { - $byteChars[$i] = ord(substr($pass_utf8, $i * 2, 1)); - if ($byteChars[$i] == 0) { - $byteChars[$i] = ord(substr($pass_utf8, $i * 2 + 1, 1)); - } - } - - // build low-order word and hig-order word and combine them - $combinedKey = $this->buildCombinedKey($byteChars); - // build reversed hexadecimal string - $hex = strtoupper(dechex($combinedKey & 0xFFFFFFFF)); - $reversedHex = $hex[6].$hex[7].$hex[4].$hex[5].$hex[2].$hex[3].$hex[0].$hex[1]; - - $generatedKey = mb_convert_encoding($reversedHex, 'UCS-2LE', 'UTF-8'); - - // Implementation Notes List: - // Word requires that the initial hash of the password with the salt not be considered in the count. - // The initial hash of salt + key is not included in the iteration count. - $generatedKey = hash($this->getAlgorithm(), base64_decode($this->getSalt()) . $generatedKey, true); - for ($i = 0; $i < $this->getSpinCount(); $i++) { - $generatedKey = hash($this->getAlgorithm(), $generatedKey . pack("CCCC", $i, $i>>8, $i>>16, $i>>24), true); - } - $generatedKey = base64_encode($generatedKey); - - mb_internal_encoding($orig_encoding); - - return $generatedKey; - } - - /** - * Build combined key from low-order word and high-order word - * - * @param array $byteChars -> byte array representation of password - * @return int - */ - private function buildCombinedKey($byteChars) - { - // Compute the high-order word - // Initialize from the initial code array (see above), depending on the passwords length. - $highOrderWord = self::$initialCodeArray[sizeof($byteChars) - 1]; - - // For each character in the password: - // For every bit in the character, starting with the least significant and progressing to (but excluding) - // the most significant, if the bit is set, XOR the key’s high-order word with the corresponding word from - // the Encryption Matrix - for ($i = 0; $i < sizeof($byteChars); $i++) { - $tmp = self::$passwordMaxLength - sizeof($byteChars) + $i; - $matrixRow = self::$encryptionMatrix[$tmp]; - for ($intBit = 0; $intBit < 7; $intBit++) { - if (($byteChars[$i] & (0x0001 << $intBit)) != 0) { - $highOrderWord = ($highOrderWord ^ $matrixRow[$intBit]); - } - } - } - - // Compute low-order word - // Initialize with 0 - $lowOrderWord = 0; - // For each character in the password, going backwards - for ($i = sizeof($byteChars) - 1; $i >= 0; $i--) { - // low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR character - $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteChars[$i]); - } - // Lastly, low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR strPassword length XOR 0xCE4B. - $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ sizeof($byteChars) ^ 0xCE4B); - - // Combine the Low and High Order Word - return $this->int32(($highOrderWord << 16) + $lowOrderWord); - } - - /** - * simulate behaviour of int32 - * - * @param int $value - * @return int - */ - private function int32($value) - { - $value = ($value & 0xFFFFFFFF); - - if ($value & 0x80000000) { - $value = -((~$value & 0xFFFFFFFF) + 1); - } - - return $value; - } } diff --git a/src/PhpWord/Writer/Word2007/Part/Settings.php b/src/PhpWord/Writer/Word2007/Part/Settings.php index 11549e0873..ed9c07d3c3 100644 --- a/src/PhpWord/Writer/Word2007/Part/Settings.php +++ b/src/PhpWord/Writer/Word2007/Part/Settings.php @@ -24,6 +24,59 @@ */ class Settings extends AbstractPart { + static $algorithmMapping = [ + 1 => 'md2', + 2 => 'md4', + 3 => 'md5', + 4 => 'sha1', + 5 => '', // 'mac' -> not possible with hash() + 6 => 'ripemd', + 7 => 'ripemd160', + 8 => '', + 9 => '', //'hmac' -> not possible with hash() + 10 => '', + 11 => '', + 12 => 'sha256', + 13 => 'sha384', + 14 => 'sha512', + ]; + static $initialCodeArray = [ + 0xE1F0, + 0x1D0F, + 0xCC9C, + 0x84C0, + 0x110C, + 0x0E10, + 0xF1CE, + 0x313E, + 0x1872, + 0xE139, + 0xD40F, + 0x84F9, + 0x280C, + 0xA96A, + 0x4EC3 + ]; + static $encryptionMatrix = + [ + [0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09], + [0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF], + [0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0], + [0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40], + [0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5], + [0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A], + [0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9], + [0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0], + [0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC], + [0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10], + [0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168], + [0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C], + [0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD], + [0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC], + [0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4] + ]; + static $passwordMaxLength = 15; + /** * Settings value * @@ -169,8 +222,8 @@ private function getProtection() 'w:cryptAlgorithmType' => 'typeAny', 'w:cryptAlgorithmSid' => $protection->getAlgorithmSid(), 'w:cryptSpinCount' => $protection->getSpinCount(), - 'w:hash' => $protection->getPassword(), - 'w:salt' => $protection->getSalt(), + 'w:hash' => $this->getPasswordHash($protection), + 'w:salt' => $this->getSaltHash($protection->getSalt()), ) ); } @@ -193,4 +246,146 @@ private function getCompatibility() )); } } + + + /** + * Create a hashed password that MS Word will be able to work with + * + * @param \PhpOffice\PhpWord\Metadata\Protection $protection + * @return string + */ + private function getPasswordHash($protection) + { + $orig_encoding = mb_internal_encoding(); + mb_internal_encoding("UTF-8"); + + $password = $protection->getPassword(); + $password = mb_substr($password, 0, min(self::$passwordMaxLength, mb_strlen($password))); + + // Construct a new NULL-terminated string consisting of single-byte characters: + // Get the single-byte values by iterating through the Unicode characters of the truncated password. + // For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte. + $pass_utf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); + $byteChars = []; + for ($i = 0; $i < mb_strlen($password); $i++) { + $byteChars[$i] = ord(substr($pass_utf8, $i * 2, 1)); + if ($byteChars[$i] == 0) { + $byteChars[$i] = ord(substr($pass_utf8, $i * 2 + 1, 1)); + } + } + + // build low-order word and hig-order word and combine them + $combinedKey = $this->buildCombinedKey($byteChars); + // build reversed hexadecimal string + $hex = strtoupper(dechex($combinedKey & 0xFFFFFFFF)); + $reversedHex = $hex[6].$hex[7].$hex[4].$hex[5].$hex[2].$hex[3].$hex[0].$hex[1]; + + $generatedKey = mb_convert_encoding($reversedHex, 'UCS-2LE', 'UTF-8'); + + // Implementation Notes List: + // Word requires that the initial hash of the password with the salt not be considered in the count. + // The initial hash of salt + key is not included in the iteration count. + $algorithm = $this->getAlgorithm($protection->getAlgorithmSid()); + $generatedKey = hash($algorithm, base64_decode($this->getSaltHash($protection->getSalt())) . $generatedKey, true); + + $spinCount = (!empty($protection->getSpinCount())) ? $protection->getSpinCount() : 100000; + + for ($i = 0; $i < $spinCount; $i++) { + $generatedKey = hash($algorithm, $generatedKey . pack("CCCC", $i, $i>>8, $i>>16, $i>>24), true); + } + $generatedKey = base64_encode($generatedKey); + + mb_internal_encoding($orig_encoding); + + return $generatedKey; + } + + /** + * Get algorithm from self::$algorithmMapping + * + * @param int $sid + * @return string + */ + private function getAlgorithm($sid) + { + if (empty($sid)) { + $sid = 4; + } + + $algorithm = self::$algorithmMapping[$sid]; + if ($algorithm == '') { + $algorithm = 'sha1'; + } + + return $algorithm; + } + + /** + * Get salt hash + * + * @param string $salt + * @return string + */ + private function getSaltHash($salt) + { + return $salt; + } + + /** + * Build combined key from low-order word and high-order word + * + * @param array $byteChars -> byte array representation of password + * @return int + */ + private function buildCombinedKey($byteChars) + { + // Compute the high-order word + // Initialize from the initial code array (see above), depending on the passwords length. + $highOrderWord = self::$initialCodeArray[sizeof($byteChars) - 1]; + + // For each character in the password: + // For every bit in the character, starting with the least significant and progressing to (but excluding) + // the most significant, if the bit is set, XOR the key’s high-order word with the corresponding word from + // the Encryption Matrix + for ($i = 0; $i < sizeof($byteChars); $i++) { + $tmp = self::$passwordMaxLength - sizeof($byteChars) + $i; + $matrixRow = self::$encryptionMatrix[$tmp]; + for ($intBit = 0; $intBit < 7; $intBit++) { + if (($byteChars[$i] & (0x0001 << $intBit)) != 0) { + $highOrderWord = ($highOrderWord ^ $matrixRow[$intBit]); + } + } + } + + // Compute low-order word + // Initialize with 0 + $lowOrderWord = 0; + // For each character in the password, going backwards + for ($i = sizeof($byteChars) - 1; $i >= 0; $i--) { + // low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR character + $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteChars[$i]); + } + // Lastly, low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR strPassword length XOR 0xCE4B. + $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ sizeof($byteChars) ^ 0xCE4B); + + // Combine the Low and High Order Word + return $this->int32(($highOrderWord << 16) + $lowOrderWord); + } + + /** + * Simulate behaviour of int32 + * + * @param int $value + * @return int + */ + private function int32($value) + { + $value = ($value & 0xFFFFFFFF); + + if ($value & 0x80000000) { + $value = -((~$value & 0xFFFFFFFF) + 1); + } + + return $value; + } } From 0221414ee0855612b6a5f09412790fb2bf2dfd1e Mon Sep 17 00:00:00 2001 From: Maria Haubner Date: Fri, 10 Mar 2017 16:57:42 +0100 Subject: [PATCH 08/44] randomly genereate salt for word password protection --- src/PhpWord/Metadata/Protection.php | 14 ++++---- src/PhpWord/Writer/Word2007/Part/Settings.php | 35 +++++++++---------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index 511503e4be..a25a8f31a2 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -45,14 +45,14 @@ class Protection * * @var int */ - private $spinCount = 0; + private $spinCount = 100000; /** * Algorithm-SID (see to \PhpOffice\PhpWord\Writer\Word2007\Part\Settings::$algorithmMapping) * * @var int */ - private $algorithmSid = 0; + private $mswordAlgorithmSid = 4; /** * Hashed salt @@ -145,20 +145,20 @@ public function setSpinCount($spinCount) * * @return int */ - public function getAlgorithmSid() + public function getMswordAlgorithmSid() { - return $this->algorithmSid; + return $this->mswordAlgorithmSid; } /** * Set algorithm-sid (see \PhpOffice\PhpWord\Writer\Word2007\Part\Settings::$algorithmMapping) * - * @param $algorithmSid + * @param $mswordAlgorithmSid * @return self */ - public function setAlgorithmSid($algorithmSid) + public function setMswordAlgorithmSid($mswordAlgorithmSid) { - $this->algorithmSid = $algorithmSid; + $this->mswordAlgorithmSid = $mswordAlgorithmSid; return $this; } diff --git a/src/PhpWord/Writer/Word2007/Part/Settings.php b/src/PhpWord/Writer/Word2007/Part/Settings.php index ed9c07d3c3..c709ee6278 100644 --- a/src/PhpWord/Writer/Word2007/Part/Settings.php +++ b/src/PhpWord/Writer/Word2007/Part/Settings.php @@ -213,6 +213,9 @@ private function getProtection() ) ); } else { + if ($protection->getSalt() == null) { + $protection->setSalt(openssl_random_pseudo_bytes(16)); + } $this->settings['w:documentProtection'] = array( '@attributes' => array( 'w:enforcement' => 1, @@ -220,7 +223,7 @@ private function getProtection() 'w:cryptProviderType' => 'rsaFull', 'w:cryptAlgorithmClass' => 'hash', 'w:cryptAlgorithmType' => 'typeAny', - 'w:cryptAlgorithmSid' => $protection->getAlgorithmSid(), + 'w:cryptAlgorithmSid' => $protection->getMswordAlgorithmSid(), 'w:cryptSpinCount' => $protection->getSpinCount(), 'w:hash' => $this->getPasswordHash($protection), 'w:salt' => $this->getSaltHash($protection->getSalt()), @@ -239,11 +242,13 @@ private function getCompatibility() { $compatibility = $this->getParentWriter()->getPhpWord()->getCompatibility(); if ($compatibility->getOoxmlVersion() !== null) { - $this->settings['w:compat']['w:compatSetting'] = array('@attributes' => array( - 'w:name' => 'compatibilityMode', - 'w:uri' => 'http://schemas.microsoft.com/office/word', - 'w:val' => $compatibility->getOoxmlVersion(), - )); + $this->settings['w:compat']['w:compatSetting'] = array( + '@attributes' => array( + 'w:name' => 'compatibilityMode', + 'w:uri' => 'http://schemas.microsoft.com/office/word', + 'w:val' => $compatibility->getOoxmlVersion(), + ) + ); } } @@ -277,21 +282,19 @@ private function getPasswordHash($protection) // build low-order word and hig-order word and combine them $combinedKey = $this->buildCombinedKey($byteChars); // build reversed hexadecimal string - $hex = strtoupper(dechex($combinedKey & 0xFFFFFFFF)); - $reversedHex = $hex[6].$hex[7].$hex[4].$hex[5].$hex[2].$hex[3].$hex[0].$hex[1]; + $hex = strtoupper(dechex($combinedKey & 0xFFFFFFFF)); + $reversedHex = $hex[6] . $hex[7] . $hex[4] . $hex[5] . $hex[2] . $hex[3] . $hex[0] . $hex[1]; $generatedKey = mb_convert_encoding($reversedHex, 'UCS-2LE', 'UTF-8'); // Implementation Notes List: // Word requires that the initial hash of the password with the salt not be considered in the count. // The initial hash of salt + key is not included in the iteration count. - $algorithm = $this->getAlgorithm($protection->getAlgorithmSid()); + $algorithm = $this->getAlgorithm($protection->getMswordAlgorithmSid()); $generatedKey = hash($algorithm, base64_decode($this->getSaltHash($protection->getSalt())) . $generatedKey, true); - $spinCount = (!empty($protection->getSpinCount())) ? $protection->getSpinCount() : 100000; - - for ($i = 0; $i < $spinCount; $i++) { - $generatedKey = hash($algorithm, $generatedKey . pack("CCCC", $i, $i>>8, $i>>16, $i>>24), true); + for ($i = 0; $i < $protection->getSpinCount(); $i++) { + $generatedKey = hash($algorithm, $generatedKey . pack("CCCC", $i, $i >> 8, $i >> 16, $i >> 24), true); } $generatedKey = base64_encode($generatedKey); @@ -308,10 +311,6 @@ private function getPasswordHash($protection) */ private function getAlgorithm($sid) { - if (empty($sid)) { - $sid = 4; - } - $algorithm = self::$algorithmMapping[$sid]; if ($algorithm == '') { $algorithm = 'sha1'; @@ -328,7 +327,7 @@ private function getAlgorithm($sid) */ private function getSaltHash($salt) { - return $salt; + return base64_encode(str_pad(substr($salt, 0, 16), 16, '1')); } /** From 76246630ce4eac8ba9aac13f8b19cb4b657ca947 Mon Sep 17 00:00:00 2001 From: Maria Haubner Date: Fri, 10 Mar 2017 17:30:51 +0100 Subject: [PATCH 09/44] add test --- src/PhpWord/Writer/Word2007/Part/Settings.php | 2 +- .../Writer/Word2007/Part/SettingsTest.php | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/PhpWord/Writer/Word2007/Part/Settings.php b/src/PhpWord/Writer/Word2007/Part/Settings.php index c709ee6278..07d7a90cea 100644 --- a/src/PhpWord/Writer/Word2007/Part/Settings.php +++ b/src/PhpWord/Writer/Word2007/Part/Settings.php @@ -282,7 +282,7 @@ private function getPasswordHash($protection) // build low-order word and hig-order word and combine them $combinedKey = $this->buildCombinedKey($byteChars); // build reversed hexadecimal string - $hex = strtoupper(dechex($combinedKey & 0xFFFFFFFF)); + $hex = str_pad(strtoupper(dechex($combinedKey & 0xFFFFFFFF)), 8, '0', \STR_PAD_LEFT); $reversedHex = $hex[6] . $hex[7] . $hex[4] . $hex[5] . $hex[2] . $hex[3] . $hex[0] . $hex[1]; $generatedKey = mb_convert_encoding($reversedHex, 'UCS-2LE', 'UTF-8'); diff --git a/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php b/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php index 6ed23e44b1..110d2aff9f 100644 --- a/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php +++ b/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php @@ -50,6 +50,27 @@ public function testDocumentProtection() $this->assertTrue($doc->elementExists($path, $file)); } + /** + * Test document protection with password + * + * Note: to get comparison values, a docx was generated in Word2010 and the values taken from the settings.xml + */ + public function testDocumentProtectionWithPassword() + { + $phpWord = new PhpWord(); + $phpWord->getProtection()->setEditing('readOnly'); + $phpWord->getProtection()->setPassword('testÄö@€!$&'); + $phpWord->getProtection()->setSalt(base64_decode("uq81pJRRGFIY5U+E9gt8tA==")); + + $doc = TestHelperDOCX::getDocument($phpWord); + + $file = 'word/settings.xml'; + + $path = '/w:settings/w:documentProtection'; + $this->assertTrue($doc->elementExists($path, $file)); + $this->assertEquals($doc->getElement($path, $file)->getAttribute('w:hash'), "RA9jfY/u3DX114PMcl+uSekxsYk="); + } + /** * Test compatibility */ From 71574d1fe2a3638e4bb3f4a888c9a8d4cd815821 Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Mon, 13 Mar 2017 16:22:04 +0100 Subject: [PATCH 10/44] Code Review; minor changes to salt handling, corrected some comments --- src/PhpWord/Metadata/Protection.php | 15 ++++++++---- src/PhpWord/Writer/Word2007/Part/Settings.php | 23 +++++-------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index a25a8f31a2..88cfa99e6e 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -34,7 +34,7 @@ class Protection private $editing; /** - * Hashed password + * password * * @var string */ @@ -55,7 +55,7 @@ class Protection private $mswordAlgorithmSid = 4; /** - * Hashed salt + * salt * * @var string */ @@ -95,7 +95,7 @@ public function setEditing($editing = null) } /** - * Get password hash + * Get password * * @return string */ @@ -164,7 +164,7 @@ public function setMswordAlgorithmSid($mswordAlgorithmSid) } /** - * Get salt hash + * Get salt * * @return string */ @@ -174,13 +174,18 @@ public function getSalt() } /** - * Set salt hash + * Set salt. Salt HAS to be 16 characters, or an exception will be thrown. * * @param $salt * @return self + * @throws \InvalidArgumentException */ public function setSalt($salt) { + if ($salt !== null && strlen($salt) !== 16){ + throw new \InvalidArgumentException('salt has to be of exactly 16 bytes length'); + } + $this->salt = $salt; return $this; diff --git a/src/PhpWord/Writer/Word2007/Part/Settings.php b/src/PhpWord/Writer/Word2007/Part/Settings.php index 07d7a90cea..82f8192ad4 100644 --- a/src/PhpWord/Writer/Word2007/Part/Settings.php +++ b/src/PhpWord/Writer/Word2007/Part/Settings.php @@ -225,8 +225,8 @@ private function getProtection() 'w:cryptAlgorithmType' => 'typeAny', 'w:cryptAlgorithmSid' => $protection->getMswordAlgorithmSid(), 'w:cryptSpinCount' => $protection->getSpinCount(), - 'w:hash' => $this->getPasswordHash($protection), - 'w:salt' => $this->getSaltHash($protection->getSalt()), + 'w:hash' => $this->getEncodedPasswordHash($protection), + 'w:salt' => base64_encode($protection->getSalt()), ) ); } @@ -255,11 +255,12 @@ private function getCompatibility() /** * Create a hashed password that MS Word will be able to work with + * @link https://blogs.msdn.microsoft.com/vsod/2010/04/05/how-to-set-the-editing-restrictions-in-word-using-open-xml-sdk-2-0/ * * @param \PhpOffice\PhpWord\Metadata\Protection $protection * @return string */ - private function getPasswordHash($protection) + private function getEncodedPasswordHash($protection) { $orig_encoding = mb_internal_encoding(); mb_internal_encoding("UTF-8"); @@ -267,7 +268,6 @@ private function getPasswordHash($protection) $password = $protection->getPassword(); $password = mb_substr($password, 0, min(self::$passwordMaxLength, mb_strlen($password))); - // Construct a new NULL-terminated string consisting of single-byte characters: // Get the single-byte values by iterating through the Unicode characters of the truncated password. // For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte. $pass_utf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); @@ -291,7 +291,7 @@ private function getPasswordHash($protection) // Word requires that the initial hash of the password with the salt not be considered in the count. // The initial hash of salt + key is not included in the iteration count. $algorithm = $this->getAlgorithm($protection->getMswordAlgorithmSid()); - $generatedKey = hash($algorithm, base64_decode($this->getSaltHash($protection->getSalt())) . $generatedKey, true); + $generatedKey = hash($algorithm, $protection->getSalt() . $generatedKey, true); for ($i = 0; $i < $protection->getSpinCount(); $i++) { $generatedKey = hash($algorithm, $generatedKey . pack("CCCC", $i, $i >> 8, $i >> 16, $i >> 24), true); @@ -319,17 +319,6 @@ private function getAlgorithm($sid) return $algorithm; } - /** - * Get salt hash - * - * @param string $salt - * @return string - */ - private function getSaltHash($salt) - { - return base64_encode(str_pad(substr($salt, 0, 16), 16, '1')); - } - /** * Build combined key from low-order word and high-order word * @@ -372,7 +361,7 @@ private function buildCombinedKey($byteChars) } /** - * Simulate behaviour of int32 + * Simulate behaviour of (signed) int32 * * @param int $value * @return int From 9474e97b6a535af3edd2de039f375000d23f589f Mon Sep 17 00:00:00 2001 From: Maxim Bulygin Date: Wed, 8 Nov 2017 11:13:38 +0200 Subject: [PATCH 11/44] rename 'Object' classes to 'ObjectElement' (php72 compatibility) --- src/PhpWord/Element/AbstractContainer.php | 6 ++--- src/PhpWord/Element/AbstractElement.php | 7 ++++-- .../Element/{Object.php => ObjectElement.php} | 4 ++-- .../Element/{Object.php => ObjectElement.php} | 6 ++--- tests/PhpWord/Element/CellTest.php | 2 +- tests/PhpWord/Element/ObjectTest.php | 22 +++++++++---------- tests/PhpWord/Element/SectionTest.php | 2 +- tests/PhpWord/Writer/Word2007/ElementTest.php | 2 +- 8 files changed, 27 insertions(+), 24 deletions(-) rename src/PhpWord/Element/{Object.php => ObjectElement.php} (98%) rename src/PhpWord/Writer/Word2007/Element/{Object.php => ObjectElement.php} (97%) diff --git a/src/PhpWord/Element/AbstractContainer.php b/src/PhpWord/Element/AbstractContainer.php index d717179870..0f744baedc 100644 --- a/src/PhpWord/Element/AbstractContainer.php +++ b/src/PhpWord/Element/AbstractContainer.php @@ -37,7 +37,7 @@ * @method PageBreak addPageBreak() * @method Table addTable(mixed $style = null) * @method Image addImage(string $source, mixed $style = null, bool $isWatermark = false) - * @method \PhpOffice\PhpWord\Element\Object addObject(string $source, mixed $style = null) + * @method \PhpOffice\PhpWord\Element\ObjectElement addObject(string $source, mixed $style = null) * @method TextBox addTextBox(mixed $style = null) * @method Field addField(string $type = null, array $properties = array(), array $options = array(), mixed $text = null) * @method Line addLine(mixed $lineStyle = null) @@ -87,7 +87,7 @@ public function __call($function, $args) ); $functions = array(); foreach ($elements as $element) { - $functions['add' . strtolower($element)] = $element; + $functions['add' . strtolower($element)] = $element == 'Object' ? 'ObjectElement' : $element; } // Run valid `add` command @@ -195,7 +195,7 @@ private function checkValidity($method) 'Link' => $generalContainers, 'TextBreak' => $generalContainers, 'Image' => $generalContainers, - 'Object' => $generalContainers, + 'ObjectElement' => $generalContainers, 'Field' => $generalContainers, 'Line' => $generalContainers, 'Shape' => $generalContainers, diff --git a/src/PhpWord/Element/AbstractElement.php b/src/PhpWord/Element/AbstractElement.php index 81e185289d..042f985e9a 100644 --- a/src/PhpWord/Element/AbstractElement.php +++ b/src/PhpWord/Element/AbstractElement.php @@ -358,11 +358,14 @@ public function setParentContainer(AbstractElement $container) */ private function setMediaRelation() { - if (!$this instanceof Link && !$this instanceof Image && !$this instanceof Object) { + if (!$this instanceof Link && !$this instanceof Image && !$this instanceof ObjectElement) { return; } $elementName = substr(get_class($this), strrpos(get_class($this), '\\') + 1); + if ($elementName == 'ObjectElement') { + $elementName = 'Object'; + } $mediaPart = $this->getMediaPart(); $source = $this->getSource(); $image = null; @@ -372,7 +375,7 @@ private function setMediaRelation() $rId = Media::addElement($mediaPart, strtolower($elementName), $source, $image); $this->setRelationId($rId); - if ($this instanceof Object) { + if ($this instanceof ObjectElement) { $icon = $this->getIcon(); $rId = Media::addElement($mediaPart, 'image', $icon, new Image($icon)); $this->setImageRelationId($rId); diff --git a/src/PhpWord/Element/Object.php b/src/PhpWord/Element/ObjectElement.php similarity index 98% rename from src/PhpWord/Element/Object.php rename to src/PhpWord/Element/ObjectElement.php index 8fe83224bb..a405c405f6 100644 --- a/src/PhpWord/Element/Object.php +++ b/src/PhpWord/Element/ObjectElement.php @@ -21,9 +21,9 @@ use PhpOffice\PhpWord\Style\Image as ImageStyle; /** - * Object element + * ObjectElement element */ -class Object extends AbstractElement +class ObjectElement extends AbstractElement { /** * Ole-Object Src diff --git a/src/PhpWord/Writer/Word2007/Element/Object.php b/src/PhpWord/Writer/Word2007/Element/ObjectElement.php similarity index 97% rename from src/PhpWord/Writer/Word2007/Element/Object.php rename to src/PhpWord/Writer/Word2007/Element/ObjectElement.php index 8231ec0c43..4fca9f5321 100644 --- a/src/PhpWord/Writer/Word2007/Element/Object.php +++ b/src/PhpWord/Writer/Word2007/Element/ObjectElement.php @@ -20,11 +20,11 @@ use PhpOffice\PhpWord\Writer\Word2007\Style\Image as ImageStyleWriter; /** - * Object element writer + * ObjectElement element writer * * @since 0.10.0 */ -class Object extends AbstractElement +class ObjectElement extends AbstractElement { /** * Write object element. @@ -33,7 +33,7 @@ public function write() { $xmlWriter = $this->getXmlWriter(); $element = $this->getElement(); - if (!$element instanceof \PhpOffice\PhpWord\Element\Object) { + if (!$element instanceof \PhpOffice\PhpWord\Element\ObjectElement) { return; } diff --git a/tests/PhpWord/Element/CellTest.php b/tests/PhpWord/Element/CellTest.php index aff208c4a9..69a56a9f07 100644 --- a/tests/PhpWord/Element/CellTest.php +++ b/tests/PhpWord/Element/CellTest.php @@ -181,7 +181,7 @@ public function testAddObjectXLS() $element = $oCell->addObject($src); $this->assertCount(1, $oCell->getElements()); - $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Object', $element); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\ObjectElement', $element); } /** diff --git a/tests/PhpWord/Element/ObjectTest.php b/tests/PhpWord/Element/ObjectTest.php index 51ed19b5cc..6903f1fcb4 100644 --- a/tests/PhpWord/Element/ObjectTest.php +++ b/tests/PhpWord/Element/ObjectTest.php @@ -18,9 +18,9 @@ namespace PhpOffice\PhpWord\Element; /** - * Test class for PhpOffice\PhpWord\Element\Object + * Test class for PhpOffice\PhpWord\Element\ObjectElement * - * @coversDefaultClass \PhpOffice\PhpWord\Element\Object + * @coversDefaultClass \PhpOffice\PhpWord\Element\ObjectElement * @runTestsInSeparateProcesses */ class ObjectTest extends \PHPUnit_Framework_TestCase @@ -31,9 +31,9 @@ class ObjectTest extends \PHPUnit_Framework_TestCase public function testConstructWithSupportedFiles() { $src = __DIR__ . '/../_files/documents/reader.docx'; - $oObject = new Object($src); + $oObject = new ObjectElement($src); - $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Object', $oObject); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\ObjectElement', $oObject); $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Image', $oObject->getStyle()); $this->assertEquals($src, $oObject->getSource()); } @@ -44,9 +44,9 @@ public function testConstructWithSupportedFiles() public function testConstructWithSupportedFilesLong() { $src = __DIR__ . '/../_files/documents/sheet.xls'; - $oObject = new Object($src); + $oObject = new ObjectElement($src); - $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Object', $oObject); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\ObjectElement', $oObject); $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Image', $oObject->getStyle()); $this->assertEquals($src, $oObject->getSource()); } @@ -59,7 +59,7 @@ public function testConstructWithSupportedFilesLong() public function testConstructWithNotSupportedFiles() { $src = __DIR__ . '/../_files/xsl/passthrough.xsl'; - $oObject = new Object($src); + $oObject = new ObjectElement($src); $oObject->getSource(); } @@ -69,9 +69,9 @@ public function testConstructWithNotSupportedFiles() public function testConstructWithSupportedFilesAndStyle() { $src = __DIR__ . '/../_files/documents/sheet.xls'; - $oObject = new Object($src, array('width' => '230px')); + $oObject = new ObjectElement($src, array('width' => '230px')); - $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Object', $oObject); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\ObjectElement', $oObject); $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Image', $oObject->getStyle()); $this->assertEquals($src, $oObject->getSource()); } @@ -82,7 +82,7 @@ public function testConstructWithSupportedFilesAndStyle() public function testRelationId() { $src = __DIR__ . '/../_files/documents/sheet.xls'; - $oObject = new Object($src); + $oObject = new ObjectElement($src); $iVal = rand(1, 1000); $oObject->setRelationId($iVal); @@ -95,7 +95,7 @@ public function testRelationId() public function testImageRelationId() { $src = __DIR__ . '/../_files/documents/sheet.xls'; - $oObject = new Object($src); + $oObject = new ObjectElement($src); $iVal = rand(1, 1000); $oObject->setImageRelationId($iVal); diff --git a/tests/PhpWord/Element/SectionTest.php b/tests/PhpWord/Element/SectionTest.php index aebfc9b78e..e4f50107a7 100644 --- a/tests/PhpWord/Element/SectionTest.php +++ b/tests/PhpWord/Element/SectionTest.php @@ -70,7 +70,7 @@ public function testAddElements() 'PageBreak', 'Table', 'ListItem', - 'Object', + 'ObjectElement', 'Image', 'Title', 'TextRun', diff --git a/tests/PhpWord/Writer/Word2007/ElementTest.php b/tests/PhpWord/Writer/Word2007/ElementTest.php index 0ebc7c577d..f49778f6df 100644 --- a/tests/PhpWord/Writer/Word2007/ElementTest.php +++ b/tests/PhpWord/Writer/Word2007/ElementTest.php @@ -42,7 +42,7 @@ public function testUnmatchedElements() { $elements = array( 'CheckBox', 'Container', 'Footnote', 'Image', 'Link', 'ListItem', 'ListItemRun', - 'Object', 'PreserveText', 'Table', 'Text', 'TextBox', 'TextBreak', 'Title', 'TOC', + 'ObjectElement', 'PreserveText', 'Table', 'Text', 'TextBox', 'TextBreak', 'Title', 'TOC', 'Field', 'Line', 'Shape', 'Chart', 'FormField', 'SDT', ); foreach ($elements as $element) { From 2a8462524ed4d82dc423f08e4346d2aa52f6d670 Mon Sep 17 00:00:00 2001 From: Maxim Bulygin Date: Wed, 8 Nov 2017 13:12:47 +0200 Subject: [PATCH 12/44] changelog note --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f21cb9d68c..f92b21310c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ This is the last version to support PHP 5.3 - Loosen dependency to Zend - Images are not being printed when generating PDF - @hubertinio #1074 #431 - Fixed some PHP 7 warnings - @ likeuntomurphy #927 +- Fixed PHP 7.2 compatibility (renamed `Object` class names to `ObjectElement`) - @SailorMax #1185 - Fixed Word 97 reader - @alsofronie @Benpxpx @mario-rivera #912 #920 #892 - Fixed image loading over https - @troosan #988 - Impossibility to set different even and odd page headers - @troosan #981 From ad83196a052b4fcb0b00a278a16dc0ef53ec6c74 Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 23 Nov 2017 22:49:21 +0100 Subject: [PATCH 13/44] move password encoding in separate class fix PHPCS errors add documentation add sample --- docs/general.rst | 11 + samples/Sample_38_Protection.php | 21 ++ src/PhpWord/Metadata/Protection.php | 23 +- .../Shared/Microsoft/PasswordEncoder.php | 205 +++++++++++++++++ src/PhpWord/SimpleType/DocProtect.php | 55 +++++ src/PhpWord/Writer/Word2007/Part/Settings.php | 208 ++---------------- tests/PhpWord/Metadata/SettingsTest.php | 13 +- .../Writer/Word2007/Part/SettingsTest.php | 33 ++- 8 files changed, 360 insertions(+), 209 deletions(-) create mode 100644 samples/Sample_38_Protection.php create mode 100644 src/PhpWord/Shared/Microsoft/PasswordEncoder.php create mode 100644 src/PhpWord/SimpleType/DocProtect.php diff --git a/docs/general.rst b/docs/general.rst index b11734b16c..f6c8df1c9d 100644 --- a/docs/general.rst +++ b/docs/general.rst @@ -271,3 +271,14 @@ points to twips. $sectionStyle->setMarginLeft(\PhpOffice\PhpWord\Shared\Converter::inchToTwip(.5)); // 2 cm right margin $sectionStyle->setMarginRight(\PhpOffice\PhpWord\Shared\Converter::cmToTwip(2)); + +Document protection +------------------- + +The document (or parts of it) can be password protected. + +.. code-block:: php + + $documentProtection = $phpWord->getSettings()->getDocumentProtection(); + $documentProtection->setEditing(DocProtect::READ_ONLY); + $documentProtection->setPassword('myPassword'); diff --git a/samples/Sample_38_Protection.php b/samples/Sample_38_Protection.php new file mode 100644 index 0000000000..ee2b460b2f --- /dev/null +++ b/samples/Sample_38_Protection.php @@ -0,0 +1,21 @@ +getSettings()->getDocumentProtection(); +$documentProtection->setEditing(DocProtect::READ_ONLY); +$documentProtection->setPassword('myPassword'); + +$section = $phpWord->addSection(); +$section->addText('this document is password protected'); + +// Save file +echo write($phpWord, basename(__FILE__, '.php'), $writers); +if (!CLI) { + include_once 'Sample_Footer.php'; +} diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index be78c055ec..09d08aac03 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -17,6 +17,8 @@ namespace PhpOffice\PhpWord\Metadata; +use PhpOffice\PhpWord\SimpleType\DocProtect; + /** * Document protection class * @@ -38,28 +40,28 @@ class Protection * * @var string */ - private $password = ''; + private $password; /** - * Number of hashing iterations + * Iterations to Run Hashing Algorithm * * @var int */ private $spinCount = 100000; /** - * Algorithm-SID (see to \PhpOffice\PhpWord\Writer\Word2007\Part\Settings::$algorithmMapping) + * Cryptographic Hashing Algorithm (see to \PhpOffice\PhpWord\Writer\Word2007\Part\Settings::$algorithmMapping) * * @var int */ private $mswordAlgorithmSid = 4; /** - * salt + * Salt for Password Verifier * * @var string */ - private $salt = ''; + private $salt; /** * Create a new instance @@ -68,7 +70,9 @@ class Protection */ public function __construct($editing = null) { - $this->setEditing($editing); + if ($editing != null) { + $this->setEditing($editing); + } } /** @@ -84,11 +88,12 @@ public function getEditing() /** * Set editing protection * - * @param string $editing + * @param string $editing Any value of \PhpOffice\PhpWord\SimpleType\DocProtect * @return self */ public function setEditing($editing = null) { + DocProtect::validate($editing); $this->editing = $editing; return $this; @@ -177,12 +182,12 @@ public function getSalt() * Set salt. Salt HAS to be 16 characters, or an exception will be thrown. * * @param $salt - * @return self * @throws \InvalidArgumentException + * @return self */ public function setSalt($salt) { - if ($salt !== null && strlen($salt) !== 16){ + if ($salt !== null && strlen($salt) !== 16) { throw new \InvalidArgumentException('salt has to be of exactly 16 bytes length'); } diff --git a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php new file mode 100644 index 0000000000..40a3ea1295 --- /dev/null +++ b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php @@ -0,0 +1,205 @@ + 'md2', + 2 => 'md4', + 3 => 'md5', + 4 => 'sha1', + 5 => '', // 'mac' -> not possible with hash() + 6 => 'ripemd', + 7 => 'ripemd160', + 8 => '', + 9 => '', //'hmac' -> not possible with hash() + 10 => '', + 11 => '', + 12 => 'sha256', + 13 => 'sha384', + 14 => 'sha512', + ); + + private static $initialCodeArray = array( + 0xE1F0, + 0x1D0F, + 0xCC9C, + 0x84C0, + 0x110C, + 0x0E10, + 0xF1CE, + 0x313E, + 0x1872, + 0xE139, + 0xD40F, + 0x84F9, + 0x280C, + 0xA96A, + 0x4EC3, + ); + + private static $encryptionMatrix = array( + array(0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09), + array(0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF), + array(0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0), + array(0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40), + array(0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5), + array(0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A), + array(0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9), + array(0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0), + array(0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC), + array(0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10), + array(0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168), + array(0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C), + array(0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD), + array(0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC), + array(0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4), + ); + + private static $passwordMaxLength = 15; + + /** + * Create a hashed password that MS Word will be able to work with + * @see https://blogs.msdn.microsoft.com/vsod/2010/04/05/how-to-set-the-editing-restrictions-in-word-using-open-xml-sdk-2-0/ + * + * @param string $password + * @param number $algorithmSid + * @param string $salt + * @param number $spinCount + * @return string + */ + public static function hashPassword($password, $algorithmSid = 4, $salt = null, $spinCount = 10000) + { + $origEncoding = mb_internal_encoding(); + mb_internal_encoding('UTF-8'); + + $password = mb_substr($password, 0, min(self::$passwordMaxLength, mb_strlen($password))); + + // Get the single-byte values by iterating through the Unicode characters of the truncated password. + // For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte. + $passUtf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); + $byteChars = array(); + for ($i = 0; $i < mb_strlen($password); $i++) { + $byteChars[$i] = ord(substr($passUtf8, $i * 2, 1)); + if ($byteChars[$i] == 0) { + $byteChars[$i] = ord(substr($passUtf8, $i * 2 + 1, 1)); + } + } + + // build low-order word and hig-order word and combine them + $combinedKey = self::buildCombinedKey($byteChars); + // build reversed hexadecimal string + $hex = str_pad(strtoupper(dechex($combinedKey & 0xFFFFFFFF)), 8, '0', \STR_PAD_LEFT); + $reversedHex = $hex[6] . $hex[7] . $hex[4] . $hex[5] . $hex[2] . $hex[3] . $hex[0] . $hex[1]; + + $generatedKey = mb_convert_encoding($reversedHex, 'UCS-2LE', 'UTF-8'); + + // Implementation Notes List: + // Word requires that the initial hash of the password with the salt not be considered in the count. + // The initial hash of salt + key is not included in the iteration count. + $algorithm = self::getAlgorithm($algorithmSid); + $generatedKey = hash($algorithm, $salt . $generatedKey, true); + + for ($i = 0; $i < $spinCount; $i++) { + $generatedKey = hash($algorithm, $generatedKey . pack('CCCC', $i, $i >> 8, $i >> 16, $i >> 24), true); + } + $generatedKey = base64_encode($generatedKey); + + mb_internal_encoding($origEncoding); + + return $generatedKey; + } + + /** + * Get algorithm from self::$algorithmMapping + * + * @param int $sid + * @return string + */ + private static function getAlgorithm($sid) + { + $algorithm = self::$algorithmMapping[$sid]; + if ($algorithm == '') { + $algorithm = 'sha1'; + } + + return $algorithm; + } + + /** + * Build combined key from low-order word and high-order word + * + * @param array $byteChars byte array representation of password + * @return int + */ + private static function buildCombinedKey($byteChars) + { + // Compute the high-order word + // Initialize from the initial code array (see above), depending on the passwords length. + $highOrderWord = self::$initialCodeArray[count($byteChars) - 1]; + + // For each character in the password: + // For every bit in the character, starting with the least significant and progressing to (but excluding) + // the most significant, if the bit is set, XOR the key’s high-order word with the corresponding word from + // the Encryption Matrix + for ($i = 0; $i < count($byteChars); $i++) { + $tmp = self::$passwordMaxLength - count($byteChars) + $i; + $matrixRow = self::$encryptionMatrix[$tmp]; + for ($intBit = 0; $intBit < 7; $intBit++) { + if (($byteChars[$i] & (0x0001 << $intBit)) != 0) { + $highOrderWord = ($highOrderWord ^ $matrixRow[$intBit]); + } + } + } + + // Compute low-order word + // Initialize with 0 + $lowOrderWord = 0; + // For each character in the password, going backwards + for ($i = count($byteChars) - 1; $i >= 0; $i--) { + // low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR character + $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteChars[$i]); + } + // Lastly, low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR strPassword length XOR 0xCE4B. + $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ count($byteChars) ^ 0xCE4B); + + // Combine the Low and High Order Word + return self::int32(($highOrderWord << 16) + $lowOrderWord); + } + + /** + * Simulate behaviour of (signed) int32 + * + * @param int $value + * @return int + */ + private static function int32($value) + { + $value = ($value & 0xFFFFFFFF); + + if ($value & 0x80000000) { + $value = -((~$value & 0xFFFFFFFF) + 1); + } + + return $value; + } +} diff --git a/src/PhpWord/SimpleType/DocProtect.php b/src/PhpWord/SimpleType/DocProtect.php new file mode 100644 index 0000000000..cffa0003ed --- /dev/null +++ b/src/PhpWord/SimpleType/DocProtect.php @@ -0,0 +1,55 @@ + 'md2', - 2 => 'md4', - 3 => 'md5', - 4 => 'sha1', - 5 => '', // 'mac' -> not possible with hash() - 6 => 'ripemd', - 7 => 'ripemd160', - 8 => '', - 9 => '', //'hmac' -> not possible with hash() - 10 => '', - 11 => '', - 12 => 'sha256', - 13 => 'sha384', - 14 => 'sha512', - ]; - static $initialCodeArray = [ - 0xE1F0, - 0x1D0F, - 0xCC9C, - 0x84C0, - 0x110C, - 0x0E10, - 0xF1CE, - 0x313E, - 0x1872, - 0xE139, - 0xD40F, - 0x84F9, - 0x280C, - 0xA96A, - 0x4EC3 - ]; - static $encryptionMatrix = - [ - [0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09], - [0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF], - [0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0], - [0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40], - [0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5], - [0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A], - [0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9], - [0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0], - [0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC], - [0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10], - [0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168], - [0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C], - [0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD], - [0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC], - [0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4] - ]; - static $passwordMaxLength = 15; - /** * Settings value * @@ -238,25 +186,26 @@ private function setDocumentProtection($documentProtection) $this->settings['w:documentProtection'] = array( '@attributes' => array( 'w:enforcement' => 1, - 'w:edit' => $documentProtection->getEditing(), - ) + 'w:edit' => $documentProtection->getEditing(), + ), ); } else { if ($documentProtection->getSalt() == null) { $documentProtection->setSalt(openssl_random_pseudo_bytes(16)); } + $passwordHash = PasswordEncoder::hashPassword($documentProtection->getPassword(), $documentProtection->getMswordAlgorithmSid(), $documentProtection->getSalt(), $documentProtection->getSpinCount()); $this->settings['w:documentProtection'] = array( '@attributes' => array( - 'w:enforcement' => 1, - 'w:edit' => $documentProtection->getEditing(), - 'w:cryptProviderType' => 'rsaFull', + 'w:enforcement' => 1, + 'w:edit' => $documentProtection->getEditing(), + 'w:cryptProviderType' => 'rsaFull', 'w:cryptAlgorithmClass' => 'hash', - 'w:cryptAlgorithmType' => 'typeAny', - 'w:cryptAlgorithmSid' => $documentProtection->getMswordAlgorithmSid(), - 'w:cryptSpinCount' => $documentProtection->getSpinCount(), - 'w:hash' => $this->getEncodedPasswordHash($documentProtection), - 'w:salt' => base64_encode($documentProtection->getSalt()), - ) + 'w:cryptAlgorithmType' => 'typeAny', + 'w:cryptAlgorithmSid' => $documentProtection->getMswordAlgorithmSid(), + 'w:cryptSpinCount' => $documentProtection->getSpinCount(), + 'w:hash' => $passwordHash, + 'w:salt' => base64_encode($documentProtection->getSalt()), + ), ); } } @@ -337,135 +286,10 @@ private function getCompatibility() $this->settings['w:compat']['w:compatSetting'] = array( '@attributes' => array( 'w:name' => 'compatibilityMode', - 'w:uri' => 'http://schemas.microsoft.com/office/word', - 'w:val' => $compatibility->getOoxmlVersion(), - ) + 'w:uri' => 'http://schemas.microsoft.com/office/word', + 'w:val' => $compatibility->getOoxmlVersion(), + ), ); } } - - - /** - * Create a hashed password that MS Word will be able to work with - * @link https://blogs.msdn.microsoft.com/vsod/2010/04/05/how-to-set-the-editing-restrictions-in-word-using-open-xml-sdk-2-0/ - * - * @param \PhpOffice\PhpWord\Metadata\Protection $protection - * @return string - */ - private function getEncodedPasswordHash($protection) - { - $orig_encoding = mb_internal_encoding(); - mb_internal_encoding("UTF-8"); - - $password = $protection->getPassword(); - $password = mb_substr($password, 0, min(self::$passwordMaxLength, mb_strlen($password))); - - // Get the single-byte values by iterating through the Unicode characters of the truncated password. - // For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte. - $pass_utf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); - $byteChars = []; - for ($i = 0; $i < mb_strlen($password); $i++) { - $byteChars[$i] = ord(substr($pass_utf8, $i * 2, 1)); - if ($byteChars[$i] == 0) { - $byteChars[$i] = ord(substr($pass_utf8, $i * 2 + 1, 1)); - } - } - - // build low-order word and hig-order word and combine them - $combinedKey = $this->buildCombinedKey($byteChars); - // build reversed hexadecimal string - $hex = str_pad(strtoupper(dechex($combinedKey & 0xFFFFFFFF)), 8, '0', \STR_PAD_LEFT); - $reversedHex = $hex[6] . $hex[7] . $hex[4] . $hex[5] . $hex[2] . $hex[3] . $hex[0] . $hex[1]; - - $generatedKey = mb_convert_encoding($reversedHex, 'UCS-2LE', 'UTF-8'); - - // Implementation Notes List: - // Word requires that the initial hash of the password with the salt not be considered in the count. - // The initial hash of salt + key is not included in the iteration count. - $algorithm = $this->getAlgorithm($protection->getMswordAlgorithmSid()); - $generatedKey = hash($algorithm, $protection->getSalt() . $generatedKey, true); - - for ($i = 0; $i < $protection->getSpinCount(); $i++) { - $generatedKey = hash($algorithm, $generatedKey . pack("CCCC", $i, $i >> 8, $i >> 16, $i >> 24), true); - } - $generatedKey = base64_encode($generatedKey); - - mb_internal_encoding($orig_encoding); - - return $generatedKey; - } - - /** - * Get algorithm from self::$algorithmMapping - * - * @param int $sid - * @return string - */ - private function getAlgorithm($sid) - { - $algorithm = self::$algorithmMapping[$sid]; - if ($algorithm == '') { - $algorithm = 'sha1'; - } - - return $algorithm; - } - - /** - * Build combined key from low-order word and high-order word - * - * @param array $byteChars -> byte array representation of password - * @return int - */ - private function buildCombinedKey($byteChars) - { - // Compute the high-order word - // Initialize from the initial code array (see above), depending on the passwords length. - $highOrderWord = self::$initialCodeArray[sizeof($byteChars) - 1]; - - // For each character in the password: - // For every bit in the character, starting with the least significant and progressing to (but excluding) - // the most significant, if the bit is set, XOR the key’s high-order word with the corresponding word from - // the Encryption Matrix - for ($i = 0; $i < sizeof($byteChars); $i++) { - $tmp = self::$passwordMaxLength - sizeof($byteChars) + $i; - $matrixRow = self::$encryptionMatrix[$tmp]; - for ($intBit = 0; $intBit < 7; $intBit++) { - if (($byteChars[$i] & (0x0001 << $intBit)) != 0) { - $highOrderWord = ($highOrderWord ^ $matrixRow[$intBit]); - } - } - } - - // Compute low-order word - // Initialize with 0 - $lowOrderWord = 0; - // For each character in the password, going backwards - for ($i = sizeof($byteChars) - 1; $i >= 0; $i--) { - // low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR character - $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteChars[$i]); - } - // Lastly, low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR strPassword length XOR 0xCE4B. - $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ sizeof($byteChars) ^ 0xCE4B); - - // Combine the Low and High Order Word - return $this->int32(($highOrderWord << 16) + $lowOrderWord); - } - - /** - * Simulate behaviour of (signed) int32 - * - * @param int $value - * @return int - */ - private function int32($value) - { - $value = ($value & 0xFFFFFFFF); - - if ($value & 0x80000000) { - $value = -((~$value & 0xFFFFFFFF) + 1); - } - - return $value; - } } diff --git a/tests/PhpWord/Metadata/SettingsTest.php b/tests/PhpWord/Metadata/SettingsTest.php index bee8d0cacc..a2a80b12bd 100644 --- a/tests/PhpWord/Metadata/SettingsTest.php +++ b/tests/PhpWord/Metadata/SettingsTest.php @@ -63,13 +63,22 @@ public function testHideSpellingErrors() public function testDocumentProtection() { $oSettings = new Settings(); - $oSettings->setDocumentProtection(new Protection()); + $oSettings->setDocumentProtection(new Protection('trackedChanges')); $this->assertNotNull($oSettings->getDocumentProtection()); - $oSettings->getDocumentProtection()->setEditing('trackedChanges'); $this->assertEquals('trackedChanges', $oSettings->getDocumentProtection()->getEditing()); } + /** + * Test setting an invalid salt + * @expectedException \InvalidArgumentException + */ + public function testInvalidSalt() + { + $p = new Protection(); + $p->setSalt('123'); + } + /** * TrackRevistions */ diff --git a/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php b/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php index 8c47cb52b0..7d4ef491d7 100644 --- a/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php +++ b/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php @@ -58,15 +58,15 @@ public function testDocumentProtection() /** * Test document protection with password - * - * Note: to get comparison values, a docx was generated in Word2010 and the values taken from the settings.xml */ public function testDocumentProtectionWithPassword() { $phpWord = new PhpWord(); - $phpWord->getProtection()->setEditing('readOnly'); - $phpWord->getProtection()->setPassword('testÄö@€!$&'); - $phpWord->getProtection()->setSalt(base64_decode("uq81pJRRGFIY5U+E9gt8tA==")); + $phpWord->getSettings()->getDocumentProtection()->setEditing('readOnly'); + $phpWord->getSettings()->getDocumentProtection()->setPassword('testÄö@€!$&'); + $phpWord->getSettings()->getDocumentProtection()->setSalt(base64_decode('uq81pJRRGFIY5U+E9gt8tA==')); + $phpWord->getSettings()->getDocumentProtection()->setMswordAlgorithmSid(1); + $phpWord->getSettings()->getDocumentProtection()->setSpinCount(10); $doc = TestHelperDOCX::getDocument($phpWord); @@ -74,7 +74,28 @@ public function testDocumentProtectionWithPassword() $path = '/w:settings/w:documentProtection'; $this->assertTrue($doc->elementExists($path, $file)); - $this->assertEquals($doc->getElement($path, $file)->getAttribute('w:hash'), "RA9jfY/u3DX114PMcl+uSekxsYk="); + $this->assertEquals('rUuJbk6LuN2/qFyp7IUPQA==', $doc->getElement($path, $file)->getAttribute('w:hash')); + $this->assertEquals('1', $doc->getElement($path, $file)->getAttribute('w:cryptAlgorithmSid')); + $this->assertEquals('10', $doc->getElement($path, $file)->getAttribute('w:cryptSpinCount')); + } + + /** + * Test document protection with password only + */ + public function testDocumentProtectionWithPasswordOnly() + { + $phpWord = new PhpWord(); + $phpWord->getSettings()->getDocumentProtection()->setEditing('readOnly'); + $phpWord->getSettings()->getDocumentProtection()->setPassword('testÄö@€!$&'); + + $doc = TestHelperDOCX::getDocument($phpWord); + + $file = 'word/settings.xml'; + + $path = '/w:settings/w:documentProtection'; + $this->assertTrue($doc->elementExists($path, $file)); + $this->assertEquals('4', $doc->getElement($path, $file)->getAttribute('w:cryptAlgorithmSid')); + $this->assertEquals('100000', $doc->getElement($path, $file)->getAttribute('w:cryptSpinCount')); } /** From 2e562512f4d969edfd500252b9fe7107c1d45f74 Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 24 Nov 2017 14:45:05 +0100 Subject: [PATCH 14/44] Add unit tests for PasswordEncoder --- .../Shared/Microsoft/PasswordEncoder.php | 3 + tests/PhpWord/Metadata/SettingsTest.php | 4 +- .../Shared/Microsoft/PasswordEncoderTest.php | 91 +++++++++++++++++++ .../Writer/Word2007/Part/SettingsTest.php | 19 ---- 4 files changed, 96 insertions(+), 21 deletions(-) create mode 100644 tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php diff --git a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php index 40a3ea1295..cddcfcd3c6 100644 --- a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php +++ b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php @@ -98,8 +98,10 @@ public static function hashPassword($password, $algorithmSid = 4, $salt = null, // For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte. $passUtf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); $byteChars = array(); + for ($i = 0; $i < mb_strlen($password); $i++) { $byteChars[$i] = ord(substr($passUtf8, $i * 2, 1)); + if ($byteChars[$i] == 0) { $byteChars[$i] = ord(substr($passUtf8, $i * 2 + 1, 1)); } @@ -189,6 +191,7 @@ private static function buildCombinedKey($byteChars) /** * Simulate behaviour of (signed) int32 * + * @codeCoverageIgnore * @param int $value * @return int */ diff --git a/tests/PhpWord/Metadata/SettingsTest.php b/tests/PhpWord/Metadata/SettingsTest.php index a2a80b12bd..9830fd280c 100644 --- a/tests/PhpWord/Metadata/SettingsTest.php +++ b/tests/PhpWord/Metadata/SettingsTest.php @@ -75,8 +75,8 @@ public function testDocumentProtection() */ public function testInvalidSalt() { - $p = new Protection(); - $p->setSalt('123'); + $protection = new Protection(); + $protection->setSalt('123'); } /** diff --git a/tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php b/tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php new file mode 100644 index 0000000000..7b2bd3e78d --- /dev/null +++ b/tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php @@ -0,0 +1,91 @@ +assertEquals('10', $doc->getElement($path, $file)->getAttribute('w:cryptSpinCount')); } - /** - * Test document protection with password only - */ - public function testDocumentProtectionWithPasswordOnly() - { - $phpWord = new PhpWord(); - $phpWord->getSettings()->getDocumentProtection()->setEditing('readOnly'); - $phpWord->getSettings()->getDocumentProtection()->setPassword('testÄö@€!$&'); - - $doc = TestHelperDOCX::getDocument($phpWord); - - $file = 'word/settings.xml'; - - $path = '/w:settings/w:documentProtection'; - $this->assertTrue($doc->elementExists($path, $file)); - $this->assertEquals('4', $doc->getElement($path, $file)->getAttribute('w:cryptAlgorithmSid')); - $this->assertEquals('100000', $doc->getElement($path, $file)->getAttribute('w:cryptSpinCount')); - } - /** * Test compatibility */ From 2138ea9e9f2b5ec26210dcc322646200b32d7470 Mon Sep 17 00:00:00 2001 From: troosan Date: Mon, 4 Dec 2017 09:04:50 +0100 Subject: [PATCH 15/44] Update .travis.yml add PHP 7.2 build --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 0ec84081e1..5f382bee84 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ php: - 5.6 - 7.0 - 7.1 + - 7.2 matrix: include: @@ -17,6 +18,7 @@ matrix: allow_failures: - php: 7.0 - php: 7.1 + - php: 7.2 cache: directories: From 86115b9e2dcaa8efd10c7d6e3f2415ea36380cd6 Mon Sep 17 00:00:00 2001 From: troosan Date: Tue, 5 Dec 2017 00:14:57 +0100 Subject: [PATCH 16/44] update installation instructions --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f712c6c671..8580888829 100644 --- a/README.md +++ b/README.md @@ -67,10 +67,18 @@ PHPWord requires the following: ## Installation PHPWord is installed via [Composer](https://getcomposer.org/). -You just need to [add dependency](https://getcomposer.org/doc/04-schema.md#package-links>) on PHPWord into your package. +To [add a dependency](https://getcomposer.org/doc/04-schema.md#package-links>) to PHPWord in your project, either -Example: +Run the following to use the latest stable version +```sh + composer require phpoffice/phpword +``` +or if you want the latest master version +```sh + composer require phpoffice/phpword:dev-master +``` +You can of course also manually edit your composer.json file ```json { "require": { From 05e2f1bf638655793c032961d8166af993ee9c5b Mon Sep 17 00:00:00 2001 From: troosan Date: Tue, 5 Dec 2017 08:02:23 +0100 Subject: [PATCH 17/44] use non deprecated method --- src/PhpWord/Reader/MsDoc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpWord/Reader/MsDoc.php b/src/PhpWord/Reader/MsDoc.php index 297a85b4d1..c134377ad7 100644 --- a/src/PhpWord/Reader/MsDoc.php +++ b/src/PhpWord/Reader/MsDoc.php @@ -2224,7 +2224,7 @@ private function generatePhpWord() { foreach ($this->arraySections as $itmSection) { $oSection = $this->phpWord->addSection(); - $oSection->setSettings($itmSection->styleSection); + $oSection->setStyle($itmSection->styleSection); $sHYPERLINK = ''; foreach ($this->arrayParagraphs as $itmParagraph) { From 9081ed9868f517d84a83599a5ccd4325677736a1 Mon Sep 17 00:00:00 2001 From: troosan Date: Tue, 5 Dec 2017 17:40:23 +0100 Subject: [PATCH 18/44] fix warning --- src/PhpWord/Shared/Html.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index e19c3fb6c1..d448e697e5 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -527,11 +527,11 @@ private static function mapBorderStyle($cssBorderStyle) /** * Parse line break - * - * @param \PhpOffice\PhpWord\Element\AbstractContainer $element - */ + * + * @param \PhpOffice\PhpWord\Element\AbstractContainer $element + */ private static function parseLineBreak($element) { $element->addTextBreak(); } -} \ No newline at end of file +} From c079bf7f10123b3b7190c25b2deb58484da2e37a Mon Sep 17 00:00:00 2001 From: troosan Date: Tue, 5 Dec 2017 20:47:34 +0100 Subject: [PATCH 19/44] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e39ed3fe4..82f1cc5963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ This is the last version to support PHP 5.3 - Support for Comments - @troosan #1067 - Support for paragraph textAlignment - @troosan #1165 - Add support for HTML underline tag in addHtml - @zNightFalLz #1186 +- Add support for HTML
in addHtml - @anrikun @troosan #659 - Allow to change cell width unit - guillaume-ro-fr #986 - Allow to change the line height rule @troosan From f6dd78daa6b1c7d50a06c65801c54b15f598f23a Mon Sep 17 00:00:00 2001 From: troosan Date: Tue, 5 Dec 2017 21:39:28 +0100 Subject: [PATCH 20/44] update doc and changelog --- CHANGELOG.md | 3 ++- docs/elements.rst | 21 ++++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9679fafcd..0752a42a1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ This is the last version to support PHP 5.3 - Allow to change the line height rule @troosan - Implement PageBreak for odt writer @cookiekiller #863 #824 - Allow to force an update of all fields on opening a document - @troosan #951 +- Allow adding a CheckBox in a TextRun - @irond #727 ### Fixed - Loosen dependency to Zend @@ -41,7 +42,7 @@ This is the last version to support PHP 5.3 - Fix incorrect image size between windows and mac - @bskrtich #874 - Fix adding HTML table to document - @mogilvie @arivanbastos #324 -###Deprecated +### Deprecated - PhpWord->getProtection(), get it from the settings instead PhpWord->getSettings()->getDocumentProtection(); v0.13.0 (31 July 2016) diff --git a/docs/elements.rst b/docs/elements.rst index bf3eb5ac62..c73ffa0645 100644 --- a/docs/elements.rst +++ b/docs/elements.rst @@ -39,7 +39,7 @@ column shows the containers while the rows lists the elements. +-------+-----------------+-----------+----------+----------+---------+------------+------------+ | 15 | Endnote | v | - | - | v\*\* | v\*\* | - | +-------+-----------------+-----------+----------+----------+---------+------------+------------+ -| 16 | CheckBox | v | v | v | v | - | - | +| 16 | CheckBox | v | v | v | v | v | - | +-------+-----------------+-----------+----------+----------+---------+------------+------------+ | 17 | TextBox | v | v | v | v | - | - | +-------+-----------------+-----------+----------+----------+---------+------------+------------+ @@ -47,6 +47,8 @@ column shows the containers while the rows lists the elements. +-------+-----------------+-----------+----------+----------+---------+------------+------------+ | 19 | Line | v | v | v | v | v | v | +-------+-----------------+-----------+----------+----------+---------+------------+------------+ +| 20 | Chart | v | | | v | | | ++-------+-----------------+-----------+----------+----------+---------+------------+------------+ Legend: @@ -408,7 +410,7 @@ For instance for the INDEX field, you can do the following (See `Index Field for $section->addField('INDEX', array(), array('\\e " " \\h "A" \\c "3"'), $fieldText); Line ------- +---- Line elements can be added to sections by using ``addLine``. @@ -428,8 +430,21 @@ Available line style attributes: - ``height``. Line-object height in pt. - ``flip``. Flip the line element: true, false. +Chart +----- + +Charts can be added using + +.. code-block:: php + + $categories = array('A', 'B', 'C', 'D', 'E'); + $series = array(1, 3, 2, 5, 4); + $chart = $section->addChart('line', $categories, $series); + +check out the Sample_32_Chart.php for more options and styling. + Comments ---------- +-------- Comments can be added to a document by using ``addComment``. The comment can contain formatted text. Once the comment has been added, it can be linked to any element with ``setCommentStart``. From 253b0602417e14755bdfa0b4cb17b9154cd8f2bc Mon Sep 17 00:00:00 2001 From: troosan Date: Wed, 13 Dec 2017 07:47:49 +0100 Subject: [PATCH 21/44] correctly parse on/off values (w:val="true|false|1|0|on|off") --- src/PhpWord/Reader/Word2007/AbstractPart.php | 18 +++++++++++++++--- tests/PhpWord/Reader/Word2007Test.php | 8 ++++++++ tests/PhpWord/_files/documents/reader.docx | Bin 105546 -> 105726 bytes tests/PhpWord/_includes/XmlDocument.php | 1 + 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/PhpWord/Reader/Word2007/AbstractPart.php b/src/PhpWord/Reader/Word2007/AbstractPart.php index 521c8a7f61..4b7f6e0ab3 100644 --- a/src/PhpWord/Reader/Word2007/AbstractPart.php +++ b/src/PhpWord/Reader/Word2007/AbstractPart.php @@ -223,7 +223,7 @@ protected function readRun(XMLReader $xmlReader, \DOMElement $domNode, $parent, // $rIdIcon = $xmlReader->getAttribute('r:id', $domNode, 'w:object/v:shape/v:imagedata'); $target = $this->getMediaTarget($docPart, $rId); if (!is_null($target)) { - $textContent = ""; + $textContent = "<Object: {$target}>"; $parent->addText($textContent, $fontStyle, $paragraphStyle); } } else { @@ -477,9 +477,9 @@ private function readStyleDef($method, $attributeValue, $expected) if (self::READ_SIZE == $method) { $style = $attributeValue / 2; } elseif (self::READ_TRUE == $method) { - $style = true; + $style = $this->isOn($attributeValue); } elseif (self::READ_FALSE == $method) { - $style = false; + $style = !$this->isOn($attributeValue); } elseif (self::READ_EQUAL == $method) { $style = $attributeValue == $expected; } @@ -487,6 +487,18 @@ private function readStyleDef($method, $attributeValue, $expected) return $style; } + /** + * Parses the value of the on/off value, null is considered true as it means the w:val attribute was not present + * + * @see http://www.datypic.com/sc/ooxml/t-w_ST_OnOff.html + * @param string $value + * @return bool + */ + private function isOn($value = null) + { + return $value == null || $value == '1' || $value == 'true' || $value == 'on'; + } + /** * Returns the target of image, object, or link as stored in ::readMainRels * diff --git a/tests/PhpWord/Reader/Word2007Test.php b/tests/PhpWord/Reader/Word2007Test.php index 8b787247f4..9a555672f9 100644 --- a/tests/PhpWord/Reader/Word2007Test.php +++ b/tests/PhpWord/Reader/Word2007Test.php @@ -18,6 +18,7 @@ namespace PhpOffice\PhpWord\Reader; use PhpOffice\PhpWord\IOFactory; +use PhpOffice\PhpWord\TestHelperDOCX; /** * Test class for PhpOffice\PhpWord\Reader\Word2007 @@ -54,6 +55,13 @@ public function testLoad() { $filename = __DIR__ . '/../_files/documents/reader.docx'; $phpWord = IOFactory::load($filename); + $this->assertInstanceOf('PhpOffice\\PhpWord\\PhpWord', $phpWord); + $this->assertTrue($phpWord->getSettings()->hasDoNotTrackMoves()); + $this->assertFalse($phpWord->getSettings()->hasDoNotTrackFormatting()); + $this->assertEquals(100, $phpWord->getSettings()->getZoom()); + + $doc = TestHelperDOCX::getDocument($phpWord); + $this->assertFalse($doc->elementExists('/w:document/w:body/w:p/w:r[w:t/node()="italics"]/w:rPr/w:b')); } } diff --git a/tests/PhpWord/_files/documents/reader.docx b/tests/PhpWord/_files/documents/reader.docx index ef6b6615a90234c26186e5ea689de97d045c378a..65c761e0a6a7ebfc3594466155b30e98bc273072 100644 GIT binary patch delta 4496 zcmZXXWmJ@3)b{6|8R-V;Q3R#Ch7cqK6hTC!M5IG$P*NO1Lg|^2n;CIX5Tp^16zLA7 zOX)_sLBI#(|N1^_Js<9~&feEK`#O7nxYzo%mJ%J85yi^E1Ga?k=PNQm;kUPi?_&Zp z5QQF4H{f3c{)z*rFmC5A{5^=locn|c#xY{dGvJu>aF&0^3U%NN07jcg?C-I{AR?Ck zUjkD>%y*VvAl86lXeb2FA|(n{03LW^Ul>Mt7y&9^0A`FL8T*;|kSrZIapL{wfaz!y zIFk+=HK7>Q{{L#a_AdY!)gFm6&9|okiu#ZraSO-dI{cIp0*Sc^fzU$OAzm)-)*{v} zR!^O5oSz7LJ2~p#BZa`GPTZ$YzNg;g5CFanKp=1i2n4ds{v-NF>2}$<1L_wC-O!=@ z74!(s(ec(OMXXcg<-vD4>zZ7Zf|a-iPBJ$ajrod? zS$KZJT}_oln@^)MH;R?h7*ON=*Qti(NZK?^^-aBF&}5C5)Gs%XJ`pA{C8l_|KxfMJ_tGq%>p)fe|Y$iB{UP};l7SLq7+ zPurLOFuX2Mw;Nd^hibTHj|k9zTlGa3;@_j&;g1Pps^PY+&_E2;t*0;H`tkM2mVJC3 zNeyj&J2Y0zm6VfgGg4Hgsr}f8V&#TxR4EShLv0Jc_zp;}j|UGvf2OG3el=gGXN%`3 z|4RP%S!dy&c5KGc0E$!Hjp<;E9kj9rsAZG#M^ZhG->^4X&vQBxFW z{#MxewSc_uJ~al+rtqq=u{t#~TWeKSK}K)e`k89k&_y|Q*cEk}I+m$7{{Z2_Fs!}uoA z#~Xz&(-QPpvJQ(L<+k?S^3#7;lB73qG|bKVWxlWYQd#fT0FW%UEX7%CLyYBAt;mi&~A<-VHwDh%sECn6Ihez!~JeMJA z<}V7gOI4J)-}~^iE9=qOewEw(NGX8i%1^XWtc+v&9%9v-#r1$p4t`OSc!QRFk{4Sz zm&EcXw*E!Deem`4_~DQ;=3}YxuY7I<;-O6PDQ$74wB$N0N;X?UJw^U!pp#-i4#(QV z+)ARzl7s>a6uS@xJvw9?A0nw~%+!^oY_`By?bqy>%)0ZzDd4$6&uxP)_j7E<`R+W_ zGSMx$4)lEtf-KFNp)ny%au+uaBOE*Byp@y_X|Xg!H(B5Yf#JEl$IAnMnoG$mlV{)H z7mfUXuZQ|fD4&vvhF9iwHxT#hH~W<%nLhDoxCn0X2@WLD z7NhwXZ@fS~=-jM!$>Y+8^FEo*FW=wj;kN4HL`xdScwOX)AK1daZv95@yDsb<?iwRctKy>-0fm=0 zQJc?*i+u3I38ft7cV@UB-xUz%#7m_u-2H{nl3?xWp80KNzC;!IsoyY!(zK>7>L;mKY0$-UPy{y9Aoc$!0Oyc}ttNxO(NrQ450bYP9?TJu6YO@CyJD5iJrFO#$H(@4+KPN;^S;Xq1bW6ZVOB81D@=5)YXi;1zLko*oi8y-Y zyhm7AT$z~|XR&E3@lR&3%c5`F%z0m*$<^kmp`l+fB#|S9FkDZuQ!MYreSgDi)$~it zlvt6*ArqbY9pl-)AdfNlntw^xNelI>TtB_fk-jM*!nfTk@cHlUDkk!mPK%+LQI|ct z>v!94ze<~0f_AyYlT0ng?Nu@R$cudWoYs}8yr9^}rCS^8x&NLZCVfy$5z|rmNLk4> zJMHKE$zNOeW8}^fcAxK^ZY+D=$di`HnfXdF);~$&POS;*WBI>}|AY;2A6ep1T^eZ; z1#jolv_3Y#{dl{3e)bvIqG{dpCG_>jcGdB_+O7tE40dCL^z)t#o5eFbJoKB?TCb_) z-Q863ZeR7VZhoOOCa7<~U?wQUeH}xXy(%QR#8#YRV#Xnv*u}rEb15E7pO9XQZ6j7( zbI=FFlJI$t(=rrpR)>a8WDqJ)Vja3_fXM!OI~DN|EU&tQ<4bTm7TnqlGNt{fnP4Ox zkySKa<|Zr=ac$P8=!T1j;%D-5QG2+61-`GIO?+vQnY{C5ezPA6SkJmr4NaHHbW%R= z7+mrDCCA3S>>Bz%SOvW0|)` z)!uqK^c~4P8wn-V4U1o8Xfd*pc52$Vs=D02Lp@3IG0%bit6LsVc9!~JgLkYUzYt*I}>sO(Ls;6Elyg-oEnQ}eR*q;;QtjxwTm z1I2n9qi3?La94AZS0-lK)W2`VVuhN}RjWEt!$wHNtsZw2wpPsn%G{Gg8!2F1F=CM#$@P!pH4T|~t<^>|Z}lK*BWs)~`+L(*z#J4}3K z=NI}@mm#0UK`I?L!6E*jqJf?AcjhdCRPtI?!!xRV1%bDrisFuSk^~buHk;9=%tID& ze_{OmPW>9UKTc~(Omd;S%q{Gj!pMtP_LFTrJz@$Z%8hm^vENm6R%y4gSt2>7

%J zqpnLj?6~CKDg8N7l;7rZoKvPa5HaJB>;L)L@KT&{-}Y~OZp(u>=d?jP^y;@nU71)} zx{N8No5WQi4{ASaPg$4WS-qMhJV+2@)y99*(bFL0BxNbjefw^>{c^=@=@5=K8zrlG zj_BC7{+7vK%O37psL@YWbY*W}==Z9c-dHDv2k&NI(OlYs2I@Jhh+w^)S0C9U7V}Sc|v}h;C16rdjL?Wx2HO)!KgD;CtPEpzMpa zP7r4xUE0^LB`$cMemHqN0}6k>v}bI`Yn5_ae)naDY_Z6f_mX5dBcU6$=idiqt2Xth z_bZlEj0T+}y(@@vmR!HQH0Kxopf?EzvqaFnQ)fJa9u7#+j4(~=DO<;;9%g1YT-0mr zDLo|LHeqgIz!Et=P02RHFI5fgoX^)YmbM}{n;?xlymW?|tnS zX@@OMwflxuWPi*aHT~ApoSlD71=-34i0CBS7pp(x} zG@(#b{=`M|eMLzxrn+JUVIi$GeV1s17=Imkh?Hr58=GSbx6n0apo($%x<2+Na#1 zHPQZ>6|1GrGmh9^S>Lge2fOJtE<1_HJr}#AfEFfs6p!7cJ>%ZTHaDS49k-3e0UI_B zs6R7>?t}@eP)(are6#*6wM}p)V@=~I>7bRDc$eVsYSPaQ1BxP*(S_i{TiPMHw4?q8 zuX6AVN%3a6i7sP%vMHU!fl*b2iL#(Hte}g6_g8ASXWP$1#J;$nzxc4*X>tQmemrY3xB_}k8F-S(02Aqirm(;^wMmZ2(cvH0ZSsu$0~K^Ra@cq9~av7Z0TOS?V*in0IpAH#y*LZSZ^VE*TTNgotB1L7Zp4bgvhB_Hg7JXpy{ z44?^sqIiJpl*d`2yD*R#4M>1VuYt?Z2{@=72i$`RoP%HuL16o>u3N5fCLP{ z3gaY$U@lTHfsH~vJc#DQz^ zz*X>P!byNk1osjE(K9*pBtv6JfSl2_y^V#njk|a}2=H?JUj%19;c5uvgs>qW0=3_R z&aVMx_$hvava7!PoQ(Rknj?6?gA=HUT3 zFbF((3u_NZ08aur{7IMCJ_u-tKWRz{4ZuOcQ(bKYym6{8zB$pC#~@%-G{6L2NCcR{ zSPVb~E}Y&ont*^rZ-Be7d#e!8 z|Be{+KOGMD9*BzvNrh*SWXm>k8zdq!cH^-nl6|W* z*&ESFV$)9%c|JM7-yTJbj;unxF%QAp}+fiK4uNvXHQA=i|LQZITO^8_hmeW^sTEUD;13l(&i%kSqisN73tF>wSu)6W?L|epcx=otdeLDf5XTs z)5)TaQTx*mI_@jp z82#bQaIqmx{I%qRPuh;Zv5HsyY*D8;8W_=|{pmHXxe!AkMVJ%%T=`?EfWTlLo%ql6 zF^?=cs-`uYZ2lVjr+Sv2U8Ap;inHMME|U>@+N>ZMgunM4BUJOQ_lS%kod#Z1sN^;b z50oh&5pOg18)uSfBUQFhK_$(ik*odo+)m+oFGhRx_Pw|KVszUO*iL%zSDF1Vs?}|W zG}Ez!!e;mV-dKrcQO;LKqY~tItlM&`U%gVyOI(*ejY4vZYPfBmZ#*|1gFq(XJAyf? z(6Fso`!-e>0`BWD(76fldf%JMW13p#ZQS{~ywS-f^CkKlIa4+X;LKwn2kiead0#Iz zjgOGK(`^Uaj2OihwZ?suEav54pM&B^g1t=QYqL&)6)z!bM89+WRGs66Revy= zyzL|`$z7bmyI=B#qq;$sg{_yOypzgsxJt&w770Xle1t@g9c5n43px1x z^>VW$t%fIdjep$0nSP!9n`!?rry-LVub}vvV!3*%|LonKFNrI}g4~^njR=yak)_Ti zGE09Y_ri$em5K-gdu7i(q{+E@3fpjhPin`EySm)LrxNBZxhB-&G=kd1%<_UpVG%PI z6*oWk?m_(_-?l20YKx(4@A=c>hgk>by`$R=gHM-YL*^^ppRA#ge81AD#~1u>n7w9# zF9@wcY^d>XtltVpyb+kT3{2b=U4Yw*2StI;*`8E?G#7Oh-!I_T&dDdzcc3M4y$(fL4;;k)7;(i)l&+=v4KSW22mu1Mu^`=ZKZAH`??=5`j%E? zL)Q%}EM-xIiw+Tcp8H$|+t%}EmkF=gq}72MWrHbNK}Ykr>||a`F=}2jI`wMfCN!-B zq^-^tddO!xHk)4AC(1;cR9VG~z7e@)AJU}V52ImN0oh-IByi*fbT>^5IN+}rp~mRXcRS9nQEXqx29 zt5{ci`7dJ&FNA8V0kG_{#VYFH_B2VIL!tlj;y)xV#*sx2)t2U6b`IY*>`cBS`5nAM z1|346)}xM!%enrh+s)y$0zx>X-gPRcg8DSs!duT`kOM8T4<$&H`GdT&{udchU8l30 z#@&`L+!3gFE~zslJu7Tch8=hO;R>%_2{WzdakTDU*Dki(4R&Hkw$*%R z(H>-z%W>Ye@h$WI?Hs@PGlxag8GewQ7hkr{j|!)J;1ol>)niG`$KBwNj+3fML6z>4 z4I@Oz`Zry}=(*GuYlCvd$-(!ZlYjDG?Y+%zAdh(USn_$VWrS6YVbp47(SvQ-ZxHNN z36hv_0o_KNF%O+fn5NCr4Gl~XOMEb!3~9UHaY5?KU?Aa!EfrL3xnJ=$lVn0 zrWNj_S#2E?je@PtADZy6i%+t0{gXmnZ<+Q7YB7}SDBYx2p)V&I5-@efpSv~Qg;w=P zr=yq0ks1qPOZhtFMKk5}NPh3Ik|pdf(=_J%l(;;OgWr=_%IPshW#>*mrufNQPx2f1 zysLOW5|?z4+Y~Rc0cZ3uR|Oxc@oSZv>DP48Po*y9<{SCLXo0Tx^1?kBOoF2lvFm*! zX=X0x=UhZ5s*lq1wpPU@MEvk+vC>AqFN4fsaxvafaNMiH?PbQnM6uQe+ccKO{jO#7 z!wH>$3ltdS(q{O(&Z&ANb(j%caBG4Ot*F_5D~43EhSnTk&e%GO|5)PLaPKMe-g1sl zr?FrOcZ%tqvA-Se$$yl&sN^E0lSypy?i13|nh^HRQ-3@}ju-EHmBz4CIwrrG(4jqo zG2eXKkP?Mti#2-CQ(=;#7vF+y<5To8J}}g)yrGotU{Zf==2zkV+3ez9->fb1J(~H` z(-np=lI&{{X)r{&l+qq?R=DQY4m;a3O9Nx|b?$SlDH681FY6`3a$jbvP`b^@e8ob{ z>pwjvv42l(wE>j5-p~sx-mGd}h9%-nFu@WDSq}%U^pKamab+qzzlQa(Lq^Y5oMKHk z$?rP(r-m#_<0WP~zq9-;UL!|)=N9G$NB6RZ&$5smGz58 zuBTRg<{N9A6w-WNs>@Dso0;pAtHf32kXTcRF;k9@Wg{)QJw9dT%`*+jhle~O!L7AD zwW1=ddS%68FTdu;KT#TQFSTF)^>O+|6=rZ)hWy^;^4LqG0Szj3Y|zB0*0bxT7=O(r z+=R|tEW@QIx4wz%B>9IAeVvSs_o^@9h%DrzH<) zOe*{vGxE3ME*2&8mlC^&ay1?ftv$4F51*3v9z}RcCK_lM7jp^Yom;KC_GQj^#dyr~ zkK3z+N%9i4$(UkaL2S~cQDSXyc>bG*6oTsTzO}BC)tcBK52M?)?nRPoxp`=w*)rbx z(u9)sBOB9`{6LS8B z_EoN{_jP_=>+ill^zn7w_oqnh1Or)1m{-HIzt(+xLdR4Lb?-41al4<_-C)1FCdb5V zJ?zrJ0Ozz)_Y*jTN0Ho8>5dYk?!DhPMl&NO=G#6pldx{@m?kZ0aCv?FmBGRi3|{_MC_`; z((eencp+RS3MCQ*jYY0#?XjU<>%ALG?eYF2^X2gY-Bs|z&tFCYx^6^Ic~?0ta*d|C z8$?=&Xw}d2Bg(U7qaq5E21gJBTV4Y!Mb;IT@0Rt9*le@0%%^<2=z60DP#!vaE#p`C%!K#|DxiGDL;=>jZpx$&MG(R~ z%Q~F76zo4>@!gP$A6`jAb3nJ0SmwybhXvdB3Kcg@yMNrmF5^XiuC(tf8J8qqtSeSk zZIjY_Qnc>TQD9CCWxgPWuYv|8Gf7{GDo`=SCm1CUM-VdB0_RxDM@)JE5{RJP_i;*3#BM&PKxC*5ddV}k?YjT|3! zuUv1(oox3RzU~h#vX5_88B?>%f4`t!smbOqwKxb833b=odc!RRTP!~s#0=jw@@Pnd zZ+3?c&SR$Nq;zL_mvo(4_5_cxOaK%C0Tw!12$jY|7l(g?Ttam?4^DnZ_JpnicLJff z0ld_SfM;nzan|HNXqHU&f#M`ce}p?p7Yb%(1B|%UJ^qtgAxa1ohp1FK5q6c+0Pb?` zACaG12L+2C11vb}9Nv?3WX=R|f^f%}R(J3R(~hAMH+vv=QX%oXF6sDCzm^E(QQ_m>B{&qGw29q-GA}f58;2&ICmMt6QTIPzVHUs8UKGxU6a|@IxAK z21I2awT9>bowETg(E9O_Xzc?19|PJTIrB)U_kz2bfF>w+ET;Rxi^t;V?Rcmh-$HO} zd+cCeHo*61rizTC8tx&mF9VSO69JARdL)oP15gF^Qvn!QdhDPw0(L(>sySKY$&{DI zK>D1cDYM2v=N!P1%H0zHTN44UKRrG;GN-b<3E--S*}?kbp+16u02MSC05`#-ejeeE z@&fk&kbnoQsCZ%l@DLA}QMnu)EV2OQlU{I?&x}m}y{>?yqbwef0~c}t##58Y$4mSd DJ_F4a diff --git a/tests/PhpWord/_includes/XmlDocument.php b/tests/PhpWord/_includes/XmlDocument.php index c82c5a8e15..21a12105c2 100644 --- a/tests/PhpWord/_includes/XmlDocument.php +++ b/tests/PhpWord/_includes/XmlDocument.php @@ -97,6 +97,7 @@ public function getNodeList($path, $file = 'word/document.xml') if (null === $this->xpath) { $this->xpath = new \DOMXpath($this->dom); + $this->xpath->registerNamespace('w14', 'http://schemas.microsoft.com/office/word/2010/wordml'); } return $this->xpath->query($path); From dc7cb1ee75b2edcc3ab1c19671f3b245ec353e3f Mon Sep 17 00:00:00 2001 From: troosan Date: Wed, 13 Dec 2017 22:48:58 +0100 Subject: [PATCH 22/44] update changelog & doc --- CHANGELOG.md | 1 + src/PhpWord/Element/Comment.php | 1 + src/PhpWord/Element/TrackChange.php | 1 + src/PhpWord/Reader/Word2007/AbstractPart.php | 4 ++-- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dc8b45879..51f9243484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ This is the last version to support PHP 5.3 - Padded the $args array to remove error - @kaigoh #1150, @reformed #870 - Fix incorrect image size between windows and mac - @bskrtich #874 - Fix adding HTML table to document - @mogilvie @arivanbastos #324 +- Fix parsing on/off values (w:val="true|false|1|0|on|off") - @troosan #1221 #1219 ### Deprecated - PhpWord->getProtection(), get it from the settings instead PhpWord->getSettings()->getDocumentProtection(); diff --git a/src/PhpWord/Element/Comment.php b/src/PhpWord/Element/Comment.php index 908b87854f..188369292c 100644 --- a/src/PhpWord/Element/Comment.php +++ b/src/PhpWord/Element/Comment.php @@ -19,6 +19,7 @@ /** * Comment element + * @see http://datypic.com/sc/ooxml/t-w_CT_Comment.html */ class Comment extends TrackChange { diff --git a/src/PhpWord/Element/TrackChange.php b/src/PhpWord/Element/TrackChange.php index 9ed623f9cd..d14fc201db 100644 --- a/src/PhpWord/Element/TrackChange.php +++ b/src/PhpWord/Element/TrackChange.php @@ -19,6 +19,7 @@ /** * TrackChange element + * @see http://datypic.com/sc/ooxml/t-w_CT_TrackChange.html */ class TrackChange extends AbstractContainer { diff --git a/src/PhpWord/Reader/Word2007/AbstractPart.php b/src/PhpWord/Reader/Word2007/AbstractPart.php index 4b7f6e0ab3..6a48fd4681 100644 --- a/src/PhpWord/Reader/Word2007/AbstractPart.php +++ b/src/PhpWord/Reader/Word2007/AbstractPart.php @@ -384,7 +384,7 @@ protected function readTableStyle(XMLReader $xmlReader, \DOMElement $domNode) { $style = null; $margins = array('top', 'left', 'bottom', 'right'); - $borders = $margins + array('insideH', 'insideV'); + $borders = array_merge($margins, array('insideH', 'insideV')); if ($xmlReader->elementExists('w:tblPr', $domNode)) { if ($xmlReader->elementExists('w:tblPr/w:tblStyle', $domNode)) { @@ -422,7 +422,7 @@ private function readCellStyle(XMLReader $xmlReader, \DOMElement $domNode) 'textDirection' => array(self::READ_VALUE, 'w:textDirection'), 'gridSpan' => array(self::READ_VALUE, 'w:gridSpan'), 'vMerge' => array(self::READ_VALUE, 'w:vMerge'), - 'bgColor' => array(self::READ_VALUE, 'w:shd/w:fill'), + 'bgColor' => array(self::READ_VALUE, 'w:shd', 'w:fill'), ); return $this->readStyleDefs($xmlReader, $domNode, $styleDefs); From 9e029415cc3eafad7581346b8ed84e05d7674b08 Mon Sep 17 00:00:00 2001 From: troosan Date: Wed, 13 Dec 2017 23:17:01 +0100 Subject: [PATCH 23/44] align with pull request submitted in PHPOffice/Commom --- src/PhpWord/Metadata/Protection.php | 23 ++++--- .../Shared/Microsoft/PasswordEncoder.php | 68 ++++++++++++------- src/PhpWord/Writer/Word2007/Part/Settings.php | 2 +- .../Shared/Microsoft/PasswordEncoderTest.php | 6 +- 4 files changed, 58 insertions(+), 41 deletions(-) diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index 09d08aac03..bb1cc1ad23 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -18,6 +18,7 @@ namespace PhpOffice\PhpWord\Metadata; use PhpOffice\PhpWord\SimpleType\DocProtect; +use PhpOffice\PhpWord\Shared\Microsoft\PasswordEncoder; /** * Document protection class @@ -50,11 +51,11 @@ class Protection private $spinCount = 100000; /** - * Cryptographic Hashing Algorithm (see to \PhpOffice\PhpWord\Writer\Word2007\Part\Settings::$algorithmMapping) + * Cryptographic Hashing Algorithm (see constants defined in \PhpOffice\PhpWord\Shared\Microsoft\PasswordEncoder) * - * @var int + * @var string */ - private $mswordAlgorithmSid = 4; + private $algorithm = PasswordEncoder::ALGORITHM_SHA_1; /** * Salt for Password Verifier @@ -146,24 +147,24 @@ public function setSpinCount($spinCount) } /** - * Get algorithm-sid + * Get algorithm * - * @return int + * @return string */ - public function getMswordAlgorithmSid() + public function getAlgorithm() { - return $this->mswordAlgorithmSid; + return $this->algorithm; } /** - * Set algorithm-sid (see \PhpOffice\PhpWord\Writer\Word2007\Part\Settings::$algorithmMapping) + * Set algorithm * - * @param $mswordAlgorithmSid + * @param $algorithm * @return self */ - public function setMswordAlgorithmSid($mswordAlgorithmSid) + public function setMswordAlgorithmSid($algorithm) { - $this->mswordAlgorithmSid = $mswordAlgorithmSid; + $this->algorithm = $algorithm; return $this; } diff --git a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php index cddcfcd3c6..a3ba345c31 100644 --- a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php +++ b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php @@ -22,21 +22,36 @@ */ class PasswordEncoder { + const ALGORITHM_MD2 = 'MD2'; + const ALGORITHM_MD4 = 'MD4'; + const ALGORITHM_MD5 = 'MD5'; + const ALGORITHM_SHA_1 = 'SHA-1'; + const ALGORITHM_SHA_256 = 'SHA-256'; + const ALGORITHM_SHA_384 = 'SHA-384'; + const ALGORITHM_SHA_512 = 'SHA-512'; + const ALGORITHM_RIPEMD = 'RIPEMD'; + const ALGORITHM_RIPEMD_160 = 'RIPEMD-160'; + const ALGORITHM_MAC = 'MAC'; + const ALGORITHM_HMAC= 'HMAC'; + + /** + * Mapping between algorithm name and algorithm ID + * + * @var array + * @see https://msdn.microsoft.com/en-us/library/documentformat.openxml.wordprocessing.writeprotection.cryptographicalgorithmsid(v=office.14).aspx + */ private static $algorithmMapping = array( - 1 => 'md2', - 2 => 'md4', - 3 => 'md5', - 4 => 'sha1', - 5 => '', // 'mac' -> not possible with hash() - 6 => 'ripemd', - 7 => 'ripemd160', - 8 => '', - 9 => '', //'hmac' -> not possible with hash() - 10 => '', - 11 => '', - 12 => 'sha256', - 13 => 'sha384', - 14 => 'sha512', + self::ALGORITHM_MD2 => array(1, 'md2'), + self::ALGORITHM_MD4 => array(2, 'md4'), + self::ALGORITHM_MD5 => array(3, 'md5'), + self::ALGORITHM_SHA_1 => array(4, 'sha1'), + self::ALGORITHM_MAC => array(5, ''), // 'mac' -> not possible with hash() + self::ALGORITHM_RIPEMD => array(6, 'ripemd'), + self::ALGORITHM_RIPEMD_160 => array(7, 'ripemd160'), + self::ALGORITHM_HMAC => array(9, ''), //'hmac' -> not possible with hash() + self::ALGORITHM_SHA_256 => array(12, 'sha256'), + self::ALGORITHM_SHA_384 => array(13, 'sha384'), + self::ALGORITHM_SHA_512 => array(14, 'sha512'), ); private static $initialCodeArray = array( @@ -82,12 +97,12 @@ class PasswordEncoder * @see https://blogs.msdn.microsoft.com/vsod/2010/04/05/how-to-set-the-editing-restrictions-in-word-using-open-xml-sdk-2-0/ * * @param string $password - * @param number $algorithmSid + * @param string $algorithmName * @param string $salt - * @param number $spinCount + * @param integer $spinCount * @return string */ - public static function hashPassword($password, $algorithmSid = 4, $salt = null, $spinCount = 10000) + public static function hashPassword($password, $algorithmName = PasswordEncoder::ALGORITHM_SHA_1, $salt = null, $spinCount = 10000) { $origEncoding = mb_internal_encoding(); mb_internal_encoding('UTF-8'); @@ -118,7 +133,7 @@ public static function hashPassword($password, $algorithmSid = 4, $salt = null, // Implementation Notes List: // Word requires that the initial hash of the password with the salt not be considered in the count. // The initial hash of salt + key is not included in the iteration count. - $algorithm = self::getAlgorithm($algorithmSid); + $algorithm = self::getAlgorithm($algorithmName); $generatedKey = hash($algorithm, $salt . $generatedKey, true); for ($i = 0; $i < $spinCount; $i++) { @@ -134,12 +149,12 @@ public static function hashPassword($password, $algorithmSid = 4, $salt = null, /** * Get algorithm from self::$algorithmMapping * - * @param int $sid + * @param string $algorithmName * @return string */ - private static function getAlgorithm($sid) + private static function getAlgorithm($algorithmName) { - $algorithm = self::$algorithmMapping[$sid]; + $algorithm = self::$algorithmMapping[$algorithmName][1]; if ($algorithm == '') { $algorithm = 'sha1'; } @@ -155,16 +170,17 @@ private static function getAlgorithm($sid) */ private static function buildCombinedKey($byteChars) { + $byteCharsLength = count($byteChars); // Compute the high-order word // Initialize from the initial code array (see above), depending on the passwords length. - $highOrderWord = self::$initialCodeArray[count($byteChars) - 1]; + $highOrderWord = self::$initialCodeArray[$byteCharsLength - 1]; // For each character in the password: // For every bit in the character, starting with the least significant and progressing to (but excluding) // the most significant, if the bit is set, XOR the key’s high-order word with the corresponding word from // the Encryption Matrix - for ($i = 0; $i < count($byteChars); $i++) { - $tmp = self::$passwordMaxLength - count($byteChars) + $i; + for ($i = 0; $i < $byteCharsLength; $i++) { + $tmp = self::$passwordMaxLength - $byteCharsLength + $i; $matrixRow = self::$encryptionMatrix[$tmp]; for ($intBit = 0; $intBit < 7; $intBit++) { if (($byteChars[$i] & (0x0001 << $intBit)) != 0) { @@ -177,12 +193,12 @@ private static function buildCombinedKey($byteChars) // Initialize with 0 $lowOrderWord = 0; // For each character in the password, going backwards - for ($i = count($byteChars) - 1; $i >= 0; $i--) { + for ($i = $byteCharsLength - 1; $i >= 0; $i--) { // low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR character $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteChars[$i]); } // Lastly, low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR strPassword length XOR 0xCE4B. - $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ count($byteChars) ^ 0xCE4B); + $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteCharsLength ^ 0xCE4B); // Combine the Low and High Order Word return self::int32(($highOrderWord << 16) + $lowOrderWord); diff --git a/src/PhpWord/Writer/Word2007/Part/Settings.php b/src/PhpWord/Writer/Word2007/Part/Settings.php index 565aab2c13..f292583e83 100644 --- a/src/PhpWord/Writer/Word2007/Part/Settings.php +++ b/src/PhpWord/Writer/Word2007/Part/Settings.php @@ -193,7 +193,7 @@ private function setDocumentProtection($documentProtection) if ($documentProtection->getSalt() == null) { $documentProtection->setSalt(openssl_random_pseudo_bytes(16)); } - $passwordHash = PasswordEncoder::hashPassword($documentProtection->getPassword(), $documentProtection->getMswordAlgorithmSid(), $documentProtection->getSalt(), $documentProtection->getSpinCount()); + $passwordHash = PasswordEncoder::hashPassword($documentProtection->getPassword(), $documentProtection->getAlgorithm(), $documentProtection->getSalt(), $documentProtection->getSpinCount()); $this->settings['w:documentProtection'] = array( '@attributes' => array( 'w:enforcement' => 1, diff --git a/tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php b/tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php index 7b2bd3e78d..c42a6eb4f6 100644 --- a/tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php +++ b/tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php @@ -51,7 +51,7 @@ public function testEncodePasswordWithSalt() $salt = base64_decode('uq81pJRRGFIY5U+E9gt8tA=='); //when - $hashPassword = PasswordEncoder::hashPassword($password, 4, $salt); + $hashPassword = PasswordEncoder::hashPassword($password, PasswordEncoder::ALGORITHM_SHA_1, $salt); //then TestCase::assertEquals('QiDOcpia1YzSVJPiKPwWebl9p/0=', $hashPassword); @@ -67,7 +67,7 @@ public function testDafaultsToSha1IfUnsupportedAlgorithm() $salt = base64_decode('uq81pJRRGFIY5U+E9gt8tA=='); //when - $hashPassword = PasswordEncoder::hashPassword($password, 5, $salt); + $hashPassword = PasswordEncoder::hashPassword($password, PasswordEncoder::ALGORITHM_MAC, $salt); //then TestCase::assertEquals('QiDOcpia1YzSVJPiKPwWebl9p/0=', $hashPassword); @@ -83,7 +83,7 @@ public function testEncodePasswordWithNullAsciiCodeInPassword() $salt = base64_decode('uq81pJRRGFIY5U+E9gt8tA=='); //when - $hashPassword = PasswordEncoder::hashPassword($password, 5, $salt, 1); + $hashPassword = PasswordEncoder::hashPassword($password, PasswordEncoder::ALGORITHM_MAC, $salt, 1); //then TestCase::assertEquals('rDV9sgdDsztoCQlvRCb1lF2wxNg=', $hashPassword); From f7d2ad7201bc91f79253a6e0fdbcdaf0154679d5 Mon Sep 17 00:00:00 2001 From: troosan Date: Wed, 13 Dec 2017 23:24:37 +0100 Subject: [PATCH 24/44] formatting --- src/PhpWord/Metadata/Protection.php | 2 +- .../Shared/Microsoft/PasswordEncoder.php | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index bb1cc1ad23..634751fb3b 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -17,8 +17,8 @@ namespace PhpOffice\PhpWord\Metadata; -use PhpOffice\PhpWord\SimpleType\DocProtect; use PhpOffice\PhpWord\Shared\Microsoft\PasswordEncoder; +use PhpOffice\PhpWord\SimpleType\DocProtect; /** * Document protection class diff --git a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php index a3ba345c31..a6a607a1fa 100644 --- a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php +++ b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php @@ -32,7 +32,7 @@ class PasswordEncoder const ALGORITHM_RIPEMD = 'RIPEMD'; const ALGORITHM_RIPEMD_160 = 'RIPEMD-160'; const ALGORITHM_MAC = 'MAC'; - const ALGORITHM_HMAC= 'HMAC'; + const ALGORITHM_HMAC = 'HMAC'; /** * Mapping between algorithm name and algorithm ID @@ -41,17 +41,17 @@ class PasswordEncoder * @see https://msdn.microsoft.com/en-us/library/documentformat.openxml.wordprocessing.writeprotection.cryptographicalgorithmsid(v=office.14).aspx */ private static $algorithmMapping = array( - self::ALGORITHM_MD2 => array(1, 'md2'), - self::ALGORITHM_MD4 => array(2, 'md4'), - self::ALGORITHM_MD5 => array(3, 'md5'), - self::ALGORITHM_SHA_1 => array(4, 'sha1'), - self::ALGORITHM_MAC => array(5, ''), // 'mac' -> not possible with hash() - self::ALGORITHM_RIPEMD => array(6, 'ripemd'), + self::ALGORITHM_MD2 => array(1, 'md2'), + self::ALGORITHM_MD4 => array(2, 'md4'), + self::ALGORITHM_MD5 => array(3, 'md5'), + self::ALGORITHM_SHA_1 => array(4, 'sha1'), + self::ALGORITHM_MAC => array(5, ''), // 'mac' -> not possible with hash() + self::ALGORITHM_RIPEMD => array(6, 'ripemd'), self::ALGORITHM_RIPEMD_160 => array(7, 'ripemd160'), - self::ALGORITHM_HMAC => array(9, ''), //'hmac' -> not possible with hash() - self::ALGORITHM_SHA_256 => array(12, 'sha256'), - self::ALGORITHM_SHA_384 => array(13, 'sha384'), - self::ALGORITHM_SHA_512 => array(14, 'sha512'), + self::ALGORITHM_HMAC => array(9, ''), //'hmac' -> not possible with hash() + self::ALGORITHM_SHA_256 => array(12, 'sha256'), + self::ALGORITHM_SHA_384 => array(13, 'sha384'), + self::ALGORITHM_SHA_512 => array(14, 'sha512'), ); private static $initialCodeArray = array( @@ -99,10 +99,10 @@ class PasswordEncoder * @param string $password * @param string $algorithmName * @param string $salt - * @param integer $spinCount + * @param int $spinCount * @return string */ - public static function hashPassword($password, $algorithmName = PasswordEncoder::ALGORITHM_SHA_1, $salt = null, $spinCount = 10000) + public static function hashPassword($password, $algorithmName = self::ALGORITHM_SHA_1, $salt = null, $spinCount = 10000) { $origEncoding = mb_internal_encoding(); mb_internal_encoding('UTF-8'); From 5a57409df028bb609f9f180424c0a0f489334b6f Mon Sep 17 00:00:00 2001 From: troosan Date: Wed, 13 Dec 2017 23:55:48 +0100 Subject: [PATCH 25/44] fix tests --- src/PhpWord/Metadata/Protection.php | 2 +- src/PhpWord/Shared/Microsoft/PasswordEncoder.php | 11 +++++++++++ src/PhpWord/Writer/Word2007/Part/Settings.php | 4 ++-- tests/PhpWord/Writer/Word2007/Part/SettingsTest.php | 3 ++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index 634751fb3b..35391cb20b 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -162,7 +162,7 @@ public function getAlgorithm() * @param $algorithm * @return self */ - public function setMswordAlgorithmSid($algorithm) + public function setAlgorithm($algorithm) { $this->algorithm = $algorithm; diff --git a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php index a6a607a1fa..d3a03d9740 100644 --- a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php +++ b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php @@ -162,6 +162,17 @@ private static function getAlgorithm($algorithmName) return $algorithm; } + /** + * Returns the algorithm ID + * + * @param sting $algorithmName + * @return int + */ + public static function getAlgorithmId($algorithmName) + { + return self::$algorithmMapping[$algorithmName][0]; + } + /** * Build combined key from low-order word and high-order word * diff --git a/src/PhpWord/Writer/Word2007/Part/Settings.php b/src/PhpWord/Writer/Word2007/Part/Settings.php index 6ac5ec4aa0..e56e2612bd 100644 --- a/src/PhpWord/Writer/Word2007/Part/Settings.php +++ b/src/PhpWord/Writer/Word2007/Part/Settings.php @@ -183,7 +183,7 @@ private function setOnOffValue($settingName, $booleanValue) private function setDocumentProtection($documentProtection) { if ($documentProtection->getEditing() !== null) { - if (empty($documentProtection->getPassword())) { + if ($documentProtection->getPassword() == null) { $this->settings['w:documentProtection'] = array( '@attributes' => array( 'w:enforcement' => 1, @@ -202,7 +202,7 @@ private function setDocumentProtection($documentProtection) 'w:cryptProviderType' => 'rsaFull', 'w:cryptAlgorithmClass' => 'hash', 'w:cryptAlgorithmType' => 'typeAny', - 'w:cryptAlgorithmSid' => $documentProtection->getMswordAlgorithmSid(), + 'w:cryptAlgorithmSid' => PasswordEncoder::getAlgorithmId($documentProtection->getAlgorithm()), 'w:cryptSpinCount' => $documentProtection->getSpinCount(), 'w:hash' => $passwordHash, 'w:salt' => base64_encode($documentProtection->getSalt()), diff --git a/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php b/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php index 7a355042d4..1e6af56792 100644 --- a/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php +++ b/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php @@ -24,6 +24,7 @@ use PhpOffice\PhpWord\SimpleType\Zoom; use PhpOffice\PhpWord\Style\Language; use PhpOffice\PhpWord\TestHelperDOCX; +use PhpOffice\PhpWord\Shared\Microsoft\PasswordEncoder; /** * Test class for PhpOffice\PhpWord\Writer\Word2007\Part\Settings @@ -65,7 +66,7 @@ public function testDocumentProtectionWithPassword() $phpWord->getSettings()->getDocumentProtection()->setEditing('readOnly'); $phpWord->getSettings()->getDocumentProtection()->setPassword('testÄö@€!$&'); $phpWord->getSettings()->getDocumentProtection()->setSalt(base64_decode('uq81pJRRGFIY5U+E9gt8tA==')); - $phpWord->getSettings()->getDocumentProtection()->setMswordAlgorithmSid(1); + $phpWord->getSettings()->getDocumentProtection()->setAlgorithm(PasswordEncoder::ALGORITHM_MD2); $phpWord->getSettings()->getDocumentProtection()->setSpinCount(10); $doc = TestHelperDOCX::getDocument($phpWord); From 5d5362a3fda20d3e79c089510a19e696d836cf63 Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 14 Dec 2017 00:15:23 +0100 Subject: [PATCH 26/44] sort imports --- tests/PhpWord/Writer/Word2007/Part/SettingsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php b/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php index 1e6af56792..50b444b80a 100644 --- a/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php +++ b/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php @@ -21,10 +21,10 @@ use PhpOffice\PhpWord\ComplexType\TrackChangesView; use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\Settings; +use PhpOffice\PhpWord\Shared\Microsoft\PasswordEncoder; use PhpOffice\PhpWord\SimpleType\Zoom; use PhpOffice\PhpWord\Style\Language; use PhpOffice\PhpWord\TestHelperDOCX; -use PhpOffice\PhpWord\Shared\Microsoft\PasswordEncoder; /** * Test class for PhpOffice\PhpWord\Writer\Word2007\Part\Settings From 87acd3764bb0a3ce435475cca9b3a123d8df944f Mon Sep 17 00:00:00 2001 From: Gabriel Caruso Date: Thu, 14 Dec 2017 12:21:16 -0200 Subject: [PATCH 27/44] Clean elses --- src/PhpWord/Shared/OLERead.php | 32 ++++++++++++------------ src/PhpWord/Shared/PCLZip/pclzip.lib.php | 20 +++++++-------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/PhpWord/Shared/OLERead.php b/src/PhpWord/Shared/OLERead.php index 1321b8daa7..bcdda0c379 100644 --- a/src/PhpWord/Shared/OLERead.php +++ b/src/PhpWord/Shared/OLERead.php @@ -115,7 +115,7 @@ public function read($sFileName) $bbdBlocks = (self::BIG_BLOCK_SIZE - self::BIG_BLOCK_DEPOT_BLOCKS_POS)/4; } // @codeCoverageIgnoreEnd - + for ($i = 0; $i < $bbdBlocks; ++$i) { $bigBlockDepotBlocks[$i] = self::getInt4d($this->data, $pos); $pos += 4; @@ -193,26 +193,26 @@ public function getStream($stream) } return $streamData; - } else { - $numBlocks = $this->props[$stream]['size'] / self::BIG_BLOCK_SIZE; - if ($this->props[$stream]['size'] % self::BIG_BLOCK_SIZE != 0) { - ++$numBlocks; - } + } - if ($numBlocks == 0) { - return '';// @codeCoverageIgnore - } + $numBlocks = $this->props[$stream]['size'] / self::BIG_BLOCK_SIZE; + if ($this->props[$stream]['size'] % self::BIG_BLOCK_SIZE != 0) { + ++$numBlocks; + } - $block = $this->props[$stream]['startBlock']; + if ($numBlocks == 0) { + return '';// @codeCoverageIgnore + } - while ($block != -2) { - $pos = ($block + 1) * self::BIG_BLOCK_SIZE; - $streamData .= substr($this->data, $pos, self::BIG_BLOCK_SIZE); - $block = self::getInt4d($this->bigBlockChain, $block*4); - } + $block = $this->props[$stream]['startBlock']; - return $streamData; + while ($block != -2) { + $pos = ($block + 1) * self::BIG_BLOCK_SIZE; + $streamData .= substr($this->data, $pos, self::BIG_BLOCK_SIZE); + $block = self::getInt4d($this->bigBlockChain, $block*4); } + + return $streamData; } /** diff --git a/src/PhpWord/Shared/PCLZip/pclzip.lib.php b/src/PhpWord/Shared/PCLZip/pclzip.lib.php index 5620c754e8..3fbc932744 100644 --- a/src/PhpWord/Shared/PCLZip/pclzip.lib.php +++ b/src/PhpWord/Shared/PCLZip/pclzip.lib.php @@ -1244,9 +1244,9 @@ public function errorCode() { if (PCLZIP_ERROR_EXTERNAL == 1) { return (PclErrorCode()); - } else { - return ($this->error_code); } + + return ($this->error_code); } // -------------------------------------------------------------------------------- @@ -1289,9 +1289,9 @@ public function errorName($p_with_code = false) if ($p_with_code) { return ($v_value . ' (' . $this->error_code . ')'); - } else { - return ($v_value); } + + return ($v_value); } // -------------------------------------------------------------------------------- @@ -1304,13 +1304,13 @@ public function errorInfo($p_full = false) { if (PCLZIP_ERROR_EXTERNAL == 1) { return (PclErrorString()); - } else { - if ($p_full) { - return ($this->errorName(true) . " : " . $this->error_string); - } else { - return ($this->error_string . " [code " . $this->error_code . "]"); - } } + + if ($p_full) { + return ($this->errorName(true) . " : " . $this->error_string); + } + + return ($this->error_string . " [code " . $this->error_code . "]"); } // -------------------------------------------------------------------------------- From 46a037ebd0ace99f10052d19aa6511a85b471f69 Mon Sep 17 00:00:00 2001 From: troosan Date: Mon, 18 Dec 2017 17:02:34 +0100 Subject: [PATCH 28/44] add composer scripts --- composer.json | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 70b60b46e6..41730374f4 100644 --- a/composer.json +++ b/composer.json @@ -34,12 +34,22 @@ "name": "Antoine de Troostembergh" } ], + "scripts": { + "check": [ + "./vendor/bin/php-cs-fixer fix --ansi --dry-run --diff", + "./vendor/bin/phpcs --report-width=200 --report-summary --report-full samples/ src/ tests/ --ignore=src/PhpWord/Shared/PCLZip --standard=PSR2 -n", + "./vendor/bin/phpmd src/,tests/ text ./phpmd.xml.dist --exclude pclzip.lib.php", + "./vendor/bin/phpunit --color=always" + ], + "fix": [ + "./vendor/bin/php-cs-fixer fix --ansi" + ] + }, "require": { "php": ">=5.3.3", "ext-xml": "*", "zendframework/zend-escaper": "^2.2", - "zendframework/zend-stdlib": "^2.2 || ^3.0", - "phpoffice/common": "^0.2" + "zendframework/zend-stdlib": "^2.2 || ^3.0" }, "require-dev": { "phpunit/phpunit": "^4.8.36", From 81dd5a20e5df19f971c9580df92627365b7f4d73 Mon Sep 17 00:00:00 2001 From: troosan Date: Mon, 18 Dec 2017 22:28:07 +0100 Subject: [PATCH 29/44] rename ObjectElement to OLEObject --- src/PhpWord/Element/AbstractContainer.php | 6 ++--- src/PhpWord/Element/AbstractElement.php | 6 ++--- .../{ObjectElement.php => OLEObject.php} | 4 ++-- .../{ObjectElement.php => OLEObject.php} | 6 ++--- tests/PhpWord/Element/CellTest.php | 2 +- tests/PhpWord/Element/ObjectTest.php | 22 +++++++++---------- tests/PhpWord/Element/SectionTest.php | 2 +- tests/PhpWord/Writer/Word2007/ElementTest.php | 2 +- 8 files changed, 25 insertions(+), 25 deletions(-) rename src/PhpWord/Element/{ObjectElement.php => OLEObject.php} (98%) rename src/PhpWord/Writer/Word2007/Element/{ObjectElement.php => OLEObject.php} (95%) diff --git a/src/PhpWord/Element/AbstractContainer.php b/src/PhpWord/Element/AbstractContainer.php index 1dabda7772..540483437b 100644 --- a/src/PhpWord/Element/AbstractContainer.php +++ b/src/PhpWord/Element/AbstractContainer.php @@ -37,7 +37,7 @@ * @method PageBreak addPageBreak() * @method Table addTable(mixed $style = null) * @method Image addImage(string $source, mixed $style = null, bool $isWatermark = false) - * @method \PhpOffice\PhpWord\Element\ObjectElement addObject(string $source, mixed $style = null) + * @method \PhpOffice\PhpWord\Element\OLEObject addObject(string $source, mixed $style = null) * @method TextBox addTextBox(mixed $style = null) * @method Field addField(string $type = null, array $properties = array(), array $options = array(), mixed $text = null) * @method Line addLine(mixed $lineStyle = null) @@ -87,7 +87,7 @@ public function __call($function, $args) ); $functions = array(); foreach ($elements as $element) { - $functions['add' . strtolower($element)] = $element == 'Object' ? 'ObjectElement' : $element; + $functions['add' . strtolower($element)] = $element == 'Object' ? 'OLEObject' : $element; } // Run valid `add` command @@ -193,7 +193,7 @@ private function checkValidity($method) 'Link' => $generalContainers, 'TextBreak' => $generalContainers, 'Image' => $generalContainers, - 'ObjectElement' => $generalContainers, + 'OLEObject' => $generalContainers, 'Field' => $generalContainers, 'Line' => $generalContainers, 'Shape' => $generalContainers, diff --git a/src/PhpWord/Element/AbstractElement.php b/src/PhpWord/Element/AbstractElement.php index 042f985e9a..df89dd24c3 100644 --- a/src/PhpWord/Element/AbstractElement.php +++ b/src/PhpWord/Element/AbstractElement.php @@ -358,12 +358,12 @@ public function setParentContainer(AbstractElement $container) */ private function setMediaRelation() { - if (!$this instanceof Link && !$this instanceof Image && !$this instanceof ObjectElement) { + if (!$this instanceof Link && !$this instanceof Image && !$this instanceof OLEObject) { return; } $elementName = substr(get_class($this), strrpos(get_class($this), '\\') + 1); - if ($elementName == 'ObjectElement') { + if ($elementName == 'OLEObject') { $elementName = 'Object'; } $mediaPart = $this->getMediaPart(); @@ -375,7 +375,7 @@ private function setMediaRelation() $rId = Media::addElement($mediaPart, strtolower($elementName), $source, $image); $this->setRelationId($rId); - if ($this instanceof ObjectElement) { + if ($this instanceof OLEObject) { $icon = $this->getIcon(); $rId = Media::addElement($mediaPart, 'image', $icon, new Image($icon)); $this->setImageRelationId($rId); diff --git a/src/PhpWord/Element/ObjectElement.php b/src/PhpWord/Element/OLEObject.php similarity index 98% rename from src/PhpWord/Element/ObjectElement.php rename to src/PhpWord/Element/OLEObject.php index a405c405f6..5da94c3a82 100644 --- a/src/PhpWord/Element/ObjectElement.php +++ b/src/PhpWord/Element/OLEObject.php @@ -21,9 +21,9 @@ use PhpOffice\PhpWord\Style\Image as ImageStyle; /** - * ObjectElement element + * OLEObject element */ -class ObjectElement extends AbstractElement +class OLEObject extends AbstractElement { /** * Ole-Object Src diff --git a/src/PhpWord/Writer/Word2007/Element/ObjectElement.php b/src/PhpWord/Writer/Word2007/Element/OLEObject.php similarity index 95% rename from src/PhpWord/Writer/Word2007/Element/ObjectElement.php rename to src/PhpWord/Writer/Word2007/Element/OLEObject.php index 4fca9f5321..50891d97f4 100644 --- a/src/PhpWord/Writer/Word2007/Element/ObjectElement.php +++ b/src/PhpWord/Writer/Word2007/Element/OLEObject.php @@ -20,11 +20,11 @@ use PhpOffice\PhpWord\Writer\Word2007\Style\Image as ImageStyleWriter; /** - * ObjectElement element writer + * OLEObject element writer * * @since 0.10.0 */ -class ObjectElement extends AbstractElement +class OLEObject extends AbstractElement { /** * Write object element. @@ -33,7 +33,7 @@ public function write() { $xmlWriter = $this->getXmlWriter(); $element = $this->getElement(); - if (!$element instanceof \PhpOffice\PhpWord\Element\ObjectElement) { + if (!$element instanceof \PhpOffice\PhpWord\Element\OLEObject) { return; } diff --git a/tests/PhpWord/Element/CellTest.php b/tests/PhpWord/Element/CellTest.php index 11a75ff7e7..a1132cfad8 100644 --- a/tests/PhpWord/Element/CellTest.php +++ b/tests/PhpWord/Element/CellTest.php @@ -181,7 +181,7 @@ public function testAddObjectXLS() $element = $oCell->addObject($src); $this->assertCount(1, $oCell->getElements()); - $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\ObjectElement', $element); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\OLEObject', $element); } /** diff --git a/tests/PhpWord/Element/ObjectTest.php b/tests/PhpWord/Element/ObjectTest.php index 7e21795634..ba761b70c4 100644 --- a/tests/PhpWord/Element/ObjectTest.php +++ b/tests/PhpWord/Element/ObjectTest.php @@ -18,9 +18,9 @@ namespace PhpOffice\PhpWord\Element; /** - * Test class for PhpOffice\PhpWord\Element\ObjectElement + * Test class for PhpOffice\PhpWord\Element\OLEObject * - * @coversDefaultClass \PhpOffice\PhpWord\Element\ObjectElement + * @coversDefaultClass \PhpOffice\PhpWord\Element\OLEObject * @runTestsInSeparateProcesses */ class ObjectTest extends \PHPUnit\Framework\TestCase @@ -31,9 +31,9 @@ class ObjectTest extends \PHPUnit\Framework\TestCase public function testConstructWithSupportedFiles() { $src = __DIR__ . '/../_files/documents/reader.docx'; - $oObject = new ObjectElement($src); + $oObject = new OLEObject($src); - $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\ObjectElement', $oObject); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\OLEObject', $oObject); $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Image', $oObject->getStyle()); $this->assertEquals($src, $oObject->getSource()); } @@ -44,9 +44,9 @@ public function testConstructWithSupportedFiles() public function testConstructWithSupportedFilesLong() { $src = __DIR__ . '/../_files/documents/sheet.xls'; - $oObject = new ObjectElement($src); + $oObject = new OLEObject($src); - $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\ObjectElement', $oObject); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\OLEObject', $oObject); $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Image', $oObject->getStyle()); $this->assertEquals($src, $oObject->getSource()); } @@ -59,7 +59,7 @@ public function testConstructWithSupportedFilesLong() public function testConstructWithNotSupportedFiles() { $src = __DIR__ . '/../_files/xsl/passthrough.xsl'; - $oObject = new ObjectElement($src); + $oObject = new OLEObject($src); $oObject->getSource(); } @@ -69,9 +69,9 @@ public function testConstructWithNotSupportedFiles() public function testConstructWithSupportedFilesAndStyle() { $src = __DIR__ . '/../_files/documents/sheet.xls'; - $oObject = new ObjectElement($src, array('width' => '230px')); + $oObject = new OLEObject($src, array('width' => '230px')); - $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\ObjectElement', $oObject); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\OLEObject', $oObject); $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Image', $oObject->getStyle()); $this->assertEquals($src, $oObject->getSource()); } @@ -82,7 +82,7 @@ public function testConstructWithSupportedFilesAndStyle() public function testRelationId() { $src = __DIR__ . '/../_files/documents/sheet.xls'; - $oObject = new ObjectElement($src); + $oObject = new OLEObject($src); $iVal = rand(1, 1000); $oObject->setRelationId($iVal); @@ -95,7 +95,7 @@ public function testRelationId() public function testImageRelationId() { $src = __DIR__ . '/../_files/documents/sheet.xls'; - $oObject = new ObjectElement($src); + $oObject = new OLEObject($src); $iVal = rand(1, 1000); $oObject->setImageRelationId($iVal); diff --git a/tests/PhpWord/Element/SectionTest.php b/tests/PhpWord/Element/SectionTest.php index fc67c43994..20f0f0f759 100644 --- a/tests/PhpWord/Element/SectionTest.php +++ b/tests/PhpWord/Element/SectionTest.php @@ -70,7 +70,7 @@ public function testAddElements() 'PageBreak', 'Table', 'ListItem', - 'ObjectElement', + 'OLEObject', 'Image', 'Title', 'TextRun', diff --git a/tests/PhpWord/Writer/Word2007/ElementTest.php b/tests/PhpWord/Writer/Word2007/ElementTest.php index eb613ae03b..4f0d50d96a 100644 --- a/tests/PhpWord/Writer/Word2007/ElementTest.php +++ b/tests/PhpWord/Writer/Word2007/ElementTest.php @@ -44,7 +44,7 @@ public function testUnmatchedElements() { $elements = array( 'CheckBox', 'Container', 'Footnote', 'Image', 'Link', 'ListItem', 'ListItemRun', - 'ObjectElement', 'PreserveText', 'Table', 'Text', 'TextBox', 'TextBreak', 'Title', 'TOC', + 'OLEObject', 'PreserveText', 'Table', 'Text', 'TextBox', 'TextBreak', 'Title', 'TOC', 'Field', 'Line', 'Shape', 'Chart', 'FormField', 'SDT', 'Bookmark', ); foreach ($elements as $element) { From 7908491ba3e24f170da5aea61fbf9b85c6c21abb Mon Sep 17 00:00:00 2001 From: troosan Date: Tue, 19 Dec 2017 22:14:52 +0100 Subject: [PATCH 30/44] revert mistakenly deleted line --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 41730374f4..2774ad98a9 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,8 @@ "php": ">=5.3.3", "ext-xml": "*", "zendframework/zend-escaper": "^2.2", - "zendframework/zend-stdlib": "^2.2 || ^3.0" + "zendframework/zend-stdlib": "^2.2 || ^3.0", + "phpoffice/common": "^0.2" }, "require-dev": { "phpunit/phpunit": "^4.8.36", From 3e6745f14697d74b74e8d3ab9af670c8fad2ee3f Mon Sep 17 00:00:00 2001 From: SRG Group Date: Thu, 21 Dec 2017 00:03:52 +0100 Subject: [PATCH 31/44] HTML image support & TextRun paragraph style (#934) * Adding setParagraphStyle to Textrun for indentation * Html Image support added * fix formatting, add tests & update changelog --- CHANGELOG.md | 3 ++ src/PhpWord/Element/TextRun.php | 24 +++++++++- src/PhpWord/Shared/Html.php | 58 +++++++++++++++++++++++ tests/PhpWord/Element/ListItemRunTest.php | 4 +- tests/PhpWord/Element/TextRunTest.php | 33 ++++++++++++- tests/PhpWord/Shared/HtmlTest.php | 19 ++++++++ 6 files changed, 136 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51f9243484..ae1618a2ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ This is the last version to support PHP 5.3 - Implement PageBreak for odt writer @cookiekiller #863 #824 - Allow to force an update of all fields on opening a document - @troosan #951 - Allow adding a CheckBox in a TextRun - @irond #727 +- Add support for HTML img tag - @srggroup #934 +- Add support for password protection for docx - @mariahaubner #1019 ### Fixed - Loosen dependency to Zend @@ -43,6 +45,7 @@ This is the last version to support PHP 5.3 - Fix incorrect image size between windows and mac - @bskrtich #874 - Fix adding HTML table to document - @mogilvie @arivanbastos #324 - Fix parsing on/off values (w:val="true|false|1|0|on|off") - @troosan #1221 #1219 +- Fix error on Empty Dropdown Entry - @ComputerTinker #592 ### Deprecated - PhpWord->getProtection(), get it from the settings instead PhpWord->getSettings()->getDocumentProtection(); diff --git a/src/PhpWord/Element/TextRun.php b/src/PhpWord/Element/TextRun.php index d8a898b4ca..6d9ae9f4a1 100644 --- a/src/PhpWord/Element/TextRun.php +++ b/src/PhpWord/Element/TextRun.php @@ -43,7 +43,7 @@ class TextRun extends AbstractContainer */ public function __construct($paragraphStyle = null) { - $this->paragraphStyle = $this->setNewStyle(new Paragraph(), $paragraphStyle); + $this->paragraphStyle = $this->setParagraphStyle($paragraphStyle); } /** @@ -55,4 +55,26 @@ public function getParagraphStyle() { return $this->paragraphStyle; } + + /** + * Set Paragraph style + * + * @param string|array|\PhpOffice\PhpWord\Style\Paragraph $style + * @return string|\PhpOffice\PhpWord\Style\Paragraph + */ + public function setParagraphStyle($style = null) + { + if (is_array($style)) { + $this->paragraphStyle = new Paragraph(); + $this->paragraphStyle->setStyleByArray($style); + } elseif ($style instanceof Paragraph) { + $this->paragraphStyle = $style; + } elseif (null === $style) { + $this->paragraphStyle = new Paragraph(); + } else { + $this->paragraphStyle = $style; + } + + return $this->paragraphStyle; + } } diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 739bfb1646..8310e515c2 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -136,6 +136,7 @@ protected static function parseNode($node, $element, $styles = array(), $data = 'ul' => array('List', null, null, $styles, $data, 3, null), 'ol' => array('List', null, null, $styles, $data, 7, null), 'li' => array('ListItem', $node, $element, $styles, $data, null, null), + 'img' => array('Image', $node, $element, $styles, null, null, null), 'br' => array('LineBreak', null, $element, $styles, null, null, null), ); @@ -506,6 +507,63 @@ private static function parseStyle($attribute, $styles) return $styles; } + /** + * Parse image node + * + * @param \DOMNode $node + * @param \PhpOffice\PhpWord\Element\AbstractContainer $element + * + * @return \PhpOffice\PhpWord\Element\Image + **/ + private static function parseImage($node, $element) + { + $style = array(); + foreach ($node->attributes as $attribute) { + switch ($attribute->name) { + case 'src': + $src = $attribute->value; + break; + case 'width': + $width = $attribute->value; + $style['width'] = $width; + break; + case 'height': + $height = $attribute->value; + $style['height'] = $height; + break; + case 'style': + $styleattr = explode(';', $attribute->value); + foreach ($styleattr as $attr) { + if (strpos($attr, ':')) { + list($k, $v) = explode(':', $attr); + switch ($k) { + case 'float': + if (trim($v) == 'right') { + $style['hPos'] = \PhpOffice\PhpWord\Style\Image::POS_RIGHT; + $style['hPosRelTo'] = \PhpOffice\PhpWord\Style\Image::POS_RELTO_PAGE; + $style['pos'] = \PhpOffice\PhpWord\Style\Image::POS_RELATIVE; + $style['wrap'] = \PhpOffice\PhpWord\Style\Image::WRAP_TIGHT; + $style['overlap'] = true; + } + if (trim($v) == 'left') { + $style['hPos'] = \PhpOffice\PhpWord\Style\Image::POS_LEFT; + $style['hPosRelTo'] = \PhpOffice\PhpWord\Style\Image::POS_RELTO_PAGE; + $style['pos'] = \PhpOffice\PhpWord\Style\Image::POS_RELATIVE; + $style['wrap'] = \PhpOffice\PhpWord\Style\Image::WRAP_TIGHT; + $style['overlap'] = true; + } + break; + } + } + } + break; + } + } + $newElement = $element->addImage($src, $style); + + return $newElement; + } + /** * Transforms a CSS border style into a word border style * diff --git a/tests/PhpWord/Element/ListItemRunTest.php b/tests/PhpWord/Element/ListItemRunTest.php index 999756ba0e..84beec02e4 100644 --- a/tests/PhpWord/Element/ListItemRunTest.php +++ b/tests/PhpWord/Element/ListItemRunTest.php @@ -27,13 +27,13 @@ class ListItemRunTest extends \PHPUnit\Framework\TestCase /** * New instance */ - public function testConstructNull() + public function testConstruct() { $oListItemRun = new ListItemRun(); $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\ListItemRun', $oListItemRun); $this->assertCount(0, $oListItemRun->getElements()); - $this->assertNull($oListItemRun->getParagraphStyle()); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Paragraph', $oListItemRun->getParagraphStyle()); } /** diff --git a/tests/PhpWord/Element/TextRunTest.php b/tests/PhpWord/Element/TextRunTest.php index 27f5af6ba6..59b8b89fdb 100644 --- a/tests/PhpWord/Element/TextRunTest.php +++ b/tests/PhpWord/Element/TextRunTest.php @@ -18,6 +18,8 @@ namespace PhpOffice\PhpWord\Element; use PhpOffice\PhpWord\PhpWord; +use PhpOffice\PhpWord\SimpleType\Jc; +use PhpOffice\PhpWord\Style\Paragraph; /** * Test class for PhpOffice\PhpWord\Element\TextRun @@ -29,13 +31,13 @@ class TextRunTest extends \PHPUnit\Framework\TestCase /** * New instance */ - public function testConstructNull() + public function testConstruct() { $oTextRun = new TextRun(); $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\TextRun', $oTextRun); $this->assertCount(0, $oTextRun->getElements()); - $this->assertNull($oTextRun->getParagraphStyle()); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Paragraph', $oTextRun->getParagraphStyle()); } /** @@ -62,6 +64,21 @@ public function testConstructArray() $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Paragraph', $oTextRun->getParagraphStyle()); } + /** + * New instance with object + */ + public function testConstructObject() + { + $oParagraphStyle = new Paragraph(); + $oParagraphStyle->setAlignment(Jc::BOTH); + $oTextRun = new TextRun($oParagraphStyle); + + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\TextRun', $oTextRun); + $this->assertCount(0, $oTextRun->getElements()); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Paragraph', $oTextRun->getParagraphStyle()); + $this->assertEquals(Jc::BOTH, $oTextRun->getParagraphStyle()->getAlignment()); + } + /** * Add text */ @@ -152,4 +169,16 @@ public function testCreateFootnote() $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Footnote', $element); $this->assertCount(1, $oTextRun->getElements()); } + + /** + * Get paragraph style + */ + public function testParagraph() + { + $oText = new TextRun('paragraphStyle'); + $this->assertEquals('paragraphStyle', $oText->getParagraphStyle()); + + $oText->setParagraphStyle(array('alignment' => Jc::CENTER, 'spaceAfter' => 100)); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Paragraph', $oText->getParagraphStyle()); + } } diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index bfe24c58a9..d168c09e9b 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -252,4 +252,23 @@ public function testParseLineBreak() $this->assertEquals('This is some text', $doc->getElement('/w:document/w:body/w:p/w:r[1]/w:t')->nodeValue); $this->assertEquals('with a linebreak.', $doc->getElement('/w:document/w:body/w:p/w:r[2]/w:t')->nodeValue); } + + public function testParseImage() + { + $src = __DIR__ . '/../_files/images/firefox.png'; + + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + $html = '

'; + Html::addHtml($section, $html); + + $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); + + $baseXpath = '/w:document/w:body/w:p/w:r'; + $this->assertTrue($doc->elementExists($baseXpath . '/w:pict/v:shape')); + $this->assertStringMatchesFormat('%Swidth:150pt%S', $doc->getElementAttribute($baseXpath . '[1]/w:pict/v:shape', 'style')); + $this->assertStringMatchesFormat('%Sheight:200pt%S', $doc->getElementAttribute($baseXpath . '[1]/w:pict/v:shape', 'style')); + $this->assertStringMatchesFormat('%Smso-position-horizontal:right%S', $doc->getElementAttribute($baseXpath . '[1]/w:pict/v:shape', 'style')); + $this->assertStringMatchesFormat('%Smso-position-horizontal:left%S', $doc->getElementAttribute($baseXpath . '[2]/w:pict/v:shape', 'style')); + } } From ed704da5b2fcf76f8c0dca4b9836ae3bcda65604 Mon Sep 17 00:00:00 2001 From: troosan Date: Mon, 25 Dec 2017 01:24:20 +0100 Subject: [PATCH 32/44] set release date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae1618a2ad..47567dfcad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ Change Log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -v0.14.0 (?? Dec 2017) +v0.14.0 (28 Dec 2017) ---------------------- This release fixes several bugs and adds some new features. This is the last version to support PHP 5.3 From 56720df4875df75ed42ed07f4e3bdfc851cccce1 Mon Sep 17 00:00:00 2001 From: troosan Date: Mon, 25 Dec 2017 01:32:34 +0100 Subject: [PATCH 33/44] update version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8580888829..ac5f3b958b 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ You can of course also manually edit your composer.json file ```json { "require": { - "phpoffice/phpword": "v0.13.*" + "phpoffice/phpword": "v0.14.*" } } ``` From 7250b15e74ff91c72d103ca40ae12f4524a0bf76 Mon Sep 17 00:00:00 2001 From: troosan Date: Mon, 25 Dec 2017 08:33:02 +0100 Subject: [PATCH 34/44] Title can be added in Cell --- src/PhpWord/Element/AbstractContainer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpWord/Element/AbstractContainer.php b/src/PhpWord/Element/AbstractContainer.php index 0de0cbce8d..e3022b50f2 100644 --- a/src/PhpWord/Element/AbstractContainer.php +++ b/src/PhpWord/Element/AbstractContainer.php @@ -209,7 +209,7 @@ private function checkValidity($method) 'Footnote' => array('Section', 'TextRun', 'Cell'), 'Endnote' => array('Section', 'TextRun', 'Cell'), 'PreserveText' => array('Section', 'Header', 'Footer', 'Cell'), - 'Title' => array('Section'), + 'Title' => array('Section', 'Cell'), 'TOC' => array('Section'), 'PageBreak' => array('Section'), 'Chart' => array('Section', 'Cell'), From 512cf952aeaaa9badcce3c0eb88114f6beaeeb22 Mon Sep 17 00:00:00 2001 From: troosan Date: Mon, 25 Dec 2017 20:42:37 +0100 Subject: [PATCH 35/44] randomise temp directory name to avoid collisions --- src/PhpWord/Writer/AbstractWriter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpWord/Writer/AbstractWriter.php b/src/PhpWord/Writer/AbstractWriter.php index 50a0cad35c..2be03b06fc 100644 --- a/src/PhpWord/Writer/AbstractWriter.php +++ b/src/PhpWord/Writer/AbstractWriter.php @@ -216,7 +216,7 @@ public function setTempDir($value) protected function getTempFile($filename) { // Temporary directory - $this->setTempDir(Settings::getTempDir() . '/PHPWordWriter/'); + $this->setTempDir(Settings::getTempDir() . uniqid('/PHPWordWriter_'). '/'); // Temporary file $this->originalFilename = $filename; From fce1bf28c870132bef8c0f604477e14b2fa54185 Mon Sep 17 00:00:00 2001 From: troosan Date: Mon, 25 Dec 2017 22:05:46 +0100 Subject: [PATCH 36/44] format code --- src/PhpWord/Writer/AbstractWriter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpWord/Writer/AbstractWriter.php b/src/PhpWord/Writer/AbstractWriter.php index 2be03b06fc..884769d724 100644 --- a/src/PhpWord/Writer/AbstractWriter.php +++ b/src/PhpWord/Writer/AbstractWriter.php @@ -216,7 +216,7 @@ public function setTempDir($value) protected function getTempFile($filename) { // Temporary directory - $this->setTempDir(Settings::getTempDir() . uniqid('/PHPWordWriter_'). '/'); + $this->setTempDir(Settings::getTempDir() . uniqid('/PHPWordWriter_') . '/'); // Temporary file $this->originalFilename = $filename; From b614497ae6dd44280be1c2dda56772198bcd25ae Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 29 Dec 2017 02:30:53 +0100 Subject: [PATCH 37/44] fix dependencies to have 7.1 compatible build (#1228) * add assertions in test methods without assertions * loosen dependencies so 7.0 & 7.1 builds can succeed * fix some scrutinizer errors * update release date --- .scrutinizer.yml | 2 +- .travis.yml | 4 +- CHANGELOG.md | 4 +- composer.json | 9 ++- docs/ISSUE_TEMPLATE.md | 17 ++++-- docs/PULL_REQUEST_TEMPLATE.md | 7 +-- run_tests.sh | 20 ------- src/PhpWord/Element/AbstractElement.php | 12 ++-- src/PhpWord/Element/Field.php | 2 +- src/PhpWord/Element/TrackChange.php | 2 - src/PhpWord/Metadata/Protection.php | 2 +- src/PhpWord/Reader/Word2007/Settings.php | 2 +- src/PhpWord/Shared/Converter.php | 38 ++++++------- src/PhpWord/Shared/Html.php | 1 + .../Shared/Microsoft/PasswordEncoder.php | 2 +- src/PhpWord/Shared/ZipArchive.php | 2 +- src/PhpWord/Style/Paper.php | 8 +-- src/PhpWord/Writer/PDF/MPDF.php | 24 +++++--- src/PhpWord/Writer/Word2007/Part/Comments.php | 4 +- tests/PhpWord/Shared/ZipArchiveTest.php | 56 +++++++++---------- tests/PhpWord/Writer/ODTextTest.php | 1 + tests/PhpWord/Writer/RTFTest.php | 1 + .../Writer/Word2007/Part/DocumentTest.php | 3 +- 23 files changed, 110 insertions(+), 113 deletions(-) delete mode 100755 run_tests.sh diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 6f982d8e89..c8fe57cf63 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -15,7 +15,7 @@ tools: ruleset: phpmd.xml.dist external_code_coverage: enabled: true - timeout: 900 + timeout: 1200 php_cpd: true # php_sim: # Temporarily disabled to allow focus on things other than duplicates # min_mass: 40 diff --git a/.travis.yml b/.travis.yml index 0ec84081e1..d63b7bb22c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,14 +9,14 @@ php: - 5.6 - 7.0 - 7.1 + - 7.2 matrix: include: - php: 5.6 env: COVERAGE=1 allow_failures: - - php: 7.0 - - php: 7.1 + - php: 7.2 cache: directories: diff --git a/CHANGELOG.md b/CHANGELOG.md index 47567dfcad..93945189a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,10 @@ Change Log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -v0.14.0 (28 Dec 2017) +v0.14.0 (29 Dec 2017) ---------------------- This release fixes several bugs and adds some new features. -This is the last version to support PHP 5.3 +This version brings compatibility with PHP 7.0 & 7.1 ### Added - Possibility to control the footnote numbering - @troosan #1068 diff --git a/composer.json b/composer.json index 2774ad98a9..3cc4b131cf 100644 --- a/composer.json +++ b/composer.json @@ -46,23 +46,22 @@ ] }, "require": { - "php": ">=5.3.3", + "php": "^5.3.3 || ^7.0", "ext-xml": "*", "zendframework/zend-escaper": "^2.2", "zendframework/zend-stdlib": "^2.2 || ^3.0", "phpoffice/common": "^0.2" }, "require-dev": { - "phpunit/phpunit": "^4.8.36", + "phpunit/phpunit": "^4.8.36 || ^5.0", "phpdocumentor/phpdocumentor":"2.*", - "twig/twig":"1.27", "squizlabs/php_codesniffer": "^2.7", "friendsofphp/php-cs-fixer": "^2.0", "phpmd/phpmd": "2.*", - "phploc/phploc": "2.*", + "phploc/phploc": "2.* || 3.* || 4.*", "dompdf/dompdf":"0.8.*", "tecnickcom/tcpdf": "6.*", - "mpdf/mpdf": "5.*" + "mpdf/mpdf": "5.* || 6.* || 7.*" }, "suggest": { "ext-zip": "Allows writing OOXML and ODF", diff --git a/docs/ISSUE_TEMPLATE.md b/docs/ISSUE_TEMPLATE.md index 58981f8eca..ee811b00a4 100644 --- a/docs/ISSUE_TEMPLATE.md +++ b/docs/ISSUE_TEMPLATE.md @@ -1,28 +1,35 @@ -Issue tracker is **ONLY** used for reporting bugs. NO NEW FEATURE ACCEPTED! Use [stackoverflow](https://stackoverflow.com/questions/tagged/phpword) for supporting issues. +This is: + +- [ ] a bug report +- [ ] a feature request +- [ ] **not** a usage question (ask them on https://stackoverflow.com/questions/tagged/phpword) # Expected Behavior Please describe the behavior you are expecting. -# Current Behavior +### Current Behavior What is the current behavior? -# Failure Information +### Failure Information Please help provide information about the failure. -## How to Reproduce +### How to Reproduce Please provide a code sample that reproduces the issue. ```php +addSection(); $section->... ``` -## Context +### Context * PHP version: * PHPWord version: 0.14 diff --git a/docs/PULL_REQUEST_TEMPLATE.md b/docs/PULL_REQUEST_TEMPLATE.md index ad9788c436..cff513a356 100644 --- a/docs/PULL_REQUEST_TEMPLATE.md +++ b/docs/PULL_REQUEST_TEMPLATE.md @@ -1,12 +1,11 @@ -# Description +### Description Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. Fixes # (issue) -# Checklist: +### Checklist: -- [ ] I have commented my code, particularly in hard-to-understand areas -- [ ] I have run phpunit, phpcs, php-cs-fixer, phpmd +- [ ] I have run `composer check` and no errors were reported - [ ] The new code is covered by unit tests - [ ] I have update the documentation to describe the changes diff --git a/run_tests.sh b/run_tests.sh deleted file mode 100755 index a5d94259b8..0000000000 --- a/run_tests.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -echo "Running composer update" -composer update - -## PHP_CodeSniffer -echo "Running CodeSniffer" -./vendor/bin/phpcs src/ tests/ --standard=PSR2 -n --ignore=src/PhpWord/Shared/PCLZip - -## PHP-CS-Fixer -echo "Running CS Fixer" -./vendor/bin/php-cs-fixer fix --diff --verbose --dry-run - -## PHP Mess Detector -echo "Running Mess Detector" -./vendor/bin/phpmd src/,tests/ text ./phpmd.xml.dist --exclude pclzip.lib.php - -## PHPUnit -echo "Running PHPUnit" -./vendor/bin/phpunit -c ./ - diff --git a/src/PhpWord/Element/AbstractElement.php b/src/PhpWord/Element/AbstractElement.php index 81e185289d..a65c50f4d2 100644 --- a/src/PhpWord/Element/AbstractElement.php +++ b/src/PhpWord/Element/AbstractElement.php @@ -228,7 +228,7 @@ public function setElementIndex($value) /** * Get element unique ID * - * @return int + * @return string */ public function getElementId() { @@ -425,18 +425,18 @@ protected function setNewStyle($styleObject, $styleValue = null, $returnObject = /** * Set enum value * - * @param mixed $value - * @param array $enum - * @param mixed $default + * @param string|null $value + * @param string[] $enum + * @param string|null $default * * @throws \InvalidArgumentException - * @return mixed + * @return string|null * * @todo Merge with the same method in AbstractStyle */ protected function setEnumVal($value = null, $enum = array(), $default = null) { - if ($value != null && trim($value) != '' && !empty($enum) && !in_array($value, $enum)) { + if ($value !== null && trim($value) != '' && !empty($enum) && !in_array($value, $enum)) { throw new \InvalidArgumentException("Invalid style value: {$value}"); } elseif ($value === null || trim($value) == '') { $value = $default; diff --git a/src/PhpWord/Element/Field.php b/src/PhpWord/Element/Field.php index 6ea63c6b68..7b33a4799d 100644 --- a/src/PhpWord/Element/Field.php +++ b/src/PhpWord/Element/Field.php @@ -211,7 +211,7 @@ public function getOptions() * @throws \InvalidArgumentException * @return null|string|TextRun */ - public function setText($text) + public function setText($text = null) { if (isset($text)) { if (is_string($text) || $text instanceof TextRun) { diff --git a/src/PhpWord/Element/TrackChange.php b/src/PhpWord/Element/TrackChange.php index d14fc201db..44327f263f 100644 --- a/src/PhpWord/Element/TrackChange.php +++ b/src/PhpWord/Element/TrackChange.php @@ -52,8 +52,6 @@ public function __construct($author, \DateTime $date = null) { $this->author = $author; $this->date = $date; - - return $this; } /** diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index 35391cb20b..39ebc3dead 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -182,7 +182,7 @@ public function getSalt() /** * Set salt. Salt HAS to be 16 characters, or an exception will be thrown. * - * @param $salt + * @param string $salt * @throws \InvalidArgumentException * @return self */ diff --git a/src/PhpWord/Reader/Word2007/Settings.php b/src/PhpWord/Reader/Word2007/Settings.php index c116425ef3..ccdbed2542 100644 --- a/src/PhpWord/Reader/Word2007/Settings.php +++ b/src/PhpWord/Reader/Word2007/Settings.php @@ -152,7 +152,7 @@ protected function setRevisionView(XMLReader $xmlReader, PhpWord $phpWord, \DOME $revisionView = new TrackChangesView(); $revisionView->setMarkup(filter_var($xmlReader->getAttribute('w:markup', $node), FILTER_VALIDATE_BOOLEAN)); $revisionView->setComments($xmlReader->getAttribute('w:comments', $node)); - $revisionView->setInsDel($xmlReader->getAttribute('w:insDel', $node)); + $revisionView->setInsDel(filter_var($xmlReader->getAttribute('w:insDel', $node), FILTER_VALIDATE_BOOLEAN)); $revisionView->setFormatting(filter_var($xmlReader->getAttribute('w:formatting', $node), FILTER_VALIDATE_BOOLEAN)); $revisionView->setInkAnnotations(filter_var($xmlReader->getAttribute('w:inkAnnotations', $node), FILTER_VALIDATE_BOOLEAN)); $phpWord->getSettings()->setRevisionView($revisionView); diff --git a/src/PhpWord/Shared/Converter.php b/src/PhpWord/Shared/Converter.php index bae8985d37..56687c986a 100644 --- a/src/PhpWord/Shared/Converter.php +++ b/src/PhpWord/Shared/Converter.php @@ -33,7 +33,7 @@ class Converter /** * Convert centimeter to twip * - * @param int $centimeter + * @param float $centimeter * @return float */ public static function cmToTwip($centimeter = 1) @@ -44,7 +44,7 @@ public static function cmToTwip($centimeter = 1) /** * Convert centimeter to inch * - * @param int $centimeter + * @param float $centimeter * @return float */ public static function cmToInch($centimeter = 1) @@ -55,7 +55,7 @@ public static function cmToInch($centimeter = 1) /** * Convert centimeter to pixel * - * @param int $centimeter + * @param float $centimeter * @return float */ public static function cmToPixel($centimeter = 1) @@ -66,7 +66,7 @@ public static function cmToPixel($centimeter = 1) /** * Convert centimeter to point * - * @param int $centimeter + * @param float $centimeter * @return float */ public static function cmToPoint($centimeter = 1) @@ -77,8 +77,8 @@ public static function cmToPoint($centimeter = 1) /** * Convert centimeter to EMU * - * @param int $centimeter - * @return int + * @param float $centimeter + * @return float */ public static function cmToEmu($centimeter = 1) { @@ -88,8 +88,8 @@ public static function cmToEmu($centimeter = 1) /** * Convert inch to twip * - * @param int $inch - * @return int + * @param float $inch + * @return float */ public static function inchToTwip($inch = 1) { @@ -99,7 +99,7 @@ public static function inchToTwip($inch = 1) /** * Convert inch to centimeter * - * @param int $inch + * @param float $inch * @return float */ public static function inchToCm($inch = 1) @@ -110,8 +110,8 @@ public static function inchToCm($inch = 1) /** * Convert inch to pixel * - * @param int $inch - * @return int + * @param float $inch + * @return float */ public static function inchToPixel($inch = 1) { @@ -121,8 +121,8 @@ public static function inchToPixel($inch = 1) /** * Convert inch to point * - * @param int $inch - * @return int + * @param float $inch + * @return float */ public static function inchToPoint($inch = 1) { @@ -132,8 +132,8 @@ public static function inchToPoint($inch = 1) /** * Convert inch to EMU * - * @param int $inch - * @return int + * @param float $inch + * @return float */ public static function inchToEmu($inch = 1) { @@ -144,7 +144,7 @@ public static function inchToEmu($inch = 1) * Convert pixel to twip * * @param int $pixel - * @return int + * @return float */ public static function pixelToTwip($pixel = 1) { @@ -188,7 +188,7 @@ public static function pixelToEmu($pixel = 1) * Convert point to twip unit * * @param int $point - * @return int + * @return float */ public static function pointToTwip($point = 1) { @@ -210,7 +210,7 @@ public static function pointToPixel($point = 1) * Convert point to EMU * * @param int $point - * @return int + * @return float */ public static function pointToEmu($point = 1) { @@ -221,7 +221,7 @@ public static function pointToEmu($point = 1) * Convert EMU to pixel * * @param int $emu - * @return int + * @return float */ public static function emuToPixel($emu = 1) { diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 8310e515c2..d8a10b5702 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -518,6 +518,7 @@ private static function parseStyle($attribute, $styles) private static function parseImage($node, $element) { $style = array(); + $src = null; foreach ($node->attributes as $attribute) { switch ($attribute->name) { case 'src': diff --git a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php index d3a03d9740..1c7b4c6c8e 100644 --- a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php +++ b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php @@ -165,7 +165,7 @@ private static function getAlgorithm($algorithmName) /** * Returns the algorithm ID * - * @param sting $algorithmName + * @param string $algorithmName * @return int */ public static function getAlgorithmId($algorithmName) diff --git a/src/PhpWord/Shared/ZipArchive.php b/src/PhpWord/Shared/ZipArchive.php index bb42a92aff..3d8d0a4133 100644 --- a/src/PhpWord/Shared/ZipArchive.php +++ b/src/PhpWord/Shared/ZipArchive.php @@ -161,7 +161,7 @@ public function close() { if (!$this->usePclzip) { if ($this->zip->close() === false) { - throw new Exception("Could not close zip file {$this->filename}."); + throw new Exception("Could not close zip file {$this->filename}: "); } } diff --git a/src/PhpWord/Style/Paper.php b/src/PhpWord/Style/Paper.php index 2fbf59d282..09e4769e90 100644 --- a/src/PhpWord/Style/Paper.php +++ b/src/PhpWord/Style/Paper.php @@ -118,14 +118,14 @@ class Paper extends AbstractStyle /** * Width * - * @var int (twip) + * @var float (twip) */ private $width; /** * Height * - * @var int (twip) + * @var float (twip) */ private $height; @@ -175,7 +175,7 @@ public function setSize($size) /** * Get width * - * @return int + * @return float */ public function getWidth() { @@ -185,7 +185,7 @@ public function getWidth() /** * Get height * - * @return int + * @return float */ public function getHeight() { diff --git a/src/PhpWord/Writer/PDF/MPDF.php b/src/PhpWord/Writer/PDF/MPDF.php index 80c2eccf90..e238057b1f 100644 --- a/src/PhpWord/Writer/PDF/MPDF.php +++ b/src/PhpWord/Writer/PDF/MPDF.php @@ -17,6 +17,8 @@ namespace PhpOffice\PhpWord\Writer\PDF; +use PhpOffice\PhpWord\PhpWord; +use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\Writer\WriterInterface; /** @@ -27,12 +29,14 @@ */ class MPDF extends AbstractRenderer implements WriterInterface { - /** - * Name of renderer include file - * - * @var string - */ - protected $includeFile = 'mpdf.php'; + public function __construct(PhpWord $phpWord) + { + if (file_exists(Settings::getPdfRendererPath() . '/mpdf.php')) { + // MPDF version 5.* needs this file to be included, later versions not + $this->includeFile = 'mpdf.php'; + } + parent::__construct($phpWord); + } /** * Save PhpWord to file. @@ -48,7 +52,13 @@ public function save($filename = null) $orientation = strtoupper('portrait'); // Create PDF - $pdf = new \mpdf(); + if ($this->includeFile != null) { + // MPDF version 5.* + $pdf = new \mpdf(); + } else { + // MPDF version > 6.* + $pdf = new \Mpdf\Mpdf(); + } $pdf->_setPageSize($paperSize, $orientation); $pdf->addPage($orientation); diff --git a/src/PhpWord/Writer/Word2007/Part/Comments.php b/src/PhpWord/Writer/Word2007/Part/Comments.php index 4551ca921e..2b8f9267a2 100644 --- a/src/PhpWord/Writer/Word2007/Part/Comments.php +++ b/src/PhpWord/Writer/Word2007/Part/Comments.php @@ -29,7 +29,7 @@ class Comments extends AbstractPart /** * Comments collection to be written * - * @var \PhpOffice\PhpWord\Collection\Comments + * @var \PhpOffice\PhpWord\Element\Comment[] */ protected $elements; @@ -92,7 +92,7 @@ protected function writeComment(XMLWriter $xmlWriter, Comment $comment) /** * Set element * - * @param \PhpOffice\PhpWord\Collection\Comments $elements + * @param \PhpOffice\PhpWord\Element\Comment[] $elements * @return self */ public function setElements($elements) diff --git a/tests/PhpWord/Shared/ZipArchiveTest.php b/tests/PhpWord/Shared/ZipArchiveTest.php index 91f0f030b1..cb095127a6 100644 --- a/tests/PhpWord/Shared/ZipArchiveTest.php +++ b/tests/PhpWord/Shared/ZipArchiveTest.php @@ -27,34 +27,34 @@ */ class ZipArchiveTest extends \PHPUnit\Framework\TestCase { - /** - * Test close method exception: Working in local, not working in Travis - * - * expectedException \PhpOffice\PhpWord\Exception\Exception - * expectedExceptionMessage Could not close zip file - * covers ::close - */ - public function testCloseException() - { - // $zipFile = __DIR__ . "/../_files/documents/ziptest.zip"; - - // $object = new ZipArchive(); - // $object->open($zipFile, ZipArchive::CREATE); - // $object->addFromString('content/string.txt', 'Test'); - - // // Lock the file - // $resource = fopen($zipFile, "w"); - // flock($resource, LOCK_EX); - - // // Closing the file should throws an exception - // $object->close(); - - // // Unlock the file - // flock($resource, LOCK_UN); - // fclose($resource); - - // @unlink($zipFile); - } +// /** +// * Test close method exception: Working in local, not working in Travis +// * +// * expectedException \PhpOffice\PhpWord\Exception\Exception +// * expectedExceptionMessage Could not close zip file +// * covers ::close +// */ +// public function testCloseException() +// { +// $zipFile = __DIR__ . "/../_files/documents/ziptest.zip"; + +// $object = new ZipArchive(); +// $object->open($zipFile, ZipArchive::CREATE); +// $object->addFromString('content/string.txt', 'Test'); + +// // Lock the file +// $resource = fopen($zipFile, "w"); +// flock($resource, LOCK_EX); + +// // Closing the file should throws an exception +// $object->close(); + +// // Unlock the file +// flock($resource, LOCK_UN); +// fclose($resource); + +// @unlink($zipFile); +// } /** * Test all methods diff --git a/tests/PhpWord/Writer/ODTextTest.php b/tests/PhpWord/Writer/ODTextTest.php index bb1b953821..1984de0f07 100644 --- a/tests/PhpWord/Writer/ODTextTest.php +++ b/tests/PhpWord/Writer/ODTextTest.php @@ -110,6 +110,7 @@ public function testSavePhpOutput() $section->addText('Test'); $writer = new ODText($phpWord); $writer->save('php://output'); + $this->assertNotNull($this->getActualOutput()); } /** diff --git a/tests/PhpWord/Writer/RTFTest.php b/tests/PhpWord/Writer/RTFTest.php index f444204354..803087e567 100644 --- a/tests/PhpWord/Writer/RTFTest.php +++ b/tests/PhpWord/Writer/RTFTest.php @@ -111,5 +111,6 @@ public function testSavePhpOutput() $section->addText(htmlspecialchars('Test', ENT_COMPAT, 'UTF-8')); $writer = new RTF($phpWord); $writer->save('php://output'); + $this->assertNotNull($this->getActualOutput()); } } diff --git a/tests/PhpWord/Writer/Word2007/Part/DocumentTest.php b/tests/PhpWord/Writer/Word2007/Part/DocumentTest.php index 42c098cda7..6998e717ee 100644 --- a/tests/PhpWord/Writer/Word2007/Part/DocumentTest.php +++ b/tests/PhpWord/Writer/Word2007/Part/DocumentTest.php @@ -58,7 +58,8 @@ public function testWriteCustomProps() $docInfo->setCustomProperty('key6', new \DateTime()); $docInfo->setCustomProperty('key7', time(), DocInfo::PROPERTY_TYPE_DATE); - TestHelperDOCX::getDocument($phpWord); + $doc = TestHelperDOCX::getDocument($phpWord); + $this->assertNotNull($doc); // $this->assertTrue($doc->elementExists('/Properties/property[name="key1"]/vt:lpwstr')); // $this->assertTrue($doc->elementExists('/Properties/property[name="key2"]/vt:bool')); From fd7ee764380ad1c99c9075be4963d19d239bec54 Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 29 Dec 2017 03:01:36 +0100 Subject: [PATCH 38/44] create alias for develop branch --- composer.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/composer.json b/composer.json index 3cc4b131cf..aa4a2415b1 100644 --- a/composer.json +++ b/composer.json @@ -74,5 +74,10 @@ "psr-4": { "PhpOffice\\PhpWord\\": "src/PhpWord" } + }, + "extra": { + "branch-alias": { + "dev-develop": "0.15.0-dev" + } } } From d2b9e88047c0533db351bcefc0bedbe703b09411 Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 29 Dec 2017 14:36:07 +0100 Subject: [PATCH 39/44] add parsing of "align" HTML attribute --- CHANGELOG.md | 7 ++++++ samples/Sample_26_Html.php | 2 +- src/PhpWord/Shared/Html.php | 39 ++++++++++++++++++++----------- tests/PhpWord/Shared/HtmlTest.php | 4 +++- 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93945189a9..b5396d8d2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ Change Log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +v0.15.0 (?? ??? 2018) +---------------------- +### Added +- Parsing of "align" HTML attribute - @troosan + +### Fixed + v0.14.0 (29 Dec 2017) ---------------------- This release fixes several bugs and adds some new features. diff --git a/samples/Sample_26_Html.php b/samples/Sample_26_Html.php index ba06b0634d..b993f83485 100644 --- a/samples/Sample_26_Html.php +++ b/samples/Sample_26_Html.php @@ -28,7 +28,7 @@ '; -$html .= ' +$html .= '
diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index d8a10b5702..38d326c1fa 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -85,6 +85,8 @@ protected static function parseInlineStyle($node, $styles = array()) case 'style': $styles = self::parseStyle($attribute, $styles); break; + case 'align': + $styles['alignment'] = self::mapAlign($attribute->value); } } } @@ -431,20 +433,7 @@ private static function parseStyle($attribute, $styles) } break; case 'text-align': - switch ($cValue) { - case 'left': - $styles['alignment'] = Jc::START; - break; - case 'right': - $styles['alignment'] = Jc::END; - break; - case 'center': - $styles['alignment'] = Jc::CENTER; - break; - case 'justify': - $styles['alignment'] = Jc::BOTH; - break; - } + $styles['alignment'] = self::mapAlign($cValue); break; case 'font-size': $styles['size'] = Converter::cssToPoint($cValue); @@ -584,6 +573,28 @@ private static function mapBorderStyle($cssBorderStyle) } } + /** + * Transforms a HTML/CSS alignment into a \PhpOffice\PhpWord\SimpleType\Jc + * + * @param string $cssAlignment + * @return string|null + */ + private static function mapAlign($cssAlignment) + { + switch ($cssAlignment) { + case 'left': + return Jc::START; + case 'right': + return Jc::END; + case 'center': + return Jc::CENTER; + case 'justify': + return Jc::BOTH; + } + + return null; + } + /** * Parse line break * diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index d168c09e9b..c7d3647010 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -187,7 +187,7 @@ public function testParseTable() { $phpWord = new \PhpOffice\PhpWord\PhpWord(); $section = $phpWord->addSection(); - $html = '
header a
+ $html = '
@@ -205,6 +205,8 @@ public function testParseTable() $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); $this->assertTrue($doc->elementExists('/w:document/w:body/w:tbl')); $this->assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr/w:tc')); + $this->assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tblPr/w:jc')); + $this->assertEquals(Jc::START, $doc->getElementAttribute('/w:document/w:body/w:tbl/w:tblPr/w:jc', 'w:val')); } /** From 1d8e7b8374547fac04c432cda2f54503077829f2 Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 29 Dec 2017 14:36:56 +0100 Subject: [PATCH 40/44] split composer scripts, add description (only works with composer 1.6) --- composer.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index aa4a2415b1..a79c8e19ae 100644 --- a/composer.json +++ b/composer.json @@ -35,16 +35,28 @@ } ], "scripts": { + "test": [ + "./vendor/bin/phpunit --color=always" + ], + "test-no-coverage": [ + "./vendor/bin/phpunit --color=always --no-coverage" + ], "check": [ "./vendor/bin/php-cs-fixer fix --ansi --dry-run --diff", "./vendor/bin/phpcs --report-width=200 --report-summary --report-full samples/ src/ tests/ --ignore=src/PhpWord/Shared/PCLZip --standard=PSR2 -n", "./vendor/bin/phpmd src/,tests/ text ./phpmd.xml.dist --exclude pclzip.lib.php", - "./vendor/bin/phpunit --color=always" + "@test" ], "fix": [ "./vendor/bin/php-cs-fixer fix --ansi" ] }, + "scripts-descriptions": { + "test": "Runs all unit tests", + "test-no-coverage": "Runs all unit tests, without code coverage", + "check": "Runs PHP CheckStyle and PHP Mess detector", + "fix": "Fixes issues found by PHP-CS" + }, "require": { "php": "^5.3.3 || ^7.0", "ext-xml": "*", From b20cd4fa9f5db9ee8c028d7ebe34597e9c540340 Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 29 Dec 2017 14:37:26 +0100 Subject: [PATCH 41/44] output the source code of the sample that was run --- samples/Sample_Header.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/samples/Sample_Header.php b/samples/Sample_Header.php index c49960490c..36478ad6be 100644 --- a/samples/Sample_Header.php +++ b/samples/Sample_Header.php @@ -43,13 +43,19 @@ // Populate samples $files = ''; if ($handle = opendir('.')) { - while (false !== ($file = readdir($handle))) { + $sampleFiles = array(); + while (false !== ($sampleFile = readdir($handle))) { + $sampleFiles[] = $sampleFile; + } + sort($sampleFiles); + closedir($handle); + + foreach ($sampleFiles as $file) { if (preg_match('/^Sample_\d+_/', $file)) { $name = str_replace('_', ' ', preg_replace('/(Sample_|\.php)/', '', $file)); $files .= "
  • {$name}
  • "; } } - closedir($handle); } /** @@ -78,6 +84,11 @@ function write($phpWord, $filename, $writers) } $result .= getEndingNotes($writers); + $result .= '
    ';
    +    if (file_exists($filename . '.php')) {
    +        $result .= highlight_file($filename . '.php', true);
    +    }
    +    $result .= '
    '; return $result; } From 46e179d1484b12005a0882e5bf12ee76bb5c6856 Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 29 Dec 2017 14:42:48 +0100 Subject: [PATCH 42/44] add instructions on how to run the samples in a browser --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ac5f3b958b..4e3d1e2dc8 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,8 @@ $objWriter->save('helloWorld.html'); /* Note: we skip PDF, because "HTML-to-PDF" approach is used to create PDF documents. */ ``` -More examples are provided in the [samples folder](samples/). You can also read the [Developers' Documentation](http://phpword.readthedocs.org/) and the [API Documentation](http://phpoffice.github.io/PHPWord/docs/master/) for more detail. +More examples are provided in the [samples folder](samples/). For an easy access to those samples launch `php -S localhost:8000` in the samples directory then browse to [http://localhost:8000](http://localhost:8000) to view the samples. +You can also read the [Developers' Documentation](http://phpword.readthedocs.org/) and the [API Documentation](http://phpoffice.github.io/PHPWord/docs/master/) for more detail. ## Contributing From 709ea1e14c2765ef2c439a46bafde1e027bd23f1 Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 29 Dec 2017 14:48:12 +0100 Subject: [PATCH 43/44] update changelog [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5396d8d2a..1f22bb2ea3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). v0.15.0 (?? ??? 2018) ---------------------- ### Added -- Parsing of "align" HTML attribute - @troosan +- Parsing of "align" HTML attribute - @troosan #1231 ### Fixed From 537d07ee99c8ea9ad7c27f0c327b3d0d564c1c24 Mon Sep 17 00:00:00 2001 From: troosan Date: Mon, 18 Dec 2017 22:28:07 +0100 Subject: [PATCH 44/44] rename ObjectElement to OLEObject --- src/PhpWord/Element/AbstractContainer.php | 6 +- src/PhpWord/Element/AbstractElement.php | 7 +- src/PhpWord/Element/OLEObject.php | 169 ++++++++++++++++++ .../Writer/Word2007/Element/OLEObject.php | 88 +++++++++ tests/PhpWord/Element/CellTest.php | 2 +- tests/PhpWord/Element/ObjectTest.php | 22 +-- tests/PhpWord/Element/SectionTest.php | 2 +- tests/PhpWord/Writer/Word2007/ElementTest.php | 2 +- 8 files changed, 279 insertions(+), 19 deletions(-) create mode 100644 src/PhpWord/Element/OLEObject.php create mode 100644 src/PhpWord/Writer/Word2007/Element/OLEObject.php diff --git a/src/PhpWord/Element/AbstractContainer.php b/src/PhpWord/Element/AbstractContainer.php index e3022b50f2..b00424b77e 100644 --- a/src/PhpWord/Element/AbstractContainer.php +++ b/src/PhpWord/Element/AbstractContainer.php @@ -37,7 +37,7 @@ * @method PageBreak addPageBreak() * @method Table addTable(mixed $style = null) * @method Image addImage(string $source, mixed $style = null, bool $isWatermark = false) - * @method \PhpOffice\PhpWord\Element\Object addObject(string $source, mixed $style = null) + * @method \PhpOffice\PhpWord\Element\OLEObject addObject(string $source, mixed $style = null) * @method TextBox addTextBox(mixed $style = null) * @method Field addField(string $type = null, array $properties = array(), array $options = array(), mixed $text = null) * @method Line addLine(mixed $lineStyle = null) @@ -87,7 +87,7 @@ public function __call($function, $args) ); $functions = array(); foreach ($elements as $element) { - $functions['add' . strtolower($element)] = $element; + $functions['add' . strtolower($element)] = $element == 'Object' ? 'OLEObject' : $element; } // Run valid `add` command @@ -193,7 +193,7 @@ private function checkValidity($method) 'Link' => $generalContainers, 'TextBreak' => $generalContainers, 'Image' => $generalContainers, - 'Object' => $generalContainers, + 'OLEObject' => $generalContainers, 'Field' => $generalContainers, 'Line' => $generalContainers, 'Shape' => $generalContainers, diff --git a/src/PhpWord/Element/AbstractElement.php b/src/PhpWord/Element/AbstractElement.php index a65c50f4d2..63892b74ea 100644 --- a/src/PhpWord/Element/AbstractElement.php +++ b/src/PhpWord/Element/AbstractElement.php @@ -358,11 +358,14 @@ public function setParentContainer(AbstractElement $container) */ private function setMediaRelation() { - if (!$this instanceof Link && !$this instanceof Image && !$this instanceof Object) { + if (!$this instanceof Link && !$this instanceof Image && !$this instanceof OLEObject) { return; } $elementName = substr(get_class($this), strrpos(get_class($this), '\\') + 1); + if ($elementName == 'OLEObject') { + $elementName = 'Object'; + } $mediaPart = $this->getMediaPart(); $source = $this->getSource(); $image = null; @@ -372,7 +375,7 @@ private function setMediaRelation() $rId = Media::addElement($mediaPart, strtolower($elementName), $source, $image); $this->setRelationId($rId); - if ($this instanceof Object) { + if ($this instanceof OLEObject) { $icon = $this->getIcon(); $rId = Media::addElement($mediaPart, 'image', $icon, new Image($icon)); $this->setImageRelationId($rId); diff --git a/src/PhpWord/Element/OLEObject.php b/src/PhpWord/Element/OLEObject.php new file mode 100644 index 0000000000..5da94c3a82 --- /dev/null +++ b/src/PhpWord/Element/OLEObject.php @@ -0,0 +1,169 @@ +source = $source; + $this->style = $this->setNewStyle(new ImageStyle(), $style, true); + $this->icon = realpath(__DIR__ . "/../resources/{$ext}.png"); + + return $this; + } + + throw new InvalidObjectException(); + } + + /** + * Get object source + * + * @return string + */ + public function getSource() + { + return $this->source; + } + + /** + * Get object style + * + * @return \PhpOffice\PhpWord\Style\Image + */ + public function getStyle() + { + return $this->style; + } + + /** + * Get object icon + * + * @return string + */ + public function getIcon() + { + return $this->icon; + } + + /** + * Get image relation ID + * + * @return int + */ + public function getImageRelationId() + { + return $this->imageRelationId; + } + + /** + * Set Image Relation ID. + * + * @param int $rId + */ + public function setImageRelationId($rId) + { + $this->imageRelationId = $rId; + } + + /** + * Get Object ID + * + * @deprecated 0.10.0 + * + * @return int + * + * @codeCoverageIgnore + */ + public function getObjectId() + { + return $this->relationId + 1325353440; + } + + /** + * Set Object ID + * + * @deprecated 0.10.0 + * + * @param int $objId + * + * @codeCoverageIgnore + */ + public function setObjectId($objId) + { + $this->relationId = $objId; + } +} diff --git a/src/PhpWord/Writer/Word2007/Element/OLEObject.php b/src/PhpWord/Writer/Word2007/Element/OLEObject.php new file mode 100644 index 0000000000..50891d97f4 --- /dev/null +++ b/src/PhpWord/Writer/Word2007/Element/OLEObject.php @@ -0,0 +1,88 @@ +getXmlWriter(); + $element = $this->getElement(); + if (!$element instanceof \PhpOffice\PhpWord\Element\OLEObject) { + return; + } + + $rIdObject = $element->getRelationId() + ($element->isInSection() ? 6 : 0); + $rIdImage = $element->getImageRelationId() + ($element->isInSection() ? 6 : 0); + $shapeId = md5($rIdObject . '_' . $rIdImage); + $objectId = $element->getRelationId() + 1325353440; + + $style = $element->getStyle(); + $styleWriter = new ImageStyleWriter($xmlWriter, $style); + + if (!$this->withoutP) { + $xmlWriter->startElement('w:p'); + $styleWriter->writeAlignment(); + } + $this->writeCommentRangeStart(); + + $xmlWriter->startElement('w:r'); + $xmlWriter->startElement('w:object'); + $xmlWriter->writeAttribute('w:dxaOrig', '249'); + $xmlWriter->writeAttribute('w:dyaOrig', '160'); + + // Icon + $xmlWriter->startElement('v:shape'); + $xmlWriter->writeAttribute('id', $shapeId); + $xmlWriter->writeAttribute('type', '#_x0000_t75'); + $xmlWriter->writeAttribute('style', 'width:104px;height:67px'); + $xmlWriter->writeAttribute('o:ole', ''); + + $xmlWriter->startElement('v:imagedata'); + $xmlWriter->writeAttribute('r:id', 'rId' . $rIdImage); + $xmlWriter->writeAttribute('o:title', ''); + $xmlWriter->endElement(); // v:imagedata + + $xmlWriter->endElement(); // v:shape + + // Object + $xmlWriter->startElement('o:OLEObject'); + $xmlWriter->writeAttribute('Type', 'Embed'); + $xmlWriter->writeAttribute('ProgID', 'Package'); + $xmlWriter->writeAttribute('ShapeID', $shapeId); + $xmlWriter->writeAttribute('DrawAspect', 'Icon'); + $xmlWriter->writeAttribute('ObjectID', '_' . $objectId); + $xmlWriter->writeAttribute('r:id', 'rId' . $rIdObject); + $xmlWriter->endElement(); // o:OLEObject + + $xmlWriter->endElement(); // w:object + $xmlWriter->endElement(); // w:r + + $this->endElementP(); // w:p + } +} diff --git a/tests/PhpWord/Element/CellTest.php b/tests/PhpWord/Element/CellTest.php index 4e8daa0ecd..a1132cfad8 100644 --- a/tests/PhpWord/Element/CellTest.php +++ b/tests/PhpWord/Element/CellTest.php @@ -181,7 +181,7 @@ public function testAddObjectXLS() $element = $oCell->addObject($src); $this->assertCount(1, $oCell->getElements()); - $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Object', $element); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\OLEObject', $element); } /** diff --git a/tests/PhpWord/Element/ObjectTest.php b/tests/PhpWord/Element/ObjectTest.php index 71f12974d1..ba761b70c4 100644 --- a/tests/PhpWord/Element/ObjectTest.php +++ b/tests/PhpWord/Element/ObjectTest.php @@ -18,9 +18,9 @@ namespace PhpOffice\PhpWord\Element; /** - * Test class for PhpOffice\PhpWord\Element\Object + * Test class for PhpOffice\PhpWord\Element\OLEObject * - * @coversDefaultClass \PhpOffice\PhpWord\Element\Object + * @coversDefaultClass \PhpOffice\PhpWord\Element\OLEObject * @runTestsInSeparateProcesses */ class ObjectTest extends \PHPUnit\Framework\TestCase @@ -31,9 +31,9 @@ class ObjectTest extends \PHPUnit\Framework\TestCase public function testConstructWithSupportedFiles() { $src = __DIR__ . '/../_files/documents/reader.docx'; - $oObject = new Object($src); + $oObject = new OLEObject($src); - $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Object', $oObject); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\OLEObject', $oObject); $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Image', $oObject->getStyle()); $this->assertEquals($src, $oObject->getSource()); } @@ -44,9 +44,9 @@ public function testConstructWithSupportedFiles() public function testConstructWithSupportedFilesLong() { $src = __DIR__ . '/../_files/documents/sheet.xls'; - $oObject = new Object($src); + $oObject = new OLEObject($src); - $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Object', $oObject); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\OLEObject', $oObject); $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Image', $oObject->getStyle()); $this->assertEquals($src, $oObject->getSource()); } @@ -59,7 +59,7 @@ public function testConstructWithSupportedFilesLong() public function testConstructWithNotSupportedFiles() { $src = __DIR__ . '/../_files/xsl/passthrough.xsl'; - $oObject = new Object($src); + $oObject = new OLEObject($src); $oObject->getSource(); } @@ -69,9 +69,9 @@ public function testConstructWithNotSupportedFiles() public function testConstructWithSupportedFilesAndStyle() { $src = __DIR__ . '/../_files/documents/sheet.xls'; - $oObject = new Object($src, array('width' => '230px')); + $oObject = new OLEObject($src, array('width' => '230px')); - $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Object', $oObject); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\OLEObject', $oObject); $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Image', $oObject->getStyle()); $this->assertEquals($src, $oObject->getSource()); } @@ -82,7 +82,7 @@ public function testConstructWithSupportedFilesAndStyle() public function testRelationId() { $src = __DIR__ . '/../_files/documents/sheet.xls'; - $oObject = new Object($src); + $oObject = new OLEObject($src); $iVal = rand(1, 1000); $oObject->setRelationId($iVal); @@ -95,7 +95,7 @@ public function testRelationId() public function testImageRelationId() { $src = __DIR__ . '/../_files/documents/sheet.xls'; - $oObject = new Object($src); + $oObject = new OLEObject($src); $iVal = rand(1, 1000); $oObject->setImageRelationId($iVal); diff --git a/tests/PhpWord/Element/SectionTest.php b/tests/PhpWord/Element/SectionTest.php index 8b6c9a4398..20f0f0f759 100644 --- a/tests/PhpWord/Element/SectionTest.php +++ b/tests/PhpWord/Element/SectionTest.php @@ -70,7 +70,7 @@ public function testAddElements() 'PageBreak', 'Table', 'ListItem', - 'Object', + 'OLEObject', 'Image', 'Title', 'TextRun', diff --git a/tests/PhpWord/Writer/Word2007/ElementTest.php b/tests/PhpWord/Writer/Word2007/ElementTest.php index 12f810ce79..4f0d50d96a 100644 --- a/tests/PhpWord/Writer/Word2007/ElementTest.php +++ b/tests/PhpWord/Writer/Word2007/ElementTest.php @@ -44,7 +44,7 @@ public function testUnmatchedElements() { $elements = array( 'CheckBox', 'Container', 'Footnote', 'Image', 'Link', 'ListItem', 'ListItemRun', - 'Object', 'PreserveText', 'Table', 'Text', 'TextBox', 'TextBreak', 'Title', 'TOC', + 'OLEObject', 'PreserveText', 'Table', 'Text', 'TextBox', 'TextBreak', 'Title', 'TOC', 'Field', 'Line', 'Shape', 'Chart', 'FormField', 'SDT', 'Bookmark', ); foreach ($elements as $element) {
    header a