Passed
Pull Request — release-2.1 (#6101)
by Jon
23:29
created

ExportProfileData_Background::exportHtml()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 45
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 6
eloc 26
c 5
b 0
f 0
nc 12
nop 1
dl 0
loc 45
rs 8.8817
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
?>