Passed
Pull Request — master (#30)
by
unknown
02:55 queued 01:12
created

DocxMustache::retrieveFilesList()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 3
nc 2
nop 2
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 $useStoragePath = false;
19
    public $zipper;
20
    public $imageManipulation;
21
    public $verbose;
22
23
    public function __construct($items, $local_template_file)
24
    {
25
        $this->items = $items;
26
        $this->template_file_name = basename($local_template_file);
27
        $this->template_file = $local_template_file;
28
        $this->word_doc = false;
29
        $this->zipper = new \Wrklst\Zipper\Zipper();
30
31
        //name of disk for storage
32
        $this->storageDisk = 'local';
33
34
        //prefix within your storage path
35
        $this->storagePathPrefix = 'app/';
36
37
        //if you use img urls that support manipulation via parameter
38
        $this->imageManipulation = ''; //'&w=1800';
39
40
        $this->verbose = false;
41
    }
42
43
    public function Execute($dpi = 72)
44
    {
45
        $this->CopyTmplate();
46
        $this->zipper->make($this->StoragePath($this->local_path.$this->template_file_name));
47
        $this->ReadTeamplate($dpi);
48
    }
49
50
    /**
51
     * @param string $file
52
     */
53
    public function StoragePath($file)
54
    {
55
        if ($this->useStoragePath) {
56
            return \Storage::disk($this->storageDisk)->path($file);
57
        }
58
59
        return storage_path($file);
60
    }
61
62
    /**
63
     * @param string $msg
64
     */
65
    protected function Log($msg)
66
    {
67
        //introduce logging method here to keep track of process
68
        // can be overwritten in extended class to log with custom preocess logger
69
        if ($this->verbose) {
70
            Log::error($msg);
71
        }
72
    }
73
74
    public function CleanUpTmpDirs()
75
    {
76
        $now = time();
77
        $isExpired = ($now - (60 * 240));
78
        $disk = \Storage::disk($this->storageDisk);
79
        $all_dirs = $disk->directories($this->storagePathPrefix.'DocxMustache');
80
        foreach ($all_dirs as $dir) {
81
            //delete dirs older than 20min
82
            if ($disk->lastModified($dir) < $isExpired) {
83
                $disk->deleteDirectory($dir);
84
            }
85
        }
86
    }
87
88
    public function GetTmpDir()
89
    {
90
        $this->CleanUpTmpDirs();
91
        $path = $this->storagePathPrefix.'DocxMustache/'.uniqid($this->template_file).'/';
92
        \File::makeDirectory($this->StoragePath($path), 0775, true);
93
94
        return $path;
95
    }
96
97
    public function CopyTmplate()
98
    {
99
        $this->Log('Get Copy of Template');
100
        $this->local_path = $this->GetTmpDir();
101
        \Storage::disk($this->storageDisk)->copy($this->storagePathPrefix.$this->template_file, $this->local_path.$this->template_file_name);
102
    }
103
104
    protected function exctractOpenXmlFile($file)
105
    {
106
        $this->zipper
107
            ->make($this->StoragePath($this->local_path.$this->template_file_name))
108
            ->extractTo($this->StoragePath($this->local_path), [$file], \Wrklst\Zipper\Zipper::WHITELIST);
109
    }
110
111
    protected function ReadOpenXmlFile($file, $type = 'file')
112
    {
113
        if ($type == 'file') {
114
            if ($file_contents = \Storage::disk($this->storageDisk)->get($this->local_path.$file)) {
115
                return $file_contents;
116
            } else {
117
                throw new Exception('Cannot not read file '.$file);
118
            }
119
        } else {
120
            if ($xml_object = simplexml_load_file($this->StoragePath($this->local_path.$file))) {
121
                return $xml_object;
122
            } else {
123
                throw new Exception('Cannot load XML Object from file '.$file);
124
            }
125
        }
126
    }
127
128
    protected function SaveOpenXmlFile($file, $folder, $content)
129
    {
130
        \Storage::disk($this->storageDisk)
131
            ->put($this->local_path.$file, $content);
132
        //add new content to word doc
133
        if ($folder) {
134
            $this->zipper->folder($folder)
135
                ->add($this->StoragePath($this->local_path.$file));
136
        } else {
137
            $this->zipper
138
                ->add($this->StoragePath($this->local_path.$file));
139
        }
140
    }
141
142
    protected function SaveOpenXmlObjectToFile($xmlObject, $file, $folder)
143
    {
144
        if ($xmlString = $xmlObject->asXML()) {
145
            $this->SaveOpenXmlFile($file, $folder, $xmlString);
146
        } else {
147
            throw new Exception('Cannot generate xml for '.$file);
148
        }
149
    }
150
151
    public function ReadTeamplate($dpi)
152
    {
153
        $this->Log('Analyze Template');
154
155
        $this->relevant_files = [];
0 ignored issues
show
Bug introduced by
The property relevant_files does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
156
        //get every File in docx-Archive
157
        $this->zipper
158
            ->getRepository()->each([$this, 'retrieveFilesList']);
159
        foreach ($this->relevant_files as $file) {
160
            $this->SubstituteOpenXmlFile($file, $dpi);
161
        }
162
163
        $this->zipper->close();
164
    }
165
166
    public function retrieveFilesList($file, $stats)
0 ignored issues
show
Unused Code introduced by
The parameter $stats is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
167
    {
168
        $this->exctractOpenXmlFile($file);
169
        if (substr($file, -3) === 'xml' && substr($file, 0, 4) === 'word') {
170
            $this->relevant_files[] = $file;
171
        }
172
    }
173
174
    public function SubstituteOpenXmlFile($file, $dpi)
175
    {
176
        $this->word_doc = $this->ReadOpenXmlFile($file, 'file');
177
        // $this->Log('Merge Data into Template');
178
        $this->word_doc = MustacheRender::render($this->items, $this->word_doc);
179
180
        $this->word_doc = HtmlConversion::convert($this->word_doc);
181
        if ($file == 'word/document.xml') {
182
            $this->ImageReplacer($dpi);
183
        }
184
        $this->SaveOpenXmlFile($file, 'word', $this->word_doc);
185
    }
186
187
    protected function AddContentType($imageCt = 'jpeg')
188
    {
189
        $ct_file = $this->ReadOpenXmlFile('[Content_Types].xml', 'object');
190
191
        if (! ($ct_file instanceof \Traversable)) {
192
            throw new Exception('Cannot traverse through [Content_Types].xml.');
193
        }
194
195
        //check if content type for jpg has been set
196
        $i = 0;
197
        $ct_already_set = false;
198
        foreach ($ct_file as $ct) {
199
            if ((string) $ct_file->Default[$i]['Extension'] == $imageCt) {
0 ignored issues
show
Bug introduced by
Accessing Default on the interface Traversable suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
200
                $ct_already_set = true;
201
            }
202
            $i++;
203
        }
204
205
        //if content type for jpg has not been set, add it to xml
206
        // and save xml to file and add it to the archive
207
        if (! $ct_already_set) {
208
            $sxe = $ct_file->addChild('Default');
209
            $sxe->addAttribute('Extension', $imageCt);
210
            $sxe->addAttribute('ContentType', 'image/'.$imageCt);
211
            $this->SaveOpenXmlObjectToFile($ct_file, '[Content_Types].xml', false);
212
        }
213
    }
214
215
    protected function FetchReplaceableImages(&$main_file, $ns)
216
    {
217
        //set up basic arrays to keep track of imgs
218
        $imgs = [];
219
        $imgs_replaced = []; // so they can later be removed from media and relation file.
220
        $newIdCounter = 1;
221
222
        //iterate through all drawing containers of the xml document
223
        foreach ($main_file->xpath('//w:drawing') as $k=>$drawing) {
224
            //figure out if there is a URL saved in the description field of the img
225
            $img_url = $this->AnalyseImgUrlString($drawing->children($ns['wp'])->xpath('wp:docPr')[0]->attributes()['descr']);
226
            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->xpath('wp:docPr')[0]->attributes()['descr'] = $img_url['rest'];
227
228
            //if there is a url, save this img as a img to be replaced
229
            if ($img_url['valid']) {
230
                $ueid = 'wrklstId'.$newIdCounter;
231
                $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'];
232
233
                //get dimensions
234
                $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'];
235
                $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'];
236
237
                //remember img as being replaced
238
                $imgs_replaced[$wasId] = $wasId;
239
240
                //set new img id
241
                $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;
242
243
                $imgs[] = [
244
                    'cx'     => (int) $cx,
245
                    'cy'     => (int) $cy,
246
                    'wasId'  => $wasId,
247
                    'id'     => $ueid,
248
                    'url'    => $img_url['url'],
249
                    'path'    => $img_url['path'],
250
                    'mode'    => $img_url['mode'],
251
                ];
252
253
                $newIdCounter++;
254
            }
255
        }
256
257
        return [
258
            'imgs'          => $imgs,
259
            'imgs_replaced' => $imgs_replaced,
260
        ];
261
    }
262
263
    protected function RemoveReplaceImages($imgs_replaced, &$rels_file)
264
    {
265
        //TODO: check if the same img is used at a different position int he file as well, as otherwise broken images are produced.
266
        //iterate through replaced images and clean rels files from them
267
        foreach ($imgs_replaced as $img_replaced) {
268
            $i = 0;
269
            foreach ($rels_file as $rel) {
270
                if ((string) $rel->attributes()['Id'] == $img_replaced) {
271
                    $this->zipper->remove('word/'.(string) $rel->attributes()['Target']);
272
                    unset($rels_file->Relationship[$i]);
273
                }
274
                $i++;
275
            }
276
        }
277
    }
278
279
    protected function InsertImages($ns, &$imgs, &$rels_file, &$main_file, $dpi)
280
    {
281
        $docimage = new DocImage();
282
        $allowed_imgs = $docimage->AllowedContentTypeImages();
283
        $image_i = 1;
284
        //iterate through replacable images
285
        foreach ($imgs as $k=>$img) {
286
            $this->Log('Merge Images into Template - '.round($image_i / count($imgs) * 100).'%');
287
            //get file type of img and test it against supported imgs
288
            if ($imgageData = $docimage->GetImageFromUrl($img['mode'] == 'url' ? $img['url'] : $img['path'], $img['mode'] == 'url' ? $this->imageManipulation : '')) {
289
                $imgs[$k]['img_file_src'] = str_replace('wrklstId', 'wrklst_image', $img['id']).$allowed_imgs[$imgageData['mime']];
290
                $imgs[$k]['img_file_dest'] = str_replace('wrklstId', 'wrklst_image', $img['id']).'.jpeg';
291
292
                $resampled_img = $docimage->ResampleImage($this, $imgs, $k, $imgageData['data'], $dpi);
293
294
                $sxe = $rels_file->addChild('Relationship');
295
                $sxe->addAttribute('Id', $img['id']);
296
                $sxe->addAttribute('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image');
297
                $sxe->addAttribute('Target', 'media/'.$imgs[$k]['img_file_dest']);
298
299
                foreach ($main_file->xpath('//w:drawing') as $k=>$drawing) {
300
                    if (null !== $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
301
                        ->graphic->graphicData->children($ns['pic'])->pic->blipFill &&
302
                        $img['id'] == $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
303
                        ->graphic->graphicData->children($ns['pic'])->pic->blipFill->children($ns['a'])
304
                        ->blip->attributes($ns['r'])['embed']) {
305
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
306
                            ->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])
307
                            ->xfrm->ext->attributes()['cx'] = $resampled_img['width_emus'];
308
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
309
                            ->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])
310
                            ->xfrm->ext->attributes()['cy'] = $resampled_img['height_emus'];
311
                        //anchor images
312
                        if (isset($main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->anchor)) {
313
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->anchor->extent->attributes()['cx'] = $resampled_img['width_emus'];
314
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->anchor->extent->attributes()['cy'] = $resampled_img['height_emus'];
315
                        }
316
                        //inline images
317
                        elseif (isset($main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline)) {
318
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline->extent->attributes()['cx'] = $resampled_img['width_emus'];
319
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline->extent->attributes()['cy'] = $resampled_img['height_emus'];
320
                        }
321
322
                        break;
323
                    }
324
                }
