Completed
Pull Request — release-2.1 (#6101)
by Jon
06:15
created

ExportProfileData_Background::add_dtd()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 4
b 0
f 0
nc 1
nop 9
dl 0
loc 8
rs 10

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/**
4
 * This file incrementally exports a member's profile data to a downloadable file.
5
 *
6
 * Simple Machines Forum (SMF)
7
 *
8
 * @package SMF
9
 * @author Simple Machines https://www.simplemachines.org
10
 * @copyright 2020 Simple Machines and individual contributors
11
 * @license https://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1 RC2
14
 */
15
16
/**
17
 * Class ExportProfileData_Background
18
 */
19
class ExportProfileData_Background extends SMF_BackgroundTask
20
{
21
	/**
22
	 * Some private variables to help the static functions in this class.
23
	 */
24
	private static $export_details = array();
25
	private static $real_modSettings = array();
26
	private static $xslt_info = array('stylesheet' => '', 'dtd' => '');
27
28
	/**
29
	 * This is the main dispatcher for the class.
30
	 * It calls the correct private function based on the information stored in
31
	 * the task details.
32
	 *
33
	 * @return bool Always returns true
34
	 */
35
	public function execute()
36
	{
37
		global $sourcedir;
38
39
		if (!defined('EXPORTING'))
40
			define('EXPORTING', 1);
41
42
		// This could happen if the user manually changed the URL params of the export request.
43
		if ($this->_details['format'] == 'HTML' && (!class_exists('DOMDocument') || !class_exists('XSLTProcessor')))
44
		{
45
			require_once($sourcedir . DIRECTORY_SEPARATOR . 'Profile-Export.php');
46
			$export_formats = get_export_formats();
47
48
			$this->_details['format'] = 'XML_XSLT';
49
			$this->_details['format_settings'] = $export_formats['XML_XSLT'];
50
		}
51
52
		// Inform static functions of the export format, etc.
53
		self::$export_details = $this->_details;
54
55
		// For exports only, members can always see their own posts, even in boards that they can no longer access.
56
		$member_info = $this->getMinUserInfo(array($this->_details['uid']));
57
		$member_info = array_merge($member_info[$this->_details['uid']], array(
58
			'buddies' => array(),
59
			'query_see_board' => '1=1',
60
			'query_see_message_board' => '1=1',
61
			'query_see_topic_board' => '1=1',
62
			'query_wanna_see_board' => '1=1',
63
			'query_wanna_see_message_board' => '1=1',
64
			'query_wanna_see_topic_board' => '1=1',
65
		));
66
67
		// Use some temporary integration hooks to manipulate BBC parsing during export.
68
		add_integration_function('integrate_pre_parsebbc', 'ExportProfileData_Background::pre_parsebbc', false);
69
		add_integration_function('integrate_post_parsebbc', 'ExportProfileData_Background::post_parsebbc', false);
70
		add_integration_function('integrate_bbc_codes', 'ExportProfileData_Background::bbc_codes', false);
71
		add_integration_function('integrate_post_parseAttachBBC', 'ExportProfileData_Background::post_parseAttachBBC', false);
72
		add_integration_function('integrate_attach_bbc_validate', 'ExportProfileData_Background::attach_bbc_validate', false);
73
74
		// We currently support exporting to XML and HTML
75
		if ($this->_details['format'] == 'XML')
76
			$this->exportXml($member_info);
77
		elseif ($this->_details['format'] == 'HTML')
78
			$this->exportHtml($member_info);
79
		elseif ($this->_details['format'] == 'XML_XSLT')
80
			$this->exportXmlXslt($member_info);
81
82
		return true;
83
	}
84
85
	/**
86
	 * The workhorse of this class. Compiles profile data to XML files.
87
	 *
88
	 * @param array $member_info Minimal $user_info about the relevant member.
89
	 */
90
	protected function exportXml($member_info)
91
	{
92
		global $smcFunc, $sourcedir, $context, $modSettings, $settings, $user_info, $mbname;
93
		global $user_profile, $txt, $scripturl, $query_this_board;
94
95
		// For convenience...
96
		$uid = $this->_details['uid'];
97
		$lang = $this->_details['lang'];
98
		$included = $this->_details['included'];
99
		$start = $this->_details['start'];
100
		$latest = $this->_details['latest'];
101
		$datatype = $this->_details['datatype'];
102
103
		if (!isset($included[$datatype]['func']) || !isset($included[$datatype]['langfile']))
104
			return;
105
106
		require_once($sourcedir . DIRECTORY_SEPARATOR . 'News.php');
107
		require_once($sourcedir . DIRECTORY_SEPARATOR . 'ScheduledTasks.php');
108
109
		// Setup.
110
		$done = false;
111
		$delay = 0;
112
		$func = $included[$datatype]['func'];
0 ignored issues
show
Unused Code introduced by
The assignment to $func is dead and can be removed.
Loading history...
113
		$context['xmlnews_uid'] = $uid;
114
		$context['xmlnews_limit'] = !empty($modSettings['export_rate']) ? $modSettings['export_rate'] : 250;
115
		$context[$datatype . '_start'] = $start[$datatype];
116
		$datatypes = array_keys($included);
117
118
		// Fake a wee bit of $user_info so that loading the member data & language doesn't choke.
119
		$user_info = $member_info;
120
121
		loadEssentialThemeData();
122
		$settings['actual_theme_dir'] = $settings['theme_dir'];
123
		$context['user']['id'] = $uid;
124
		$context['user']['language'] = $lang;
125
		loadMemberData($uid);
126
		loadLanguage(implode('+', array_unique(array('index', 'Modifications', 'Stats', 'Profile', $included[$datatype]['langfile']))), $lang);
127
128
		// @todo Ask lawyers whether the GDPR requires us to include posts in the recycle bin.
129
		$query_this_board = '{query_see_board}' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? ' AND b.id_board != ' . $modSettings['recycle_board'] : '');
130
131
		// We need a valid export directory.
132
		if (empty($modSettings['export_dir']) || !file_exists($modSettings['export_dir']))
133
		{
134
			require_once($sourcedir . DIRECTORY_SEPARATOR . 'Profile-Export.php');
135
			if (create_export_dir() === false)
136
				return;
137
		}
138
139
		$export_dir_slash = $modSettings['export_dir'] . DIRECTORY_SEPARATOR;
140
141
		$idhash = hash_hmac('sha1', $uid, get_auth_secret());
142
		$idhash_ext = $idhash . '.' . $this->_details['format_settings']['extension'];
143
144
		// Increment the file number until we reach one that doesn't exist.
145
		$filenum = 1;
146
		$realfile = $export_dir_slash . $filenum . '_' . $idhash_ext;
147
		while (file_exists($realfile))
148
			$realfile = $export_dir_slash . ++$filenum . '_' . $idhash_ext;
149
150
		$tempfile = $export_dir_slash . $idhash_ext . '.tmp';
151
		$progressfile = $export_dir_slash . $idhash_ext . '.progress.json';
152
153
		$feed_meta = array(
154
			'title' => sprintf($txt['profile_of_username'], $user_profile[$uid]['real_name']),
155
			'desc' => sentence_list(array_map(function ($datatype) use ($txt) { return $txt[$datatype]; }, array_keys($included))),
156
			'author' => $mbname,
157
			'source' => $scripturl . '?action=profile;u=' . $uid,
158
			'self' => '', // Unused, but can't be null.
159
			'page' => &$filenum,
160
		);
161
162
		// Some paranoid hosts disable or hamstring the disk space functions in an attempt at security via obscurity.
163
		$check_diskspace = !empty($modSettings['export_min_diskspace_pct']) && function_exists('disk_free_space') && function_exists('disk_total_space') && intval(@disk_total_space($modSettings['export_dir']) >= 1440);
164
		$minspace = $check_diskspace ? ceil(disk_total_space($modSettings['export_dir']) * $modSettings['export_min_diskspace_pct'] / 100) : 0;
165
166
		// If a necessary file is missing, we need to start over.
167
		if (!file_exists($tempfile) || !file_exists($progressfile) || filesize($progressfile) == 0)
168
		{
169
			foreach (array_merge(array($tempfile, $progressfile), glob($export_dir_slash . '*_' . $idhash_ext)) as $fpath)
0 ignored issues
show
Bug introduced by
It seems like glob($export_dir_slash . '*_' . $idhash_ext) can also be of type false; however, parameter $array2 of array_merge() does only seem to accept array|null, 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

169
			foreach (array_merge(array($tempfile, $progressfile), /** @scrutinizer ignore-type */ glob($export_dir_slash . '*_' . $idhash_ext)) as $fpath)
Loading history...
170
				@unlink($fpath);
171
172
			$filenum = 1;
173
			$realfile = $export_dir_slash . $filenum . '_' . $idhash_ext;
174
175
			buildXmlFeed('smf', array(), $feed_meta, 'profile');
176
			file_put_contents($tempfile, implode('', $context['feed']));
177
178
			$progress = array_fill_keys($datatypes, 0);
179
			file_put_contents($progressfile, $smcFunc['json_encode']($progress));
180
		}
181
		else
182
			$progress = $smcFunc['json_decode'](file_get_contents($progressfile), true);
183
184
		// If the temporary file has grown enormous, save it so we can start a new one.
185
		// Under normal circumstances this should never happen.
186
		if (file_exists($tempfile) && filesize($tempfile) >= 1024 * 1024 * 250)
187
		{
188
			rename($tempfile, $realfile);
189
			$realfile = $export_dir_slash . ++$filenum . '_' . $idhash_ext;
190
		}
191
192
		// Get the data, always in ascending order.
193
		$xml_data = call_user_func($included[$datatype]['func'], 'smf', true);
194
195
		// No data retrived? Just move on then.
196
		if (empty($xml_data))
197
			$datatype_done = true;
198
199
		// Basic profile data is quick and easy.
200
		elseif ($datatype == 'profile')
201
		{
202
			buildXmlFeed('smf', $xml_data, $feed_meta, 'profile');
203
			file_put_contents($tempfile, implode('', $context['feed']));
204
205
			$progress[$datatype] = time();
206
			$datatype_done = true;
207
208
			// Cache for subsequent reuse.
209
			$profile_basic_items = $context['feed']['items'];
210
			cache_put_data('export_profile_basic-' . $uid, $profile_basic_items, MAX_CLAIM_THRESHOLD);
211
		}
212
213
		// Posts and PMs...
214
		else
215
		{
216
			// We need the basic profile data in every export file.
217
			$profile_basic_items = cache_get_data('export_profile_basic-' . $uid, MAX_CLAIM_THRESHOLD);
218
			if (empty($profile_basic_items))
219
			{
220
				$profile_data = call_user_func($included['profile']['func'], 'smf', true);
221
				buildXmlFeed('smf', $profile_data, $feed_meta, 'profile');
222
				$profile_basic_items = $context['feed']['items'];
223
				cache_put_data('export_profile_basic-' . $uid, $profile_basic_items, MAX_CLAIM_THRESHOLD);
224
				unset($context['feed']);
225
			}
226
227
			$per_page = $this->_details['format_settings']['per_page'];
228
			$prev_item_count = empty($this->_details['item_count']) ? 0 : $this->_details['item_count'];
229
230
			// Remember the last item so we know where to start next time.
231
			$last_item = end($xml_data);
232
			if (isset($last_item['content'][0]['content']) && $last_item['content'][0]['tag'] === 'id')
233
				$last_id = $last_item['content'][0]['content'];
234
235
			// Split $xml_data into reasonably sized chunks.
236
			if (empty($prev_item_count))
237
			{
238
				$xml_data = array_chunk($xml_data, $per_page);
239
			}
240
			else
241
			{
242
				$first_chunk = array_splice($xml_data, 0, $per_page - $prev_item_count);
243
				$xml_data = array_merge(array($first_chunk), array_chunk($xml_data, $per_page));
244
				unset($first_chunk);
245
			}
246
247
			foreach ($xml_data as $chunk => $items)
248
			{
249
				unset($new_item_count);
250
251
				// Build the XML string from the data.
252
				buildXmlFeed('smf', $items, $feed_meta, 'profile');
253
254
				// If disk space is insufficient, pause for a day so the admin can fix it.
255
				if ($check_diskspace && disk_free_space($modSettings['export_dir']) - $minspace <= strlen(implode('', $context['feed']) . self::$xslt_info['stylesheet']))
256
				{
257
					loadLanguage('Errors');
258
					log_error(sprintf($txt['export_low_diskspace'], $modSettings['export_min_diskspace_pct']));
259
260
					$delay = 86400;
261
				}
262
				else
263
				{
264
					// We need a file to write to, of course.
265
					if (!file_exists($tempfile))
266
						file_put_contents($tempfile, implode('', array($context['feed']['header'], $profile_basic_items, $context['feed']['footer'])));
267
268
					// Insert the new data before the feed footer.
269
					$handle = fopen($tempfile, 'r+');
270
					if (is_resource($handle))
271
					{
272
						fseek($handle, strlen($context['feed']['footer']) * -1, SEEK_END);
273
274
						$bytes_written = fwrite($handle, $context['feed']['items'] . $context['feed']['footer']);
275
276
						// If we couldn't write everything, revert the changes and consider the write to have failed.
277
						if ($bytes_written > 0 && $bytes_written < strlen($context['feed']['items'] . $context['feed']['footer']))
278
						{
279
							fseek($handle, $bytes_written * -1, SEEK_END);
280
							$pointer_pos = ftell($handle);
281
							ftruncate($handle, $pointer_pos);
282
							rewind($handle);
283
							fseek($handle, 0, SEEK_END);
284
							fwrite($handle, $context['feed']['footer']);
285
286
							$bytes_written = false;
287
						}
288
289
						fclose($handle);
290
					}
291
292
					// Write failed. We'll try again next time.
293
					if (empty($bytes_written))
294
					{
295
						$delay = MAX_CLAIM_THRESHOLD;
296
						break;
297
					}
298
299
					// All went well.
300
					else
301
					{
302
						// Track progress by ID where appropriate, and by time otherwise.
303
						$progress[$datatype] = !isset($last_id) ? time() : $last_id;
304
305
						// Are we done with this datatype yet?
306
						if (!isset($last_id) || (count($items) < $per_page && $last_id >= $latest[$datatype]))
307
							$datatype_done = true;
308
309
						// Finished the file for this chunk, so move on to the next one.
310
						if (count($items) >= $per_page - $prev_item_count)
311
						{
312
							rename($tempfile, $realfile);
313
							$realfile = $export_dir_slash . ++$filenum . '_' . $idhash_ext;
314
315
							$prev_item_count = $new_item_count = 0;
316
						}
317
						// This was the last chunk.
318
						else
319
						{
320
							// Should we append more items to this file next time?
321
							$new_item_count = isset($last_id) ? $prev_item_count + count($items) : 0;
322
						}
323
					}
324
				}
325
			}
326
		}
327
328
		if (!empty($datatype_done))
329
		{
330
			$datatype_key = array_search($datatype, $datatypes);
331
			$done = !isset($datatypes[$datatype_key + 1]);
332
333
			if (!$done)
334
				$datatype = $datatypes[$datatype_key + 1];
335
		}
336
337
		// Remove the .tmp extension from the final tempfile so the system knows it's done.
338
		if (!empty($done))
339
		{
340
			rename($tempfile, $realfile);
341
		}
342
343
		// Oops. Apparently some sneaky monkey cancelled the export while we weren't looking.
344
		elseif (!file_exists($progressfile))
345
		{
346
			@unlink($tempfile);
347
			return;
348
		}
349
350
		// We have more work to do again later.
351
		else
352
		{
353
			$start[$datatype] = $progress[$datatype];
354
355
			$new_details = array(
356
				'format' => $this->_details['format'],
357
				'uid' => $uid,
358
				'lang' => $lang,
359
				'included' => $included,
360
				'start' => $start,
361
				'latest' => $latest,
362
				'datatype' => $datatype,
363
				'format_settings' => $this->_details['format_settings'],
364
				'last_page' => $this->_details['last_page'],
365
				'dlfilename' => $this->_details['dlfilename'],
366
			);
367
			if (!empty($new_item_count))
368
				$new_details['item_count'] = $new_item_count;
369
370
			$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
371
				array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
372
				array('$sourcedir/tasks/ExportProfileData.php', 'ExportProfileData_Background', $smcFunc['json_encode']($new_details), time() - MAX_CLAIM_THRESHOLD + $delay),
373
				array()
374
			);
375
376
			if (!file_exists($tempfile))
377
			{
378
				buildXmlFeed('smf', array(), $feed_meta, 'profile');
379
				file_put_contents($tempfile, implode('', array($context['feed']['header'], !empty($profile_basic_items) ? $profile_basic_items : '', $context['feed']['footer'])));
380
			}
381
		}
382
383
		file_put_contents($progressfile, $smcFunc['json_encode']($progress));
384
	}
