ManageAttachments::action_removeall()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 17
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
ccs 0
cts 8
cp 0
crap 6
1
<?php
2
3
/**
4
 * Handles the job of attachment and avatar maintenance / management.
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 Beta 1
14
 *
15
 */
16
17
namespace ElkArte\AdminController;
18
19
use ElkArte\AbstractController;
20
use ElkArte\Action;
21
use ElkArte\Attachments\AttachmentsDirectory;
22
use ElkArte\Graphics\Image;
23
use ElkArte\Graphics\Manipulators\Gd2;
24
use ElkArte\Graphics\Manipulators\ImageMagick;
25
use ElkArte\Helper\FileFunctions;
26
use ElkArte\Helper\Util;
27
use ElkArte\Languages\Loader;
28
use ElkArte\SettingsForm\SettingsForm;
29
use Exception;
30
use FilesystemIterator;
31
use UnexpectedValueException;
32
33
/**
34
 * This is the attachments and avatars controller class.
35
 * It is doing the job of attachments and avatars maintenance and management.
36
 *
37
 */
38
class ManageAttachments extends AbstractController
39
{
40
	/** @var int Loop counter for paused attachment maintenance actions */
41
	public int $step;
42
43
	/** @var int substep counter for paused attachment maintenance actions */
44
	public int $substep;
45
46
	/** @var int Substep at the beginning of a maintenance loop */
47
	public int $starting_substep;
48
49
	/** @var int Current directory key being processed */
50
	public int $current_dir;
51
52
	/** @var string Used during transfer of files */
53
	public string $from;
54
55
	/** @var string Type of attachment management in use */
56
	public string $auto;
57
58
	/** @var string Destination when transferring attachments */
59
	public string $to;
60
61
	/** @var FileFunctions */
62
	public FileFunctions $file_functions;
63
64
	/**
65
	 * Pre-dispatch, load functions needed by all methods
66
	 */
67
	public function pre_dispatch()
68
	{
69
		// These get used often enough that it makes sense to include them for every action
70
		require_once(SUBSDIR . '/Attachments.subs.php');
71
		require_once(SUBSDIR . '/ManageAttachments.subs.php');
72
		$this->file_functions = FileFunctions::instance();
73
	}
74
75
	/**
76
	 * The main 'Attachments and Avatars' admin.
77
	 *
78
	 * What it does:
79
	 *
80
	 * - This method is the entry point for index.php?action=admin;area=manageattachments,
81
	 * and it calls a function based on the sub-action.
82
	 * - It requires the manage_attachments permission.
83
	 *
84
	 * @event integrate_sa_manage_attachments
85
	 * @uses ManageAttachments template.
86
	 * @uses Admin language file.
87
	 * @uses template layer 'manage_files' for showing the tab bar.
88
	 *
89
	 * @see AbstractController::action_index()
90 2
	 */
91
	public function action_index()
92
	{
93 2
		global $txt, $context;
94 2
95 2
		// You have to be able to moderate the forum to do this.
96
		isAllowedTo('manage_attachments');
97
98
		// Set up the template stuff we'll probably need.
99
		theme()->getTemplates()->load('ManageAttachments');
100
101
		// All the things we can do with attachments
102
		$subActions = [
103
			'attachments' => [$this, 'action_attachSettings_display'],
104
			'avatars' => [
105
				'controller' => ManageAvatars::class,
106
				'function' => 'action_index'],
107
			'attachpaths' => [$this, 'action_attachpaths'],
108
			'browse' => [$this, 'action_browse'],
109
			'byAge' => [$this, 'action_byAge'],
110
			'bySize' => [$this, 'action_bySize'],
111
			'maintenance' => [$this, 'action_maintenance'],
112
			'repair' => [$this, 'action_repair'],
113
			'remove' => [$this, 'action_remove'],
114
			'removeall' => [$this, 'action_removeall'],
115
			'transfer' => [$this, 'action_transfer'],
116
		];
117
118
		// Get ready for some action
119
		$action = new Action('manage_attachments');
120
121
		// The default page title is good.
122
		$context['page_title'] = $txt['attachments_avatars'];
123
124
		// Get the subAction, call integrate_sa_manage_attachments
125
		$subAction = $action->initialize($subActions, 'browse');
126
		$context['sub_action'] = $subAction;
127
128
		// This uses admin tabs - as it should!
129
		$context[$context['admin_menu_name']]['object']->prepareTabData([
130
			'title' => 'attachments_avatars',
131
			'help' => 'manage_files',
132
			'description' => 'attachments_desc',
133
		]);
134
135
		// Finally, go to where we want to go
136
		$action->dispatch($subAction);
137
	}
138
139
	/**
140
	 * Allows showing / changing attachment settings.
141
	 *
142
	 * - This is the default sub-action of the 'Attachments and Avatars' center.
143
	 * - Called by index.php?action=admin;area=manageattachments;sa=attachments.
144
	 *
145
	 * @event integrate_save_attachment_settings
146
	 * @uses 'attachments' sub template.
147
	 */
148
	public function action_attachSettings_display(): void
149
	{
150
		global $modSettings, $context;
151
152
		// initialize the form
153
		$settingsForm = new SettingsForm(SettingsForm::DB_ADAPTER);
154
		$attachmentsDir = new AttachmentsDirectory($modSettings, database());
155
156
		// Initialize settings
157
		$settingsForm->setConfigVars($this->_settings());
158
159
		theme()->addInlineJavascript('
160
	let storing_type = document.getElementById(\'automanage_attachments\'),
161
		base_dir = document.getElementById(\'use_subdirectories_for_attachments\');
162
163
	createEventListener(storing_type)
164
	storing_type.addEventListener("change", toggleSubDir, false);
165
	createEventListener(base_dir)
166
	base_dir.addEventListener("change", toggleSubDir, false);
167
	toggleSubDir();', true);
168
169
		// Saving settings?
170
		if ($this->_req->hasQuery('save'))
171
		{
172
			checkSession();
173
174
			if ($this->_req->hasPost('attachmentEnable'))
175
			{
176
				enableModules('attachments', ['post', 'display']);
177
			}
178
			else
179
			{
180
				disableModules('attachments', ['post', 'display']);
181
			}
182
183
			// Read posted values safely
184
			$autoManage = $this->_req->getPost('automanage_attachments', 'intval', 0);
185
			$useSubdirectories = $this->_req->getPost('use_subdirectories_for_attachments', 'intval', 0);
186
			$baseDirPosted = $this->_req->getPost('basedirectory_for_attachments', 'trim');
187
			$uploadDirPosted = $this->_req->getPost('attachmentUploadDir', 'trim');
188
			$webpEnable = !empty($this->_req->getPost('attachment_webp_enable', null, ''));
189
			$attachExtensions = $this->_req->getPost('attachmentExtensions', 'trim');
190
191
			// Default/Manual implies no subdirectories
192
			if ($autoManage === 0)
193
			{
194
				$useSubdirectories = 0;
195
			}
196
197
			// Changing the attachment upload directory
198
			if ($uploadDirPosted !== null)
199
			{
200
				if (!empty($uploadDirPosted)
201
					&& $modSettings['attachmentUploadDir'] !== $uploadDirPosted
202
					&& $this->file_functions->fileExists($modSettings['attachmentUploadDir']))
203
				{
204
					rename($modSettings['attachmentUploadDir'], $uploadDirPosted);
205
				}
206
207
				$modSettings['attachmentUploadDir'] = [1 => $uploadDirPosted];
208
			}
209
210
			// Adding / changing the subdirectory's for attachments
211
			if (!empty($useSubdirectories))
212
			{
213
				// Make sure we have a base directory defined
214
				if (empty($baseDirPosted))
215
				{
216
					$baseDirPosted = (empty($modSettings['basedirectory_for_attachments']) ? (BOARDDIR) : $modSettings['basedirectory_for_attachments']);
217
				}
218
219
				// The current base directories that we know
220
				if (!empty($modSettings['attachment_basedirectories']))
221
				{
222
					if (!is_array($modSettings['attachment_basedirectories']))
223
					{
224
						$modSettings['attachment_basedirectories'] = Util::unserialize($modSettings['attachment_basedirectories']);
225
					}
226
				}
227
				else
228
				{
229
					$modSettings['attachment_basedirectories'] = [];
230
				}
231
232
				// Trying to use a nonexistent base directory
233
				if (!empty($baseDirPosted) && $attachmentsDir->isBaseDir($baseDirPosted) === false)
234
				{
235
					$currentAttachmentUploadDir = $attachmentsDir->currentDirectoryId();
236
237
					// If this is a new directory being defined, attempt to create it
238
					if ($attachmentsDir->directoryExists($baseDirPosted) === false)
239
					{
240
						try
241
						{
242
							$attachmentsDir->createDirectory($baseDirPosted);
243
						}
244
						catch (Exception)
245
						{
246
							$baseDirPosted = $modSettings['basedirectory_for_attachments'];
247
						}
248
					}
249
250
					// The base directory should be in our list of available bases
251
					if (!in_array($baseDirPosted, $modSettings['attachment_basedirectories']))
252
					{
253
						$modSettings['attachment_basedirectories'][$modSettings['currentAttachmentUploadDir']] = $baseDirPosted;
254
						updateSettings(['attachment_basedirectories' => serialize($modSettings['attachment_basedirectories']), 'currentAttachmentUploadDir' => $currentAttachmentUploadDir,]);
255
					}
256
				}
257
			}
258
259
			// Allow or not webp extensions.
260
			if (!empty($webpEnable) && $attachExtensions !== null && !str_contains((string) $attachExtensions, 'webp'))
261
			{
262
				$attachExtensions = rtrim((string) $attachExtensions, ',') . ',webp';
263
			}
264
265
			call_integration_hook('integrate_save_attachment_settings');
266
267
			// Build a config array from posted values and override with sanitized ones
268
			$config = (array) $this->_req->post;
269
			$config['automanage_attachments'] = $autoManage;
270
			$config['use_subdirectories_for_attachments'] = $useSubdirectories;
271
			if ($baseDirPosted !== null)
272
			{
273
				$config['basedirectory_for_attachments'] = $baseDirPosted;
274
			}
275
			if ($uploadDirPosted !== null)
276
			{
277
				$config['attachmentUploadDir'] = serialize($modSettings['attachmentUploadDir']);
278
			}
279
			if ($attachExtensions !== null)
280
			{
281
				$config['attachmentExtensions'] = $attachExtensions;
282
			}
283
284
			$settingsForm->setConfigValues($config);
285
			$settingsForm->save();
286
			redirectexit('action=admin;area=manageattachments;sa=attachments');
287
		}
288
289 2
		$context['post_url'] = getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'attachments', 'save']);
290
		$settingsForm->prepare();
291 2
292
		$context['sub_template'] = 'show_settings';
293
	}
294 2
295 2
	/**
296
	 * Retrieve and return the administration settings for attachments.
297
	 *
298 2
	 * @event integrate_modify_attachment_settings
299
	 */
300
	private function _settings()
301
	{
302
		global $modSettings, $txt, $context, $settings;
303
304 2
		// Get the current attachment directory.
305
		$attachmentsDir = new AttachmentsDirectory($modSettings, database());
306
		$context['attachmentUploadDir'] = $attachmentsDir->getCurrent();
307
308
		// First time here?
309 2
		if (empty($modSettings['attachment_basedirectories']) && $modSettings['currentAttachmentUploadDir'] == 1 && (is_array($modSettings['attachmentUploadDir']) && $attachmentsDir->countDirs() == 1))
310
		{
311 2
			$modSettings['attachmentUploadDir'] = $modSettings['attachmentUploadDir'][1];
312
		}
313
314
		// If not set, show a default path for the base directory
315
		if (!$this->_req->hasQuery('save') && empty($modSettings['basedirectory_for_attachments']))
316
		{
317 2
			$modSettings['basedirectory_for_attachments'] = $context['attachmentUploadDir'];
318
		}
319
320
		$context['valid_upload_dir'] = $this->file_functions->isDir($context['attachmentUploadDir']) && $this->file_functions->isWritable($context['attachmentUploadDir']);
321 2
322 2
		if ($attachmentsDir->autoManageEnabled())
323
		{
324 2
			$context['valid_basedirectory'] = !empty($modSettings['basedirectory_for_attachments']) && $this->file_functions->isWritable($modSettings['basedirectory_for_attachments']);
325 2
		}
326 2
		else
327 2
		{
328 2
			$context['valid_basedirectory'] = true;
329 2
		}
330 2
331
		// A bit of razzle-dazzle with the $txt strings. :)
332
		$txt['basedirectory_for_attachments_warning'] = str_replace('{attach_repair_url}', getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'attachpaths']), $txt['basedirectory_for_attachments_warning']);
333 2
		$txt['attach_current_dir_warning'] = str_replace('{attach_repair_url}', getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'attachpaths']), $txt['attach_current_dir_warning']);
334
		$txt['attachment_path'] = $context['attachmentUploadDir'];
335
		$txt['basedirectory_for_attachments_path'] = $modSettings['basedirectory_for_attachments'] ?? '';
336 2
		$txt['use_subdirectories_for_attachments_note'] = empty($modSettings['attachment_basedirectories']) || empty($modSettings['use_subdirectories_for_attachments']) ? $txt['use_subdirectories_for_attachments_note'] : '';
337 2
		$txt['attachmentUploadDir_multiple_configure'] = '<a class="linkbutton" href="' . getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'attachpaths']) . '">' . $txt['attachmentUploadDir_multiple_configure'] . '</a>';
338 2
		$txt['attach_current_dir'] = $attachmentsDir->autoManageEnabled() ? $txt['attach_last_dir'] : $txt['attach_current_dir'];
339 2
		$txt['attach_current_dir_warning'] = $txt['attach_current_dir'] . $txt['attach_current_dir_warning'];
340 2
		$txt['basedirectory_for_attachments_warning'] = $txt['basedirectory_for_attachments_current'] . $txt['basedirectory_for_attachments_warning'];
341
342
		// Perform several checks to determine imaging capabilities.
343 2
		$image = new Image($settings['default_theme_dir'] . '/images/blank.png');
344
		$testImg = Gd2::canUse() || ImageMagick::canUse();
345 2
		$testImgRotate = ImageMagick::canUse() || (Gd2::canUse() && function_exists('exif_read_data'));
346 2
347
		// Check on webp support, and correct if wrong
348 2
		$testWebP = $image->hasWebpSupport();
349 2
		if (!$testWebP && !empty($modSettings['attachment_webp_enable']))
350 2
		{
351
			updateSettings(['attachment_webp_enable' => 0]);
352
		}
353
354 2
		// Check if the server settings support these upload size values
355
		$post_max_size = ini_get('post_max_size');
356 2
		$upload_max_filesize = ini_get('upload_max_filesize');
357
		$testPM = empty($post_max_size) || memoryReturnBytes($post_max_size) >= (isset($modSettings['attachmentPostLimit']) ? $modSettings['attachmentPostLimit'] * 1024 : 0);
358 2
		$testUM = empty($upload_max_filesize) || memoryReturnBytes($upload_max_filesize) >= (isset($modSettings['attachmentSizeLimit']) ? $modSettings['attachmentSizeLimit'] * 1024 : 0);
359
360 2
		// Set some helpful information for the UI
361
		$post_max_size_text = sprintf($txt['zero_for_system_limit'], $post_max_size === '' || $post_max_size === '0' || $post_max_size === false ? $txt['none'] : $post_max_size, 'post_max_size');
362 2
		$upload_max_filesize_text = sprintf($txt['zero_for_system_limit'], $upload_max_filesize === '' || $upload_max_filesize === '0' || $upload_max_filesize === false ? $txt['none'] : $upload_max_filesize, 'upload_max_filesize');
363 2
364 2
		$config_vars = [
365
			['title', 'attachment_manager_settings'],
366 2
			// Are attachments enabled?
367 2
			['select', 'attachmentEnable', [$txt['attachmentEnable_deactivate'], $txt['attachmentEnable_enable_all'], $txt['attachmentEnable_disable_new']]],
368 2
			'',
369 2
			// Directory and size limits.
370 2
			['select', 'automanage_attachments', [0 => $txt['attachments_normal'], 1 => $txt['attachments_auto_space'], 2 => $txt['attachments_auto_years'], 3 => $txt['attachments_auto_months'], 4 => $txt['attachments_auto_16']]],
371 2
			['check', 'use_subdirectories_for_attachments', 'subtext' => $txt['use_subdirectories_for_attachments_note']],
372
			(empty($modSettings['attachment_basedirectories'])
373
				? ['text', 'basedirectory_for_attachments', 40,]
374
				: ['var_message', 'basedirectory_for_attachments', 'message' => 'basedirectory_for_attachments_path', 'invalid' => empty($context['valid_basedirectory']), 'text_label' => (empty($context['valid_basedirectory'])
375
					? $txt['basedirectory_for_attachments_warning']
376
					: $txt['basedirectory_for_attachments_current'])]
377 2
			),
378
			Util::is_serialized($modSettings['attachmentUploadDir'])
379 2
				? ['var_message', 'attach_current_directory', 'postinput' => $txt['attachmentUploadDir_multiple_configure'], 'message' => 'attachment_path', 'invalid' => empty($context['valid_upload_dir']),
380
				'text_label' => (empty($context['valid_upload_dir'])
381
					? $txt['attach_current_dir_warning']
382
					: $txt['attach_current_dir'])]
383
				: ['text', 'attachmentUploadDir', 'postinput' => $txt['attachmentUploadDir_multiple_configure'], 40, 'invalid' => !$context['valid_upload_dir']],
384
			['int', 'attachmentDirFileLimit', 'subtext' => $txt['zero_for_no_limit'], 6],
385
			['int', 'attachmentDirSizeLimit', 'subtext' => $txt['zero_for_no_limit'], 6, 'postinput' => $txt['kilobyte']],
386
			'',
387
			// Posting limits
388 2
			['int', 'attachmentPostLimit', 'subtext' => $post_max_size_text, 6, 'postinput' => $testPM === false ? $txt['attachment_postsize_warning'] : $txt['kilobyte'], 'invalid' => $testPM === false],
389 2
			['int', 'attachmentSizeLimit', 'subtext' => $upload_max_filesize_text, 6, 'postinput' => $testUM === false ? $txt['attachment_postsize_warning'] : $txt['kilobyte'], 'invalid' => $testUM === false],
390 2
			['int', 'attachmentNumPerPostLimit', 'subtext' => $txt['zero_for_no_limit'], 6],
391
			'',
392
			['check', 'attachment_webp_enable', 'disabled' => !$testWebP, 'postinput' => $testWebP ? "" : $txt['attachment_webp_enable_na']],
393
			['check', 'attachment_autorotate', 'disabled' => !$testImgRotate, 'postinput' => $testImgRotate ? '' : $txt['attachment_autorotate_na']],
394 2
			// Resize limits
395
			['title', 'attachment_image_resize'],
396 2
			['check', 'attachment_image_resize_enabled'],
397
			['check', 'attachment_image_resize_reformat'],
398
			['text', 'attachment_image_resize_width', 'subtext' => $txt['zero_for_no_limit'], 6, 'postinput' => $txt['attachment_image_resize_post']],
399
			['text', 'attachment_image_resize_height', 'subtext' => $txt['zero_for_no_limit'], 6, 'postinput' => $txt['attachment_image_resize_post']],
400
			// Security Items
401
			['title', 'attachment_security_settings'],
402 2
			// Extension checks etc.
403
			['check', 'attachmentCheckExtensions'],
404 2
			['text', 'attachmentExtensions', 40],
405
			'',
406
			// Image checks.
407
			['warning', $testImg === false ? 'attachment_img_enc_warning' : ''],
408
			['check', 'attachment_image_reencode'],
409
			// Thumbnail settings.
410
			['title', 'attachment_thumbnail_settings'],
411
			['check', 'attachmentShowImages'],
412
			['check', 'attachmentThumbnails'],
413
			['text', 'attachmentThumbWidth', 6],
414
			['text', 'attachmentThumbHeight', 6],
415
			'',
416
			['int', 'max_image_width', 'subtext' => $txt['zero_for_no_limit']],
417
			['int', 'max_image_height', 'subtext' => $txt['zero_for_no_limit']],
418
		];
