ManageAttachments::_settings()   F
last analyzed

Complexity

Conditions 37
Paths > 20000

Size

Total Lines 125
Code Lines 80

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 51
CRAP Score 44.643

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 80
dl 0
loc 125
rs 0
c 1
b 0
f 0
cc 37
nc 10616832
nop 0
ccs 51
cts 62
cp 0.8226
crap 44.643

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 dev
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 $step;
42
43
	/** @var int substep counter for paused attachment maintenance actions */
44
	public $substep;
45
46
	/** @var int Substep at the beginning of a maintenance loop */
47
	public $starting_substep;
48
49
	/** @var int Current directory key being processed */
50
	public $current_dir;
51
52
	/** @var string Used during transfer of files */
53
	public $from;
54
55
	/** @var string Type of attachment management in use */
56
	public $auto;
57
58
	/** @var string Destination when transferring attachments */
59
	public $to;
60
61
	/** @var \ElkArte\Helper\FileFunctions */
62
	public $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
		// Setup the template stuff we'll probably need.
99
		theme()->getTemplates()->load('ManageAttachments');
100
101
		// All the things we can do with attachments
102
		$subActions = array(
103
			'attachments' => array($this, 'action_attachSettings_display'),
104
			'avatars' => array(
105
				'controller' => ManageAvatars::class,
106
				'function' => 'action_index'),
107
			'attachpaths' => array($this, 'action_attachpaths'),
108
			'browse' => array($this, 'action_browse'),
109
			'byAge' => array($this, 'action_byAge'),
110
			'bySize' => array($this, 'action_bySize'),
111
			'maintenance' => array($this, 'action_maintenance'),
112
			'repair' => array($this, 'action_repair'),
113
			'remove' => array($this, 'action_remove'),
114
			'removeall' => array($this, 'action_removeall'),
115
			'transfer' => array($this, 'action_transfer'),
116
		);
117
118
		// Get ready for some action
119
		$action = new Action('manage_attachments');
120
121
		// 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()
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 (isset($this->_req->query->save))
171
		{
172
			checkSession();
173
174
			if (!empty($this->_req->post->attachmentEnable))
175
			{
176
				enableModules('attachments', array('post', 'display'));
177
			}
178
			else
179
			{
180
				disableModules('attachments', array('post', 'display'));
181
			}
182
183
			// Default/Manual implies no subdirectories
184
			if (empty($this->_req->post->automanage_attachments))
185
			{
186
				$this->_req->post->use_subdirectories_for_attachments = 0;
187
			}
188
189
			// Changing the attachment upload directory
190
			if (isset($this->_req->post->attachmentUploadDir))
191
			{
192
				if (!empty($this->_req->post->attachmentUploadDir)
193
					&& $modSettings['attachmentUploadDir'] !== $this->_req->post->attachmentUploadDir
194
					&& $this->file_functions->fileExists($modSettings['attachmentUploadDir']))
195
				{
196
					rename($modSettings['attachmentUploadDir'], $this->_req->post->attachmentUploadDir);
197
				}
198
199
				$modSettings['attachmentUploadDir'] = array(1 => $this->_req->post->attachmentUploadDir);
200
				$this->_req->post->attachmentUploadDir = serialize($modSettings['attachmentUploadDir']);
201
			}
202
203
			// Adding / changing the sub directory's for attachments
204
			if (!empty($this->_req->post->use_subdirectories_for_attachments))
205
			{
206
				// Make sure we have a base directory defined
207
				if (empty($this->_req->post->basedirectory_for_attachments))
208
				{
209
					$this->_req->post->basedirectory_for_attachments = (empty($modSettings['basedirectory_for_attachments']) ? (BOARDDIR) : $modSettings['basedirectory_for_attachments']);
210
				}
211
212
				// The current base directories that we know
213
				if (!empty($modSettings['attachment_basedirectories']))
214
				{
215
					if (!is_array($modSettings['attachment_basedirectories']))
216
					{
217
						$modSettings['attachment_basedirectories'] = Util::unserialize($modSettings['attachment_basedirectories']);
218
					}
219
				}
220
				else
221
				{
222
					$modSettings['attachment_basedirectories'] = array();
223
				}
224
225
				// Trying to use a nonexistent base directory
226
				if (!empty($this->_req->post->basedirectory_for_attachments) && $attachmentsDir->isBaseDir($this->_req->post->basedirectory_for_attachments) === false)
227
				{
228
					$currentAttachmentUploadDir = $attachmentsDir->currentDirectoryId();
229
230
					// If this is a new directory being defined, attempt to create it
231
					if ($attachmentsDir->directoryExists($this->_req->post->basedirectory_for_attachments) === false)
232
					{
233
						try
234
						{
235
							$attachmentsDir->createDirectory($this->_req->post->basedirectory_for_attachments);
236
						}
237
						catch (Exception)
238
						{
239
							$this->_req->post->basedirectory_for_attachments = $modSettings['basedirectory_for_attachments'];
240
						}
241
					}
242
243
					// The base directory should be in our list of available bases
244
					if (!in_array($this->_req->post->basedirectory_for_attachments, $modSettings['attachment_basedirectories']))
245
					{
246
						$modSettings['attachment_basedirectories'][$modSettings['currentAttachmentUploadDir']] = $this->_req->post->basedirectory_for_attachments;
247
						updateSettings(array('attachment_basedirectories' => serialize($modSettings['attachment_basedirectories']), 'currentAttachmentUploadDir' => $currentAttachmentUploadDir,));
248
249
						$this->_req->post->attachmentUploadDir = serialize($modSettings['attachmentUploadDir']);
250
					}
251
				}
252
			}
253
254
			// Allow or not webp extensions.
255
			if (!empty($this->_req->post->attachment_webp_enable) && strpos($this->_req->post->attachmentExtensions, 'webp') === false)
256
			{
257
				$this->_req->post->attachmentExtensions .= ',webp';
258
			}
259
260
			call_integration_hook('integrate_save_attachment_settings');
261
262
			$settingsForm->setConfigValues((array) $this->_req->post);
263
			$settingsForm->save();
264
			redirectexit('action=admin;area=manageattachments;sa=attachments');
265
		}
266
267
		$context['post_url'] = getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'attachments', 'save']);
268
		$settingsForm->prepare();
269
270
		$context['sub_template'] = 'show_settings';
271
	}
272
273
	/**
274
	 * Retrieve and return the administration settings for attachments.
275
	 *
276
	 * @event integrate_modify_attachment_settings
277
	 */
278
	private function _settings()
279
	{
280
		global $modSettings, $txt, $context, $settings;
281
282
		// Get the current attachment directory.
283
		$attachmentsDir = new AttachmentsDirectory($modSettings, database());
284
		$context['attachmentUploadDir'] = $attachmentsDir->getCurrent();
285
286
		// First time here?
287
		if (empty($modSettings['attachment_basedirectories']) && $modSettings['currentAttachmentUploadDir'] == 1 && (is_array($modSettings['attachmentUploadDir']) && $attachmentsDir->countDirs() == 1))
288
		{
289 2
			$modSettings['attachmentUploadDir'] = $modSettings['attachmentUploadDir'][1];
290
		}
291 2
292
		// If not set, show a default path for the base directory
293
		if (!isset($this->_req->query->save) && empty($modSettings['basedirectory_for_attachments']))
294 2
		{
295 2
			$modSettings['basedirectory_for_attachments'] = $context['attachmentUploadDir'];
296
		}
297
298 2
		$context['valid_upload_dir'] = $this->file_functions->isDir($context['attachmentUploadDir']) && $this->file_functions->isWritable($context['attachmentUploadDir']);
299
300
		if ($attachmentsDir->autoManageEnabled())
301
		{
302
			$context['valid_basedirectory'] = !empty($modSettings['basedirectory_for_attachments']) && $this->file_functions->isWritable($modSettings['basedirectory_for_attachments']);
303
		}
304 2
		else
305
		{
306
			$context['valid_basedirectory'] = true;
307
		}
308
309 2
		// A bit of razzle dazzle with the $txt strings. :)
310
		$txt['basedirectory_for_attachments_warning'] = str_replace('{attach_repair_url}', getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'attachpaths']), $txt['basedirectory_for_attachments_warning']);
311 2
		$txt['attach_current_dir_warning'] = str_replace('{attach_repair_url}', getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'attachpaths']), $txt['attach_current_dir_warning']);
312
313
		$txt['attachment_path'] = $context['attachmentUploadDir'];
314
		$txt['basedirectory_for_attachments_path'] = $modSettings['basedirectory_for_attachments'] ?? '';
315
		$txt['use_subdirectories_for_attachments_note'] = empty($modSettings['attachment_basedirectories']) || empty($modSettings['use_subdirectories_for_attachments']) ? $txt['use_subdirectories_for_attachments_note'] : '';