385
386
	/**
387
	 * Compiles profile data to HTML.
388
	 *
389
	 * Internally calls exportXml() and then uses an XSLT stylesheet to
390
	 * transform the XML files into HTML.
391
	 *
392
	 * @param array $member_info Minimal $user_info about the relevant member.
393
	 */
394
	protected function exportHtml($member_info)
395
	{
396
		global $modSettings, $context, $smcFunc, $sourcedir;
397
398
		$context['export_last_page'] = $this->_details['last_page'];
399
		$context['export_dlfilename'] = $this->_details['dlfilename'];
400
401
		// Perform the export to XML.
402
		$this->exportXml($member_info);
403
404
		// Determine which files, if any, are ready to be transformed.
405
		$export_dir_slash = $modSettings['export_dir'] . DIRECTORY_SEPARATOR;
406
		$idhash = hash_hmac('sha1', $this->_details['uid'], get_auth_secret());
407
		$idhash_ext = $idhash . '.' . $this->_details['format_settings']['extension'];
408
409
		$new_exportfiles = array();
410
		foreach (glob($export_dir_slash . '*_' . $idhash_ext) as $completed_file)
411
		{
412
			if (file_get_contents($completed_file, false, null, 0, 6) == '<?xml ')
413
				$new_exportfiles[] = $completed_file;
414
		}
415
		if (empty($new_exportfiles))
416
			return;
417
418
		// Just in case...
419
		@set_time_limit(MAX_CLAIM_THRESHOLD * 2);
420
		if (function_exists('apache_reset_timeout'))
421
			@apache_reset_timeout();
422
423
		// Get the XSLT stylesheet.
424
		require_once($sourcedir . DIRECTORY_SEPARATOR . 'Profile-Export.php');
425
		self::$xslt_info = get_xslt_stylesheet($this->_details['format'], $this->_details['uid']);
426
427
		// Set up the XSLT processor.
428
		$xslt = new DOMDocument();
429
		$xslt->loadXML(self::$xslt_info['stylesheet']);
430
		$xsltproc = new XSLTProcessor();
431
		$xsltproc->importStylesheet($xslt);
432
433
		// Transform the files to HTML.
434
		$xmldoc = new DOMDocument();
435
		foreach ($new_exportfiles as $exportfile)
436
		{
437
			$xmldoc->load($exportfile);
438
			$xsltproc->transformToURI($xmldoc, $exportfile);
439
		}
440
	}
