ExportProfileData_Background   F
last analyzed

Complexity

Total Complexity 104

Size/Duplication

Total Lines 745
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 366
dl 0
loc 745
rs 2
c 0
b 0
f 0
wmc 104

10 Methods

Rating   Name   Duplication   Size   Complexity  
C execute() 0 64 12
A bbc_codes() 0 7 3
A pre_parsebbc() 0 27 6
B attach_bbc_validate() 0 66 5
A post_parsebbc() 0 7 3
A post_parseAttachBBC() 0 13 2
A add_dtd() 0 8 1
B exportXmlXslt() 0 67 11
B exportHtml() 0 72 11
F exportXml() 0 305 50

How to fix   Complexity   

Complex Class

Complex classes like ExportProfileData_Background often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ExportProfileData_Background, and based on these observations, apply Extract Interface, too.

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

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

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

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