Passed
Pull Request — release-2.1 (#6101)
by Jon
03:54
created

ExportProfileData_Background   F

Complexity

Total Complexity 106

Size/Duplication

Total Lines 724
Duplicated Lines 0 %

Importance

Changes 30
Bugs 2 Features 0
Metric Value
eloc 370
c 30
b 2
f 0
dl 0
loc 724
rs 2
wmc 106

10 Methods

Rating   Name   Duplication   Size   Complexity  
B execute() 0 65 11
F exportXml() 0 303 49
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 66 11
C exportHtml() 0 75 15

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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