|
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']; |
|
|
|
|
|
|
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) |
|
|
|
|
|
|
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_START + $this->time_limit < $finished + $max_transform_time * 2) |
|
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
|
|
|
?> |