316
		$txt['attachmentUploadDir_multiple_configure'] = '<a class="linkbutton" href="' . getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'attachpaths']) . '">' . $txt['attachmentUploadDir_multiple_configure'] . '</a>';
317 2
		$txt['attach_current_dir'] = $attachmentsDir->autoManageEnabled() ? $txt['attach_last_dir'] : $txt['attach_current_dir'];
318
		$txt['attach_current_dir_warning'] = $txt['attach_current_dir'] . $txt['attach_current_dir_warning'];
319
		$txt['basedirectory_for_attachments_warning'] = $txt['basedirectory_for_attachments_current'] . $txt['basedirectory_for_attachments_warning'];
320
321 2
		// Perform several checks to determine imaging capabilities.
322 2
		$image = new Image($settings['default_theme_dir'] . '/images/blank.png');
323
		$testImg = Gd2::canUse() || ImageMagick::canUse();
324 2
		$testImgRotate = ImageMagick::canUse() || (Gd2::canUse() && function_exists('exif_read_data'));
325 2
326 2
		// Check on webp support, and correct if wrong
327 2
		$testWebP = $image->hasWebpSupport();
328 2
		if (!$testWebP && !empty($modSettings['attachment_webp_enable']))
329 2
		{
330 2
			updateSettings(['attachment_webp_enable' => 0]);
331
		}
332
333 2
		// Check if the server settings support these upload size values
334
		$post_max_size = ini_get('post_max_size');
335
		$upload_max_filesize = ini_get('upload_max_filesize');
336 2
		$testPM = empty($post_max_size) || memoryReturnBytes($post_max_size) >= (isset($modSettings['attachmentPostLimit']) ? $modSettings['attachmentPostLimit'] * 1024 : 0);
337 2
		$testUM = empty($upload_max_filesize) || memoryReturnBytes($upload_max_filesize) >= (isset($modSettings['attachmentSizeLimit']) ? $modSettings['attachmentSizeLimit'] * 1024 : 0);
338 2
339 2
		// Set some helpful information for the UI
340 2
		$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');
341
		$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');
342
343 2
		$config_vars = array(
344
			array('title', 'attachment_manager_settings'),
345 2
			// Are attachments enabled?
346 2
			array('select', 'attachmentEnable', array($txt['attachmentEnable_deactivate'], $txt['attachmentEnable_enable_all'], $txt['attachmentEnable_disable_new'])),
347
			'',
348 2
			// Directory and size limits.
349 2
			array('select', 'automanage_attachments', array(0 => $txt['attachments_normal'], 1 => $txt['attachments_auto_space'], 2 => $txt['attachments_auto_years'], 3 => $txt['attachments_auto_months'], 4 => $txt['attachments_auto_16'])),
350 2
			array('check', 'use_subdirectories_for_attachments', 'subtext' => $txt['use_subdirectories_for_attachments_note']),
351
			(empty($modSettings['attachment_basedirectories'])
352
				? array('text', 'basedirectory_for_attachments', 40,)
353
				: array('var_message', 'basedirectory_for_attachments', 'message' => 'basedirectory_for_attachments_path', 'invalid' => empty($context['valid_basedirectory']), 'text_label' => (empty($context['valid_basedirectory'])
354 2
					? $txt['basedirectory_for_attachments_warning']
355
					: $txt['basedirectory_for_attachments_current']))
356 2
			),
357
			Util::is_serialized($modSettings['attachmentUploadDir'])
358 2
				? array('var_message', 'attach_current_directory', 'postinput' => $txt['attachmentUploadDir_multiple_configure'], 'message' => 'attachment_path', 'invalid' => empty($context['valid_upload_dir']),
359
				'text_label' => (empty($context['valid_upload_dir'])
360 2
					? $txt['attach_current_dir_warning']
361
					: $txt['attach_current_dir']))
362 2
				: array('text', 'attachmentUploadDir', 'postinput' => $txt['attachmentUploadDir_multiple_configure'], 40, 'invalid' => !$context['valid_upload_dir']),
363 2
			array('int', 'attachmentDirFileLimit', 'subtext' => $txt['zero_for_no_limit'], 6),
364 2
			array('int', 'attachmentDirSizeLimit', 'subtext' => $txt['zero_for_no_limit'], 6, 'postinput' => $txt['kilobyte']),
365
			'',
366 2
			// Posting limits
367 2
			array('int', 'attachmentPostLimit', 'subtext' => $post_max_size_text, 6, 'postinput' => $testPM === false ? $txt['attachment_postsize_warning'] : $txt['kilobyte'], 'invalid' => $testPM === false),
368 2
			array('int', 'attachmentSizeLimit', 'subtext' => $upload_max_filesize_text, 6, 'postinput' => $testUM === false ? $txt['attachment_postsize_warning'] : $txt['kilobyte'], 'invalid' => $testUM === false),
369 2
			array('int', 'attachmentNumPerPostLimit', 'subtext' => $txt['zero_for_no_limit'], 6),
370 2
			'',
371 2
			array('check', 'attachment_webp_enable', 'disabled' => !$testWebP, 'postinput' => $testWebP ? "" : $txt['attachment_webp_enable_na']),
372
			array('check', 'attachment_autorotate', 'disabled' => !$testImgRotate, 'postinput' => $testImgRotate ? '' : $txt['attachment_autorotate_na']),
373
			// Resize limits
374
			array('title', 'attachment_image_resize'),
375
			array('check', 'attachment_image_resize_enabled'),
376
			array('check', 'attachment_image_resize_reformat'),
377 2
			array('text', 'attachment_image_resize_width', 'subtext' => $txt['zero_for_no_limit'], 6, 'postinput' => $txt['attachment_image_resize_post']),
378
			array('text', 'attachment_image_resize_height', 'subtext' => $txt['zero_for_no_limit'], 6, 'postinput' => $txt['attachment_image_resize_post']),
379 2
			// Security Items
380
			array('title', 'attachment_security_settings'),
381
			// Extension checks etc.
382
			array('check', 'attachmentCheckExtensions'),
383
			array('text', 'attachmentExtensions', 40),
384
			'',
385
			// Image checks.
386
			array('warning', $testImg === false ? 'attachment_img_enc_warning' : ''),
387
			array('check', 'attachment_image_reencode'),
388 2
			// Thumbnail settings.
389 2
			array('title', 'attachment_thumbnail_settings'),
390 2
			array('check', 'attachmentShowImages'),
391
			array('check', 'attachmentThumbnails'),
392
			array('text', 'attachmentThumbWidth', 6),
393
			array('text', 'attachmentThumbHeight', 6),
394 2
			'',
395
			array('int', 'max_image_width', 'subtext' => $txt['zero_for_no_limit']),
396 2
			array('int', 'max_image_height', 'subtext' => $txt['zero_for_no_limit']),
397
		);
398
399
		// Add new settings with a nice hook, makes them available for admin settings search as well
400
		call_integration_hook('integrate_modify_attachment_settings', array(&$config_vars));
401
402 2
		return $config_vars;
403
	}
404 2
405
	/**
406
	 * Public method to return the config settings, used for admin search
407
	 */
408
	public function settings_search()
409
	{
410
		return $this->_settings();
411
	}
412
413
	/**
414
	 * Show a list of attachment or avatar files.
415
	 *
416
	 * - Called by
417
	 *     ?action=admin;area=manageattachments;sa=browse for attachments
418
	 *     ?action=admin;area=manageattachments;sa=browse;avatars for avatars.
419
	 *     ?action=admin;area=manageattachments;sa=browse;thumbs for thumbnails.
420
	 * - Allows sorting by name, date, size and member.
421
	 * - Paginates results.
422
	 *
423
	 * @uses the 'browse' sub template
424
	 */
425
	public function action_browse()
