ExportProfileData_Background::add_dtd()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
c 0
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 contains code used to incrementally export a member's profile data
5
 * to one or more downloadable files.
6
 *
7
 * Simple Machines Forum (SMF)
8
 *
9
 * @package SMF
10
 * @author Simple Machines https://www.simplemachines.org
11
 * @copyright 2022 Simple Machines and individual contributors
12
 * @license https://www.simplemachines.org/about/smf/license.php BSD
13
 *
14
 * @version 2.1.0
15
 */
16
17
/**
18
 * @todo Find a way to throttle the export rate dynamically when dealing with
19
 * truly enormous amounts of data. Specifically, if the dataset contains lots
20
 * of posts that are ridiculously large, one or another part of the system
21
 * might choke.
22
 */
23
24
/**
25
 * Class ExportProfileData_Background
26
 */
27
class ExportProfileData_Background extends SMF_BackgroundTask
28
{
29
	/**
30
	 * @var array A copy of $this->_details for access by the static functions
31
	 * called from integration hooks.
32
	 *
33
	 * Even though this info is unique to a specific instance of this class, we
34
	 * can get away with making this variable static because only one instance
35
	 * of this class exists at a time.
36
	 */
37
	private static $export_details = array();
38
39
	/**
40
	 * @var array Temporary backup of the $modSettings array
41
	 */
42
	private static $real_modSettings = array();
43
44
	/**
45
	 * @var array The XSLT stylesheet used to transform the XML into HTML and
46
	 * a (possibly empty) DOCTYPE declaration to insert into the source XML.
47
	 *
48
	 * Even though this info is unique to a specific instance of this class, we
49
	 * can get away with making this variable static because only one instance
50
	 * of this class exists at a time.
51
	 */
52
	private static $xslt_info = array('stylesheet' => '', 'doctype' => '');
53
54
	/**
55
	 * @var array Info to create a follow-up background task, if necessary.
56
	 */
57
	private $next_task = array();
58
59
	/**
60
	 * @var array Used to ensure we exit long running tasks cleanly.
61
	 */
62
	private $time_limit = 30;
63
64
	/**
65
	 * This is the main dispatcher for the class.
66
	 * It calls the correct private function based on the information stored in
67
	 * the task details.
68
	 *
69
	 * @return bool Always returns true
70
	 */
71
	public function execute()
72
	{
73
		global $sourcedir, $smcFunc;
74
75
		if (!defined('EXPORTING'))
76
			define('EXPORTING', 1);
77
78
		// Avoid leaving files in an inconsistent state.
79
		ignore_user_abort(true);
80
81
		$this->time_limit = (ini_get('safe_mode') === false && @set_time_limit(MAX_CLAIM_THRESHOLD) !== false) ? MAX_CLAIM_THRESHOLD : ini_get('max_execution_time');
0 ignored issues
show
Documentation Bug introduced by
It seems like ini_get('safe_mode') ===...t('max_execution_time') of type integer or string is incompatible with the declared type array of property $time_limit.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
82
83
		// This could happen if the user manually changed the URL params of the export request.
84
		if ($this->_details['format'] == 'HTML' && (!class_exists('DOMDocument') || !class_exists('XSLTProcessor')))
85
		{
86
			require_once($sourcedir . DIRECTORY_SEPARATOR . 'Profile-Export.php');
87
			$export_formats = get_export_formats();
88
89
			$this->_details['format'] = 'XML_XSLT';
90
			$this->_details['format_settings'] = $export_formats['XML_XSLT'];
91
		}
92
93
		// Inform static functions of the export format, etc.
94
		self::$export_details = $this->_details;
95
96
		// For exports only, members can always see their own posts, even in boards that they can no longer access.
97
		$member_info = $this->getMinUserInfo(array($this->_details['uid']));
98
		$member_info = array_merge($member_info[$this->_details['uid']], array(
99
			'buddies' => array(),
100
			'query_see_board' => '1=1',
101
			'query_see_message_board' => '1=1',
102
			'query_see_topic_board' => '1=1',
103
			'query_wanna_see_board' => '1=1',
104
			'query_wanna_see_message_board' => '1=1',
105
			'query_wanna_see_topic_board' => '1=1',
106
		));
107
108
		// Use some temporary integration hooks to manipulate BBC parsing during export.
109
		foreach (array('pre_parsebbc', 'post_parsebbc', 'bbc_codes', 'post_parseAttachBBC', 'attach_bbc_validate') as $hook)
110
			add_integration_function('integrate_' . $hook, 'ExportProfileData_Background::' . $hook, false);
111
112
		// Perform the export.
113
		if ($this->_details['format'] == 'XML')
114
			$this->exportXml($member_info);
115
116
		elseif ($this->_details['format'] == 'HTML')
117
			$this->exportHtml($member_info);
118
119
		elseif ($this->_details['format'] == 'XML_XSLT')
120
			$this->exportXmlXslt($member_info);
121
122
		// If necessary, create a new background task to continue the export process.
123
		if (!empty($this->next_task))
124
		{
125
			$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
126
				array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
127
				$this->next_task,
128
				array()
129
			);
130
		}
131
132
		ignore_user_abort(false);
133
134
		return true;
135
	}
