30 |
31 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SVG Support for Joomla! 4
2 |
3 | A simple plugin which adds SVG support to Joomla 4.
4 |
5 | [Download](https://github.com/nikosdion/joomlasvg/releases)
6 |
7 | ## Description
8 |
9 | This is a simple plugin which adds SVG as a valid image type in Joomla's MediaHelper. This allows you to preview SVG images in the Media Manager and select SVG files in media fields.
10 |
11 | Features:
12 |
13 | * Preview SVGs in the Media Manager.
14 | * Select SVGs anywhere you can choose an image, as long as they use Joomla's Media field. This includes article Fields. If that doesn't work for you check if this is overridden by something else, e.g. JCE.
15 |
16 | Requirements:
17 |
18 | * Joomla 4.0 or later
19 | * PHP 7.2 or later
20 | * Go to your site's backend, Content, Media, Options and add `svg` to the “Allowed Extensions” and “Legal Image Extensions (File Types)” fields.
21 |
22 | ## FAQ
23 |
24 | ### What are the minimum requirements?
25 |
26 | PHP 7.2. Joomla 4.0.
27 |
28 | ### What are SVGs again?
29 |
30 | SVG stands for Scalable Vector Graphics. It's an XML-based file format for vector images. They can be scaled to any size without an adverse effect in quality.
31 |
32 | ### Why do I need this plugin?
33 |
34 | You don't **need** this plugin – unless you want to use SVGs efficiently in Joomla 4 without spending a lot of money and / or effort.
35 |
36 | Joomla 4's Media Manager can only be configured to allow uploading of SVG files. It cannot show a preview of them and you cannot select them in an image picker. You can only use them in articles, if you manually enter the URL to them. This plugin addresses these shortcomings.
37 |
38 | ### Aren't SVGs insecure?
39 |
40 | Somewhat, less than they used to, but Joomla does check if they are safe when they are being uploaded.
41 |
42 | SVGs allow for JavaScript inside them to support things like animation and interactivity. The problem is that JavaScript can also be used for nefarious purposes. That's why we don't allow all but extremely trusted people to upload JavaScript to our site. This is what made SVGs unsafe.
43 |
44 | Around 2017 all major browsers implemented a simple security feature. If an SVG file is included in an `` tag they will refuse to execute any scripts. That makes SVGs relatively safe to use. However, it's conceivable that a malicious SVG is uploaded and a Super User is tricked into opening it directly on their browser, executing JavaScript.
45 |
46 | Joomla simply checks if there are unsafe SVG features being used and refuses the upload.
47 |
48 | A better way would be strict sanitization but Joomla chose not to do that. If an SVG upload fails it's Joomla's problem, not this plugin's problem. We know how to do it right but Joomla doesn't. Simple as that.
49 |
50 | ### How does it work?
51 |
52 | Go to your site's backend, Content, Media, Options and add `svg` to the “Allowed Extensions” and “Legal Image Extensions (File Types)” fields.
53 |
54 | Install the package. Go to Extensions, Plugins. Enable the "System - SVG Support for Joomla!" plugin. Now you can use SVGs.
55 |
56 | ### No, really, HOW does it work – on a technical level?
57 |
58 | Adding SVG support to Joomla requires three things:
59 |
60 | * Modifying MediaModelList. There list of image extensions is hardcoded in the PHP code. Moreover, there is no error handling when retrieving the image size and type which also needs to be patched.
61 | * Modifying Joomla\CMS\Helper\MediaHelper. This tells Joomla if a file is an image and the list of file extensions is hardcoded in the PHP code as well. It's also different than the one in MediaModelList because why not? Joomla isn't known for internal consistency. Moreover, we need to patch the canUpload method to sanitize the SVGs on upload, removing all kinds of JavaScript and other nastiness that can result in a security vulnerability. We use the same library as the WordPress Safe SVG plugin.
62 | * Modifying the Media Manager options. This is to allow Joomla to accept SVG files as acceptable file types.
63 |
64 | Since modifying the code code directly is a bad idea (core hacks make it impossible to update your site) the approach I used is in-memory patching. The raw PHP code is loaded in memory and patched. The resulting file is written to an in-memory buffer through PHP streams and then loaded from there. Since the patched class is in memory Joomla won't try to reload it from disk. Therefore it is using our patched code.
65 |
66 | ### Isn't this a core hack?
67 |
68 | Technically? Yes. It modifies core code to achieve its goal. There is no other way because of all the things that are hardcoded in Joomla and date back to Joomla 1.5 or even 1.0.
69 |
70 | Practically? No. You can still update Joomla without fearing that you will lose SVG support. That's why this plugin is doing in-memory patching instead of modifying core files on disk.
71 |
72 | To make the point clearer. If you disable the plugin the Joomla core code is no longer modified. If you enable (publish) the plugin the Joomla core code is modified, but ONLY in memory. That's a super safe way to do it.
73 |
74 | ### What you're doing is insane!
75 |
76 | I find pleasure in doing things that are, um, “unique” in their execution. More so when I'm told it can't be done. If I get to help people doing what I enjoy most, all the better!
77 |
78 | ### This must have taken forever to write
79 |
80 | About two hours. Oh, you were talking about the code, not the README! Sorry, the code only took me just over an hour.
81 |
82 | ### Why is this not in the Joomla Extensions Directory (JED)?
83 |
84 | The JED does not allow extensions which modify core code. Granted, this rule was written with permanent file modifications in mind but the way it's worded would also apply on this plugin as well.
85 |
86 | Even if the wording wasn't the way it is, I still believe that the JED should reject this plugin because it sets a bad example for other third party developers who don't have my experience with core code and haven't found the once-in-a-decade use case where modifying the core code is necessary AND the modification will not even be considered by the Joomla project for the remaining lifetime of the Joomla 3 series (more on that later).
87 |
88 | To put things in perspective, this is the ONLY thing I found in 15 years that absolutely required patching core files to implement. It's also a case of we could have this change in the core but people don't want to take responsibility.
89 |
90 | ### I installed the plugin and it broke my site!
91 |
92 | Delete the folder `plugins/system/joomlasvg/services`. Go to your site's backend and uninstall this plugin.
93 |
94 | ### I uninstalled the plugin and my site is still broken!
95 |
96 | Obviously, your problem is not this plugin since it makes ZERO permanent changes to your site whatsoever.
97 |
98 | ### I installed this plugin and I still can't select SVG images!
99 |
100 | Have you followed the instructions about the Media component's Options? If not, do it now.
101 |
102 | If you still have a problem, you probably have JCE Editor Pro installed. Go to its options and set "JCE File Browser in Image Fields" to No. When that option is enabled JCE overrides Joomla's Media Manager with its own.
103 |
104 | If you'd rather use JCE's media manager instead please do NOT use this plugin here; you don't need it.
--------------------------------------------------------------------------------
/build/build.properties:
--------------------------------------------------------------------------------
1 | ;; =============================================================================
2 | ;; Non-privileged Phing properties for this project
3 | ;; =============================================================================
4 |
5 | ; ------------------------------------------------------------------------------
6 | ; Package building
7 | ; ------------------------------------------------------------------------------
8 | ; The name of the component, must be in the form something, NOT com_something!
9 | build.component=usertype
10 | ; Do not include FOF 3 in the package
11 | build.fof=0
12 | ; Do not include Akeeba Strapper
13 | build.strapper=0
14 | ; Should I include a Self Check .php manifest in each component package?
15 | build.selfcheck=0
16 | ; Do you have a Core / Pro version? If this is 0 only the Core release will be built
17 | build.has_pro=0
18 | ; Do you have CLI script? If yes, a file_example package will be built for you
19 | build.has_cli=0
20 |
21 | ; GitHub Releases setup
22 | ; ------------------------------------------------------------------------------
23 | release.method=github
24 | github.organization=nikosdion
25 | github.repository=joomlasvg
26 | github.release.file=plg_system_joomlasvg*.zip
--------------------------------------------------------------------------------
/build/build.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/build/templates/link.php:
--------------------------------------------------------------------------------
1 | =7.2.0",
27 | "ext-json": "*",
28 | "ext-fileinfo": "*"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/composer.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_readme": [
3 | "This file locks the dependencies of your project to a known state",
4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 | "This file is @generated automatically"
6 | ],
7 | "content-hash": "ec6ce82d72358fd3f9345b36bba8b9d8",
8 | "packages": [],
9 | "packages-dev": [],
10 | "aliases": [],
11 | "minimum-stability": "stable",
12 | "stability-flags": [],
13 | "prefer-stable": false,
14 | "prefer-lowest": false,
15 | "platform": {
16 | "php": ">=7.2.0",
17 | "ext-json": "*",
18 | "ext-fileinfo": "*"
19 | },
20 | "platform-dev": [],
21 | "platform-overrides": {
22 | "php": "7.2.0"
23 | },
24 | "plugin-api-version": "2.3.0"
25 | }
26 |
--------------------------------------------------------------------------------
/plugins/system/joomlasvg/.htaccess:
--------------------------------------------------------------------------------
1 |
2 | Order deny,allow
3 | Deny from all
4 |
5 |
6 |
7 | Require all denied
8 |
9 |
10 |
--------------------------------------------------------------------------------
/plugins/system/joomlasvg/installscript.php:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | PLG_SYSTEM_JOOMLASVG
10 | 2.0.1
11 | 2022-11-29
12 |
13 | Nicholas K. Dionysopoulos
14 | no-reply@dionysopoulos.me
15 | https://www.dionysopoulos.me
16 |
17 | Copyright (c)2020-2023 Nicholas K. Dionysopoulos
18 | GNU GPL v3 or later
19 |
20 | PLG_SYSTEM_JOOMLASVG_XML_DESC
21 | Joomla\Plugin\System\Joomlasvg
22 |
23 |
24 | src
25 | services
26 |
27 | .htaccess
28 | web.config
29 |
30 |
31 |
32 | en-GB/en-GB.plg_system_joomlasvg.ini
33 | en-GB/en-GB.plg_system_joomlasvg.sys.ini
34 |
35 |
36 | installscript.php
37 |
38 |
39 | https://raw.githubusercontent.com/nikosdion/joomlasvg/main/update/joomlasvg.xml
40 |
41 |
42 |
--------------------------------------------------------------------------------
/plugins/system/joomlasvg/language/en-GB/en-GB.plg_system_joomlasvg.ini:
--------------------------------------------------------------------------------
1 | ;; @package JoomlaSVGSupport
2 | ;; @copyright Copyright (c)2020-2023 Nicholas K. Dionysopoulos
3 | ;; @license GNU General Public License version 3, or later
4 |
--------------------------------------------------------------------------------
/plugins/system/joomlasvg/language/en-GB/en-GB.plg_system_joomlasvg.sys.ini:
--------------------------------------------------------------------------------
1 | ;; @package JoomlaSVGSupport
2 | ;; @copyright Copyright (c)2020-2023 Nicholas K. Dionysopoulos
3 | ;; @license GNU General Public License version 3, or later
4 |
5 | PLG_SYSTEM_JOOMLASVG="System - SVG Support for Joomla!"
6 | PLG_SYSTEM_JOOMLASVG_XML_DESC="Adds SVG support to Joomla 4."
--------------------------------------------------------------------------------
/plugins/system/joomlasvg/services/provider.php:
--------------------------------------------------------------------------------
1 | set(
36 | PluginInterface::class,
37 | function (Container $container) {
38 | $plugin = PluginHelper::getPlugin('system', 'joomlasvg');
39 | $dispatcher = $container->get(DispatcherInterface::class);
40 |
41 | return new JoomlaSVG(
42 | $dispatcher,
43 | (array) $plugin
44 | );
45 | }
46 | );
47 | }
48 | };
49 |
--------------------------------------------------------------------------------
/plugins/system/joomlasvg/src/Buffer.php:
--------------------------------------------------------------------------------
1 | name = $url['host'] . ($url['path'] ?? '');
141 | $this->position = 0;
142 |
143 | if (!isset(static::$buffers[$this->name]))
144 | {
145 | static::$buffers[$this->name] = null;
146 | }
147 |
148 | return true;
149 | }
150 |
151 | public function stream_set_option($option, $arg1 = null, $arg2 = null)
152 | {
153 | return false;
154 | }
155 |
156 | public function unlink($path)
157 | {
158 | $url = parse_url($path);
159 | $name = $url['host'];
160 |
161 | if (isset(static::$buffers[$name]))
162 | {
163 | unset (static::$buffers[$name]);
164 | }
165 | }
166 |
167 | public function stream_stat()
168 | {
169 | return [
170 | 'dev' => 0,
171 | 'ino' => 0,
172 | 'mode' => 0644,
173 | 'nlink' => 0,
174 | 'uid' => 0,
175 | 'gid' => 0,
176 | 'rdev' => 0,
177 | 'size' => strlen(static::$buffers[$this->name]),
178 | 'atime' => 0,
179 | 'mtime' => 0,
180 | 'ctime' => 0,
181 | 'blksize' => -1,
182 | 'blocks' => -1,
183 | ];
184 | }
185 |
186 | /**
187 | * Read stream
188 | *
189 | * @param integer $count How many bytes of data from the current position should be returned.
190 | *
191 | * @return mixed The data from the stream up to the specified number of bytes (all data if
192 | * the total number of bytes in the stream is less than $count. Null if
193 | * the stream is empty.
194 | *
195 | * @see streamWrapper::stream_read
196 | * @since 11.1
197 | */
198 | public function stream_read($count)
199 | {
200 | $ret = substr(static::$buffers[$this->name], $this->position, $count);
201 | $this->position += strlen($ret);
202 |
203 | return $ret;
204 | }
205 |
206 | /**
207 | * Write stream
208 | *
209 | * @param string $data The data to write to the stream.
210 | *
211 | * @return integer
212 | *
213 | * @see streamWrapper::stream_write
214 | * @since 11.1
215 | */
216 | public function stream_write($data)
217 | {
218 | static::$buffers[$this->name] = static::$buffers[$this->name] ?? '';
219 |
220 | $left = substr(static::$buffers[$this->name], 0, $this->position);
221 | $right = substr(static::$buffers[$this->name], $this->position + strlen($data));
222 | static::$buffers[$this->name] = $left . $data . $right;
223 | $this->position += strlen($data);
224 |
225 | return strlen($data);
226 | }
227 |
228 | /**
229 | * Function to get the current position of the stream
230 | *
231 | * @return integer
232 | *
233 | * @see streamWrapper::stream_tell
234 | * @since 11.1
235 | */
236 | public function stream_tell()
237 | {
238 | return $this->position;
239 | }
240 |
241 | /**
242 | * Function to test for end of file pointer
243 | *
244 | * @return boolean True if the pointer is at the end of the stream
245 | *
246 | * @see streamWrapper::stream_eof
247 | * @since 11.1
248 | */
249 | public function stream_eof()
250 | {
251 | return $this->position >= strlen(static::$buffers[$this->name]);
252 | }
253 |
254 | /**
255 | * The read write position updates in response to $offset and $whence
256 | *
257 | * @param integer $offset The offset in bytes
258 | * @param integer $whence Position the offset is added to
259 | * Options are SEEK_SET, SEEK_CUR, and SEEK_END
260 | *
261 | * @return boolean True if updated
262 | *
263 | * @see streamWrapper::stream_seek
264 | * @since 11.1
265 | */
266 | public function stream_seek($offset, $whence)
267 | {
268 | switch ($whence)
269 | {
270 | case SEEK_SET:
271 | if ($offset < strlen(static::$buffers[$this->name]) && $offset >= 0)
272 | {
273 | $this->position = $offset;
274 |
275 | return true;
276 | }
277 | else
278 | {
279 | return false;
280 | }
281 | break;
282 |
283 | case SEEK_CUR:
284 | if ($offset >= 0)
285 | {
286 | $this->position += $offset;
287 |
288 | return true;
289 | }
290 | else
291 | {
292 | return false;
293 | }
294 | break;
295 |
296 | case SEEK_END:
297 | if (strlen(static::$buffers[$this->name]) + $offset >= 0)
298 | {
299 | $this->position = strlen(static::$buffers[$this->name]) + $offset;
300 |
301 | return true;
302 | }
303 | else
304 | {
305 | return false;
306 | }
307 | break;
308 |
309 | default:
310 | return false;
311 | }
312 | }
313 | }
314 |
315 | if (Buffer::canRegisterWrapper())
316 | {
317 | stream_wrapper_register('plgSystemJoomlaSVGBuffer', Buffer::class);
318 | }
319 |
--------------------------------------------------------------------------------
/plugins/system/joomlasvg/src/Extension/JoomlaSVG.php:
--------------------------------------------------------------------------------
1 | 'onAfterInitialise',
60 | ];
61 | }
62 |
63 | /**
64 | * Executed when Joomla boots up. Used to do in-memory patching of the core files involved in SVG support.
65 | *
66 | * @since 1.0.0
67 | * @noinspection PhpUnused
68 | */
69 | public function onAfterInitialise(Event $e)
70 | {
71 | require_once __DIR__ . '/../Buffer.php';
72 |
73 | $this->loadLanguage();
74 |
75 | // This patches the MediaHelper to add SVG preview support to the Media Manager *AND* sanitize SVGs
76 | if (!class_exists(MediaHelper::class, false))
77 | {
78 | $this->patchMediaHelper();
79 | }
80 |
81 | // This patches BannerHelper to add SVG support to Banners.
82 | if (!class_exists(BannerHelper::class, false))
83 | {
84 | $this->patchBannerHelper();
85 | }
86 |
87 | if (!class_exists(Image::class, false))
88 | {
89 | $this->patchImage();
90 | }
91 | }
92 |
93 | /**
94 | * In-memory patching of the MediaHelper core helper file.
95 | *
96 | * @since 1.0.0
97 | */
98 | private function patchMediaHelper()
99 | {
100 | $source = JPATH_LIBRARIES . '/src/Helper/MediaHelper.php';
101 |
102 | $phpContent = file_get_contents($source);
103 | $phpContent = str_replace('\'xcf|odg|gif|jpg|jpeg|png|bmp|webp\'', "'" . $this->getImageExtensionsPipe() . "'", $phpContent);
104 |
105 | BufferStreamHandler::stream_register();
106 |
107 | $bufferLocation = 'plgSystemJoomlaSVGBuffer://plgSystemJoomlaSVGMediaHelper.php';
108 |
109 | file_put_contents($bufferLocation, $phpContent);
110 | require_once $bufferLocation;
111 | }
112 |
113 | /**
114 | * In-memory patching of the BannerHelper core helper file.
115 | *
116 | * @since 1.0.0
117 | */
118 | private function patchBannerHelper()
119 | {
120 | $source = JPATH_SITE . '/components/com_banners/src/Helper/BannerHelper.php';
121 |
122 | $phpContent = file_get_contents($source);
123 | $phpContent = str_replace('bmp|gif|jpe?g|png|webp', $this->getImageExtensionsPipe(), $phpContent);
124 |
125 | BufferStreamHandler::stream_register();
126 |
127 | $bufferLocation = 'plgSystemJoomlaSVGBuffer://plgSystemJoomlaSVGBannerHelper.php';
128 |
129 | file_put_contents($bufferLocation, $phpContent);
130 |
131 | require_once $bufferLocation;
132 | }
133 |
134 | /**
135 | * In-memory patching of the core Image helper file.
136 | *
137 | * @since 2.0.0
138 | */
139 | private function patchImage()
140 | {
141 | $source = JPATH_SITE . '/libraries/src/Image/Image.php';
142 | $phpContent = file_get_contents($source);
143 | $replaceWith = <<< PHP
144 | if (\Joomla\Plugin\System\Joomlasvg\Extension\JoomlaSVG::isSVG(\$path)) {
145 | return (object) [
146 | 'width' => 1024,
147 | 'height' => 1024,
148 | 'type' => IMAGETYPE_UNKNOWN,
149 | 'attributes' => 'height="60" width="60"',
150 | 'bits' => null,
151 | 'channels' => null,
152 | 'mime' => 'image/svg+xml',
153 | 'filesize' => filesize(\$path),
154 | 'orientation' => 'square',
155 | ];
156 | }
157 |
158 | \$info = getimagesize(\$path);
159 | PHP;
160 | $phpContent = str_replace('$info = getimagesize($path);', $replaceWith, $phpContent);
161 |
162 | BufferStreamHandler::stream_register();
163 |
164 | $bufferLocation = 'plgSystemJoomlaSVGBuffer://plgSystemJoomlaSVGImage.php';
165 |
166 | file_put_contents($bufferLocation, $phpContent);
167 |
168 | require_once $bufferLocation;
169 | }
170 |
171 | /**
172 | * Get the image extensions from the Media Manager configuration
173 | *
174 | * @return string
175 | *
176 | * @since 2.0.0
177 | */
178 | private function getImageExtensionsPipe(): string
179 | {
180 | $mediaParams = ComponentHelper::getParams('com_media');
181 | $extensions = $mediaParams->get('image_extensions', 'bmp,gif,jpg,jpeg,png,webp');
182 | $extensions = array_map(
183 | 'trim',
184 | explode(',', $extensions)
185 | );
186 | $extensions = array_merge($extensions, ['svg', 'webp', 'gif', 'jpg', 'jpeg', 'png', 'bmp']);
187 |
188 | $extensions[] = 'svg';
189 | $extensions[] = 'SVG';
190 | $extensions = array_unique($extensions);
191 |
192 | return implode('|', $extensions);
193 | }
194 | }
--------------------------------------------------------------------------------
/plugins/system/joomlasvg/web.config:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/update/joomlasvg.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 | 2.0.1
11 |
12 | stable
13 |
14 |
15 |
16 | https://github.com/nikosdion/joomlasvg/releases/download/2.0.1/plg_system_joomlasvg-2.0.1.zip
17 |
18 | https://github.com/nikosdion/joomlasvg/releases/download/2.0.1
19 |
20 |
21 |
22 | 7.2.0
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | Joomla SVG Support
32 | A simple plugin which adds SVG support to Joomla 4.
33 | plg_system_joomlasvg
34 | plugin
35 | Nicholas K. Dionysopoulos
36 | https://www.dionysopoulos.me
37 | Updates
38 | 0
39 |
40 |
41 |
42 | 2.0.0
43 |
44 | stable
45 |
46 |
47 |
48 | https://github.com/nikosdion/joomlasvg/releases/download/2.0.0/plg_system_joomlasvg-2.0.0.zip
49 |
50 | https://github.com/nikosdion/joomlasvg/releases/download/2.0.0
51 |
52 |
53 |
54 | 7.2.0
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | Joomla SVG Support
64 | A simple plugin which adds SVG support to Joomla 4.
65 | plg_system_joomlasvg
66 | plugin
67 | Nicholas K. Dionysopoulos
68 | https://www.dionysopoulos.me
69 | Updates
70 | 0
71 |
72 |
73 |
--------------------------------------------------------------------------------