419
420
		// Add new settings with a nice hook, makes them available for admin settings search as well
421
		call_integration_hook('integrate_modify_attachment_settings', [&$config_vars]);
422
423
		return $config_vars;
424
	}
425
426
	/**
427
	 * Public method to return the config settings, used for admin search
428
	 */
429
	public function settings_search()
430
	{
431
		return $this->_settings();
432
	}
433
434
	/**
435
	 * Show a list of attachment or avatar files.
436
	 *
437
	 * - Called by
438
	 *     ?action=admin;area=manageattachments;sa=browse for attachments
439
	 *     ?action=admin;area=manageattachments;sa=browse;avatars for avatars.
440
	 *     ?action=admin;area=manageattachments;sa=browse;thumbs for thumbnails.
441
	 * - Allows sorting by name, date, size, and member.
442
	 * - Paginates results.
443
	 *
444
	 * @uses the 'browse' sub template
445
	 */
446
	public function action_browse(): void
447
	{
448
		global $context, $txt;
449
450
		// Attachments or avatars?
451
		$context['browse_type'] = $this->_req->hasQuery('avatars') ? 'avatars' : ($this->_req->hasQuery('thumbs') ? 'thumbs' : 'attachments');
452
		loadJavascriptFile('topic.js');
453
454
		// Set the options for the list component.
455
		$listOptions = [
456
			'id' => 'attach_browse',
457
			'title' => $txt['attachment_manager_browse_files'],
458
			'items_per_page' => 25,
459
			'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'browse'] + ($context['browse_type'] === 'avatars' ? ['avatars'] : ($context['browse_type'] === 'thumbs' ? ['thumbs'] : []))),
460
			'default_sort_col' => 'name',
461
			'no_items_label' => $txt['attachment_manager_' . ($context['browse_type'] === 'avatars' ? 'avatars' : ($context['browse_type'] === 'thumbs' ? 'thumbs' : 'attachments')) . '_no_entries'],
462
			'get_items' => [
463
				'function' => 'list_getFiles',
464
				'params' => [
465
					$context['browse_type'],
466
				],
467
			],
468
			'get_count' => [
469
				'function' => 'list_getNumFiles',
470
				'params' => [
471
					$context['browse_type'],
472
				],
473
			],
474
			'columns' => [
475
				'name' => [
476
					'header' => [
477
						'value' => $txt['attachment_name'],
478
						'class' => 'grid50',
479
					],
480
					'data' => [
481
						'function' => static function ($rowData) {
482
							global $modSettings, $context;
483
484
							$link = '<a href="';
485
							// In the case of a custom avatar, URL attachments have a fixed directory.
486
							if ((int) $rowData['attachment_type'] === 1)
487
							{
488
								$link .= sprintf('%1$s/%2$s', $modSettings['custom_avatar_url'], $rowData['filename']);
489
							}
490
							// By default, avatars are downloaded almost as attachments.
491
							elseif ($context['browse_type'] === 'avatars')
492
							{
493
								$link .= getUrl('attach', ['action' => 'dlattach', 'type' => 'avatar', 'attach' => (int) $rowData['id_attach'], 'name' => $rowData['filename']]);
494
							}
495
							// Normal attachments are always linked to a topic ID.
496
							else
497
							{
498
								$link .= getUrl('attach', ['action' => 'dlattach', 'topic' => ((int) $rowData['id_topic']) . '.0', 'attach' => (int) $rowData['id_attach'], 'name' => $rowData['filename']]);
499
							}
500
							$link .= '"';
501
502
							// Show a popup on click if it's a picture, and we know its dimensions (use a rand message to prevent navigation)
503
							if (!empty($rowData['width']) && !empty($rowData['height']))
504
							{
505
								$link .= 'id="link_' . $rowData['id_attach'] . '" data-lightboxmessage="' . random_int(0, 100000) . '" data-lightboximage="' . $rowData['id_attach'] . '"';
506
							}
507
508
							$link .= sprintf('>%1$s</a>', preg_replace('~&amp;#(\\\\d{1,7}|x[0-9a-fA-F]{1,6});~', '&#\\\\1;', htmlspecialchars($rowData['filename'], ENT_COMPAT, 'UTF-8')));
509
510
							// Show the dimensions.
511
							if (!empty($rowData['width']) && !empty($rowData['height']))
512
							{
513
								$link .= sprintf(' <span class="smalltext">%1$dx%2$d</span>', $rowData['width'], $rowData['height']);
514
							}
515
516
							return $link;
517
						},
518
					],
519
					'sort' => [
520
						'default' => 'a.filename',
521
						'reverse' => 'a.filename DESC',
522
					],
523
				],
524
				'filesize' => [
525
					'header' => [
526
						'value' => $txt['attachment_file_size'],
527
						'class' => 'nowrap',
528
					],
529
					'data' => [
530
						'function' => static fn($rowData) => byte_format($rowData['size']),
531
					],
532
					'sort' => [
533
						'default' => 'a.size',
534
						'reverse' => 'a.size DESC',
535
					],
536
				],
537
				'member' => [
538
					'header' => [
539
						'value' => $context['browse_type'] === 'avatars' ? $txt['attachment_manager_member'] : $txt['posted_by'],
540
						'class' => 'nowrap',
541
					],
542
					'data' => [
543
						'function' => static function ($rowData) {
544
							// In case of an attachment, return the poster of the attachment.
545
							if (empty($rowData['id_member']))
546
							{
547
								return htmlspecialchars($rowData['poster_name'], ENT_COMPAT, 'UTF-8');
548
							}
549
550
							return '<a href="' . getUrl('profile', ['action' => 'profile', 'u' => (int) $rowData['id_member'], 'name' => $rowData['poster_name']]) . '">' . $rowData['poster_name'] . '</a>';
551
						},
552
					],
553
					'sort' => [
554
						'default' => 'mem.real_name',
555
						'reverse' => 'mem.real_name DESC',
556
					],
557
				],
558
				'date' => [
559
					'header' => [
560
						'value' => $context['browse_type'] === 'avatars' ? $txt['attachment_manager_last_active'] : $txt['date'],
561
						'class' => 'nowrap',
562
					],
563
					'data' => [
564
						'function' => static function ($rowData) {
565
							global $txt, $context;
566
567
							// The date the message containing the attachment was posted or the owner of the avatar was active.
568
							$date = empty($rowData['poster_time']) ? $txt['never'] : standardTime($rowData['poster_time']);
569
							// Add a link to the topic in case of an attachment.
570
							if ($context['browse_type'] !== 'avatars')
571
							{
572
								$date .= '<br />' . $txt['in'] . ' <a href="' . getUrl('topic', ['topic' => (int) $rowData['id_topic'], 'start' => 'msg' . (int) $rowData['id_msg'], 'subject' => $rowData['subject']]) . '#msg' . (int) $rowData['id_msg'] . '">' . $rowData['subject'] . '</a>';
573
							}
574
575
							return $date;
576
						},
577
					],
578
					'sort' => [
579
						'default' => $context['browse_type'] === 'avatars' ? 'mem.last_login' : 'm.id_msg',
580
						'reverse' => $context['browse_type'] === 'avatars' ? 'mem.last_login DESC' : 'm.id_msg DESC',
581
					],
582
				],
583
				'downloads' => [
584
					'header' => [
585
						'value' => $txt['downloads'],
586
						'class' => 'nowrap',
587
					],
588
					'data' => [
589
						'db' => 'downloads',
590
						'comma_format' => true,
591
					],
592
					'sort' => [
593
						'default' => 'a.downloads',
594
						'reverse' => 'a.downloads DESC',
595
					],
596
				],
597
				'check' => [
598
					'header' => [
599
						'value' => '<input type="checkbox" onclick="invertAll(this, this.form);" class="input_check" />',
600
						'class' => 'centertext',
601
					],
602
					'data' => [
603
						'sprintf' => [
604
							'format' => '<input type="checkbox" name="remove[%1$d]" class="input_check" />',
605
							'params' => [
606
								'id_attach' => false,
607
							],
608
						],
609
						'class' => 'centertext',
610
					],
611
				],
612
			],
613
			'form' => [
614
				'href' => getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'remove', ($context['browse_type'] === 'avatars' ? 'avatars' : ($context['browse_type'] === 'thumbs' ? 'thumbs' : ''))]),
615
				'include_sort' => true,
616
				'include_start' => true,
617
				'hidden_fields' => [
618
					'type' => $context['browse_type'],
619
				],
620
			],
