Passed
Pull Request — release-2.1 (#6101)
by Jon
11:13
created

ExportProfileData_Background::add_dtd()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 5
b 0
f 0
nc 1
nop 9
dl 0
loc 8
rs 10

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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

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