Passed
Pull Request — release-2.1 (#6101)
by Jon
03:54
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
		$libxml_options = 0;
461
		if (LIBXML_VERSION >= 20621 && defined(LIBXML_COMPACT))
462
			$libxml_options = $libxml_options | LIBXML_COMPACT;
463
		if (LIBXML_VERSION >= 20700 && defined(LIBXML_PARSEHUGE))
464
			$libxml_options = $libxml_options | LIBXML_PARSEHUGE;
465
		if (LIBXML_VERSION >= 20900 && defined(LIBXML_BIGLINES))
466
			$libxml_options = $libxml_options | LIBXML_BIGLINES;
467
468
		// Transform the files to HTML.
469
		$i = 0;
470
		$num_files = count($new_exportfiles);
471
		$max_transform_time = 0;
472
		$xmldoc = new DOMDocument();
473
		foreach ($new_exportfiles as $exportfile)
474
		{
475
			if (function_exists('apache_reset_timeout'))
476
				@apache_reset_timeout();
477
478
			$started = microtime(true);
479
			$xmldoc->load($exportfile, $libxml_options);
480
			$xsltproc->transformToURI($xmldoc, $exportfile);
481
			$finished = microtime(true);
482
483
			$max_transform_time = max($max_transform_time, $finished - $started);
484
485
			// When deadlines loom, sometimes the best solution is procrastination.
486
			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...
487
			{
488
				// After all, there's always next time.
489
				if (empty($this->next_task))
490
				{
491
					$progressfile = $export_dir_slash . $idhash_ext . '.progress.json';
492
493
					$new_details = $this->_details;
494
					$new_details['start'] = $smcFunc['json_decode'](file_get_contents($progressfile), true);
495
496
					$this->next_task = array('$sourcedir/tasks/ExportProfileData.php', 'ExportProfileData_Background', $smcFunc['json_encode']($new_details), time() - MAX_CLAIM_THRESHOLD);
497
				}
498
499
				// So let's just relax and take a well deserved...
500
				break;
501
			}
502
		}
503
	}
504
505
	/**
506
	 * Compiles profile data to XML with embedded XSLT.
507
	 *
508
	 * Internally calls exportXml() and then embeds an XSLT stylesheet into
509
	 * the XML so that it can be processed by the client.
510
	 *
511
	 * @param array $member_info Minimal $user_info about the relevant member.
512
	 */
513
	protected function exportXmlXslt($member_info)
514
	{
515
		global $modSettings, $context, $smcFunc, $sourcedir;
516
517
		$context['export_last_page'] = $this->_details['last_page'];
518
		$context['export_dlfilename'] = $this->_details['dlfilename'];
519
520
		// Embedded XSLT requires adding a special DTD and processing instruction in the main XML document.
521
		add_integration_function('integrate_xml_data', 'ExportProfileData_Background::add_dtd', false);
522
523
		// Perform the export to XML.
524
		$this->exportXml($member_info);
525
526
		// Make sure we have everything we need.
527
		if (empty(self::$xslt_info['stylesheet']))
528
		{
529
			require_once($sourcedir . DIRECTORY_SEPARATOR . 'Profile-Export.php');
530
			self::$xslt_info = get_xslt_stylesheet($this->_details['format'], $this->_details['uid']);
531
		}
532
		if (empty($context['feed']['footer']))
533
		{
534
			require_once($sourcedir . DIRECTORY_SEPARATOR . 'News.php');
535
			buildXmlFeed('smf', array(), array_fill_keys(array('title', 'desc', 'source', 'self'), ''), 'profile');
536
		}
537
538
		// Find any completed files that don't yet have the stylesheet embedded in them.
539
		$export_dir_slash = $modSettings['export_dir'] . DIRECTORY_SEPARATOR;
540
		$idhash = hash_hmac('sha1', $this->_details['uid'], get_auth_secret());
541
		$idhash_ext = $idhash . '.' . $this->_details['format_settings']['extension'];
542
543
		$test_length = strlen(self::$xslt_info['stylesheet'] . $context['feed']['footer']);
544
545
		$new_exportfiles = array();
546
		foreach (glob($export_dir_slash . '*_' . $idhash_ext) as $completed_file)
547
		{
548
			if (filesize($completed_file) < $test_length || file_get_contents($completed_file, false, null, $test_length * -1) !== self::$xslt_info['stylesheet'] . $context['feed']['footer'])
549
				$new_exportfiles[] = $completed_file;
550
		}
551
		if (empty($new_exportfiles))
552
			return;
553
554
		// Embedding the XSLT means writing to the file yet again.
555
		foreach ($new_exportfiles as $exportfile)
556
		{
557
			$handle = fopen($exportfile, 'r+');
558
			if (is_resource($handle))
559
			{
560
				flock($handle, LOCK_EX);
561
562
				fseek($handle, strlen($context['feed']['footer']) * -1, SEEK_END);
563
564
				$bytes_written = fwrite($handle, self::$xslt_info['stylesheet'] . $context['feed']['footer']);
565
566
				// If we couldn't write everything, revert the changes.
567
				if ($bytes_written > 0 && $bytes_written < strlen(self::$xslt_info['stylesheet'] . $context['feed']['footer']))
568
				{
569
					fseek($handle, $bytes_written * -1, SEEK_END);
570
					$pointer_pos = ftell($handle);
571
					ftruncate($handle, $pointer_pos);
572
					rewind($handle);
573
					fseek($handle, 0, SEEK_END);
574
					fwrite($handle, $context['feed']['footer']);
575
				}
576
577
				flock($handle, LOCK_UN);
578
				fclose($handle);
579
			}
580
		}
581
	}