426
	{
427
		global $context, $txt, $modSettings;
428
429
		// Attachments or avatars?
430
		$context['browse_type'] = isset($this->_req->query->avatars) ? 'avatars' : (isset($this->_req->query->thumbs) ? 'thumbs' : 'attachments');
431
		loadJavascriptFile('topic.js');
432
433
		// Set the options for the list component.
434
		$listOptions = array(
435
			'id' => 'attach_browse',
436
			'title' => $txt['attachment_manager_browse_files'],
437
			'items_per_page' => $modSettings['defaultMaxMessages'],
438
			'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'browse'] + ($context['browse_type'] === 'avatars' ? ['avatars'] : ($context['browse_type'] === 'thumbs' ? ['thumbs'] : []))),
439
			'default_sort_col' => 'name',
440
			'no_items_label' => $txt['attachment_manager_' . ($context['browse_type'] === 'avatars' ? 'avatars' : ($context['browse_type'] === 'thumbs' ? 'thumbs' : 'attachments')) . '_no_entries'],
441
			'get_items' => array(
442
				'function' => 'list_getFiles',
443
				'params' => array(
444
					$context['browse_type'],
445
				),
446
			),
447
			'get_count' => array(
448
				'function' => 'list_getNumFiles',
449
				'params' => array(
450
					$context['browse_type'],
451
				),
452
			),
453
			'columns' => array(
454
				'name' => array(
455
					'header' => array(
456
						'value' => $txt['attachment_name'],
457
						'class' => 'grid50',
458
					),
459
					'data' => array(
460
						'function' => static function ($rowData) {
461
							global $modSettings, $context;
462
463
							$link = '<a href="';
464
							// In case of a custom avatar URL attachments have a fixed directory.
465
							if ((int) $rowData['attachment_type'] === 1)
466
							{
467
								$link .= sprintf('%1$s/%2$s', $modSettings['custom_avatar_url'], $rowData['filename']);
468
							}
469
							// By default, avatars are downloaded almost as attachments.
470
							elseif ($context['browse_type'] === 'avatars')
471
							{
472
								$link .= getUrl('attach', ['action' => 'dlattach', 'type' => 'avatar', 'attach' => (int) $rowData['id_attach'], 'name' => $rowData['filename']]);
473
							}
474
							// Normal attachments are always linked to a topic ID.
475
							else
476
							{
477
								$link .= getUrl('attach', ['action' => 'dlattach', 'topic' => ((int) $rowData['id_topic']) . '.0', 'attach' => (int) $rowData['id_attach'], 'name' => $rowData['filename']]);
478
							}
479
							$link .= '"';
480
481
							// Show a popup on click if it's a picture and we know its dimensions (use rand message to prevent navigation)
482
							if (!empty($rowData['width']) && !empty($rowData['height']))
483
							{
484
								$link .= 'id="link_' . $rowData['id_attach'] . '" data-lightboxmessage="' . random_int(0, 100000) . '" data-lightboximage="' . $rowData['id_attach'] . '"';
485
							}
486
487
							$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')));
488
489
							// Show the dimensions.
490
							if (!empty($rowData['width']) && !empty($rowData['height']))
491
							{
492
								$link .= sprintf(' <span class="smalltext">%1$dx%2$d</span>', $rowData['width'], $rowData['height']);
493
							}
494
495
							return $link;
496
						},
497
					),
498
					'sort' => array(
499
						'default' => 'a.filename',
500
						'reverse' => 'a.filename DESC',
501
					),
502
				),
503
				'filesize' => array(
504
					'header' => array(
505
						'value' => $txt['attachment_file_size'],
506
						'class' => 'nowrap',
507
					),
508
					'data' => array(
509
						'function' => static fn($rowData) => byte_format($rowData['size']),
510
					),
511
					'sort' => array(
512
						'default' => 'a.size',
513
						'reverse' => 'a.size DESC',
514
					),
515
				),
516
				'member' => array(
517
					'header' => array(
518
						'value' => $context['browse_type'] === 'avatars' ? $txt['attachment_manager_member'] : $txt['posted_by'],
519
						'class' => 'nowrap',
520
					),
521
					'data' => array(
522
						'function' => static function ($rowData) {
523
							// In case of an attachment, return the poster of the attachment.
524
							if (empty($rowData['id_member']))
525
							{
526
								return htmlspecialchars($rowData['poster_name'], ENT_COMPAT, 'UTF-8');
527
							}
528
529
							return '<a href="' . getUrl('profile', ['action' => 'profile', 'u' => (int) $rowData['id_member'], 'name' => $rowData['poster_name']]) . '">' . $rowData['poster_name'] . '</a>';
530
						},
531
					),
532
					'sort' => array(
533
						'default' => 'mem.real_name',
534
						'reverse' => 'mem.real_name DESC',
535
					),
536
				),
537
				'date' => array(
538
					'header' => array(
539
						'value' => $context['browse_type'] === 'avatars' ? $txt['attachment_manager_last_active'] : $txt['date'],
540
						'class' => 'nowrap',
541
					),
542
					'data' => array(
543
						'function' => static function ($rowData) {
544
							global $txt, $context;
545
546
							// The date the message containing the attachment was posted or the owner of the avatar was active.
547
							$date = empty($rowData['poster_time']) ? $txt['never'] : standardTime($rowData['poster_time']);
548
							// Add a link to the topic in case of an attachment.
549
							if ($context['browse_type'] !== 'avatars')
550
							{
551
								$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>';
552
							}
553
554
							return $date;
555
						},
556
					),
557
					'sort' => array(
558
						'default' => $context['browse_type'] === 'avatars' ? 'mem.last_login' : 'm.id_msg',
559
						'reverse' => $context['browse_type'] === 'avatars' ? 'mem.last_login DESC' : 'm.id_msg DESC',
560
					),
561
				),
562
				'downloads' => array(
563
					'header' => array(
564
						'value' => $txt['downloads'],
565
						'class' => 'nowrap',
566
					),
567
					'data' => array(
568
						'db' => 'downloads',
569
						'comma_format' => true,
570
					),
571
					'sort' => array(
572
						'default' => 'a.downloads',
573
						'reverse' => 'a.downloads DESC',
574
					),
575
				),
576
				'check' => array(
577
					'header' => array(
578
						'value' => '<input type="checkbox" onclick="invertAll(this, this.form);" class="input_check" />',
579
						'class' => 'centertext',
580
					),
581
					'data' => array(
582
						'sprintf' => array(
583
							'format' => '<input type="checkbox" name="remove[%1$d]" class="input_check" />',
584
							'params' => array(
585
								'id_attach' => false,
586
							),
587
						),
588
						'class' => 'centertext',
589
					),
590
				),
591
			),
592
			'form' => array(
593
				'href' => getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'remove', ($context['browse_type'] === 'avatars' ? 'avatars' : ($context['browse_type'] === 'thumbs' ? 'thumbs' : ''))]),
594
				'include_sort' => true,
595
				'include_start' => true,
596
				'hidden_fields' => array(
597
					'type' => $context['browse_type'],
598
				),
599
			),
600
			'additional_rows' => array(
601
				array(
602
					'position' => 'below_table_data',
603
					'value' => '<input type="submit" name="remove_submit" class="right_submit" value="' . $txt['quickmod_delete_selected'] . '" onclick="return confirm(\'' . $txt['confirm_delete_attachments'] . '\');" />',
604
				),
605
			),
606
			'list_menu' => array(
607
				'show_on' => 'top',
608
				'class' => 'flow_flex_right',
609
				'links' => array(
610
					array(
611
						'href' => getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'browse']),
612
						'is_selected' => $context['browse_type'] === 'attachments',
613
						'label' => $txt['attachment_manager_attachments']
614
					),
615
					array(
616
						'href' => getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'browse', 'avatars']),
617
						'is_selected' => $context['browse_type'] === 'avatars',
618
						'label' => $txt['attachment_manager_avatars']
619
					),
620
					array(
621
						'href' => getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'browse', 'thumbs']),
622
						'is_selected' => $context['browse_type'] === 'thumbs',
623
						'label' => $txt['attachment_manager_thumbs']
624
					),
625
				),
626
			),
627
		);
628
629
		// Create the list.
630
		createList($listOptions);
631
	}
632
633
	/**
634
	 * Show several file maintenance options.
635
	 *
636
	 * What it does:
637
	 *
638
	 * - Called by ?action=admin;area=manageattachments;sa=maintain.
639
	 * - Calculates file statistics (total file size, number of attachments,
640
	 * number of avatars, attachment space available).
641
	 *
642
	 * @uses the 'maintenance' sub template.
643
	 */
644
	public function action_maintenance()
