mirror of https://github.com/joomla/joomla-cms.git
528 lines
17 KiB
PHP
528 lines
17 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Joomla! Content Management System
|
|
*
|
|
* @copyright (C) 2013 Open Source Matters, Inc. <https://www.joomla.org>
|
|
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
|
*/
|
|
|
|
namespace Joomla\CMS\Helper;
|
|
|
|
use enshrined\svgSanitize\Sanitizer;
|
|
use Joomla\CMS\Component\ComponentHelper;
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\Filesystem\File;
|
|
use Joomla\CMS\Filter\InputFilter;
|
|
use Joomla\CMS\Language\Text;
|
|
use Joomla\CMS\Plugin\PluginHelper;
|
|
use Joomla\Registry\Registry;
|
|
|
|
// phpcs:disable PSR1.Files.SideEffects
|
|
\defined('JPATH_PLATFORM') or die;
|
|
// phpcs:enable PSR1.Files.SideEffects
|
|
|
|
/**
|
|
* Media helper class
|
|
*
|
|
* @since 3.2
|
|
*/
|
|
class MediaHelper
|
|
{
|
|
/**
|
|
* A special list of blocked executable extensions, skipping executables that are
|
|
* typically executable in the webserver context as those are fetched from
|
|
* Joomla\CMS\Filter\InputFilter
|
|
*
|
|
* @var string[]
|
|
* @since 4.0.0
|
|
*/
|
|
public const EXECUTABLES = [
|
|
'js', 'exe', 'dll', 'go', 'ade', 'adp', 'bat', 'chm', 'cmd', 'com', 'cpl', 'hta',
|
|
'ins', 'isp', 'jse', 'lib', 'mde', 'msc', 'msp', 'mst', 'pif', 'scr', 'sct', 'shb',
|
|
'sys', 'vb', 'vbe', 'vbs', 'vxd', 'wsc', 'wsf', 'wsh', 'html', 'htm', 'msi',
|
|
];
|
|
|
|
/**
|
|
* Checks if the file is an image
|
|
*
|
|
* @param string $fileName The filename
|
|
*
|
|
* @return boolean
|
|
*
|
|
* @since 3.2
|
|
*/
|
|
public static function isImage($fileName)
|
|
{
|
|
static $imageTypes = 'xcf|odg|gif|jpg|jpeg|png|bmp|webp';
|
|
|
|
return preg_match("/\.(?:$imageTypes)$/i", $fileName);
|
|
}
|
|
|
|
/**
|
|
* Gets the file extension for purposed of using an icon
|
|
*
|
|
* @param string $fileName The filename
|
|
*
|
|
* @return string File extension to determine icon
|
|
*
|
|
* @since 3.2
|
|
*/
|
|
public static function getTypeIcon($fileName)
|
|
{
|
|
return strtolower(substr($fileName, strrpos($fileName, '.') + 1));
|
|
}
|
|
|
|
/**
|
|
* Get the Mime type
|
|
*
|
|
* @param string $file The link to the file to be checked
|
|
* @param boolean $isImage True if the passed file is an image else false
|
|
*
|
|
* @return mixed the mime type detected false on error
|
|
*
|
|
* @since 3.7.2
|
|
*/
|
|
public static function getMimeType($file, $isImage = false)
|
|
{
|
|
// If we can't detect anything mime is false
|
|
$mime = false;
|
|
|
|
try {
|
|
if ($isImage && \function_exists('exif_imagetype')) {
|
|
$mime = image_type_to_mime_type(exif_imagetype($file));
|
|
} elseif ($isImage && \function_exists('getimagesize')) {
|
|
$imagesize = getimagesize($file);
|
|
$mime = $imagesize['mime'] ?? false;
|
|
} elseif (\function_exists('mime_content_type')) {
|
|
// We have mime magic.
|
|
$mime = mime_content_type($file);
|
|
} elseif (\function_exists('finfo_open')) {
|
|
// We have fileinfo
|
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
|
$mime = finfo_file($finfo, $file);
|
|
finfo_close($finfo);
|
|
}
|
|
} catch (\Exception $e) {
|
|
// If we have any kind of error here => false;
|
|
return false;
|
|
}
|
|
|
|
// If we can't detect the mime try it again
|
|
if ($mime === 'application/octet-stream' && $isImage === true) {
|
|
$mime = static::getMimeType($file, false);
|
|
}
|
|
|
|
if (
|
|
($mime === 'application/octet-stream' || $mime === 'image/svg' || $mime === 'image/svg+xml')
|
|
&& !$isImage && strtolower(pathinfo($file, PATHINFO_EXTENSION)) === 'svg' && self::isValidSvg($file, false)
|
|
) {
|
|
return 'image/svg+xml';
|
|
}
|
|
|
|
// We have a mime here
|
|
return $mime;
|
|
}
|
|
|
|
/**
|
|
* Checks the Mime type
|
|
*
|
|
* @param string $mime The mime to be checked
|
|
* @param string $component The optional name for the component storing the parameters
|
|
*
|
|
* @return boolean true if mime type checking is disabled or it passes the checks else false
|
|
*
|
|
* @since 3.7
|
|
*/
|
|
private function checkMimeType($mime, $component = 'com_media'): bool
|
|
{
|
|
$params = ComponentHelper::getParams($component);
|
|
|
|
if ($params->get('check_mime', 1)) {
|
|
$allowedMime = $params->get(
|
|
'upload_mime',
|
|
'image/jpeg,image/gif,image/png,image/bmp,image/webp,application/msword,application/excel,' .
|
|
'application/pdf,application/powerpoint,text/plain,application/x-zip'
|
|
);
|
|
|
|
// Get the mime type configuration
|
|
$allowedMime = array_map('trim', explode(',', str_replace('\\', '', $allowedMime)));
|
|
|
|
// Mime should be available and in the allowed list
|
|
return !empty($mime) && \in_array($mime, $allowedMime);
|
|
}
|
|
|
|
// We don't check mime at all or it passes the checks
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks the file extension
|
|
*
|
|
* @param string $extension The extension to be checked
|
|
* @param string $component The optional name for the component storing the parameters
|
|
*
|
|
* @return boolean true if it passes the checks else false
|
|
*
|
|
* @since 4.0.0
|
|
*/
|
|
public static function checkFileExtension($extension, $component = 'com_media', $allowedExecutables = []): bool
|
|
{
|
|
$params = ComponentHelper::getParams($component);
|
|
|
|
// Media file names should never have executable extensions buried in them.
|
|
$executables = array_merge(self::EXECUTABLES, InputFilter::FORBIDDEN_FILE_EXTENSIONS);
|
|
|
|
// Remove allowed executables from array
|
|
if (count($allowedExecutables)) {
|
|
$executables = array_diff($executables, $allowedExecutables);
|
|
}
|
|
|
|
if (in_array($extension, $executables, true)) {
|
|
return false;
|
|
}
|
|
|
|
$allowable = array_map('trim', explode(',', $params->get('restrict_uploads_extensions', 'bmp,gif,jpg,jpeg,png,webp,ico,mp3,m4a,mp4a,ogg,mp4,mp4v,mpeg,mov,odg,odp,ods,odt,pdf,ppt,txt,xcf,xls,csv')));
|
|
$ignored = array_map('trim', explode(',', $params->get('ignore_extensions', '')));
|
|
|
|
if ($extension == '' || $extension == false || (!\in_array($extension, $allowable, true) && !\in_array($extension, $ignored, true))) {
|
|
return false;
|
|
}
|
|
|
|
// We don't check mime at all or it passes the checks
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks if the file can be uploaded
|
|
*
|
|
* @param array $file File information
|
|
* @param string $component The option name for the component storing the parameters
|
|
* @param string $allowedExecutables Array of executable file types that shall be whitelisted
|
|
*
|
|
* @return boolean
|
|
*
|
|
* @since 3.2
|
|
*/
|
|
public function canUpload($file, $component = 'com_media', $allowedExecutables = [])
|
|
{
|
|
$app = Factory::getApplication();
|
|
$params = ComponentHelper::getParams($component);
|
|
|
|
if (empty($file['name'])) {
|
|
$app->enqueueMessage(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT'), 'error');
|
|
|
|
return false;
|
|
}
|
|
|
|
if ($file['name'] !== File::makeSafe($file['name'])) {
|
|
$app->enqueueMessage(Text::_('JLIB_MEDIA_ERROR_WARNFILENAME'), 'error');
|
|
|
|
return false;
|
|
}
|
|
|
|
$filetypes = explode('.', $file['name']);
|
|
|
|
if (\count($filetypes) < 2) {
|
|
// There seems to be no extension
|
|
$app->enqueueMessage(Text::_('JLIB_MEDIA_ERROR_WARNFILETYPE'), 'error');
|
|
|
|
return false;
|
|
}
|
|
|
|
array_shift($filetypes);
|
|
|
|
// Media file names should never have executable extensions buried in them.
|
|
$executables = array_merge(self::EXECUTABLES, InputFilter::FORBIDDEN_FILE_EXTENSIONS);
|
|
|
|
// Remove allowed executables from array
|
|
if (count($allowedExecutables)) {
|
|
$executables = array_diff($executables, $allowedExecutables);
|
|
}
|
|
|
|
$check = array_intersect($filetypes, $executables);
|
|
|
|
if (!empty($check)) {
|
|
$app->enqueueMessage(Text::_('JLIB_MEDIA_ERROR_WARNFILETYPE'), 'error');
|
|
|
|
return false;
|
|
}
|
|
|
|
$filetype = array_pop($filetypes);
|
|
|
|
$allowable = array_map('trim', explode(',', $params->get('restrict_uploads_extensions', 'bmp,gif,jpg,jpeg,png,webp,ico,mp3,m4a,mp4a,ogg,mp4,mp4v,mpeg,mov,odg,odp,ods,odt,pdf,png,ppt,txt,xcf,xls,csv')));
|
|
$ignored = array_map('trim', explode(',', $params->get('ignore_extensions', '')));
|
|
|
|
if ($filetype == '' || $filetype == false || (!\in_array($filetype, $allowable) && !\in_array($filetype, $ignored))) {
|
|
$app->enqueueMessage(Text::_('JLIB_MEDIA_ERROR_WARNFILETYPE'), 'error');
|
|
|
|
return false;
|
|
}
|
|
|
|
$maxSize = (int) ($params->get('upload_maxsize', 0) * 1024 * 1024);
|
|
|
|
if ($maxSize > 0 && (int) $file['size'] > $maxSize) {
|
|
$app->enqueueMessage(Text::_('JLIB_MEDIA_ERROR_WARNFILETOOLARGE'), 'error');
|
|
|
|
return false;
|
|
}
|
|
|
|
if ($params->get('restrict_uploads', 1)) {
|
|
$allowedExtensions = array_map('trim', explode(',', $params->get('restrict_uploads_extensions', 'bmp,gif,jpg,jpeg,png,webp,ico,mp3,m4a,mp4a,ogg,mp4,mp4v,mpeg,mov,odg,odp,ods,odt,pdf,png,ppt,txt,xcf,xls,csv')));
|
|
|
|
if (\in_array($filetype, $allowedExtensions)) {
|
|
// If tmp_name is empty, then the file was bigger than the PHP limit
|
|
if (!empty($file['tmp_name'])) {
|
|
// Get the mime type this is an image file
|
|
$mime = static::getMimeType($file['tmp_name'], true);
|
|
|
|
// Did we get anything useful?
|
|
if ($mime != false) {
|
|
$result = $this->checkMimeType($mime, $component);
|
|
|
|
// If the mime type is not allowed we don't upload it and show the mime code error to the user
|
|
if ($result === false) {
|
|
$app->enqueueMessage(Text::sprintf('JLIB_MEDIA_ERROR_WARNINVALID_MIMETYPE', $mime), 'error');
|
|
|
|
return false;
|
|
}
|
|
} else {
|
|
// We can't detect the mime type so it looks like an invalid image
|
|
$app->enqueueMessage(Text::_('JLIB_MEDIA_ERROR_WARNINVALID_IMG'), 'error');
|
|
|
|
return false;
|
|
}
|
|
} else {
|
|
$app->enqueueMessage(Text::_('JLIB_MEDIA_ERROR_WARNFILETOOLARGE'), 'error');
|
|
|
|
return false;
|
|
}
|
|
} elseif (!\in_array($filetype, $ignored)) {
|
|
// Get the mime type this is not an image file
|
|
$mime = static::getMimeType($file['tmp_name'], false);
|
|
|
|
// Did we get anything useful?
|
|
if ($mime != false) {
|
|
$result = $this->checkMimeType($mime, $component);
|
|
|
|
// If the mime type is not allowed we don't upload it and show the mime code error to the user
|
|
if ($result === false) {
|
|
$app->enqueueMessage(Text::sprintf('JLIB_MEDIA_ERROR_WARNINVALID_MIMETYPE', $mime), 'error');
|
|
|
|
return false;
|
|
}
|
|
} else {
|
|
// We can't detect the mime type so it looks like an invalid file
|
|
$app->enqueueMessage(Text::_('JLIB_MEDIA_ERROR_WARNINVALID_MIME'), 'error');
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!Factory::getUser()->authorise('core.manage', $component)) {
|
|
$app->enqueueMessage(Text::_('JLIB_MEDIA_ERROR_WARNNOTADMIN'), 'error');
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($filetype === 'svg') {
|
|
return self::isValidSvg($file['tmp_name'], true);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Calculate the size of a resized image
|
|
*
|
|
* @param integer $width Image width
|
|
* @param integer $height Image height
|
|
* @param integer $target Target size
|
|
*
|
|
* @return array The new width and height
|
|
*
|
|
* @since 3.2
|
|
*/
|
|
public static function imageResize($width, $height, $target)
|
|
{
|
|
/*
|
|
* Takes the larger size of the width and height and applies the
|
|
* formula accordingly. This is so this script will work
|
|
* dynamically with any size image
|
|
*/
|
|
if ($width > $height) {
|
|
$percentage = ($target / $width);
|
|
} else {
|
|
$percentage = ($target / $height);
|
|
}
|
|
|
|
// Gets the new value and applies the percentage, then rounds the value
|
|
$width = round($width * $percentage);
|
|
$height = round($height * $percentage);
|
|
|
|
return [$width, $height];
|
|
}
|
|
|
|
/**
|
|
* Counts the files and directories in a directory that are not php or html files.
|
|
*
|
|
* @param string $dir Directory name
|
|
*
|
|
* @return array The number of media files and directories in the given directory
|
|
*
|
|
* @since 3.2
|
|
*/
|
|
public function countFiles($dir)
|
|
{
|
|
$total_file = 0;
|
|
$total_dir = 0;
|
|
|
|
if (is_dir($dir)) {
|
|
$d = dir($dir);
|
|
|
|
while (($entry = $d->read()) !== false) {
|
|
if ($entry[0] !== '.' && strpos($entry, '.html') === false && strpos($entry, '.php') === false && is_file($dir . DIRECTORY_SEPARATOR . $entry)) {
|
|
$total_file++;
|
|
}
|
|
|
|
if ($entry[0] !== '.' && is_dir($dir . DIRECTORY_SEPARATOR . $entry)) {
|
|
$total_dir++;
|
|
}
|
|
}
|
|
|
|
$d->close();
|
|
}
|
|
|
|
return [$total_file, $total_dir];
|
|
}
|
|
|
|
/**
|
|
* Small helper function that properly converts any
|
|
* configuration options to their byte representation.
|
|
*
|
|
* @param string|integer $val The value to be converted to bytes.
|
|
*
|
|
* @return integer The calculated bytes value from the input.
|
|
*
|
|
* @since 3.3
|
|
*/
|
|
public function toBytes($val)
|
|
{
|
|
switch ($val[\strlen($val) - 1]) {
|
|
case 'M':
|
|
case 'm':
|
|
return (int) $val * 1048576;
|
|
case 'K':
|
|
case 'k':
|
|
return (int) $val * 1024;
|
|
case 'G':
|
|
case 'g':
|
|
return (int) $val * 1073741824;
|
|
default:
|
|
return $val;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Method to check if the given directory is a directory configured in FileSystem - Local plugin
|
|
*
|
|
* @param string $directory
|
|
*
|
|
* @return boolean
|
|
*
|
|
* @since 4.0.0
|
|
*/
|
|
public static function isValidLocalDirectory($directory)
|
|
{
|
|
$plugin = PluginHelper::getPlugin('filesystem', 'local');
|
|
|
|
if ($plugin) {
|
|
$params = new Registry($plugin->params);
|
|
|
|
$directories = $params->get('directories', '[{"directory": "images"}]');
|
|
|
|
// Do a check if default settings are not saved by user
|
|
// If not initialize them manually
|
|
if (is_string($directories)) {
|
|
$directories = json_decode($directories);
|
|
}
|
|
|
|
foreach ($directories as $directoryEntity) {
|
|
if ($directoryEntity->directory === $directory) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Helper method get clean data for value stores in a Media form field by removing adapter information
|
|
* from the value if available (in this case, the value will have this format:
|
|
* images/headers/blue-flower.jpg#joomlaImage://local-images/headers/blue-flower.jpg?width=700&height=180)
|
|
*
|
|
* @param string $value
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 4.0.0
|
|
*/
|
|
public static function getCleanMediaFieldValue($value)
|
|
{
|
|
if ($pos = strpos($value, '#')) {
|
|
return substr($value, 0, $pos);
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Check if a file is a valid SVG
|
|
*
|
|
* @param string $file
|
|
* @param bool $shouldLogErrors
|
|
*
|
|
* @return boolean
|
|
*
|
|
* @since 4.3.0
|
|
*/
|
|
private static function isValidSvg($file, $shouldLogErrors = true): bool
|
|
{
|
|
$sanitizer = new Sanitizer();
|
|
|
|
$isValid = $sanitizer->sanitize(file_get_contents($file));
|
|
|
|
$svgErrors = $sanitizer->getXmlIssues();
|
|
|
|
/**
|
|
* We allow comments and temp fix for bugs in svg-santitizer
|
|
* https://github.com/darylldoyle/svg-sanitizer/issues/64
|
|
* https://github.com/darylldoyle/svg-sanitizer/issues/63
|
|
* https://github.com/darylldoyle/svg-sanitizer/pull/65
|
|
* https://github.com/darylldoyle/svg-sanitizer/issues/82
|
|
*/
|
|
foreach ($svgErrors as $i => $error) {
|
|
if (
|
|
($error['message'] === 'Suspicious node \'#comment\'')
|
|
|| ($error['message'] === 'Suspicious attribute \'space\'')
|
|
|| ($error['message'] === 'Suspicious attribute \'enable-background\'')
|
|
|| ($error['message'] === 'Suspicious node \'svg\'')
|
|
) {
|
|
unset($svgErrors[$i]);
|
|
}
|
|
}
|
|
|
|
if ($isValid === false || count($svgErrors)) {
|
|
if ($shouldLogErrors) {
|
|
Factory::getApplication()->enqueueMessage(Text::_('JLIB_MEDIA_ERROR_WARNIEXSS'), 'error');
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|