Completed
Branch develop (09fbe4)
by
unknown
29:23
created

Utils::backup_tables()   F

Complexity

Conditions 27
Paths > 20000

Size

Total Lines 160

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 27
nc 74886
nop 2
dl 0
loc 160
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/* Copyright (C) 2016 Destailleur Laurent <[email protected]>
3
 *
4
 * This program is free software; you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation; either version 3 of the License, or
7
 * any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16
 */
17
18
/**
19
 *   	\file       htdocs/core/class/utils.class.php
20
 *      \ingroup    core
21
 *		\brief      File for Utils class
22
 */
23
24
25
/**
26
 *		Class to manage utility methods
27
 */
28
class Utils
29
{
30
	/**
31
     * @var DoliDB Database handler.
32
     */
33
    public $db;
34
35
	var $output;   // Used by Cron method to return message
36
	var $result;   // Used by Cron method to return data
37
38
	/**
39
	 *	Constructor
40
	 *
41
	 *  @param	DoliDB	$db		Database handler
42
	 */
43
	function __construct($db)
44
	{
45
		$this->db = $db;
46
	}
47
48
49
	/**
50
	 *  Purge files into directory of data files.
51
	 *  CAN BE A CRON TASK
52
	 *
53
	 *  @param	string		$choice		Choice of purge mode ('tempfiles', '' or 'tempfilesold' to purge temp older than 24h, 'allfiles', 'logfile')
54
	 *  @return	int						0 if OK, < 0 if KO (this function is used also by cron so only 0 is OK)
55
	 */
56
	function purgeFiles($choice='tempfilesold')
57
	{
58
		global $conf, $langs, $dolibarr_main_data_root;
59
60
		$langs->load("admin");
61
62
		dol_syslog("Utils::purgeFiles choice=".$choice, LOG_DEBUG);
63
		require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
64
65
		$filesarray=array();
66
		if (empty($choice)) $choice='tempfilesold';
67
68
		if ($choice=='tempfiles' || $choice=='tempfilesold')
69
		{
70
			// Delete temporary files
71
			if ($dolibarr_main_data_root)
72
			{
73
				$filesarray=dol_dir_list($dolibarr_main_data_root, "directories", 1, '^temp$', '', 'name', SORT_ASC, 2, 0, '', 1);	// Do not follow symlinks
74
				if ($choice == 'tempfilesold')
75
				{
76
					$now = dol_now();
77
					foreach($filesarray as $key => $val)
78
					{
79
						if ($val['date'] > ($now - (24 * 3600))) unset($filesarray[$key]);	// Discard files not older than 24h
80
					}
81
				}
82
			}
83
		}
84
85
		if ($choice=='allfiles')
86
		{
87
			// Delete all files (except install.lock, do not follow symbolic links)
88
			if ($dolibarr_main_data_root)
89
			{
90
				$filesarray=dol_dir_list($dolibarr_main_data_root, "all", 0, '', 'install\.lock$', 'name', SORT_ASC, 0, 0, '', 1);
91
			}
92
		}
93
94
		if ($choice=='logfile')
95
		{
96
			// Define files log
97
			if ($dolibarr_main_data_root)
98
			{
99
				$filesarray=dol_dir_list($dolibarr_main_data_root, "files", 0, '.*\.log[\.0-9]*(\.gz)?$', 'install\.lock$', 'name', SORT_ASC, 0, 0, '', 1);
100
			}
101
102
			$filelog='';
103
			if (! empty($conf->syslog->enabled))
104
			{
105
				$filelog=$conf->global->SYSLOG_FILE;
106
				$filelog=preg_replace('/DOL_DATA_ROOT/i',DOL_DATA_ROOT,$filelog);
107
108
				$alreadyincluded=false;
109
				foreach ($filesarray as $tmpcursor)
110
				{
111
					if ($tmpcursor['fullname'] == $filelog) { $alreadyincluded=true; }
112
				}
113
				if (! $alreadyincluded) $filesarray[]=array('fullname'=>$filelog,'type'=>'file');
114
			}
115
		}
116
117
		$count=0;
118
		$countdeleted=0;
119
		$counterror=0;
120
		if (count($filesarray))
121
		{
122
			foreach($filesarray as $key => $value)
123
			{
124
				//print "x ".$filesarray[$key]['fullname']."-".$filesarray[$key]['type']."<br>\n";
125
				if ($filesarray[$key]['type'] == 'dir')
126
				{
127
					$startcount=0;
128
					$tmpcountdeleted=0;
129
					$result=dol_delete_dir_recursive($filesarray[$key]['fullname'], $startcount, 1, 0, $tmpcountdeleted);
130
					$count+=$result;
131
					$countdeleted+=$tmpcountdeleted;
132
				}
133
				elseif ($filesarray[$key]['type'] == 'file')
134
				{
135
					// If (file that is not logfile) or (if mode is logfile)
136
					if ($filesarray[$key]['fullname'] != $filelog || $choice=='logfile')
0 ignored issues
show
Bug introduced by
The variable $filelog does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
137
					{
138
						$result=dol_delete_file($filesarray[$key]['fullname'], 1, 1);
139
						if ($result)
140
						{
141
							$count++;
142
							$countdeleted++;
143
						}
144
						else
145
						{
146
							$counterror++;
147
						}
148
					}
149
				}
150
			}
151
152
			// Update cachenbofdoc
153
			if (! empty($conf->ecm->enabled) && $choice=='allfiles')
154
			{
155
				require_once DOL_DOCUMENT_ROOT.'/ecm/class/ecmdirectory.class.php';
156
				$ecmdirstatic = new EcmDirectory($this->db);
157
				$result = $ecmdirstatic->refreshcachenboffile(1);
158
			}
159
		}
160
161
		if ($count > 0)
162
		{
163
			$this->output=$langs->trans("PurgeNDirectoriesDeleted", $countdeleted);
164
			if ($count > $countdeleted) $this->output.='<br>'.$langs->trans("PurgeNDirectoriesFailed", ($count - $countdeleted));
165
		}
166
		else $this->output=$langs->trans("PurgeNothingToDelete").($choice == 'tempfilesold' ? ' (older than 24h)':'');
167
168
		//return $count;
169
		return 0;     // This function can be called by cron so must return 0 if OK
170
	}
171
172
173
	/**
174
	 *  Make a backup of database
175
	 *  CAN BE A CRON TASK
176
	 *
177
	 *  @param	string		$compression	   'gz' or 'bz' or 'none'
178
	 *  @param  string      $type              'mysql', 'postgresql', ...
179
	 *  @param  int         $usedefault        1=Use default backup profile (Set this to 1 when used as cron)
180
	 *  @param  string      $file              'auto' or filename to build
181
	 *  @param  int         $keeplastnfiles    Keep only last n files (not used yet)
182
	 *  @param	int		    $execmethod		   0=Use default method (that is 1 by default), 1=Use the PHP 'exec', 2=Use the 'popen' method
183
	 *  @return	int						       0 if OK, < 0 if KO (this function is used also by cron so only 0 is OK)
184
	 */
185
	function dumpDatabase($compression='none', $type='auto', $usedefault=1, $file='auto', $keeplastnfiles=0, $execmethod=0)
186
	{
187
		global $db, $conf, $langs, $dolibarr_main_data_root;
188
		global $dolibarr_main_db_name, $dolibarr_main_db_host, $dolibarr_main_db_user, $dolibarr_main_db_port, $dolibarr_main_db_pass;
189
190
		$langs->load("admin");
191
192
		dol_syslog("Utils::dumpDatabase type=".$type." compression=".$compression." file=".$file, LOG_DEBUG);
193
		require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
194
195
		// Check compression parameter
196
		if (! in_array($compression, array('none', 'gz', 'bz', 'zip')))
197
		{
198
			$langs->load("errors");
199
			$this->error=$langs->transnoentitiesnoconv("ErrorBadValueForParameter", $compression, "Compression");
200
			return -1;
201
		}
202
203
		// Check type parameter
204
		if ($type == 'auto') $type = $db->type;
205
		if (! in_array($type, array('postgresql', 'pgsql', 'mysql', 'mysqli', 'mysqlnobin')))
206
		{
207
			$langs->load("errors");
208
			$this->error=$langs->transnoentitiesnoconv("ErrorBadValueForParameter", $type, "Basetype");
209
			return -1;
210
		}
211
212
		// Check file parameter
213
		if ($file == 'auto')
214
		{
215
			$prefix='dump';
216
			$ext='.sql';
217
			if (in_array($type, array('mysql', 'mysqli')))  { $prefix='mysqldump'; $ext='sql'; }
218
			//if ($label == 'PostgreSQL') { $prefix='pg_dump'; $ext='dump'; }
219
			if (in_array($type, array('pgsql'))) { $prefix='pg_dump'; $ext='sql'; }
220
			$file=$prefix.'_'.$dolibarr_main_db_name.'_'.dol_sanitizeFileName(DOL_VERSION).'_'.strftime("%Y%m%d%H%M").'.'.$ext;
221
		}
222
223
		$outputdir  = $conf->admin->dir_output.'/backup';
224
		$result=dol_mkdir($outputdir);
225
226
227
		// MYSQL
228
		if ($type == 'mysql' || $type == 'mysqli')
229
		{
230
			$cmddump=$conf->global->SYSTEMTOOLS_MYSQLDUMP;
231
232
233
			$outputfile = $outputdir.'/'.$file;
234
			// for compression format, we add extension
235
			$compression=$compression ? $compression : 'none';
236
			if ($compression == 'gz') $outputfile.='.gz';
237
			if ($compression == 'bz') $outputfile.='.bz2';
238
			$outputerror = $outputfile.'.err';
239
			dol_mkdir($conf->admin->dir_output.'/backup');
240
241
			// Parameteres execution
242
			$command=$cmddump;
243
			if (preg_match("/\s/",$command)) $command=escapeshellarg($command);	// Use quotes on command
244
245
			//$param=escapeshellarg($dolibarr_main_db_name)." -h ".escapeshellarg($dolibarr_main_db_host)." -u ".escapeshellarg($dolibarr_main_db_user)." -p".escapeshellarg($dolibarr_main_db_pass);
246
			$param=$dolibarr_main_db_name." -h ".$dolibarr_main_db_host;
247
			$param.=" -u ".$dolibarr_main_db_user;
248
			if (! empty($dolibarr_main_db_port)) $param.=" -P ".$dolibarr_main_db_port;
249
			if (! GETPOST("use_transaction"))    $param.=" -l --single-transaction";
250
			if (GETPOST("disable_fk") || $usedefault) $param.=" -K";
251
			if (GETPOST("sql_compat") && GETPOST("sql_compat") != 'NONE') $param.=" --compatible=".escapeshellarg(GETPOST("sql_compat","alpha"));
252
			if (GETPOST("drop_database"))        $param.=" --add-drop-database";
253
			if (GETPOST("sql_structure") || $usedefault)
254
			{
255
				if (GETPOST("drop") || $usedefault)	$param.=" --add-drop-table=TRUE";
256
				else 							    $param.=" --add-drop-table=FALSE";
257
			}
258
			else
259
			{
260
				$param.=" -t";
261
			}
262
			if (GETPOST("disable-add-locks")) $param.=" --add-locks=FALSE";
263
			if (GETPOST("sql_data") || $usedefault)
264
			{
265
				$param.=" --tables";
266
				if (GETPOST("showcolumns") || $usedefault)	 $param.=" -c";
267
				if (GETPOST("extended_ins") || $usedefault) $param.=" -e";
268
				else $param.=" --skip-extended-insert";
269
				if (GETPOST("delayed"))	 	 $param.=" --delayed-insert";
270
				if (GETPOST("sql_ignore"))	 $param.=" --insert-ignore";
271
				if (GETPOST("hexforbinary") || $usedefault) $param.=" --hex-blob";
272
			}
273
			else
274
			{
275
				$param.=" -d";    // No row information (no data)
276
			}
277
			$param.=" --default-character-set=utf8";    // We always save output into utf8 charset
278
			$paramcrypted=$param;
279
			$paramclear=$param;
280
			if (! empty($dolibarr_main_db_pass))
281
			{
282
				$paramcrypted.=' -p"'.preg_replace('/./i','*',$dolibarr_main_db_pass).'"';
283
				$paramclear.=' -p"'.str_replace(array('"','`'),array('\"','\`'),$dolibarr_main_db_pass).'"';
284
			}
285
286
			$errormsg='';
287
288
			// Debut appel methode execution
289
			$fullcommandcrypted=$command." ".$paramcrypted." 2>&1";
290
			$fullcommandclear=$command." ".$paramclear." 2>&1";
291
			if ($compression == 'none') $handle = fopen($outputfile, 'w');
292
			if ($compression == 'gz')   $handle = gzopen($outputfile, 'w');
293
			if ($compression == 'bz')   $handle = bzopen($outputfile, 'w');
294
295
			if ($handle)
296
			{
297
				$execmethod=1;
298
				if (! empty($conf->global->MAIN_EXEC_USE_POPEN)) $execmethod=$conf->global->MAIN_EXEC_USE_POPEN;
299
				if (empty($execmethod)) $execmethod=1;
300
301
				$ok=0;
302
				dol_syslog("Run command with method ".$execmethod." with ".$fullcommandcrypted);
303
304
				// TODO Replace with executeCLI function
305
				if ($execmethod == 1)
306
				{
307
					exec($fullcommandclear, $readt, $retval);
308
					$result = $retval;
309
310
					if ($retval != 0)
311
					{
312
						$langs->load("errors");
313
						dol_syslog("Datadump retval after exec=".$retval, LOG_ERR);
314
						$error = 'Error '.$retval;
315
						$ok=0;
316
					}
317
					else
318
					{
319
						$i=0;
320
						if (!empty($readt))
321
						foreach($readt as $key=>$read)
322
						{
323
							$i++;   // output line number
324
							if ($i == 1 && preg_match('/Warning.*Using a password/i', $read)) continue;
325
							fwrite($handle,$read);
0 ignored issues
show
Bug introduced by
The variable $handle does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
326
							if (preg_match('/'.preg_quote('-- Dump completed').'/i',$read)) $ok=1;
327
							elseif (preg_match('/'.preg_quote('SET SQL_NOTES=@OLD_SQL_NOTES').'/i',$read)) $ok=1;
328
						}
329
					}
330
				}
331
				if ($execmethod == 2)	// With this method, there is no way to get the return code, only output
332
				{
333
					$handlein = popen($fullcommandclear, 'r');
334
					$i=0;
335
					while (!feof($handlein))
336
					{
337
						$i++;   // output line number
338
						$read = fgets($handlein);
339
						// Exclude warning line we don't want
340
						if ($i == 1 && preg_match('/Warning.*Using a password/i', $read)) continue;
341
						fwrite($handle,$read);
342
						if (preg_match('/'.preg_quote('-- Dump completed').'/i',$read)) $ok=1;
343
						elseif (preg_match('/'.preg_quote('SET SQL_NOTES=@OLD_SQL_NOTES').'/i',$read)) $ok=1;
344
					}
345
					pclose($handlein);
346
347
				}
348
349
350
				if ($compression == 'none') fclose($handle);
351
				if ($compression == 'gz')   gzclose($handle);
352
				if ($compression == 'bz')   bzclose($handle);
353
354
				if (! empty($conf->global->MAIN_UMASK))
355
					@chmod($outputfile, octdec($conf->global->MAIN_UMASK));
356
			}
357
			else
358
			{
359
				$langs->load("errors");
360
				dol_syslog("Failed to open file ".$outputfile,LOG_ERR);
361
				$errormsg=$langs->trans("ErrorFailedToWriteInDir");
362
			}
363
364
			// Get errorstring
365
			if ($compression == 'none') $handle = fopen($outputfile, 'r');
366
			if ($compression == 'gz')   $handle = gzopen($outputfile, 'r');
367
			if ($compression == 'bz')   $handle = bzopen($outputfile, 'r');
368
			if ($handle)
369
			{
370
				// Get 2048 first chars of error message.
371
				$errormsg = fgets($handle,2048);
372
				// Close file
373
				if ($compression == 'none') fclose($handle);
374
				if ($compression == 'gz')   gzclose($handle);
375
				if ($compression == 'bz')   bzclose($handle);
376
				if ($ok && preg_match('/^-- MySql/i',$errormsg)) $errormsg='';	// Pas erreur
0 ignored issues
show
Bug introduced by
The variable $ok does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
377
				else
378
				{
379
					// Renommer fichier sortie en fichier erreur
380
					//print "$outputfile -> $outputerror";
381
					@dol_delete_file($outputerror, 1, 0, 0, null, false, 0);
382
					@rename($outputfile,$outputerror);
383
					// Si safe_mode on et command hors du parametre exec, on a un fichier out vide donc errormsg vide
384
					if (! $errormsg)
385
					{
386
						$langs->load("errors");
387
						$errormsg=$langs->trans("ErrorFailedToRunExternalCommand");
388
					}
389
				}
390
			}
391
			// Fin execution commande
392
393
			$this->output = $errormsg;
394
			$this->error = $errormsg;
395
			$this->result = array("commandbackuplastdone" => $command." ".$paramcrypted, "commandbackuptorun" => "");
396
			//if (empty($this->output)) $this->output=$this->result['commandbackuplastdone'];
397
		}
398
399
		// MYSQL NO BIN
400
		if ($type == 'mysqlnobin')
401
		{
402
			$outputfile = $outputdir.'/'.$file;
403
			$outputfiletemp = $outputfile.'-TMP.sql';
404
			// for compression format, we add extension
405
			$compression=$compression ? $compression : 'none';
406
			if ($compression == 'gz') $outputfile.='.gz';
407
			if ($compression == 'bz') $outputfile.='.bz2';
408
			$outputerror = $outputfile.'.err';
409
			dol_mkdir($conf->admin->dir_output.'/backup');
410
411
			if ($compression == 'gz' or $compression == 'bz')
412
			{
413
				$this->backup_tables($outputfiletemp);
414
				dol_compress_file($outputfiletemp, $outputfile, $compression);
415
				unlink($outputfiletemp);
416
			}
417
			else
418
			{
419
				$this->backup_tables($outputfile);
420
			}
421
422
			$this->output = "";
423
			$this->result = array("commandbackuplastdone" => "", "commandbackuptorun" => "");
424
		}
425
426
		// POSTGRESQL
427
		if ($type == 'postgresql' || $type == 'pgsql')
428
		{
429
			$cmddump=$conf->global->SYSTEMTOOLS_POSTGRESQLDUMP;
430
431
			$outputfile = $outputdir.'/'.$file;
432
			// for compression format, we add extension
433
			$compression=$compression ? $compression : 'none';
434
			if ($compression == 'gz') $outputfile.='.gz';
435
			if ($compression == 'bz') $outputfile.='.bz2';
436
			$outputerror = $outputfile.'.err';
437
			dol_mkdir($conf->admin->dir_output.'/backup');
438
439
			// Parameteres execution
440
			$command=$cmddump;
441
			if (preg_match("/\s/",$command)) $command=escapeshellarg($command);	// Use quotes on command
442
443
			//$param=escapeshellarg($dolibarr_main_db_name)." -h ".escapeshellarg($dolibarr_main_db_host)." -u ".escapeshellarg($dolibarr_main_db_user)." -p".escapeshellarg($dolibarr_main_db_pass);
444
			//$param="-F c";
445
			$param="-F p";
446
			$param.=" --no-tablespaces --inserts -h ".$dolibarr_main_db_host;
447
			$param.=" -U ".$dolibarr_main_db_user;
448
			if (! empty($dolibarr_main_db_port)) $param.=" -p ".$dolibarr_main_db_port;
449
			if (GETPOST("sql_compat") && GETPOST("sql_compat") == 'ANSI') $param.="  --disable-dollar-quoting";
450
			if (GETPOST("drop_database"))        $param.=" -c -C";
451
			if (GETPOST("sql_structure"))
452
			{
453
				if (GETPOST("drop"))			 $param.=" --add-drop-table";
454
				if (! GETPOST("sql_data"))       $param.=" -s";
455
			}
456
			if (GETPOST("sql_data"))
457
			{
458
				if (! GETPOST("sql_structure"))	 $param.=" -a";
459
				if (GETPOST("showcolumns"))	     $param.=" -c";
460
			}
461
			$param.=' -f "'.$outputfile.'"';
462
			//if ($compression == 'none')
463
			if ($compression == 'gz')   $param.=' -Z 9';
464
			//if ($compression == 'bz')
465
			$paramcrypted=$param;
466
			$paramclear=$param;
467
			/*if (! empty($dolibarr_main_db_pass))
468
			 {
469
			 $paramcrypted.=" -W".preg_replace('/./i','*',$dolibarr_main_db_pass);
470
			 $paramclear.=" -W".$dolibarr_main_db_pass;
471
			 }*/
472
			$paramcrypted.=" -w ".$dolibarr_main_db_name;
473
			$paramclear.=" -w ".$dolibarr_main_db_name;
474
475
			$this->output = "";
476
			$this->result = array("commandbackuplastdone" => "", "commandbackuptorun" => $command." ".$paramcrypted);
477
		}
478
479
		// Clean old files
480
		if ($keeplastnfiles > 0)
481
		{
482
			$tmpfiles = dol_dir_list($conf->admin->dir_output.'/backup', 'files', 0, '', '(\.err|\.old|\.sav)$', 'date', SORT_DESC);
483
			$i=0;
484
			foreach($tmpfiles as $key => $val)
485
			{
486
				$i++;
487
				if ($i <= $keeplastnfiles) continue;
488
				dol_delete_file($val['fullname'], 0, 0, 0, null, false, 0);
489
			}
490
		}
491
492
		return 0;
493
	}
494
495
496
497
	/**
498
	 * Execute a CLI command.
499
	 *
500
	 * @param 	string	$command		Command line to execute.
501
	 * @param 	string	$outputfile		Output file (used only when method is 2). For exemple $conf->admin->dir_temp.'/out.tmp';
502
	 * @param	int		$execmethod		0=Use default method (that is 1 by default), 1=Use the PHP 'exec', 2=Use the 'popen' method
503
	 * @return	array					array('result'=>...,'output'=>...,'error'=>...). result = 0 means OK.
504
	 */
505
	function executeCLI($command, $outputfile, $execmethod=0)
506
	{
507
		global $conf, $langs;
508
509
		$result = 0;
510
		$output = '';
511
		$error = '';
512
513
		$command=escapeshellcmd($command);
514
		$command.=" 2>&1";
515
516
		if (! empty($conf->global->MAIN_EXEC_USE_POPEN)) $execmethod=$conf->global->MAIN_EXEC_USE_POPEN;
517
		if (empty($execmethod)) $execmethod=1;
518
		//$execmethod=1;
519
520
		dol_syslog("Utils::executeCLI execmethod=".$execmethod." system:".$command, LOG_DEBUG);
521
		$output_arr=array();
522
523
		if ($execmethod == 1)
524
		{
525
			exec($command, $output_arr, $retval);
526
			$result = $retval;
527
			if ($retval != 0)
528
			{
529
				$langs->load("errors");
530
				dol_syslog("Utils::executeCLI retval after exec=".$retval, LOG_ERR);
531
				$error = 'Error '.$retval;
532
			}
533
		}
534
		if ($execmethod == 2)	// With this method, there is no way to get the return code, only output
535
		{
536
			$ok=0;
537
			$handle = fopen($outputfile, 'w+b');
538
			if ($handle)
539
			{
540
				dol_syslog("Utils::executeCLI run command ".$command);
541
				$handlein = popen($command, 'r');
542
				while (!feof($handlein))
543
				{
544
					$read = fgets($handlein);
545
					fwrite($handle,$read);
546
					$output_arr[]=$read;
547
				}
548
				pclose($handlein);
549
				fclose($handle);
550
			}
551
			if (! empty($conf->global->MAIN_UMASK)) @chmod($outputfile, octdec($conf->global->MAIN_UMASK));
552
		}
553
554
		// Update with result
555
		if (is_array($output_arr) && count($output_arr)>0)
556
		{
557
			foreach($output_arr as $val)
558
			{
559
				$output.=$val.($execmethod == 2 ? '' : "\n");
560
			}
561
		}
562
563
		dol_syslog("Utils::executeCLI result=".$result." output=".$output." error=".$error, LOG_DEBUG);
564
565
		return array('result'=>$result, 'output'=>$output, 'error'=>$error);
566
	}
567
568
	/**
569
	 * Generate documentation of a Module
570
	 *
571
	 * @param 	string	$module		Module name
572
	 * @return	int					<0 if KO, >0 if OK
573
	 */
574
	function generateDoc($module)
575
	{
576
		global $conf, $langs;
577
		global $dirins;
578
579
		$error = 0;
580
581
		$modulelowercase=strtolower($module);
582
583
		// Dir for module
584
		$dir = $dirins.'/'.$modulelowercase;
585
		// Zip file to build
586
		$FILENAMEDOC='';
587
588
		// Load module
589
		dol_include_once($modulelowercase.'/core/modules/mod'.$module.'.class.php');
590
		$class='mod'.$module;
591
592
		if (class_exists($class))
593
		{
594
			try {
595
				$moduleobj = new $class($this->db);
596
			}
597
			catch(Exception $e)
598
			{
599
				$error++;
600
				dol_print_error($e->getMessage());
601
			}
602
		}
603
		else
604
		{
605
			$error++;
606
			$langs->load("errors");
607
			dol_print_error($langs->trans("ErrorFailedToLoadModuleDescriptorForXXX", $module));
608
			exit;
609
		}
610
611
		$arrayversion=explode('.',$moduleobj->version,3);
612
		if (count($arrayversion))
613
		{
614
			$FILENAMEASCII=strtolower($module).'.asciidoc';
615
			$FILENAMEDOC=strtolower($module).'.html';			// TODO Use/text PDF
616
617
			$dirofmodule = dol_buildpath(strtolower($module), 0).'/doc';
618
			$dirofmoduletmp = dol_buildpath(strtolower($module), 0).'/doc/temp';
619
			$outputfiledoc = $dirofmodule.'/'.$FILENAMEDOC;
620
			if ($dirofmodule)
621
			{
622
				if (! dol_is_dir($dirofmodule)) dol_mkdir($dirofmodule);
623
				if (! dol_is_dir($dirofmoduletmp)) dol_mkdir($dirofmoduletmp);
624
				if (! is_writable($dirofmoduletmp))
625
				{
626
					$this->error = 'Dir '.$dirofmoduletmp.' does not exists or is not writable';
627
					return -1;
628
				}
629
630
				$destfile=$dirofmoduletmp.'/'.$FILENAMEASCII;
631
632
				$fhandle = fopen($destfile, 'w+');
633
				if ($fhandle)
634
				{
635
					$specs=dol_dir_list(dol_buildpath(strtolower($module).'/doc', 0), 'files', 1, '(\.md|\.asciidoc)$', array('\/temp\/'));
636
637
					$i = 0;
638
					foreach ($specs as $spec)
639
					{
640
						if (preg_match('/notindoc/', $spec['relativename'])) continue;	// Discard file
641
						if (preg_match('/disabled/', $spec['relativename'])) continue;	// Discard file
642
643
						$pathtofile = strtolower($module).'/doc/'.$spec['relativename'];
644
						$format='asciidoc';
645
						if (preg_match('/\.md$/i', $spec['name'])) $format='markdown';
646
647
						$filecursor = @file_get_contents($spec['fullname']);
648
						if ($filecursor)
649
						{
650
							fwrite($fhandle, ($i ? "\n<<<\n\n" : "").$filecursor."\n");
651
						}
652
						else
653
						{
654
							$this->error = 'Failed to concat content of file '.$spec['fullname'];
655
							return -1;
656
						}
657
658
						$i++;
659
					}
660
661
					fwrite($fhandle, "\n\n\n== DATA SPECIFICATIONS...\n\n");
662
663
					// TODO
664
					fwrite($fhandle, "TODO...");
665
666
					fclose($fhandle);
667
				}
668
669
				$conf->global->MODULEBUILDER_ASCIIDOCTOR='asciidoctor';
670
				if (empty($conf->global->MODULEBUILDER_ASCIIDOCTOR))
671
				{
672
					dol_print_error('', 'Module setup not complete');
673
					exit;
674
				}
675
676
				$command=$conf->global->MODULEBUILDER_ASCIIDOCTOR.' '.$destfile.' -n -o '.$dirofmodule.'/'.$FILENAMEDOC;
677
				$outfile=$dirofmoduletmp.'/out.tmp';
678
679
				require_once DOL_DOCUMENT_ROOT.'/core/class/utils.class.php';
680
				$utils = new Utils($db);
0 ignored issues
show
Bug introduced by
The variable $db does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
681
				$resarray = $utils->executeCLI($command, $outfile);
682
				if ($resarray['result'] != '0')
683
				{
684
					$this->error = $resarray['error'].' '.$resarray['output'];
685
				}
686
				$result = ($resarray['result'] == 0) ? 1 : 0;
687
			}
688
			else
689
			{
690
				$result = 0;
691
			}
692
693
			if ($result > 0)
694
			{
695
				return 1;
696
			}
697
			else
698
			{
699
				$error++;
700
				$langs->load("errors");
701
				$this->error = $langs->trans("ErrorFailToGenerateFile", $outputfiledoc);
702
			}
703
		}
704
		else
705
		{
706
			$error++;
707
			$langs->load("errors");
708
			$this->error = $langs->trans("ErrorCheckVersionIsDefined");
709
		}
710
711
		return -1;
712
	}
713
714
	/**
715
	 * This saves syslog files and compresses older ones.
716
	 * Nb of archive to keep is defined into $conf->global->SYSLOG_FILE_SAVES
717
	 * CAN BE A CRON TASK
718
	 *
719
	 * @return	int						0 if OK, < 0 if KO
720
	 */
721
    function compressSyslogs()
722
    {
723
		global $conf;
724
725
		if(empty($conf->loghandlers['mod_syslog_file'])) { // File Syslog disabled
726
			return 0;
727
		}
728
729
		if(! function_exists('gzopen')) {
730
			$this->error = 'Support for gzopen not available in this PHP';
731
			return -1;
732
		}
733
734
		dol_include_once('/core/lib/files.lib.php');
735
736
		$nbSaves = ! empty($conf->global->SYSLOG_FILE_SAVES) ? intval($conf->global->SYSLOG_FILE_SAVES) : 14;
737
738
		if (empty($conf->global->SYSLOG_FILE)) {
739
			$mainlogdir = DOL_DATA_ROOT;
740
			$mainlog = 'dolibarr.log';
741
		} else {
742
			$mainlogfull = str_replace('DOL_DATA_ROOT', DOL_DATA_ROOT, $conf->global->SYSLOG_FILE);
743
			$mainlogdir = dirname($mainlogfull);
744
			$mainlog = basename($mainlogfull);
745
		}
746
747
		$tabfiles = dol_dir_list(DOL_DATA_ROOT, 'files', 0, '^(dolibarr_.+|odt2pdf)\.log$'); // Also handle other log files like dolibarr_install.log
748
		$tabfiles[] = array('name' => $mainlog, 'path' => $mainlogdir);
749
750
		foreach($tabfiles as $file) {
751
752
			$logname = $file['name'];
753
			$logpath = $file['path'];
754
755
			if (dol_is_file($logpath.'/'.$logname) && dol_filesize($logpath.'/'.$logname) > 0)	// If log file exists and is not empty
756
			{
757
				// Handle already compressed files to rename them and add +1
758
759
				$filter = '^'.preg_quote($logname, '/').'\.([0-9]+)\.gz$';
760
761
				$gzfilestmp = dol_dir_list($logpath, 'files', 0, $filter);
762
				$gzfiles = array();
763
764
				foreach($gzfilestmp as $gzfile) {
765
					$tabmatches = array();
766
					preg_match('/'.$filter.'/i', $gzfile['name'], $tabmatches);
767
768
					$numsave = intval($tabmatches[1]);
769
770
					$gzfiles[$numsave] = $gzfile;
771
				}
772
773
				krsort($gzfiles, SORT_NUMERIC);
774
775
				foreach($gzfiles as $numsave => $dummy) {
776
					if (dol_is_file($logpath.'/'.$logname.'.'.($numsave+1).'.gz')) {
777
						return -2;
778
					}
779
780
					if($numsave >= $nbSaves) {
781
						dol_delete_file($logpath.'/'.$logname.'.'.$numsave.'.gz', 0, 0, 0, null, false, 0);
782
					} else {
783
						dol_move($logpath.'/'.$logname.'.'.$numsave.'.gz', $logpath.'/'.$logname.'.'.($numsave+1).'.gz', 0, 1, 0, 0);
784
					}
785
				}
786
787
				// Compress current file and recreate it
788
789
				if ($nbSaves > 0) {			// If $nbSaves is 1, we keep 1 archive .gz file, If 2, we keep 2 .gz files
790
					$gzfilehandle = gzopen($logpath.'/'.$logname.'.1.gz', 'wb9');
791
792
					if (empty($gzfilehandle)) {
793
						$this->error = 'Failted to open file '.$logpath.'/'.$logname.'.1.gz';
794
						return -3;
795
					}
796
797
					$sourcehandle = fopen($logpath.'/'.$logname, 'r');
798
799
					if (empty($sourcehandle)) {
800
						$this->error = 'Failed to open file '.$logpath.'/'.$logname;
801
						return -4;
802
					}
803
804
					while(! feof($sourcehandle)) {
805
						gzwrite($gzfilehandle, fread($sourcehandle, 512 * 1024)); // Read 512 kB at a time
806
					}
807
808
					fclose($sourcehandle);
809
					gzclose($gzfilehandle);
810
811
					@chmod($logpath.'/'.$logname.'.1.gz', octdec(empty($conf->global->MAIN_UMASK)?'0664':$conf->global->MAIN_UMASK));
812
				}
813
814
				dol_delete_file($logpath.'/'.$logname, 0, 0, 0, null, false, 0);
815
816
				// Create empty file
817
				$newlog = fopen($logpath.'/'.$logname, 'a+');
818
				fclose($newlog);
819
820
				//var_dump($logpath.'/'.$logname." - ".octdec(empty($conf->global->MAIN_UMASK)?'0664':$conf->global->MAIN_UMASK));
821
				@chmod($logpath.'/'.$logname, octdec(empty($conf->global->MAIN_UMASK)?'0664':$conf->global->MAIN_UMASK));
822
			}
823
		}
824
825
		$this->output = 'Archive log files (keeping last SYSLOG_FILE_SAVES='.$nbSaves.' files) done.';
826
		return 0;
827
	}
828
829
	/**	Backup the db OR just a table without mysqldump binary, with PHP only (does not require any exec permission)
830
	 *	Author: David Walsh (http://davidwalsh.name/backup-mysql-database-php)
831
	 *	Updated and enhanced by Stephen Larroque (lrq3000) and by the many commentators from the blog
832
	 *	Note about foreign keys constraints: for Dolibarr, since there are a lot of constraints and when imported the tables will be inserted in the dumped order, not in constraints order, then we ABSOLUTELY need to use SET FOREIGN_KEY_CHECKS=0; when importing the sql dump.
833
	 *	Note2: db2SQL by Howard Yeend can be an alternative, by using SHOW FIELDS FROM and SHOW KEYS FROM we could generate a more precise dump (eg: by getting the type of the field and then precisely outputting the right formatting - in quotes, numeric or null - instead of trying to guess like we are doing now).
834
	 *
835
	 *	@param	string	$outputfile		Output file name
836
	 *	@param	string	$tables			Table name or '*' for all
837
	 *	@return	int						<0 if KO, >0 if OK
838
	 */
839
	function backup_tables($outputfile, $tables='*')
840
	{
841
		global $db, $langs;
842
		global $errormsg;
843
844
		// Set to UTF-8
845
		if (is_a($db, 'DoliDBMysqli')) {
846
			/** @var DoliDBMysqli $db */
847
			$db->db->set_charset('utf8');
848
		} else {
849
			/** @var DoliDB $db */
850
			$db->query('SET NAMES utf8');
851
			$db->query('SET CHARACTER SET utf8');
852
		}
853
854
		//get all of the tables
855
		if ($tables == '*')
856
		{
857
			$tables = array();
858
			$result = $db->query('SHOW FULL TABLES WHERE Table_type = \'BASE TABLE\'');
859
			while($row = $db->fetch_row($result))
860
			{
861
				$tables[] = $row[0];
862
			}
863
		}
864
		else
865
		{
866
			$tables = is_array($tables) ? $tables : explode(',',$tables);
867
		}
868
869
		//cycle through
870
		$handle = fopen($outputfile, 'w+');
871
		if (fwrite($handle, '') === false)
872
		{
873
			$langs->load("errors");
874
			dol_syslog("Failed to open file ".$outputfile,LOG_ERR);
875
			$errormsg=$langs->trans("ErrorFailedToWriteInDir");
876
			return -1;
877
		}
878
879
		// Print headers and global mysql config vars
880
		$sqlhead = '';
881
		$sqlhead .= "-- ".$db::LABEL." dump via php with Dolibarr ".DOL_VERSION."
882
--
883
-- Host: ".$db->db->host_info."    Database: ".$db->database_name."
884
-- ------------------------------------------------------
885
-- Server version	".$db->db->server_info."
886
887
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
888
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
889
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
890
/*!40101 SET NAMES utf8 */;
891
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
892
/*!40103 SET TIME_ZONE='+00:00' */;
893
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
894
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
895
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
896
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
897
898
";
899
900
		if (GETPOST("nobin_disable_fk")) $sqlhead .= "SET FOREIGN_KEY_CHECKS=0;\n";
901
		//$sqlhead .= "SET SQL_MODE=\"NO_AUTO_VALUE_ON_ZERO\";\n";
902
		if (GETPOST("nobin_use_transaction")) $sqlhead .= "SET AUTOCOMMIT=0;\nSTART TRANSACTION;\n";
903
904
		fwrite($handle, $sqlhead);
905
906
		$ignore = '';
907
		if (GETPOST("nobin_sql_ignore")) $ignore = 'IGNORE ';
908
		$delayed = '';
909
		if (GETPOST("nobin_delayed")) $delayed = 'DELAYED ';
910
911
		// Process each table and print their definition + their datas
912
		foreach($tables as $table)
913
		{
914
			// Saving the table structure
915
			fwrite($handle, "\n--\n-- Table structure for table `".$table."`\n--\n");
916
917
			if (GETPOST("nobin_drop")) fwrite($handle,"DROP TABLE IF EXISTS `".$table."`;\n"); // Dropping table if exists prior to re create it
918
			fwrite($handle,"/*!40101 SET @saved_cs_client     = @@character_set_client */;\n");
919
			fwrite($handle,"/*!40101 SET character_set_client = utf8 */;\n");
920
			$resqldrop=$db->query('SHOW CREATE TABLE '.$table);
921
			$row2 = $db->fetch_row($resqldrop);
922
			if (empty($row2[1]))
923
			{
924
				fwrite($handle, "\n-- WARNING: Show create table ".$table." return empy string when it should not.\n");
925
			}
926
			else
927
			{
928
				fwrite($handle,$row2[1].";\n");
929
				//fwrite($handle,"/*!40101 SET character_set_client = @saved_cs_client */;\n\n");
930
931
				// Dumping the data (locking the table and disabling the keys check while doing the process)
932
				fwrite($handle, "\n--\n-- Dumping data for table `".$table."`\n--\n");
933
				if (!GETPOST("nobin_nolocks")) fwrite($handle, "LOCK TABLES `".$table."` WRITE;\n"); // Lock the table before inserting data (when the data will be imported back)
934
				if (GETPOST("nobin_disable_fk")) fwrite($handle, "ALTER TABLE `".$table."` DISABLE KEYS;\n");
935
				else fwrite($handle, "/*!40000 ALTER TABLE `".$table."` DISABLE KEYS */;\n");
936
937
				$sql='SELECT * FROM '.$table;
938
				$result = $db->query($sql);
939
				while($row = $db->fetch_row($result))
940
				{
941
					// For each row of data we print a line of INSERT
942
					fwrite($handle,'INSERT '.$delayed.$ignore.'INTO `'.$table.'` VALUES (');
943
					$columns = count($row);
944
					for($j=0; $j<$columns; $j++) {
945
						// Processing each columns of the row to ensure that we correctly save the value (eg: add quotes for string - in fact we add quotes for everything, it's easier)
946
						if ($row[$j] == null && !is_string($row[$j])) {
947
							// IMPORTANT: if the field is NULL we set it NULL
948
							$row[$j] = 'NULL';
949
						} elseif(is_string($row[$j]) && $row[$j] == '') {
950
							// if it's an empty string, we set it as an empty string
951
							$row[$j] = "''";
952
						} elseif(is_numeric($row[$j]) && !strcmp($row[$j], $row[$j]+0) ) { // test if it's a numeric type and the numeric version ($nb+0) == string version (eg: if we have 01, it's probably not a number but rather a string, else it would not have any leading 0)
953
							// if it's a number, we return it as-is
954
							//	                    $row[$j] = $row[$j];
955
						} else { // else for all other cases we escape the value and put quotes around
956
							$row[$j] = addslashes($row[$j]);
957
							$row[$j] = preg_replace("#\n#", "\\n", $row[$j]);
958
							$row[$j] = "'".$row[$j]."'";
959
						}
960
					}
961
					fwrite($handle,implode(',', $row).");\n");
962
				}
963
				if (GETPOST("nobin_disable_fk")) fwrite($handle, "ALTER TABLE `".$table."` ENABLE KEYS;\n"); // Enabling back the keys/index checking
964
				if (!GETPOST("nobin_nolocks")) fwrite($handle, "UNLOCK TABLES;\n"); // Unlocking the table
965
				fwrite($handle,"\n\n\n");
966
			}
967
		}
968
969
		/* Backup Procedure structure*/
970
		/*
971
		 $result = $db->query('SHOW PROCEDURE STATUS');
972
		 if ($db->num_rows($result) > 0)
973
		 {
974
		 while ($row = $db->fetch_row($result)) { $procedures[] = $row[1]; }
975
		 foreach($procedures as $proc)
976
		 {
977
		 fwrite($handle,"DELIMITER $$\n\n");
978
		 fwrite($handle,"DROP PROCEDURE IF EXISTS '$name'.'$proc'$$\n");
979
		 $resqlcreateproc=$db->query("SHOW CREATE PROCEDURE '$proc'");
980
		 $row2 = $db->fetch_row($resqlcreateproc);
981
		 fwrite($handle,"\n".$row2[2]."$$\n\n");
982
		 fwrite($handle,"DELIMITER ;\n\n");
983
		 }
984
		 }
985
		 */
986
		/* Backup Procedure structure*/
987
988
		// Write the footer (restore the previous database settings)
989
		$sqlfooter="\n\n";
990
		if (GETPOST("nobin_use_transaction")) $sqlfooter .= "COMMIT;\n";
991
		if (GETPOST("nobin_disable_fk")) $sqlfooter .= "SET FOREIGN_KEY_CHECKS=1;\n";
992
		$sqlfooter.="\n\n-- Dump completed on ".date('Y-m-d G-i-s');
993
		fwrite($handle, $sqlfooter);
994
995
		fclose($handle);
996
997
		return 1;
998
	}
999
}