1 | <?php |
||||
2 | |||||
3 | namespace Mfonte\Sitemap\Renderer; |
||||
4 | |||||
5 | use \DateTime; |
||||
0 ignored issues
–
show
|
|||||
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
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
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.');
}
![]() |
|||||
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 |
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:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths