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