136
137
	/**
138
	 * The workhorse of this class. Compiles profile data to XML files.
139
	 *
140
	 * @param array $member_info Minimal $user_info about the relevant member.
141
	 */
142
	protected function exportXml($member_info)
143
	{
144
		global $smcFunc, $sourcedir, $context, $modSettings, $settings, $user_info, $mbname;
145
		global $user_profile, $txt, $scripturl, $query_this_board;
146
147
		// For convenience...
148
		$uid = $this->_details['uid'];
149
		$lang = $this->_details['lang'];
150
		$included = $this->_details['included'];
151
		$start = $this->_details['start'];
152
		$latest = $this->_details['latest'];
153
		$datatype = $this->_details['datatype'];
154
155
		if (!isset($included[$datatype]['func']) || !isset($included[$datatype]['langfile']))
156
			return;
157
158
		require_once($sourcedir . DIRECTORY_SEPARATOR . 'News.php');
159
		require_once($sourcedir . DIRECTORY_SEPARATOR . 'ScheduledTasks.php');
160
161
		// Setup.
162
		$done = false;
163
		$delay = 0;
164
		$context['xmlnews_uid'] = $uid;
165
		$context['xmlnews_limit'] = !empty($modSettings['export_rate']) ? $modSettings['export_rate'] : 250;
166
		$context[$datatype . '_start'] = $start[$datatype];
167
		$datatypes = array_keys($included);
168
169
		// Fake a wee bit of $user_info so that loading the member data & language doesn't choke.
170
		$user_info = $member_info;
171
172
		loadEssentialThemeData();
173
		$settings['actual_theme_dir'] = $settings['theme_dir'];
174
		$context['user']['id'] = $uid;
175
		$context['user']['language'] = $lang;
176
		loadMemberData($uid);
177
		loadLanguage(implode('+', array_unique(array('index', 'Modifications', 'Stats', 'Profile', $included[$datatype]['langfile']))), $lang);
178
179
		// @todo Ask lawyers whether the GDPR requires us to include posts in the recycle bin.
180
		$query_this_board = '{query_see_message_board}' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? ' AND m.id_board != ' . $modSettings['recycle_board'] : '');
181
182
		// We need a valid export directory.
183
		if (empty($modSettings['export_dir']) || !is_dir($modSettings['export_dir']) || !smf_chmod($modSettings['export_dir']))
184
		{
185
			require_once($sourcedir . DIRECTORY_SEPARATOR . 'Profile-Export.php');
186
			if (create_export_dir() === false)
187
				return;
188
		}
189
190
		$export_dir_slash = $modSettings['export_dir'] . DIRECTORY_SEPARATOR;
191
192
		$idhash = hash_hmac('sha1', $uid, get_auth_secret());
193
		$idhash_ext = $idhash . '.' . $this->_details['format_settings']['extension'];
194
195
		// Increment the file number until we reach one that doesn't exist.
196
		$filenum = 1;
197
		$realfile = $export_dir_slash . $filenum . '_' . $idhash_ext;
198
		while (file_exists($realfile))
199
			$realfile = $export_dir_slash . ++$filenum . '_' . $idhash_ext;
200
201
		$tempfile = $export_dir_slash . $idhash_ext . '.tmp';
202
		$progressfile = $export_dir_slash . $idhash_ext . '.progress.json';
