Failed Conditions
Branch release-2.1 (4e22cf)
by Rick
06:39
created

ManageLanguages.php ➔ ManageLanguages()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 33
Code Lines 19

Duplication

Lines 33
Ratio 100 %

Importance

Changes 0
Metric Value
cc 3
eloc 19
nc 4
nop 0
dl 33
loc 33
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file handles the administration of languages tasks.
5
 *
6
 * Simple Machines Forum (SMF)
7
 *
8
 * @package SMF
9
 * @author Simple Machines http://www.simplemachines.org
10
 * @copyright 2017 Simple Machines and individual contributors
11
 * @license http://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1 Beta 4
14
 */
15
16
if (!defined('SMF'))
17
	die('No direct access...');
18
19
/**
20
 * This is the main function for the languages area.
21
 * It dispatches the requests.
22
 * Loads the ManageLanguages template. (sub-actions will use it)
23
 * @todo lazy loading.
0 ignored issues
show
Coding Style introduced by
Comment refers to a TODO task

This check looks TODO comments that have been left in the code.

``TODO``s show that something is left unfinished and should be attended to.

Loading history...
24
 *
25
 * @uses ManageSettings language file
26
 */
27 View Code Duplication
function ManageLanguages()
0 ignored issues
show
Duplication introduced by
This function seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
28
{
29
	global $context, $txt;
30
31
	loadTemplate('ManageLanguages');
32
	loadLanguage('ManageSettings');
33
34
	$context['page_title'] = $txt['edit_languages'];
35
	$context['sub_template'] = 'show_settings';
36
37
	$subActions = array(
38
		'edit' => 'ModifyLanguages',
39
		'add' => 'AddLanguage',
40
		'settings' => 'ModifyLanguageSettings',
41
		'downloadlang' => 'DownloadLanguage',
42
		'editlang' => 'ModifyLanguage',
43
	);
44
45
	// By default we're managing languages.
46
	$_REQUEST['sa'] = isset($_REQUEST['sa']) && isset($subActions[$_REQUEST['sa']]) ? $_REQUEST['sa'] : 'edit';
47
	$context['sub_action'] = $_REQUEST['sa'];
48
49
	// Load up all the tabs...
50
	$context[$context['admin_menu_name']]['tab_data'] = array(
51
		'title' => $txt['language_configuration'],
52
		'description' => $txt['language_description'],
53
	);
54
55
	call_integration_hook('integrate_manage_languages', array(&$subActions));
56
57
	// Call the right function for this sub-action.
58
	call_helper($subActions[$_REQUEST['sa']]);
59
}
60
61
/**
62
 * Interface for adding a new language
63
 *
64
 * @uses ManageLanguages template, add_language sub-template.
65
 */
66
function AddLanguage()
67
{
68
	global $context, $sourcedir, $txt, $smcFunc;
69
70
	// Are we searching for new languages courtesy of Simple Machines?
71
	if (!empty($_POST['smf_add_sub']))
72
	{
73
		// Need fetch_web_data.
74
		require_once($sourcedir . '/Subs-Package.php');
75
76
		$context['smf_search_term'] = $smcFunc['htmlspecialchars'](trim($_POST['smf_add']));
77
78
		$listOptions = array(
79
			'id' => 'smf_languages',
80
			'get_items' => array(
81
				'function' => 'list_getLanguagesList',
82
			),
83
			'columns' => array(
84
				'name' => array(
85
					'header' => array(
86
						'value' => $txt['name'],
87
					),
88
					'data' => array(
89
						'db' => 'name',
90
					),
91
				),
92
				'description' => array(
93
					'header' => array(
94
						'value' => $txt['add_language_smf_desc'],
95
					),
96
					'data' => array(
97
						'db' => 'description',
98
					),
99
				),
100
				'version' => array(
101
					'header' => array(
102
						'value' => $txt['add_language_smf_version'],
103
					),
104
					'data' => array(
105
						'db' => 'version',
106
					),
107
				),
108
				'utf8' => array(
109
					'header' => array(
110
						'value' => $txt['add_language_smf_utf8'],
111
					),
112
					'data' => array(
113
						'db' => 'utf8',
114
					),
115
				),
116
				'install_link' => array(
117
					'header' => array(
118
						'value' => $txt['add_language_smf_install'],
119
						'class' => 'centercol',
120
					),
121
					'data' => array(
122
						'db' => 'install_link',
123
						'class' => 'centercol',
124
					),
125
				),
126
			),
127
		);
128
129
		require_once($sourcedir . '/Subs-List.php');
130
		createList($listOptions);
131
132
		$context['default_list'] = 'smf_languages';
133
	}
134
135
	$context['sub_template'] = 'add_language';
136
}
137
138
/**
139
 * Gets a list of available languages from the mother ship
140
 * Will return a subset if searching, otherwise all avaialble
141
 *
142
 * @return array An array containing information about each available language
0 ignored issues
show
Documentation introduced by
Should the return type not be null|array? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
143
 */
144
function list_getLanguagesList()
145
{
146
	global $forum_version, $context, $sourcedir, $smcFunc, $txt, $scripturl;
147
148
	// We're going to use this URL.
149
	$url = 'https://download.simplemachines.org/fetch_language.php?version=' . urlencode(strtr($forum_version, array('SMF ' => '')));
150
151
	// Load the class file and stick it into an array.
152
	require_once($sourcedir . '/Class-Package.php');
153
	$language_list = new xmlArray(fetch_web_data($url), true);
0 ignored issues
show
Security Bug introduced by
It seems like fetch_web_data($url) targeting fetch_web_data() can also be of type false; however, xmlArray::__construct() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
154
155
	// Check that the site responded and that the language exists.
156
	if (!$language_list->exists('languages'))
157
		$context['smf_error'] = 'no_response';
158
	elseif (!$language_list->exists('languages/language'))
159
		$context['smf_error'] = 'no_files';
160
	else
161
	{
162
		$language_list = $language_list->path('languages[0]');
163
		$lang_files = $language_list->set('language');
164
		$smf_languages = array();
165
		foreach ($lang_files as $file)
166
		{
167
			// Were we searching?
168
			if (!empty($context['smf_search_term']) && strpos($file->fetch('name'), $smcFunc['strtolower']($context['smf_search_term'])) === false)
169
				continue;
170
171
			$smf_languages[] = array(
172
				'id' => $file->fetch('id'),
173
				'name' => $smcFunc['ucwords']($file->fetch('name')),
174
				'version' => $file->fetch('version'),
175
				'utf8' => $file->fetch('utf8') ? $txt['yes'] : $txt['no'],
176
				'description' => $file->fetch('description'),
177
				'install_link' => '<a href="' . $scripturl . '?action=admin;area=languages;sa=downloadlang;did=' . $file->fetch('id') . ';' . $context['session_var'] . '=' . $context['session_id'] . '">' . $txt['add_language_smf_install'] . '</a>',
178
			);
179
		}
180
		if (empty($smf_languages))
181
			$context['smf_error'] = 'no_files';
182
		else
183
			return $smf_languages;
184
	}
185
}
186
187
/**
188
 * Download a language file from the Simple Machines website.
189
 * Requires a valid download ID ("did") in the URL.
190
 * Also handles installing language files.
191
 * Attempts to chmod things as needed.
192
 * Uses a standard list to display information about all the files and where they'll be put.
193
 *
194
 * @uses ManageLanguages template, download_language sub-template.
195
 * @uses Admin template, show_list sub-template.
196
 */