645
	{
646
		global $context, $modSettings;
647
648
		theme()->getTemplates()->load('ManageAttachments');
649
		$context['sub_template'] = 'maintenance';
650
651
		// We need our attachments directories...
652
		$attachmentDirectory = new AttachmentsDirectory($modSettings, database());
653
		$attach_dirs = $attachmentDirectory->getPaths();
654
655
		// Get the number of attachments...
656
		$context['num_attachments'] = comma_format(getAttachmentCountByType('attachments'), 0);
657
658
		// Also get the avatar amount...
659
		$context['num_avatars'] = comma_format(getAttachmentCountByType('avatars'), 0);
660
661
		// Total size of attachments
662
		$context['attachment_total_size'] = overallAttachmentsSize();
663
664
		// Total size and files from the current attachment dir.
665
		$current_dir = currentAttachDirProperties();
666
667
		// If they specified a limit only....
668
		if ($attachmentDirectory->hasSizeLimit())
669
		{
670
			$context['attachment_space'] = comma_format($attachmentDirectory->remainingSpace($current_dir['size']), 2);
0 ignored issues
show
Bug introduced by
It seems like $attachmentDirectory->re...e($current_dir['size']) can also be of type false; however, parameter $number of comma_format() does only seem to accept double, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

670
			$context['attachment_space'] = comma_format(/** @scrutinizer ignore-type */ $attachmentDirectory->remainingSpace($current_dir['size']), 2);
Loading history...
671
		}
672
673
		if ($attachmentDirectory->hasNumFilesLimit())
674
		{
675
			$context['attachment_files'] = comma_format($attachmentDirectory->remainingFiles($current_dir['files']), 0);
676
		}
677
678
		$context['attachment_current_size'] = byte_format($current_dir['size']);
679
		$context['attachment_current_files'] = comma_format($current_dir['files'], 0);
680
		$context['attach_multiple_dirs'] = count($attach_dirs) > 1;
681
		$context['attach_dirs'] = $attach_dirs;
682
		$context['base_dirs'] = empty($modSettings['attachment_basedirectories']) ? array() : Util::unserialize($modSettings['attachment_basedirectories']);
683
		$context['checked'] = $this->_req->getSession('checked', true);
684
685
		if (!empty($_SESSION['results']))
686
		{
687
			$context['results'] = implode('<br />', $this->_req->session->results);
688
			unset($_SESSION['results']);
689
		}
690
	}
691
692
	/**
693
	 * Remove attachments older than a given age.
694
	 *
695
	 * - Called from the maintenance screen by ?action=admin;area=manageattachments;sa=byAge.
696
	 * - It optionally adds a certain text to the messages the attachments were removed from.
697
	 *
698
	 * @todo refactor this silly superglobals use...
699
	 */
700
	public function action_byAge()
701
	{
702
		checkSession('post', 'admin');
703
704
		// @todo Ignore messages in topics that are stickied?
705
706
		// Deleting an attachment?
707
		if (!$this->_req->compareQuery('type', 'avatars', 'trim|strval'))
708
		{
709
			// Get rid of all the old attachments.
710
			$messages = removeAttachments(array('attachment_type' => 0, 'poster_time' => (time() - 24 * 60 * 60 * $this->_req->post->age)), 'messages', true);
711
712
			// Update the messages to reflect the change.
713
			if (!empty($messages) && !empty($this->_req->post->notice))
714
			{
715
				setRemovalNotice($messages, $this->_req->post->notice);
716
			}
717
		}
718
		// Remove all the old avatars.
719
		else
720
		{
721
			removeAttachments(array('not_id_member' => 0, 'last_login' => (time() - 24 * 60 * 60 * $this->_req->post->age)), 'members');
722
		}
723
724
		redirectexit('action=admin;area=manageattachments' . (empty($this->_req->query->avatars) ? ';sa=maintenance' : ';avatars'));
725
	}
726
727
	/**
728
	 * Remove attachments larger than a given size.
729
	 *
730
	 * - Called from the maintenance screen by ?action=admin;area=manageattachments;sa=bySize.
731
	 * - Optionally adds a certain text to the messages the attachments were removed from.
732
	 */
733
	public function action_bySize()
734
	{
735
		checkSession('post', 'admin');
736
737
		// Find humongous attachments.
738
		$messages = removeAttachments(array('attachment_type' => 0, 'size' => 1024 * $this->_req->post->size), 'messages', true);
739
740
		// And make a note on the post.
741
		if (!empty($messages) && !empty($this->_req->post->notice))
742
		{
743
			setRemovalNotice($messages, $this->_req->post->notice);
744
		}
745
746
		redirectexit('action=admin;area=manageattachments;sa=maintenance');
747
	}
748
749
	/**
750
	 * Remove a selection of attachments or avatars.
751
	 *
752
	 * - Called from the browse screen as submitted form by ?action=admin;area=manageattachments;sa=remove
753
	 */
754
	public function action_remove()
755
	{
756
		global $language;
757
758
		checkSession();
759
760
		if (!empty($this->_req->post->remove))
761
		{
762
			// There must be a quicker way to pass this safety test??
763
			$attachments = array();
764
			foreach ($this->_req->post->remove as $removeID => $dummy)
765
			{
766
				$attachments[] = (int) $removeID;
767
			}
768
769
			if ($this->_req->compareQuery('type', 'avatars', 'trim|strval') && !empty($attachments))
770
			{
771
				removeAttachments(array('id_attach' => $attachments));
772
			}
773
			elseif (!empty($attachments))
774
			{
775
				$messages = removeAttachments(array('id_attach' => $attachments), 'messages', true);
776
777
				// And change the message to reflect this.
778
				if (!empty($messages))
779
				{
780
					$mtxt = [];
781
					$lang = new Loader($language, $mtxt, database());
782
					$lang->load('Admin');
783
					setRemovalNotice($messages, $mtxt['attachment_delete_admin']);
784
				}
785
			}
786
		}
787
788
		$sort = $this->_req->getQuery('sort', 'trim|strval', 'date');
789
		redirectexit('action=admin;area=manageattachments;sa=browse;' . $this->_req->query->type . ';sort=' . $sort . (isset($this->_req->query->desc) ? ';desc' : '') . ';start=' . $this->_req->query->start);
790
	}
791
792
	/**
793
	 * Removes all attachments in a single click
794
	 *
795
	 * - Called from the maintenance screen by ?action=admin;area=manageattachments;sa=removeall.
796
	 */
797
	public function action_removeall()
798
	{
799
		global $txt;
800
801
		checkSession('get', 'admin');
802
803
		$messages = removeAttachments(array('attachment_type' => 0), '', true);
804
805
		$notice = $this->_req->getPost('notice', 'trim|strval', $txt['attachment_delete_admin']);
806
807
		// Add the notice on the end of the changed messages.
808
		if (!empty($messages))
809
		{
810
			setRemovalNotice($messages, $notice);
811
		}
812
813
		redirectexit('action=admin;area=manageattachments;sa=maintenance');
814
	}
815
816
	/**
817
	 * This function will perform many attachment checks and provides ways to fix them
818
	 *
819
	 * What it does:
820
	 *
821
	 * Checks for the following common issues
822
	 * - Orphan Thumbnails
823
	 * - Attachments that have no thumbnails
824
	 * - Attachments that list thumbnails, but actually, don't have any
825
	 * - Attachments list in the wrong_folder
826
	 * - Attachments that don't exist on disk any longer
827
	 * - Attachments that are zero size
828
	 * - Attachments that file size does not match the DB size
829
	 * - Attachments that no longer have a message
830
	 * - Avatars with no members associated with them.
831
	 * - Attachments that are in the attachment folder, but not listed in the DB
832
	 */
833
	public function action_repair()
