Passed
Pull Request — release-2.1 (#6101)
by Jon
23:29
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
	private static $next_task = array();
28
29
	/**
30
	 * This is the main dispatcher for the class.
31
	 * It calls the correct private function based on the information stored in
32
	 * the task details.
33
	 *
34
	 * @return bool Always returns true
35
	 */
36
	public function execute()
37
	{
38
		global $sourcedir, $smcFunc;
39
40
		if (!defined('EXPORTING'))
41
			define('EXPORTING', 1);
42
43
		// Avoid leaving files in an inconsistent state.
44
		ignore_user_abort(true);
45
		@set_time_limit(MAX_CLAIM_THRESHOLD);
46
47
		// This could happen if the user manually changed the URL params of the export request.
48
		if ($this->_details['format'] == 'HTML' && (!class_exists('DOMDocument') || !class_exists('XSLTProcessor')))
49
		{
50
			require_once($sourcedir . DIRECTORY_SEPARATOR . 'Profile-Export.php');
51
			$export_formats = get_export_formats();
52
53
			$this->_details['format'] = 'XML_XSLT';
54
			$this->_details['format_settings'] = $export_formats['XML_XSLT'];
55
		}
56
57
		// Inform static functions of the export format, etc.
58
		self::$export_details = $this->_details;
59
60
		// For exports only, members can always see their own posts, even in boards that they can no longer access.
61
		$member_info = $this->getMinUserInfo(array($this->_details['uid']));
62
		$member_info = array_merge($member_info[$this->_details['uid']], array(
63
			'buddies' => array(),
64
			'query_see_board' => '1=1',
65
			'query_see_message_board' => '1=1',
66
			'query_see_topic_board' => '1=1',
67
			'query_wanna_see_board' => '1=1',
68
			'query_wanna_see_message_board' => '1=1',
69
			'query_wanna_see_topic_board' => '1=1',
70
		));
71
72
		// Use some temporary integration hooks to manipulate BBC parsing during export.
73
		add_integration_function('integrate_pre_parsebbc', 'ExportProfileData_Background::pre_parsebbc', false);
74
		add_integration_function('integrate_post_parsebbc', 'ExportProfileData_Background::post_parsebbc', false);
75
		add_integration_function('integrate_bbc_codes', 'ExportProfileData_Background::bbc_codes', false);
76
		add_integration_function('integrate_post_parseAttachBBC', 'ExportProfileData_Background::post_parseAttachBBC', false);
77
		add_integration_function('integrate_attach_bbc_validate', 'ExportProfileData_Background::attach_bbc_validate', false);
78
79
		// We currently support exporting to XML and HTML
80
		if ($this->_details['format'] == 'XML')
81
			$this->exportXml($member_info);
82
		elseif ($this->_details['format'] == 'HTML')
83
			$this->exportHtml($member_info);
84
		elseif ($this->_details['format'] == 'XML_XSLT')
85
			$this->exportXmlXslt($member_info);
86
87
		// If necessary, create a new background task to continue the export process.
88
		if (!empty(self::$next_task))
89
		{
90
			$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
91
				array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
92
				self::$next_task,
93
				array()
94
			);
95
		}
96
97
		ignore_user_abort(false);
98
99
		return true;
100
	}
101
102
	/**
103
	 * The workhorse of this class. Compiles profile data to XML files.
104
	 *
105
	 * @param array $member_info Minimal $user_info about the relevant member.
106
	 */
107
	protected function exportXml($member_info)
108
	{
109
		global $smcFunc, $sourcedir, $context, $modSettings, $settings, $user_info, $mbname;
110
		global $user_profile, $txt, $scripturl, $query_this_board;
111
112
		// For convenience...
113
		$uid = $this->_details['uid'];
114
		$lang = $this->_details['lang'];
115
		$included = $this->_details['included'];
116
		$start = $this->_details['start'];
117
		$latest = $this->_details['latest'];
118
		$datatype = $this->_details['datatype'];
119
120
		if (!isset($included[$datatype]['func']) || !isset($included[$datatype]['langfile']))
121
			return;
122
123
		require_once($sourcedir . DIRECTORY_SEPARATOR . 'News.php');
124
		require_once($sourcedir . DIRECTORY_SEPARATOR . 'ScheduledTasks.php');
125
126
		// Setup.
127
		$done = false;
128
		$delay = 0;
129
		$func = $included[$datatype]['func'];
0 ignored issues
show
Unused Code introduced by
The assignment to $func is dead and can be removed.
Loading history...
130
		$context['xmlnews_uid'] = $uid;
131
		$context['xmlnews_limit'] = !empty($modSettings['export_rate']) ? $modSettings['export_rate'] : 250;
132
		$context[$datatype . '_start'] = $start[$datatype];
133
		$datatypes = array_keys($included);
134
135
		// Fake a wee bit of $user_info so that loading the member data & language doesn't choke.
136
		$user_info = $member_info;
137
138
		loadEssentialThemeData();
139
		$settings['actual_theme_dir'] = $settings['theme_dir'];
140
		$context['user']['id'] = $uid;
141
		$context['user']['language'] = $lang;
142
		loadMemberData($uid);
143
		loadLanguage(implode('+', array_unique(array('index', 'Modifications', 'Stats', 'Profile', $included[$datatype]['langfile']))), $lang);
144
145
		// @todo Ask lawyers whether the GDPR requires us to include posts in the recycle bin.
146
		$query_this_board = '{query_see_message_board}' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? ' AND m.id_board != ' . $modSettings['recycle_board'] : '');
147
148
		// We need a valid export directory.
149
		if (empty($modSettings['export_dir']) || !file_exists($modSettings['export_dir']))
150
		{
151
			require_once($sourcedir . DIRECTORY_SEPARATOR . 'Profile-Export.php');
152
			if (create_export_dir() === false)
153
				return;
154
		}
155
156
		$export_dir_slash = $modSettings['export_dir'] . DIRECTORY_SEPARATOR;
157
158
		$idhash = hash_hmac('sha1', $uid, get_auth_secret());
159
		$idhash_ext = $idhash . '.' . $this->_details['format_settings']['extension'];
160
161
		// Increment the file number until we reach one that doesn't exist.
162
		$filenum = 1;
163
		$realfile = $export_dir_slash . $filenum . '_' . $idhash_ext;
164
		while (file_exists($realfile))
165
			$realfile = $export_dir_slash . ++$filenum . '_' . $idhash_ext;
166
167
		$tempfile = $export_dir_slash . $idhash_ext . '.tmp';
168
		$progressfile = $export_dir_slash . $idhash_ext . '.progress.json';
169
170
		$feed_meta = array(
171
			'title' => sprintf($txt['profile_of_username'], $user_profile[$uid]['real_name']),
172
			'desc' => sentence_list(array_map(function ($datatype) use ($txt) { return $txt[$datatype]; }, array_keys($included))),
173
			'author' => $mbname,
174
			'source' => $scripturl . '?action=profile;u=' . $uid,
175
			'self' => '', // Unused, but can't be null.
176
			'page' => &$filenum,
177
		);
178
179
		// Some paranoid hosts disable or hamstring the disk space functions in an attempt at security via obscurity.
180
		$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);
181
		$minspace = $check_diskspace ? ceil(disk_total_space($modSettings['export_dir']) * $modSettings['export_min_diskspace_pct'] / 100) : 0;
182
183
		// If a necessary file is missing, we need to start over.
184
		if (!file_exists($tempfile) || !file_exists($progressfile) || filesize($progressfile) == 0)
185
		{
186
			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

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