ManageAttachments::_saveBasePaths()   C
last analyzed

Complexity

Conditions 17
Paths 56

Size

Total Lines 85
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 306

Importance

Changes 0
Metric Value
cc 17
eloc 37
nc 56
nop 1
dl 0
loc 85
ccs 0
cts 31
cp 0
crap 306
rs 5.2166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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