197
function DownloadLanguage()
198
{
199
	global $context, $sourcedir, $forum_version, $boarddir, $txt, $scripturl, $modSettings;
200
201
	loadLanguage('ManageSettings');
202
	require_once($sourcedir . '/Subs-Package.php');
203
204
	// Clearly we need to know what to request.
205
	if (!isset($_GET['did']))
206
		fatal_lang_error('no_access', false);
207
208
	// Some lovely context.
209
	$context['download_id'] = $_GET['did'];
210
	$context['sub_template'] = 'download_language';
211
	$context['menu_data_' . $context['admin_menu_id']]['current_subsection'] = 'add';
212
213
	// Can we actually do the installation - and do they want to?
214
	if (!empty($_POST['do_install']) && !empty($_POST['copy_file']))
215
	{
216
		checkSession('get');
217
		validateToken('admin-dlang');
218
219
		$chmod_files = array();
220
		$install_files = array();
221
222
		// Check writable status.
223
		foreach ($_POST['copy_file'] as $file)
224
		{
225
			// Check it's not very bad.
226
			if (strpos($file, '..') !== false || (strpos($file, 'Themes') !== 0 && !preg_match('~agreement\.[A-Za-z-_0-9]+\.txt$~', $file)))
227
				fatal_error($txt['languages_download_illegal_paths']);
228
229
			$chmod_files[] = $boarddir . '/' . $file;
230
			$install_files[] = $file;
231
		}
232
233
		// Call this in case we have work to do.
234
		$file_status = create_chmod_control($chmod_files);
235
		$files_left = $file_status['files']['notwritable'];
236
237
		// Something not writable?
238
		if (!empty($files_left))
239
			$context['error_message'] = $txt['languages_download_not_chmod'];
240
		// Otherwise, go go go!
241
		elseif (!empty($install_files))
242
		{
243
			read_tgz_file('https://download.simplemachines.org/fetch_language.php?version=' . urlencode(strtr($forum_version, array('SMF ' => ''))) . ';fetch=' . urlencode($_GET['did']), $boarddir, false, true, $install_files);
244
245
			// Make sure the files aren't stuck in the cache.
246
			package_flush_cache();
247
			$context['install_complete'] = sprintf($txt['languages_download_complete_desc'], $scripturl . '?action=admin;area=languages');
248
249
			return;
250
		}
251
	}
252
253
	// Open up the old china.
254
	if (!isset($archive_content))
0 ignored issues
show
Bug introduced by
The variable $archive_content seems only to be defined at a later point. As such the call to isset() seems to always evaluate to false.

This check marks calls to isset(...) or empty(...) that are found before the variable itself is defined. These will always have the same result.

This is likely the result of code being shifted around. Consider removing these calls.

Loading history...
255
		$archive_content = read_tgz_file('https://download.simplemachines.org/fetch_language.php?version=' . urlencode(strtr($forum_version, array('SMF ' => ''))) . ';fetch=' . urlencode($_GET['did']), null);
256
257
	if (empty($archive_content))
258
		fatal_error($txt['add_language_error_no_response']);
259
260
	// Now for each of the files, let's do some *stuff*
261
	$context['files'] = array(
262
		'lang' => array(),
263
		'other' => array(),
264
	);
265
	$context['make_writable'] = array();
266
	foreach ($archive_content as $file)
0 ignored issues
show
Bug introduced by
The expression $archive_content of type array|false is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
267
	{
268
		$dirname = dirname($file['filename']);
269
		$filename = basename($file['filename']);
270
		$extension = substr($filename, strrpos($filename, '.') + 1);
271
272
		// Don't do anything with files we don't understand.
273
		if (!in_array($extension, array('php', 'jpg', 'gif', 'jpeg', 'png', 'txt')))
274
			continue;
275
276
		// Basic data.
277
		$context_data = array(
278
			'name' => $filename,
279
			'destination' => $boarddir . '/' . $file['filename'],
280
			'generaldest' => $file['filename'],
281
			'size' => $file['size'],
282
			// Does chmod status allow the copy?
283
			'writable' => false,
284
			// Should we suggest they copy this file?
285
			'default_copy' => true,
286
			// Does the file already exist, if so is it same or different?
287
			'exists' => false,
288
		);
289
290
		// Does the file exist, is it different and can we overwrite?
291
		if (file_exists($boarddir . '/' . $file['filename']))
292
		{
293
			if (is_writable($boarddir . '/' . $file['filename']))
294
				$context_data['writable'] = true;
295
296
			// Finally, do we actually think the content has changed?
297
			if ($file['size'] == filesize($boarddir . '/' . $file['filename']) && $file['md5'] == md5_file($boarddir . '/' . $file['filename']))
298
			{
299
				$context_data['exists'] = 'same';
300
				$context_data['default_copy'] = false;
301
			}
302
			// Attempt to discover newline character differences.
303
			elseif ($file['md5'] == md5(preg_replace("~[\r]?\n~", "\r\n", file_get_contents($boarddir . '/' . $file['filename']))))
304
			{
305
				$context_data['exists'] = 'same';
306
				$context_data['default_copy'] = false;
307
			}
308
			else
309
				$context_data['exists'] = 'different';
310
		}
311
		// No overwrite?
312
		else
313
		{
314
			// Can we at least stick it in the directory...
315
			if (is_writable($boarddir . '/' . $dirname))
316
				$context_data['writable'] = true;
317
		}
318
319
		// I love PHP files, that's why I'm a developer and not an artistic type spending my time drinking absinth and living a life of sin...
320
		if ($extension == 'php' && preg_match('~\w+\.\w+(?:-utf8)?\.php~', $filename))
321
		{
322
			$context_data += array(
323
				'version' => '??',
324
				'cur_version' => false,
325
				'version_compare' => 'newer',
326
			);
327
328
			list ($name, $language) = explode('.', $filename);
0 ignored issues
show
Unused Code introduced by
The assignment to $language is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
329
330
			// Let's get the new version, I like versions, they tell me that I'm up to date.
331
			if (preg_match('~\s*Version:\s+(.+?);\s*' . preg_quote($name, '~') . '~i', $file['preview'], $match) == 1)
332
				$context_data['version'] = $match[1];
333
334
			// Now does the old file exist - if so what is it's version?
335
			if (file_exists($boarddir . '/' . $file['filename']))
336
			{
337
				// OK - what is the current version?
338
				$fp = fopen($boarddir . '/' . $file['filename'], 'rb');
339
				$header = fread($fp, 768);
340
				fclose($fp);
341
342
				// Find the version.
343
				if (preg_match('~(?://|/\*)\s*Version:\s+(.+?);\s*' . preg_quote($name, '~') . '(?:[\s]{2}|\*/)~i', $header, $match) == 1)
344
				{
345
					$context_data['cur_version'] = $match[1];
346
347
					// How does this compare?
348
					if ($context_data['cur_version'] == $context_data['version'])
349
						$context_data['version_compare'] = 'same';
350
					elseif ($context_data['cur_version'] > $context_data['version'])
351
						$context_data['version_compare'] = 'older';
352
353
					// Don't recommend copying if the version is the same.
354
					if ($context_data['version_compare'] != 'newer')
355
						$context_data['default_copy'] = false;
356
				}
357
			}
358
359
			// Add the context data to the main set.
360
			$context['files']['lang'][] = $context_data;
361
		}
362
		elseif ($extension == '.txt' && stripos($filename, 'agreement') !== false)
363
		{
364
			// Registration agreement is a primary file
365
			$context['files']['lang'][] = $context_data;
366
		}
367
		else
368
		{
369
			// There shouldn't be anything else, but load this into "other" in case we decide to handle it in the future
370
			$context['files']['other'][] = $context_data;
371
		}
372
373
		// Collect together all non-writable areas.
374
		if (!$context_data['writable'])
375
			$context['make_writable'][] = $context_data['destination'];
376
	}
377
378
	// Before we go to far can we make anything writable, eh, eh?
379
	if (!empty($context['make_writable']))
380
	{
381
		// What is left to be made writable?
382
		$file_status = create_chmod_control($context['make_writable']);
383
		$context['still_not_writable'] = $file_status['files']['notwritable'];
384
385
		// Mark those which are now writable as such.
386
		foreach ($context['files'] as $type => $data)
387
		{
388
			if ($type == 'lang')
389
			{
390 View Code Duplication
				foreach ($data as $k => $file)
391
					if (!$file['writable'] && !in_array($file['destination'], $context['still_not_writable']))
392
						$context['files'][$type][$k]['writable'] = true;
393
			}
394
			else
395
			{
396
				foreach ($data as $theme => $files)
397 View Code Duplication
					foreach ($files as $k => $file)
398
						if (!$file['writable'] && !in_array($file['destination'], $context['still_not_writable']))
399
							$context['files'][$type][$theme][$k]['writable'] = true;
400
			}
401
		}
402
403
		// Are we going to need more language stuff?
404
		if (!empty($context['still_not_writable']))
405
			loadLanguage('Packages');
406
	}
407
408
	// This is the list for the main files.
409
	$listOptions = array(
410
		'id' => 'lang_main_files_list',
411
		'title' => $txt['languages_download_main_files'],
412
		'get_items' => array(
413
			'function' => function() use ($context)
414
			{
415
				return $context['files']['lang'];
416
			},
417
		),
418
		'columns' => array(
419
			'name' => array(
420
				'header' => array(
421
					'value' => $txt['languages_download_filename'],
422
				),
423
				'data' => array(
424
					'function' => function($rowData) use ($txt)
425
					{
426
						return '<strong>' . $rowData['name'] . '</strong><br><span class="smalltext">' . $txt['languages_download_dest'] . ': ' . $rowData['destination'] . '</span>' . ($rowData['version_compare'] == 'older' ? '<br>' . $txt['languages_download_older'] : '');
427
					},
428
				),
429
			),
430
			'writable' => array(
431
				'header' => array(
432
					'value' => $txt['languages_download_writable'],
433
				),
434
				'data' => array(
435 View Code Duplication
					'function' => function($rowData) use ($txt)
436
					{
437
						return '<span style="color: ' . ($rowData['writable'] ? 'green' : 'red') . ';">' . ($rowData['writable'] ? $txt['yes'] : $txt['no']) . '</span>';
438
					},
439
				),
440
			),
441
			'version' => array(
442
				'header' => array(
443
					'value' => $txt['languages_download_version'],
444
				),
445
				'data' => array(
446
					'function' => function($rowData) use ($txt)
447
					{
448
						return '<span style="color: ' . ($rowData['version_compare'] == 'older' ? 'red' : ($rowData['version_compare'] == 'same' ? 'orange' : 'green')) . ';">' . $rowData['version'] . '</span>';
449
					},
450
				),
451
			),
452
			'exists' => array(
453
				'header' => array(
454
					'value' => $txt['languages_download_exists'],
455
				),
456
				'data' => array(
457
					'function' => function($rowData) use ($txt)
458
					{
459
						return $rowData['exists'] ? ($rowData['exists'] == 'same' ? $txt['languages_download_exists_same'] : $txt['languages_download_exists_different']) : $txt['no'];
460
					},
461
				),
462
			),
463
			'copy' => array(
464
				'header' => array(
465
					'value' => $txt['languages_download_copy'],
466
					'class' => 'centercol',
467
				),
468
				'data' => array(
469
					'function' => function($rowData)
470
					{
471
						return '<input type="checkbox" name="copy_file[]" value="' . $rowData['generaldest'] . '"' . ($rowData['default_copy'] ? ' checked' : '') . ' class="input_check">';
472
					},
473
					'style' => 'width: 4%;',
474
					'class' => 'centercol',
475
				),
476
			),
477
		),
478
	);
479
480
	// Kill the cache, as it is now invalid..
481
	if (!empty($modSettings['cache_enable']))
482
	{
483
		cache_put_data('known_languages', null, !empty($modSettings['cache_enable']) && $modSettings['cache_enable'] < 1 ? 86400 : 3600);
484
		cache_put_data('known_languages_all', null, !empty($modSettings['cache_enable']) && $modSettings['cache_enable'] < 1 ? 86400 : 3600);
485
	}
486
487
	require_once($sourcedir . '/Subs-List.php');
488
	createList($listOptions);
489
490
	$context['default_list'] = 'lang_main_files_list';
491
	createToken('admin-dlang');
492
}
493
494
/**
495
 * This lists all the current languages and allows editing of them.
496
 */
497
function ModifyLanguages()
498
{
499
	global $txt, $context, $scripturl, $modSettings;
500
	global $sourcedir, $language, $boarddir;
501
502
	// Setting a new default?
503
	if (!empty($_POST['set_default']) && !empty($_POST['def_language']))
504
	{
505
		checkSession();
506
		validateToken('admin-lang');
507
508
		getLanguages();
509
		$lang_exists = false;
510
		foreach ($context['languages'] as $lang)
511
		{
512
			if ($_POST['def_language'] == $lang['filename'])
513
			{
514
				$lang_exists = true;
515
				break;
516
			}
517
		}
518
519
		if ($_POST['def_language'] != $language && $lang_exists)
520
		{
521
			require_once($sourcedir . '/Subs-Admin.php');
522
			updateSettingsFile(array('language' => '\'' . $_POST['def_language'] . '\''));
523
			$language = $_POST['def_language'];
524
		}
525
	}
526
527
	// Create another one time token here.
528
	createToken('admin-lang');
529
530
	$listOptions = array(
531
		'id' => 'language_list',
532
		'items_per_page' => $modSettings['defaultMaxListItems'],
533
		'base_href' => $scripturl . '?action=admin;area=languages',
534
		'title' => $txt['edit_languages'],
535
		'get_items' => array(
536
			'function' => 'list_getLanguages',
537
		),
538
		'get_count' => array(
539
			'function' => 'list_getNumLanguages',
540
		),
541
		'columns' => array(
542
			'default' => array(
543
				'header' => array(
544
					'value' => $txt['languages_default'],
545
					'class' => 'centercol',
546
				),
547
				'data' => array(
548
					'function' => function($rowData)
549
					{
550
						return '<input type="radio" name="def_language" value="' . $rowData['id'] . '"' . ($rowData['default'] ? ' checked' : '') . ' onclick="highlightSelected(\'list_language_list_' . $rowData['id'] . '\');" class="input_radio">';
551
					},
552
					'style' => 'width: 8%;',
553
					'class' => 'centercol',
554
				),
555
			),
556
			'name' => array(
557
				'header' => array(
558
					'value' => $txt['languages_lang_name'],
559
				),
560
				'data' => array(
561
					'function' => function($rowData) use ($scripturl)
562
					{
563
						return sprintf('<a href="%1$s?action=admin;area=languages;sa=editlang;lid=%2$s">%3$s</a>', $scripturl, $rowData['id'], $rowData['name']);
564
					},
565
				),
566
			),
567
			'character_set' => array(
568
				'header' => array(
569
					'value' => $txt['languages_character_set'],
570
				),
571
				'data' => array(
572
					'db_htmlsafe' => 'char_set',
573
				),
574
			),
575
			'count' => array(
576
				'header' => array(
577
					'value' => $txt['languages_users'],
578
				),
579
				'data' => array(
580
					'db_htmlsafe' => 'count',
581
				),
582
			),
583
			'locale' => array(
584
				'header' => array(
585
					'value' => $txt['languages_locale'],
586
				),
587
				'data' => array(
588
					'db_htmlsafe' => 'locale',
589
				),
590
			),
591
		),
592
		'form' => array(
593
			'href' => $scripturl . '?action=admin;area=languages',
594
			'token' => 'admin-lang',
595
		),
596
		'additional_rows' => array(
597
			array(
598
				'position' => 'top_of_list',
599
				'value' => '<input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '"><input type="submit" name="set_default" value="' . $txt['save'] . '"' . (is_writable($boarddir . '/Settings.php') ? '' : ' disabled') . ' class="button_submit">',
600
			),
601
			array(
602
				'position' => 'bottom_of_list',
603
				'value' => '<input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '"><input type="submit" name="set_default" value="' . $txt['save'] . '"' . (is_writable($boarddir . '/Settings.php') ? '' : ' disabled') . ' class="button_submit">',
604
			),
605
		),
606
	);
607
608
	// We want to highlight the selected language. Need some Javascript for this.
609
	addInlineJavaScript('
610
	function highlightSelected(box)
611
	{
612
		$("tr.highlight2").removeClass("highlight2");
613
		$("#" + box).addClass("highlight2");
614
	}
615
	highlightSelected("list_language_list_' . ($language == '' ? 'english' : $language) . '");', true);
616
617
	// Display a warning if we cannot edit the default setting.
618
	if (!is_writable($boarddir . '/Settings.php'))
619
		$listOptions['additional_rows'][] = array(
620
				'position' => 'after_title',
621
				'value' => $txt['language_settings_writable'],
622
				'class' => 'smalltext alert',
623
			);
624
625
	require_once($sourcedir . '/Subs-List.php');
626
	createList($listOptions);
627
628
	$context['sub_template'] = 'show_list';
629
	$context['default_list'] = 'language_list';
630
}
631
632
/**
633
 * How many languages?
634
 * Callback for the list in ManageLanguageSettings().
635
 * @return int The number of available languages
636
 */
637
function list_getNumLanguages()
638
{
639
	return count(getLanguages());
640
}
641
642
/**
643
 * Fetch the actual language information.
644
 * Callback for $listOptions['get_items']['function'] in ManageLanguageSettings.
645
 * Determines which languages are available by looking for the "index.{language}.php" file.
646
 * Also figures out how many users are using a particular language.
647
 * @return array An array of information about currenty installed languages
648
 */
649
function list_getLanguages()
650
{
651
	global $settings, $smcFunc, $language, $context, $txt;
652
653
	$languages = array();
654
	// Keep our old entries.
655
	$old_txt = $txt;
656
	$backup_actual_theme_dir = $settings['actual_theme_dir'];
657
	$backup_base_theme_dir = !empty($settings['base_theme_dir']) ? $settings['base_theme_dir'] : '';
658
659
	// Override these for now.
660
	$settings['actual_theme_dir'] = $settings['base_theme_dir'] = $settings['default_theme_dir'];
661
	getLanguages();
662
663
	// Put them back.
664
	$settings['actual_theme_dir'] = $backup_actual_theme_dir;
665
	if (!empty($backup_base_theme_dir))
666
		$settings['base_theme_dir'] = $backup_base_theme_dir;
667
	else
668
		unset($settings['base_theme_dir']);
669
670
	// Get the language files and data...
671
	foreach ($context['languages'] as $lang)
672
	{
673
		// Load the file to get the character set.
674
		require($settings['default_theme_dir'] . '/languages/index.' . $lang['filename'] . '.php');
675
676
		$languages[$lang['filename']] = array(
677
			'id' => $lang['filename'],
678
			'count' => 0,
679
			'char_set' => $txt['lang_character_set'],
680
			'default' => $language == $lang['filename'] || ($language == '' && $lang['filename'] == 'english'),
681
			'locale' => $txt['lang_locale'],
682
			'name' => $smcFunc['ucwords'](strtr($lang['filename'], array('_' => ' ', '-utf8' => ''))),
683
		);
684
	}
685
686
	// Work out how many people are using each language.
687
	$request = $smcFunc['db_query']('', '
688
		SELECT lngfile, COUNT(*) AS num_users
689
		FROM {db_prefix}members
690
		GROUP BY lngfile',
691
		array(
692
		)
693
	);
694
	while ($row = $smcFunc['db_fetch_assoc']($request))
695
	{
696
		// Default?
697
		if (empty($row['lngfile']) || !isset($languages[$row['lngfile']]))
698
			$row['lngfile'] = $language;
699
700
		if (!isset($languages[$row['lngfile']]) && isset($languages['english']))
701
			$languages['english']['count'] += $row['num_users'];
702
		elseif (isset($languages[$row['lngfile']]))
703
			$languages[$row['lngfile']]['count'] += $row['num_users'];
704
	}
705
	$smcFunc['db_free_result']($request);
706
707
	// Restore the current users language.
708
	$txt = $old_txt;
709
710
	// Return how many we have.
711
	return $languages;
712
}
713
714
/**
715
 * Edit language related settings.
716
 *
717
 * @param bool $return_config Whether to return the $config_vars array (used in admin search)
718
 * @return void|array Returns nothing or the $config_vars array if $return_config is true
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<*,array>|null.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
719
 */
720
function ModifyLanguageSettings($return_config = false)
721
{
722
	global $scripturl, $context, $txt, $boarddir, $sourcedir;
723
724
	// We'll want to save them someday.
725
	require_once $sourcedir . '/ManageServer.php';
726
727
	// Warn the user if the backup of Settings.php failed.
728
	$settings_not_writable = !is_writable($boarddir . '/Settings.php');
729
	$settings_backup_fail = !@is_writable($boarddir . '/Settings_bak.php') || !@copy($boarddir . '/Settings.php', $boarddir . '/Settings_bak.php');
730
731
	/* If you're writing a mod, it's a bad idea to add things here....
732
	For each option:
733
		variable name, description, type (constant), size/possible values, helptext.
734
	OR	an empty string for a horizontal rule.
735
	OR	a string for a titled section. */
736
	$config_vars = array(
737
		'language' => array('language', $txt['default_language'], 'file', 'select', array(), null, 'disabled' => $settings_not_writable),
738
		array('userLanguage', $txt['userLanguage'], 'db', 'check', null, 'userLanguage'),
739
	);
740
741
	call_integration_hook('integrate_language_settings', array(&$config_vars));
742
743
	if ($return_config)
744
		return $config_vars;
745
746
	// Get our languages. No cache
747
	getLanguages(false);
748
	foreach ($context['languages'] as $lang)
749
		$config_vars['language'][4][$lang['filename']] = array($lang['filename'], $lang['name']);
750
751
	// Saving settings?
752 View Code Duplication
	if (isset($_REQUEST['save']))
753
	{
754
		checkSession();
755
756
		call_integration_hook('integrate_save_language_settings', array(&$config_vars));
757
758
		saveSettings($config_vars);
759
		if (!$settings_not_writable && !$settings_backup_fail)
760
			$_SESSION['adm-save'] = true;
761
		redirectexit('action=admin;area=languages;sa=settings');
762
	}
763
764
	// Setup the template stuff.
765
	$context['post_url'] = $scripturl . '?action=admin;area=languages;sa=settings;save';
766
	$context['settings_title'] = $txt['language_settings'];
767
	$context['save_disabled'] = $settings_not_writable;
768
769 View Code Duplication
	if ($settings_not_writable)
770
		$context['settings_message'] = '<div class="centertext"><strong>' . $txt['settings_not_writable'] . '</strong></div><br>';
771
	elseif ($settings_backup_fail)
772
		$context['settings_message'] = '<div class="centertext"><strong>' . $txt['admin_backup_fail'] . '</strong></div><br>';
773
774
	// Fill the config array.
775
	prepareServerSettingsContext($config_vars);
776
}
777
778
/**
779
 * Edit a particular set of language entries.
780
 */
781
function ModifyLanguage()
782
{
783
	global $settings, $context, $smcFunc, $txt, $modSettings, $boarddir, $sourcedir, $language;
784
785
	loadLanguage('ManageSettings');
786
787
	// Select the languages tab.
788
	$context['menu_data_' . $context['admin_menu_id']]['current_subsection'] = 'edit';
789
	$context['page_title'] = $txt['edit_languages'];
790
	$context['sub_template'] = 'modify_language_entries';
791
792
	$context['lang_id'] = $_GET['lid'];
793
	list($theme_id, $file_id) = empty($_REQUEST['tfid']) || strpos($_REQUEST['tfid'], '+') === false ? array(1, '') : explode('+', $_REQUEST['tfid']);
794
795
	// Clean the ID - just in case.
796
	preg_match('~([A-Za-z0-9_-]+)~', $context['lang_id'], $matches);
797
	$context['lang_id'] = $matches[1];
798
799
	// Get all the theme data.
800
	$request = $smcFunc['db_query']('', '
801
		SELECT id_theme, variable, value
802
		FROM {db_prefix}themes
803
		WHERE id_theme != {int:default_theme}
804
			AND id_member = {int:no_member}
805
			AND variable IN ({string:name}, {string:theme_dir})',
806
		array(
807
			'default_theme' => 1,
808
			'no_member' => 0,
809
			'name' => 'name',
810
			'theme_dir' => 'theme_dir',
811
		)
812
	);
813
	$themes = array(
814
		1 => array(
815
			'name' => $txt['dvc_default'],
816
			'theme_dir' => $settings['default_theme_dir'],
817
		),
818
	);
819
	while ($row = $smcFunc['db_fetch_assoc']($request))
820
		$themes[$row['id_theme']][$row['variable']] = $row['value'];
821
	$smcFunc['db_free_result']($request);
822
823
	// This will be where we look
824
	$lang_dirs = array();
825
826
	// Does a hook need to add in some additional places to look for languages?
827
	call_integration_hook('integrate_modifylanguages', array(&$themes, &$lang_dirs));
828
829
	// Check we have themes with a path and a name - just in case - and add the path.
830
	foreach ($themes as $id => $data)
831
	{
832
		if (count($data) != 2)
833
			unset($themes[$id]);
834
		elseif (is_dir($data['theme_dir'] . '/languages'))
835
			$lang_dirs[$id] = $data['theme_dir'] . '/languages';
836
837
		// How about image directories?
838
		if (is_dir($data['theme_dir'] . '/images/' . $context['lang_id']))
839
			$images_dirs[$id] = $data['theme_dir'] . '/images/' . $context['lang_id'];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$images_dirs was never initialized. Although not strictly required by PHP, it is generally a good practice to add $images_dirs = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
840
	}
841
842
	$current_file = $file_id ? $lang_dirs[$theme_id] . '/' . $file_id . '.' . $context['lang_id'] . '.php' : '';
843
844
	// Now for every theme get all the files and stick them in context!
845
	$context['possible_files'] = array();
846
	foreach ($lang_dirs as $theme => $theme_dir)
847
	{
848
		// Open it up.
849
		$dir = dir($theme_dir);
850
		while ($entry = $dir->read())
851
		{
852
			// We're only after the files for this language.
853
			if (preg_match('~^([A-Za-z]+)\.' . $context['lang_id'] . '\.php$~', $entry, $matches) == 0)
854
				continue;
855
856
			if (!isset($context['possible_files'][$theme]))
857
				$context['possible_files'][$theme] = array(
858
					'id' => $theme,
859
					'name' => $themes[$theme]['name'],
860
					'files' => array(),
861
				);
862
863
			$context['possible_files'][$theme]['files'][] = array(
864
				'id' => $matches[1],
865
				'name' => isset($txt['lang_file_desc_' . $matches[1]]) ? $txt['lang_file_desc_' . $matches[1]] : $matches[1],
866
				'selected' => $theme_id == $theme && $file_id == $matches[1],
867
			);
868
		}
869
		$dir->close();
870
		usort($context['possible_files'][$theme]['files'], function($val1, $val2)
871
		{
872
			return strcmp($val1['name'], $val2['name']);
873
		});
874
	}
875
876
	// We no longer wish to speak this language.
877
	if (!empty($_POST['delete_main']) && $context['lang_id'] != 'english')
878
	{
879
		checkSession();
880
		validateToken('admin-mlang');
881
882
		// @todo Todo: FTP Controls?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
883
		require_once($sourcedir . '/Subs-Package.php');
884
885
		// First, Make a backup?
886
		if (!empty($modSettings['package_make_backups']) && (!isset($_SESSION['last_backup_for']) || $_SESSION['last_backup_for'] != $context['lang_id'] . '$$$'))
887
		{
888
			$_SESSION['last_backup_for'] = $context['lang_id'] . '$$$';
889
			$result = package_create_backup('backup_lang_' . $context['lang_id']);
890
			if (!$result)
891
				fatal_lang_error('could_not_language_backup', false);
892
		}
893
894
		// Second, loop through the array to remove the files.
895
		foreach ($lang_dirs as $curPath)
896
		{
897
			foreach ($context['possible_files'][1]['files'] as $lang)
898
				if (file_exists($curPath . '/' . $lang['id'] . '.' . $context['lang_id'] . '.php'))
899
					unlink($curPath . '/' . $lang['id'] . '.' . $context['lang_id'] . '.php');
900
901
			// Check for the email template.
902 View Code Duplication
			if (file_exists($curPath . '/EmailTemplates.' . $context['lang_id'] . '.php'))
903
				unlink($curPath . '/EmailTemplates.' . $context['lang_id'] . '.php');
904
		}
905
906
		// Third, the agreement file.
907 View Code Duplication
		if (file_exists($boarddir . '/agreement.' . $context['lang_id'] . '.txt'))
908
			unlink($boarddir . '/agreement.' . $context['lang_id'] . '.txt');
909
910
		// Fourth, a related images folder, if it exists...
911
		if (!empty($images_dirs))
912
			foreach ($images_dirs as $curPath)
913
				if (is_dir($curPath))
914
					deltree($curPath);
915
916
		// Members can no longer use this language.
917
		$smcFunc['db_query']('', '
918
			UPDATE {db_prefix}members
919
			SET lngfile = {empty}
920
			WHERE lngfile = {string:current_language}',
921
			array(
922
				'empty_string' => '',
923
				'current_language' => $context['lang_id'],
924
			)
925
		);
926
927
		// Fifth, update getLanguages() cache.
928 View Code Duplication
		if (!empty($modSettings['cache_enable']))
929
		{
930
			cache_put_data('known_languages', null, !empty($modSettings['cache_enable']) && $modSettings['cache_enable'] < 1 ? 86400 : 3600);
931
		}
932
933
		// Sixth, if we deleted the default language, set us back to english?
934
		if ($context['lang_id'] == $language)
935
		{
936
			require_once($sourcedir . '/Subs-Admin.php');
937
			$language = 'english';
938
			updateSettingsFile(array('language' => '\'' . $language . '\''));
939
		}
940
941
		// Seventh, get out of here.
942
		redirectexit('action=admin;area=languages;sa=edit;' . $context['session_var'] . '=' . $context['session_id']);
943
	}
944
945
	// Saving primary settings?
946
	$madeSave = false;
947
	if (!empty($_POST['save_main']) && !$current_file)
948
	{
949
		checkSession();
950
		validateToken('admin-mlang');
951
952
		// Read in the current file.
953
		$current_data = implode('', file($settings['default_theme_dir'] . '/languages/index.' . $context['lang_id'] . '.php'));
954
		// These are the replacements. old => new
955
		$replace_array = array(
956
			'~\$txt\[\'lang_character_set\'\]\s=\s(\'|")[^\r\n]+~' => '$txt[\'lang_character_set\'] = \'' . preg_replace('~[^\w-]~i', '', $_POST['character_set']) . '\';',
957
			'~\$txt\[\'lang_locale\'\]\s=\s(\'|")[^\r\n]+~' => '$txt[\'lang_locale\'] = \'' . preg_replace('~[^\w-]~i', '', $_POST['locale']) . '\';',
958
			'~\$txt\[\'lang_dictionary\'\]\s=\s(\'|")[^\r\n]+~' => '$txt[\'lang_dictionary\'] = \'' . preg_replace('~[^\w-]~i', '', $_POST['dictionary']) . '\';',
959
			'~\$txt\[\'lang_spelling\'\]\s=\s(\'|")[^\r\n]+~' => '$txt[\'lang_spelling\'] = \'' . preg_replace('~[^\w-]~i', '', $_POST['spelling']) . '\';',
960
			'~\$txt\[\'lang_rtl\'\]\s=\s[A-Za-z0-9]+;~' => '$txt[\'lang_rtl\'] = ' . (!empty($_POST['rtl']) ? 'true' : 'false') . ';',
961
		);
962
		$current_data = preg_replace(array_keys($replace_array), array_values($replace_array), $current_data);
963
		$fp = fopen($settings['default_theme_dir'] . '/languages/index.' . $context['lang_id'] . '.php', 'w+');
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $fp. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
964
		fwrite($fp, $current_data);
965
		fclose($fp);
966
967
		$madeSave = true;
968
	}
969
970
	// Quickly load index language entries.
971
	$old_txt = $txt;
972
	require($settings['default_theme_dir'] . '/languages/index.' . $context['lang_id'] . '.php');
973
	$context['lang_file_not_writable_message'] = is_writable($settings['default_theme_dir'] . '/languages/index.' . $context['lang_id'] . '.php') ? '' : sprintf($txt['lang_file_not_writable'], $settings['default_theme_dir'] . '/languages/index.' . $context['lang_id'] . '.php');
974
	// Setup the primary settings context.
975
	$context['primary_settings'] = array(
976
		'name' => $smcFunc['ucwords'](strtr($context['lang_id'], array('_' => ' ', '-utf8' => ''))),
977
		'character_set' => $txt['lang_character_set'],
978
		'locale' => $txt['lang_locale'],
979
		'dictionary' => $txt['lang_dictionary'],
980
		'spelling' => $txt['lang_spelling'],
981
		'rtl' => $txt['lang_rtl'],
982
	);
983
984
	// Restore normal service.
985
	$txt = $old_txt;
986
987
	// Are we saving?
988
	$save_strings = array();
989
	if (isset($_POST['save_entries']) && !empty($_POST['entry']))
990
	{
991
		checkSession();
992
		validateToken('admin-mlang');
993
994
		// Clean each entry!
995
		foreach ($_POST['entry'] as $k => $v)
996
		{
997
			// Only try to save if it's changed!
998
			if ($_POST['entry'][$k] != $_POST['comp'][$k])
999
				$save_strings[$k] = cleanLangString($v, false);
1000
		}
1001
	}
1002
1003
	// If we are editing a file work away at that.
1004
	if ($current_file)
1005
	{
1006
		$context['entries_not_writable_message'] = is_writable($current_file) ? '' : sprintf($txt['lang_entries_not_writable'], $current_file);
1007
1008
		$entries = array();
1009
		// We can't just require it I'm afraid - otherwise we pass in all kinds of variables!
1010
		$multiline_cache = '';
1011
		foreach (file($current_file) as $line)
1012
		{
1013
			// Got a new entry?
1014
			if ($line[0] == '$' && !empty($multiline_cache))
1015
			{
1016
				preg_match('~\$(helptxt|txt|editortxt)\[\'(.+)\'\]\s?=\s?(.+);~ms', strtr($multiline_cache, array("\r" => '')), $matches);
1017 View Code Duplication
				if (!empty($matches[3]))
1018
				{
1019
					$entries[$matches[2]] = array(
1020
						'type' => $matches[1],
1021
						'full' => $matches[0],
1022
						'entry' => $matches[3],
1023
					);
1024
					$multiline_cache = '';
1025
				}
1026
			}
1027
			$multiline_cache .= $line;
1028
		}
1029
		// Last entry to add?
1030
		if ($multiline_cache)
1031
		{
1032
			preg_match('~\$(helptxt|txt|editortxt)\[\'(.+)\'\]\s?=\s?(.+);~ms', strtr($multiline_cache, array("\r" => '')), $matches);
1033 View Code Duplication
			if (!empty($matches[3]))
1034
				$entries[$matches[2]] = array(
1035
					'type' => $matches[1],
1036
					'full' => $matches[0],
1037
					'entry' => $matches[3],
1038
				);
1039
		}
1040
1041
		// These are the entries we can definitely save.
1042
		$final_saves = array();
1043
1044
		$context['file_entries'] = array();
1045
		foreach ($entries as $entryKey => $entryValue)
1046
		{
1047
			// Ignore some things we set separately.
1048
			$ignore_files = array('lang_character_set', 'lang_locale', 'lang_dictionary', 'lang_spelling', 'lang_rtl');
1049
			if (in_array($entryKey, $ignore_files))
1050
				continue;
1051
1052
			// These are arrays that need breaking out.
1053
			$arrays = array('days', 'days_short', 'months', 'months_titles', 'months_short', 'happy_birthday_author', 'karlbenson1_author', 'nite0859_author', 'zwaldowski_author', 'geezmo_author', 'karlbenson2_author');
1054
			if (in_array($entryKey, $arrays))
1055
			{
1056
				// Get off the first bits.
1057
				$entryValue['entry'] = substr($entryValue['entry'], strpos($entryValue['entry'], '(') + 1, strrpos($entryValue['entry'], ')') - strpos($entryValue['entry'], '('));
1058
				$entryValue['entry'] = explode(',', strtr($entryValue['entry'], array(' ' => '')));
1059
1060
				// Now create an entry for each item.
1061
				$cur_index = 0;
1062
				$save_cache = array(
1063
					'enabled' => false,
1064
					'entries' => array(),
1065
				);
1066
				foreach ($entryValue['entry'] as $id => $subValue)
1067
				{
1068
					// Is this a new index?
1069
					if (preg_match('~^(\d+)~', $subValue, $matches))
1070
					{
1071
						$cur_index = $matches[1];
1072
						$subValue = substr($subValue, strpos($subValue, '\''));
1073
					}
1074
1075
					// Clean up some bits.
1076
					$subValue = strtr($subValue, array('"' => '', '\'' => '', ')' => ''));
1077
1078
					// Can we save?
1079
					if (isset($save_strings[$entryKey . '-+- ' . $cur_index]))
1080
					{
1081
						$save_cache['entries'][$cur_index] = strtr($save_strings[$entryKey . '-+- ' . $cur_index], array('\'' => ''));
1082
						$save_cache['enabled'] = true;
1083
					}
1084
					else
1085
						$save_cache['entries'][$cur_index] = $subValue;
1086
1087
					$context['file_entries'][] = array(
1088
						'key' => $entryKey . '-+- ' . $cur_index,
1089
						'value' => $subValue,
1090
						'rows' => 1,
1091
					);
1092
					$cur_index++;
1093
				}
1094
1095
				// Do we need to save?
1096
				if ($save_cache['enabled'])
1097
				{
1098
					// Format the string, checking the indexes first.
1099
					$items = array();
1100
					$cur_index = 0;
1101
					foreach ($save_cache['entries'] as $k2 => $v2)
1102
					{
1103
						// Manually show the custom index.
1104
						if ($k2 != $cur_index)
1105
						{
1106
							$items[] = $k2 . ' => \'' . $v2 . '\'';
1107
							$cur_index = $k2;
1108
						}
1109
						else
1110
							$items[] = '\'' . $v2 . '\'';
1111
1112
						$cur_index++;
1113
					}
1114
					// Now create the string!
1115
					$final_saves[$entryKey] = array(
1116
						'find' => $entryValue['full'],
1117
						'replace' => '$' . $entryValue['type'] . '[\'' . $entryKey . '\'] = array(' . implode(', ', $items) . ');',
1118
					);
1119
				}
1120
			}
1121
			else
1122
			{
1123
				// Saving?
1124
				if (isset($save_strings[$entryKey]) && $save_strings[$entryKey] != $entryValue['entry'])
1125
				{
1126
					// @todo Fix this properly.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
1127
					if ($save_strings[$entryKey] == '')
1128
						$save_strings[$entryKey] = '\'\'';
1129
1130
					// Set the new value.
1131
					$entryValue['entry'] = $save_strings[$entryKey];
1132
					// And we know what to save now!
1133
					$final_saves[$entryKey] = array(
1134
						'find' => $entryValue['full'],
1135
						'replace' => '$' . $entryValue['type'] . '[\'' . $entryKey . '\'] = ' . $save_strings[$entryKey] . ';',
1136
					);
1137
				}
1138
1139
				$editing_string = cleanLangString($entryValue['entry'], true);
1140
				$context['file_entries'][] = array(
1141
					'key' => $entryKey,
1142
					'value' => $editing_string,
1143
					'rows' => (int) (strlen($editing_string) / 38) + substr_count($editing_string, "\n") + 1,
1144
				);
1145
			}
1146
		}
1147
1148
		// Any saves to make?
1149
		if (!empty($final_saves))
1150
		{
1151
			checkSession();
1152
1153
			$file_contents = implode('', file($current_file));
1154
			foreach ($final_saves as $save)
1155
				$file_contents = strtr($file_contents, array($save['find'] => $save['replace']));
1156
1157
			// Save the actual changes.
1158
			$fp = fopen($current_file, 'w+');
1159
			fwrite($fp, strtr($file_contents, array("\r" => '')));
1160
			fclose($fp);
1161
1162
			$madeSave = true;
1163
		}
1164
1165
		// Another restore.
1166
		$txt = $old_txt;
1167
	}
1168
1169
	// If we saved, redirect.
1170
	if ($madeSave)
1171
		redirectexit('action=admin;area=languages;sa=editlang;lid=' . $context['lang_id']);
1172
1173
	createToken('admin-mlang');
1174
}
1175
1176
/**
1177
 * This function cleans language entries to/from display.
1178
 * @todo This function could be two functions?
0 ignored issues
show
Coding Style introduced by
Comment refers to a TODO task

This check looks TODO comments that have been left in the code.

``TODO``s show that something is left unfinished and should be attended to.

Loading history...
1179
 *
1180
 * @param string $string The language string
1181
 * @param bool $to_display Whether or not this is going to be displayed
1182
 * @return string The cleaned string
1183
 */
1184
function cleanLangString($string, $to_display = true)
1185
{
1186
	global $smcFunc;
1187
1188
	// If going to display we make sure it doesn't have any HTML in it - etc.
1189
	$new_string = '';
1190
	if ($to_display)
1191
	{
1192
		// Are we in a string (0 = no, 1 = single quote, 2 = parsed)
1193
		$in_string = 0;
1194
		$is_escape = false;
1195
		for ($i = 0; $i < strlen($string); $i++)
1196
		{
1197
			// Handle escapes first.
1198
			if ($string{$i} == '\\')
1199
			{
1200
				// Toggle the escape.
1201
				$is_escape = !$is_escape;
1202
				// If we're now escaped don't add this string.
1203
				if ($is_escape)
1204
					continue;
1205
			}
1206
			// Special case - parsed string with line break etc?
1207
			elseif (($string{$i} == 'n' || $string{$i} == 't') && $in_string == 2 && $is_escape)
1208
			{
1209
				// Put the escape back...
1210
				$new_string .= $string{$i} == 'n' ? "\n" : "\t";
1211
				$is_escape = false;
1212
				continue;
1213
			}
1214
			// Have we got a single quote?
1215 View Code Duplication
			elseif ($string{$i} == '\'')
1216
			{
1217
				// Already in a parsed string, or escaped in a linear string, means we print it - otherwise something special.
1218
				if ($in_string != 2 && ($in_string != 1 || !$is_escape))
1219
				{
1220
					// Is it the end of a single quote string?
1221
					if ($in_string == 1)
1222
						$in_string = 0;
1223
					// Otherwise it's the start!
1224
					else
1225
						$in_string = 1;
1226
1227
					// Don't actually include this character!
1228
					continue;
1229
				}
1230
			}
1231
			// Otherwise a double quote?
1232 View Code Duplication
			elseif ($string{$i} == '"')
1233
			{
1234
				// Already in a single quote string, or escaped in a parsed string, means we print it - otherwise something special.
1235
				if ($in_string != 1 && ($in_string != 2 || !$is_escape))
1236
				{
1237
					// Is it the end of a double quote string?
1238
					if ($in_string == 2)
1239
						$in_string = 0;
1240
					// Otherwise it's the start!
1241
					else
1242
						$in_string = 2;
1243
1244
					// Don't actually include this character!
1245
					continue;
1246
				}
1247
			}
1248
			// A join/space outside of a string is simply removed.
1249
			elseif ($in_string == 0 && (empty($string{$i}) || $string{$i} == '.'))
1250
				continue;
1251
			// Start of a variable?
1252
			elseif ($in_string == 0 && $string{$i} == '$')
1253
			{
1254
				// Find the whole of it!
1255
				preg_match('~([\$A-Za-z0-9\'\[\]_-]+)~', substr($string, $i), $matches);
1256
				if (!empty($matches[1]))
1257
				{
1258
					// Come up with some pseudo thing to indicate this is a var.
1259
					/**
1260
					 * @todo Do better than this, please!
0 ignored issues
show
Coding Style introduced by
Comment refers to a TODO task

This check looks TODO comments that have been left in the code.

``TODO``s show that something is left unfinished and should be attended to.

Loading history...
1261
					 */
1262
					$new_string .= '{%' . $matches[1] . '%}';
1263
1264
					// We're not going to reparse this.
1265
					$i += strlen($matches[1]) - 1;
1266
				}
1267
1268
				continue;
1269
			}
1270
			// Right, if we're outside of a string we have DANGER, DANGER!
1271
			elseif ($in_string == 0)
1272
			{
1273
				continue;
1274
			}
1275
1276
			// Actually add the character to the string!
1277
			$new_string .= $string{$i};
1278
			// If anything was escaped it ain't any longer!
1279
			$is_escape = false;
1280
		}
1281
1282
		// Unhtml then rehtml the whole thing!
1283
		$new_string = $smcFunc['htmlspecialchars'](un_htmlspecialchars($new_string));
1284
	}
1285
	else
1286
	{
1287
		// Keep track of what we're doing...
1288
		$in_string = 0;
1289
		// This is for deciding whether to HTML a quote.
1290
		$in_html = false;
1291
		for ($i = 0; $i < strlen($string); $i++)
1292
		{
1293
			// We don't do parsed strings apart from for breaks.
1294
			if ($in_string == 2)
1295
			{
1296
				$in_string = 0;
1297
				$new_string .= '"';
1298
			}
1299
1300
			// Not in a string yet?
1301
			if ($in_string != 1)
1302
			{
1303
				$in_string = 1;
1304
				$new_string .= ($new_string ? ' . ' : '') . '\'';
1305
			}
1306
1307
			// Is this a variable?
1308
			if ($string{$i} == '{' && $string{$i + 1} == '%' && $string{$i + 2} == '$')
1309
			{
1310
				// Grab the variable.
1311
				preg_match('~\{%([\$A-Za-z0-9\'\[\]_-]+)%\}~', substr($string, $i), $matches);
1312
				if (!empty($matches[1]))
1313
				{
1314
					if ($in_string == 1)
1315
						$new_string .= '\' . ';
1316
					elseif ($new_string)
1317
						$new_string .= ' . ';
1318
1319
					$new_string .= $matches[1];
1320
					$i += strlen($matches[1]) + 3;
1321
					$in_string = 0;
1322
				}
1323
1324
				continue;
1325
			}
1326
			// Is this a lt sign?
1327 View Code Duplication
			elseif ($string{$i} == '<')
1328
			{
1329
				// Probably HTML?
1330
				if ($string{$i + 1} != ' ')
1331
					$in_html = true;
1332
				// Assume we need an entity...
1333
				else
1334
				{
1335
					$new_string .= '&lt;';
1336
					continue;
1337
				}
1338
			}
1339
			// What about gt?
1340 View Code Duplication
			elseif ($string{$i} == '>')
1341
			{
1342
				// Will it be HTML?
1343
				if ($in_html)
1344
					$in_html = false;
1345
				// Otherwise we need an entity...
1346
				else
1347
				{
1348
					$new_string .= '&gt;';
1349
					continue;
1350
				}
1351
			}
1352
			// Is it a slash? If so escape it...
1353
			if ($string{$i} == '\\')
1354
				$new_string .= '\\';
1355
			// The infamous double quote?
1356
			elseif ($string{$i} == '"')
1357
			{
1358
				// If we're in HTML we leave it as a quote - otherwise we entity it.
1359
				if (!$in_html)
1360
				{
1361
					$new_string .= '&quot;';
1362
					continue;
1363
				}
1364
			}
1365
			// A single quote?
1366
			elseif ($string{$i} == '\'')
1367
			{
1368
				// Must be in a string so escape it.
1369
				$new_string .= '\\';
1370
			}
1371
1372
			// Finally add the character to the string!
1373
			$new_string .= $string{$i};
1374
		}
1375
1376
		// If we ended as a string then close it off.
1377
		if ($in_string == 1)
1378
			$new_string .= '\'';
1379
		elseif ($in_string == 2)
1380
			$new_string .= '"';
1381
	}
1382
1383
	return $new_string;
1384
}
1385
1386
?>