834
	{
835
		global $modSettings, $context, $txt;
836
837
		checkSession('get');
838
839
		// If we choose cancel, redirect right back.
840
		if (isset($this->_req->post->cancel))
841
		{
842
			redirectexit('action=admin;area=manageattachments;sa=maintenance');
843
		}
844
845
		// Try give us a while to sort this out...
846
		detectServer()->setTimeLimit(600);
847
848
		$this->step = $this->_req->getQuery('step', 'intval', 0);
849
		$this->substep = $this->_req->getQuery('substep', 'intval', 0);
850
		$this->starting_substep = $this->substep;
851
852
		// Don't recall the session just in case.
853
		if ($this->step === 0 && $this->substep === 0)
854
		{
855
			unset($_SESSION['attachments_to_fix'], $_SESSION['attachments_to_fix2']);
856
857
			// If we're actually fixing stuff - work out what.
858
			if (isset($this->_req->query->fixErrors))
859
			{
860
				// Nothing?
861
				if (empty($this->_req->post->to_fix))
862
				{
863
					redirectexit('action=admin;area=manageattachments;sa=maintenance');
864
				}
865
866
				foreach ($this->_req->post->to_fix as $value)
867
				{
868
					$_SESSION['attachments_to_fix'][] = $value;
869
				}
870
			}
871
		}
872
873
		// All the valid problems are here:
874
		$context['repair_errors'] = [
875
			'missing_thumbnail_parent' => 0,
876
			'parent_missing_thumbnail' => 0,
877
			'file_missing_on_disk' => 0,
878
			'file_wrong_size' => 0,
879
			'file_size_of_zero' => 0,
880
			'attachment_no_msg' => 0,
881
			'avatar_no_member' => 0,
882
			'wrong_folder' => 0,
883
			'missing_extension' => 0,
884
			'files_without_attachment' => 0,
885
		];
886
887
		$to_fix = empty($_SESSION['attachments_to_fix']) ? array() : $_SESSION['attachments_to_fix'];
888
		$context['repair_errors'] = $_SESSION['attachments_to_fix2'] ?? $context['repair_errors'];
889
		$fix_errors = isset($this->_req->query->fixErrors);
890
891
		// Get stranded thumbnails.
892
		if ($this->step <= 0)
893
		{
894
			$thumbnails = getMaxThumbnail();
895
896
			for (; $this->substep < $thumbnails; $this->substep += 500)
897
			{
898
				$removed = findOrphanThumbnails($this->substep, $fix_errors, $to_fix);
899
				$context['repair_errors']['missing_thumbnail_parent'] += count($removed);
900
901
				pauseAttachmentMaintenance($to_fix, $thumbnails, $this->starting_substep, $this->substep, $this->step, $fix_errors);
902
			}
903
904
			// Done here, on to the next
905
			$this->step = 1;
906
			$this->substep = 0;
907
			pauseAttachmentMaintenance($to_fix, 0, $this->starting_substep, $this->substep, $this->step, $fix_errors);
908
		}
909
910
		// Find parents which think they have thumbnails, but actually, don't.
911
		if ($this->step <= 1)
912
		{
913
			$thumbnails = maxNoThumb();
914
915
			for (; $this->substep < $thumbnails; $this->substep += 500)
916
			{
917
				$to_update = findParentsOrphanThumbnails($this->substep, $fix_errors, $to_fix);
918
				$context['repair_errors']['parent_missing_thumbnail'] += count($to_update);
919
920
				pauseAttachmentMaintenance($to_fix, $thumbnails, $this->starting_substep, $this->substep, $this->step, $fix_errors);
921
			}
922
923
			// Another step done, but many to go
924
			$this->step = 2;
925
			$this->substep = 0;
926
			pauseAttachmentMaintenance($to_fix, 0, $this->starting_substep, $this->substep, $this->step, $fix_errors);
927
		}
928
929
		// This may take forever I'm afraid, but life sucks... recount EVERY attachments!
930
		if ($this->step <= 2)
931
		{
932
			$thumbnails = maxAttachment();
933
934
			for (; $this->substep < $thumbnails; $this->substep += 250)
935
			{
936
				$repair_errors = repairAttachmentData($this->substep, $fix_errors, $to_fix);
937
938
				foreach ($repair_errors as $key => $value)
939
				{
940
					$context['repair_errors'][$key] += $value;
941
				}
942
943
				pauseAttachmentMaintenance($to_fix, $thumbnails, $this->starting_substep, $this->substep, $this->step, $fix_errors);
944
			}
945
946
			// And onward we go
947
			$this->step = 3;
948
			$this->substep = 0;
949
			pauseAttachmentMaintenance($to_fix, 0, $this->starting_substep, $this->substep, $this->step, $fix_errors);
950
		}
951
952
		// Get avatars with no members associated with them.
953
		if ($this->step <= 3)
954
		{
955
			$thumbnails = maxAttachment();
956
957
			for (; $this->substep < $thumbnails; $this->substep += 500)
958
			{
959
				$to_remove = findOrphanAvatars($this->substep, $fix_errors, $to_fix);
960
				$context['repair_errors']['avatar_no_member'] += count($to_remove);
961
962
				pauseAttachmentMaintenance($to_fix, $thumbnails, $this->starting_substep, $this->substep, $this->step, $fix_errors);
963
			}
964
965
			$this->step = 4;
966
			$this->substep = 0;
967
			pauseAttachmentMaintenance($to_fix, 0, $this->starting_substep, $this->substep, $this->step, $fix_errors);
968
		}
969
970
		// What about attachments, who are missing a message :'(
971
		if ($this->step <= 4)
972
		{
973
			$thumbnails = maxAttachment();
974
975
			for (; $this->substep < $thumbnails; $this->substep += 500)
976
			{
977
				$to_remove = findOrphanAttachments($this->substep, $fix_errors, $to_fix);
978
				$context['repair_errors']['attachment_no_msg'] += count($to_remove);
979
980
				pauseAttachmentMaintenance($to_fix, $thumbnails, $this->starting_substep, $this->substep, $this->step, $fix_errors);
981
			}
982
983
			$this->step = 5;
984
			$this->substep = 0;
985
			pauseAttachmentMaintenance($to_fix, 0, $this->starting_substep, $this->substep, $this->step, $fix_errors);
986
		}
987
988
		// What about files who are not recorded in the database?
989
		if ($this->step <= 5)
990
		{
991
			// Just use the current path for temp files.
992
			if (!is_array($modSettings['attachmentUploadDir']))
993
			{
994
				$modSettings['attachmentUploadDir'] = Util::unserialize($modSettings['attachmentUploadDir']);
995
			}
996
997
			$attach_dirs = $modSettings['attachmentUploadDir'];
998
			$current_check = 0;
999
			$max_checks = 500;
1000
			$attachment_count = getAttachmentCountFromDisk();
1001
1002
			$files_checked = empty($this->substep) ? 0 : $this->substep;
1003
			foreach ($attach_dirs as $attach_dir)
1004
			{
1005
				try
1006
				{
1007
					$files = new FilesystemIterator($attach_dir, FilesystemIterator::SKIP_DOTS);
1008
					foreach ($files as $file)
1009
					{
1010
						if ($file->getFilename() === '.htaccess')
1011
						{
1012
							continue;
1013
						}
1014
1015
						if ($files_checked <= $current_check)
1016
						{
1017
							// Temporary file, get rid of it!
1018
							if (strpos($file->getFilename(), 'post_tmp_') !== false)
1019
							{
1020
								// Temp file is more than 5 hours old!
1021
								if ($file->getMTime() < time() - 18000)
1022
								{
1023
									$this->file_functions->delete($file->getPathname());
1024
								}
1025
							}
1026
							// That should be an attachment, let's check if we have it in the database
1027
							elseif (strpos($file->getFilename(), '_') !== false)
1028
							{
1029
								$attachID = (int) substr($file->getFilename(), 0, strpos($file->getFilename(), '_'));
1030
								if ($attachID !== 0 && !validateAttachID($attachID))
1031
								{
1032
									if ($fix_errors && in_array('files_without_attachment', $to_fix))
1033
									{
1034
										$this->file_functions->delete($file->getPathname());
1035
									}
1036
									else
1037
									{
1038
										$context['repair_errors']['files_without_attachment']++;
1039
									}
1040
								}
1041
							}
1042
							elseif ($file->getFilename() !== 'index.php' && !$file->isDir())
1043
							{
1044
								if ($fix_errors && in_array('files_without_attachment', $to_fix, true))
1045
								{
1046
									$this->file_functions->delete($file->getPathname());
1047
								}
1048
								else
1049
								{
1050
									$context['repair_errors']['files_without_attachment']++;
1051
								}
1052
							}
1053
						}
1054
1055
						$current_check++;
1056
						$this->substep = $current_check;
1057
1058
						if ($current_check - $files_checked >= $max_checks)
1059
						{
1060
							pauseAttachmentMaintenance($to_fix, $attachment_count, $this->starting_substep, $this->substep, $this->step, $fix_errors);
1061
						}
1062
					}
1063
				}
1064
				catch (UnexpectedValueException)
1065
				{
1066
					// @todo for now do nothing...
1067
				}
1068
			}
1069
1070
			$this->step = 5;
1071
			$this->substep = 0;
1072
			pauseAttachmentMaintenance($to_fix, 0, $this->starting_substep, $this->substep, $this->step, $fix_errors);
1073
		}
1074
1075
		// Got here we must be doing well - just the template! :D
1076
		$context['page_title'] = $txt['repair_attachments'];
1077
		$context[$context['admin_menu_name']]['current_subsection'] = 'maintenance';
1078
		$context['sub_template'] = 'attachment_repair';
1079
1080
		// What stage are we at?
1081
		$context['completed'] = $fix_errors;
1082
		$context['errors_found'] = false;
1083
		foreach ($context['repair_errors'] as $number)
1084
		{
1085
			if (!empty($number))
1086
			{
1087
				$context['errors_found'] = true;
1088
				break;
1089
			}
1090
		}
1091
	}
1092
1093
	/**
1094
	 * Function called in-between each round of attachments and avatar repairs.
1095
	 *
1096
	 * What it does:
1097
	 *
1098
	 * - Called by repairAttachments().
1099
	 * - If repairAttachments() has more steps added, this function needs to be updated!
1100
	 *
1101
	 * @param array $to_fix attachments to fix
1102
	 * @param int $max_substep = 0
1103
	 * @throws \ElkArte\Exceptions\Exception
1104
	 * @todo Move to ManageAttachments.subs.php
1105
	 */
1106
	private function _pauseAttachmentMaintenance($to_fix, $max_substep = 0)