441
442
	/**
443
	 * Compiles profile data to XML with embedded XSLT.
444
	 *
445
	 * Internally calls exportXml() and then embeds an XSLT stylesheet into
446
	 * the XML so that it can be processed by the client.
447
	 *
448
	 * @param array $member_info Minimal $user_info about the relevant member.
449
	 */
450
	protected function exportXmlXslt($member_info)
451
	{
452
		global $modSettings, $context, $smcFunc, $sourcedir;
453
454
		$context['export_last_page'] = $this->_details['last_page'];
455
		$context['export_dlfilename'] = $this->_details['dlfilename'];
456
457
		// Embedded XSLT requires adding a special DTD and processing instruction in the main XML document.
458
		add_integration_function('integrate_xml_data', 'ExportProfileData_Background::add_dtd', false);
459
460
		// Perform the export to XML.
461
		$this->exportXml($member_info);
462
463
		// Make sure we have everything we need.
464
		if (empty(self::$xslt_info['stylesheet']))
465
		{
466
			require_once($sourcedir . DIRECTORY_SEPARATOR . 'Profile-Export.php');
467
			self::$xslt_info = get_xslt_stylesheet($this->_details['format'], $this->_details['uid']);
468
		}
469
		if (empty($context['feed']['footer']))
470
		{
471
			require_once($sourcedir . DIRECTORY_SEPARATOR . 'News.php');
472
			buildXmlFeed('smf', array(), array_fill_keys(array('title', 'desc', 'source', 'self'), ''), 'profile');
473
		}
474
475
		// Find any completed files that don't yet have the stylesheet embedded in them.
476
		$export_dir_slash = $modSettings['export_dir'] . DIRECTORY_SEPARATOR;
477
		$idhash = hash_hmac('sha1', $this->_details['uid'], get_auth_secret());
478
		$idhash_ext = $idhash . '.' . $this->_details['format_settings']['extension'];
479
480
		$test_length = strlen(self::$xslt_info['stylesheet']) + strlen($context['feed']['footer']);
481
482
		$new_exportfiles = array();
483
		foreach (glob($export_dir_slash . '*_' . $idhash_ext) as $completed_file)
484
		{
485
			if (filesize($completed_file) < $test_length || file_get_contents($completed_file, false, null, $test_length * -1) !== self::$xslt_info['stylesheet'] . $context['feed']['footer'])
486
				$new_exportfiles[] = $completed_file;
487
		}
488
		if (empty($new_exportfiles))
489
			return;
490
491
		// Embedding the XSLT means writing to the file yet again.
492
		foreach ($new_exportfiles as $exportfile)
493
		{
494
			$handle = fopen($exportfile, 'r+');
495
			if (is_resource($handle))
496
			{
497
				fseek($handle, strlen($context['feed']['footer']) * -1, SEEK_END);
498
499
				$bytes_written = fwrite($handle, self::$xslt_info['stylesheet'] . $context['feed']['footer']);
500
501
				// If we couldn't write everything, revert the changes.
502
				if ($bytes_written > 0 && $bytes_written < strlen(self::$xslt_info['stylesheet'] . $context['feed']['footer']))
503
				{
504
					fseek($handle, $bytes_written * -1, SEEK_END);
505
					$pointer_pos = ftell($handle);
506
					ftruncate($handle, $pointer_pos);
507
					rewind($handle);
508
					fseek($handle, 0, SEEK_END);
509
					fwrite($handle, $context['feed']['footer']);
510
				}
511
512
				fclose($handle);
513
			}
514
		}
515
	}