203
204
		$feed_meta = array(
205
			'title' => sprintf($txt['profile_of_username'], $user_profile[$uid]['real_name']),
206
			'desc' => sentence_list(array_map(
207
				function ($datatype) use ($txt)
208
				{
209
					return $txt[$datatype];
210
				},
211
				array_keys($included)
212
			)),
213
			'author' => $mbname,
214
			'source' => $scripturl . '?action=profile;u=' . $uid,
215
			'self' => '', // Unused, but can't be null.
216
			'page' => &$filenum,
217
		);
218
219
		// Some paranoid hosts disable or hamstring the disk space functions in an attempt at security via obscurity.
220
		$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);
221
		$minspace = $check_diskspace ? ceil(disk_total_space($modSettings['export_dir']) * $modSettings['export_min_diskspace_pct'] / 100) : 0;
222
223
		// If a necessary file is missing, we need to start over.
224
		if (!file_exists($tempfile) || !file_exists($progressfile) || filesize($progressfile) == 0)
225
		{
226
			foreach (array_merge(array($tempfile, $progressfile), glob($export_dir_slash . '*_' . $idhash_ext)) as $fpath)
227
				@unlink($fpath);
228
229
			$filenum = 1;
230
			$realfile = $export_dir_slash . $filenum . '_' . $idhash_ext;
231
232
			buildXmlFeed('smf', array(), $feed_meta, 'profile');
233
			file_put_contents($tempfile, implode('', $context['feed']), LOCK_EX);
234
235
			$progress = array_fill_keys($datatypes, 0);
236
			file_put_contents($progressfile, $smcFunc['json_encode']($progress));
237
		}
238
		else
239
			$progress = $smcFunc['json_decode'](file_get_contents($progressfile), true);
240
241
		// Get the data, always in ascending order.
242
		$xml_data = call_user_func($included[$datatype]['func'], 'smf', true);
243
244
		// No data retrived? Just move on then.
245
		if (empty($xml_data))
246
			$datatype_done = true;
247
248
		// Basic profile data is quick and easy.
249
		elseif ($datatype == 'profile')
250
		{
251
			buildXmlFeed('smf', $xml_data, $feed_meta, 'profile');
252
			file_put_contents($tempfile, implode('', $context['feed']), LOCK_EX);
253
254
			$progress[$datatype] = time();
255
			$datatype_done = true;
256
257
			// Cache for subsequent reuse.
258
			$profile_basic_items = $context['feed']['items'];
259
			cache_put_data('export_profile_basic-' . $uid, $profile_basic_items, MAX_CLAIM_THRESHOLD);
260
		}
261
262
		// Posts and PMs...
263
		else
