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

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