516
517
	/**
518
	 * Adds a custom DOCTYPE definition and an XSLT processing instruction to
519
	 * the main XML file's header.
520
	 */
521
	public static function add_dtd(&$xml_data, &$feed_meta, &$namespaces, &$extraFeedTags, &$forceCdataKeys, &$nsKeys, $xml_format, $subaction, &$dtd)
522
	{
523
		global $sourcedir;
524
525
		require_once($sourcedir . DIRECTORY_SEPARATOR . 'Profile-Export.php');
526
		self::$xslt_info = get_xslt_stylesheet(self::$export_details['format'], self::$export_details['uid']);
527
528
		$dtd = self::$xslt_info['dtd'];
529
	}
530
531
	/**
532
	 * Adjusts some parse_bbc() parameters for the special case of exports.
533
	 */
534
	public static function pre_parsebbc(&$message, &$smileys, &$cache_id, &$parse_tags)
535
	{
536
		global $modSettings, $context, $user_info;
537
538
		$cache_id = '';
539
540
		if (in_array(self::$export_details['format'], array('HTML', 'XML_XSLT')))
541
		{
542
			foreach (array('smileys_url', 'attachmentThumbnails') as $var)
543
				if (isset($modSettings[$var]))
544
					self::$real_modSettings[$var] = $modSettings[$var];
545
546
			$modSettings['smileys_url'] = '.';
547
			$modSettings['attachmentThumbnails'] = false;
548
		}
549
		else
550
		{
551
			$smileys = false;
552
553
			if (!isset($modSettings['disabledBBC']))
554
				$modSettings['disabledBBC'] = 'attach';
555
			else
556
			{
557
				self::$real_modSettings['disabledBBC'] = $modSettings['disabledBBC'];
558
559
				if (strpos($modSettings['disabledBBC'], 'attach') === false)
560
					$modSettings['disabledBBC'] = implode(',', array_merge(array_filter(explode(',', $modSettings['disabledBBC'])), array('attach')));
561
			}
562
		}
563
	}