264
		{
265
			// We need the basic profile data in every export file.
266
			$profile_basic_items = cache_get_data('export_profile_basic-' . $uid, MAX_CLAIM_THRESHOLD);
267
			if (empty($profile_basic_items))
268
			{
269
				$profile_data = call_user_func($included['profile']['func'], 'smf', true);
270
				buildXmlFeed('smf', $profile_data, $feed_meta, 'profile');
271
				$profile_basic_items = $context['feed']['items'];
272
				cache_put_data('export_profile_basic-' . $uid, $profile_basic_items, MAX_CLAIM_THRESHOLD);
273
				unset($context['feed']);
274
			}
275
276
			$per_page = $this->_details['format_settings']['per_page'];
277
			$prev_item_count = empty($this->_details['item_count']) ? 0 : $this->_details['item_count'];
278
279
			// If the temp file has grown enormous, save it so we can start a new one.
280
			clearstatcache();
281
			if (file_exists($tempfile) && filesize($tempfile) >= 1024 * 1024 * 250)
282
			{
283
				rename($tempfile, $realfile);
284
				$realfile = $export_dir_slash . ++$filenum . '_' . $idhash_ext;
285
286
				if (empty($context['feed']['header']))
287
					buildXmlFeed('smf', array(), $feed_meta, 'profile');
288
289
				file_put_contents($tempfile, implode('', array($context['feed']['header'], $profile_basic_items, $context['feed']['footer'])), LOCK_EX);
290
291
				$prev_item_count = 0;
292
			}
293
294
			// Split $xml_data into reasonably sized chunks.
295
			if (empty($prev_item_count))
296
			{
297
				$xml_data = array_chunk($xml_data, $per_page);
298
			}
299
			else
300
			{
301
				$first_chunk = array_splice($xml_data, 0, $per_page - $prev_item_count);
302
				$xml_data = array_merge(array($first_chunk), array_chunk($xml_data, $per_page));
303
				unset($first_chunk);
304
			}
305
306
			foreach ($xml_data as $chunk => $items)
307
			{
308
				unset($new_item_count, $last_id);
309
310
				// Remember the last item so we know where to start next time.
311
				$last_item = end($items);
312
				if (isset($last_item['content'][0]['content']) && $last_item['content'][0]['tag'] === 'id')
313
					$last_id = $last_item['content'][0]['content'];
314
315
				// Build the XML string from the data.
316
				buildXmlFeed('smf', $items, $feed_meta, 'profile');
317
318
				// If disk space is insufficient, pause for a day so the admin can fix it.
319
				if ($check_diskspace && disk_free_space($modSettings['export_dir']) - $minspace <= strlen(implode('', $context['feed']) . self::$xslt_info['stylesheet']))
320
				{
321
					loadLanguage('Errors');
322
					log_error(sprintf($txt['export_low_diskspace'], $modSettings['export_min_diskspace_pct']));
323
324
					$delay = 86400;
325
				}
326
				else
327
				{
328
					// We need a file to write to, of course.
329
					if (!file_exists($tempfile))
330
						file_put_contents($tempfile, implode('', array($context['feed']['header'], $profile_basic_items, $context['feed']['footer'])), LOCK_EX);
331
332
					// Insert the new data before the feed footer.
333
					$handle = fopen($tempfile, 'r+');
334
					if (is_resource($handle))
335
					{
336
						flock($handle, LOCK_EX);
337
338
						fseek($handle, strlen($context['feed']['footer']) * -1, SEEK_END);
339
340
						$bytes_written = fwrite($handle, $context['feed']['items'] . $context['feed']['footer']);
341
342
						// If we couldn't write everything, revert the changes and consider the write to have failed.
343
						if ($bytes_written > 0 && $bytes_written < strlen($context['feed']['items'] . $context['feed']['footer']))
344
						{
345
							fseek($handle, $bytes_written * -1, SEEK_END);
346
							$pointer_pos = ftell($handle);
347
							ftruncate($handle, $pointer_pos);
348
							rewind($handle);
349
							fseek($handle, 0, SEEK_END);
350
							fwrite($handle, $context['feed']['footer']);
351
352
							$bytes_written = false;
353
						}
354
355
						flock($handle, LOCK_UN);
356
						fclose($handle);
357
					}
358
359
					// Write failed. We'll try again next time.
360
					if (empty($bytes_written))
361
					{
362
						$delay = MAX_CLAIM_THRESHOLD;
363
						break;
364
					}
365
366
					// All went well.
367
					else
368
					{
369
						// Track progress by ID where appropriate, and by time otherwise.
370
						$progress[$datatype] = !isset($last_id) ? time() : $last_id;
371
						file_put_contents($progressfile, $smcFunc['json_encode']($progress));
372
373
						// Are we done with this datatype yet?
374
						if (!isset($last_id) || (count($items) < $per_page && $last_id >= $latest[$datatype]))
375
							$datatype_done = true;
376
377
						// Finished the file for this chunk, so move on to the next one.
378
						if (count($items) >= $per_page - $prev_item_count)
379
						{
380
							rename($tempfile, $realfile);
381
							$realfile = $export_dir_slash . ++$filenum . '_' . $idhash_ext;
382
							$prev_item_count = $new_item_count = 0;
383
						}
384
						// This was the last chunk.
385
						else
386
						{
387
							// Should we append more items to this file next time?
388
							$new_item_count = isset($last_id) ? $prev_item_count + count($items) : 0;
389
						}
390
					}
391
				}
392
			}
393
		}
394
395
		if (!empty($datatype_done))
396
		{
397
			$datatype_key = array_search($datatype, $datatypes);
398
			$done = !isset($datatypes[$datatype_key + 1]);
399
400
			if (!$done)
401
				$datatype = $datatypes[$datatype_key + 1];
402
		}
403
404
		// Remove the .tmp extension from the final tempfile so the system knows it's done.
405
		if (!empty($done))
406
		{
407
			rename($tempfile, $realfile);
408
		}
409
410
		// Oops. Apparently some sneaky monkey cancelled the export while we weren't looking.
411
		elseif (!file_exists($progressfile))
412
		{
413
			@unlink($tempfile);
414
			return;
415
		}
416
417
		// We have more work to do again later.
418
		else
419
		{
420
			$start[$datatype] = $progress[$datatype];
421
422
			$new_details = array(
423
				'format' => $this->_details['format'],
424
				'uid' => $uid,
425
				'lang' => $lang,
426
				'included' => $included,
427
				'start' => $start,
428
				'latest' => $latest,
429
				'datatype' => $datatype,
430
				'format_settings' => $this->_details['format_settings'],
431
				'last_page' => $this->_details['last_page'],
432
				'dlfilename' => $this->_details['dlfilename'],
433
			);
434
			if (!empty($new_item_count))
435
				$new_details['item_count'] = $new_item_count;
436
437
			$this->next_task = array('$sourcedir/tasks/ExportProfileData.php', 'ExportProfileData_Background', $smcFunc['json_encode']($new_details), time() - MAX_CLAIM_THRESHOLD + $delay);
438
439
			if (!file_exists($tempfile))
440
			{
441
				buildXmlFeed('smf', array(), $feed_meta, 'profile');
442
				file_put_contents($tempfile, implode('', array($context['feed']['header'], !empty($profile_basic_items) ? $profile_basic_items : '', $context['feed']['footer'])), LOCK_EX);
443
			}
444
		}
445
446
		file_put_contents($progressfile, $smcFunc['json_encode']($progress));
447
	}
448
449
	/**
450
	 * Compiles profile data to HTML.
451
	 *
452
	 * Internally calls exportXml() and then uses an XSLT stylesheet to
453
	 * transform the XML files into HTML.
454
	 *
455
	 * @param array $member_info Minimal $user_info about the relevant member.
456
	 */
457
	protected function exportHtml($member_info)
458
	{
459
		global $modSettings, $context, $smcFunc, $sourcedir;
460
461
		$context['export_last_page'] = $this->_details['last_page'];
462
		$context['export_dlfilename'] = $this->_details['dlfilename'];
463
464
		// Perform the export to XML.
465
		$this->exportXml($member_info);
466
467
		// Determine which files, if any, are ready to be transformed.
468
		$export_dir_slash = $modSettings['export_dir'] . DIRECTORY_SEPARATOR;
469
		$idhash = hash_hmac('sha1', $this->_details['uid'], get_auth_secret());
470
		$idhash_ext = $idhash . '.' . $this->_details['format_settings']['extension'];
471
472
		$new_exportfiles = array();
473
		foreach (glob($export_dir_slash . '*_' . $idhash_ext) as $completed_file)
474
		{
475
			if (file_get_contents($completed_file, false, null, 0, 6) == '<?xml ')
476
				$new_exportfiles[] = $completed_file;
477
		}
478
		if (empty($new_exportfiles))
479
			return;
480
481
		// Get the XSLT stylesheet.
482
		require_once($sourcedir . DIRECTORY_SEPARATOR . 'Profile-Export.php');
483
		self::$xslt_info = get_xslt_stylesheet($this->_details['format'], $this->_details['uid']);
484
485
		// Set up the XSLT processor.
486
		$xslt = new DOMDocument();
487
		$xslt->loadXML(self::$xslt_info['stylesheet']);
488
		$xsltproc = new XSLTProcessor();
489
		$xsltproc->importStylesheet($xslt);
490
491
		$libxml_options = 0;
492
		foreach (array('LIBXML_COMPACT', 'LIBXML_PARSEHUGE', 'LIBXML_BIGLINES') as $libxml_option)
493
			if (defined($libxml_option))
494
				$libxml_options = $libxml_options | constant($libxml_option);
495
496
		// Transform the files to HTML.
497
		$i = 0;
498
		$num_files = count($new_exportfiles);
499
		$max_transform_time = 0;
500
		$xmldoc = new DOMDocument();
501
		foreach ($new_exportfiles as $exportfile)
502
		{
503
			if (function_exists('apache_reset_timeout'))
504
				@apache_reset_timeout();
505
506
			$started = microtime(true);
507
			$xmldoc->load($exportfile, $libxml_options);
508
			$xsltproc->transformToURI($xmldoc, $exportfile);
509
			$finished = microtime(true);
510
511
			$max_transform_time = max($max_transform_time, $finished - $started);
512
513
			// When deadlines loom, sometimes the best solution is procrastination.
514
			if (++$i < $num_files && TIME_START + $this->time_limit < $finished + $max_transform_time * 2)
515
			{
516
				// After all, there's always next time.
517
				if (empty($this->next_task))
518
				{
519
					$progressfile = $export_dir_slash . $idhash_ext . '.progress.json';
520
521
					$new_details = $this->_details;
522
					$new_details['start'] = $smcFunc['json_decode'](file_get_contents($progressfile), true);
523
524
					$this->next_task = array('$sourcedir/tasks/ExportProfileData.php', 'ExportProfileData_Background', $smcFunc['json_encode']($new_details), time() - MAX_CLAIM_THRESHOLD);
525
				}
526
527
				// So let's just relax and take a well deserved...
528
				break;
529
			}
530
		}
531
	}
532
533
	/**
534
	 * Compiles profile data to XML with embedded XSLT.
535
	 *
536
	 * Internally calls exportXml() and then embeds an XSLT stylesheet into
537
	 * the XML so that it can be processed by the client.
538
	 *
539
	 * @param array $member_info Minimal $user_info about the relevant member.
540
	 */
541
	protected function exportXmlXslt($member_info)
542
	{
543
		global $modSettings, $context, $smcFunc, $sourcedir;
544
545
		$context['export_last_page'] = $this->_details['last_page'];
546
		$context['export_dlfilename'] = $this->_details['dlfilename'];
547
548
		// Embedded XSLT requires adding a special DTD and processing instruction in the main XML document.
549
		add_integration_function('integrate_xml_data', 'ExportProfileData_Background::add_dtd', false);
550
551
		// Perform the export to XML.
552
		$this->exportXml($member_info);
553
554
		// Make sure we have everything we need.
555
		if (empty(self::$xslt_info['stylesheet']))
556
		{
557
			require_once($sourcedir . DIRECTORY_SEPARATOR . 'Profile-Export.php');
558
			self::$xslt_info = get_xslt_stylesheet($this->_details['format'], $this->_details['uid']);
559
		}
560
		if (empty($context['feed']['footer']))
561
		{
562
			require_once($sourcedir . DIRECTORY_SEPARATOR . 'News.php');
563
			buildXmlFeed('smf', array(), array_fill_keys(array('title', 'desc', 'source', 'self'), ''), 'profile');
564
		}
565
566
		// Find any completed files that don't yet have the stylesheet embedded in them.
567
		$export_dir_slash = $modSettings['export_dir'] . DIRECTORY_SEPARATOR;
568
		$idhash = hash_hmac('sha1', $this->_details['uid'], get_auth_secret());
569
		$idhash_ext = $idhash . '.' . $this->_details['format_settings']['extension'];
570
571
		$test_length = strlen(self::$xslt_info['stylesheet'] . $context['feed']['footer']);
572
573
		$new_exportfiles = array();
574
		clearstatcache();
575
		foreach (glob($export_dir_slash . '*_' . $idhash_ext) as $completed_file)
576
		{
577
			if (filesize($completed_file) < $test_length || file_get_contents($completed_file, false, null, $test_length * -1) !== self::$xslt_info['stylesheet'] . $context['feed']['footer'])
578
				$new_exportfiles[] = $completed_file;
579
		}
580
		if (empty($new_exportfiles))
581
			return;
582
583
		// Embedding the XSLT means writing to the file yet again.
584
		foreach ($new_exportfiles as $exportfile)
585
		{
586
			$handle = fopen($exportfile, 'r+');
587
			if (is_resource($handle))
588
			{
589
				flock($handle, LOCK_EX);
590
591
				fseek($handle, strlen($context['feed']['footer']) * -1, SEEK_END);
592
593
				$bytes_written = fwrite($handle, self::$xslt_info['stylesheet'] . $context['feed']['footer']);
594
595
				// If we couldn't write everything, revert the changes.
596
				if ($bytes_written > 0 && $bytes_written < strlen(self::$xslt_info['stylesheet'] . $context['feed']['footer']))
597
				{
598
					fseek($handle, $bytes_written * -1, SEEK_END);
599
					$pointer_pos = ftell($handle);
600
					ftruncate($handle, $pointer_pos);
601
					rewind($handle);
602
					fseek($handle, 0, SEEK_END);
603
					fwrite($handle, $context['feed']['footer']);
604
				}
605
606
				flock($handle, LOCK_UN);
607
				fclose($handle);
608
			}
609
		}
610
	}
611
612
	/**
613
	 * Adds a custom DOCTYPE definition and an XSLT processing instruction to
614
	 * the main XML file's header.
615
	 */
616
	public static function add_dtd(&$xml_data, &$feed_meta, &$namespaces, &$extraFeedTags, &$forceCdataKeys, &$nsKeys, $xml_format, $subaction, &$doctype)
617
	{
618
		global $sourcedir;
619
620
		require_once($sourcedir . DIRECTORY_SEPARATOR . 'Profile-Export.php');
621
		self::$xslt_info = get_xslt_stylesheet(self::$export_details['format'], self::$export_details['uid']);
622
623
		$doctype = self::$xslt_info['doctype'];
624
	}
625
626
	/**
627
	 * Adjusts some parse_bbc() parameters for the special case of exports.
628
	 */
629
	public static function pre_parsebbc(&$message, &$smileys, &$cache_id, &$parse_tags)
630
	{
631
		global $modSettings, $context, $user_info;
632
633
		$cache_id = '';
634
635
		if (in_array(self::$export_details['format'], array('HTML', 'XML_XSLT')))
636
		{
637
			foreach (array('smileys_url', 'attachmentThumbnails') as $var)
638
				if (isset($modSettings[$var]))
639
					self::$real_modSettings[$var] = $modSettings[$var];
640
641
			$modSettings['smileys_url'] = '.';
642
			$modSettings['attachmentThumbnails'] = false;
643
		}
644
		else
645
		{
646
			$smileys = false;
647
648
			if (!isset($modSettings['disabledBBC']))
649
				$modSettings['disabledBBC'] = 'attach';
650
			else
651
			{
652
				self::$real_modSettings['disabledBBC'] = $modSettings['disabledBBC'];
653
654
				if (strpos($modSettings['disabledBBC'], 'attach') === false)
655
					$modSettings['disabledBBC'] = implode(',', array_merge(array_filter(explode(',', $modSettings['disabledBBC'])), array('attach')));
656
			}
657
		}
658
	}
659
660
	/**
661
	 * Reverses changes made by pre_parsebbc()
662
	 */
663
	public static function post_parsebbc(&$message, &$smileys, &$cache_id, &$parse_tags)
664
	{
665
		global $modSettings, $context;
666
667
		foreach (array('disabledBBC', 'smileys_url', 'attachmentThumbnails') as $var)
668
			if (isset(self::$real_modSettings[$var]))
669
				$modSettings[$var] = self::$real_modSettings[$var];
670
	}
671
672
	/**
673
	 * Adjusts certain BBCodes for the special case of exports.
674
	 */
675
	public static function bbc_codes(&$codes, &$no_autolink_tags)
676
	{
677
		foreach ($codes as &$code)
678
		{
679
			// To make the "Select" link work we'd need to embed a bunch more JS. Not worth it.
680
			if ($code['tag'] === 'code')
681
				$code['content'] = preg_replace('~<a class="codeoperation\b.*?</a>~', '', $code['content']);
682
		}
683
	}
684
685
	/**
686
	 * Adjusts the attachment download URL for the special case of exports.
687
	 */
688
	public static function post_parseAttachBBC(&$attachContext)
689
	{
690
		global $scripturl, $context;
691
		static $dltokens;
692
693
		if (empty($dltokens[$context['xmlnews_uid']]))
694
		{
695
			$idhash = hash_hmac('sha1', $context['xmlnews_uid'], get_auth_secret());
696
			$dltokens[$context['xmlnews_uid']] = hash_hmac('sha1', $idhash, get_auth_secret());
697
		}
698
699
		$attachContext['orig_href'] = $scripturl . '?action=profile;area=dlattach;u=' . $context['xmlnews_uid'] . ';attach=' . $attachContext['id'] . ';t=' . $dltokens[$context['xmlnews_uid']];
700
		$attachContext['href'] = rawurlencode($attachContext['id'] . ' - ' . html_entity_decode($attachContext['name']));
701
	}