582
583
	/**
584
	 * Adds a custom DOCTYPE definition and an XSLT processing instruction to
585
	 * the main XML file's header.
586
	 */
587
	public static function add_dtd(&$xml_data, &$feed_meta, &$namespaces, &$extraFeedTags, &$forceCdataKeys, &$nsKeys, $xml_format, $subaction, &$doctype)
588
	{
589
		global $sourcedir;
590
591
		require_once($sourcedir . DIRECTORY_SEPARATOR . 'Profile-Export.php');
592
		self::$xslt_info = get_xslt_stylesheet(self::$export_details['format'], self::$export_details['uid']);
593
594
		$doctype = self::$xslt_info['doctype'];
595
	}
596
597
	/**
598
	 * Adjusts some parse_bbc() parameters for the special case of exports.
599
	 */
600
	public static function pre_parsebbc(&$message, &$smileys, &$cache_id, &$parse_tags)
601
	{
602
		global $modSettings, $context, $user_info;
603
604
		$cache_id = '';
605
606
		if (in_array(self::$export_details['format'], array('HTML', 'XML_XSLT')))
607
		{
608
			foreach (array('smileys_url', 'attachmentThumbnails') as $var)
609
				if (isset($modSettings[$var]))
610
					self::$real_modSettings[$var] = $modSettings[$var];
611
612
			$modSettings['smileys_url'] = '.';
613
			$modSettings['attachmentThumbnails'] = false;
614
		}
615
		else
616
		{
617
			$smileys = false;
618
619
			if (!isset($modSettings['disabledBBC']))
620
				$modSettings['disabledBBC'] = 'attach';
621
			else
622
			{
623
				self::$real_modSettings['disabledBBC'] = $modSettings['disabledBBC'];
624
625
				if (strpos($modSettings['disabledBBC'], 'attach') === false)
626
					$modSettings['disabledBBC'] = implode(',', array_merge(array_filter(explode(',', $modSettings['disabledBBC'])), array('attach')));
627
			}
628
		}
629
	}
630
631
	/**
632
	 * Reverses changes made by pre_parsebbc()
633
	 */
634
	public static function post_parsebbc(&$message, &$smileys, &$cache_id, &$parse_tags)
635
	{
636
		global $modSettings, $context;
637
638
		foreach (array('disabledBBC', 'smileys_url', 'attachmentThumbnails') as $var)
639
			if (isset(self::$real_modSettings[$var]))
640
				$modSettings[$var] = self::$real_modSettings[$var];
641
	}
642
643
	/**
644
	 * Adjusts certain BBCodes for the special case of exports.
645
	 */
646
	public static function bbc_codes(&$codes, &$no_autolink_tags)
647
	{
648
		foreach ($codes as &$code)
649
		{
650
			// To make the "Select" link work we'd need to embed a bunch more JS. Not worth it.
651
			if ($code['tag'] === 'code')
652
				$code['content'] = preg_replace('~<a class="codeoperation\b.*?</a>~', '', $code['content']);
653
		}
654
	}
655
656
	/**
657
	 * Adjusts the attachment download URL for the special case of exports.
658
	 */
659
	public static function post_parseAttachBBC(&$attachContext)