564
565
	/**
566
	 * Reverses changes made by pre_parsebbc()
567
	 */
568
	public static function post_parsebbc(&$message, &$smileys, &$cache_id, &$parse_tags)
569
	{
570
		global $modSettings, $context;
571
572
		foreach (array('disabledBBC', 'smileys_url', 'attachmentThumbnails') as $var)
573
			if (isset(self::$real_modSettings[$var]))
574
				$modSettings[$var] = self::$real_modSettings[$var];
575
	}
576
577
	/**
578
	 * Adjusts certain BBCodes for the special case of exports.
579
	 */
580
	public static function bbc_codes(&$codes, &$no_autolink_tags)
581
	{
582
		foreach ($codes as &$code)
583
		{
584
			// To make the "Select" link work we'd need to embed a bunch more JS. Not worth it.
585
			if ($code['tag'] === 'code')
586
				$code['content'] = preg_replace('~<a class="codeoperation\b.*?</a>~', '', $code['content']);
587
		}
588
	}
589
590
	/**
591
	 * Adjusts the attachment download URL for the special case of exports.
592
	 */
593
	public static function post_parseAttachBBC(&$attachContext)
594
	{
595
		global $scripturl, $context;
596
		static $dltokens;
597
598
		if (empty($dltokens[$context['xmlnews_uid']]))
599
		{
600
			$idhash = hash_hmac('sha1', $context['xmlnews_uid'], get_auth_secret());
601
			$dltokens[$context['xmlnews_uid']] = hash_hmac('sha1', $idhash, get_auth_secret());
602
		}
603
604
		$attachContext['orig_href'] = $scripturl . '?action=profile;area=dlattach;u=' . $context['xmlnews_uid'] . ';attach=' . $attachContext['id'] . ';t=' . $dltokens[$context['xmlnews_uid']];
605
		$attachContext['href'] = rawurlencode($attachContext['id'] . ' - ' . html_entity_decode($attachContext['name']));
606
	}
