NativeRenderer::renderUrl()   F
last analyzed

Complexity

Conditions 22
Paths 128

Size

Total Lines 66
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 42
c 1
b 1
f 0
dl 0
loc 66
rs 3.9333
cc 22
nc 128
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Mfonte\Sitemap\Renderer;
4
5
use \DateTime;
0 ignored issues
show
Bug introduced by
The type \DateTime was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
6
7
use Mfonte\Sitemap\Tags\Sitemap;
8
use Mfonte\Sitemap\Tags\Url;
9
10
class NativeRenderer
11
{
12
    /**
13
     * Params Array. Should be injected into current instance with a compact() call, for example: compact('tags', 'hasImages', 'hasNews')
14
     *
15
     * @var array
16
     */
17
    private array $params;
18
    private string $tempFile;
19
20
    public static function instance(array $params) : self
21
    {
22
        return new self($params);
23
    }
24
25
    public function __construct(array $params)
26
    {
27
        $this->params = $params;
28
        $this->tempFile = tempnam(sys_get_temp_dir(), 'mfonte_sitemap_nativerenderer_' . sha1(uniqid()));
29
    }
30
31
    /**
32
     * Renders the sitemap or sitemap index
33
     *
34
     * @param string $type - sitemap or sitemapIndex
35
     *
36
     * @return string
37
     */
38
    public function render(string $type) : string
39
    {
40
        try {
41
            switch($type) {
42
                case 'sitemap':
43
                    $this->renderSitemap();
44
45
                    break;
46
                case 'sitemapIndex':
47
                    $this->renderSitemapIndex();
48
49
                    break;
50
                default:
51
                    throw new \Exception('Invalid Render Type', 999);
52
            }
53
        } catch(\Exception $e) {
54
            if ($e->getCode() === 999) {
55
                throw new \Exception('The render type must be "sitemap" or "sitemapIndex"');
56
            }
57
58
            throw new \Exception('Error while rendering the xml: ' . $e->getMessage());
59
        }
60
        
61
        // if the tidy extension is not available, return the xml as it was rendered natively.
62
        if (! function_exists('tidy_parse_file')) {
63
            return $this->asString();
64
        }
65
66
        // if the tidy extension is available, format the xml with tidy
67
        $tidyInstance = tidy_parse_file($this->tempFile, [
68
            'indent'         => true,
69
            'output-xml'     => true,
70
            'input-xml'      => true,
71
            'wrap'           => 0,
72
            'indent-spaces'  => 2,
73
            'newline'        => 'LF',
74
        ]);
75
76
        if ($tidyInstance === false) {
77
            throw new \Exception('Tidy: Error while loading the Sitemap xml with tidy_parse_file()');
78
        }
79
80
        if ($tidyInstance->errorBuffer) {
81
            throw new \Exception('Tidy: Errors while loading the Sitemap xml with tidy_parse_file(): ' . "\n" . $tidyInstance->errorBuffer);
82
        }
83
84
        $formatted = tidy_clean_repair($tidyInstance);
85
        if ($formatted === false) {
86
            throw new \Exception('Tidy: Error while cleaning the Sitemap xml');
87
        }
88
89
        // save the formatted xml back to the temporary file
90
        file_put_contents($this->tempFile, (string) $tidyInstance);
91
92
        return $this->asString();
93
    }
94
95
    /**
96
     * Renders the sitemap index
97
     */
98
    private function renderSitemapIndex()
99
    {
100
        $this->append('<?xml version="1.0" encoding="UTF-8"?>');
101
        $this->append('<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">');
102
103
        foreach ($this->params['tags'] as $tag) {
104
            /** @var Sitemap $tag */
105
            
106
            $this->append('<sitemap>', 1);
107
            if (! empty($tag->url)) {
108
                $this->append('<loc>' . $this->format(url($tag->url)) . '</loc>', 2);
109
            }
110
111
            if (! empty($tag->lastModificationDate)) {
112
                $this->append('<lastmod>' . $tag->lastModificationDate->format(DateTime::ATOM) . '</lastmod>', 2);
113
            }
114
115
            $this->append('</sitemap>', 1);
116
        }
117
118
        $this->append('</sitemapindex>');
119
    }
120
121
    /**
122
     * Renders the sitemap
123
     */
124
    private function renderSitemap()
125
    {
126
        $this->append('<?xml version="1.0" encoding="UTF-8"?>');
127
        $this->append('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"', 0, false);
128
        if ($this->params['hasImages']) {
129
            $this->append(' xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"', 0, false);
130
        }
131
        if ($this->params['hasNews']) {
132
            $this->append(' xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"', 0, false);
133
        }
134
        $this->append('>', 0);
135
136
        foreach ($this->params['tags'] as $tag) {
137
            $this->renderUrl($tag);
138
        }
139
140
        $this->append('</urlset>', 0, false);
141
    }
142
143
    /**
144
     * Renders a Url tag
145
     *
146
     * @param Url $tag
147
     */
148
    private function renderUrl(Url $tag)
149
    {
150
        $this->append('<url>', 1);
151
        if (! empty($tag->url)) {
152
            $this->append('<loc>' . $this->format(url($tag->url)) . '</loc>', 2);
153
        }
154
        if (count($tag->alternates)) {
155
            foreach ($tag->alternates as $alternate) {
156
                $this->append('<xhtml:link rel="alternate" hreflang="' . $this->format($alternate->locale) . '" href="' . $this->format(url($alternate->url)) . '" />', 2);
157
            }
158
        }
159
        if (! empty($tag->lastModificationDate)) {
160
            $this->append('<lastmod>' . $tag->lastModificationDate->format(DateTime::ATOM) . '</lastmod>', 2);
161
        }
162
        if (! empty($tag->changeFrequency)) {
163
            $this->append('<changefreq>' . $this->format($tag->changeFrequency) . '</changefreq>', 2);
164
        }
165
        if (! empty($tag->priority)) {
166
            $this->append('<priority>' . number_format($tag->priority, 1) . '</priority>', 2);
167
        }
168
        if (count($tag->images)) {
169
            foreach ($tag->images as $image) {
170
                if (! empty($image->url)) {
171
                    $this->append('<image:image>', 2);
172
                    $this->append('<image:loc>' . url($image->url) . '</image:loc>', 3);
173
                    if (! empty($image->caption)) {
174
                        $this->append('<image:caption>' . $this->format($image->caption) . '</image:caption>', 3);
175
                    }
176
                    if (! empty($image->geo_location)) {
177
                        $this->append('<image:geo_location>' . $this->format($image->geo_location) . '</image:geo_location>', 3);
178
                    }
179
                    if (! empty($image->title)) {
180
                        $this->append('<image:title>' . $this->format($image->title) . '</image:title>', 3);
181
                    }
182
                    if (! empty($image->license)) {
183
                        $this->append('<image:license>' . $this->format($image->license) . '</image:license>', 3);
184
                    }
185
                    $this->append('</image:image>', 2);
186
                }
187
            }
188
        }
189
        if (count($tag->news)) {
190
            foreach ($tag->news as $new) {
191
                $this->append('<news:news>', 2);
192
                if (! empty($new->publication_date)) {
193
                    $this->append('<news:publication_date>' . $new->publication_date->format('Y-m-d') . '</news:publication_date>', 3);
194
                }
195
                if (! empty($new->title)) {
196
                    $this->append('<news:title>' . $this->format($new->title) . '</news:title>', 3);
197
                }
198
                if (! empty($new->name) || ! empty($new->language)) {
199
                    $this->append('<news:publication>', 3);
200
                    if (! empty($new->name)) {
201
                        $this->append('<news:name>' . $this->format($new->name) . '</news:name>', 4);
202
                    }
203
204
                    if (! empty($new->language)) {
205
                        $this->append('<news:language>' . $this->format($new->language) . '</news:language>', 4);
206
                    }
207
                    $this->append('</news:publication>', 3);
208
                }
209
                $this->append('</news:news>', 2);
210
            }
211
        }
212
213
        $this->append('</url>', 1);
214
    }
215
216
    /**
217
     * Returns the contents of the temporary file as a string
218
     *
219
     * @return string
220
     */
221
    private function asString() : string
222
    {
223
        if (!is_file($this->tempFile)) {
224
            throw new \Exception('The generated Sitemap temporary file does not exist');
225
        }
226
227
        if (!is_readable($this->tempFile)) {
228
            throw new \Exception('The generated Sitemap temporary file is not readable');
229
        }
230
231
        $contents = file_get_contents($this->tempFile);
232
        unlink($this->tempFile);
233
234
        if ($contents === false) {
235
            throw new \Exception('Error while reading the generated Sitemap temporary file');
236
        }
237
        if (empty($contents)) {
238
            throw new \Exception('The generated Sitemap temporary file is empty');
239
        }
240
241
        return $contents;
242
    }
243
244
    /**
245
     * Appends content to the temporary file
246
     *
247
     * @param string $content
248
     * @param string $indentLevel
249
     * @param string $newline
250
     */
251
    private function append(string $content, int $indentLevel = 0, bool $newline = true)
252
    {
253
        if (!is_file($this->tempFile)) {
254
            @touch($this->tempFile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for touch(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

254
            /** @scrutinizer ignore-unhandled */ @touch($this->tempFile);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
255
        }
256
257
        if (!is_file($this->tempFile)) {
258
            throw new \Exception('The temporary file does not exist');
259
        }
260
261
        if (!is_writable($this->tempFile)) {
262
            throw new \Exception('The temporary file is not writable');
263
        }
264
265
        $content = ($indentLevel) ? str_repeat(' ', $indentLevel * 2) . $content : $content;
266
        $content = ($newline) ? $content . "\n" : $content;
267
        $result = file_put_contents($this->tempFile, $content, FILE_APPEND);
268
269
        if ($result === false) {
270
            throw new \Exception('Error while writing to the temporary file');
271
        }
272
    }
273
274
    /**
275
     * Formats a tag text so that it does not contain invalid characters for the XML format.
276
     *
277
     * @param string|null $text
278
     *
279
     * @return string
280
     */
281
    private function format(?string $text = null) : string
282
    {
283
        $text = html_entity_decode($text ?? '', ENT_QUOTES | ENT_IGNORE, 'UTF-8');
284
285
        // remove any occurrence of UTF-8 encoding of a NO-BREAK SPACE codepoint, that we have decoded above
286
        $text = str_replace(chr(194).chr(160), ' ', $text);
287
        $text = trim(preg_replace('/\s\s+/', ' ', $text));
288
289
        return trim(htmlspecialchars($text, ENT_QUOTES | ENT_IGNORE, 'UTF-8'));
290
    }
291
}
292