621
			'additional_rows' => [
622
				[
623
					'position' => 'below_table_data',
624
					'value' => '<input type="submit" name="remove_submit" class="right_submit" value="' . $txt['quickmod_delete_selected'] . '" onclick="return confirm(\'' . $txt['confirm_delete_attachments'] . '\');" />',
625
				],
626
			],
627
			'list_menu' => [
628
				'show_on' => 'top',
629
				'class' => 'flow_flex_right',
630
				'links' => [
631
					[
632
						'href' => getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'browse']),
633
						'is_selected' => $context['browse_type'] === 'attachments',
634
						'label' => $txt['attachment_manager_attachments']
635
					],
636
					[
637
						'href' => getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'browse', 'avatars']),
638
						'is_selected' => $context['browse_type'] === 'avatars',
639
						'label' => $txt['attachment_manager_avatars']
640
					],
641
					[
642
						'href' => getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'browse', 'thumbs']),
643
						'is_selected' => $context['browse_type'] === 'thumbs',
644
						'label' => $txt['attachment_manager_thumbs']
645
					],
646
				],
647
			],
648
		];
649
650
		// Create the list.
651
		createList($listOptions);
652
	}
653
654
	/**
655
	 * Show several file maintenance options.
656
	 *
657
	 * What it does:
658
	 *
659
	 * - Called by ?action=admin;area=manageattachments;sa=maintain.
660
	 * - Calculates file statistics (total file size, number of attachments,
661
	 * number of avatars, attachment space available).
662
	 *
663
	 * @uses the 'maintenance' sub template.
664
	 */
665
	public function action_maintenance(): void
666
	{
667
		global $context, $modSettings;
668
669
		theme()->getTemplates()->load('ManageAttachments');
670
		$context['sub_template'] = 'maintenance';
671
672
		// We need our attachments directories...
673
		$attachmentDirectory = new AttachmentsDirectory($modSettings, database());
674
		$attach_dirs = $attachmentDirectory->getPaths();
675
676
		// Get the number of attachments...
677
		$context['num_attachments'] = comma_format(getAttachmentCountByType('attachments'), 0);
678
679
		// Also get the avatar amount...
680
		$context['num_avatars'] = comma_format(getAttachmentCountByType('avatars'), 0);
681
682
		// Total size of attachments
683
		$context['attachment_total_size'] = overallAttachmentsSize();
684
685
		// Total size and files from the current attachment dir.
686
		$current_dir = currentAttachDirProperties();
687
688
		// If they specified a limit only...
689
		if ($attachmentDirectory->hasSizeLimit())
690
		{
691
			$context['attachment_space'] = byte_format($attachmentDirectory->remainingSpace($current_dir['size']));
692
		}
693
694
		if ($attachmentDirectory->hasNumFilesLimit())
695
		{
696
			$context['attachment_files'] = comma_format($attachmentDirectory->remainingFiles($current_dir['files']), 0);
697
		}
698
699
		$context['attachment_current_size'] = byte_format($current_dir['size']);
700
		$context['attachment_current_files'] = comma_format($current_dir['files'], 0);
701
		$context['attach_multiple_dirs'] = count($attach_dirs) > 1;
702
		$context['attach_dirs'] = $attach_dirs;
703
		$context['base_dirs'] = empty($modSettings['attachment_basedirectories']) ? [] : Util::unserialize($modSettings['attachment_basedirectories']);
704
		$context['checked'] = $this->_req->getSession('checked', true);
705
706
		if (!empty($_SESSION['results']))
707
		{
708
			$context['results'] = implode('<br />', $this->_req->session->results);
709
			unset($_SESSION['results']);
710
		}
711
	}
712
713
	/**
714
	 * Remove attachments older than a given age.
715
	 *
716
	 * - Called from the maintenance screen by ?action=admin;area=manageattachments;sa=byAge.
717
	 * - It optionally adds a certain text to the messages the attachments were removed from.
718
	 */
719
	public function action_byAge(): void
720
	{
721
		checkSession('post', 'admin');
722
723
		// @todo Ignore messages in topics that are stickied?
724
725
		// Inputs
726
		$age = $this->_req->getPost('age', 'intval', 0);
727
		$notice = $this->_req->getPost('notice', 'trim');
728
729
		// Deleting an attachment?
730
		if (!$this->_req->compareQuery('type', 'avatars', 'trim|strval'))
731
		{
732
			// Get rid of all the old attachments.
733
			$messages = removeAttachments(['attachment_type' => 0, 'poster_time' => (time() - 24 * 60 * 60 * $age)], 'messages', true);
734
735
			// Update the messages to reflect the change.
736
			if (!empty($messages) && !empty($notice))
737
			{
738
				setRemovalNotice($messages, $notice);
739
			}
740
		}
741
		// Remove all the old avatars.
742
		else
743
		{
744
			removeAttachments(['not_id_member' => 0, 'last_login' => (time() - 24 * 60 * 60 * $age)], 'members');
745
		}
746
747
		redirectexit('action=admin;area=manageattachments' . (empty($this->_req->query->avatars) ? ';sa=maintenance' : ';avatars'));
748
	}
749
750
	/**
751
	 * Remove attachments larger than a given size.
752
	 *
753
	 * - Called from the maintenance screen by ?action=admin;area=manageattachments;sa=bySize.
754
	 * - Optionally adds a certain text to the messages the attachments were removed from.
755
	 */
756
	public function action_bySize(): void
757
	{
758
		checkSession('post', 'admin');
759
760
		$size = $this->_req->getPost('size', 'intval', 0);
761
		$notice = $this->_req->getPost('notice', 'trim');
762
763
		// Find humongous attachments.
764
		$messages = removeAttachments(['attachment_type' => 0, 'size' => 1024 * $size], 'messages', true);
765
766
		// And make a note on the post.
767
		if (!empty($messages) && !empty($notice))
768
		{
769
			setRemovalNotice($messages, $notice);
770
		}
771
772
		redirectexit('action=admin;area=manageattachments;sa=maintenance');
773
	}
774
775
	/**
776
	 * Remove a selection of attachments or avatars.
777
	 *
778
	 * - Called from the browse screen as submitted form by ?action=admin;area=manageattachments;sa=remove
779
	 */
780
	public function action_remove(): void
781
	{
782
		global $language;
783
784
		checkSession();
785
786
		$to_remove = $this->_req->getPost('remove', null, []);
787
		if (!empty($to_remove) && is_array($to_remove))
788
		{
789
			// There must be a quicker way to pass this safety test??
790
			$attachments = [];
791
			foreach ($to_remove as $removeID => $dummy)
792
			{
793
				$attachments[] = (int) $removeID;
794
			}
795
796
			if ($this->_req->compareQuery('type', 'avatars', 'trim|strval') && !empty($attachments))
797
			{
798
				removeAttachments(['id_attach' => $attachments]);
799
			}
800
			elseif (!empty($attachments))
801
			{
802
				$messages = removeAttachments(['id_attach' => $attachments], 'messages', true);
803
804
				// And change the message to reflect this.
805
				if (!empty($messages))
806
				{
807
					$mtxt = [];
808
					$lang = new Loader($language, $mtxt, database());
809
					$lang->load('Admin');
810
					setRemovalNotice($messages, $mtxt['attachment_delete_admin']);
811
				}
812
			}
813
		}
814
815
		$sort = $this->_req->getQuery('sort', 'trim|strval', 'date');
816
		$type = $this->_req->getQuery('type', 'trim|strval', 'attachments');
817
		$start = $this->_req->getQuery('start', 'intval', 0);
818
		$desc = $this->_req->hasQuery('desc') ? ';desc' : '';
819
		redirectexit('action=admin;area=manageattachments;sa=browse;' . $type . ';sort=' . $sort . $desc . ';start=' . $start);
820
	}
821
822
	/**
823
	 * Removes all attachments in a single click
824
	 *
825
	 * - Called from the maintenance screen by ?action=admin;area=manageattachments;sa=removeall.
826
	 */
827
	public function action_removeall(): void
828
	{
829
		global $txt;
830
831
		checkSession('get', 'admin');
832
833
		$messages = removeAttachments(['attachment_type' => 0], '', true);
834
835
		$notice = $this->_req->getPost('notice', 'trim|strval', $txt['attachment_delete_admin']);
836
837
		// Add the notice at the end of the changed messages.
838
		if (!empty($messages))
839
		{
840
			setRemovalNotice($messages, $notice);
841
		}
842
843
		redirectexit('action=admin;area=manageattachments;sa=maintenance');
844
	}
845
846
	/**
847
	 * This function will perform many attachment checks and provides ways to fix them
848
	 *
849
	 * What it does:
850
	 *
851
	 * Checks for the following common issues
852
	 * - Orphan Thumbnails
853
	 * - Attachments that have no thumbnails
854
	 * - Attachments that list thumbnails, but actually, don't have any
855
	 * - Attachments list in the wrong_folder
856
	 * - Attachments that don't exist on disk any longer
857
	 * - Attachments that are zero size
858
	 * - Attachments that file size does not match the DB size
859
	 * - Attachments that no longer have a message
860
	 * - Avatars with no members associated with them.
861
	 * - Attachments that are in the attachment folder, but not listed in the DB
862
	 */
