Passed
Pull Request — release-2.1 (#6101)
by Jon
04:02
created

ExportProfileData_Background::execute()   B

Complexity

Conditions 8
Paths 16

Size

Total Lines 48
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

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