607
608
	/**
609
	 * Adjusts the format of the HTML produced by the attach BBCode.
610
	 */
611
	public static function attach_bbc_validate(&$returnContext, $currentAttachment, $tag, $data, $disabled, $params)
612
	{
613
		global $smcFunc, $txt;
614
615
		$orig_link = '<a href="' . $currentAttachment['orig_href'] . '" class="bbc_link">' . $txt['export_download_original'] . '</a>';
616
		$hidden_orig_link = ' <a href="' . $currentAttachment['orig_href'] . '" class="bbc_link dlattach_' . $currentAttachment['id'] . '" style="display:none; flex: 1 0 auto; margin: auto;">' . $txt['export_download_original'] . '</a>';
617
618
		if ($params['{display}'] == 'link')
619
		{
620
			$returnContext .= ' (' . $orig_link . ')';
621
		}
622
		elseif (!empty($currentAttachment['is_image']))
623
		{
624
			$returnContext = '<span style="display: inline-flex; justify-content: center; align-items: center; position: relative;">' . preg_replace(
625
				array(
626
					'thumbnail_toggle' => '~</?a\b[^>]*>~',
627
					'src' => '~src="' . preg_quote($currentAttachment['href'], '~') . ';image"~',
628
				),
629
				array(
630
					'thumbnail_toggle' => '',
631
					'src' => 'src="' . $currentAttachment['href'] . '" onerror="$(\'.dlattach_' . $currentAttachment['id'] . '\').show(); $(\'.dlattach_' . $currentAttachment['id'] . '\').css({\'position\': \'absolute\'});"',
632
				),
633
				$returnContext
634
			) . $hidden_orig_link . '</span>' ;
635
		}
636
		elseif (strpos($currentAttachment['mime_type'], 'video/') === 0)
637
		{
638
			$returnContext = preg_replace(
639
				array(
640
					'src' => '~src="' . preg_quote($currentAttachment['href'], '~') . '"~',
641
					'opening_tag' => '~^<div class="videocontainer"~',
642
					'closing_tag' => '~</div>$~',
643
				),
644
				array(
645
					'src' => '$0 onerror="$(this).fadeTo(0, 0.2); $(\'.dlattach_' . $currentAttachment['id'] . '\').show(); $(\'.dlattach_' . $currentAttachment['id'] . '\').css({\'position\': \'absolute\'});"',
646
					'opening_tag' => '<div class="videocontainer" style="display: flex; justify-content: center; align-items: center; position: relative;"',
647
					'closing_tag' =>  $hidden_orig_link . '</div>',
648
				),
649
				$returnContext
650
			);
651
		}
652
		elseif (strpos($currentAttachment['mime_type'], 'audio/') === 0)
653
		{
654
			$returnContext = '<span style="display: inline-flex; justify-content: center; align-items: center; position: relative;">' . preg_replace(
655
				array(
656
					'opening_tag' => '~^<audio\b~',
657
				),
658
				array(
659
					'opening_tag' => '<audio onerror="$(this).fadeTo(0, 0); $(\'.dlattach_' . $currentAttachment['id'] . '\').show(); $(\'.dlattach_' . $currentAttachment['id'] . '\').css({\'position\': \'absolute\'});"',
660
				),
661
				$returnContext
662
			) . $hidden_orig_link . '</span>';
663
		}
664
		else
665
		{
666
			$returnContext = '<span style="display: inline-flex; justify-content: center; align-items: center; position: relative;">' . preg_replace(
667
				array(
668
					'obj_opening' => '~^<object\b~',
669
					'link' => '~<a href="' . preg_quote($currentAttachment['href'], '~') . '" class="bbc_link">([^<]*)</a>~',
670
				),
671
				array(
672
					'obj_opening' => '<object onerror="$(this).fadeTo(0, 0.2); $(\'.dlattach_' . $currentAttachment['id'] . '\').show(); $(\'.dlattach_' . $currentAttachment['id'] . '\').css({\'position\': \'absolute\'});"~',
673
					'link' => '$0 (' . $orig_link . ')',
674
				),
675
				$returnContext
676
			) . $hidden_orig_link . '</span>';
677
		}
678
	}
679
}
680
681
?>