863
	public function action_repair(): void
864
	{
865
		global $modSettings, $context, $txt;
866
867
		checkSession('get');
868
869
		// If we choose to cancel, redirect right back.
870
		if ($this->_req->hasPost('cancel'))
871
		{
872
			redirectexit('action=admin;area=manageattachments;sa=maintenance');
873
		}
874
875
		// Try to give us a while to sort this out...
876
		detectServer()->setTimeLimit(600);
877
878
		$this->step = $this->_req->getQuery('step', 'intval', 0);
879
		$this->substep = $this->_req->getQuery('substep', 'intval', 0);
880
		$this->starting_substep = $this->substep;
881
882
		// Don't recall the session just in case.
883
		if ($this->step === 0 && $this->substep === 0)
884
		{
885
			unset($_SESSION['attachments_to_fix'], $_SESSION['attachments_to_fix2']);
886
887
			// If we're actually fixing stuff - work out what.
888
			if ($this->_req->hasQuery('fixErrors'))
889
			{
890
				// Nothing?
891
				$to_fix_post = $this->_req->getPost('to_fix', null, []);
892
				if (empty($to_fix_post))
893
				{
894
					redirectexit('action=admin;area=manageattachments;sa=maintenance');
895
				}
896
897
				foreach ((array) $to_fix_post as $value)
898
				{
899
					$_SESSION['attachments_to_fix'][] = $value;
900
				}
901
			}
902
		}
903
904
		// All the valid problems are here:
905
		$context['repair_errors'] = [
906
			'missing_thumbnail_parent' => 0,
907
			'parent_missing_thumbnail' => 0,
908
			'file_missing_on_disk' => 0,
909
			'file_wrong_size' => 0,
910
			'file_size_of_zero' => 0,
911
			'attachment_no_msg' => 0,
912
			'avatar_no_member' => 0,
913
			'wrong_folder' => 0,
914
			'missing_extension' => 0,
915
			'files_without_attachment' => 0,
916
		];
917
918
		$to_fix = empty($_SESSION['attachments_to_fix']) ? [] : $_SESSION['attachments_to_fix'];
919
		$context['repair_errors'] = $_SESSION['attachments_to_fix2'] ?? $context['repair_errors'];
920
		$fix_errors = $this->_req->hasQuery('fixErrors');
921
922
		// Get stranded thumbnails.
923
		if ($this->step <= 0)
924
		{
925
			$thumbnails = getMaxThumbnail();
926
927
			for (; $this->substep < $thumbnails; $this->substep += 500)
928
			{
929
				$removed = findOrphanThumbnails($this->substep, $fix_errors, $to_fix);
930
				$context['repair_errors']['missing_thumbnail_parent'] += count($removed);
931
932
				pauseAttachmentMaintenance($to_fix, $thumbnails, $this->starting_substep, $this->substep, $this->step, $fix_errors);
933
			}
934
935
			// Done here, on to the next
936
			$this->step = 1;
937
			$this->substep = 0;
938
			pauseAttachmentMaintenance($to_fix, 0, $this->starting_substep, $this->substep, $this->step, $fix_errors);
939
		}
940
941
		// Find parents who think they have thumbnails, but actually, don't.
942
		if ($this->step <= 1)
943
		{
944
			$thumbnails = maxNoThumb();
945
946
			for (; $this->substep < $thumbnails; $this->substep += 500)
947
			{
948
				$to_update = findParentsOrphanThumbnails($this->substep, $fix_errors, $to_fix);
949
				$context['repair_errors']['parent_missing_thumbnail'] += count($to_update);
950
951
				pauseAttachmentMaintenance($to_fix, $thumbnails, $this->starting_substep, $this->substep, $this->step, $fix_errors);
952
			}
953
954
			// Another step done, but many to go
955
			$this->step = 2;
956
			$this->substep = 0;
957
			pauseAttachmentMaintenance($to_fix, 0, $this->starting_substep, $this->substep, $this->step, $fix_errors);
958
		}
959
960
		// This may take forever, I'm afraid, but life sucks... recount EVERY attachment!
961
		if ($this->step <= 2)
962
		{
963
			$thumbnails = maxAttachment();
964
965
			for (; $this->substep < $thumbnails; $this->substep += 250)
966
			{
967
				$repair_errors = repairAttachmentData($this->substep, $fix_errors, $to_fix);
968
969
				foreach ($repair_errors as $key => $value)
970
				{
971
					$context['repair_errors'][$key] += $value;
972
				}
973
974
				pauseAttachmentMaintenance($to_fix, $thumbnails, $this->starting_substep, $this->substep, $this->step, $fix_errors);
975
			}
976
977
			// And onward we go
978
			$this->step = 3;
979
			$this->substep = 0;
980
			pauseAttachmentMaintenance($to_fix, 0, $this->starting_substep, $this->substep, $this->step, $fix_errors);
981
		}
982
983
		// Get avatars with no members associated with them.
984
		if ($this->step <= 3)
985
		{
986
			$thumbnails = maxAttachment();
987
988
			for (; $this->substep < $thumbnails; $this->substep += 500)
989
			{
990
				$to_remove = findOrphanAvatars($this->substep, $fix_errors, $to_fix);
991
				$context['repair_errors']['avatar_no_member'] += count($to_remove);
992
993
				pauseAttachmentMaintenance($to_fix, $thumbnails, $this->starting_substep, $this->substep, $this->step, $fix_errors);
994
			}
995
996
			$this->step = 4;
997
			$this->substep = 0;
998
			pauseAttachmentMaintenance($to_fix, 0, $this->starting_substep, $this->substep, $this->step, $fix_errors);
999
		}
1000
1001
		// What about attachments, who are missing a message :'(
1002
		if ($this->step <= 4)
1003
		{
1004
			$thumbnails = maxAttachment();
1005
1006
			for (; $this->substep < $thumbnails; $this->substep += 500)
1007
			{
1008
				$to_remove = findOrphanAttachments($this->substep, $fix_errors, $to_fix);
1009
				$context['repair_errors']['attachment_no_msg'] += count($to_remove);
1010
1011
				pauseAttachmentMaintenance($to_fix, $thumbnails, $this->starting_substep, $this->substep, $this->step, $fix_errors);
1012
			}
1013
1014
			$this->step = 5;
1015
			$this->substep = 0;
1016
			pauseAttachmentMaintenance($to_fix, 0, $this->starting_substep, $this->substep, $this->step, $fix_errors);
1017
		}
1018
1019
		// What about files that are not recorded in the database?
1020
		if ($this->step <= 5)
1021
		{
1022
			// Just use the current path for temp files.
1023
			if (!is_array($modSettings['attachmentUploadDir']))
1024
			{
1025
				$modSettings['attachmentUploadDir'] = Util::unserialize($modSettings['attachmentUploadDir']);
1026
			}
1027
1028
			$attach_dirs = $modSettings['attachmentUploadDir'];
1029
			$current_check = 0;
1030
			$max_checks = 500;
1031
			$attachment_count = getAttachmentCountFromDisk();
1032
1033
			$files_checked = empty($this->substep) ? 0 : $this->substep;
1034
			foreach ($attach_dirs as $attach_dir)
1035
			{
1036
				try
1037
				{
1038
					$files = new FilesystemIterator($attach_dir, FilesystemIterator::SKIP_DOTS);
1039
					foreach ($files as $file)
1040
					{
1041
						if ($file->getFilename() === '.htaccess')
1042
						{
1043
							continue;
1044
						}
1045
1046
						if ($files_checked <= $current_check)
1047
						{
1048
							// Temporary file, get rid of it!
1049
							if (str_contains($file->getFilename(), 'post_tmp_'))
1050
							{
1051
								// Temp file is more than 5 hours old!
1052
								if ($file->getMTime() < time() - 18000)
1053
								{
1054
									$this->file_functions->delete($file->getPathname());
1055
								}
1056
							}
1057
							// That should be an attachment, let's check if we have it in the database
1058
							elseif (str_contains($file->getFilename(), '_'))
1059
							{
1060
								$attachID = (int) substr($file->getFilename(), 0, strpos($file->getFilename(), '_'));
1061
								if ($attachID !== 0 && !validateAttachID($attachID))
1062
								{
1063
									if ($fix_errors && in_array('files_without_attachment', $to_fix))
1064
									{
1065
										$this->file_functions->delete($file->getPathname());
1066
									}
1067
									else
1068
									{
1069
										$context['repair_errors']['files_without_attachment']++;
1070
									}
1071
								}
1072
							}
1073
							elseif ($file->getFilename() !== 'index.php' && !$file->isDir())
1074
							{
1075
								if ($fix_errors && in_array('files_without_attachment', $to_fix, true))
1076
								{
1077
									$this->file_functions->delete($file->getPathname());
1078
								}
1079
								else
1080
								{
1081
									$context['repair_errors']['files_without_attachment']++;
1082
								}
1083
							}
1084
						}
1085
1086
						$current_check++;
1087
						$this->substep = $current_check;
1088
1089
						if ($current_check - $files_checked >= $max_checks)
1090
						{
1091
							pauseAttachmentMaintenance($to_fix, $attachment_count, $this->starting_substep, $this->substep, $this->step, $fix_errors);
1092
						}
1093
					}
1094
				}
1095
				catch (UnexpectedValueException)
1096
				{
1097
					// @todo for now do nothing...
1098
				}
1099
			}
1100
1101
			$this->step = 5;
1102
			$this->substep = 0;
1103
			pauseAttachmentMaintenance($to_fix, 0, $this->starting_substep, $this->substep, $this->step, $fix_errors);
1104
		}
1105
1106
		// Got here we must be doing well - just the template! :D
1107
		$context['page_title'] = $txt['repair_attachments'];
1108
		$context[$context['admin_menu_name']]['current_subsection'] = 'maintenance';
1109
		$context['sub_template'] = 'attachment_repair';
1110
1111
		// What stage are we at?
1112
		$context['completed'] = $fix_errors;
1113
		$context['errors_found'] = false;
1114
		foreach ($context['repair_errors'] as $number)
1115
		{
1116
			if (!empty($number))
1117
			{
1118
				$context['errors_found'] = true;
1119
				break;
1120
			}
1121
		}
1122
	}