1107
	{
1108
		global $context, $txt, $time_start;
1109
1110
		// Try get more time...
1111
		detectServer()->setTimeLimit(600);
1112
1113
		// Have we already used our maximum time?
1114
		if (microtime(true) - $time_start < 3 || $this->starting_substep == $this->substep)
1115
		{
1116
			return;
1117
		}
1118
1119
		$context['continue_get_data'] = '?action=admin;area=manageattachments;sa=repair' . (isset($this->_req->query->fixErrors) ? ';fixErrors' : '') . ';step=' . $this->step . ';substep=' . $this->substep . ';' . $context['session_var'] . '=' . $context['session_id'];
1120
		$context['page_title'] = $txt['not_done_title'];
1121
		$context['continue_post_data'] = '';
1122
		$context['continue_countdown'] = '2';
1123
		$context['sub_template'] = 'not_done';
1124
1125
		// Specific stuff to not break this template!
1126
		$context[$context['admin_menu_name']]['current_subsection'] = 'maintenance';
1127
1128
		// Change these two if more steps are added!
1129
		if (empty($max_substep))
1130
		{
1131
			$context['continue_percent'] = round(($this->step * 100) / 25);
1132
		}
1133
		else
1134
		{
1135
			$context['continue_percent'] = round(($this->step * 100 + ($this->substep * 100) / $max_substep) / 25);
1136
		}
1137
1138
		// Never more than 100%!
1139
		$context['continue_percent'] = min($context['continue_percent'], 100);
1140
1141
		// Save the needed information for the next look
1142
		$_SESSION['attachments_to_fix'] = $to_fix;
1143
		$_SESSION['attachments_to_fix2'] = $context['repair_errors'];
1144
1145
		obExit();
1146
	}
1147
1148
	/**
1149
	 * This function lists and allows updating of multiple attachments paths.
1150
	 */
1151
	public function action_attachpaths()
1152
	{
1153
		global $modSettings, $context, $txt;
1154
1155
		$attachmentsDir = new AttachmentsDirectory($modSettings, database());
1156
		$errors = array();
1157
1158
		// Saving or changing attachment paths
1159
		if (isset($this->_req->post->save))
1160
		{
1161
			$this->_savePaths($attachmentsDir);
1162
		}
1163
1164
		// Saving a base directory?
1165
		if (isset($this->_req->post->save2))
1166
		{
1167
			$this->_saveBasePaths($attachmentsDir);
1168
		}
1169
1170
		// Have some errors to show?
1171
		if (isset($_SESSION['errors']))
1172
		{
1173
			if (is_array($_SESSION['errors']))
1174
			{
1175
				if (!empty($_SESSION['errors']['dir']))
1176
				{
1177
					foreach ($_SESSION['errors']['dir'] as $error)
1178
					{
1179
						$errors['dir'][] = Util::htmlspecialchars($error, ENT_QUOTES);
1180
					}
1181
				}
1182
1183
				if (!empty($_SESSION['errors']['base']))
1184
				{
1185
					foreach ($_SESSION['errors']['base'] as $error)
1186
					{
1187
						$errors['base'][] = Util::htmlspecialchars($error, ENT_QUOTES);
1188
					}
1189
				}
1190
			}
1191
1192
			unset($_SESSION['errors']);
1193
		}
1194
1195
		// Show the list of base and path directories + any errors generated
1196
		$listOptions = array(
1197
			'id' => 'attach_paths',
1198
			'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'attachpaths', '{sesstion_data}']),
1199
			'title' => $txt['attach_paths'],
1200
			'get_items' => array(
1201
				'function' => 'list_getAttachDirs',
1202
			),
1203
			'columns' => array(
1204
				'current_dir' => array(
1205
					'header' => array(
1206
						'value' => $txt['attach_current'],
1207
						'class' => 'centertext',
1208
					),
1209
					'data' => array(
1210
						'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" />',
1211
						'class' => 'grid8 centertext',
1212
					),
1213
				),
1214
				'path' => array(
1215
					'header' => array(
1216
						'value' => $txt['attach_path'],
1217
					),
1218
					'data' => array(
1219
						'function' => static fn($rowData) => '
1220
							<input type="hidden" name="dirs[' . $rowData['id'] . ']" value="' . $rowData['path'] . '" />
1221
							<input type="text" size="40" name="dirs[' . $rowData['id'] . ']" value="' . $rowData['path'] . '"' . (empty($rowData['disable_base_dir']) ? '' : ' disabled="disabled"') . ' class="input_text"/>',
1222
						'class' => 'grid50',
1223
					),
1224
				),
1225
				'current_size' => array(
1226
					'header' => array(
1227
						'value' => $txt['attach_current_size'],
1228
					),
1229
					'data' => array(
1230
						'db' => 'current_size',
1231
					),
1232
				),
1233
				'num_files' => array(
1234
					'header' => array(
1235
						'value' => $txt['attach_num_files'],
1236
					),
1237
					'data' => array(
1238
						'db' => 'num_files',
1239
					),
1240
				),
1241
				'status' => array(
1242
					'header' => array(
1243
						'value' => $txt['attach_dir_status'],
1244
					),
1245
					'data' => array(
1246
						'db' => 'status',
1247
						'class' => 'grid20',
1248
					),
1249
				),
1250
			),
1251
			'form' => array(
1252
				'href' => getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'attachpaths', '{sesstion_data}']),
1253
			),
1254
			'additional_rows' => array(
1255
				array(
1256
					'class' => 'submitbutton',
1257
					'position' => 'below_table_data',
1258
					'value' => '
1259
					<input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '" />
1260
					<input type="submit" name="new_path" value="' . $txt['attach_add_path'] . '" />
1261
					<input type="submit" name="save" value="' . $txt['save'] . '" />',
1262
				),
1263
				empty($errors['dir']) ? array(
1264
					'position' => 'top_of_list',
1265
					'value' => $txt['attach_dir_desc'],
1266
					'style' => 'padding: 5px 10px;',
1267
					'class' => 'description'
1268
				) : array(
1269
					'position' => 'top_of_list',
1270
					'value' => $txt['attach_dir_save_problem'] . '<br />' . implode('<br />', $errors['dir']),
1271
					'style' => 'padding-left: 2.75em;',
1272
					'class' => 'warningbox',
1273
				),
1274
			),
1275
		);
1276
		createList($listOptions);
1277
1278
		if (!empty($modSettings['attachment_basedirectories']))
1279
		{
1280
			$listOptions2 = array(
1281
				'id' => 'base_paths',
1282
				'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'attachpaths', '{sesstion_data}']),
1283
				'title' => $txt['attach_base_paths'],
1284
				'get_items' => array(
1285
					'function' => 'list_getBaseDirs',
1286
				),
1287
				'columns' => array(
1288
					'current_dir' => array(
1289
						'header' => array(
1290
							'value' => $txt['attach_current'],
1291
							'class' => 'centertext',
1292
						),
1293
						'data' => array(
1294
							'function' => static fn($rowData) => '<input type="radio" name="current_base_dir" value="' . $rowData['id'] . '" ' . ($rowData['current'] ? ' checked="checked"' : '') . ' class="input_radio" />',
1295
							'class' => 'grid8 centertext',
1296
						),
1297
					),
1298
					'path' => array(
1299
						'header' => array(
1300
							'value' => $txt['attach_path'],
1301
						),
1302
						'data' => array(
1303
							'db' => 'path',
1304
							'class' => 'grid50',
1305
						),
1306
					),
1307
					'num_dirs' => array(
1308
						'header' => array(
1309
							'value' => $txt['attach_num_dirs'],
1310
						),
1311
						'data' => array(
1312
							'db' => 'num_dirs',
1313
						),
1314
					),
1315
					'status' => array(
1316
						'header' => array(
1317
							'value' => $txt['attach_dir_status'],
1318
						),
1319
						'data' => array(
1320
							'db' => 'status',
1321
							'class' => 'grid20',
1322
						),
1323
					),
1324
				),
1325
				'form' => array(
1326
					'href' => getUrl('admin', ['action' => 'admin', 'area' => 'manageattachments', 'sa' => 'attachpaths', '{sesstion_data}']),
1327
				),
1328
				'additional_rows' => array(
1329
					array(
1330
						'class' => 'submitbutton',
1331
						'position' => 'below_table_data',
1332
						'value' => '
1333
							<input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '" />
1334
							<input type="submit" name="new_base_path" value="' . $txt['attach_add_path'] . '" />
1335
							<input type="submit" name="save2" value="' . $txt['save'] . '" />',
1336
					),
1337
					empty($errors['base']) ? array(
1338
						'position' => 'top_of_list',
1339
						'value' => $txt['attach_dir_base_desc'],
1340
						'style' => 'padding: 5px 10px;',
1341
						'class' => 'description'
1342
					) : array(
1343
						'position' => 'top_of_list',
1344
						'value' => $txt['attach_dir_save_problem'] . '<br />' . implode('<br />', $errors['base']),
1345
						'style' => 'padding-left: 2.75em',
1346
						'class' => 'warningbox',
1347
					),
1348
				),
1349
			);
1350
			createList($listOptions2);
1351
		}
1352
1353
		// Fix up our template.
