Test Setup Failed
Push — master ( fce549...3beee5 )
by Tobias
07:17 queued 01:27
created

DocxMustache::AddContentType()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 3 Features 0
Metric Value
c 7
b 3
f 0
dl 0
loc 27
rs 8.439
cc 5
eloc 15
nc 7
nop 1
1
<?php
2
3
namespace WrkLst\DocxMustache;
4
5
use Exception;
6
use Illuminate\Support\Facades\Log;
7
8
//Custom DOCX template class to change content based on mustache templating engine.
9
class DocxMustache
10
{
11
    public $items;
12
    public $word_doc;
13
    public $template_file_name;
14
    public $template_file;
15
    public $local_path;
16
    public $storageDisk;
17
    public $storagePathPrefix;
18
    public $zipper;
19
    public $imageManipulation;
20
    public $verbose;
21
22
    public function __construct($items, $local_template_file)
23
    {
24
        $this->items = $items;
25
        $this->template_file_name = basename($local_template_file);
26
        $this->template_file = $local_template_file;
27
        $this->word_doc = false;
28
        $this->zipper = new \Chumper\Zipper\Zipper();
29
30
        //name of disk for storage
31
        $this->storageDisk = 'local';
32
33
        //prefix within your storage path
34
        $this->storagePathPrefix = 'app/';
35
36
        //if you use img urls that support manipulation via parameter
37
        $this->imageManipulation = ''; //'&w=1800';
38
39
        $this->verbose = false;
40
    }
41
42
    public function Execute()
43
    {
44
        $this->CopyTmplate();
45
        $this->ReadTeamplate();
46
    }
47
48
    /**
49
     * @param string $file
50
     */
51
    public function StoragePath($file)
52
    {
53
        return storage_path($file);
54
    }
55
56
    /**
57
     * @param string $msg
58
     */
59
    protected function Log($msg)
60
    {
61
        //introduce logging method here to keep track of process
62
        // can be overwritten in extended class to log with custom preocess logger
63
        if ($this->verbose) {
64
            Log::error($msg);
65
        }
66
    }
67
68
    public function CleanUpTmpDirs()
69
    {
70
        $now = time();
71
        $isExpired = ($now - (60 * 240));
72
        $disk = \Storage::disk($this->storageDisk);
73
        $all_dirs = $disk->directories($this->storagePathPrefix.'DocxMustache');
74
        foreach ($all_dirs as $dir) {
75
            //delete dirs older than 20min
76
            if ($disk->lastModified($dir) < $isExpired) {
77
                $disk->deleteDirectory($dir);
78
            }
79
        }
80
    }
81
82
    public function GetTmpDir()
83
    {
84
        $this->CleanUpTmpDirs();
85
        $path = $this->storagePathPrefix.'DocxMustache/'.uniqid($this->template_file).'/';
86
        \File::makeDirectory($this->StoragePath($path), 0775, true);
87
88
        return $path;
89
    }
90
91
    public function CopyTmplate()
92
    {
93
        $this->Log('Get Copy of Template');
94
        $this->local_path = $this->GetTmpDir();
95
        \Storage::disk($this->storageDisk)->copy($this->storagePathPrefix.$this->template_file, $this->local_path.$this->template_file_name);
96
    }
97
98
    protected function exctractOpenXmlFile($file)
99
    {
100
        $this->zipper->make($this->StoragePath($this->local_path.$this->template_file_name))
101
            ->extractTo($this->StoragePath($this->local_path), [$file], \Chumper\Zipper\Zipper::WHITELIST);
102
    }
103
104
    protected function ReadOpenXmlFile($file, $type = 'file')
105
    {
106
        $this->exctractOpenXmlFile($file);
107
108
        if ($type == 'file') {
109
            if ($file_contents = \Storage::disk($this->storageDisk)->get($this->local_path.$file)) {
110
                return $file_contents;
111
            } else {
112
                throw new Exception('Cannot not read file '.$file);
113
            }
114
        } else {
115
            if ($xml_object = simplexml_load_file($this->StoragePath($this->local_path.$file))) {
116
                return $xml_object;
117
            } else {
118
                throw new Exception('Cannot load XML Object from file '.$file);
119
            }
120
        }
121
    }
122
123
    protected function SaveOpenXmlFile($file, $folder, $content)
124
    {
125
        \Storage::disk($this->storageDisk)
126
            ->put($this->local_path.$file, $content);
127
        //add new content to word doc
128
        if ($folder) {
129
            $this->zipper->folder($folder)
130
                ->add($this->StoragePath($this->local_path.$file));
131
        } else {
132
            $this->zipper
133
                ->add($this->StoragePath($this->local_path.$file));
134
        }
135
    }
136
137
    protected function SaveOpenXmlObjectToFile($xmlObject, $file, $folder)
138
    {
139
        if ($xmlString = $xmlObject->asXML()) {
140
            $this->SaveOpenXmlFile($file, $folder, $xmlString);
141
        } else {
142
            throw new Exception('Cannot generate xml for '.$file);
143
        }
144
    }
145
146
    public function ReadTeamplate()
147
    {
148
        $this->Log('Analyze Template');
149
        //get the main document out of the docx archive
150
        $this->word_doc = $this->ReadOpenXmlFile('word/document.xml', 'file');
151
152
        $this->Log('Merge Data into Template');
153
154
        $this->word_doc = MustacheRender::render($this->items, $this->word_doc);
155
156
        $this->word_doc = HtmlConversion::convert($this->word_doc);
157
158
        $this->ImageReplacer();
159
160
        $this->Log('Compact Template with Data');
161
162
        $this->SaveOpenXmlFile('word/document.xml', 'word', $this->word_doc);
163
        $this->zipper->close();
164
    }
165
166
    protected function AddContentType($imageCt = 'jpeg')
167
    {
168
        $ct_file = $this->ReadOpenXmlFile('[Content_Types].xml', 'object');
169
170
        if (! ($ct_file instanceof \Traversable)) {
171
            throw new Exception('Cannot traverse through [Content_Types].xml.');
172
        }
173
174
        //check if content type for jpg has been set
175
        $i = 0;
176
        $ct_already_set = false;
177
        foreach ($ct_file as $ct) {
178
            if ((string) $ct_file->Default[$i]['Extension'] == $imageCt) {
179
                $ct_already_set = true;
180
            }
181
            $i++;
182
        }
183
184
        //if content type for jpg has not been set, add it to xml
185
        // and save xml to file and add it to the archive
186
        if (! $ct_already_set) {
187
            $sxe = $ct_file->addChild('Default');
188
            $sxe->addAttribute('Extension', $imageCt);
189
            $sxe->addAttribute('ContentType', 'image/'.$imageCt);
190
            $this->SaveOpenXmlObjectToFile($ct_file, '[Content_Types].xml', false);
191
        }
192
    }
193
194
    protected function FetchReplaceableImages(&$main_file, $ns)
195
    {
196
        //set up basic arrays to keep track of imgs
197
        $imgs = [];
198
        $imgs_replaced = []; // so they can later be removed from media and relation file.
199
        $newIdCounter = 1;
200
201
        //iterate through all drawing containers of the xml document
202
        foreach ($main_file->xpath('//w:drawing') as $k=>$drawing) {
203
            //figure out if there is a URL saved in the description field of the img
204
            $img_url = $this->AnalyseImgUrlString($drawing->children($ns['wp'])->xpath('wp:docPr')[0]->attributes()['descr']);
205
            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->xpath('wp:docPr')[0]->attributes()['descr'] = $img_url['rest'];
206
207
            //if there is a url, save this img as a img to be replaced
208
            if ($img_url['valid']) {
209
                $ueid = 'wrklstId'.$newIdCounter;
210
                $wasId = (string) $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])->graphic->graphicData->children($ns['pic'])->pic->blipFill->children($ns['a'])->blip->attributes($ns['r'])['embed'];
211
212
                //get dimensions
213
                $cx = (int) $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])->xfrm->ext->attributes()['cx'];