1123
1124
	/**
1125
	 * Function called in-between each round of attachments and avatar repairs.
1126
	 *
1127
	 * What it does:
1128
	 *
1129
	 * - Called by repairAttachments().
1130
	 * - If repairAttachments() has more steps added, this function needs to be updated!
1131
	 *
1132
	 * @param array $to_fix attachments to fix
1133
	 * @param int $max_substep = 0
1134
	 * @throws \ElkArte\Exceptions\Exception
1135
	 * @todo Move to ManageAttachments.subs.php
1136
	 */
1137
	private function _pauseAttachmentMaintenance(array $to_fix, int $max_substep = 0): void
1138
	{
1139
		global $context, $txt, $time_start;
1140
1141
		// Try to get more time...
1142
		detectServer()->setTimeLimit(600);
1143
1144
		// Have we already used our maximum time?
1145
		if (microtime(true) - $time_start < 3 || $this->starting_substep == $this->substep)
1146
		{
1147
			return;
1148
		}
1149
1150
		$context['continue_get_data'] = '?action=admin;area=manageattachments;sa=repair' . ($this->_req->hasQuery('fixErrors') ? ';fixErrors' : '') . ';step=' . $this->step . ';substep=' . $this->substep . ';' . $context['session_var'] . '=' . $context['session_id'];
1151
		$context['page_title'] = $txt['not_done_title'];
1152
		$context['continue_post_data'] = '';
1153
		$context['continue_countdown'] = '2';
1154
		$context['sub_template'] = 'not_done';
1155
1156
		// Specific items to not break this template!
1157
		$context[$context['admin_menu_name']]['current_subsection'] = 'maintenance';
1158
1159
		// Change these two if more steps are added!
1160
		if (empty($max_substep))
1161
		{
1162
			$context['continue_percent'] = round(($this->step * 100) / 25);
1163
		}
1164
		else
1165
		{
1166
			$context['continue_percent'] = round(($this->step * 100 + ($this->substep * 100) / $max_substep) / 25);
1167
		}
1168
1169
		// Never more than 100%!
1170
		$context['continue_percent'] = min($context['continue_percent'], 100);
1171
1172
		// Save the necessary information for the next look
1173
		$_SESSION['attachments_to_fix'] = $to_fix;
1174
		$_SESSION['attachments_to_fix2'] = $context['repair_errors'];
1175
1176
		obExit();
1177
	}
1178
1179
	/**
1180
	 * This function lists and allows updating of multiple attachments paths.
1181
	 */
1182
	public function action_attachpaths(): void
1183
	{
1184
		global $modSettings, $context, $txt;
1185
1186
		$attachmentsDir = new AttachmentsDirectory($modSettings, database());
1187
		$errors = [];
1188
1189
		// Saving or changing attachment paths
1190
		if ($this->_req->hasPost('save'))
1191
		{
1192
			$this->_savePaths($attachmentsDir);
1193
		}
1194
1195
		// Saving a base directory?
1196
		if ($this->_req->hasPost('save2'))
1197
		{
1198
			$this->_saveBasePaths($attachmentsDir);
1199
		}
1200
1201
		// Have some errors to show?
1202
		if (isset($_SESSION['errors']))
1203
		{
1204
			if (is_array($_SESSION['errors']))
1205
			{
1206
				if (!empty($_SESSION['errors']['dir']))
1207
				{
1208
					foreach ($_SESSION['errors']['dir'] as $error)
1209
					{
1210
						$errors['dir'][] = Util::htmlspecialchars($error, ENT_QUOTES);
1211
					}
1212
				}
1213
1214
				if (!empty($_SESSION['errors']['base']))
1215
				{
1216
					foreach ($_SESSION['errors']['base'] as $error)
1217
					{
1218
						$errors['base'][] = Util::htmlspecialchars($error, ENT_QUOTES);
1219
					}
1220
				}
1221
			}
1222
1223
			unset($_SESSION['errors']);
1224
		}
1225
1226
		// Show the list of base and path directories + any errors generated
1227
		$listOptions = [
1228
			'id' => 'attach_paths',
1229
			'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'attachpaths', '{sesstion_data}']),
1230
			'title' => $txt['attach_paths'],
1231
			'get_items' => [
1232
				'function' => 'list_getAttachDirs',
1233
			],
1234
			'columns' => [
1235
				'current_dir' => [
1236
					'header' => [
1237
						'value' => $txt['attach_current'],
1238
						'class' => 'centertext',
1239
					],
1240
					'data' => [
1241
						'function' => static fn($rowData) => '<input type="radio" name="current_dir" value="' . $rowData['id'] . '" ' . ($rowData['current'] ? ' checked="checked"' : '') . (empty($rowData['disable_current']) ? '' : ' disabled="disabled"') . ' class="input_radio" />',
1242
						'class' => 'grid8 centertext',
1243
					],
1244
				],
1245
				'path' => [
1246
					'header' => [
1247
						'value' => $txt['attach_path'],
1248
					],
1249
					'data' => [
1250
						'function' => static fn($rowData) => '
1251
							<input type="hidden" name="dirs[' . $rowData['id'] . ']" value="' . $rowData['path'] . '" />
1252
							<input type="text" size="40" name="dirs[' . $rowData['id'] . ']" value="' . $rowData['path'] . '"' . ((empty($rowData['disable_base_dir']) && empty($rowData['disable_current'])) ? '' : ' disabled="disabled"') . ' class="input_text"/>',
1253
						'class' => 'grid50',
1254
					],
1255
				],
1256
				'current_size' => [
1257
					'header' => [
1258
						'value' => $txt['attach_current_size'],
1259
					],
1260
					'data' => [
1261
						'db' => 'current_size',
1262
					],
1263
				],
1264
				'num_files' => [
1265
					'header' => [
1266
						'value' => $txt['attach_num_files'],
1267
					],
1268
					'data' => [
1269
						'db' => 'num_files',
1270
					],
1271
				],
1272
				'status' => [
1273
					'header' => [
1274
						'value' => $txt['attach_dir_status'],
1275
					],
1276
					'data' => [
1277
						'db' => 'status',
1278
						'class' => 'grid20',
1279
					],
1280
				],
1281
			],
1282
			'form' => [
1283
				'href' => getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'attachpaths', '{sesstion_data}']),
1284
			],
1285
			'additional_rows' => [
1286
				[
1287
					'class' => 'submitbutton',
1288
					'position' => 'below_table_data',
1289
					'value' => '
1290
					<input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '" />
1291
					<input type="submit" name="new_path" value="' . $txt['attach_add_path'] . '" />
1292
					<input type="submit" name="save" value="' . $txt['save'] . '" />',
1293
				],
1294
				empty($errors['dir']) ? [
1295
					'position' => 'top_of_list',
1296
					'value' => $txt['attach_dir_desc'],
1297
					'style' => 'padding: 5px 10px;',
1298
					'class' => 'description'
1299
				] : [
1300
					'position' => 'top_of_list',
1301
					'value' => $txt['attach_dir_save_problem'] . '<br />' . implode('<br />', $errors['dir']),
1302
					'style' => 'padding-left: 2.75em;',
1303
					'class' => 'warningbox',
1304
				],
1305
			],
1306
		];
1307
		createList($listOptions);
1308
1309
		if (!empty($modSettings['attachment_basedirectories']))
1310
		{
1311
			$listOptions2 = [
1312
				'id' => 'base_paths',
1313
				'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'attachpaths', '{sesstion_data}']),
1314
				'title' => $txt['attach_base_paths'],
1315
				'get_items' => [
1316
					'function' => 'list_getBaseDirs',
1317
				],
1318
				'columns' => [
1319
					'current_dir' => [
1320
						'header' => [
1321
							'value' => $txt['attach_current'],
1322
							'class' => 'centertext',
1323
						],
1324
						'data' => [
1325
							'function' => static fn($rowData) => '<input type="radio" name="current_base_dir" value="' . $rowData['id'] . '" ' . ($rowData['current'] ? ' checked="checked"' : '') . ' class="input_radio" />',
1326
							'class' => 'grid8 centertext',
1327
						],
1328
					],
1329
					'path' => [
1330
						'header' => [
1331
							'value' => $txt['attach_path'],
1332
						],
1333
						'data' => [
1334
							'db' => 'path',
1335
							'class' => 'grid50',
1336
						],
1337
					],
1338
					'num_dirs' => [
1339
						'header' => [
1340
							'value' => $txt['attach_num_dirs'],
1341
						],
1342
						'data' => [
1343
							'db' => 'num_dirs',
1344
						],
1345
					],
1346
					'status' => [
1347
						'header' => [
1348
							'value' => $txt['attach_dir_status'],
1349
						],
1350
						'data' => [
1351
							'db' => 'status',
1352
							'class' => 'grid20',
1353
						],
1354
					],
1355
				],
1356
				'form' => [
1357
					'href' => getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'attachpaths', '{sesstion_data}']),
1358
				],