325
            }
326
            $image_i++;
327
        }
328
    }
329
330
    protected function ImageReplacer($dpi)
331
    {
332
        $this->Log('Load XML Document to Merge Images');
333
334
        //load main doc xml
335
        libxml_use_internal_errors(true);
336
        $main_file = simplexml_load_string($this->word_doc);
337
338
        if (gettype($main_file) == 'object') {
339
            $this->Log('Merge Images into Template');
340
341
            //get all namespaces of the document
342
            $ns = $main_file->getNamespaces(true);
343
344
            $replaceableImage = $this->FetchReplaceableImages($main_file, $ns);
345
            $imgs = $replaceableImage['imgs'];
346
            $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...
347
348
            $rels_file = $this->ReadOpenXmlFile('word/_rels/document.xml.rels', 'object');
349
350
            //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.
351
            //$this->RemoveReplaceImages($imgs_replaced, $rels_file);
352
353
            //add jpg content type if not set
354
            $this->AddContentType('jpeg');
355
356
            $this->InsertImages($ns, $imgs, $rels_file, $main_file, $dpi);
357
358
            $this->SaveOpenXmlObjectToFile($rels_file, 'word/_rels/document.xml.rels', 'word/_rels');
359
360
            if ($main_file_xml = $main_file->asXML()) {
361
                $this->word_doc = $main_file_xml;
362
            } else {
363
                throw new Exception('Cannot generate xml for word/document.xml.');
364
            }
365
        } else {
366
            $xmlerror = '';
367
            $errors = libxml_get_errors();
368
            foreach ($errors as $error) {
369
                // handle errors here
370
                $xmlerror .= $this->display_xml_error($error, explode("\n", $this->word_doc));
371
            }
372
            libxml_clear_errors();
373
            $this->Log('Error: Could not load XML file. '.$xmlerror);
374
            libxml_clear_errors();
375
        }
376
    }
