with srcset with 2x and webp versions. For example this code:
225 |
226 | ```php
227 | echo PictureWidget::widget([
228 | 'model' => $file,
229 | 'alt' => 'Some alternative text',
230 | 'width' => 100
231 | ]);
232 | ```
233 |
234 | will make this html:
235 |
236 | ```html
237 |
238 |
239 |
241 |
244 |
245 | ```
246 |
247 | Additional parameters allowed to pass media-queries to widget.
248 |
249 | ### Listing the files
250 |
251 | There is a widget for listing all files. It supports [Lightbox2](https://lokeshdhakar.com/projects/lightbox2/) gallery
252 | to display images and MS Office files preview. Its also supports downloading of the all the files attached to the field
253 | in a ZIP-archive.
254 |
255 | ```php
256 | echo \floor12\files\components\FileListWidget::widget([
257 | 'files' => $model->docs,
258 | 'downloadAll' => true,
259 | 'zipTitle' => "Documents of {$user->fullname}"
260 | ])
261 | ```
262 |
263 | An array of `File` objects must be passed to the widget `files` field. Also additional parameters available:
264 |
265 | - `title` - optionally set the title of the block (by default its taken from `AttributeLabels()`)",
266 | - `downloadAll` - show the "download all" button,
267 | - `zipTitle` - set the file name of zip archive,
268 | - `passFirst` - skip first file in array (it is often necessary to display the gallery without the first picture. For
269 | example, in the news view page, when the first image used to be news main image).
270 |
271 | 
272 |
273 | ### InputWidget for ActiveFrom
274 |
275 | To display files block in your forms use the `floor12\files\components\FileInputWidget`:
276 |
277 | ```php
278 | = $form->field($model, 'avatar')->widget(floor12\files\components\FileInputWidget::class) ?>
279 | ```
280 |
281 | Moreover, if `maxFiles` parameter in` FileValidator` equals to 1 or more, `FileInputWidget` will take the necessary form
282 | to load one file or several at once. If necessary, you can pass `uploadButtonText` and` uploadButtonClass` parameters to
283 | the widget.
284 |
285 | ## Contributing
286 |
287 | I will be glad of any help in the development, support and bug reporting of this module.
--------------------------------------------------------------------------------
/README_RU.md:
--------------------------------------------------------------------------------
1 | # yii2-module-files
2 |
3 | [](https://travis-ci.org/floor12/yii2-module-files)
4 | [](https://scrutinizer-ci.com/g/floor12/yii2-module-files/?branch=master)
5 | [](https://packagist.org/packages/floor12/yii2-module-files)
6 | [](https://packagist.org/packages/floor12/yii2-module-files)
7 | [](https://packagist.org/packages/floor12/yii2-module-files)
8 | [](https://packagist.org/packages/floor12/yii2-module-files)
9 |
10 | ## Информация о модуле
11 |
12 | 
13 |
14 | Это модуль разработан для того, чтобы решить проблему создание полей с файлами в ActiveRecord моделях фреймворка Yii2.
15 | Основными компонентами модуля являются:
16 |
17 | - `floor12\files\components\FileBehaviour` - поведение, которое необходимо подключить к ActiveRecord модели;
18 | - `floor12\files\components\FileInputWidget` - виджет для формы, позволяющий добавлять, редактировать и в целом работать
19 | с добавленными файлами;
20 | - `floor12\files\components\FileListWidget` - дополнительный виджет для вывода списка файлов с возможностями просмотра
21 | изображений в галереи Lightbox2, загрузке всех файлов текущего поля в формате zip, а так же просмотром Word и Excel
22 | файлов с помощью онлайн офиса от Microsoft.
23 |
24 | ### Основные функции модуля
25 |
26 | - добавление одного и более полей с файлами к ActiveRecord модели;
27 | - настройка валидация этих полей при помощи стандартного `FileValidator`, указанного в секции `rules()`;
28 | - в случае работы с изображениями - возможность сконфигурировать пропорции изображения (в этом случае при загрузке
29 | изображения через `FileInputWidget` виджет автоматически откроет окно для обрезки изображения с нужными пропорциями);
30 | - возможность создания миниатюр для использования в различных местах шаблонов изображений оптимальных размеров. Так же
31 | эти миниатюры могут поддерживают формат WEBP;
32 | - возможность скачивания всех добавленных в одно поле файлов в виде ZIP-архива
33 | - при загрузке изображений через `FileInputWidget` имеется возможность изменять порядок объектов драг-н-дропом, изменять
34 | размер и имя;
35 | - при загрузке файлов драг-н-дропом порядок файлов сохраняется тем же, что и был в момент выделения файлов на
36 | компьютере (это очень удобно, если необходимо добавить к модели, к примеру, 50 изображений в строгом порядке);
37 | - при загрузке изображений автоматическое определение горизонта по EXIF-метке;
38 | - при необходимости добавления изображений к модели не через веб-интерфейс сайта, а при помощь консольных парсеров и
39 | других похожих случаев - такая возможность имеется. Для этого в системе предусмотрено два
40 | класса: `FileCreateFromInstance.php` и `FileCreateFromPath.php`.
41 | - при работе с видео файлами - перекодировка их в h264 при помощи утилиты ffmpeg;
42 |
43 | ### Интернационализация
44 |
45 | На данный этап модуль поддерживает следующие языки:
46 |
47 | - Английский
48 | - Русский
49 |
50 | ### Принцип работы
51 |
52 | Информация о файлах хранится в таблице `file` и содержит связи с моделью через три поля:
53 |
54 | - `class` - полное имя класса связанной модели
55 | - `field` - имя поля модели
56 | - `object_id` - primary key модели
57 |
58 | При работе с виджетом добавления файлов, во время добавления файла в форму происходит его фоновая загрузка и обработка.
59 | В результате этой обработки он записывается на диск и для него создается запись в таблице `file`, где поля `class`
60 | и `field` заполнены данными из модели, а `object_id` присваивается только после сохранения модели ActiveRecord, к
61 | которой подключено поведение. При удалении файла из формы он не удаляется с диска и из таблицы `file`, а просто его
62 | object_id будет обращен в 0. Для периодической очистки такого рода бесхозных файлов можно периодически использоваться
63 | консольную команду `files/console/clean`.
64 |
65 | ## Установка и настройка
66 |
67 | Устанавливаем модуль через composer:
68 | Выполняем команду
69 |
70 | ```bash
71 | $ composer require floor12/yii2-module-files
72 | ```
73 |
74 | или добавляем в секцию "required" файла composer.json
75 |
76 | ```json
77 | "floor12/yii2-module-files": "dev-master"
78 | ```
79 |
80 | Далее выполняем миграцию для создания таблицы `file`
81 |
82 | ```bash
83 | $ ./yii migrate --migrationPath=@vendor/floor12/yii2-module-files/src/migrations/
84 | ```
85 |
86 | Так как для работы модуля требуются контроллеры, прописываем модуль в конфигурации Yii2 приложения:
87 |
88 | ```php
89 | 'modules' => [
90 | 'files' => [
91 | 'class' => 'floor12\files\Module',
92 | 'storage' => '@app/storage',
93 | 'cache' => '@app/storage_cache',
94 | 'token_salt' => 'some_random_salt',
95 | ],
96 | ],
97 | ...
98 | ```
99 |
100 | Параметры:
101 |
102 | - `storage` - алиас пути к хранилищу файлов и исходников изображений на диске, по умолчанию располагается в папке
103 | storage в корне проекта;
104 | - `cache` - алиас пути к хранилищу миниатюр изображений, которые модуль создает "на лету" по запросу и кеширует;
105 | - `token_salt` - уникальная соль для безопасной работы виджета загрузки файлов.
106 |
107 | ## Использование
108 |
109 | ### Настройка модели ActiveRecord
110 |
111 | Для добавление к модели ActiveRecord одного или нескольких полей с файлами, необходимо подключить к
112 | ней `floor12\files\components\FileBehaviour'` и перечислить названия полей, которые будут хранить файлы. Например, для
113 | модели User здесь будут определены 2 поля для хранения файлов: `avatar` и `documents`:
114 |
115 | ```php
116 | public function behaviors()
117 | {
118 | return [
119 | 'files' => [
120 | 'class' => 'floor12\files\components\FileBehaviour',
121 | 'attributes' => [
122 | 'avatar',
123 | 'documents'
124 | ],
125 | ],
126 | ...
127 | ```
128 |
129 | Чтобы отобразить красивые названия для полей, прописываем для них лейблы, как будто это обычные поля модели:
130 |
131 | ```php
132 | public function attributeLabels()
133 | {
134 | return [
135 | ...
136 | 'avatar' => 'Аватар',
137 | 'documents' => 'Документы',
138 | ...
139 | ];
140 | }
141 | ```
142 |
143 | В методе `rules()` описываем правила валидации для наших файловых полей:
144 |
145 | ```php
146 | public function rules()
147 | {
148 | return [
149 | //Аватар является обязательным полем
150 | ['avatar', 'required'],
151 |
152 | //В поле Аватар можно поместить 1 файл с разрешением 'jpg', 'png', 'jpeg', 'gif'
153 | ['avatar', 'file', 'extensions' => ['jpg', 'png', 'jpeg', 'gif'], 'maxFiles' => 1],
154 |
155 | //А в поле documents можно помещать несколько документов в формате MS Word или Excel
156 | ['documents', 'file', 'extensions' => ['docx', 'xlsx'], 'maxFiles' => 10],
157 | ...
158 | ```
159 |
160 | ### Обращение к файлам
161 |
162 | Если `maxFiles` в `FileValidator` будет равен единице, то поле модели будет хранить экземпляр
163 | класса `floor12\files\models\File`. Например:
164 |
165 | ```php
166 | // Поле href хранит в себе ссылку на исходник файла или картинки
167 | echo Html::img($model->avatar->href)
168 |
169 | //Такая запись равнозначна, так как объект File при приведении к строке возвращает href
170 | echo Html::img($model->avatar)
171 | ```
172 |
173 | Если файл является изображением, то можно запросить его миниатюру, передав в специальный метод ширину, высоту и флаг
174 | конвертации в WEBP:
175 |
176 | `File::getPreviewWebPath(int $width = 0, int $height = 0 ,bool $webp = false)`
177 |
178 | Пример использования:
179 |
180 | ```php
181 | //Запрашиваем миниатюру аватара пользователя шириной 200 пикселей
182 | echo Html::img($model->avatar->getPreviewWebPath(200));
183 |
184 | //Запрашиваем миниатюру аватара пользователя шириной 200 пикселей и в формате WEBP
185 | echo Html::img($model->avatar->getPreviewWebPath(200, true));
186 |
187 | ```
188 |
189 | В случае, если `maxFiles > 1` и файлов можно загрузить несколько, то поле с файлами будет содержать не экземпляр
190 | объекта `floor12 \files\models\File`, а массив объектов:
191 |
192 | ```php
193 | foreach ($model->docs as $doc}
194 | Html::a($doc->title, $doc->href);
195 | ```
196 |
197 | Вот еще один пример продвинутого использования миниатюр. В данном варианте мы используем современные теги `picture`
198 | и `source`, а так же медиа-запросы. В результате, у нас имеется 8 миниатюр, 4 в формате webp для тех браузерв, которые
199 | поддерживают этот формат, а 4 в формате jpeg. Кроме того, устройствам с ретиной будут показывать изображения с двойным
200 | разрешением. а обычным экранам - картинки обычного размера. Так же в примере используется разделение на разные
201 | изображения при разной ширине экрана:
202 |
203 | ```php
204 |
205 |
208 |
211 |
214 |
219 |
220 | ```
221 |
222 | ### Виджет для тега Picture
223 |
224 | Если объект типа `File` является изображением (`$file->isImage() === true`), то для него можно
225 | использовать `PictureWidget`. Этот виджет поможет в несколько строк кода сгенерировать тег picture с набором source и
226 | srcset на базе заданных параметров. Например:
227 |
228 | ```php
229 | echo PictureWidget::widget([
230 | 'model' => $file,
231 | 'alt' => 'Some alternative text',
232 | 'width' => 100
233 | ]);
234 | ```
235 |
236 | сгенерирует следующий html код:
237 |
238 | ```html
239 |
240 |
241 |
243 |
246 |
247 | ```
248 |
249 | Дополнительный параметры можно посмотреть в исходном коде виджета.
250 |
251 | ### Виджет для списка файлов
252 |
253 | В поставке модуля, есть виджет для вывода всех файлов, который дает возможность просматривать список файлов конкретного
254 | поля, подключая для его отображения галерею [Lightbox2](https://lokeshdhakar.com/projects/lightbox2/) если в списке есть
255 | картинки, осуществлять предпросмотр файлов MS Office, а так же имеется возможно скачать все приложенные к модели файлы
256 | ZIP-архивом.
257 |
258 | ```php
259 | echo \floor12\files\components\FileListWidget::widget([
260 | 'files' => $model->docs,
261 | 'downloadAll' => true,
262 | 'zipTitle' => "Документы пользователя {$model->fullname}"
263 | ])
264 | ```
265 |
266 | В виджет необходимо передать массив объектов `File`, а так же можно задать дополнительные параметры:
267 |
268 | - `title` - опционально задать заголовок блока (по-умолчанию берется из AttributeLabels)",
269 | - `downloadAll` - показать кнопку "скачать все файлы",
270 | - `zipTitle` - задать название файла для zip-архива,
271 | - `passFirst` - пропустить при выводе первый файл (часто бывает необходимо вывести галерею без первой картинки,
272 | например, в новости, так как первая картинка "ушла" на обложку самой новости - как пример).
273 |
274 | 
275 |
276 | ### Виджет для ActiveForm
277 |
278 | Во время редактирования модели, необходимо использовать виджет `floor12\files\components\FileInputWidget`:
279 |
280 | ```php
281 | = $form->field($model, 'avatar')->widget(floor12\files\components\FileInputWidget::class) ?>
282 | ```
283 |
284 | При этом, в зависимости того. установлен ли параметр `maxFiles` в `FileValidator` равный единицы или
285 | более, `FileInputWidget` примет необходимый вид, для загрузки одного файла или сразу нескольких. При необходимости можно
286 | передать в виджет текст и для кнопки и класс для кнопки загрузки через параметры `uploadButtonText`
287 | и `uploadButtonClass`.
288 |
289 | ## Участие в разработке
290 |
291 | Буду рад любой помощи в разработке, поддержке и баг-репортах на этот модуль.
--------------------------------------------------------------------------------
/assets/yii2-floor12-files-block.css:
--------------------------------------------------------------------------------
1 | div.files-block a,
2 | div.files-block a:hover {
3 | text-decoration: none; }
4 | div.files-block > label {
5 | display: block;
6 | margin-bottom: 2px; }
7 | div.files-block > ul {
8 | display: flex;
9 | flex-wrap: wrap;
10 | padding: 0;
11 | list-style: none; }
12 | div.files-block > ul li {
13 | margin: 0 10px 10px 0;
14 | position: relative; }
15 | div.files-block > ul li a.f12-file-object {
16 | margin: 0 10px 10px 0;
17 | width: 70px;
18 | height: 70px;
19 | overflow: hidden;
20 | position: relative;
21 | border-radius: 3px;
22 | display: flex;
23 | background-size: cover;
24 | flex-direction: column;
25 | background-color: #e9e9e9;
26 | align-items: center; }
27 | div.files-block > ul li a.f12-file-object svg {
28 | width: 50px;
29 | height: 38px !important;
30 | min-height: 38px;
31 | margin: 5px 0 0 0; }
32 | div.files-block > ul li a.f12-file-object span {
33 | overflow: hidden;
34 | text-overflow: ellipsis;
35 | font-size: 8px;
36 | margin: 3px 5px;
37 | text-align: center;
38 | overflow-wrap: break-word;
39 | max-width: 65px; }
40 | div.files-block > ul li a.files-download-btn {
41 | position: absolute;
42 | top: 0;
43 | right: 10px;
44 | padding: 1px 5px;
45 | background: #ffffffc4;
46 | z-index: 10;
47 | border-bottom-left-radius: 3px; }
48 | div.files-block > ul li a.f12-files-btn-download-all {
49 | border: 0;
50 | background: #e9e9e9;
51 | padding: 4px 10px;
52 | border-radius: 2px;
53 | margin: 30px 0 0 0;
54 | display: block; }
55 |
56 | /*# sourceMappingURL=yii2-floor12-files-block.css.map */
57 |
--------------------------------------------------------------------------------
/assets/yii2-floor12-files-block.css.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "mappings": "AACE;uBACQ;EACN,eAAe,EAAE,IAAI;AAGvB,uBAAQ;EACN,OAAO,EAAE,KAAK;EACd,aAAa,EAAE,GAAG;AAGpB,oBAAK;EACH,OAAO,EAAE,IAAI;EACb,SAAS,EAAE,IAAI;EACf,OAAO,EAAE,CAAC;EACV,UAAU,EAAE,IAAI;EAEhB,uBAAG;IACD,MAAM,EAAE,aAAa;IACrB,QAAQ,EAAE,QAAQ;IAElB,yCAAkB;MAChB,MAAM,EAAE,aAAa;MACrB,KAAK,EAAE,IAAI;MACX,MAAM,EAAE,IAAI;MACZ,QAAQ,EAAE,MAAM;MAChB,QAAQ,EAAE,QAAQ;MAClB,aAAa,EAAE,GAAG;MAClB,OAAO,EAAE,IAAI;MACb,eAAe,EAAE,KAAK;MACtB,cAAc,EAAE,MAAM;MACtB,gBAAgB,EAAE,OAAO;MACzB,WAAW,EAAE,MAAM;MAEnB,6CAAI;QACF,KAAK,EAAE,IAAI;QACX,MAAM,EAAE,eAAe;QACvB,UAAU,EAAE,IAAI;QAChB,MAAM,EAAE,SAAS;MAGnB,8CAAK;QACH,QAAQ,EAAE,MAAM;QAChB,aAAa,EAAE,QAAQ;QACvB,SAAS,EAAE,GAAG;QACd,MAAM,EAAE,OAAO;QACf,UAAU,EAAE,MAAM;QAClB,aAAa,EAAE,UAAU;QACzB,SAAS,EAAE,IAAI;IAInB,4CAAqB;MACnB,QAAQ,EAAE,QAAQ;MAClB,GAAG,EAAE,CAAC;MACN,KAAK,EAAE,IAAI;MACX,OAAO,EAAE,OAAO;MAChB,UAAU,EAAE,SAAS;MACrB,OAAO,EAAE,EAAE;MACX,yBAAyB,EAAE,GAAG;IAGhC,oDAA6B;MAC3B,MAAM,EAAE,CAAC;MACT,UAAU,EAAE,OAAO;MACnB,OAAO,EAAE,QAAQ;MACjB,aAAa,EAAE,GAAG;MAClB,MAAM,EAAE,UAAU;MAClB,OAAO,EAAE,KAAK",
4 | "sources": ["yii2-floor12-files-block.scss"],
5 | "names": [],
6 | "file": "yii2-floor12-files-block.css"
7 | }
8 |
--------------------------------------------------------------------------------
/assets/yii2-floor12-files-block.js:
--------------------------------------------------------------------------------
1 | function filesDownloadAll(title, event, yiiDownloadAllLink) {
2 | obj = $(event.target).parents('div.files-block');
3 | hashes = "";
4 | $.each(obj.find('.f12-file-object'), function (key, val) {
5 | hashes += "&hash[]=" + $(val).data('hash');
6 | });
7 | console.log(hashes);
8 |
9 | window.open(yiiDownloadAllLink + "?title=" + title + hashes);
10 |
11 | }
--------------------------------------------------------------------------------
/assets/yii2-floor12-files-block.scss:
--------------------------------------------------------------------------------
1 | div.files-block {
2 | a,
3 | a:hover {
4 | text-decoration: none;
5 | }
6 |
7 | > label {
8 | display: block;
9 | margin-bottom: 2px;
10 | }
11 |
12 | > ul {
13 | display: flex;
14 | flex-wrap: wrap;
15 | padding: 0;
16 | list-style: none;
17 |
18 | li {
19 | margin: 0 10px 10px 0;
20 | position: relative;
21 |
22 | a.f12-file-object {
23 | margin: 0 10px 10px 0;
24 | width: 70px;
25 | height: 70px;
26 | overflow: hidden;
27 | position: relative;
28 | border-radius: 3px;
29 | display: flex;
30 | background-size: cover;
31 | flex-direction: column;
32 | background-color: #e9e9e9;
33 | align-items: center;
34 |
35 | svg {
36 | width: 50px;
37 | height: 38px !important;
38 | min-height: 38px;
39 | margin: 5px 0 0 0;
40 | }
41 |
42 | span {
43 | overflow: hidden;
44 | text-overflow: ellipsis;
45 | font-size: 8px;
46 | margin: 3px 5px;
47 | text-align: center;
48 | overflow-wrap: break-word;
49 | max-width: 65px;
50 | }
51 | }
52 |
53 | a.files-download-btn {
54 | position: absolute;
55 | top: 0;
56 | right: 10px;
57 | padding: 1px 5px;
58 | background: #ffffffc4;
59 | z-index: 10;
60 | border-bottom-left-radius: 3px;
61 | }
62 |
63 | a.f12-files-btn-download-all {
64 | border: 0;
65 | background: #e9e9e9;
66 | padding: 4px 10px;
67 | border-radius: 2px;
68 | margin: 30px 0 0 0;
69 | display: block;
70 | }
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/assets/yii2-floor12-files.css:
--------------------------------------------------------------------------------
1 |
2 | div.floor12-files-widget-block {
3 | background-color: #fcfcfc;
4 | padding: 6px 4px 0 7px;
5 | border: 1px solid #e3e3e3;
6 | border-radius: 4px;
7 | transition: all 0.3s;
8 | margin: -2px -5px;
9 | }
10 |
11 | div.floor12-files-widget-block button.btn-upload {
12 | float: left;
13 | height: 60px;
14 | margin: 0 10px 8px 0;
15 | transition: all 0.3s;
16 | width: 80px;
17 | background-color: #c0c0c0;
18 | color: #ffffff;
19 | background: linear-gradient(45deg, rgb(151, 151, 151) 0%, rgb(184, 184, 184) 92%);
20 | border: none;
21 | }
22 |
23 | div.floor12-files-widget-single-block button.btn-upload {
24 | transition: all 0.3s;
25 | color: #ffffff;
26 | background: #c0c0c0 linear-gradient(45deg, rgb(151, 151, 151) 0%, rgb(184, 184, 184) 92%);
27 | border: none;
28 | padding: 10px;
29 | border-radius: 2px;
30 | }
31 |
32 | div.floor12-files-widget-block button.btn-upload div.icon {
33 | display: block;
34 | font-size: 20px;
35 | }
36 |
37 | div.floor12-files-widget-block-drug-over {
38 | background-color: #fbfffd;
39 | border: 1px #357025 dotted;
40 |
41 | }
42 |
43 | div.floor12-file-object {
44 | width: 80px;
45 | height: 60px;
46 | margin: 0 10px 8px 0;
47 | background-color: #ebebeb;
48 | border-radius: 4px;
49 | float: left;
50 | text-align: center;
51 | font-size: 60%;
52 | cursor: pointer;
53 | /* transition: all 0.2s; */
54 | text-overflow: ellipsis;
55 | overflow: hidden;
56 | font-weight: 600;
57 | white-space: nowrap;
58 | padding: 0 4px;
59 | position: relative;
60 | }
61 |
62 | div.floor12-file-object .floor12-file-percents {
63 | position: absolute;
64 | width: 90%;
65 | text-align: center;
66 | top: 22px;
67 | font-size: 120%;
68 | color: #666;
69 | font-weight: bold;
70 | }
71 |
72 | div.floor12-file-object svg.progress-circle {
73 | transform: rotate(-90deg);
74 | }
75 |
76 | div.floor12-file-object svg circle {
77 | transition: stroke-dashoffset 0.5s linear;
78 | }
79 |
80 | div.floor12-file-object div.file-icon {
81 | font-size: 360%;
82 | display: block;
83 | margin: 0;
84 | color: #787878;
85 | }
86 |
87 | div.floor12-file-object div.file-icon svg {
88 | width: 1em;
89 | height: 1em;
90 | margin: 6px 0 0 0;
91 | vertical-align: -.125em;
92 | }
93 |
94 |
95 | div.floor12-file-object-image {
96 | background-size: cover;
97 | background-position: center;
98 | border: 1px #e1e1e1 solid;
99 | }
100 |
101 | div.floor12-file-object-uploading {
102 | background-position: center;
103 | background-repeat: no-repeat;
104 | font-size: 130%;
105 | text-align: center;
106 | line-height: 62px;
107 | width: 74px;
108 | position: absolute;
109 | height: 59px;
110 | margin-top: 3px;
111 | }
112 |
113 | div.floor12-file-object-bar {
114 | width: 100%;
115 | background: #b0b0b0;
116 | height: 6px;
117 | border-radius: 2px;
118 | }
119 |
120 | ul.dropdown-menu-file-object-multi {
121 | margin-top: -20px;
122 | margin-left: -41px;
123 | }
124 |
125 | ul.dropdown-menu-file-object-single {
126 | top: 0;
127 | }
128 |
129 | ul.dropdown-menu-file-object > li > a {
130 | padding: 3px 10px;
131 | }
132 |
133 | ul.dropdown-menu-file-object > li > a > svg {
134 | width: 18px;
135 | }
136 |
137 | div.floor12-file-object:hover {
138 | box-shadow: 1px 1px 16px #00000063;
139 | }
140 |
141 | div.floor12-file-object:hover a.btn-file-object-delete {
142 | opacity: 1;
143 | }
144 |
145 | div.floor12-files-widget-single-block div.files-btn-group {
146 | width: 100%;
147 | }
148 |
149 | div.floor12-single-file-ratio {
150 | width: 100%;
151 | height: 0;
152 | padding-bottom: 70%;
153 | border-radius: 3px;
154 | }
155 |
156 | div#cropperModal {
157 | z-index: 999999999;
158 | }
159 |
160 | #cropperArea {
161 | width: 100%;
162 | }
163 |
164 | #cropperArea img {
165 | width: 100%;
166 | max-height: 600px;
167 | max-width: 100%;
168 | }
169 |
170 | #yii2-file-title-editor {
171 | position: absolute;
172 | width: 100%;
173 | max-width: 420px;
174 | border-radius: 3px;
175 | box-shadow: 0 1px 4px #00000024;
176 | display: none;
177 | z-index: 999999999;
178 | }
179 |
180 | #yii2-file-alt-editor {
181 | position: absolute;
182 | width: 100%;
183 | max-width: 420px;
184 | border-radius: 3px;
185 | box-shadow: 0 1px 4px #00000024;
186 | display: none;
187 | z-index: 999999999;
188 | }
189 |
190 | .input-group-btn .btn {
191 | padding-bottom: 7px;
192 | }
193 |
194 | div.floor12-single-file-object-no-image {
195 | padding: 5px 12px;
196 | border-radius: 2px;
197 | transition: all 0.3s;
198 | background-color: #c0c0c0;
199 | color: #ffffff;
200 | background: linear-gradient(45deg, rgb(151, 151, 151) 0%, rgb(184, 184, 184) 92%);
201 | border: none;
202 | }
203 |
204 | div.floor12-files-widget-list-multi {
205 | min-height: 68px;
206 | }
207 |
208 | /*progress*/
209 |
210 |
--------------------------------------------------------------------------------
/assets/yii2-floor12-files.js:
--------------------------------------------------------------------------------
1 | console.log('Yii2 files model init.');
2 |
3 |
4 | var currentCroppingImageId;
5 | var currentRenamingFileId;
6 | var cropper;
7 | var removeFileOnCropCancel;
8 | var yii2CropperRoute;
9 | var yii2UploadRoute;
10 | var uploaderSettings = {};
11 |
12 | $(document).on('change', '.yii2-files-upload-field', function () {
13 |
14 | obj = $(this);
15 |
16 | var formData = new FormData();
17 | formData.append('file', obj[0].files[0]);
18 | formData.append('modelClass', obj.data('modelclass'));
19 | formData.append('attribute', obj.data('attribute'));
20 | formData.append('mode', obj.data('mode'));
21 | formData.append('ratio', obj.data('ratio'));
22 | formData.append('name', obj.data('name'));
23 | formData.append('count', 1);
24 | formData.append('_fileFormToken', yii2FileFormToken);
25 |
26 |
27 | $.ajax({
28 | url: yii2UploadRoute,
29 | type: 'POST',
30 | data: formData,
31 | processData: false, // tell jQuery not to process the data
32 | contentType: false, // tell jQuery not to set contentType
33 | success: function (response) {
34 | id = '#files-widget-block_' + obj.data('block');
35 | $(response).appendTo(id).find('div.floor12-files-widget-list');
36 | }
37 | });
38 | });
39 |
40 |
41 | function clipboard(text) {
42 | //based on https://stackoverflow.com/a/12693636
43 | document.oncopy = function (event) {
44 | event.clipboardData.setData("Text", text);
45 | event.preventDefault();
46 | };
47 | document.execCommand("Copy");
48 | document.oncopy = undefined;
49 | f12notification.info(text, 1);
50 | }
51 |
52 | function updateProgressCircle(val, btnGroup) {
53 | result = 169.646 * (1 - val / 100);
54 | btnGroup.querySelector('svg #progress-circle').setAttribute('stroke-dashoffset', result);
55 | btnGroup.querySelector('.floor12-file-percents').innerHTML = val + '%';
56 | // setAttribute('stroke-dashoffset', result);
57 | }
58 |
59 | var observer = new MutationObserver(function (mutations) {
60 | percent = mutations[0].target.style.width.replace('%', '');
61 | btnGroup = mutations[0].target.parentElement.parentElement;
62 | updateProgressCircle(percent, btnGroup);
63 | });
64 |
65 | var lastUploader = null;
66 |
67 | function Yii2FilesUploaderSet(id, className, attribute, scenario, name) {
68 |
69 | var mode = 'multi';
70 | var blockName = "#" + id;
71 | var block = $(blockName);
72 | var uploadButton = block.find('button.btn-upload')[0];
73 | var filesList = block.find('.floor12-files-widget-list')[0];
74 | var ratio = 0;
75 |
76 | var csrf = block.parents('form').find('input[name=' + yii2CsrfParam + ']').val();
77 |
78 | if (block.data('ratio'))
79 | ratio = block.data('ratio');
80 |
81 | if (block.hasClass('floor12-files-widget-single-block')) {
82 | mode = 'single';
83 | toggleSingleUploadButton(block);
84 | }
85 |
86 | uploaderSettings[id] = {
87 | modelClass: className,
88 | attribute: attribute,
89 | scenario: scenario,
90 | mode: mode,
91 | name: name,
92 | ratio: ratio,
93 | count: block.find('.floor12-files-widget-list .floor12-file-object').length,
94 | _fileFormToken: yii2FileFormToken
95 | }
96 |
97 | uploaderSettings[id][yii2CsrfParam] = csrf
98 | console.log(uploaderSettings[id]);
99 | var uploader = new ss.SimpleUpload({
100 | button: uploadButton,
101 | url: yii2UploadRoute,
102 | name: 'file',
103 | dropzone: block,
104 | dragClass: 'floor12-files-widget-block-drug-over',
105 | multiple: true,
106 | multipleSelect: true,
107 | data: uploaderSettings[id],
108 | onSubmit: function (filename, extension, uploadBtn, size) {
109 | uploaderSettings[id].count++;
110 | var svg = '\t';
114 |
115 | var fileId = generateId(filename);
116 | var btnGroup = document.createElement('div');
117 | var fileObject = document.createElement('div');
118 | var bar = document.createElement('div');
119 | var percents = document.createElement('div');
120 | btnGroup.setAttribute('id', fileId);
121 | btnGroup.className = 'btn-group files-btn-group';
122 | fileObject.className = 'floor12-file-object';
123 | percents.className = 'floor12-file-percents';
124 | this.setProgressBar(bar);
125 |
126 | fileObject.innerHTML = svg;
127 |
128 | observer.observe(bar, {
129 | attributes: true
130 | });
131 |
132 | fileObject.appendChild(bar);
133 | fileObject.appendChild(percents);
134 | btnGroup.appendChild(fileObject);
135 |
136 | if (mode == 'single') {
137 | $(filesList).html('');
138 | }
139 | $(filesList).append(btnGroup);
140 | },
141 | onComplete: function (filename, response) {
142 | if (!response) {
143 | console.log(filename + 'upload failed');
144 | uploaderSettings[id].count--;
145 | return false;
146 | }
147 | f12notification.info(FileUploadedText, 1);
148 | idName = "#" + generateId(filename);
149 | $(idName).replaceWith($(response));
150 | if (mode == 'single')
151 | toggleSingleUploadButton(block);
152 | },
153 | onError: function (filename, errorType, status, statusText, response, uploadBtn, fileSize) {
154 | console.log(uploaderSettings[id]);
155 | uploaderSettings[id].count--;
156 | data = {
157 | responseText: response,
158 | status: status,
159 | statusText: statusText,
160 | };
161 | processError(data);
162 | idName = "#" + generateId(filename);
163 | $(idName).remove();
164 | }
165 |
166 | // progressUrl: 'uploadProgress.php', // enables cross-browser progress support (more info below)
167 | // responseType: 'json',
168 | // allowedExtensions: ['jpg', 'jpeg', 'png', 'gif'],
169 | // maxSize: 1024, // kilobytes
170 | // hoverClass: 'ui-state-hover',
171 | // focusClass: 'ui-state-focus',
172 | // disabledClass: 'ui-state-disabled',
173 | });
174 | lastUploader = uploader;
175 | }
176 |
177 | function generateId(filename) {
178 | return 'id-' + filename.replace(/[^0-9a-zA-Z]/g, "");
179 | }
180 |
181 | function showUploadButton(event) {
182 | obj = $(event.target);
183 | obj.parents('div.floor12-files-widget-single-block').find('button').show();
184 | }
185 |
186 | function toggleSingleUploadButton(block) {
187 | if (block.find('div.floor12-single-file-object').length > 0)
188 | block.find('button').hide();
189 | else
190 | block.find('button').show();
191 | }
192 |
193 | function sortableFiles() {
194 | $(".floor12-files-widget-list-multi").sortable({
195 | opacity: 0.5,
196 | revert: 1,
197 | items: "div.files-btn-group",
198 | connectWith: ".floor12-files-widget-list-multi"
199 | });
200 | }
201 |
202 | function removeFile(id) {
203 | id = "#yii2-file-object-" + id;
204 | const blockId = $(id).parents('.files-widget-block').attr('id');
205 |
206 | $(id).parents('div.files-btn-group').fadeOut(200, function () {
207 | $(this).remove();
208 | f12notification.info(FileRemovedText, 1);
209 | });
210 |
211 | uploaderSettings[blockId].count--;
212 | return false;
213 | }
214 |
215 | function removeAllFiles(event) {
216 | console.log('removeAllFiles');
217 | $(event.target).parents('div.floor12-files-widget-list').find('div.files-btn-group').fadeOut(200, function () {
218 | $(this).remove();
219 | });
220 | const blockId = $(event.target).parents('.floor12-files-widget-block').attr('id');
221 | uploaderSettings[blockId].count = 0;
222 | f12notification.info(FilesRemovedText, 1);
223 | return false;
224 | }
225 |
226 | function initCropperLayout() {
227 | if (yii2CropperRoute.length > 0)
228 | $.get(yii2CropperRoute, function (response) {
229 | $('body').append(response);
230 |
231 | $('#yii2-file-title-editor input').on('keyup', function (e) {
232 | if (e.keyCode == 13) {
233 | saveFileTitle()
234 | }
235 | });
236 | })
237 | }
238 |
239 | function initCropper(id, url, ratio, remove) {
240 | $('#cropperModal').modal({keyboard: false, backdrop: 'static'});
241 |
242 | currentCroppingImageId = id;
243 |
244 | removeFileOnCropCancel = false;
245 | if (remove)
246 | removeFileOnCropCancel = true;
247 |
248 | currentCropImage = $('
').attr('src', url);
249 | $('#cropperArea').html("");
250 | $('#cropperArea').append(currentCropImage);
251 |
252 | autoCrop = false;
253 | aspectRatio = NaN;
254 | $('#cropper-btn-cancel').show();
255 |
256 | console.log(cropperHideCancel);
257 | if (cropperHideCancel == 'true') {
258 | $('#cropper-btn-cancel').hide();
259 | }
260 |
261 |
262 | if (ratio) {
263 | autoCrop = true;
264 | aspectRatio = ratio;
265 | $('.cropper-ratio-btn-group').hide();
266 | }
267 |
268 | setTimeout(function () {
269 | cropper = currentCropImage.cropper({
270 | viewMode: 1,
271 | background: false,
272 | zoomable: false,
273 | autoCrop: autoCrop,
274 | aspectRatio: aspectRatio,
275 | });
276 | }, 1000)
277 | }
278 |
279 | function stopCrop(id) {
280 | $('#cropperModal').modal('hide');
281 | if (removeFileOnCropCancel)
282 | removeFile(currentCroppingImageId);
283 | }
284 |
285 | function cropImage() {
286 | сropBoxData = cropper.cropper('getCropBoxData');
287 | imageData = cropper.cropper('getImageData');
288 | canvasData = cropper.cropper('getCanvasData');
289 | ratio = imageData.height / imageData.naturalHeight;
290 | cropLeft = (сropBoxData.left - canvasData.left) / ratio;
291 | cropTop = (сropBoxData.top - canvasData.top) / ratio;
292 | cropWidth = сropBoxData.width / ratio;
293 | cropHeight = сropBoxData.height / ratio;
294 | rotated = imageData.rotate;
295 |
296 | data = {
297 | id: currentCroppingImageId,
298 | width: cropWidth,
299 | height: cropHeight,
300 | top: cropTop,
301 | left: cropLeft,
302 | rotated: rotated,
303 | _fileFormToken: yii2FileFormToken
304 | };
305 |
306 | removeFileOnCropCancel = false;
307 |
308 | $.ajax({
309 | url: yii2CropRoute,
310 | 'method': 'POST',
311 | data: data,
312 | success: function (response) {
313 | id = '#yii2-file-object-' + currentCroppingImageId;
314 | if ($(id).find('img').length)
315 | $(id).find('img').attr('src', response);
316 | else {
317 | $(id).css('background-image', 'none');
318 | $(id).css('background-image', 'url(' + response + ')');
319 | }
320 | stopCrop();
321 | f12notification.info(FileSavedText, 1);
322 | },
323 | error: function (response) {
324 | processError(response);
325 | }
326 | })
327 | }
328 |
329 | function showRenameFileForm(id, event) {
330 | var blockId = '#yii2-file-object-' + id;
331 | var title = $(blockId).attr('title');
332 | currentRenamingFileId = id;
333 | $('#yii2-file-title-editor').css('top', event.clientY).css('left', event.clientX - 70).fadeIn(100);
334 | $('#yii2-file-title-editor input').val(title).focus();
335 | }
336 |
337 | function showAltForm(id, event) {
338 | var blockId = '#yii2-file-object-' + id;
339 | var alt = $(blockId).data('alt');
340 | console.log(alt);
341 | currentRenamingFileId = id;
342 | $('#yii2-file-alt-editor').css('top', event.clientY).css('left', event.clientX - 70).fadeIn(100);
343 | $('#yii2-file-alt-editor input').val(alt).focus();
344 | }
345 |
346 | function hideYii2FileTitleEditor() {
347 | $('#yii2-file-title-editor').fadeOut(100);
348 | currentRenamingFileId = null;
349 | }
350 |
351 | function hideYii2FileAltEditor() {
352 | $('#yii2-file-alt-editor').fadeOut(100);
353 | currentRenamingFileId = null;
354 | }
355 |
356 | function saveFileTitle() {
357 | $('#yii2-file-title-editor').fadeOut(100);
358 | val = $('#yii2-file-title-editor input').val();
359 | blockId = '#yii2-file-object-' + currentRenamingFileId;
360 | $(blockId).attr('title', val);
361 | $(blockId).attr('data-title', val);
362 |
363 | $.ajax({
364 | url: yii2RenameRoute,
365 | method: 'POST',
366 | data: {id: currentRenamingFileId, title: val, _fileFormToken: yii2FileFormToken},
367 | success: function () {
368 | f12notification.info(FileRenamedText, 1);
369 | },
370 | error: function (response) {
371 | processError(response);
372 | }
373 | }
374 | );
375 | currentRenamingFileId = null;
376 | }
377 |
378 | function saveFileAlt() {
379 | $('#yii2-file-alt-editor').fadeOut(100);
380 | val = $('#yii2-file-alt-editor input').val();
381 | blockId = '#yii2-file-object-' + currentRenamingFileId;
382 | $(blockId).data('alt', val)
383 |
384 | $.ajax({
385 | url: yii2AltRoute,
386 | method: 'POST',
387 | data: {id: currentRenamingFileId, alt: val, _fileFormToken: yii2FileFormToken},
388 | success: function () {
389 | f12notification.info(FileRenamedText, 1);
390 | },
391 | error: function (response) {
392 | processError(response);
393 | }
394 | }
395 | );
396 | currentRenamingFileId = null;
397 | }
398 |
399 | $(document).ready(function () {
400 | setInterval(function () {
401 | sortableFiles()
402 | }, 2000);
403 |
404 | sortableFiles();
405 |
406 | initCropperLayout();
407 |
408 | });
409 |
410 |
411 |
--------------------------------------------------------------------------------
/assets/yii2-floor12-lightbox-params.js:
--------------------------------------------------------------------------------
1 | lightbox.option({
2 | 'resizeDuration': 50,
3 | 'fadeDuration': 50,
4 | 'imageFadeDuration': 50,
5 | });
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "floor12/yii2-module-files",
3 | "description": "Yii2 module to upload and manage files to your models.",
4 | "type": "yii2-extension",
5 | "minimum-stability": "stable",
6 | "keywords": [
7 | "floor12",
8 | "yii2 module",
9 | "files",
10 | "upload",
11 | "yii",
12 | "yii2",
13 | "yii 2"
14 | ],
15 | "license": "MIT",
16 | "authors": [
17 | {
18 | "name": "floor12",
19 | "email": "floor12@floor12.net",
20 | "role": "Developer"
21 | }
22 | ],
23 | "require": {
24 | "php": ">=7.3",
25 | "yiisoft/yii2": "^2.0.",
26 | "ext-gd": "*",
27 | "ext-zip": "*",
28 | "ext-exif": "*",
29 | "yiisoft/yii2-jui": "*",
30 | "yii2mod/yii2-enum": "^1.7",
31 | "floor12/yii2-notification": "*",
32 | "bower-asset/cropper": "*",
33 | "bower-asset/lightbox2": "*",
34 | "bower-asset/simple-ajax-uploader": ">=2.6.7",
35 | "ext-fileinfo": "*"
36 | },
37 | "require-dev": {
38 | "phpunit/phpunit": ">=7.0"
39 | },
40 | "autoload": {
41 | "psr-4": {
42 | "floor12\\files\\": "src"
43 | }
44 | },
45 | "autoload-dev": {
46 | "psr-4": {
47 | "floor12\\files\\tests\\": "tests"
48 | }
49 | },
50 | "repositories": [
51 | {
52 | "type": "composer",
53 | "url": "https://asset-packagist.org"
54 | }
55 | ]
56 | }
57 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 | ./tests
11 |
12 |
13 |
14 |
15 | ./src/
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Module.php:
--------------------------------------------------------------------------------
1 | 'db'];
66 | /**
67 | * @var Connection
68 | */
69 | public $db;
70 |
71 | /**
72 | * @inheritdoc
73 | */
74 | public function init()
75 | {
76 | $this->registerTranslations();
77 | $this->db = Yii::$app->{$this->params['db']};
78 | $this->storageFullPath = Yii::getAlias($this->storage);
79 | $this->cacheFullPath = Yii::getAlias($this->cache);
80 | }
81 |
82 | /**
83 | * @return void
84 | */
85 | public function registerTranslations()
86 | {
87 | $i18n = Yii::$app->i18n;
88 | $i18n->translations['files'] = [
89 | 'class' => 'yii\i18n\PhpMessageSource',
90 | 'sourceLanguage' => 'en-US',
91 | 'basePath' => '@vendor/floor12/yii2-module-files/src/messages',
92 | ];
93 | }
94 |
95 | }
--------------------------------------------------------------------------------
/src/actions/GetFileAction.php:
--------------------------------------------------------------------------------
1 | $hash]);
17 |
18 | if (!$model)
19 | throw new NotFoundHttpException("Запрашиваемый файл не найден");
20 |
21 | if (!file_exists($model->rootPath))
22 | throw new NotFoundHttpException('Запрашиваемый файл не найден на диске.');
23 |
24 | Yii::$app->response->headers->set('Last-Modified', date("c", $model->created));
25 | Yii::$app->response->headers->set('Cache-Control', 'public, max-age=' . (60 * 60 * 24 * 15));
26 |
27 | if ($model->type == FileType::IMAGE && $model->watermark) {
28 | $image = new SimpleImage();
29 | $image->load($model->rootPath);
30 | $image->watermark($model->watermark);
31 | $res = $image->output(IMAGETYPE_JPEG);
32 | Yii::$app->response->sendContentAsFile($res, $model->title, ['inline' => true, 'mimeType' => "image/jpeg", 'filesize' => $model->size]);
33 | } else {
34 | $stream = fopen($model->rootPath, 'rb');
35 | Yii::$app->response->sendStreamAsFile($stream, $model->title, ['inline' => true, 'mimeType' => $model->content_type, 'filesize' => $model->size]);
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/actions/GetPreviewAction.php:
--------------------------------------------------------------------------------
1 | loadAndCheckModel($hash);
35 | $this->width = $width;
36 |
37 | if ($width &&
38 | $this->model->content_type !== 'image/svg+xml' &&
39 | $this->model->content_type !== 'image/svg') {
40 | $this->sendPreview($width, $webp);
41 | } else {
42 | $this->sendAsIs();
43 | }
44 | }
45 |
46 | /**
47 | * @param string $hash
48 | * @throws NotFoundHttpException
49 | */
50 | private function loadAndCheckModel(string $hash): void
51 | {
52 | $this->model = File::findOne(['hash' => $hash]);
53 | if (!$this->model)
54 | throw new NotFoundHttpException("Запрашиваемый файл не найден");
55 |
56 | if (!$this->model->isImage() && !$this->model->isVideo())
57 | throw new NotFoundHttpException("Запрашиваемый файл не является изображением");
58 |
59 | if (!file_exists($this->model->rootPath))
60 | throw new NotFoundHttpException('Запрашиваемый файл не найден на диске.');
61 | }
62 |
63 | /**
64 | * @param $width
65 | * @throws InvalidConfigException
66 | * @throws NotFoundHttpException
67 | * @throws RangeNotSatisfiableHttpException
68 | */
69 | protected function sendPreview($width, $webp)
70 | {
71 | $filename = Yii::createObject(ImagePreviewer::class, [$this->model, $width, $webp])->getUrl();
72 |
73 | if (!file_exists($filename))
74 | throw new NotFoundHttpException('Запрашиваемый файл не найден на диске.');
75 |
76 | $response = Yii::$app->response;
77 | $response->format = Response::FORMAT_RAW;
78 | $coontentType = mime_content_type($filename);
79 | $this->setHeaders($response, $coontentType, md5($this->model->created));
80 | $stream = fopen($filename, 'rb');
81 | Yii::$app->response->sendStreamAsFile($stream, $this->model->title, [
82 | 'inline' => true,
83 | 'mimeType' => $this->model->content_type,
84 | 'filesize' => $this->model->size
85 | ]);
86 | }
87 |
88 | /**
89 | * @param $response
90 | * @param string $contentType
91 | * @param string $etag
92 | */
93 | private function setHeaders($response, string $contentType, string $etag): void
94 | {
95 | $response->headers->set('Last-Modified', date("c", $this->model->created));
96 | $response->headers->set('Cache-Control', 'public, max-age=' . self::HEADER_CACHE_TIME);
97 | $response->headers->set('Content-Type', $contentType . '; charset=utf-8');
98 | $response->headers->set('ETag', $etag);
99 | }
100 |
101 | /**
102 | * @throws RangeNotSatisfiableHttpException
103 | */
104 | protected function sendAsIs()
105 | {
106 | $stream = fopen($this->model->rootPath, 'rb');
107 | Yii::$app->response->sendStreamAsFile($stream, $this->model->title, [
108 | 'inline' => true,
109 | 'mimeType' => $this->model->content_type,
110 | 'filesize' => $this->model->size
111 | ]);
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/assets/CropperAsset.php:
--------------------------------------------------------------------------------
1 | ';
15 | const TRASH = '';
16 | const PLUS = '';
17 | const CROP = '';
18 | const RENAME = '';
19 | const VIEW = '';
20 | const DOWNLOAD = '';
21 | const EXCLAMATION = '';
22 | const LINK = '';
23 |
24 | const FILE = '';
25 | const FILE_WORD = '';
26 | const FILE_EXCEL = '';
27 | const FILE_POWERPOINT = '';
28 | const FILE_ARCHIVE = '';
29 | const FILE_AUDIO = '';
30 | const FILE_PDF = '';
31 | const FILE_VIDEO = '';
32 |
33 | const SAVE = '';
34 | const CLOSE = '';
35 | const ARROW_LEFT = '';
36 | const ARROW_RIGHT = '';
37 | const ROTATE_LEFT = '';
38 | const ROTATE_RIGHT = '';
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/src/assets/LightboxAsset.php:
--------------------------------------------------------------------------------
1 | 'filesSave',
40 | ActiveRecord::EVENT_AFTER_UPDATE => 'filesSave',
41 | ActiveRecord::EVENT_AFTER_DELETE => 'filesDelete',
42 | ActiveRecord::EVENT_BEFORE_VALIDATE => 'validateRequiredFields'
43 | ];
44 | }
45 |
46 | protected $cachedFiles = [];
47 |
48 |
49 | /**
50 | * Метод сохранения в базу связей с файлами. Вызывается после сохранения основной модели AR.
51 | * @throws ErrorException
52 | * @throws \yii\db\Exception
53 | */
54 |
55 | public function filesSave()
56 | {
57 | $order = 0;
58 | if ($this->_values) {
59 |
60 | foreach ($this->_values as $field => $ids) {
61 |
62 | Yii::$app->db->createCommand()->update(
63 | "{{%file}}",
64 | ['object_id' => 0],
65 | [
66 | 'class' => $this->owner->className(),
67 | 'object_id' => $this->owner->id,
68 | 'field' => $field,
69 | ]
70 | )->execute();
71 |
72 | if ($ids) foreach ($ids as $id) {
73 | if (empty($id))
74 | continue;
75 | $file = File::findOne($id);
76 | if ($file) {
77 | $file->object_id = $this->owner->id;
78 | $file->ordering = $order++;
79 | $file->save();
80 | if (!$file->save()) {
81 | throw new ErrorException('Невозможно обновить объект File.');
82 | }
83 | }
84 |
85 | }
86 | }
87 | }
88 | }
89 |
90 | public function filesDelete()
91 | {
92 | File::deleteAll([
93 | 'class' => $this->owner->className(),
94 | 'object_id' => $this->owner->id,
95 | ]);
96 | }
97 |
98 | public function validateRequiredFields()
99 | {
100 | foreach ($this->attributes as $attributeName => $params) {
101 | $attributeIds = $this->getRealAttributeName($attributeName);
102 |
103 | if (
104 | isset($params['required']) &&
105 | $params['required'] &&
106 | in_array($this->owner->scenario, $params['requiredOn']) &&
107 | !in_array($this->owner->scenario, $params['requiredExcept']) &&
108 | !isset($this->_values[$attributeIds][1])
109 | )
110 | $this->owner->addError($attributeName, $params['requiredMessage']);
111 | }
112 | }
113 |
114 | /**
115 | * Устанавливаем валидаторы.
116 | * @param ActiveRecord $owner
117 | */
118 | public
119 | function attach($owner)
120 | {
121 | parent::attach($owner);
122 |
123 | // Получаем валидаторы AR
124 | $validators = $owner->validators;
125 |
126 | // Пробегаемся по валидаторам и вычисляем, какие из них касаются наших файл-полей
127 | if ($validators)
128 | foreach ($validators as $key => $validator) {
129 |
130 | // Сначала пробегаемся по файловым валидаторам
131 | if ($validator::className() == 'yii\validators\FileValidator' || $validator::className() == 'floor12\files\components\ReformatValidator') {
132 | foreach ($this->attributes as $field => $params) {
133 |
134 | if (is_string($params)) {
135 | $field = $params;
136 | $params = [];
137 | }
138 |
139 | $index = array_search($field, $validator->attributes);
140 | if ($index !== false) {
141 | $this->attributes[$field]['validator'][$validator::className()] = $validator;
142 | unset($validator->attributes[$index]);
143 | }
144 | }
145 | }
146 |
147 |
148 | if ($validator::className() == 'yii\validators\RequiredValidator') {
149 | foreach ($this->attributes as $field => $params) {
150 |
151 | if (is_string($params)) {
152 | $field = $params;
153 | $params = [];
154 | }
155 |
156 | $index = array_search($field, $validator->attributes);
157 | if ($index !== false) {
158 | unset($validator->attributes[$index]);
159 | $this->attributes[$field]['required'] = true;
160 | $this->attributes[$field]['requiredExcept'] = $validator->except;
161 | $this->attributes[$field]['requiredOn'] = sizeof($validator->on) ? $validator->on : [ActiveRecord::SCENARIO_DEFAULT];
162 | $this->attributes[$field]['requiredMessage'] = str_replace("{attribute}", $this->owner->getAttributeLabel($field), $validator->message);
163 | }
164 | }
165 | }
166 |
167 |
168 | }
169 |
170 | // Добавляем дефолтный валидатор для прилетающих айдишников
171 | if ($this->attributes) foreach ($this->attributes as $fieldName => $fieldParams) {
172 | $validator = Validator::createValidator('safe', $owner, ["{$fieldName}_ids"]);
173 | $validators->append($validator);
174 | }
175 | }
176 |
177 |
178 | /**
179 | * @inheritdoc
180 | */
181 | public function canGetProperty($name, $checkVars = true)
182 | {
183 | return array_key_exists($name, $this->attributes) ?
184 | true : parent::canGetProperty($name, $checkVars);
185 | }
186 |
187 |
188 | /**
189 | * @inheritdoc
190 | */
191 | public function canSetProperty($name, $checkVars = true)
192 | {
193 | if (array_key_exists($this->getRealAttributeName($name), $this->attributes))
194 | return true;
195 |
196 | return parent::canSetProperty($name, $checkVars = true);
197 | }
198 |
199 |
200 | /**
201 | * @inheritdoc
202 | */
203 | public function __get($att_name)
204 | {
205 | if (isset($this->_values[$att_name])) {
206 | unset($this->_values[$att_name][0]);
207 | if (sizeof($this->_values[$att_name]))
208 | return array_map(function ($fileId) {
209 | return File::findOne($fileId);
210 | }, $this->_values[$att_name]);
211 | } else {
212 | if (!isset($this->cachedFiles[$att_name])) {
213 | if (
214 | isset($this->attributes[$att_name]['validator']) &&
215 | isset($this->attributes[$att_name]['validator']['yii\validators\FileValidator']) &&
216 | $this->attributes[$att_name]['validator']['yii\validators\FileValidator']->maxFiles > 1
217 | )
218 | $this->cachedFiles[$att_name] = File::find()
219 | ->where(
220 | [
221 | 'object_id' => $this->owner->id,
222 | 'field' => $att_name,
223 | 'class' => $this->owner->className()
224 | ])
225 | ->orderBy('ordering ASC')
226 | ->all();
227 | else {
228 | $this->cachedFiles[$att_name] = File::find()
229 | ->where(
230 | [
231 | 'object_id' => $this->owner->id,
232 | 'field' => $att_name,
233 | 'class' => $this->owner->className()
234 | ])
235 | ->orderBy('ordering ASC')
236 | ->one();
237 | }
238 | }
239 | return $this->cachedFiles[$att_name];
240 | }
241 | }
242 |
243 |
244 | /**
245 | * @inheritdoc
246 | */
247 | public
248 | function __set($name, $value)
249 | {
250 | $attribute = $this->getRealAttributeName($name);
251 |
252 | if (array_key_exists($attribute, $this->attributes))
253 | $this->_values[$attribute] = $value;
254 | }
255 |
256 |
257 | /** Отбрасываем постфикс _ids
258 | * @param $attribute string
259 | * @return string
260 | */
261 | private
262 | function getRealAttributeName($attribute)
263 | {
264 | return str_replace("_ids", "", $attribute);
265 | }
266 | }
267 |
--------------------------------------------------------------------------------
/src/components/FileInputWidget.php:
--------------------------------------------------------------------------------
1 | registerTranslations();
38 | $this->block_id = rand(9999999, 999999999);
39 |
40 | if (!$this->uploadButtonText)
41 | $this->uploadButtonText = Yii::t('files', 'Upload');
42 |
43 | $this->ratio = $this->model->getBehavior('files')->attributes[$this->attribute]['ratio'] ?? null;
44 |
45 | if (
46 | isset($this->model->behaviors['files']->attributes[$this->attribute]['validator']) &&
47 | isset($this->model->behaviors['files']->attributes[$this->attribute]['validator']['yii\validators\FileValidator']) &&
48 | $this->model->behaviors['files']->attributes[$this->attribute]['validator']['yii\validators\FileValidator']->maxFiles > 1
49 | ) {
50 | $mode = self::MODE_MULTI;
51 | $this->layout = self::VIEW_MULTI;
52 | }
53 |
54 | parent::init();
55 | }
56 |
57 | public function registerTranslations()
58 | {
59 | $i18n = Yii::$app->i18n;
60 | $i18n->translations['files'] = [
61 | 'class' => 'yii\i18n\PhpMessageSource',
62 | 'sourceLanguage' => 'en-US',
63 | 'basePath' => '@vendor/floor12/yii2-module-files/src/messages',
64 | ];
65 | }
66 |
67 | public function run()
68 | {
69 |
70 | $uploadRoute = Url::toRoute(['/files/default/upload']);
71 | $deleteRoute = Url::toRoute(['/files/default/delete']);
72 | $cropperRoute = Url::toRoute(['/files/default/cropper']);
73 | $cropRoute = Url::toRoute(['/files/default/crop']);
74 | $renameRoute = Url::toRoute(['/files/default/rename']);
75 | $altRoute = Url::toRoute(['/files/default/alt']);
76 |
77 | $className = new ClassnameEncoder($this->model->classname());
78 |
79 | $this->getView()->registerJs("Yii2FilesUploaderSet('files-widget-block_{$this->block_id}','{$className}','{$this->attribute}','{$this->model->scenario}','{$this->name}')", View::POS_READY, $this->block_id);
80 | $this->getView()->registerJs("yii2UploadRoute = '{$uploadRoute}'", View::POS_BEGIN, 'yii2UploadRoute');
81 | $this->getView()->registerJs("yii2CsrfParam = '" . Yii::$app->request->csrfParam . "'", View::POS_BEGIN, 'yii2CsrfFieldName');
82 | $this->getView()->registerJs("yii2DeleteRoute = '{$deleteRoute}'", View::POS_BEGIN, 'yii2DeleteRoute');
83 | $this->getView()->registerJs("yii2CropperRoute = '{$cropperRoute}'", View::POS_BEGIN, 'yii2DeleteRoute');
84 | $this->getView()->registerJs("yii2CropRoute = '{$cropRoute}'", View::POS_BEGIN, 'yii2CropRoute');
85 | $this->getView()->registerJs("yii2RenameRoute = '{$renameRoute}'", View::POS_BEGIN, 'yii2RenameRoute');
86 | $this->getView()->registerJs("yii2AltRoute = '{$altRoute}'", View::POS_BEGIN, 'yii2AltRoute');
87 | $this->getView()->registerJs("yii2FileFormToken = '" . self::generateToken() . "'", View::POS_BEGIN, 'yii2FileFormToken');
88 | $this->getView()->registerJs("FileUploadedText = '" . Yii::t('files', 'The file is uploaded') . "'", View::POS_BEGIN, 'FileUploadedText');
89 | $this->getView()->registerJs("FileSavedText = '" . Yii::t('files', 'The file is saved') . "'", View::POS_BEGIN, 'FileSavedText');
90 | $this->getView()->registerJs("FileRemovedText = '" . Yii::t('files', 'The file is removed') . "'", View::POS_BEGIN, 'FileRemovedText');
91 | $this->getView()->registerJs("FilesRemovedText = '" . Yii::t('files', 'The files are removed') . "'", View::POS_BEGIN, 'FilesRemovedText');
92 | $this->getView()->registerJs("FileRenamedText = '" . Yii::t('files', 'The file is renamed') . "'", View::POS_BEGIN, 'FileRenamedText');
93 | $this->getView()->registerJs("cropperHideCancel = '{$this->cropperHideCancel}'", View::POS_BEGIN, 'cropperHideCancel');
94 |
95 | FileInputWidgetAsset::register($this->getView());
96 |
97 | return $this->render($this->layout, [
98 | 'className' => $this->model->classname(),
99 | 'uploadButtonText' => $this->uploadButtonText,
100 | 'uploadButtonClass' => $this->uploadButtonClass,
101 | 'block_id' => $this->block_id,
102 | 'scenario' => $this->model->scenario,
103 | 'attribute' => $this->attribute,
104 | 'model' => $this->model,
105 | 'ratio' => $this->ratio,
106 | 'name' => $this->name,
107 | 'value' => $this->value
108 | ]);
109 | }
110 |
111 | /** Генератор токена защиты форм
112 | * @return string
113 | */
114 | public static function generateToken()
115 | {
116 | return md5(Yii::$app->getModule('files')->token_salt . Yii::$app->request->userAgent . Yii::$app->name);
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/components/FileListWidget.php:
--------------------------------------------------------------------------------
1 | files))
34 | return null;
35 | Yii::$app->getModule('files')->registerTranslations();
36 | if (empty($this->lightboxKey)) {
37 | $this->lightboxKey = $this->files[0]->field . '-' . $this->files[0]->object_id;
38 | }
39 | parent::init();
40 | }
41 |
42 |
43 | /**
44 | * @return string|null
45 | */
46 | public function run()
47 | {
48 | FileListAsset::register($this->getView());
49 |
50 | $this->getView()->registerJs("yiiDownloadAllLink = '" . Url::toRoute('files/default/zip') . "'", View::POS_BEGIN, 'yiiDownloadAllLink');
51 |
52 | if ($this->passFirst && sizeof($this->files) > 0)
53 | $this->files = array_slice($this->files, 1);
54 |
55 |
56 | return $this->render('fileListWidget', [
57 | 'files' => $this->files,
58 | 'zipTitle' => $this->zipTitle,
59 | 'title' => $this->title,
60 | 'downloadAll' => $this->downloadAll,
61 | 'lightboxKey' => $this->lightboxKey,
62 | 'allowImageSrcDownload' => $this->allowImageSrcDownload,
63 | ]);
64 | }
65 | }
--------------------------------------------------------------------------------
/src/components/PictureListWidget.php:
--------------------------------------------------------------------------------
1 | models)) {
35 | return '';
36 | }
37 |
38 | $renderedPictures = [];
39 |
40 | if ($this->passFirst && sizeof($this->models) > 0)
41 | $this->models = array_slice($this->models, 1);
42 |
43 | $lightboxKey = $this->models[0]->field . '-' . $this->models[0]->object_id;
44 |
45 | if ($this->lightbox) {
46 | LightboxAsset::register($this->getView());
47 | }
48 |
49 | foreach ($this->models as $model) {
50 |
51 | $widget = PictureWidget::widget([
52 | 'model' => $model,
53 | 'width' => $this->width,
54 | 'classImg' => $this->classImg,
55 | 'classPicture' => $this->classPicture,
56 | 'alt' => $this->alt,
57 | ]);
58 |
59 | if ($this->lightbox) {
60 | $widget = Html::a($widget, $model->getPreviewWebPath($this->lightboxImageWidth), [
61 | 'data-lightbox' => $lightboxKey
62 | ]);
63 | }
64 |
65 | $renderedPictures[] = Html::tag(
66 | 'li',
67 | $widget, [
68 | 'class' => $this->classLi
69 | ]);
70 | }
71 | return Html::tag('ul', implode($renderedPictures), [
72 | 'class' => $this->classUl
73 | ]);
74 | }
75 | }
--------------------------------------------------------------------------------
/src/components/PictureWidget.php:
--------------------------------------------------------------------------------
1 | model) || !in_array($this->model->type, [FileType::IMAGE, FileType::VIDEO]))
32 | return null;
33 |
34 | if (is_array($this->width))
35 | $this->view = 'mediaPictureWidget';
36 |
37 | return $this->render($this->view, [
38 | 'model' => $this->model,
39 | 'width' => $this->width,
40 | 'alt' => $this->alt,
41 | 'classPicture' => $this->classPicture,
42 | 'classImg' => $this->classImg,
43 | ]);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/SimpleImage.php:
--------------------------------------------------------------------------------
1 | image_type = $image_info[2];
25 | if ($this->image_type == IMAGETYPE_JPEG) {
26 | $this->image = imagecreatefromjpeg($filename);
27 | } elseif ($this->image_type == IMAGETYPE_GIF) {
28 | $this->image = imagecreatefromgif($filename);
29 | imageSaveAlpha($this->image, true);
30 | } elseif ($this->image_type == IMAGETYPE_PNG) {
31 | $this->image = @imagecreatefrompng($filename); // https://stackoverflow.com/questions/22745076/libpng-warning-iccp-known-incorrect-srgb-profile
32 | imageSaveAlpha($this->image, true);
33 | } elseif ($this->image_type == IMAGETYPE_WEBP) {
34 | $this->image = imagecreatefromwebp($filename);
35 | }
36 | }
37 |
38 | function save($filename, $image_type = IMAGETYPE_JPEG, $compression = 75, $permissions = null)
39 | {
40 | if ($image_type == IMAGETYPE_JPEG) {
41 | imagejpeg($this->image, $filename, $compression);
42 | } elseif ($image_type == IMAGETYPE_GIF) {
43 | imagegif($this->image, $filename);
44 | } elseif ($image_type == IMAGETYPE_PNG) {
45 | imagepng($this->image, $filename);
46 | } elseif ($image_type == IMAGETYPE_WEBP) {
47 | $dst = imagecreatetruecolor(imagesx($this->image), imagesy($this->image));
48 | imagealphablending($dst, false);
49 | imagesavealpha($dst, true);
50 | $transparent = imagecolorallocatealpha($dst, 255, 255, 255, 127);
51 | imagefilledrectangle($dst, 0, 0, imagesx($this->image), imagesy($this->image), $transparent);
52 | imagecopy($dst, $this->image, 0, 0, 0, 0, imagesx($this->image), imagesy($this->image));
53 | imagewebp($dst, $filename);
54 | }
55 |
56 | if ($permissions != null) {
57 | chmod($filename, $permissions);
58 | }
59 | }
60 |
61 | function output($image_type = IMAGETYPE_JPEG)
62 | {
63 | ob_start();
64 | if ($image_type == IMAGETYPE_JPEG) {
65 | imagejpeg($this->image);
66 | } elseif ($image_type == IMAGETYPE_GIF) {
67 | imagegif($this->image);
68 | } elseif ($image_type == IMAGETYPE_PNG) {
69 | imagepng($this->image);
70 | } elseif ($image_type == IMAGETYPE_WEBP) {
71 | $dst = imagecreatetruecolor(imagesx($this->image), imagesy($this->image));
72 | imagealphablending($dst, false);
73 | imagesavealpha($dst, true);
74 | $transparent = imagecolorallocatealpha($dst, 255, 255, 255, 127);
75 | imagefilledrectangle($dst, 0, 0, imagesx($this->image), imagesy($this->image), $transparent);
76 | imagecopy($dst, $this->image, 0, 0, 0, 0, imagesx($this->image), imagesy($this->image));
77 | imagewebp($dst);
78 | }
79 | return ob_get_clean();
80 | }
81 |
82 | function resizeToHeight($height)
83 | {
84 | $ratio = $height / $this->getHeight();
85 | $width = $this->getWidth() * $ratio;
86 | $this->resize($width, $height);
87 | }
88 |
89 | function getHeight()
90 | {
91 | try {
92 | return imagesy($this->image);
93 | } catch (\Throwable $exception) {
94 | throw new ErrorException('Unable to get height of image. Probably the image is corrupted.');
95 | }
96 |
97 | }
98 |
99 | function getWidth()
100 | {
101 | try {
102 | return imagesx($this->image);
103 | } catch (\Throwable $exception) {
104 | throw new ErrorException('Unable to get width of image. Probably the image is corrupted.');
105 | }
106 | }
107 |
108 | function resize($width, $height)
109 | {
110 | $new_image = imagecreatetruecolor($width, $height);
111 | imagealphablending($new_image, false);
112 | imagesavealpha($new_image, true);
113 | $transparent = imagecolorallocatealpha($new_image, 255, 255, 255, 127);
114 | imagefilledrectangle($new_image, 0, 0, $width, $height, $transparent);
115 | imagecopyresampled($new_image, $this->image, 0, 0, 0, 0, $width, $height, $this->getWidth(), $this->getHeight());
116 | $this->image = $new_image;
117 | }
118 |
119 | function resizeToWidth($width)
120 | {
121 | $ratio = $width / $this->getWidth();
122 | $height = $this->getheight() * $ratio;
123 | $this->resize((int)$width, (int)$height);
124 | }
125 |
126 | function scale($scale)
127 | {
128 | $width = $this->getWidth() * $scale / 100;
129 | $height = $this->getheight() * $scale / 100;
130 | $this->resize((int)$width, (int)$height);
131 | }
132 |
133 | function rotate($direction)
134 | {
135 | $degrees = 90;
136 | if ($direction == 2)
137 | $degrees = 270;
138 | $this->image = imagerotate($this->image, $degrees, 0);
139 | }
140 |
141 | function rotateDegrees($degrees)
142 | {
143 | $this->image = imagerotate($this->image, $degrees, 0);
144 | }
145 |
146 |
147 | public function watermark($path)
148 | {
149 | $stamp = imagecreatefrompng($path);
150 |
151 | $transparentStamp = imagecreatetruecolor($this->getWidth(), $this->getHeight());
152 | imagealphablending($transparentStamp, false);
153 | imagesavealpha($transparentStamp, true);
154 | $transparent = imagecolorallocatealpha($transparentStamp, 255, 255, 255, 127);
155 | imagecolortransparent($transparentStamp, $transparent);
156 | imagefilledrectangle($transparentStamp, 0, 0, $this->getWidth(), $this->getHeight(), $transparent);
157 | imagecopyresampled($transparentStamp, $stamp, 0, 0, 0, 0, $this->getWidth(), $this->getHeight(), $this->getWidth(), $this->getHeight());
158 |
159 | $newImage = imagecreatetruecolor($this->getWidth(), $this->getHeight());
160 | imagecopyresampled($newImage, $this->image, 0, 0, 0, 0, $this->getWidth(), $this->getHeight(), $this->getWidth(), $this->getHeight());
161 | imagecopyresampled($newImage, $transparentStamp, 0, 0, 0, 0, $this->getWidth(), $this->getHeight(), $this->getWidth(), $this->getHeight());
162 | $this->image = $newImage;
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/components/VideoWidget.php:
--------------------------------------------------------------------------------
1 | model->type !== FileType::VIDEO)
25 | return null;
26 | $source = Html::tag('source', null, ['src' => $this->model->getHref(), 'type' => $this->model->content_type]);
27 | return Html::tag('video', $source, $this->options);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/views/_fileListWidget.php:
--------------------------------------------------------------------------------
1 |
31 |
32 | type == FileType::IMAGE) { ?>
33 |
34 |
35 |
36 | = IconHelper::DOWNLOAD ?>
37 |
38 |
39 |
40 |
43 |
44 |
45 |
46 |
47 | getModule('files')->allowOfficePreview && in_array($model->content_type, $doc_contents)) { ?>
48 |
50 | = IconHelper::VIEW ?>
51 |
52 |
53 |
54 |
56 | = $model->icon ?>
57 | = $model->title ?>
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/src/components/views/fileListWidget.php:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
28 |
29 |
30 |
48 |
--------------------------------------------------------------------------------
/src/components/views/mediaPictureWidget.php:
--------------------------------------------------------------------------------
1 |
15 |
16 | >
17 | $widthValue) { ?>
18 |
24 |
25 | $widthValue) { ?>
26 |
32 |
33 |
>
34 |
35 |
--------------------------------------------------------------------------------
/src/components/views/multiFileInputWidget.php:
--------------------------------------------------------------------------------
1 | "files-upload-field-{$attribute}",
31 | 'class' => 'yii2-files-upload-field',
32 | 'data-modelclass' => $model::className(),
33 | 'data-attribute' => $attribute,
34 | 'data-mode' => 'multi',
35 | 'data-ratio' => $ratio ?? 0,
36 | 'data-block' => $block_id,
37 |
38 | ]) ?>
39 |
40 |
54 |
--------------------------------------------------------------------------------
/src/components/views/pictureWidget.php:
--------------------------------------------------------------------------------
1 |
14 |
15 | >
16 |
21 |
26 |
>
27 |
28 |
--------------------------------------------------------------------------------
/src/components/views/singleFileInputWidget.php:
--------------------------------------------------------------------------------
1 | "files-upload-field-{$attribute}",
32 | 'class' => 'yii2-files-upload-field',
33 | 'data-modelclass' => $model::className(),
34 | 'data-attribute' => $attribute,
35 | 'data-mode' => 'single',
36 | 'data-name' => $name ?: (new ReflectionClass($model))->getShortName() . "[{$attribute}_ids][]",
37 | 'data-ratio' => $ratio ?? 0,
38 | 'data-block' => $block_id,
39 |
40 | ]) ?>
41 |
42 |
58 |
--------------------------------------------------------------------------------
/src/controllers/ConsoleController.php:
--------------------------------------------------------------------------------
1 | where(['object_id' => '0'])->andWhere(['<', 'created', $time])->all();
35 | if ($files) foreach ($files as $file) {
36 | $file->delete();
37 | }
38 | }
39 |
40 | function actionClear()
41 | {
42 | $countDeleted = $countOk = 0;
43 | $module = Yii::$app->getModule('files');
44 | $path1 = $module->storageFullPath;
45 | foreach (scandir($path1) as $folder1) {
46 | $path2 = $path1 . '/' . $folder1;
47 | if ($this->checkFolderItem($folder1)) {
48 | continue;
49 | }
50 | foreach (scandir($path2) as $folder2) {
51 | $path3 = $path2 . '/' . $folder2;
52 | if ($this->checkFolderItem($folder2)) {
53 | continue;
54 | };
55 | foreach (scandir($path3) as $filename) {
56 | $path4 = $path3 . '/' . $filename;
57 | if ($this->checkFolderItem($filename)) {
58 | continue;
59 | };
60 | $dbFileName = "/{$folder1}/{$folder2}/{$filename}";
61 | if (is_file($path4)) {
62 | $this->stdout($path4 . "...");
63 | if (File::find()->where(['filename' => $dbFileName])->count() === 0) {
64 | $this->stdout('no' . PHP_EOL, Console::FG_RED);
65 | unlink($path4);
66 | $countDeleted++;
67 | } else {
68 | $this->stdout('ok' . PHP_EOL, Console::FG_GREEN);
69 | $countOk++;
70 | }
71 | }
72 | }
73 | }
74 | }
75 | $this->stdout('Deleted: ' . $countDeleted . PHP_EOL, Console::FG_YELLOW);
76 | $this->stdout('Ok: ' . $countOk . PHP_EOL, Console::FG_GREEN);
77 | }
78 |
79 | private function checkFolderItem($string)
80 | {
81 | $ignoreItems = ['.', '..', '.gitignore', 'summerfiles'];
82 | if (in_array($string, $ignoreItems)) {
83 | return true;
84 | }
85 | return false;
86 | }
87 |
88 |
89 | /**
90 | * Run `./yii files/console/clean-cache` to remove all generated images and previews
91 | */
92 | function actionCleanCache()
93 | {
94 | $module = Yii::$app->getModule('files');
95 | $commands = [];
96 | $commands[] = "find {$module->storageFullPath} -regextype egrep -regex \".+/.{32}_.*\" -exec rm -rf {} \;";
97 | $commands[] = "find {$module->cacheFullPath} -regextype egrep -regex \".+/.{32}_.*\" -exec rm -rf {} \;";
98 | $commands[] = "find {$module->storageFullPath} -regextype egrep -regex \".+/.{32}\..{3,4}\.jpg\" -exec rm -rf {} \;";
99 | $commands[] = "find {$module->cacheFullPath} -regextype egrep -regex \".+/.{32}\..{3,4}\.jpg\" -exec rm -rf {} \;";
100 |
101 | array_map(function ($command) {
102 | exec($command);
103 | }, $commands);
104 |
105 | }
106 |
107 | /**
108 | * Run `./yii files/console/convert` to proccess one video file from queue with ffmpeg
109 | * @return bool|int
110 | */
111 | function actionConvert()
112 | {
113 | $ffmpeg = Yii::$app->getModule('files')->ffmpeg;
114 |
115 | if (!file_exists($ffmpeg))
116 | return $this->stdout("ffmpeg is not found: {$ffmpeg}" . PHP_EOL, Console::FG_RED);
117 |
118 | if (!is_executable($ffmpeg))
119 | return $this->stdout("ffmpeg is not executable: {$ffmpeg}" . PHP_EOL, Console::FG_RED);
120 |
121 | $file = File::find()
122 | ->where(['type' => FileType::VIDEO, 'video_status' => VideoStatus::QUEUE])
123 | ->andWhere(['!=', 'object_id', 0])
124 | ->one();
125 |
126 | if (!$file)
127 | return $this->stdout("Convert queue is empty" . PHP_EOL, Console::FG_GREEN);
128 |
129 | if (!file_exists($file->rootPath))
130 | return $this->stdout("Source file is not found: {$file->rootPath}" . PHP_EOL, Console::FG_RED);
131 |
132 |
133 | $file->video_status = VideoStatus::CONVERTING;
134 | $file->save();
135 | $width = $this->getVideoWidth($file->class, $file->field);
136 | $height = $this->getVideoHeight($file->class, $file->field);
137 | $newFileName = $file->filename . ".mp4";
138 | $newFilePath = $file->rootPath . ".mp4";
139 | $command = Yii::$app->getModule('files')->ffmpeg . " -i {$file->rootPath} -vf scale={$width}:{$height} -threads 4 {$newFilePath}";
140 | echo $command . PHP_EOL;
141 | exec($command,
142 | $outout, $result);
143 | if ($result == 0) {
144 | @unlink($file->rootPath);
145 | $file->filename = $newFileName;
146 | $file->content_type = 'video/mp4';
147 | $file->video_status = VideoStatus::READY;
148 | } else {
149 | $file->video_status = VideoStatus::QUEUE;
150 | }
151 | $file->save();
152 |
153 | return $this->stdout("File converted: {$file->rootPath}" . PHP_EOL, Console::FG_GREEN);
154 | }
155 |
156 | protected
157 | function getVideoWidth($classname, $field)
158 | {
159 | /** @var ActiveRecord $ownerClassObject */
160 | $ownerClassObject = new $classname;
161 | return $ownerClassObject->getBehavior('files')->attributes[$field]['videoWidth'] ?? 1280;
162 | }
163 |
164 | protected
165 | function getVideoHeight($classname, $field)
166 | {
167 | /** @var ActiveRecord $ownerClassObject */
168 | $ownerClassObject = new $classname;
169 | return $ownerClassObject->getBehavior('files')->attributes[$field]['videoHeight'] ?? -1;
170 | }
171 |
172 |
173 | }
174 |
--------------------------------------------------------------------------------
/src/controllers/DefaultController.php:
--------------------------------------------------------------------------------
1 | [
48 | 'class' => VerbFilter::class,
49 | 'actions' => [
50 | 'zip' => ['GET', 'HEAD'],
51 | 'cropper' => ['GET', 'HEAD'],
52 | 'crop' => ['POST', 'HEAD'],
53 | 'rename' => ['POST', 'HEAD'],
54 | 'upload' => ['POST', 'HEAD'],
55 | 'get' => ['GET', 'HEAD'],
56 | 'preview' => ['GET', 'HEAD'],
57 | ],
58 | ],
59 | ];
60 | }
61 |
62 |
63 | /**
64 | * @inheritdoc
65 | */
66 | public function beforeAction($action)
67 | {
68 | $this->checkFormToken();
69 | return parent::beforeAction($action);
70 | }
71 |
72 | /** Првоеряем токен
73 | * @throws BadRequestHttpException
74 | */
75 | private function checkFormToken()
76 | {
77 | if (in_array($this->action->id, $this->actionsToCheck) && FileInputWidget::generateToken() != Yii::$app->request->post('_fileFormToken'))
78 | throw new BadRequestHttpException('File-form token is wrong or missing.');
79 | }
80 |
81 | /**
82 | * @param array $hash
83 | * @param string $title
84 | */
85 | public function actionZip(array $hash, $title = 'files')
86 | {
87 | $md5 = md5(serialize($hash));
88 | $files = File::find()->where(["IN", "hash", $hash])->all();
89 |
90 | $zip = new ZipArchive;
91 | $path = Yii::getAlias("@runtime/zip");
92 | if (!file_exists($path)) {
93 | mkdir($path, 0777, true);
94 | }
95 | $filename = "{$path}/{$md5}.zip";
96 | if (file_exists($filename))
97 | @unlink($filename);
98 | if (sizeof($files) && $zip->open($filename, ZipArchive::CREATE)) {
99 | foreach ($files as $file) {
100 | $zip->addFile($file->rootPath, $file->title);
101 | }
102 | $zip->close();
103 | return Yii::$app->response->sendFile($filename, "{$title}.zip");
104 | } else {
105 | throw new NotFoundHttpException("Error while zipping files.");
106 | }
107 | }
108 |
109 | /** Возвращаем HTML шаблон для внедрения в основной макет
110 | * @return string
111 | */
112 | public function actionCropper()
113 | {
114 | return $this->renderPartial('_cropper');
115 | }
116 |
117 | /** Кропаем и поворачиваем картинку, возращая ее новый адрес.
118 | * @return string
119 | * @throws InvalidConfigException
120 | * @throws \yii\base\ErrorException
121 | */
122 | public function actionCrop()
123 | {
124 | return Yii::createObject(FileCropRotate::class, [Yii::$app->request->post()])->execute();
125 | }
126 |
127 | /** Переименовываем файл
128 | * @return string
129 | * @throws BadRequestHttpException
130 | * @throws InvalidConfigException
131 | */
132 | public function actionRename()
133 | {
134 | return Yii::createObject(FileRename::class, [Yii::$app->request->post()])->execute();
135 | }
136 |
137 | /** Alt
138 | * @return string
139 | * @throws BadRequestHttpException
140 | * @throws InvalidConfigException
141 | */
142 | public function actionAlt()
143 | {
144 | return Yii::createObject(FileAlt::class, [Yii::$app->request->post()])->execute();
145 | }
146 |
147 |
148 | /** Создаем новый файл
149 | * @return string
150 | * @throws BadRequestHttpException
151 | * @throws InvalidConfigException
152 | */
153 | public function actionUpload()
154 | {
155 | $model = Yii::createObject(FileCreateFromInstance::class, [
156 | UploadedFile::getInstanceByName('file'),
157 | Yii::$app->request->post(),
158 | Yii::$app->user->identity,
159 | ])->execute();
160 |
161 |
162 | if ($model->errors) {
163 | throw new BadRequestHttpException('Ошибки валидации модели');
164 | }
165 |
166 | $ratio = Yii::$app->request->post('ratio') ?? null;
167 |
168 | $name = Yii::$app->request->post('name') ?? null;
169 |
170 | $view = Yii::$app->request->post('mode') == 'single' ? "_single" : "_file";
171 |
172 | if ($ratio)
173 | $this->getView()->registerJs("initCropper({$model->id}, '{$model->href}', {$ratio}, true);");
174 |
175 | return $this->renderAjax($view, [
176 | 'model' => $model,
177 | 'ratio' => $ratio,
178 | 'name' => $name
179 | ]);
180 | }
181 |
182 | /**
183 | * @return array|string[]
184 | */
185 | public function actions()
186 | {
187 | return [
188 | 'get' => GetFileAction::class,
189 | 'image' => GetPreviewAction::class
190 | ];
191 | }
192 |
193 | /*
194 | * Выдача файлов через контроллер.
195 | */
196 | public function actionPreview($hash)
197 | {
198 | $model = File::findOne(['hash' => $hash]);
199 |
200 | if (!$model)
201 | throw new NotFoundHttpException("Запрашиваемый файл не найден в базе.");
202 |
203 | $response = Yii::$app->response;
204 | $response->format = Response::FORMAT_RAW;
205 | $response->getHeaders()->set('Content-Type', 'image/jpeg; charset=utf-8');
206 |
207 | Yii::$app->response->headers->set('Last-Modified', date("c", $model->created));
208 | Yii::$app->response->headers->set('Cache-Control', 'public, max-age=' . (60 * 60 * 24 * 15));
209 |
210 | if (!file_exists($model->getRootPreviewPath()))
211 | throw new NotFoundHttpException('Preview not found.');
212 |
213 | $response->sendFile($model->getRootPreviewPath(), 'preview.jpg');
214 |
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/src/logic/ClassnameEncoder.php:
--------------------------------------------------------------------------------
1 | encoded = str_replace("\\", "\\\\", $className);
29 | }
30 |
31 | /**
32 | * @return mixed|string
33 | */
34 | public function __toString()
35 | {
36 | return $this->encoded;
37 | }
38 | }
--------------------------------------------------------------------------------
/src/logic/FileAlt.php:
--------------------------------------------------------------------------------
1 | alt = $data['alt'];
37 |
38 | $this->_file = File::findOne($data['id']);
39 |
40 | if (!$this->_file)
41 | throw new NotFoundHttpException('File not found.');
42 |
43 | }
44 |
45 | /**
46 | * @return string
47 | * @throws BadRequestHttpException
48 | */
49 | public function execute()
50 | {
51 | $this->_file->alt = $this->alt;
52 |
53 | if (!$this->_file->save())
54 | throw new BadRequestHttpException('Unable to save file.');
55 |
56 | return $this->alt;
57 | }
58 | }
--------------------------------------------------------------------------------
/src/logic/FileCreateFromInstance.php:
--------------------------------------------------------------------------------
1 | _onlyUploaded = $onlyUploaded;
37 |
38 | if (!isset($data['attribute']) || !$data['attribute'] || !isset($data['modelClass']) || !$data['modelClass'])
39 | throw new BadRequestHttpException("Attribute or class name not set.");
40 |
41 | // Загружаем полученные данные
42 | $this->_instance = $file;
43 | $this->_attribute = $data['attribute'];
44 |
45 | if (!file_exists($this->_instance->tempName))
46 | throw new ErrorException("Tmp file not found on disk.");
47 |
48 | // Инициализируем класс владельца файла для валидаций и ставим сценарий
49 | $this->_owner = new $data['modelClass'];
50 |
51 | if (isset($data['scenario']))
52 | $this->_owner->setScenario($data['scenario']);
53 |
54 |
55 | if (isset($this->_owner->behaviors['files']->attributes[$this->_attribute]['validator'])) {
56 | foreach ($this->_owner->behaviors['files']->attributes[$this->_attribute]['validator'] as $validator) {
57 | if ($validator->maxFiles && (int)$data['count'] > $validator->maxFiles) {
58 | throw new BadRequestHttpException('The maximum number of files has been exceeded: ' . $validator->maxFiles);
59 | }
60 |
61 | if (!$validator->validate($this->_instance, $error))
62 | throw new BadRequestHttpException($error);
63 | }
64 |
65 | }
66 |
67 | // Создаем модель нового файла и заполняем первоначальными данными
68 | $this->_model = new File();
69 | $this->_model->created = time();
70 | $this->_model->field = $this->_attribute;
71 | $this->_model->class = $data['modelClass'];
72 |
73 | $this->_model->filename = new PathGenerator(Yii::$app->getModule('files')->storageFullPath) . '.' . $this->_instance->extension;
74 | $this->_model->title = $this->_instance->name;
75 | $this->_model->content_type = \yii\helpers\FileHelper::getMimeType($this->_instance->tempName);
76 | $this->_model->size = $this->_instance->size;
77 | $this->_model->type = $this->detectType();
78 | if ($identity)
79 | $this->_model->user_id = $identity->getId();
80 | if ($this->_model->type == FileType::VIDEO)
81 | $this->_model->video_status = 0;
82 |
83 | //Генерируем полный новый адрес сохранения файла
84 | $this->_fullPath = Yii::$app->getModule('files')->storageFullPath . DIRECTORY_SEPARATOR . $this->_model->filename;
85 | }
86 |
87 | /**
88 | * @return string
89 | */
90 | public function detectType()
91 | {
92 | $contentTypeArray = explode('/', $this->_model->content_type);
93 | if ($contentTypeArray[0] == 'image')
94 | return FileType::IMAGE;
95 | if ($contentTypeArray[0] == 'video')
96 | return FileType::VIDEO;
97 | return FileType::FILE;
98 | }
99 |
100 | /**
101 | * @return File
102 | */
103 |
104 | public function execute()
105 | {
106 | $path = Yii::$app->getModule('files')->storageFullPath . $this->_model->filename;
107 |
108 | if ($this->_model->save()) {
109 | if (!$this->_onlyUploaded)
110 | copy($this->_instance->tempName, $this->_fullPath);
111 | else
112 | $this->_instance->saveAs($this->_fullPath, false);
113 | }
114 |
115 | if ($this->_model->type == FileType::IMAGE) {
116 | $this->rotateAfterUpload();
117 | $this->resizeAfterUpload();
118 | }
119 |
120 | return $this->_model;
121 | }
122 |
123 |
124 | protected function rotateAfterUpload()
125 | {
126 | $exif = '';
127 | @$exif = exif_read_data($this->_fullPath);
128 | if (isset($exif['Orientation'])) {
129 | $ort = $exif['Orientation'];
130 | $rotatingImage = new SimpleImage();
131 | $rotatingImage->load($this->_fullPath);
132 | switch ($ort) {
133 | case 3: // 180 rotate left
134 | $rotatingImage->rotateDegrees(180);
135 | $rotatingImage->save($this->_fullPath);
136 | break;
137 | case 6: // 90 rotate right
138 | $rotatingImage->rotateDegrees(270);
139 | $rotatingImage->save($this->_fullPath);
140 | break;
141 | case 8: // 90 rotate left
142 | $rotatingImage->rotateDegrees(90);
143 | $rotatingImage->save($this->_fullPath);
144 | }
145 |
146 | }
147 | }
148 |
149 | protected function resizeAfterUpload()
150 | {
151 | $maxWidth = $this->_owner->behaviors['files']->attributes[$this->_attribute]['maxWidth'] ?? 0;
152 | $maxHeight = $this->_owner->behaviors['files']->attributes[$this->_attribute]['maxHeight'] ?? 0;
153 |
154 | if ($maxWidth && $maxHeight) {
155 | $resizer = new FileResize($this->_model, $maxWidth, $maxHeight);
156 | $resizer->execute();
157 | }
158 |
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/logic/FileCreateFromPath.php:
--------------------------------------------------------------------------------
1 | model = $model;
31 |
32 | if (!$filePath || !$className || !$fieldName || !$storagePath)
33 | throw new ErrorException("Empty params not allowed.");
34 |
35 | if (!file_exists($storagePath))
36 | throw new ErrorException("File storage not found on disk.");
37 |
38 | if (!file_exists($filePath))
39 | throw new ErrorException("File not found on disk.");
40 |
41 | if (!is_writable($storagePath))
42 | throw new ErrorException("File storage is not writable.");
43 | $this->filePath = $filePath;
44 | $this->fileName = $fileName;
45 | $this->fieldName = $fieldName;
46 | $this->className = $className;
47 | $this->storagePath = $storagePath;
48 |
49 | }
50 |
51 | /** Основная работка
52 | * @return bool
53 | * @throws ErrorException
54 | */
55 | public function execute()
56 | {
57 | // копируем файл в хранилище
58 | $tmp_extansion = explode('?', pathinfo($this->filePath, PATHINFO_EXTENSION));
59 | $extansion = $tmp_extansion[0];
60 | $filename = new PathGenerator($this->storagePath) . "." . $extansion;
61 | $new_path = $this->storagePath . $filename;
62 | copy($this->filePath, $new_path);
63 |
64 | // создаем запись в базе
65 | $this->model->field = $this->fieldName;
66 | $this->model->class = $this->className;
67 | $this->model->filename = $filename;
68 | if ($this->model->filename)
69 | $this->model->title = $this->model->filename;
70 | else
71 | $this->model->title = rand(0, 99999); #такой прикол )
72 | $this->model->content_type = $this->model->mime_content_type($new_path);
73 | $this->model->type = $this->detectType();
74 | $this->model->size = filesize($new_path);
75 | $this->model->created = time();
76 | if ($this->model->type == FileType::VIDEO)
77 | $this->model->video_status = 0;
78 |
79 | if ($this->model->save()) {
80 |
81 | if ($this->model->type == FileType::IMAGE) {
82 | $exif = '';
83 | @$exif = exif_read_data($new_path);
84 | if (isset($exif['Orientation'])) {
85 | $ort = $exif['Orientation'];
86 | $rotatingImage = new SimpleImage();
87 | $rotatingImage->load($new_path);
88 | switch ($ort) {
89 |
90 | case 3: // 180 rotate left
91 | $rotatingImage->rotateDegrees(180);
92 | break;
93 | case 6: // 90 rotate right
94 | $rotatingImage->rotateDegrees(270);
95 | break;
96 | case 8: // 90 rotate left
97 | $rotatingImage->rotateDegrees(90);
98 | }
99 | $rotatingImage->save($new_path);
100 | }
101 | }
102 | return true;
103 | }
104 | return false;
105 | }
106 |
107 | /**
108 | * @return integer
109 | */
110 | private function detectType()
111 | {
112 | $contentTypeArray = explode('/', $this->model->content_type);
113 | if ($contentTypeArray[0] == 'image')
114 | return FileType::IMAGE;
115 | if ($contentTypeArray[0] == 'video')
116 | return FileType::VIDEO;
117 | return FileType::FILE;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/logic/FileCropRotate.php:
--------------------------------------------------------------------------------
1 | _file = File::findOne($data['id']);
32 |
33 | if (!$this->_file)
34 | throw new NotFoundHttpException('File not found');
35 |
36 | if ($this->_file->type != FileType::IMAGE)
37 | throw new BadRequestHttpException('Requested file is not an image.');
38 |
39 | if (!file_exists($this->_file->rootPath))
40 | throw new BadRequestHttpException('File not found in file storage.');
41 |
42 | $this->_height = (int)$data['height'];
43 | $this->_width = (int)$data['width'];
44 | $this->_top = (int)$data['top'];
45 | $this->_left = (int)$data['left'];
46 | $this->_rotated = (int)($data['rotated'] ?? 0);
47 |
48 | if (!$this->_height && !$this->_width) {
49 | list($this->_width, $this->_height) = getimagesize($this->_file->rootPath);
50 | }
51 |
52 | }
53 |
54 | public function execute()
55 | {
56 |
57 | $src = $this->imageCreateFromAny();
58 |
59 | $src = imagerotate($src, -$this->_rotated, 0);
60 |
61 | $dest = imagecreatetruecolor($this->_width, $this->_height);
62 |
63 | imagecopy($dest, $src, 0, 0, $this->_left, $this->_top, $this->_width, $this->_height);
64 |
65 | $newName = new PathGenerator(Yii::$app->getModule('files')->storageFullPath) . '.jpeg';
66 |
67 | $newPath = Yii::$app->getModule('files')->storageFullPath . '/' . $newName;
68 |
69 | $oldPath = $this->_file->rootPath;
70 |
71 | imagejpeg($dest, $newPath, 80);
72 |
73 | imagedestroy($dest);
74 |
75 | imagedestroy($src);
76 |
77 | $this->_file->filename = $newName;
78 | $this->_file->content_type = $this->_file->mime_content_type($newPath);
79 | $this->_file->size = filesize($newPath);
80 | $this->_file->changeHash();
81 | if ($this->_file->save()) {
82 | @unlink($oldPath);
83 | return $this->_file->href;
84 | } else
85 | throw new ErrorException("Error while saving file model.");
86 |
87 |
88 | }
89 |
90 |
91 | /**
92 | * Method to read files from any mime types
93 | * @return resource
94 | * @throws BadRequestHttpException
95 | */
96 |
97 | private function imageCreateFromAny()
98 | {
99 | $type = exif_imagetype($this->_file->rootPath);
100 | $allowedTypes = array(
101 | 1, // [] gif
102 | 2, // [] jpg
103 | 3, // [] png
104 | 6 // [] bmp
105 | );
106 | if (!in_array($type, $allowedTypes)) {
107 | throw new BadRequestHttpException('File must have GIF, JPG, PNG or BMP mime-type.');
108 |
109 | }
110 | switch ($type) {
111 | case 1 :
112 | $im = imageCreateFromGif($this->_file->rootPath);
113 | break;
114 | case 2 :
115 | $im = imageCreateFromJpeg($this->_file->rootPath);
116 | break;
117 | case 3 :
118 | $im = imageCreateFromPng($this->_file->rootPath);
119 | break;
120 | case 6 :
121 | $im = imageCreateFromBmp($this->_file->rootPath);
122 | break;
123 | }
124 | return $im;
125 | }
126 | }
--------------------------------------------------------------------------------
/src/logic/FileRename.php:
--------------------------------------------------------------------------------
1 | _title = $data['title'];
37 |
38 | $this->_file = File::findOne($data['id']);
39 |
40 | if (!$this->_file)
41 | throw new NotFoundHttpException('File not found.');
42 |
43 | }
44 |
45 | /**
46 | * @return string
47 | * @throws BadRequestHttpException
48 | */
49 | public function execute()
50 | {
51 | $this->_file->title = $this->_title;
52 |
53 | if (!$this->_file->save())
54 | throw new BadRequestHttpException('Unable to save file.');
55 |
56 | return $this->_title;
57 | }
58 | }
--------------------------------------------------------------------------------
/src/logic/FileResize.php:
--------------------------------------------------------------------------------
1 | _file = $file;
35 |
36 | if ($this->_file->type != FileType::IMAGE)
37 | throw new ErrorException('This file is not an image.');
38 |
39 | if (!file_exists($this->_file->rootPath))
40 | throw new ErrorException('File not found on disk');
41 |
42 | $this->_maxHeight = $maxHeight;
43 | $this->_maxWidth = $maxWidth;
44 | $this->_compression = $compression;
45 |
46 | }
47 |
48 |
49 | /** Непосредственная обработка
50 | * @return bool
51 | * @throws ErrorException
52 | */
53 | public function execute(): bool
54 | {
55 | if ($this->_file->content_type == 'image/svg+xml')
56 | return true;
57 |
58 | $image = new SimpleImage();
59 | $image->load($this->_file->rootPath);
60 |
61 | if ($image->getWidth() > $this->_maxWidth || $image->getHeight() > $this->_maxHeight) {
62 | $image->resizeToWidth($this->_maxWidth);
63 | if ($this->_file->content_type == 'image/png')
64 | $this->_imageType = IMAGETYPE_PNG;
65 | $image->save($this->_file->rootPath, $this->_imageType, $this->_compression);
66 | $this->_file->size = filesize($this->_file->rootPath);
67 | return $this->_file->save(false, ['size']);
68 | }
69 | return true;
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/src/logic/ImagePreviewer.php:
--------------------------------------------------------------------------------
1 | model = $model;
30 | $this->width = $width;
31 | $this->webp = $webp;
32 |
33 | if (!$this->model->isImage() && !$this->model->isVideo())
34 | throw new ErrorException('File is not an image or video.');
35 | }
36 |
37 | /**
38 | * @return string
39 | * @throws \ErrorException
40 | * @throws \yii\base\InvalidConfigException
41 | */
42 | public function getUrl()
43 | {
44 | if ($this->model->isSvg())
45 | return $this->model->getRootPath();
46 |
47 | $cachePath = Yii::$app->getModule('files')->cacheFullPath;
48 | $jpegName = $this->model->makeNameWithSize($this->model->filename, $this->width);
49 | $webpName = $this->model->makeNameWithSize($this->model->filename, $this->width, true);
50 |
51 | $this->fileName = $cachePath . DIRECTORY_SEPARATOR . $jpegName;
52 | $this->fileNameWebp = $cachePath . DIRECTORY_SEPARATOR . $webpName;
53 |
54 | $this->prepareFolder();
55 |
56 | $sourceImagePath = $this->model->rootPath;
57 | if ($this->model->isVideo()) {
58 | $sourceImagePath = $this->fileName . '.jpeg';
59 | if (!is_file($sourceImagePath))
60 | Yii::createObject(VideoFrameExtractor::class, [
61 | $this->model->rootPath,
62 | $sourceImagePath
63 | ])->extract();
64 | }
65 |
66 | if (!is_file($this->fileName) || filesize($this->fileName) == 0)
67 | $this->createPreview($sourceImagePath, $this->model->getWatermark());
68 |
69 | if ($this->webp && !file_exists($this->fileNameWebp))
70 | $this->createPreviewWebp();
71 |
72 | if ($this->webp)
73 | return $this->fileNameWebp;
74 |
75 | return $this->fileName;
76 | }
77 |
78 | /**
79 | * Generate all folders for storing image thumbnails cache.
80 | */
81 | protected function prepareFolder()
82 | {
83 | if (!file_exists(Yii::$app->getModule('files')->cacheFullPath))
84 | mkdir(Yii::$app->getModule('files')->cacheFullPath);
85 | $lastFolder = '/';
86 | $explodes = explode('/', $this->fileName);
87 | array_pop($explodes);
88 | if (empty($explodes))
89 | return;
90 | foreach ($explodes as $folder) {
91 | if (empty($folder))
92 | continue;
93 | $lastFolder = $lastFolder . $folder . '/';
94 | if (!file_exists($lastFolder))
95 | mkdir($lastFolder);
96 | }
97 | }
98 |
99 | /**
100 | * Creat JPG preview
101 | * @param $sourceImagePath
102 | * @throws ErrorException
103 | */
104 | protected function createPreview($sourceImagePath, $watermarkInPng = null)
105 | {
106 | $img = new SimpleImage();
107 | $img->load($sourceImagePath);
108 |
109 | if ($watermarkInPng)
110 | $img->watermark($watermarkInPng);
111 |
112 | $imgWidth = $img->getWidth();
113 | $imgHeight = $img->getHeight();
114 |
115 | if ($this->width && $this->width < $imgWidth) {
116 | $ratio = $this->width / $imgWidth;
117 | $img->resizeToWidth($this->width);
118 | }
119 |
120 | $saveType = $img->image_type;
121 | if ($saveType == IMG_WEBP || $saveType == IMG_QUADRATIC) {
122 | $saveType = IMG_JPEG;
123 | }
124 |
125 | $img->save($this->fileName, $saveType);
126 | }
127 |
128 |
129 | /**
130 | * Create webp from default preview
131 | */
132 | protected function createPreviewWebp()
133 | {
134 | $img = new SimpleImage();
135 | $img->load($this->fileName);
136 | $img->save($this->fileNameWebp, IMAGETYPE_WEBP, 70);
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/logic/PathGenerator.php:
--------------------------------------------------------------------------------
1 | path = $path1 . DIRECTORY_SEPARATOR . md5(rand(0, 1000) . time());
47 | }
48 |
49 | public function __toString()
50 | {
51 | return $this->path;
52 | }
53 | }
--------------------------------------------------------------------------------
/src/logic/VideoFrameExtractor.php:
--------------------------------------------------------------------------------
1 | ffmpegBin = Yii::$app->getModule('files')->ffmpeg;
21 |
22 | if (!file_exists($this->ffmpegBin))
23 | throw new ErrorException("ffmpeg is not found: {$this->ffmpegBin}");
24 |
25 | if (!is_executable($this->ffmpegBin))
26 | throw new ErrorException("ffmpeg is not executable: {$this->ffmpegBin}");
27 |
28 | if (!is_file($videoSourceFilename))
29 | throw new ErrorException('File not found on disk.');
30 |
31 | $this->videoSourceFilename = $videoSourceFilename;
32 | $this->outputImageFilename = $outputImageFilename;
33 | }
34 |
35 | /**
36 | * @throws ErrorException
37 | */
38 | public function extract()
39 | {
40 | $command = "{$this->ffmpegBin} -i {$this->videoSourceFilename} -ss 00:00:05.000 -vframes 1 {$this->outputImageFilename}";
41 | exec($command, $out, $result);
42 | if (is_file($this->outputImageFilename)) {
43 | return true;
44 | }
45 |
46 | $command = "{$this->ffmpegBin} -i {$this->videoSourceFilename} -ss 00:00:03.000 -vframes 1 {$this->outputImageFilename}";
47 | exec($command, $out, $result);
48 | if (is_file($this->outputImageFilename)) {
49 | return true;
50 | }
51 |
52 |
53 | $command = "{$this->ffmpegBin} -i {$this->videoSourceFilename} -ss 00:00:01.000 -vframes 1 {$this->outputImageFilename}";
54 | exec($command, $out, $result);
55 | if (is_file($this->outputImageFilename)) {
56 | return true;
57 | }
58 |
59 | throw new ErrorException('FFmpeg frame extracting fail:' . print_r($out, true));
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/src/messages/ru/files.php:
--------------------------------------------------------------------------------
1 | 'Редактирование изображения',
11 | 'Cancel' => ' Отмена',
12 | 'Save' => 'Сохранить',
13 | 'Upload' => 'Загрузить',
14 | 'Filename' => 'Имя файла',
15 | 'View' => 'Просмотреть',
16 | 'Rename' => 'Переименовать',
17 | 'Delete all' => 'Удалить все',
18 | 'Delete' => 'Удалить',
19 | 'Edit' => 'Редактировать',
20 | 'Download' => 'Скачать',
21 | 'Download all' => 'Скачать все',
22 | 'The file is removed' => 'Файл удален',
23 | 'The files are removed' => 'Файлы удалены',
24 | 'The file is uploaded' => 'Файл загружен',
25 | 'The file is saved' => 'Файл сохранен',
26 | 'The file is renamed' => 'Файл переименован',
27 | 'Copy link to clipboard' => 'Копировать ссылку',
28 | 'queued' => 'в очереди',
29 | 'converting' => 'конвертируется',
30 | 'ready' => 'готово',
31 | 'file' => 'файл',
32 | 'image' => 'изображение',
33 | 'video' => 'видео',
34 | ];
--------------------------------------------------------------------------------
/src/migrations/m180627_121715_files.php:
--------------------------------------------------------------------------------
1 | db->driverName === 'mysql') {
12 | $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB';
13 | }
14 |
15 | $this->createTable(
16 | '{{%file}}',
17 | [
18 | 'id' => $this->primaryKey(11),
19 | 'class' => $this->string(255)->notNull(),
20 | 'field' => $this->string(255)->notNull(),
21 | 'object_id' => $this->integer(11)->notNull()->defaultValue(0),
22 | 'title' => $this->string(255)->notNull(),
23 | 'filename' => $this->string(255)->notNull(),
24 | 'content_type' => $this->string(255)->notNull(),
25 | 'type' => $this->integer(1)->notNull(),
26 | 'video_status' => $this->integer(1)->null()->defaultValue(null),
27 | 'ordering' => $this->integer(11)->notNull()->defaultValue(0),
28 | 'created' => $this->integer(11)->notNull(),
29 | 'user_id' => $this->integer(11)->null(),
30 | 'size' => $this->integer(20)->notNull(),
31 | 'hash' => $this->string(255)->null(),
32 | ], $tableOptions
33 | );
34 | }
35 |
36 | public function safeDown()
37 | {
38 |
39 | $this->dropTable('{{%file}}');
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/migrations/m190803_101632_alter_file.php:
--------------------------------------------------------------------------------
1 | createIndex('file-class', '{{%file}}', 'class');
16 | $this->createIndex('file-object_id', '{{%file}}', 'object_id');
17 | $this->createIndex('file-field', '{{%file}}', 'field');
18 | $this->createIndex('file-type', '{{%file}}', 'type');
19 | $this->createIndex('file-hash', '{{%file}}', 'hash');
20 | $this->createIndex('file-filename', '{{%file}}', 'filename');
21 | }
22 |
23 | /**
24 | * {@inheritdoc}
25 | */
26 | public function safeDown()
27 | {
28 | echo "m190803_101632_alter_file cannot be reverted.\n";
29 |
30 | return false;
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/src/migrations/m230602_185211_add_alt_to_file.php:
--------------------------------------------------------------------------------
1 | addColumn('file', 'alt', $this->string(512)->null());
16 | }
17 |
18 | /**
19 | * {@inheritdoc}
20 | */
21 | public function safeDown(): void
22 | {
23 | $this->dropColumn('file', 'alt');
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/models/File.php:
--------------------------------------------------------------------------------
1 | getModule('files')->db;
51 | }
52 |
53 | /**
54 | * @inheritdoc
55 | */
56 |
57 | public static function tableName()
58 | {
59 | return '{{%file}}';
60 | }
61 |
62 | /**
63 | * Create hash if its empty
64 | * @param bool $insert
65 | * @return bool
66 | */
67 | public function beforeSave($insert)
68 | {
69 | if (!$this->hash) {
70 | $this->changeHash();
71 | }
72 | return parent::beforeSave($insert);
73 | }
74 |
75 | /**
76 | * Change object hash
77 | */
78 | public function changeHash()
79 | {
80 | $this->hash = md5(time() . rand(99999, 99999999));
81 |
82 | }
83 |
84 | /**
85 | * @return string
86 | */
87 | public function getIcon()
88 | {
89 | $icon = IconHelper::FILE;
90 |
91 | if ($this->content_type == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')
92 | $icon = IconHelper::FILE_WORD;
93 |
94 | if ($this->content_type == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
95 | $icon = IconHelper::FILE_EXCEL;
96 |
97 | if ($this->content_type == 'application/vnd.openxmlformats-officedocument.presentationml.presentation')
98 | $icon = IconHelper::FILE_POWERPOINT;
99 |
100 | if ($this->content_type == 'application/x-zip-compressed')
101 | $icon = IconHelper::FILE_ARCHIVE;
102 |
103 | if ($this->content_type == 'application/octet-stream')
104 | $icon = IconHelper::FILE_ARCHIVE;
105 |
106 | if (preg_match('/audio/', $this->content_type))
107 | $icon = IconHelper::FILE_AUDIO;
108 |
109 | if (preg_match('/pdf/', $this->content_type))
110 | $icon = IconHelper::FILE_PDF;
111 |
112 | if ($this->type == FileType::VIDEO)
113 | $icon = IconHelper::FILE_VIDEO;
114 |
115 | return $icon;
116 | }
117 |
118 | function mime_content_type($filename)
119 | {
120 | $idx = explode('.', $filename);
121 | $count_explode = count($idx);
122 | $idx = strtolower($idx[$count_explode - 1]);
123 |
124 | $mimet = array(
125 | 'txt' => 'text/plain',
126 | 'htm' => 'text/html',
127 | 'html' => 'text/html',
128 | 'php' => 'text/html',
129 | 'css' => 'text/css',
130 | 'js' => 'application/javascript',
131 | 'json' => 'application/json',
132 | 'xml' => 'application/xml',
133 | 'swf' => 'application/x-shockwave-flash',
134 | 'flv' => 'video/x-flv',
135 |
136 | // images
137 | 'png' => 'image/png',
138 | 'jpe' => 'image/jpeg',
139 | 'jpeg' => 'image/jpeg',
140 | 'jpg' => 'image/jpeg',
141 | 'gif' => 'image/gif',
142 | 'bmp' => 'image/bmp',
143 | 'ico' => 'image/vnd.microsoft.icon',
144 | 'tiff' => 'image/tiff',
145 | 'tif' => 'image/tiff',
146 | 'svg' => 'image/svg+xml',
147 | 'svgz' => 'image/svg+xml',
148 |
149 | // archives
150 | 'zip' => 'application/zip',
151 | 'rar' => 'application/x-rar-compressed',
152 | 'exe' => 'application/x-msdownload',
153 | 'msi' => 'application/x-msdownload',
154 | 'cab' => 'application/vnd.ms-cab-compressed',
155 |
156 | // audio/video
157 | 'mp3' => 'audio/mpeg',
158 | 'qt' => 'video/quicktime',
159 | 'mov' => 'video/quicktime',
160 |
161 | // adobe
162 | 'pdf' => 'application/pdf',
163 | 'psd' => 'image/vnd.adobe.photoshop',
164 | 'ai' => 'application/postscript',
165 | 'eps' => 'application/postscript',
166 | 'ps' => 'application/postscript',
167 |
168 | // ms office
169 | 'doc' => 'application/msword',
170 | 'rtf' => 'application/rtf',
171 | 'xls' => 'application/vnd.ms-excel',
172 | 'ppt' => 'application/vnd.ms-powerpoint',
173 | 'docx' => 'application/msword',
174 | 'xlsx' => 'application/vnd.ms-excel',
175 | 'pptx' => 'application/vnd.ms-powerpoint',
176 |
177 |
178 | // open office
179 | 'odt' => 'application/vnd.oasis.opendocument.text',
180 | 'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
181 | );
182 |
183 | if (isset($mimet[$idx])) {
184 | return $mimet[$idx];
185 | } else {
186 | return 'application/octet-stream';
187 | }
188 | }
189 |
190 | /**
191 | * @inheritdoc
192 | */
193 |
194 | public function rules()
195 | {
196 | return [
197 | [['class', 'field', 'filename', 'content_type', 'type'], 'required'],
198 | [['object_id', 'type', 'video_status', 'ordering'], 'integer'],
199 | [['class', 'field', 'title', 'filename', 'content_type', 'alt'], 'string', 'max' => 255],
200 | ];
201 | }
202 |
203 | /**
204 | * @inheritdoc
205 | */
206 |
207 | public function attributeLabels()
208 | {
209 | return [
210 | 'id' => Yii::t('app', 'ID'),
211 | 'class' => Yii::t('app', 'Class'),
212 | 'field' => Yii::t('app', 'Field'),
213 | 'object_id' => Yii::t('app', 'Object ID'),
214 | 'title' => Yii::t('app', 'Title'),
215 | 'filename' => Yii::t('app', 'Filename'),
216 | 'content_type' => Yii::t('app', 'Con tent Type'),
217 | 'type' => Yii::t('app', 'Type'),
218 | 'video_status' => Yii::t('app', 'Video Status'),
219 | 'alt' => Yii::t('app', 'Alternative title'),
220 | ];
221 | }
222 |
223 | /**
224 | * Return root path of preview
225 | * @return string
226 | */
227 |
228 | public function getRootPreviewPath()
229 | {
230 | if ($this->isSvg())
231 | return $this->getRootPath();
232 |
233 | return Yii::$app->getModule('files')->storageFullPath . $this->filename . '.jpg';
234 | }
235 |
236 | /**
237 | * @return bool
238 | */
239 | public function isSvg()
240 | {
241 | return $this->content_type == 'image/svg+xml';
242 | }
243 |
244 | /**
245 | * Return root path of image
246 | * @return string
247 | */
248 |
249 | public function getRootPath()
250 | {
251 | return Yii::$app->getModule('files')->storageFullPath . DIRECTORY_SEPARATOR . $this->filename;
252 | }
253 |
254 |
255 | /**
256 | * Return web path
257 | * @return string
258 | */
259 |
260 | public function getHref()
261 | {
262 | return Url::to(['/files/default/get', 'hash' => $this->hash]);
263 | }
264 |
265 | /**
266 | * @return bool
267 | */
268 | public function isImage(): bool
269 | {
270 | return $this->type == FileType::IMAGE;
271 | }
272 |
273 | /**
274 | * Delete files from disk
275 | */
276 |
277 | public function afterDelete()
278 | {
279 | $this->deleteFiles();
280 | parent::afterDelete();
281 | }
282 |
283 | /**
284 | * Method to read files from any mime types
285 | * @return bool
286 | */
287 |
288 | // public function imageCreateFromAny()
289 | // {
290 | // $type = exif_imagetype($this->rootPath);
291 | // $allowedTypes = array(
292 | // 1, // [] gif
293 | // 2, // [] jpg
294 | // 3, // [] png
295 | // 6 // [] bmp
296 | // );
297 | // if (!in_array($type, $allowedTypes)) {
298 | // return false;
299 | // }
300 | // switch ($type) {
301 | // case 1 :
302 | // $im = imageCreateFromGif($this->rootPath);
303 | // break;
304 | // case 2 :
305 | // $im = imageCreateFromJpeg($this->rootPath);
306 | // break;
307 | // case 3 :
308 | // $im = imageCreateFromPng($this->rootPath);
309 | // break;
310 | // case 6 :
311 | // $im = imageCreateFromBmp($this->rootPath);
312 | // break;
313 | // }
314 | // return $im;
315 | // }
316 |
317 | /**
318 | * Delete all files
319 | */
320 | public function deleteFiles()
321 | {
322 | $extension = pathinfo($this->rootPath, PATHINFO_EXTENSION);
323 | array_map('unlink', glob(str_replace(".{$extension}", '*', $this->rootPath)));
324 | }
325 |
326 | /**
327 | * Set object_id to 0 to break link with object
328 | * @return void
329 | */
330 | public function setZeroObject()
331 | {
332 | $this->object_id = 0;
333 | $this->save(false);
334 | }
335 |
336 | /**
337 | * @return mixed|null
338 | */
339 | public function getWatermark()
340 | {
341 | $owner = new $this->class();
342 | if (
343 | isset($owner->behaviors['files']) &&
344 | isset($owner->behaviors['files']->attributes[$this->field]) &&
345 | isset($owner->behaviors['files']->attributes[$this->field]['watermark'])
346 | )
347 | return $owner->behaviors['files']->attributes[$this->field]['watermark'];
348 | }
349 |
350 | /**
351 | * @return string
352 | */
353 | public function __toString()
354 | {
355 | return $this->href;
356 | }
357 |
358 | /**
359 | * Return webp path to preview
360 | * @param int $width
361 | * @param bool $webp
362 | * @return string
363 | * @throws ErrorException
364 | */
365 | public function getPreviewWebPath(int $width = 0, bool $webp = false)
366 | {
367 | if (!file_exists($this->getRootPath()))
368 | return null;
369 |
370 | if (!$this->isVideo() && !$this->isImage())
371 | throw new ErrorException('Requiested file is not an image and its implsible to resize it.');
372 |
373 | if (Yii::$app->getModule('files')->hostStatic)
374 | return
375 | Yii::$app->getModule('files')->hostStatic .
376 | $this->makeNameWithSize($this->filename, $width, $webp) .
377 | "?hash={$this->hash}&width={$width}&webp=" . intval($webp);
378 |
379 | return Url::toRoute(['/files/default/image', 'hash' => $this->hash, 'width' => $width, 'webp' => $webp]);
380 | }
381 |
382 | /**
383 | * @return bool
384 | */
385 | public function isVideo(): bool
386 | {
387 | return $this->type == FileType::VIDEO;
388 | }
389 |
390 | /**
391 | * Creates file paths to file versions
392 | * @param $name
393 | * @param int $width
394 | * @param bool $webp
395 | * @return string
396 | */
397 | public function makeNameWithSize($name, $width = 0, $webp = false)
398 | {
399 | $extension = pathinfo($this->rootPath, PATHINFO_EXTENSION);
400 | $rootPath = str_replace(".{$extension}", '', $name) . "_w" . $width . ".{$extension}";
401 | return str_replace($extension, $webp ? 'webp' : 'jpeg', $rootPath);
402 | }
403 |
404 | /**
405 | * Returns full path to custom preview version
406 | * @param int $width
407 | * @param bool $webp
408 | * @return string
409 | * @throws ErrorException
410 | */
411 | public function getPreviewRootPath($width = 0, $webp = false)
412 | {
413 | if (!$this->isVideo() && !$this->isImage())
414 | throw new ErrorException('Requiested file is not an image and its implsible to resize it.');
415 | return $this->makeNameWithSize($this->rootPath, $width, $webp);
416 | }
417 |
418 | /**
419 | * @return bool
420 | */
421 | public function isFile(): bool
422 | {
423 | return $this->type == FileType::FILE;
424 | }
425 |
426 | }
427 |
--------------------------------------------------------------------------------
/src/models/FileType.php:
--------------------------------------------------------------------------------
1 | 'file',
17 | self::IMAGE => 'image',
18 | self::VIDEO => 'video',
19 | ];
20 |
21 | public static $messageCategory = 'files';
22 | }
--------------------------------------------------------------------------------
/src/models/VideoStatus.php:
--------------------------------------------------------------------------------
1 | 'queued',
19 | self::CONVERTING => 'converting',
20 | self::READY => 'ready',
21 | ];
22 |
23 | }
--------------------------------------------------------------------------------
/src/views/default/_cropper.php:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | = Yii::t('files', 'Filename') ?>:
21 |
24 |
25 |
29 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | ALT:
40 |
43 |
44 |
48 |
52 |
53 |
54 |
55 |
56 |
104 |
--------------------------------------------------------------------------------
/src/views/default/_file.php:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
34 |
"
36 | style="= ($model->type == FileType::IMAGE) ? "background-image: url(" . $model->getPreviewWebPath(160) . ")" : NULL ?>"
37 | data-hash="= $model->hash ?>"
38 | data-filename="= $model->getHref() ?>"
39 | data-title="= $model->title ?>"
40 | data-alt="= $model->alt ?>"
41 | data-toggle="dropdown"
42 | aria-haspopup="true"
43 | aria-expanded="false" title="= $model->title ?>">
44 |
45 |
46 | = Html::hiddenInput((new ReflectionClass($model->class))->getShortName() . "[{$model->field}_ids][]", $model->id, ['class' => 'f12-file-input']) ?>
47 | = Html::hiddenInput((new ReflectionClass($model->class))->getShortName() . "[{$model->field}]", 1) ?>
48 |
49 | type != FileType::IMAGE): ?>
50 |
= $model->icon ?>
51 | = $model->title ?>
52 |
53 |
54 |
55 |
56 |
57 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/src/views/default/_single.php:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 | type == FileType::IMAGE): ?>
36 |
40 |