1354
		$context[$context['admin_menu_name']]['current_subsection'] = 'attachpaths';
1355
		$context['page_title'] = $txt['attach_path_manage'];
1356
	}
1357
1358
	/**
1359
	 * Saves any changes or additions to the attachment paths
1360
	 *
1361
	 * @param AttachmentsDirectory $attachmentsDir
1362
	 * @return void
1363
	 */
1364
	private function _savePaths($attachmentsDir)
1365
	{
1366
		global $txt, $context;
1367
1368
		checkSession();
1369
1370
		$current_dir = $this->_req->getPost('current_dir', 'intval', 1);
1371
		$dirs = $this->_req->getPost('dirs');
1372
		$new_dirs = [];
1373
1374
		// Can't use these directories for attachments
1375
		require_once(SUBSDIR . '/Themes.subs.php');
1376
		$themes = installedThemes();
1377
		$reserved_dirs = array(BOARDDIR, SOURCEDIR, SUBSDIR, CONTROLLERDIR, CACHEDIR, EXTDIR, LANGUAGEDIR, ADMINDIR);
1378
		foreach ($themes as $theme)
1379
		{
1380
			$reserved_dirs[] = $theme['theme_dir'];
1381
		}
1382
1383
		foreach ($dirs as $id => $path)
1384
		{
1385
			$id = (int) $id;
1386
			if ($id < 1)
1387
			{
1388
				continue;
1389
			}
1390
1391
			// If it doesn't look like a directory, probably is not a directory
1392
			$real_path = rtrim(trim($path), DIRECTORY_SEPARATOR);
1393
			if (preg_match('~[/\\\\]~', $real_path) !== 1)
1394
			{
1395
				$real_path = realpath(BOARDDIR . DIRECTORY_SEPARATOR . ltrim($real_path, DIRECTORY_SEPARATOR));
1396
			}
1397
1398
			// Hmm, a new path maybe?
1399
			if ($attachmentsDir->directoryExists($id) === false)
1400
			{
1401
				// or is it?
1402
				if ($attachmentsDir->directoryExists($path))
1403
				{
1404
					$errors[] = $path . ': ' . $txt['attach_dir_duplicate_msg'];
1405
					continue;
1406
				}
1407
1408
				// or is it a system dir?
1409
				if (in_array($real_path, $reserved_dirs))
1410
				{
1411
					$errors[] = $real_path . ': ' . $txt['attach_dir_reserved'];
1412
					continue;
1413
				}
1414
1415
				// OK, so let's try to create it then.
1416
				try
1417
				{
1418
					$attachmentsDir->createDirectory($path);
1419
				}
1420
				catch (Exception $e)
1421
				{
1422
					$errors[] = $path . ': ' . $txt[$e->getMessage()];
1423
					continue;
1424
				}
1425
			}
1426
1427
			// Changing a directory name or trying to remove it entirely?
1428
			try
1429
			{
1430
				if (!empty($path))
1431
				{
1432
					$attachmentsDir->rename($id, $real_path);
1433
				}
1434
				elseif ($attachmentsDir->delete($id, $real_path) === true)
1435
				{
1436
					// Don't add it back, its now gone!
1437
					continue;
1438
				}
1439
			}
1440
			catch (Exception $e)
1441
			{
1442
				$errors[] = $real_path . ': ' . $txt[$e->getMessage()];
1443
			}
1444
1445
			$new_dirs[$id] = $real_path;
1446
		}
1447
1448
		// We need to make sure the current directory is right.
1449
		if (empty($current_dir) && !empty($attachmentsDir->currentDirectoryId()))
1450
		{
1451
			$current_dir = $attachmentsDir->currentDirectoryId();
1452
		}
1453
1454
		// Find the current directory if there's no value carried,
1455
		if (empty($current_dir) || empty($new_dirs[$current_dir]))
1456
		{
1457
			$current_dir = $attachmentsDir->currentDirectoryId();
1458
		}
1459
1460
		// If the user wishes to go back, update the last_dir array
1461
		if ($current_dir !== $attachmentsDir->currentDirectoryId())
1462
		{
1463
			$attachmentsDir->updateLastDirs($current_dir);
1464
		}
1465
1466
		// Going back to just one path?
1467
		if (count($new_dirs) === 1)
1468
		{
1469
			// We might need to reset the paths. This loop will just loop through once.
1470
			foreach ($new_dirs as $id => $dir)
1471
			{
1472
				if ($id !== 1)
1473
				{
1474
					updateAttachmentIdFolder($id, 1);
1475
				}
1476
1477
				$update = array('currentAttachmentUploadDir' => 1, 'attachmentUploadDir' => serialize(array(1 => $dir)),);
1478
			}
1479
		}
1480
		else
1481
		{
1482
			// Save it to the database.
1483
			$update = array('currentAttachmentUploadDir' => $current_dir, 'attachmentUploadDir' => serialize($new_dirs),);
1484
		}
1485
1486
		if (!empty($update))
1487
		{
1488
			updateSettings($update);
1489
		}
1490
1491
		if (!empty($errors))
1492
		{
1493
			$_SESSION['errors']['dir'] = $errors;
1494
		}
1495
1496
		redirectexit('action=admin;area=manageattachments;sa=attachpaths;' . $context['session_var'] . '=' . $context['session_id']);
1497
	}
1498
1499
	/**
1500
	 * Saves changes to the attachment base directory section
1501
	 *
1502
	 * @param AttachmentsDirectory $attachmentsDir
1503
	 * @return void
1504
	 */
1505
	private function _saveBasePaths($attachmentsDir)
1506
	{
1507
		global $modSettings, $txt, $context;
1508
1509
		checkSession();
1510
1511
		$attachmentBaseDirectories = $attachmentsDir->getBaseDirs();
1512
		$attachmentUploadDir = $attachmentsDir->getPaths();
1513
		$current_base_dir = $this->_req->getPost('current_base_dir', 'intval', 1);
1514
		$new_base_dir = $this->_req->getPost('new_base_dir', 'trim|htmlspecialchars', '');
1515
		$base_dirs = $this->_req->getPost('base_dir');
1516
1517
		// Changing the current base directory?
1518
		if (empty($new_base_dir) && !empty($current_base_dir) && $modSettings['basedirectory_for_attachments'] !== $attachmentUploadDir[$current_base_dir])
1519
		{
1520
			$update = ['basedirectory_for_attachments' => $attachmentUploadDir[$current_base_dir]];
1521
		}
1522
1523
		// Modifying / Removing a basedir entry
1524
		if (isset($base_dirs))
1525
		{
1526
			// Renaming the base dir, we can try
1527
			foreach ($base_dirs as $id => $dir)
1528
			{
1529
				if (!empty($dir) && $dir !== $attachmentUploadDir[$id] && @rename($attachmentUploadDir[$id], $dir))
1530
				{
1531
					$attachmentUploadDir[$id] = $dir;
1532
					$attachmentBaseDirectories[$id] = $dir;
1533
					$update = (array('attachmentUploadDir' => serialize($attachmentUploadDir), 'attachment_basedirectories' => serialize($attachmentBaseDirectories), 'basedirectory_for_attachments' => $attachmentUploadDir[$current_base_dir],));
1534
				}
1535
1536
				// Or remove it (from selection only)
1537
				if (empty($dir))
1538
				{
1539
					// Can't remove the currently active one
1540
					if ($id === $current_base_dir)
1541
					{
1542
						$errors[] = $attachmentUploadDir[$id] . ': ' . $txt['attach_dir_is_current'];
1543
						continue;
1544
					}
1545
1546
					// Removed from selection (not disc)
1547
					unset($attachmentBaseDirectories[$id]);
1548
					$update = ['attachment_basedirectories' => empty($attachmentBaseDirectories) ? '' : serialize($attachmentBaseDirectories), 'basedirectory_for_attachments' => $attachmentUploadDir[$current_base_dir] ?? '',];
1549
				}
1550
			}
1551
		}
1552
1553
		// Adding a new one?
1554
		if (!empty($new_base_dir))
1555
		{
1556
			$current_dir = $attachmentsDir->currentDirectoryId();
1557
1558
			// If it does not exist, try to create it.
1559
			if (!in_array($new_base_dir, $attachmentUploadDir))
1560
			{
1561
				try
1562
				{
1563
					$attachmentsDir->createDirectory($new_base_dir);
1564
				}
1565
				catch (Exception)
1566
				{
1567
					$errors[] = $new_base_dir . ': ' . $txt['attach_dir_base_no_create'];
1568
				}
1569
			}
1570
1571
			// Find the new key
1572
			$modSettings['currentAttachmentUploadDir'] = array_search($new_base_dir, $attachmentsDir->getPaths(), true);
1573
			if (!in_array($new_base_dir, $attachmentBaseDirectories))
1574
			{
1575
				$attachmentBaseDirectories[$modSettings['currentAttachmentUploadDir']] = $new_base_dir;
1576
			}
1577
1578
			ksort($attachmentBaseDirectories);
1579
			$update = ['attachment_basedirectories' => serialize($attachmentBaseDirectories), 'basedirectory_for_attachments' => $new_base_dir, 'currentAttachmentUploadDir' => $current_dir,];
1580
		}
1581
1582
		$_SESSION['errors']['base'] = $errors ?? null;
1583
1584
		if (!empty($update))
1585
		{
1586
			updateSettings($update);
1587
		}
1588
1589
		redirectexit('action=admin;area=manageattachments;sa=attachpaths;' . $context['session_var'] . '=' . $context['session_id']);
1590
	}
