Passed
Pull Request — release-2.1 (#6101)
by Jon
04:32 queued 16s
created

ExportProfileData_Background::bbc_codes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 3
nc 3
nop 2
dl 0
loc 7
rs 10
c 0
b 0
f 0
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
?>