* */ /** * Модуль управления медиа-данными (изображения, видео и т.п.) * * @package application.modules.media * @since 2.0 */ class ModuleMedia extends ModuleORM { /** * Список типов медиа * Свои кастомные типы необходимо нумеровать с 1000 */ const MEDIA_TYPE_IMAGE = 1; const MEDIA_TYPE_VIDEO = 2; /** * Список типов для проверки доступа */ const TYPE_CHECK_ALLOW_ADD = 'add'; const TYPE_CHECK_ALLOW_REMOVE = 'remove'; const TYPE_CHECK_ALLOW_UPDATE = 'update'; const TYPE_CHECK_ALLOW_PREVIEW = 'preview'; // возможность создания превью для объекта const TYPE_CHECK_ALLOW_VIEW_LIST = 'view_list'; // просмотр списка медиа у объекта /** * Объект текущего пользователя * * @var ModuleUser_EntityUser|null */ protected $oUserCurrent; protected $oMapper = null; protected $aTargetTypes = array( 'topic' => array( 'allow_preview' => true, ), 'comment' => array(), 'blog' => array(), 'talk' => array(), ); /** * Список доступных типов медиа * * @var array */ protected $aMediaTypes = array( self::MEDIA_TYPE_IMAGE, self::MEDIA_TYPE_VIDEO ); /** * Инициализация * */ public function Init() { parent::Init(); $this->oMapper = Engine::GetMapper(__CLASS__); $this->oUserCurrent = $this->User_GetUserCurrent(); } /** * Возвращает список типов объектов * * @return array */ public function GetTargetTypes() { return $this->aTargetTypes; } /** * Добавляет в разрешенные новый тип * * @param string $sTargetType Тип * @param array $aParams Параметры * @return bool */ public function AddTargetType($sTargetType, $aParams = array()) { if (!array_key_exists($sTargetType, $this->aTargetTypes)) { $this->aTargetTypes[$sTargetType] = $aParams; return true; } return false; } /** * Проверяет разрешен ли данный тип * * @param string $sTargetType Тип * @return bool */ public function IsAllowTargetType($sTargetType) { return in_array($sTargetType, array_keys($this->aTargetTypes)); } /** * Возвращает парметры нужного типа * * @param string $sTargetType * * @return array|null */ public function GetTargetTypeParams($sTargetType) { if ($this->IsAllowTargetType($sTargetType)) { return $this->aTargetTypes[$sTargetType]; } return null; } /** * Возвращает конкретный параметр нужного типа * * @param string $sTargetType * @param string $sName * * @return mixed|null */ public function GetTargetTypeParam($sTargetType, $sName) { $aParams = $this->GetTargetTypeParams($sTargetType); if ($aParams and array_key_exists($sName, $aParams)) { return $aParams[$sName]; } return null; } /** * Проверяет разрешен ли тип медиа * * @param string $sType * * @return bool */ public function IsAllowMediaType($sType) { return in_array($sType, $this->aMediaTypes); } /** * Возвращает тип медиа по имени файла (имя файла должно содержать его расширение) * * @param $sFile * @return int|null */ public function GetMediaTypeByFileName($sFile) { $aPathInfo = pathinfo($sFile); $sExtension = isset($aPathInfo['extension']) ? $aPathInfo['extension'] : 'unknown'; return $this->GetMediaTypeByFileExtension($sExtension); } /** * Возвращает тип медиа по расширению файла * * @param $sExtension * @return int|null */ public function GetMediaTypeByFileExtension($sExtension) { $sExtension = strtolower($sExtension); if (in_array($sExtension, array('jpg', 'jpeg', 'gif', 'png'))) { return self::MEDIA_TYPE_IMAGE; } return null; } /** * Возвращает название медиа типа по его значению * Название получается автоматически из константы с определением типа * * @param $iType * @return null|string */ public function GetMediaTypeName($iType) { $oRefl = new ReflectionObject($this); foreach ($oRefl->getConstants() as $sName => $mValue) { if (strpos($sName, 'MEDIA_TYPE_') === 0 and $mValue == $iType) { return strtolower(substr($sName, strlen('MEDIA_TYPE_'))); } } return null; } /** * Проверка объекта target - владелец медиа * * @param string $sTargetType Тип * @param int $iTargetId ID владельца * @param string $sAllowType * @param array $aParams * @param string|null $sTargetTmp * @return bool */ public function CheckTarget($sTargetType, $iTargetId = null, $sAllowType = null, $aParams = array(), $sTargetTmp = null) { if (!$this->IsAllowTargetType($sTargetType)) { return false; } /** * Проверка на максимальное количество файлов */ if ($sAllowType == self::TYPE_CHECK_ALLOW_ADD and ($iTargetId or $sTargetTmp)) { $iCountAllow = $this->GetConfigParam('max_count_files', $sTargetType); if (is_numeric($iCountAllow)) { $aFilterCount = array('target_type' => $sTargetType); if ($iTargetId) { $aFilterCount['target_id'] = $iTargetId; } else { $aFilterCount['target_tmp'] = $sTargetTmp; } $iCount = $this->getCountFromTargetByFilter($aFilterCount); if ($iCount >= $iCountAllow) { return $this->Lang_Get('media.error.max_count_files'); } } } $sMethod = 'CheckTarget' . func_camelize($sTargetType); if (method_exists($this, $sMethod)) { if (!array_key_exists('user', $aParams)) { $aParams['user'] = $this->oUserCurrent; } return $this->$sMethod($iTargetId, $sAllowType, $aParams); } return false; } public function NotifyCreatePreviewTarget($sTargetType, $iTargetId, $oRelationTarget) { if (!$this->IsAllowTargetType($sTargetType)) { return false; } $sMethod = 'NotifyCreatePreviewTarget' . func_camelize($sTargetType); if (method_exists($this, $sMethod)) { return $this->$sMethod($iTargetId, $oRelationTarget); } return false; } public function NotifyRemovePreviewTarget($sTargetType, $iTargetId, $oRelationTarget) { if (!$this->IsAllowTargetType($sTargetType)) { return false; } $sMethod = 'NotifyRemovePreviewTarget' . func_camelize($sTargetType); if (method_exists($this, $sMethod)) { return $this->$sMethod($iTargetId, $oRelationTarget); } return false; } /** * Возвращает параметр конфига с учетом текущего target_type * * @param string $sParam Ключ конфига относительно module.media * @param string $sTargetType Тип * * @return mixed */ public function GetConfigParam($sParam, $sTargetType) { $mValue = Config::Get("module.media.type.{$sTargetType}.{$sParam}"); if (!$mValue) { $mValue = Config::Get("module.media.{$sParam}"); } return $mValue; } /** * Проверяем файл на максимальный размер с учетом типа медиа * * @param $iSize * @param $iMediaType * @param $sTargetType * @return bool */ public function CheckFileUploadSize($iSize, $iMediaType, $sTargetType) { $sConfigParam = 'max_size'; $sMediaTypeName = $this->Media_GetMediaTypeName($iMediaType); if (is_null($iMaxSizeKb = $this->GetConfigParam($sMediaTypeName . '.' . $sConfigParam, $sTargetType))) { $iMaxSizeKb = $this->GetConfigParam($sConfigParam, $sTargetType); } if ($iSize > $iMaxSizeKb * 1024) { return $this->Lang_Get('media.error.too_large', array('size' => $iMaxSizeKb)); } return true; } public function Upload($aFile, $sTargetType, $sTargetId, $sTargetTmp = null) { if (is_string($aFile)) { return $this->UploadUrl($aFile, $sTargetType, $sTargetId, $sTargetTmp); } else { return $this->UploadLocal($aFile, $sTargetType, $sTargetId, $sTargetTmp); } } public function UploadLocal($aFile, $sTargetType, $sTargetId, $sTargetTmp = null) { if (!is_array($aFile) || !isset($aFile['error']) || !isset($aFile['tmp_name']) || !is_string($aFile['tmp_name']) || !isset($aFile['name']) || !isset($aFile['size'])) { return false; } if ($aFile['error'] != UPLOAD_ERR_OK) { switch ($aFile['error']) { case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: return $this->Lang_Get('media.error.too_large', array('size' => @func_ini_return_bytes(ini_get('upload_max_filesize')) / 1024)); default: return $this->Lang_Get('media.error.upload'); } } $aPathInfo = pathinfo($aFile['name']); $sExtension = isset($aPathInfo['extension']) ? $aPathInfo['extension'] : 'unknown'; $sFileName = $aPathInfo['filename'] . '.' . $sExtension; /** * Проверяем на размер файла */ $iMediaType = $this->GetMediaTypeByFileExtension($sExtension); if (true !== ($mRes = $this->CheckFileUploadSize($aFile['size'], $iMediaType, $sTargetType))) { return $mRes; } /** * Копируем загруженный файл */ $sDirTmp = Config::Get('path.tmp.server') . '/media/'; if (!is_dir($sDirTmp)) { @mkdir($sDirTmp, 0777, true); } $sFileTmp = $sDirTmp . $sFileName; if (!move_uploaded_file($aFile['tmp_name'], $sFileTmp)) { return $this->Lang_Get('media.error.upload'); } return $this->ProcessingFile($sFileTmp, $sTargetType, $sTargetId, $sTargetTmp); } public function UploadUrl($sFileUrl, $sTargetType, $sTargetId, $sTargetTmp = null) { /** * Проверяем, является ли файл изображением * TODO: файл может быть не только изображением, поэтому требуется рефакторинг */ if (!$aImageInfo = (@getimagesize($sFileUrl))) { return $this->Lang_Get('media.error.not_image'); } $aTypeImage = array( 1 => 'gif', 2 => 'jpg', 3 => 'png' ); // see http://php.net/manual/en/function.exif-imagetype.php $sExtension = isset($aTypeImage[$aImageInfo[2]]) ? $aTypeImage[$aImageInfo[2]] : 'jpg'; /** * Открываем файловый поток и считываем файл поблочно, * контролируя максимальный размер изображения */ $rFile = fopen($sFileUrl, 'r'); if (!$rFile) { return $this->Lang_Get('media.error.upload'); } $iMaxSizeKb = $this->GetConfigParam('max_size', $sTargetType); $iSizeKb = 0; $sContent = ''; while (!feof($rFile) and $iSizeKb < $iMaxSizeKb) { $sContent .= fread($rFile, 1024 * 2); $iSizeKb++; } /** * Если конец файла не достигнут, * значит файл имеет недопустимый размер */ if (!feof($rFile)) { return $this->Lang_Get('media.error.too_large', array('size' => $iMaxSizeKb)); } fclose($rFile); /** * Копируем загруженный файл */ $sDirTmp = Config::Get('path.tmp.server') . '/media/'; if (!is_dir($sDirTmp)) { @mkdir($sDirTmp, 0777, true); } $sFileTmp = $sDirTmp . func_generator() . '.' . $sExtension; $rFile = fopen($sFileTmp, 'w'); fwrite($rFile, $sContent); fclose($rFile); return $this->ProcessingFile($sFileTmp, $sTargetType, $sTargetId, $sTargetTmp); } public function ProcessingFile($sFileTmp, $sTargetType, $sTargetId, $sTargetTmp = null) { /** * Определяем тип медиа по файлу и запускаем обработку */ $iType = $this->GetMediaTypeByFileName($sFileTmp); if ($iType == self::MEDIA_TYPE_IMAGE) { return $this->ProcessingFileImage($sFileTmp, $sTargetType, $sTargetId, $sTargetTmp); } return $this->Lang_Get('media.error.incorrect_type'); } public function ProcessingFileImage($sFileTmp, $sTargetType, $sTargetId, $sTargetTmp = null) { $aPathInfo = pathinfo($sFileTmp); $aParams = $this->Image_BuildParams('media.' . $sTargetType); /** * Если объект изображения не создан, возвращаем ошибку */ if (!$oImage = $this->Image_Open($sFileTmp, $aParams)) { $this->Fs_RemoveFileLocal($sFileTmp); return $this->Image_GetLastError(); } $iWidth = $oImage->getWidth(); $iHeight = $oImage->getHeight(); $sPath = $this->GetSaveDir($sTargetType, $sTargetId); $iFileSize = $this->Fs_GetFileSize($sFileTmp); /** * Уникальное имя файла */ $sFileName = func_generator(20); /** * Сохраняем оригинальную копию * Оригинал храним без вотермарка */ $sFileResult = null; $mOriginalSize = $this->GetConfigParam('image.original', $sTargetType); if ($mOriginalSize === true) { if (!$sFileResult = $oImage->saveSmart($sPath, $sFileName, array('skip_watermark' => true))) { $this->Fs_RemoveFileLocal($sFileTmp); return $this->Image_GetLastError(); } } elseif (is_string($mOriginalSize)) { /** * Ресайзим оригинал */ $aOriginalSize = $this->ParsedImageSize($mOriginalSize); if ($aOriginalSize['crop']) { $oImage->cropProportion($aOriginalSize['w'] / $aOriginalSize['h'], 'center'); } if (!$sFileResult = $oImage->resize($aOriginalSize['w'], $aOriginalSize['h'], true)->saveSmart($sPath, $sFileName, array('skip_watermark' => true)) ) { $this->Fs_RemoveFileLocal($sFileTmp); return $this->Image_GetLastError(); } $iFileSize = $this->Fs_GetFileSize($sFileResult); } $aSizes = $this->GetConfigParam('image.sizes', $sTargetType); /** * Перед запуском генерации подчищаем память */ unset($oImage); /** * Генерируем варианты с необходимыми размерами */ $sFileResultLast = $this->GenerateImageBySizes($sFileTmp, $sPath, $sFileName, $aSizes, $aParams); if (!$sFileResult) { /** * Оригинала нет, поэтому получаем фейковый путь до основного файла (нужен для получения файлов других размеров) */ $aPathInfoLast = pathinfo($sFileResultLast); $aFileNamePart = explode('_', $aPathInfoLast['filename']); $sFileResult = $aPathInfoLast['dirname'] . '/' . $aFileNamePart[0] . '.' . $aPathInfoLast['extension']; } /** * Сохраняем медиа */ $oMedia = Engine::GetEntity('ModuleMedia_EntityMedia'); $oMedia->setUserId($this->oUserCurrent ? $this->oUserCurrent->getId() : null); $oMedia->setType(self::MEDIA_TYPE_IMAGE); $oMedia->setTargetType($sTargetType); $oMedia->setFilePath($sFileResult); $oMedia->setFileName($aPathInfo['filename']); $oMedia->setFileSize($iFileSize); $oMedia->setWidth($iWidth); $oMedia->setHeight($iHeight); $oMedia->setDataOne('image_sizes', $aSizes); /** * Теперь можно удалить временный файл */ $this->Fs_RemoveFileLocal($sFileTmp); /** * Добавляем в БД */ if ($oMedia->Add()) { /** * Создаем связь с владельцем */ if ($oTarget = $this->AttachMediaToTarget($oMedia, $sTargetType, $sTargetId, $sTargetTmp)) { $oMedia->_setData(array('_relation_entity' => $oTarget)); return $oMedia; } } return false; } /** * Присоединяет медиа файл к объекту * * @param $oMedia * @param $sTargetType * @param $sTargetId * @param $sTargetTmp * @return bool|Entity */ public function AttachMediaToTarget($oMedia, $sTargetType, $sTargetId, $sTargetTmp) { /** * Создаем связь с владельцем */ $oTarget = Engine::GetEntity('ModuleMedia_EntityTarget'); $oTarget->setMediaId($oMedia->getId()); $oTarget->setTargetType($sTargetType); $oTarget->setTargetId($sTargetId ?: null); $oTarget->setTargetTmp($sTargetTmp ?: null); if ($oTarget->Add()) { return $oTarget; } return false; } /** * Создает набор отресайзанных изображений * Варианты наименований результирующих файлов в зависимости от размеров: * file_100x150 - w=100 h=150 crop=false * file_100x150crop - w=100 h=150 crop=true * file_x150 - w=null h=150 crop=false * file_100x - w=100 h=null crop=false * * @param $sFileSource * @param $sDirDist * @param $sFileName * @param $aSizes * @param null $aParams */ public function GenerateImageBySizes($sFileSource, $sDirDist, $sFileName, $aSizes, $aParams = null) { if (!$aSizes) { return; } /** * Преобразуем упрощенную запись списка размеров в полную */ foreach ($aSizes as $k => $v) { if (!is_array($v)) { $aSizes[$k] = $this->ParsedImageSize($v); } } $sFileResult = null; foreach ($aSizes as $aSize) { /** * Для каждого указанного в конфиге размера генерируем картинку */ $sNewFileName = $sFileName . '_' . $aSize['w'] . 'x' . $aSize['h']; if ($oImage = $this->Image_Open($sFileSource, $aParams)) { if ($aSize['crop']) { $oImage->cropProportion($aSize['w'] / $aSize['h'], 'center'); $sNewFileName .= 'crop'; } if (!$sFileResult = $oImage->resize($aSize['w'], $aSize['h'], true)->saveSmart($sDirDist, $sNewFileName) ) { // TODO: прерывать и возвращать false? } } } /** * Возвращаем путь до последнего созданного файла */ return $sFileResult; } public function RemoveImageBySizes($sPath, $aSizes, $bRemoveOriginal = true) { if ($aSizes) { /** * Преобразуем упрощенную запись списка размеров в полную */ foreach ($aSizes as $k => $v) { if (!is_array($v)) { $aSizes[$k] = $this->ParsedImageSize($v); } } foreach ($aSizes as $aSize) { $sSize = $aSize['w'] . 'x' . $aSize['h']; if ($aSize['crop']) { $sSize .= 'crop'; } $this->Image_RemoveFile($this->GetImagePathBySize($sPath, $sSize)); } } /** * Удаляем оригинал */ if ($bRemoveOriginal) { $this->Image_RemoveFile($sPath); } } /** * Возвращает каталог для сохранения контента медиа * * @param string $sTargetType * @param string|null $sTargetId Желательно для одного типа при формировании каталога для загрузки выбрать что-то одно - использовать $sTargetId или нет * @param string $sPostfix Дополнительный каталог для сохранения в конце цепочки * * @return string */ public function GetSaveDir($sTargetType, $sTargetId = null, $sPostfix = '') { $sPostfix = trim($sPostfix, '/'); return Config::Get('path.uploads.base') . "/media/{$sTargetType}/" . date('Y/m/d/H/') . ($sPostfix ? "{$sPostfix}/" : ''); } public function BuildCodeForEditor($oMedia, $aParams) { $sCode = ''; if ($oMedia->getType() == self::MEDIA_TYPE_IMAGE) { $aSizes = (array)$oMedia->getDataOne('image_sizes'); $sSizeParam = isset($aParams['size']) ? (string)$aParams['size'] : ''; $sSize = 'original'; $bNeedHref = false; /** * Проверяем корректность размера */ foreach ($aSizes as $aSizeAllow) { $sSizeKey = $aSizeAllow['w'] . 'x' . $aSizeAllow['h'] . ($aSizeAllow['crop'] ? 'crop' : ''); if ($sSizeKey == $sSizeParam) { $sSize = $sSizeKey; /** * Необходимость лайтбокса */ if ($aSizeAllow['w'] < $oMedia->getWidth()) { $bNeedHref = true; } } } $sPath = $oMedia->getFileWebPath($sSize == 'original' ? null : $sSize); $aParams['image_url'] = (isset($aParams['relative_web']) and $aParams['relative_web']) ? $this->GetPathRelativeWebFromWeb($sPath) : $sPath; $aParams['href_url'] = (isset($aParams['relative_web']) and $aParams['relative_web']) ? $this->GetPathRelativeWebFromWeb($oMedia->getFileWebPath()) : $oMedia->getFileWebPath(); $aParams['need_href'] = $bNeedHref; if (!isset($aParams['title'])) { $aParams['title'] = $oMedia->getDataOne('title'); } /** * Формируем HTML изображения */ $sCode = $this->BuildImageCodeForEditor($aParams); } return $sCode; } /** * Возвращает относительный веб путь до изображения * * @param string $sPath Полный веб путь до изображения * @return string */ public function GetPathRelativeWebFromWeb($sPath) { return '/' . ltrim(parse_url($sPath, PHP_URL_PATH), '/'); } /** * Формирует HTML изображения * * @param $aParams * @return string */ public function BuildImageCodeForEditor($aParams) { $sCode = ' $sDataValue) { if ($sDataValue) { $sDataParams .= ' data-' . $sDataName . '="' . htmlspecialchars($sDataValue) . '"'; } } } if (isset($aParams['need_href']) and $aParams['need_href']) { $sCode .= ' />'; $sLbxGroup = ''; if (isset($aParams['lbx_group'])) { $sLbxGroup = ' data-rel="' . htmlspecialchars($aParams['lbx_group']) . '"'; } $sCode = '' . $sCode . ''; } else { $sCode .= $sDataParams . ' />'; } return $sCode; } public function GetMediaByTarget($sTargetType, $iTargetId, $iUserId = null) { return $this->oMapper->GetMediaByTarget($sTargetType, $iTargetId, $iUserId); } public function GetMediaByTargetTmp($sTargetTmp, $iUserId = null) { return $this->oMapper->GetMediaByTargetTmp($sTargetTmp, $iUserId); } /** * Выполняет удаление файлов медиа-объекта * * @param $oMedia */ public function DeleteFiles($oMedia) { /** * Сначала удаляем все файлы */ if ($oMedia->getType() == self::MEDIA_TYPE_IMAGE) { $aSizes = $oMedia->getDataOne('image_sizes'); $this->RemoveImageBySizes($oMedia->getFilePath(), $aSizes); } } /** * Возвращает список media с учетов прав доступа текущего пользователя * В методе происходит проверка на права: * 1. разрешаем объекты, которые создал пользователь * 2. разрешаем объекты, которые связаны таргетом, к которому у пользователя есть доступ на редактирование * @param array $aId * * @return array */ public function GetAllowMediaItemsById($aId) { $aIdItems = array(); foreach ((array)$aId as $iId) { $aIdItems[] = (int)$iId; } $aIdItemsAll = $aIdItems; if (is_array($aIdItems) and count($aIdItems)) { $iUserId = $this->oUserCurrent ? $this->oUserCurrent->getId() : null; $aMediaItems = $this->Media_GetMediaItemsByFilter(array( '#where' => array( 'id in (?a) AND ( user_id is null OR user_id = ?d )' => array( $aIdItems, $iUserId ) ), '#index-from-primary' ) ); /** * Смотрим что осталось */ if (!is_null($iUserId) and $aIdItems = array_diff($aIdItems, array_keys($aMediaItems))) { $aMediaAllowIds = array(); $aTargetMediaGroup = $this->GetTargetItemsByFilter(array( 'media_id in' => $aIdItems, '#with' => 'media', '#index-group' => 'media_id' )); $_this = $this; foreach ($aTargetMediaGroup as $iMediaId => $aTargetMedia) { /** * Проверяем каждый таргет */ foreach ($aTargetMedia as $oTargetMedia) { if ($this->Cache_Remember("media_check_target_{$oTargetMedia->getTargetType()}_{$oTargetMedia->getTargetId()}", function () use ($_this, $oTargetMedia, $iUserId) { if ($oTargetMedia->getTargetId()) { return $_this->CheckTarget($oTargetMedia->getTargetType(), $oTargetMedia->getTargetId(), ModuleMedia::TYPE_CHECK_ALLOW_ADD); } if ($oMedia = $oTargetMedia->getMedia() and $oMedia->getUserId() == $iUserId) { return true; } return false; }, false, array(), 'life', true) ) { $aMediaAllowIds[] = $oTargetMedia->getMediaId(); break; } } } if ($aMediaAllowIds) { $aMediaItems = $aMediaItems + $this->GetMediaItemsByFilter(array( 'id in' => $aMediaAllowIds, '#index-from-primary' )); } } /** * Нужно отсортировать по первоначальному массиву $aIdItems */ $aReturn = array(); foreach ($aIdItemsAll as $iId) { if (isset($aMediaItems[$iId])) { $aReturn[$iId] = $aMediaItems[$iId]; } } return $aReturn; } return array(); } /** * Обработка тега gallery в тексте *
** * @param string $sTag Тег на ктором сработал колбэк * @param array $aParams Список параметров тега * @return string */ public function CallbackParserTagGallery($sTag, $aParams) { if (isset($aParams['items'])) { $aItems = explode(',', $aParams['items']); $aItems = array_unique($aItems); } else { return ''; } if (!($aMediaItems = $this->GetAllowMediaItemsById($aItems))) { return ''; } $aParamsMedia = array( 'size' => '100crop', 'skip_title' => true, 'relative_web' => true ); $sProperties = ''; if (isset($aParams['nav']) and in_array($aParams['nav'], array('thumbs'))) { $sProperties .= ' data-nav="' . $aParams['nav'] . '" '; } $sTextResult = '*