1591
1592
	/**
1593
	 * Maintenance function to move attachments from one directory to another
1594
	 */
1595
	public function action_transfer()
1596
	{
1597
		global $modSettings, $txt;
1598
1599
		checkSession();
1600
1601
		$attachmentsDir = new AttachmentsDirectory($modSettings, database());
1602
1603
		// Clean the inputs
1604
		$this->from = $this->_req->getPost('from', 'intval');
1605
		$this->auto = $this->_req->getPost('auto', 'intval', 0);
1606
		$this->to = $this->_req->getPost('to', 'intval');
1607
		$start = empty($this->_req->post->empty_it) ? $modSettings['attachmentDirFileLimit'] : 0;
1608
		$_SESSION['checked'] = !empty($this->_req->post->empty_it);
1609
1610
		// Prepare for the moving
1611
		$limit = 501;
1612
		$results = array();
1613
		$dir_files = 0;
1614
		$current_progress = 0;
1615
		$total_moved = 0;
1616
		$total_not_moved = 0;
1617
		$total_progress = 0;
1618
1619
		// Need to know where we are moving things from
1620
		if (empty($this->from) || (empty($this->auto) && empty($this->to)))
1621
		{
1622
			$results[] = $txt['attachment_transfer_no_dir'];
1623
		}
1624
1625
		// Same location, that's easy
1626
		if ($this->from === $this->to)
1627
		{
1628
			$results[] = $txt['attachment_transfer_same_dir'];
1629
		}
1630
1631
		// No errors so determine how many we may have to move
1632
		if (empty($results))
1633
		{
1634
			// Get the total file count for the progress bar.
1635
			$total_progress = getFolderAttachmentCount($this->from);
1636
			$total_progress -= $start;
1637
1638
			if ($total_progress < 1)
1639
			{
1640
				$results[] = $txt['attachment_transfer_no_find'];
1641
			}
1642
		}
1643
1644
		// Nothing to move (no files in source or below the max limit)
1645
		if (empty($results))
1646
		{
1647
			// Moving them automatically?
1648
			if (!empty($this->auto))
1649
			{
1650
				$modSettings['automanage_attachments'] = 1;
1651
				$tmpattachmentUploadDir = Util::unserialize($modSettings['attachmentUploadDir']);
1652
1653
				// Create sub directory's off the root or from an attachment directory?
1654
				$modSettings['use_subdirectories_for_attachments'] = $this->auto == -1 ? 0 : 1;
1655
				$modSettings['basedirectory_for_attachments'] = serialize($this->auto > 0 ? $tmpattachmentUploadDir[$this->auto] : [1 => $modSettings['basedirectory_for_attachments']]);
1656
1657
				// Finally, where do they need to go
1658
				$attachmentDirectory = new AttachmentsDirectory($modSettings, database());
1659
				$attachmentDirectory->automanage_attachments_check_directory(true);
1660
				$new_dir = $attachmentDirectory->currentDirectoryId();
1661
			}
1662
			// Or to a specified directory
1663
			else
1664
			{
1665
				$new_dir = $this->to;
1666
			}
1667
1668
			$modSettings['currentAttachmentUploadDir'] = $new_dir;
1669
			$break = false;
1670
			while (!$break)
1671
			{
1672
				detectServer()->setTimeLimit(300);
1673
1674
				// If limits are set, get the file count and size for the destination folder
1675
				if ($dir_files <= 0 && (!empty($modSettings['attachmentDirSizeLimit']) || !empty($modSettings['attachmentDirFileLimit'])))
1676
				{
1677
					$current_dir = attachDirProperties($new_dir);
1678
					$dir_files = $current_dir['files'];
1679
					$dir_size = $current_dir['size'];
1680
				}
1681
1682
				// Find some attachments to move
1683
				[$tomove_count, $tomove] = findAttachmentsToMove($this->from, $start, $limit);
1684
1685
				// Nothing found to move
1686
				if ($tomove_count === 0)
1687
				{
1688
					if ($current_progress === 0)
1689
					{
1690
						$results[] = $txt['attachment_transfer_no_find'];
1691
					}
1692
1693
					break;
1694
				}
1695
1696
				// No more to move after this batch then set the finished flag.
1697
				if ($tomove_count < $limit)
1698
				{
1699
					$break = true;
1700
				}
1701
1702
				// Move them
1703
				$moved = array();
1704
				$dir_size = empty($dir_size) ? 0 : $dir_size;
1705
				$limiting_by_size_num = $attachmentsDir->hasSizeLimit() || $attachmentsDir->hasNumFilesLimit();
1706
1707
				foreach ($tomove as $row)
1708
				{
1709
					$source = getAttachmentFilename($row['filename'], $row['id_attach'], $row['id_folder'], false, $row['file_hash']);
1710
					$dest = $attachmentsDir->getPathById($new_dir) . '/' . basename($source);
1711
1712
					// Size and file count check
1713
					if ($limiting_by_size_num)
1714
					{
1715
						$dir_files++;
1716
						$dir_size += empty($row['size']) ? filesize($source) : $row['size'];
1717
1718
						// If we've reached a directory limit. Do something if we are in auto mode, otherwise set an error.
1719
						if ($attachmentsDir->remainingSpace($dir_size) === 0 || $attachmentsDir->remainingFiles($dir_files) === 0)
1720
						{
1721
							// Since we're in auto mode. Create a new folder and reset the counters.
1722
							$create_result = $attachmentsDir->manageBySpace();
1723
							if ($create_result === true)
1724
							{
1725
								$results[] = sprintf($txt['attachments_transfered'], $total_moved, $attachmentsDir->getPathById($new_dir));
1726
								if ($total_not_moved !== 0)
1727
								{
1728
									$results[] = sprintf($txt['attachments_not_transfered'], $total_not_moved);
1729
								}
1730
1731
								$dir_files = 0;
1732
								$total_moved = 0;
1733
								$total_not_moved = 0;
1734
1735
								$break = false;
1736
								break;
1737
							}
1738
							// Hmm, not in auto. Time to bail out then...
1739
							else
1740
							{
1741
								$results[] = $txt['attachment_transfer_no_room'];
1742
								$break = true;
1743
								break;
1744
							}
1745
						}
1746
					}
1747
1748
					// Actually move the file
1749
					if (@rename($source, $dest))
1750
					{
1751
						$total_moved++;
1752
						$current_progress++;
1753
						$moved[] = $row['id_attach'];
1754
					}
1755
					else
1756
					{
1757
						$total_not_moved++;
1758
					}
1759
				}
1760
1761
				// Update the database to reflect the new file location
1762
				if (!empty($moved))
1763
				{
1764
					moveAttachments($moved, $new_dir);
1765
				}
1766
1767
				$new_dir = $modSettings['currentAttachmentUploadDir'];
1768
1769
				// Create / update the progress bar.
1770
				// @todo why was this done this way?
1771
				if (!$break)
1772
				{
1773
					$percent_done = min(round($current_progress / $total_progress * 100, 0), 100);
1774
					$progressBar = '
1775
						<div class="progress_bar">
1776
							<div class="green_percent" style="width: ' . $percent_done . '%;">' . $percent_done . '%</div>
1777
						</div>';
1778
1779
					// Write it to a file so it can be displayed
1780
					$fp = fopen(BOARDDIR . '/progress.php', 'wb');
1781
					fwrite($fp, $progressBar);
1782
					fclose($fp);
1783
					usleep(500000);
1784
				}
1785
			}
1786
1787
			$results[] = sprintf($txt['attachments_transfered'], $total_moved, $attachmentsDir->getPathById($new_dir));
1788
			if ($total_not_moved !== 0)
1789
			{
1790
				$results[] = sprintf($txt['attachments_not_transfered'], $total_not_moved);
1791
			}
1792
		}
1793
1794
		// All done, time to clean up
1795
		$_SESSION['results'] = $results;
1796
		$this->file_functions->delete(BOARDDIR . '/progress.php');
1797
1798
		redirectexit('action=admin;area=manageattachments;sa=maintenance#transfer');
1799
	}
1800
}
1801