214
                $cy = (int) $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])->xfrm->ext->attributes()['cy'];
215
216
                //remember img as being replaced
217
                $imgs_replaced[$wasId] = $wasId;
218
219
                //set new img id
220
                $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])->graphic->graphicData->children($ns['pic'])->pic->blipFill->children($ns['a'])->blip->attributes($ns['r'])['embed'] = $ueid;
221
222
                $imgs[] = [
223
                    'cx'     => (int) $cx,
224
                    'cy'     => (int) $cy,
225
                    'wasId'  => $wasId,
226
                    'id'     => $ueid,
227
                    'url'    => $img_url['url'],
228
                    'path'    => $img_url['path'],
229
                    'mode'    => $img_url['mode'],
230
                ];
231
232
                $newIdCounter++;
233
            }
234
        }
235
236
        return [
237
            'imgs'          => $imgs,
238
            'imgs_replaced' => $imgs_replaced,
239
        ];
240
    }
241
242
    protected function RemoveReplaceImages($imgs_replaced, &$rels_file)
243
    {
244
        //TODO: check if the same img is used at a different position int he file as well, as otherwise broken images are produced.
245
        //iterate through replaced images and clean rels files from them
246
        foreach ($imgs_replaced as $img_replaced) {
247
            $i = 0;
248
            foreach ($rels_file as $rel) {
249
                if ((string) $rel->attributes()['Id'] == $img_replaced) {
250
                    $this->zipper->remove('word/'.(string) $rel->attributes()['Target']);
251
                    unset($rels_file->Relationship[$i]);
252
                }
253
                $i++;
254
            }
255
        }
256
    }