377
378
    /*
379
    example for extracting xml errors from
380
    http://php.net/manual/en/function.libxml-get-errors.php
381
    */
382
    protected function display_xml_error($error, $xml)
383
    {
384
        $return = $xml[$error->line - 1]."\n";
385
        $return .= str_repeat('-', $error->column)."^\n";
386
387
        switch ($error->level) {
388
            case LIBXML_ERR_WARNING:
389
                $return .= "Warning $error->code: ";
390
                break;
391
                case LIBXML_ERR_ERROR:
392
                $return .= "Error $error->code: ";
393
                break;
394
            case LIBXML_ERR_FATAL:
395
                $return .= "Fatal Error $error->code: ";
396
                break;
397
        }
398
399
        $return .= trim($error->message).
400
                    "\n  Line: $error->line".
401
                    "\n  Column: $error->column";
402
403
        if ($error->file) {
404
            $return .= "\n  File: $error->file";
405
        }
406
407
        return "$return\n\n--------------------------------------------\n\n";
408
    }
409
410
    /**
411
     * @param string $string
412
     */
413
    protected function AnalyseImgUrlString($string)
414
    {
415
        $string = (string) $string;
416
        $start = '[IMG-REPLACE]';
417
        $end = '[/IMG-REPLACE]';
418
        $start_local = '[LOCAL_IMG_REPLACE]';
419
        $end_local = '[/LOCAL_IMG_REPLACE]';
420
        $valid = false;
421
        $url = '';
422
        $path = '';
423
424
        if ($string != str_replace($start, '', $string) && $string == str_replace($start.$end, '', $string)) {
425
            $string = ' '.$string;
426
            $ini = strpos($string, $start);
427 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...
428
                $url = '';
429
                $rest = $string;
430
            } else {
431
                $ini += strlen($start);
432
                $len = ((strpos($string, $end, $ini)) - $ini);
433
                $url = substr($string, $ini, $len);
434
435
                $ini = strpos($string, $start);
436
                $len = strpos($string, $end, $ini + strlen($start)) + strlen($end);
437
                $rest = substr($string, 0, $ini).substr($string, $len);
438
            }
439
440
            $valid = true;
441
442
            //TODO: create a better url validity check
443
            if (! trim(str_replace(['http', 'https', ':', ' '], '', $url)) || $url == str_replace('http', '', $url)) {
444
                $valid = false;
445
            }
446
            $mode = 'url';
447
        } elseif ($string != str_replace($start_local, '', $string) && $string == str_replace($start_local.$end_local, '', $string)) {
448
            $string = ' '.$string;
449
            $ini = strpos($string, $start_local);
450 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...
451
                $path = '';
452
                $rest = $string;
453
            } else {
454
                $ini += strlen($start_local);
455
                $len = ((strpos($string, $end_local, $ini)) - $ini);
456
                $path = str_replace('..', '', substr($string, $ini, $len));
457
458
                $ini = strpos($string, $start_local);
459
                $len = strpos($string, $end_local, $ini + strlen($start)) + strlen($end_local);
460
                $rest = substr($string, 0, $ini).substr($string, $len);
461
            }
