1<?php
2
3namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
4
5use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
6use PhpOffice\PhpSpreadsheet\Spreadsheet;
7use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
8use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
9
10class Rels extends WriterPart
11{
12    /**
13     * Write relationships to XML format.
14     *
15     * @param Spreadsheet $spreadsheet
16     *
17     * @throws WriterException
18     *
19     * @return string XML Output
20     */
21    public function writeRelationships(Spreadsheet $spreadsheet)
22    {
23        // Create XML writer
24        $objWriter = null;
25        if ($this->getParentWriter()->getUseDiskCaching()) {
26            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
27        } else {
28            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
29        }
30
31        // XML header
32        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
33
34        // Relationships
35        $objWriter->startElement('Relationships');
36        $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships');
37
38        $customPropertyList = $spreadsheet->getProperties()->getCustomProperties();
39        if (!empty($customPropertyList)) {
40            // Relationship docProps/app.xml
41            $this->writeRelationship(
42                $objWriter,
43                4,
44                'http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties',
45                'docProps/custom.xml'
46            );
47        }
48
49        // Relationship docProps/app.xml
50        $this->writeRelationship(
51            $objWriter,
52            3,
53            'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties',
54            'docProps/app.xml'
55        );
56
57        // Relationship docProps/core.xml
58        $this->writeRelationship(
59            $objWriter,
60            2,
61            'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties',
62            'docProps/core.xml'
63        );
64
65        // Relationship xl/workbook.xml
66        $this->writeRelationship(
67            $objWriter,
68            1,
69            'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument',
70            'xl/workbook.xml'
71        );
72        // a custom UI in workbook ?
73        if ($spreadsheet->hasRibbon()) {
74            $this->writeRelationShip(
75                $objWriter,
76                5,
77                'http://schemas.microsoft.com/office/2006/relationships/ui/extensibility',
78                $spreadsheet->getRibbonXMLData('target')
79            );
80        }
81
82        $objWriter->endElement();
83
84        return $objWriter->getData();
85    }
86
87    /**
88     * Write workbook relationships to XML format.
89     *
90     * @param Spreadsheet $spreadsheet
91     *
92     * @throws WriterException
93     *
94     * @return string XML Output
95     */
96    public function writeWorkbookRelationships(Spreadsheet $spreadsheet)
97    {
98        // Create XML writer
99        $objWriter = null;
100        if ($this->getParentWriter()->getUseDiskCaching()) {
101            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
102        } else {
103            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
104        }
105
106        // XML header
107        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
108
109        // Relationships
110        $objWriter->startElement('Relationships');
111        $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships');
112
113        // Relationship styles.xml
114        $this->writeRelationship(
115            $objWriter,
116            1,
117            'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles',
118            'styles.xml'
119        );
120
121        // Relationship theme/theme1.xml
122        $this->writeRelationship(
123            $objWriter,
124            2,
125            'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme',
126            'theme/theme1.xml'
127        );
128
129        // Relationship sharedStrings.xml
130        $this->writeRelationship(
131            $objWriter,
132            3,
133            'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',
134            'sharedStrings.xml'
135        );
136
137        // Relationships with sheets
138        $sheetCount = $spreadsheet->getSheetCount();
139        for ($i = 0; $i < $sheetCount; ++$i) {
140            $this->writeRelationship(
141                $objWriter,
142                ($i + 1 + 3),
143                'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet',
144                'worksheets/sheet' . ($i + 1) . '.xml'
145            );
146        }
147        // Relationships for vbaProject if needed
148        // id : just after the last sheet
149        if ($spreadsheet->hasMacros()) {
150            $this->writeRelationShip(
151                $objWriter,
152                ($i + 1 + 3),
153                'http://schemas.microsoft.com/office/2006/relationships/vbaProject',
154                'vbaProject.bin'
155            );
156            ++$i; //increment i if needed for an another relation
157        }
158
159        $objWriter->endElement();
160
161        return $objWriter->getData();
162    }
163
164    /**
165     * Write worksheet relationships to XML format.
166     *
167     * Numbering is as follows:
168     *     rId1                 - Drawings
169     *  rId_hyperlink_x     - Hyperlinks
170     *
171     * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet
172     * @param int $pWorksheetId
173     * @param bool $includeCharts Flag indicating if we should write charts
174     *
175     * @throws WriterException
176     *
177     * @return string XML Output
178     */
179    public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet, $pWorksheetId = 1, $includeCharts = false)
180    {
181        // Create XML writer
182        $objWriter = null;
183        if ($this->getParentWriter()->getUseDiskCaching()) {
184            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
185        } else {
186            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
187        }
188
189        // XML header
190        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
191
192        // Relationships
193        $objWriter->startElement('Relationships');
194        $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships');
195
196        // Write drawing relationships?
197        $d = 0;
198        $drawingOriginalIds = [];
199        $unparsedLoadedData = $pWorksheet->getParent()->getUnparsedLoadedData();
200        if (isset($unparsedLoadedData['sheets'][$pWorksheet->getCodeName()]['drawingOriginalIds'])) {
201            $drawingOriginalIds = $unparsedLoadedData['sheets'][$pWorksheet->getCodeName()]['drawingOriginalIds'];
202        }
203
204        if ($includeCharts) {
205            $charts = $pWorksheet->getChartCollection();
206        } else {
207            $charts = [];
208        }
209
210        if (($pWorksheet->getDrawingCollection()->count() > 0) || (count($charts) > 0) || $drawingOriginalIds) {
211            $relPath = '../drawings/drawing' . $pWorksheetId . '.xml';
212            $rId = ++$d;
213
214            if (isset($drawingOriginalIds[$relPath])) {
215                $rId = (int) (substr($drawingOriginalIds[$relPath], 3));
216            }
217
218            $this->writeRelationship(
219                $objWriter,
220                $rId,
221                'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing',
222                $relPath
223            );
224        }
225
226        // Write hyperlink relationships?
227        $i = 1;
228        foreach ($pWorksheet->getHyperlinkCollection() as $hyperlink) {
229            if (!$hyperlink->isInternal()) {
230                $this->writeRelationship(
231                    $objWriter,
232                    '_hyperlink_' . $i,
233                    'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink',
234                    $hyperlink->getUrl(),
235                    'External'
236                );
237
238                ++$i;
239            }
240        }
241
242        // Write comments relationship?
243        $i = 1;
244        if (count($pWorksheet->getComments()) > 0) {
245            $this->writeRelationship(
246                $objWriter,
247                '_comments_vml' . $i,
248                'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing',
249                '../drawings/vmlDrawing' . $pWorksheetId . '.vml'
250            );
251
252            $this->writeRelationship(
253                $objWriter,
254                '_comments' . $i,
255                'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments',
256                '../comments' . $pWorksheetId . '.xml'
257            );
258        }
259
260        // Write header/footer relationship?
261        $i = 1;
262        if (count($pWorksheet->getHeaderFooter()->getImages()) > 0) {
263            $this->writeRelationship(
264                $objWriter,
265                '_headerfooter_vml' . $i,
266                'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing',
267                '../drawings/vmlDrawingHF' . $pWorksheetId . '.vml'
268            );
269        }
270
271        $this->writeUnparsedRelationship($pWorksheet, $objWriter, 'ctrlProps', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/ctrlProp');
272        $this->writeUnparsedRelationship($pWorksheet, $objWriter, 'vmlDrawings', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing');
273        $this->writeUnparsedRelationship($pWorksheet, $objWriter, 'printerSettings', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerSettings');
274
275        $objWriter->endElement();
276
277        return $objWriter->getData();
278    }
279
280    private function writeUnparsedRelationship(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet, XMLWriter $objWriter, $relationship, $type)
281    {
282        $unparsedLoadedData = $pWorksheet->getParent()->getUnparsedLoadedData();
283        if (!isset($unparsedLoadedData['sheets'][$pWorksheet->getCodeName()][$relationship])) {
284            return;
285        }
286
287        foreach ($unparsedLoadedData['sheets'][$pWorksheet->getCodeName()][$relationship] as $rId => $value) {
288            $this->writeRelationship(
289                $objWriter,
290                $rId,
291                $type,
292                $value['relFilePath']
293            );
294        }
295    }
296
297    /**
298     * Write drawing relationships to XML format.
299     *
300     * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet
301     * @param int &$chartRef Chart ID
302     * @param bool $includeCharts Flag indicating if we should write charts
303     *
304     * @throws WriterException
305     *
306     * @return string XML Output
307     */
308    public function writeDrawingRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet, &$chartRef, $includeCharts = false)
309    {
310        // Create XML writer
311        $objWriter = null;
312        if ($this->getParentWriter()->getUseDiskCaching()) {
313            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
314        } else {
315            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
316        }
317
318        // XML header
319        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
320
321        // Relationships
322        $objWriter->startElement('Relationships');
323        $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships');
324
325        // Loop through images and write relationships
326        $i = 1;
327        $iterator = $pWorksheet->getDrawingCollection()->getIterator();
328        while ($iterator->valid()) {
329            if ($iterator->current() instanceof \PhpOffice\PhpSpreadsheet\Worksheet\Drawing
330                || $iterator->current() instanceof MemoryDrawing) {
331                // Write relationship for image drawing
332                /** @var \PhpOffice\PhpSpreadsheet\Worksheet\Drawing $drawing */
333                $drawing = $iterator->current();
334                $this->writeRelationship(
335                    $objWriter,
336                    $i,
337                    'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image',
338                    '../media/' . str_replace(' ', '', $drawing->getIndexedFilename())
339                );
340
341                $i = $this->writeDrawingHyperLink($objWriter, $drawing, $i);
342            }
343
344            $iterator->next();
345            ++$i;
346        }
347
348        if ($includeCharts) {
349            // Loop through charts and write relationships
350            $chartCount = $pWorksheet->getChartCount();
351            if ($chartCount > 0) {
352                for ($c = 0; $c < $chartCount; ++$c) {
353                    $this->writeRelationship(
354                        $objWriter,
355                        $i++,
356                        'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart',
357                        '../charts/chart' . ++$chartRef . '.xml'
358                    );
359                }
360            }
361        }
362
363        $objWriter->endElement();
364
365        return $objWriter->getData();
366    }
367
368    /**
369     * Write header/footer drawing relationships to XML format.
370     *
371     * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet
372     *
373     * @throws WriterException
374     *
375     * @return string XML Output
376     */
377    public function writeHeaderFooterDrawingRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet)
378    {
379        // Create XML writer
380        $objWriter = null;
381        if ($this->getParentWriter()->getUseDiskCaching()) {
382            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
383        } else {
384            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
385        }
386
387        // XML header
388        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
389
390        // Relationships
391        $objWriter->startElement('Relationships');
392        $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships');
393
394        // Loop through images and write relationships
395        foreach ($pWorksheet->getHeaderFooter()->getImages() as $key => $value) {
396            // Write relationship for image drawing
397            $this->writeRelationship(
398                $objWriter,
399                $key,
400                'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image',
401                '../media/' . $value->getIndexedFilename()
402            );
403        }
404
405        $objWriter->endElement();
406
407        return $objWriter->getData();
408    }
409
410    /**
411     * Write Override content type.
412     *
413     * @param XMLWriter $objWriter XML Writer
414     * @param int $pId Relationship ID. rId will be prepended!
415     * @param string $pType Relationship type
416     * @param string $pTarget Relationship target
417     * @param string $pTargetMode Relationship target mode
418     *
419     * @throws WriterException
420     */
421    private function writeRelationship(XMLWriter $objWriter, $pId, $pType, $pTarget, $pTargetMode = '')
422    {
423        if ($pType != '' && $pTarget != '') {
424            // Write relationship
425            $objWriter->startElement('Relationship');
426            $objWriter->writeAttribute('Id', 'rId' . $pId);
427            $objWriter->writeAttribute('Type', $pType);
428            $objWriter->writeAttribute('Target', $pTarget);
429
430            if ($pTargetMode != '') {
431                $objWriter->writeAttribute('TargetMode', $pTargetMode);
432            }
433
434            $objWriter->endElement();
435        } else {
436            throw new WriterException('Invalid parameters passed.');
437        }
438    }
439
440    /**
441     * @param $objWriter
442     * @param \PhpOffice\PhpSpreadsheet\Worksheet\Drawing $drawing
443     * @param $i
444     *
445     * @throws WriterException
446     *
447     * @return int
448     */
449    private function writeDrawingHyperLink($objWriter, $drawing, $i)
450    {
451        if ($drawing->getHyperlink() === null) {
452            return $i;
453        }
454
455        ++$i;
456        $this->writeRelationship(
457            $objWriter,
458            $i,
459            'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink',
460            $drawing->getHyperlink()->getUrl(),
461            $drawing->getHyperlink()->getTypeHyperlink()
462        );
463
464        return $i;
465    }
466}
467