41 | = Html::hiddenInput($name ?: ((new ReflectionClass($model->class))->getShortName() . "[{$model->field}_ids][]"), $model->id) ?>
42 |
43 |
44 |
45 |
46 |
52 |
53 |
54 | = Html::hiddenInput($name ?: ((new ReflectionClass($model->class))->getShortName() . "[{$model->field}_ids][]"), $model->id) ?>
55 |
56 | type != FileType::IMAGE): ?>
57 | = $model->icon ?>
58 | = $model->title ?>
59 |
60 |
61 |
62 |
63 |
106 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | safeDown();
27 | }
28 |
29 | /**
30 | * Настраиваем основные параметры приложения: базу данных и модуль
31 | */
32 |
33 | protected function setApp()
34 | {
35 | $files = [
36 | 'class' => 'floor12\files\Module',
37 | 'storage' => '@app/storage',
38 | ];
39 | Yii::$app->setModule('files', $files);
40 |
41 |
42 | $db = [
43 | 'class' => 'yii\db\Connection',
44 | 'dsn' => "sqlite:$this->sqlite",
45 | ];
46 | Yii::$app->set('db', $db);
47 |
48 | $urlManager = [
49 | 'class' => UrlManager::class,
50 | 'enablePrettyUrl' => true,
51 | 'showScriptName' => false,
52 | 'baseUrl' => 'http://test.com',
53 | ];
54 | Yii::$app->set('urlManager', $urlManager);
55 |
56 | Yii::createObject(m180627_121715_files::class, [])->safeUp();
57 |
58 | }
59 |
60 | /**
61 | * @inheritdoc
62 | */
63 | protected function tearDown(): void
64 | {
65 | $this->destroyApplication();
66 | parent::tearDown();
67 | }
68 |
69 | /**
70 | * Убиваем приложение
71 | */
72 | protected function destroyApplication()
73 | {
74 | Yii::$app = null;
75 | }
76 |
77 | /**
78 | * @inheritdoc
79 | */
80 | public function setUp(): void
81 | {
82 | parent::setUp();
83 | $this->mockApplication();
84 | }
85 |
86 | /**
87 | * Запускаем приложение
88 | */
89 | protected function mockApplication()
90 | {
91 | new Application([
92 | 'id' => 'testapp',
93 | 'basePath' => __DIR__,
94 | 'vendorPath' => dirname(__DIR__) . '/vendor',
95 | 'runtimePath' => __DIR__ . '/runtime',
96 | ]);
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | db->driverName === 'mysql') {
13 | $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB';
14 | }
15 |
16 | $this->createTable(
17 | '{{%file}}',
18 | [
19 | 'id' => $this->primaryKey(11),
20 | 'class' => $this->string(255)->notNull(),
21 | 'field' => $this->string(255)->notNull(),
22 | 'object_id' => $this->integer(11)->notNull()->defaultValue(0),
23 | 'title' => $this->string(255)->notNull(),
24 | 'filename' => $this->string(255)->notNull(),
25 | 'content_type' => $this->string(255)->notNull(),
26 | 'type' => $this->integer(1)->notNull(),
27 | 'video_status' => $this->integer(1)->null()->defaultValue(null),
28 | 'ordering' => $this->integer(11)->notNull()->defaultValue(0),
29 | 'created' => $this->integer(11)->notNull(),
30 | 'user_id' => $this->integer(11)->null(),
31 | 'size' => $this->integer(20)->notNull(),
32 | 'hash' => $this->string(255)->null(),
33 | ], $tableOptions
34 | );
35 | }
36 |
37 | public function safeDown()
38 | {
39 |
40 | $this->dropTable('{{%file}}');
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/tests/data/photo.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/floor12/yii2-module-files/953d45bc911e0eee5df44864fd139e85925e95ec/tests/data/photo.jpeg
--------------------------------------------------------------------------------
/tests/data/photo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/floor12/yii2-module-files/953d45bc911e0eee5df44864fd139e85925e95ec/tests/data/photo.png
--------------------------------------------------------------------------------
/tests/data/testImage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/floor12/yii2-module-files/953d45bc911e0eee5df44864fd139e85925e95ec/tests/data/testImage.jpg
--------------------------------------------------------------------------------
/tests/logic/ClassnameEncoderTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('test\\\\class\\\\Name', $encoded);
25 | }
26 |
27 | /**
28 | * Проверяем обработку без слешей
29 | */
30 | public function testEncodeClassName()
31 | {
32 | $testname = 'testname';
33 | $encoded = (string)new ClassnameEncoder($testname);
34 | $this->assertEquals($encoded, $testname);
35 | }
36 |
37 | /**
38 | * Смотрим чтобы не было исключений и ошибок
39 | */
40 | public function testEncodeEmptyClassName()
41 | {
42 | $testname = '';
43 | $encoded = (string)new ClassnameEncoder($testname);
44 | $this->assertEquals($encoded, $testname);
45 | }
46 |
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/tests/logic/FileCreateFromInstanceTest.php:
--------------------------------------------------------------------------------
1 | setApp();
25 | }
26 |
27 | public function tearDown(): void
28 | {
29 | $this->clearDb();
30 | parent::tearDown();
31 | }
32 |
33 | /** Вызываем без параметров
34 | *
35 | */
36 |
37 | public function testNoParams()
38 | {
39 | $this->expectException(ArgumentCountError::class);
40 | new FileCreateFromInstance();
41 | }
42 |
43 | /** Вызываем без параметров
44 | *
45 | *
46 | */
47 |
48 | public function testBadParams()
49 | {
50 | $this->expectExceptionMessage("Attribute or class name not set.");
51 | $this->expectException(\yii\web\BadRequestHttpException::class);
52 | $instance = new UploadedFile();
53 | $data = [];
54 | new FileCreateFromInstance($instance, $data);
55 | }
56 |
57 | /** Вызываем без параметров
58 | *
59 | *
60 | */
61 |
62 | public function testWrongOwnerClassname()
63 | {
64 | $this->expectExceptionMessage("Attribute or class name not set.");
65 | $this->expectException(BadRequestHttpException::class);
66 |
67 | $instance = new UploadedFile();
68 | $data = [
69 | 'modelClass' => "notExistClassName",
70 | 'attribute' => 'images',
71 | ];
72 | $logic = new FileCreateFromInstance($instance, [], null, false);
73 | }
74 |
75 | /** Вызываем с нормальными параметрами
76 | */
77 |
78 | public function testGoodParams()
79 | {
80 |
81 | $instance = new UploadedFile();
82 | $instance->error = 0;
83 | $instance->name = 'testName.jpg';
84 | $instance->tempName = "tests/data/testImage.jpg";
85 | $instance->size = filesize($instance->tempName);
86 | $instance->type = "image/jpeg";
87 |
88 |
89 | $this->assertTrue(file_exists($instance->tempName));
90 |
91 | $data = [
92 | 'modelClass' => TestModel::class,
93 | 'attribute' => 'images',
94 | ];
95 | $logic = new FileCreateFromInstance($instance, $data, null, false);
96 |
97 | $model = $logic->execute();
98 |
99 | $this->assertTrue(is_object($model));
100 | $this->assertFalse($model->isNewRecord);
101 | $this->assertTrue(file_exists($model->rootPath), $model->rootPath);
102 | $this->assertTrue(file_exists($model->rootPath), $model->getPreviewWebPath());
103 |
104 |
105 | }
106 |
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/tests/logic/FileCreateFromPathTest.php:
--------------------------------------------------------------------------------
1 | expectExceptionMessage("File not found on disk.");
35 | $this->expectException(ErrorException::class);
36 | new FileCreateFromPath(
37 | new File(),
38 | "wrongTestFileName.png",
39 | $this->testOwnerClassName,
40 | $this->testOwnerFieldName,
41 | $this->storagePath,
42 | $this->testFileName
43 | );
44 | }
45 |
46 | /** Вызываем несуществуюий файл
47 | *
48 | *
49 | */
50 |
51 | public function testEmptyParams()
52 | {
53 | $this->expectExceptionMessage("Empty params not allowed.");
54 | $this->expectException(ErrorException::class);
55 | new FileCreateFromPath(
56 | new File(),
57 | "wrongTestFileName.png",
58 | "",
59 | $this->testOwnerFieldName,
60 | $this->storagePath,
61 | $this->testFileName
62 | );
63 | }
64 |
65 | /** Пробуем дать несуществующий адрес адрес хранилища
66 | *
67 | *
68 | */
69 |
70 | public function testWrongStorage()
71 | {
72 | $this->expectExceptionMessage("File storage not found on disk.");
73 | $this->expectException(ErrorException::class);
74 | new FileCreateFromPath(
75 | new File(),
76 | $this->testFilePath,
77 | $this->testOwnerClassName,
78 | $this->testOwnerFieldName,
79 | "wrongPath",
80 | $this->testFileName
81 | );
82 | }
83 |
84 |
85 | /** Не записываемое хранилище
86 | *
87 | *
88 | */
89 |
90 | public function testNotWritableStorage()
91 | {
92 | $this->expectExceptionMessage("File storage not found on disk.");
93 | $this->expectException(ErrorException::class);
94 | new FileCreateFromPath(
95 | new File(),
96 | $this->testFilePath,
97 | $this->testOwnerClassName,
98 | $this->testOwnerFieldName,
99 | $this->testFileName
100 | );
101 | }
102 |
103 |
104 | /**
105 | * Нормальный сценарий который пока протестировать нормально не удается.
106 | */
107 |
108 | public function testCreate()
109 | {
110 | $this->setApp();
111 |
112 | $file = new File();
113 |
114 | $logicObject = new FileCreateFromPath(
115 | $file,
116 | $this->testFilePath,
117 | $this->testOwnerClassName,
118 | $this->testOwnerFieldName,
119 | $this->storagePath,
120 | $this->testFileName
121 | );
122 | $this->assertTrue(is_object($logicObject));
123 | $this->assertTrue($logicObject->execute());
124 |
125 |
126 | // Проверяем, что файл сохранился нормально.
127 | $this->assertFalse($file->isNewRecord);
128 | $this->assertTrue(is_integer($file->id));
129 | $this->assertEquals($this->testOwnerClassName, $file->class);
130 | $this->assertEquals(FileType::IMAGE, $file->type);
131 | $this->assertEquals("image/jpeg", $file->content_type);
132 | $this->assertTrue(file_exists($file->rootPath), $file->rootPath);
133 |
134 | $this->clearDb();
135 | }
136 |
137 |
138 | }
139 |
--------------------------------------------------------------------------------
/tests/logic/FileRenameTest.php:
--------------------------------------------------------------------------------
1 | setApp();
26 | }
27 |
28 | public function tearDown(): void
29 | {
30 | $this->clearDb();
31 | parent::tearDown(); // TODO: Change the autogenerated stub
32 | }
33 |
34 | /** Пробуем не передать данные
35 | *
36 | */
37 | public function testNoParams()
38 | {
39 | $this->expectException(ArgumentCountError::class);
40 | $model = new FileRename();
41 | }
42 |
43 | /** Не передаем ID файла
44 | *
45 | *
46 | */
47 | public function testEmptyId()
48 | {
49 | $this->expectExceptionMessage("ID of file is not set.");
50 | $this->expectException(yii\web\BadRequestHttpException::class);
51 | $data = [];
52 | $model = new FileRename($data);
53 | }
54 |
55 | /** Не передаем ID файла
56 | *
57 | *
58 | */
59 | public function testEmptyTitle()
60 | {
61 | $this->expectExceptionMessage("Title of file is not set.");
62 | $this->expectException(yii\web\BadRequestHttpException::class);
63 | $data = ['id' => 1];
64 | $model = new FileRename($data);
65 | }
66 |
67 | /** Передаем ID несуществующего файла
68 | *
69 | *
70 | */
71 | public function testFileNotExitstInDb()
72 | {
73 | $this->expectExceptionMessage("File not found.");
74 | $this->expectException(yii\web\NotFoundHttpException::class);
75 | $data = ['id' => 100, 'title' => 'new name'];
76 | $model = new FileRename($data);
77 | }
78 |
79 | /** Создаем файл и пробуем его переименовать.
80 | */
81 | public function testGoodRename()
82 | {
83 | $oldName = "Old name";
84 | $newName = "New name";
85 |
86 | $model = new File();
87 | $model->title = $oldName;
88 | $model->class = 'testClassName';
89 | $model->filename = 'testFileName/jpg';
90 | $model->created = time();
91 | $model->field = 'images';
92 | $model->size = 50000;
93 | $model->user_id = 1;
94 | $model->content_type = 'image/jpeg';
95 | $model->type = FileType::IMAGE;
96 | $model->save();
97 | $this->assertFalse($model->isNewRecord);
98 |
99 | $data = [
100 | 'id' => 1,
101 | 'title' => $newName,
102 | ];
103 | $ret = Yii::createObject(FileRename::class, [$data])->execute();
104 | $this->assertEquals($ret, $newName);
105 |
106 | $model->refresh();
107 | $this->assertEquals($model->title, $newName);
108 |
109 | }
110 |
111 |
112 | }
113 |
--------------------------------------------------------------------------------
/tests/logic/PathGeneratorTest.php:
--------------------------------------------------------------------------------
1 | expectException(ArgumentCountError::class);
33 | new PathGenerator();
34 | }
35 |
36 | /**
37 | * Empty storage path check
38 | *
39 | *
40 | *
41 | */
42 | public function testEmptyStoragePath()
43 | {
44 | $this->expectException(ErrorException::class);
45 | $this->expectExceptionMessage("Storage path not set for path generator.");
46 | new PathGenerator("");
47 | }
48 |
49 | /**
50 | * Create storage path if it not exists
51 | */
52 | public function testCreateStoragePath()
53 | {
54 | $path = 'tests/st1';
55 | new PathGenerator($path);
56 | $this->assertTrue(file_exists($path));
57 | exec("rm -f $path");
58 | }
59 |
60 | /**
61 | * Create file path and check it exists
62 | */
63 | public function testGeneratePath()
64 | {
65 | $path = (string)new PathGenerator($this->storagePath);
66 | $pre_path = substr($path, 0, 6);
67 | $fullPath = "{$this->storagePath}{$pre_path}";
68 | $this->assertTrue(file_exists($fullPath), $fullPath);
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/tests/storage/.gitignore:
--------------------------------------------------------------------------------
1 | *
--------------------------------------------------------------------------------