Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
127 / 127
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
Word2007
100.00% covered (success)
100.00%
127 / 127
100.00% covered (success)
100.00%
10 / 10
34
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
3
 save
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
6
 getContentTypes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRelationships
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addHeaderFooterMedia
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 addHeaderFooterContent
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 addNotes
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
 addComments
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 addChart
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 registerContentTypes
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2/**
3 * This file is part of PHPWord - A pure PHP library for reading and writing
4 * word processing documents.
5 *
6 * PHPWord is free software distributed under the terms of the GNU Lesser
7 * General Public License version 3 as published by the Free Software Foundation.
8 *
9 * For the full copyright and license information, please read the LICENSE
10 * file that was distributed with this source code. For the full list of
11 * contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
12 *
13 * @see         https://github.com/PHPOffice/PHPWord
14 *
15 * @license     http://www.gnu.org/licenses/lgpl.txt LGPL version 3
16 */
17
18namespace PhpOffice\PhpWord\Writer;
19
20use PhpOffice\PhpWord\Element\Section;
21use PhpOffice\PhpWord\Media;
22use PhpOffice\PhpWord\PhpWord;
23use PhpOffice\PhpWord\Shared\ZipArchive;
24
25/**
26 * Word2007 writer.
27 */
28class Word2007 extends AbstractWriter implements WriterInterface
29{
30    /**
31     * Content types values.
32     *
33     * @var array
34     */
35    private $contentTypes = ['default' => [], 'override' => []];
36
37    /**
38     * Document relationship.
39     *
40     * @var array
41     */
42    private $relationships = [];
43
44    /**
45     * Create new Word2007 writer.
46     *
47     * @param \PhpOffice\PhpWord\PhpWord
48     */
49    public function __construct(?PhpWord $phpWord = null)
50    {
51        // Assign PhpWord
52        $this->setPhpWord($phpWord);
53
54        // Create parts
55        // The first four files need to be in this order for Mimetype detection to work
56        $this->parts = [
57            'ContentTypes' => '[Content_Types].xml',
58            'Rels' => '_rels/.rels',
59            'RelsDocument' => 'word/_rels/document.xml.rels',
60            'Document' => 'word/document.xml',
61            'DocPropsApp' => 'docProps/app.xml',
62            'DocPropsCore' => 'docProps/core.xml',
63            'DocPropsCustom' => 'docProps/custom.xml',
64            'Comments' => 'word/comments.xml',
65            'Styles' => 'word/styles.xml',
66            'Numbering' => 'word/numbering.xml',
67            'Settings' => 'word/settings.xml',
68            'WebSettings' => 'word/webSettings.xml',
69            'FontTable' => 'word/fontTable.xml',
70            'Theme' => 'word/theme/theme1.xml',
71            'RelsPart' => '',
72            'Header' => '',
73            'Footer' => '',
74            'Footnotes' => '',
75            'Endnotes' => '',
76            'Chart' => '',
77        ];
78        foreach (array_keys($this->parts) as $partName) {
79            $partClass = static::class . '\\Part\\' . $partName;
80            if (class_exists($partClass)) {
81                /** @var \PhpOffice\PhpWord\Writer\Word2007\Part\AbstractPart $part Type hint */
82                $part = new $partClass();
83                $part->setParentWriter($this);
84                $this->writerParts[strtolower($partName)] = $part;
85            }
86        }
87
88        // Set package paths
89        $this->mediaPaths = ['image' => 'word/media/', 'object' => 'word/embeddings/'];
90    }
91
92    /**
93     * Save document by name.
94     */
95    public function save(string $filename): void
96    {
97        $filename = $this->getTempFile($filename);
98        $zip = $this->getZipArchive($filename);
99        $phpWord = $this->getPhpWord();
100
101        // Content types
102        $this->contentTypes['default'] = [
103            'rels' => 'application/vnd.openxmlformats-package.relationships+xml',
104            'xml' => 'application/xml',
105        ];
106
107        // Add section media files
108        $sectionMedia = Media::getElements('section');
109        if (!empty($sectionMedia)) {
110            $this->addFilesToPackage($zip, $sectionMedia);
111            $this->registerContentTypes($sectionMedia);
112            foreach ($sectionMedia as $element) {
113                $this->relationships[] = $element;
114            }
115        }
116
117        // Add header/footer media files & relations
118        $this->addHeaderFooterMedia($zip, 'header');
119        $this->addHeaderFooterMedia($zip, 'footer');
120
121        // Add header/footer contents
122        $rId = Media::countElements('section') + 6; //@see Rels::writeDocRels for 6 first elements
123        $sections = $phpWord->getSections();
124        foreach ($sections as $section) {
125            $this->addHeaderFooterContent($section, $zip, 'header', $rId);
126            $this->addHeaderFooterContent($section, $zip, 'footer', $rId);
127        }
128
129        $this->addNotes($zip, $rId, 'footnote');
130        $this->addNotes($zip, $rId, 'endnote');
131        $this->addComments($zip, $rId);
132        $this->addChart($zip, $rId);
133
134        // Write parts
135        foreach ($this->parts as $partName => $fileName) {
136            if ($fileName != '') {
137                $zip->addFromString($fileName, $this->getWriterPart($partName)->write());
138            }
139        }
140
141        // Close zip archive and cleanup temp file
142        $zip->close();
143        $this->cleanupTempFile();
144    }
145
146    /**
147     * Get content types.
148     *
149     * @return array
150     */
151    public function getContentTypes()
152    {
153        return $this->contentTypes;
154    }
155
156    /**
157     * Get content types.
158     *
159     * @return array
160     */
161    public function getRelationships()
162    {
163        return $this->relationships;
164    }
165
166    /**
167     * Add header/footer media files, e.g. footer1.xml.rels.
168     *
169     * @param string $docPart
170     */
171    private function addHeaderFooterMedia(ZipArchive $zip, $docPart): void
172    {
173        $elements = Media::getElements($docPart);
174        if (!empty($elements)) {
175            foreach ($elements as $file => $media) {
176                if (count($media) > 0) {
177                    if (!empty($media)) {
178                        $this->addFilesToPackage($zip, $media);
179                        $this->registerContentTypes($media);
180                    }
181
182                    /** @var \PhpOffice\PhpWord\Writer\Word2007\Part\AbstractPart $writerPart Type hint */
183                    $writerPart = $this->getWriterPart('relspart')->setMedia($media);
184                    $zip->addFromString("word/_rels/{$file}.xml.rels", $writerPart->write());
185                }
186            }
187        }
188    }
189
190    /**
191     * Add header/footer content.
192     *
193     * @param string $elmType header|footer
194     * @param int &$rId
195     */
196    private function addHeaderFooterContent(Section &$section, ZipArchive $zip, $elmType, &$rId): void
197    {
198        $getFunction = $elmType == 'header' ? 'getHeaders' : 'getFooters';
199        $elmCount = ($section->getSectionId() - 1) * 3;
200        $elements = $section->$getFunction();
201        /** @var \PhpOffice\PhpWord\Element\AbstractElement $element Type hint */
202        foreach ($elements as &$element) {
203            ++$elmCount;
204            $element->setRelationId(++$rId);
205            $elmFile = "{$elmType}{$elmCount}.xml"; // e.g. footer1.xml
206            $this->contentTypes['override']["/word/$elmFile"] = $elmType;
207            $this->relationships[] = ['target' => $elmFile, 'type' => $elmType, 'rID' => $rId];
208
209            /** @var \PhpOffice\PhpWord\Writer\Word2007\Part\AbstractPart $writerPart Type hint */
210            $writerPart = $this->getWriterPart($elmType)->setElement($element);
211            $zip->addFromString("word/$elmFile", $writerPart->write());
212        }
213    }
214
215    /**
216     * Add footnotes/endnotes.
217     *
218     * @param int &$rId
219     * @param string $noteType
220     */
221    private function addNotes(ZipArchive $zip, &$rId, $noteType = 'footnote'): void
222    {
223        $phpWord = $this->getPhpWord();
224        $noteType = ($noteType == 'endnote') ? 'endnote' : 'footnote';
225        $partName = "{$noteType}s";
226        $method = 'get' . $partName;
227        $collection = $phpWord->$method();
228
229        // Add footnotes media files, relations, and contents
230        if ($collection->countItems() > 0) {
231            $media = Media::getElements($noteType);
232            $this->addFilesToPackage($zip, $media);
233            $this->registerContentTypes($media);
234            $this->contentTypes['override']["/word/{$partName}.xml"] = $partName;
235            $this->relationships[] = ['target' => "{$partName}.xml", 'type' => $partName, 'rID' => ++$rId];
236
237            // Write relationships file, e.g. word/_rels/footnotes.xml
238            if (!empty($media)) {
239                /** @var \PhpOffice\PhpWord\Writer\Word2007\Part\AbstractPart $writerPart Type hint */
240                $writerPart = $this->getWriterPart('relspart')->setMedia($media);
241                $zip->addFromString("word/_rels/{$partName}.xml.rels", $writerPart->write());
242            }
243
244            // Write content file, e.g. word/footnotes.xml
245            $writerPart = $this->getWriterPart($partName)->setElements($collection->getItems());
246            $zip->addFromString("word/{$partName}.xml", $writerPart->write());
247        }
248    }
249
250    /**
251     * Add comments.
252     *
253     * @param int &$rId
254     */
255    private function addComments(ZipArchive $zip, &$rId): void
256    {
257        $phpWord = $this->getPhpWord();
258        $collection = $phpWord->getComments();
259        $partName = 'comments';
260
261        // Add comment relations and contents
262        if ($collection->countItems() > 0) {
263            $this->relationships[] = ['target' => "{$partName}.xml", 'type' => $partName, 'rID' => ++$rId];
264
265            // Write content file, e.g. word/comments.xml
266            $writerPart = $this->getWriterPart($partName)->setElements($collection->getItems());
267            $zip->addFromString("word/{$partName}.xml", $writerPart->write());
268        }
269    }
270
271    /**
272     * Add chart.
273     *
274     * @param int &$rId
275     */
276    private function addChart(ZipArchive $zip, &$rId): void
277    {
278        $phpWord = $this->getPhpWord();
279
280        $collection = $phpWord->getCharts();
281        $index = 0;
282        if ($collection->countItems() > 0) {
283            /** @var \PhpOffice\PhpWord\Element\Chart $chart */
284            foreach ($collection->getItems() as $chart) {
285                ++$index;
286                ++$rId;
287                $filename = "charts/chart{$index}.xml";
288
289                // ContentTypes.xml
290                $this->contentTypes['override']["/word/{$filename}"] = 'chart';
291
292                // word/_rels/document.xml.rel
293                $this->relationships[] = ['target' => $filename, 'type' => 'chart', 'rID' => $rId];
294
295                // word/charts/chartN.xml
296                $chart->setRelationId($rId);
297                $writerPart = $this->getWriterPart('Chart');
298                $writerPart->setElement($chart);
299                $zip->addFromString("word/{$filename}", $writerPart->write());
300            }
301        }
302    }
303
304    /**
305     * Register content types for each media.
306     *
307     * @param array $media
308     */
309    private function registerContentTypes($media): void
310    {
311        foreach ($media as $medium) {
312            $mediumType = $medium['type'];
313            if ($mediumType == 'image') {
314                $extension = $medium['imageExtension'];
315                if (!isset($this->contentTypes['default'][$extension])) {
316                    $this->contentTypes['default'][$extension] = $medium['imageType'];
317                }
318            } elseif ($mediumType == 'object') {
319                if (!isset($this->contentTypes['default']['bin'])) {
320                    $this->contentTypes['default']['bin'] = 'application/vnd.openxmlformats-officedocument.oleObject';
321                }
322            }
323        }
324    }
325}