257
258
    protected function InsertImages($ns, &$imgs, &$rels_file, &$main_file)
259
    {
260
        $docimage = new DocImage();
261
        $allowed_imgs = $docimage->AllowedContentTypeImages();
262
        $image_i = 1;
263
        //iterate through replacable images
264
        foreach ($imgs as $k=>$img) {
265
            $this->Log('Merge Images into Template - '.round($image_i / count($imgs) * 100).'%');
266
            //get file type of img and test it against supported imgs
267
            if ($imgageData = $docimage->GetImageFromUrl($img['mode'] == 'url' ? $img['url'] : $img['path'], $img['mode'] == 'url' ? $this->imageManipulation : '')) {
268
                $imgs[$k]['img_file_src'] = str_replace('wrklstId', 'wrklst_image', $img['id']).$allowed_imgs[$imgageData['mime']];
269
                $imgs[$k]['img_file_dest'] = str_replace('wrklstId', 'wrklst_image', $img['id']).'.jpeg';
270
271
                $resampled_img = $docimage->ResampleImage($this, $imgs, $k, $imgageData['data']);
272
273
                $sxe = $rels_file->addChild('Relationship');
274
                $sxe->addAttribute('Id', $img['id']);
275
                $sxe->addAttribute('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image');
276
                $sxe->addAttribute('Target', 'media/'.$imgs[$k]['img_file_dest']);
277
278
                foreach ($main_file->xpath('//w:drawing') as $k=>$drawing) {
279
                    if ($img['id'] == $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
280
                        ->graphic->graphicData->children($ns['pic'])->pic->blipFill->children($ns['a'])
281
                        ->blip->attributes($ns['r'])['embed']) {
282
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
283
                            ->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])
284
                            ->xfrm->ext->attributes()['cx'] = $resampled_img['width_emus'];
285
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
286
                            ->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])
287
                            ->xfrm->ext->attributes()['cy'] = $resampled_img['height_emus'];
288
                        //anchor images
289
                        if (isset($main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->anchor)) {
290
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->anchor->extent->attributes()['cx'] = $resampled_img['width_emus'];
291
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->anchor->extent->attributes()['cy'] = $resampled_img['height_emus'];
292
                        }
293
                        //inline images
294
                        elseif (isset($main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline)) {
295
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline->extent->attributes()['cx'] = $resampled_img['width_emus'];
296
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline->extent->attributes()['cy'] = $resampled_img['height_emus'];
297
                        }
298
299
                        break;
300
                    }
301
                }
302
            }
303
            $image_i++;
304
        }
305
    }
306
307
    protected function ImageReplacer()
308
    {
309
        $this->Log('Merge Images into Template');
310
311
        //load main doc xml
312
        $main_file = simplexml_load_string($this->word_doc);
313
314
        //get all namespaces of the document
315
        $ns = $main_file->getNamespaces(true);
316
317
        $replaceableImage = $this->FetchReplaceableImages($main_file, $ns);
318
        $imgs = $replaceableImage['imgs'];
319
        $imgs_replaced = $replaceableImage['imgs_replaced'];
0 ignored issues
show
Unused Code introduced by
$imgs_replaced is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
320
321
        $rels_file = $this->ReadOpenXmlFile('word/_rels/document.xml.rels', 'object');
322
323
        //do not remove until it is checked if the same img is used at a different position int he file as well, as otherwise broken images are produced.
324
        //$this->RemoveReplaceImages($imgs_replaced, $rels_file);
0 ignored issues
show
Unused Code Comprehensibility introduced by
80% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
325
326
        //add jpg content type if not set
327
        $this->AddContentType('jpeg');
328
329
        $this->InsertImages($ns, $imgs, $rels_file, $main_file);
330
331
        $this->SaveOpenXmlObjectToFile($rels_file, 'word/_rels/document.xml.rels', 'word/_rels');
332
333
        if ($main_file_xml = $main_file->asXML()) {
334
            $this->word_doc = $main_file_xml;
335
        } else {
336
            throw new Exception('Cannot generate xml for word/document.xml.');
337
        }
338
    }