462
463
            $valid = true;
464
465
            //check if path starts with storage path
466
            if (! starts_with($path, storage_path())) {
467
                $valid = false;
468
            }
469
            $mode = 'path';
470
        } else {
471
            $mode = 'nothing';
472
            $url = '';
473
            $path = '';
474
            $rest = str_replace([$start, $end, $start_local, $end_local], '', $string);
475
        }
476
477
        return [
478
            'mode' => $mode,
479
            'url'  => trim($url),
480
            'path' => trim($path),
481
            'rest' => trim($rest),
482
            'valid' => $valid,
483
        ];
484
    }
485
486
    public function SaveAsPdf()
487
    {
488
        $this->Log('Converting DOCX to PDF');
489
        //convert to pdf with libre office
490
        $process = new \Symfony\Component\Process\Process([
491
            'soffice',
492
            '--headless',
493
            '--convert-to',
494
            'pdf',
495
            $this->StoragePath($this->local_path.$this->template_file_name),
496
            '--outdir',
497
            $this->StoragePath($this->local_path),
498
        ]);
499
        $process->start();
500
        while ($process->isRunning()) {
501
            //wait until process is ready
502
        }
503
        // executes after the command finishes
504
        if (! $process->isSuccessful()) {
505
            throw new \Symfony\Component\Process\Exception\ProcessFailedException($process);
506
        } else {
507
            $path_parts = pathinfo($this->StoragePath($this->local_path.$this->template_file_name));
508
509
            return $this->StoragePath($this->local_path.$path_parts['filename'].'.pdf');
510
        }
511
    }
512
}
513