660
	{
661
		global $scripturl, $context;
662
		static $dltokens;
663
664
		if (empty($dltokens[$context['xmlnews_uid']]))
665
		{
666
			$idhash = hash_hmac('sha1', $context['xmlnews_uid'], get_auth_secret());
667
			$dltokens[$context['xmlnews_uid']] = hash_hmac('sha1', $idhash, get_auth_secret());
668
		}
669
670
		$attachContext['orig_href'] = $scripturl . '?action=profile;area=dlattach;u=' . $context['xmlnews_uid'] . ';attach=' . $attachContext['id'] . ';t=' . $dltokens[$context['xmlnews_uid']];
671
		$attachContext['href'] = rawurlencode($attachContext['id'] . ' - ' . html_entity_decode($attachContext['name']));
672
	}
673
674
	/**
675
	 * Adjusts the format of the HTML produced by the attach BBCode.
676
	 */
677
	public static function attach_bbc_validate(&$returnContext, $currentAttachment, $tag, $data, $disabled, $params)
678
	{
679
		global $smcFunc, $txt;
680
681
		$orig_link = '<a href="' . $currentAttachment['orig_href'] . '" class="bbc_link">' . $txt['export_download_original'] . '</a>';
682
		$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>';
683
684
		if ($params['{display}'] == 'link')
685
		{
686
			$returnContext .= ' (' . $orig_link . ')';
687
		}
688
		elseif (!empty($currentAttachment['is_image']))
689
		{
690
			$returnContext = '<span style="display: inline-flex; justify-content: center; align-items: center; position: relative;">' . preg_replace(
691
				array(
692
					'thumbnail_toggle' => '~</?a\b[^>]*>~',
693
					'src' => '~src="' . preg_quote($currentAttachment['href'], '~') . ';image"~',
694
				),
695
				array(
696
					'thumbnail_toggle' => '',
697
					'src' => 'src="' . $currentAttachment['href'] . '" onerror="$(\'.dlattach_' . $currentAttachment['id'] . '\').show(); $(\'.dlattach_' . $currentAttachment['id'] . '\').css({\'position\': \'absolute\'});"',
698
				),
699
				$returnContext
700
			) . $hidden_orig_link . '</span>' ;
701
		}
702
		elseif (strpos($currentAttachment['mime_type'], 'video/') === 0)
703
		{
704
			$returnContext = preg_replace(
705
				array(
706
					'src' => '~src="' . preg_quote($currentAttachment['href'], '~') . '"~',
707
					'opening_tag' => '~^<div class="videocontainer"~',
708
					'closing_tag' => '~</div>$~',
709
				),
710
				array(
711
					'src' => '$0 onerror="$(this).fadeTo(0, 0.2); $(\'.dlattach_' . $currentAttachment['id'] . '\').show(); $(\'.dlattach_' . $currentAttachment['id'] . '\').css({\'position\': \'absolute\'});"',
712
					'opening_tag' => '<div class="videocontainer" style="display: flex; justify-content: center; align-items: center; position: relative;"',
713
					'closing_tag' =>  $hidden_orig_link . '</div>',
714
				),
715
				$returnContext
716
			);
717
		}
718
		elseif (strpos($currentAttachment['mime_type'], 'audio/') === 0)
719
		{
720
			$returnContext = '<span style="display: inline-flex; justify-content: center; align-items: center; position: relative;">' . preg_replace(
721
				array(
722
					'opening_tag' => '~^<audio\b~',
723
				),
724
				array(
725
					'opening_tag' => '<audio onerror="$(this).fadeTo(0, 0); $(\'.dlattach_' . $currentAttachment['id'] . '\').show(); $(\'.dlattach_' . $currentAttachment['id'] . '\').css({\'position\': \'absolute\'});"',
726
				),
727
				$returnContext
728
			) . $hidden_orig_link . '</span>';
729
		}
730
		else
731
		{
732
			$returnContext = '<span style="display: inline-flex; justify-content: center; align-items: center; position: relative;">' . preg_replace(
733
				array(
734
					'obj_opening' => '~^<object\b~',
735
					'link' => '~<a href="' . preg_quote($currentAttachment['href'], '~') . '" class="bbc_link">([^<]*)</a>~',
736
				),
737
				array(
738
					'obj_opening' => '<object onerror="$(this).fadeTo(0, 0.2); $(\'.dlattach_' . $currentAttachment['id'] . '\').show(); $(\'.dlattach_' . $currentAttachment['id'] . '\').css({\'position\': \'absolute\'});"~',
739
					'link' => '$0 (' . $orig_link . ')',
740
				),
741
				$returnContext
742
			) . $hidden_orig_link . '</span>';
743
		}
744
	}
745
}
746
747
?>