339
340
    /**
341
     * @param string $string
342
     */
343
    protected function AnalyseImgUrlString($string)
344
    {
345
        $string = (string) $string;
346
        $start = '[IMG-REPLACE]';
347
        $end = '[/IMG-REPLACE]';
348
        $start_local = '[LOCAL_IMG_REPLACE]';
349
        $end_local = '[/LOCAL_IMG_REPLACE]';
350
        $valid = false;
351
        $url = '';
352
        $path = '';
353
354
        if ($string != str_replace($start, '', $string) && $string == str_replace($start.$end, '', $string)) {
355
            $string = ' '.$string;
356
            $ini = strpos($string, $start);
357 View Code Duplication
            if ($ini == 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
358
                $url = '';
359
                $rest = $string;
360
            } else {
361
                $ini += strlen($start);
362
                $len = ((strpos($string, $end, $ini)) - $ini);
363
                $url = substr($string, $ini, $len);
364
365
                $ini = strpos($string, $start);
366
                $len = strpos($string, $end, $ini + strlen($start)) + strlen($end);
367
                $rest = substr($string, 0, $ini).substr($string, $len);
368
            }
369
370
            $valid = true;
371
372
            //TODO: create a better url validity check
373
            if (! trim(str_replace(['http', 'https', ':', ' '], '', $url)) || $url == str_replace('http', '', $url)) {
374
                $valid = false;
375
            }
376
            $mode = 'url';
377
        } elseif ($string != str_replace($start_local, '', $string) && $string == str_replace($start_local.$end_local, '', $string)) {
378
            $string = ' '.$string;
379
            $ini = strpos($string, $start_local);
380 View Code Duplication
            if ($ini == 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
381
                $path = '';
382
                $rest = $string;
383
            } else {
384
                $ini += strlen($start_local);
385
                $len = ((strpos($string, $end_local, $ini)) - $ini);
386
                $path = str_replace('..', '', substr($string, $ini, $len));
387
388
                $ini = strpos($string, $start_local);
389
                $len = strpos($string, $end_local, $ini + strlen($start)) + strlen($end_local);
390
                $rest = substr($string, 0, $ini).substr($string, $len);
391
            }
392
393
            $valid = true;
394
395
            //check if path starts with storage path
396
            if (! starts_with($path, storage_path())) {
397
                $valid = false;
398
            }
399
            $mode = 'path';
400
        } else {
401
            $mode = 'nothing';
402
            $url = '';
403
            $path = '';
404
            $rest = str_replace([$start, $end, $start_local, $end_local], '', $string);
405
        }
406
407
        return [
408
            'mode' => $mode,
409
            'url'  => trim($url),
410
            'path' => trim($path),
411
            'rest' => trim($rest),
412
            'valid' => $valid,
413
        ];
414
    }
415
416
    public function SaveAsPdf()
417
    {
418
        $this->Log('Converting DOCX to PDF');
419
        //convert to pdf with libre office
420
        $command = 'soffice --headless --convert-to pdf '.$this->StoragePath($this->local_path.$this->template_file_name).' --outdir '.$this->StoragePath($this->local_path);
421
        $process = new \Symfony\Component\Process\Process($command);
422
        $process->start();
423
        while ($process->isRunning()) {
424
            //wait until process is ready
425
        }
426
        // executes after the command finishes
427
        if (! $process->isSuccessful()) {
428
            throw new \Symfony\Component\Process\Exception\ProcessFailedException($process);
429
        } else {
430
            $path_parts = pathinfo($this->StoragePath($this->local_path.$this->template_file_name));
431
432
            return $this->StoragePath($this->local_path.$path_parts['filename'].'pdf');
433
        }
434
    }
435
}
436