1359
				'additional_rows' => [
1360
					[
1361
						'class' => 'submitbutton',
1362
						'position' => 'below_table_data',
1363
						'value' => '
1364
							<input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '" />
1365
							<input type="submit" name="new_base_path" value="' . $txt['attach_add_path'] . '" />
1366
							<input type="submit" name="save2" value="' . $txt['save'] . '" />',
1367
					],
1368
					empty($errors['base']) ? [
1369
						'position' => 'top_of_list',
1370
						'value' => $txt['attach_dir_base_desc'],
1371
						'style' => 'padding: 5px 10px;',
1372
						'class' => 'description'
1373
					] : [
1374
						'position' => 'top_of_list',
1375
						'value' => $txt['attach_dir_save_problem'] . '<br />' . implode('<br />', $errors['base']),
1376
						'style' => 'padding-left: 2.75em',
1377
						'class' => 'warningbox',
1378
					],
1379
				],
1380
			];
1381
			createList($listOptions2);
1382
		}
1383
1384
		// Fix up our template.
1385
		$context[$context['admin_menu_name']]['current_subsection'] = 'attachpaths';
1386
		$context['page_title'] = $txt['attach_path_manage'];
1387
	}
1388
1389
	/**
1390
	 * Saves any changes or additions to the attachment paths
1391
	 *
1392
	 * @param AttachmentsDirectory $attachmentsDir
1393
	 * @return void
1394
	 */
1395
	private function _savePaths(AttachmentsDirectory $attachmentsDir): void
1396
	{
1397
		global $txt, $context;
1398
1399
		checkSession();
1400
1401
		$current_dir = $this->_req->getPost('current_dir', 'intval', 1);
1402
		$dirs = $this->_req->getPost('dirs', null, []);
1403
		$new_dirs = [];
1404
1405
		// Can't use these directories for attachments
1406
		require_once(SUBSDIR . '/Themes.subs.php');
1407
		$themes = installedThemes();
1408
		$reserved_dirs = [BOARDDIR, SOURCEDIR, SUBSDIR, CONTROLLERDIR, CACHEDIR, EXTDIR, LANGUAGEDIR, ADMINDIR, ADDONSDIR, ELKARTEDIR];
1409
		foreach ($themes as $theme)
1410
		{
1411
			$reserved_dirs[] = $theme['theme_dir'];
1412
		}
1413
1414
		foreach ($dirs as $id => $path)
1415
		{
1416
			$id = (int) $id;
1417
			if ($id < 1)
1418
			{
1419
				continue;
1420
			}
1421
1422
			// If it doesn't look like a directory, probably is not a directory
1423
			$real_path = rtrim(trim($path), DIRECTORY_SEPARATOR);
1424
			if (preg_match('~[/\\\\]~', $real_path) !== 1)
1425
			{
1426
				$real_path = realpath(BOARDDIR . DIRECTORY_SEPARATOR . ltrim($real_path, DIRECTORY_SEPARATOR));
1427
			}
1428
1429
			// Hmm, a new path maybe?
1430
			if ($attachmentsDir->directoryExists($id) === false)
1431
			{
1432
				// or is it?
1433
				if ($attachmentsDir->directoryExists($path))
1434
				{
1435
					$errors[] = $path . ': ' . $txt['attach_dir_duplicate_msg'];
1436
					continue;
1437
				}
1438
1439
				// or is it a system dir?
1440
				if (in_array($real_path, $reserved_dirs))
1441
				{
1442
					$errors[] = $real_path . ': ' . $txt['attach_dir_reserved'];
1443
					continue;
1444
				}
1445
1446
				// OK, so let's try to create it then.
1447
				try
1448
				{
1449
					$attachmentsDir->createDirectory($path);
1450
				}
1451
				catch (Exception $e)
1452
				{
1453
					$errors[] = $path . ': ' . $txt[$e->getMessage()];
1454
					continue;
1455
				}
1456
			}
1457
1458
			// Changing a directory name or trying to remove it entirely?
1459
			try
1460
			{
1461
				if (!empty($path))
1462
				{
1463
					$attachmentsDir->rename($id, $real_path);
1464
				}
1465
				elseif ($attachmentsDir->delete($id, $real_path) === true)
1466
				{
1467
					// Don't add it back, it's now gone!
1468
					continue;
1469
				}
1470
			}
1471
			catch (Exception $e)
1472
			{
1473
				$errors[] = $real_path . ': ' . $txt[$e->getMessage()];
1474
			}
1475
1476
			$new_dirs[$id] = $real_path;
1477
		}
1478
1479
		// We need to make sure the current directory is right.
1480
		if (empty($current_dir) && !empty($attachmentsDir->currentDirectoryId()))
1481
		{
1482
			$current_dir = $attachmentsDir->currentDirectoryId();
1483
		}
1484
1485
		// Find the current directory if there's no value carried,
1486
		if (empty($current_dir) || empty($new_dirs[$current_dir]))
1487
		{
1488
			$current_dir = $attachmentsDir->currentDirectoryId();
1489
		}
1490
1491
		// If the user wishes to go back, update the last_dir array
1492
		if ($current_dir !== $attachmentsDir->currentDirectoryId())
1493
		{
1494
			$attachmentsDir->updateLastDirs($current_dir);
1495
		}
1496
1497
		// Going back to just one path?
1498
		if (count($new_dirs) === 1)
1499
		{
1500
			// We might need to reset the paths. This loop will just loop through once.
1501
			foreach ($new_dirs as $id => $dir)
1502
			{
1503
				if ($id !== 1)
1504
				{
1505
					updateAttachmentIdFolder($id, 1);
1506
				}
1507
1508
				$update = ['currentAttachmentUploadDir' => 1, 'attachmentUploadDir' => serialize([1 => $dir]),];
1509
			}
1510
		}
1511
		else
1512
		{
1513
			// Save it to the database.
1514
			$update = ['currentAttachmentUploadDir' => $current_dir, 'attachmentUploadDir' => serialize($new_dirs),];
1515
		}
1516
1517
		if (!empty($update))
1518
		{
1519
			updateSettings($update);
1520
		}
1521
1522
		if (!empty($errors))
1523
		{
1524
			$_SESSION['errors']['dir'] = $errors;
1525
		}
1526
1527
		redirectexit('action=admin;area=manageattachments;sa=attachpaths;' . $context['session_var'] . '=' . $context['session_id']);
1528
	}
1529
1530
	/**
1531
	 * Saves changes to the attachment base directory section
1532
	 *
1533
	 * @param AttachmentsDirectory $attachmentsDir
1534
	 * @return void
1535
	 */
1536
	private function _saveBasePaths(AttachmentsDirectory $attachmentsDir): void
1537
	{
1538
		global $modSettings, $txt, $context;
1539
1540
		checkSession();
1541
1542
		$attachmentBaseDirectories = $attachmentsDir->getBaseDirs();
1543
		$attachmentUploadDir = $attachmentsDir->getPaths();
1544
		$current_base_dir = $this->_req->getPost('current_base_dir', 'intval', 1);
1545
		$new_base_dir = $this->_req->getPost('new_base_dir', 'trim|htmlspecialchars', '');
1546
		$base_dirs = $this->_req->getPost('base_dir');
1547
1548
		// Changing the current base directory?
1549
		if (empty($new_base_dir) && !empty($current_base_dir) && $modSettings['basedirectory_for_attachments'] !== $attachmentUploadDir[$current_base_dir])
1550
		{
1551
			$update = ['basedirectory_for_attachments' => $attachmentUploadDir[$current_base_dir]];
1552
		}
1553
1554
		// Modifying / Removing a basedir entry
1555
		if (isset($base_dirs))
1556
		{
1557
			// Renaming the base dir, we can try
1558
			foreach ($base_dirs as $id => $dir)
1559
			{
1560
				if (!empty($dir) && $dir !== $attachmentUploadDir[$id] && @rename($attachmentUploadDir[$id], $dir))
1561
				{
1562
					$attachmentUploadDir[$id] = $dir;
1563
					$attachmentBaseDirectories[$id] = $dir;
1564
					$update = (['attachmentUploadDir' => serialize($attachmentUploadDir), 'attachment_basedirectories' => serialize($attachmentBaseDirectories), 'basedirectory_for_attachments' => $attachmentUploadDir[$current_base_dir],]);
1565
				}
1566
1567
				// Or remove it (from selection only)
1568
				if (empty($dir))
1569
				{
1570
					// Can't remove the currently active one
1571
					if ($id === $current_base_dir)
1572
					{
1573
						$errors[] = $attachmentUploadDir[$id] . ': ' . $txt['attach_dir_is_current'];
1574
						continue;
1575
					}
1576
1577
					// Removed from selection (not disc)
1578
					unset($attachmentBaseDirectories[$id]);
1579
					$update = ['attachment_basedirectories' => empty($attachmentBaseDirectories) ? '' : serialize($attachmentBaseDirectories), 'basedirectory_for_attachments' => $attachmentUploadDir[$current_base_dir] ?? '',];
1580
				}
1581
			}
1582
		}
1583
1584
		// Adding a new one?
1585
		if (!empty($new_base_dir))
1586
		{
1587
			$current_dir = $attachmentsDir->currentDirectoryId();
1588
1589
			// If it does not exist, try to create it.
1590
			if (!in_array($new_base_dir, $attachmentUploadDir))
1591
			{
1592
				try
1593
				{
1594
					$attachmentsDir->createDirectory($new_base_dir);
1595
				}
1596
				catch (Exception)
1597
				{
1598
					$errors[] = $new_base_dir . ': ' . $txt['attach_dir_base_no_create'];
1599
				}
1600
			}
1601
1602
			// Find the new key
1603
			$modSettings['currentAttachmentUploadDir'] = array_search($new_base_dir, $attachmentsDir->getPaths(), true);
1604
			if (!in_array($new_base_dir, $attachmentBaseDirectories))
1605
			{
1606
				$attachmentBaseDirectories[$modSettings['currentAttachmentUploadDir']] = $new_base_dir;
1607
			}
1608
1609
			ksort($attachmentBaseDirectories);
1610
			$update = ['attachment_basedirectories' => serialize($attachmentBaseDirectories), 'basedirectory_for_attachments' => $new_base_dir, 'currentAttachmentUploadDir' => $current_dir,];
1611
		}
1612
1613
		$_SESSION['errors']['base'] = $errors ?? null;
1614
1615
		if (!empty($update))
1616
		{
1617
			updateSettings($update);
1618
		}
1619
1620
		redirectexit('action=admin;area=manageattachments;sa=attachpaths;' . $context['session_var'] . '=' . $context['session_id']);
1621
	}