702
703
	/**
704
	 * Adjusts the format of the HTML produced by the attach BBCode.
705
	 */
706
	public static function attach_bbc_validate(&$returnContext, $currentAttachment, $tag, $data, $disabled, $params)
707
	{
708
		global $smcFunc, $txt;
709
710
		$orig_link = '<a href="' . $currentAttachment['orig_href'] . '" class="bbc_link">' . $txt['export_download_original'] . '</a>';
711
		$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>';
712
713
		if ($params['{display}'] == 'link')
714
		{
715
			$returnContext .= ' (' . $orig_link . ')';
716
		}
717
		elseif (!empty($currentAttachment['is_image']))
718
		{
719
			$returnContext = '<span style="display: inline-flex; justify-content: center; align-items: center; position: relative;">' . preg_replace(
720
				array(
721
					'thumbnail_toggle' => '~</?a\b[^>]*>~',
722
					'src' => '~src="' . preg_quote($currentAttachment['href'], '~') . ';image"~',
723
				),
724
				array(
725
					'thumbnail_toggle' => '',
726
					'src' => 'src="' . $currentAttachment['href'] . '" onerror="$(\'.dlattach_' . $currentAttachment['id'] . '\').show(); $(\'.dlattach_' . $currentAttachment['id'] . '\').css({\'position\': \'absolute\'});"',
727
				),
728
				$returnContext
729
			) . $hidden_orig_link . '</span>' ;
730
		}
731
		elseif (strpos($currentAttachment['mime_type'], 'video/') === 0)
732
		{
733
			$returnContext = preg_replace(
734
				array(
735
					'src' => '~src="' . preg_quote($currentAttachment['href'], '~') . '"~',
736
					'opening_tag' => '~^<div class="videocontainer"~',
737
					'closing_tag' => '~</div>$~',
738
				),
739
				array(
740
					'src' => '$0 onerror="$(this).fadeTo(0, 0.2); $(\'.dlattach_' . $currentAttachment['id'] . '\').show(); $(\'.dlattach_' . $currentAttachment['id'] . '\').css({\'position\': \'absolute\'});"',
741
					'opening_tag' => '<div class="videocontainer" style="display: flex; justify-content: center; align-items: center; position: relative;"',
742
					'closing_tag' =>  $hidden_orig_link . '</div>',
743
				),
744
				$returnContext
745
			);
746
		}
747
		elseif (strpos($currentAttachment['mime_type'], 'audio/') === 0)
748
		{
749
			$returnContext = '<span style="display: inline-flex; justify-content: center; align-items: center; position: relative;">' . preg_replace(
750
				array(
751
					'opening_tag' => '~^<audio\b~',
752
				),
753
				array(
754
					'opening_tag' => '<audio onerror="$(this).fadeTo(0, 0); $(\'.dlattach_' . $currentAttachment['id'] . '\').show(); $(\'.dlattach_' . $currentAttachment['id'] . '\').css({\'position\': \'absolute\'});"',
755
				),
756
				$returnContext
757
			) . $hidden_orig_link . '</span>';
758
		}
759
		else
760
		{
761
			$returnContext = '<span style="display: inline-flex; justify-content: center; align-items: center; position: relative;">' . preg_replace(
762
				array(
763
					'obj_opening' => '~^<object\b~',
764
					'link' => '~<a href="' . preg_quote($currentAttachment['href'], '~') . '" class="bbc_link">([^<]*)</a>~',
765
				),
766
				array(
767
					'obj_opening' => '<object onerror="$(this).fadeTo(0, 0.2); $(\'.dlattach_' . $currentAttachment['id'] . '\').show(); $(\'.dlattach_' . $currentAttachment['id'] . '\').css({\'position\': \'absolute\'});"~',
768
					'link' => '$0 (' . $orig_link . ')',
769
				),
770
				$returnContext
771
			) . $hidden_orig_link . '</span>';
772
		}
773
	}
774
}
775
776
?>