1622
1623
	/**
1624
	 * Maintenance function to move attachments from one directory to another
1625
	 */
1626
	public function action_transfer(): void
1627
	{
1628
		global $modSettings, $txt;
1629
1630
		checkSession();
1631
1632
		$attachmentsDir = new AttachmentsDirectory($modSettings, database());
1633
1634
		// Clean the inputs
1635
		$this->from = $this->_req->getPost('from', 'intval');
1636
		$this->auto = $this->_req->getPost('auto', 'intval', 0);
1637
		$this->to = $this->_req->getPost('to', 'intval');
1638
		$emptyIt = !empty($this->_req->getPost('empty_it', null, ''));
1639
		$start = $emptyIt ? 0 : (int) ($modSettings['attachmentDirFileLimit'] ?? 0);
1640
		$_SESSION['checked'] = $emptyIt;
1641
1642
		// Prepare for the moving
1643
		$limit = 501;
1644
		$results = [];
1645
		$dir_files = 0;
1646
		$current_progress = 0;
1647
		$total_moved = 0;
1648
		$total_not_moved = 0;
1649
		$total_progress = 0;
1650
1651
		// Need to know where we are moving things from
1652
		if (empty($this->from) || (empty($this->auto) && empty($this->to)))
1653
		{
1654
			$results[] = $txt['attachment_transfer_no_dir'];
1655
		}
1656
1657
		// Same location, that's easy
1658
		if ($this->from === $this->to)
1659
		{
1660
			$results[] = $txt['attachment_transfer_same_dir'];
1661
		}
1662
1663
		// No errors so determine how many we may have to move
1664
		if (empty($results))
1665
		{
1666
			// Get the total file count for the progress bar.
1667
			$total_progress = getFolderAttachmentCount($this->from);
1668
			$total_progress -= $start;
1669
1670
			if ($total_progress < 1)
1671
			{
1672
				$results[] = $txt['attachment_transfer_no_find'];
1673
			}
1674
		}
1675
1676
		// Nothing to move (no files in a source or below the max limit)
1677
		if (empty($results))
1678
		{
1679
			// Moving them automatically?
1680
			if (!empty($this->auto))
1681
			{
1682
				$modSettings['automanage_attachments'] = 1;
1683
				$tmpattachmentUploadDir = Util::unserialize($modSettings['attachmentUploadDir']);
1684
1685
				// Create a subdirectory off the root or from an attachment directory?
1686
				$modSettings['use_subdirectories_for_attachments'] = $this->auto == -1 ? 0 : 1;
1687
				$modSettings['basedirectory_for_attachments'] = serialize($this->auto > 0 ? $tmpattachmentUploadDir[$this->auto] : [1 => $modSettings['basedirectory_for_attachments']]);
1688
1689
				// Finally, where do they need to go?
1690
				$attachmentDirectory = new AttachmentsDirectory($modSettings, database());
1691
				$attachmentDirectory->automanage_attachments_check_directory(true);
1692
				$new_dir = $attachmentDirectory->currentDirectoryId();
1693
			}
1694
			// Or to a specified directory
1695
			else
1696
			{
1697
				$new_dir = $this->to;
1698
			}
1699
1700
			$modSettings['currentAttachmentUploadDir'] = $new_dir;
1701
			$break = false;
1702
			while (!$break)
1703
			{
1704
				detectServer()->setTimeLimit(300);
1705
1706
				// If limits are set, get the file count and size for the destination folder
1707
				if ($dir_files <= 0 && (!empty($modSettings['attachmentDirSizeLimit']) || !empty($modSettings['attachmentDirFileLimit'])))
1708
				{
1709
					$current_dir = attachDirProperties($new_dir);
1710
					$dir_files = $current_dir['files'];
1711
					$dir_size = $current_dir['size'];
1712
				}
1713
1714
				// Find some attachments to move
1715
				[$tomove_count, $tomove] = findAttachmentsToMove($this->from, $start, $limit);
1716
1717
				// Nothing found to move
1718
				if ($tomove_count === 0)
1719
				{
1720
					if ($current_progress === 0)
1721
					{
1722
						$results[] = $txt['attachment_transfer_no_find'];
1723
					}
1724
1725
					break;
1726
				}
1727
1728
				// No more to move after this batch than set the finished flag.
1729
				if ($tomove_count < $limit)
1730
				{
1731
					$break = true;
1732
				}
1733
1734
				// Move them
1735
				$moved = [];
1736
				$dir_size = empty($dir_size) ? 0 : $dir_size;
1737
				$limiting_by_size_num = $attachmentsDir->hasSizeLimit() || $attachmentsDir->hasNumFilesLimit();
1738
1739
				foreach ($tomove as $row)
1740
				{
1741
					$source = getAttachmentFilename($row['filename'], $row['id_attach'], $row['id_folder'], false, $row['file_hash']);
1742
					$dest = $attachmentsDir->getPathById($new_dir) . '/' . basename($source);
1743
1744
					// Size and file count check
1745
					if ($limiting_by_size_num)
1746
					{
1747
						$dir_files++;
1748
						$dir_size += empty($row['size']) ? filesize($source) : $row['size'];
1749
1750
						// If we've reached a directory limit. Do something if we are in auto mode, otherwise set an error.
1751
						if ($attachmentsDir->remainingSpace($dir_size) === 0 || $attachmentsDir->remainingFiles($dir_files) === 0)
1752
						{
1753
							// Since we're in auto mode. Create a new folder and reset the counters.
1754
							$create_result = $attachmentsDir->manageBySpace();
1755
							if ($create_result === true)
1756
							{
1757
								$results[] = sprintf($txt['attachments_transfered'], $total_moved, $attachmentsDir->getPathById($new_dir));
1758
								if ($total_not_moved !== 0)
1759
								{
1760
									$results[] = sprintf($txt['attachments_not_transfered'], $total_not_moved);
1761
								}
1762
1763
								$dir_files = 0;
1764
								$total_moved = 0;
1765
								$total_not_moved = 0;
1766
1767
								$break = false;
1768
							}
1769
							// Hmm, not in auto. Time to bail out then...
1770
							else
1771
							{
1772
								$results[] = $txt['attachment_transfer_no_room'];
1773
								$break = true;
1774
							}
1775
							break;
1776
						}
1777
					}
1778
1779
					// Actually move the file
1780
					if (@rename($source, $dest))
1781
					{
1782
						$total_moved++;
1783
						$current_progress++;
1784
						$moved[] = $row['id_attach'];
1785
					}
1786
					else
1787
					{
1788
						$total_not_moved++;
1789
					}
1790
				}
1791
1792
				// Update the database to reflect the new file location
1793
				if (!empty($moved))
1794
				{
1795
					moveAttachments($moved, $new_dir);
1796
				}
1797
1798
				$new_dir = $modSettings['currentAttachmentUploadDir'];
1799
1800
				// Create / update the progress bar.
1801
				// @todo why was this done this way?
1802
				if (!$break)
1803
				{
1804
					$percent_done = min(round($current_progress / $total_progress * 100, 0), 100);
1805
					$progressBar = '
1806
						<div class="progress_bar">
1807
							<div class="green_percent" style="width: ' . $percent_done . '%;">' . $percent_done . '%</div>
1808
						</div>';
1809
1810
					// Write it to a file so it can be displayed
1811
					$fp = fopen(BOARDDIR . '/progress.php', 'wb');
1812
					fwrite($fp, $progressBar);
1813
					fclose($fp);
1814
					usleep(500000);
1815
				}
1816
			}
1817
1818
			$results[] = sprintf($txt['attachments_transfered'], $total_moved, $attachmentsDir->getPathById($new_dir));
1819
			if ($total_not_moved !== 0)
1820
			{
1821
				$results[] = sprintf($txt['attachments_not_transfered'], $total_not_moved);
1822
			}
1823
		}
1824
1825
		// All done, time to clean up
1826
		$_SESSION['results'] = $results;
1827
		$this->file_functions->delete(BOARDDIR . '/progress.php');
1828
1829
		redirectexit('action=admin;area=manageattachments;sa=maintenance#transfer');
1830
	}
1831
}
1832