migration.class.php 44.5 KB
Newer Older
yllen's avatar
yllen committed
1
<?php
2
3
4
/**
 * ---------------------------------------------------------------------
 * GLPI - Gestionnaire Libre de Parc Informatique
5
 * Copyright (C) 2015-2021 Teclib' and contributors.
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 *
 * http://glpi-project.org
 *
 * based on GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2003-2014 by the INDEPNET Development Team.
 *
 * ---------------------------------------------------------------------
 *
 * LICENSE
 *
 * This file is part of GLPI.
 *
 * GLPI is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * GLPI is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with GLPI. If not, see <http://www.gnu.org/licenses/>.
 * ---------------------------------------------------------------------
yllen's avatar
yllen committed
31
32
 */

33
34
35
use Glpi\Console\Application;
use Symfony\Component\Console\Output\OutputInterface;

yllen's avatar
yllen committed
36
if (!defined('GLPI_ROOT')) {
Stefan Weil's avatar
Stefan Weil committed
37
   die("Sorry. You can't access this file directly");
yllen's avatar
yllen committed
38
39
}

moyooo's avatar
moyooo committed
40
41
42
/**
 * Migration Class
 *
43
 * @since 0.80
moyooo's avatar
moyooo committed
44
**/
yllen's avatar
yllen committed
45
46
class Migration {

47
48
   private   $change    = [];
   private   $fulltexts = [];
49
   private   $uniques   = [];
50
   private   $search_opts = [];
51
52
   protected $version;
   private   $deb;
webmyster's avatar
webmyster committed
53
   private   $lastMessage;
54
55
   private   $log_errors = 0;
   private   $current_message_area_id;
56
57
58
59
   private   $queries = [
      'pre'    => [],
      'post'   => []
   ];
webmyster's avatar
webmyster committed
60

61
62
63
64
65
66
67
68
69
70
71
72
   /**
    * List (name => value) of configuration options to add, if they're missing
    * @var array
    */
   private $configs = [];

   /**
    * Configuration context
    * @var string
    */
   private $context = 'core';

73
74
   const PRE_QUERY = 'pre';
   const POST_QUERY = 'post';
yllen's avatar
yllen committed
75

76
77
78
79
80
81
82
83
   /**
    * Output handler to use. If not set, output will be directly echoed on a format depending on
    * execution context (Web VS CLI).
    *
    * @var OutputInterface|null
    */
   protected $output_handler;

yllen's avatar
yllen committed
84
   /**
Johan Cwiklinski's avatar
Johan Cwiklinski committed
85
    * @param integer $ver Version number
yllen's avatar
yllen committed
86
   **/
yllen's avatar
yllen committed
87
   function __construct($ver) {
88

webmyster's avatar
webmyster committed
89
      $this->deb = time();
90
      $this->version = $ver;
91
92
93
94
95
96

      global $application;
      if ($application instanceof Application) {
         // $application global variable will be available if Migration is called from a CLI console command
         $this->output_handler = $application->getOutput();
      }
moyooo's avatar
moyooo committed
97
98
   }

yllen's avatar
CS    
yllen committed
99
   /**
Johan Cwiklinski's avatar
Johan Cwiklinski committed
100
    * Set version
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
101
    *
102
    * @since 0.84
yllen's avatar
CS    
yllen committed
103
    *
Johan Cwiklinski's avatar
Johan Cwiklinski committed
104
    * @param integer $ver Version number
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
105
106
    *
    * @return void
yllen's avatar
CS    
yllen committed
107
   **/
moyooo's avatar
moyooo committed
108
   function setVersion($ver) {
yllen's avatar
CS    
yllen committed
109

yllen's avatar
yllen committed
110
      $this->version = $ver;
111
      $this->addNewMessageArea("migration_message_$ver");
yllen's avatar
yllen committed
112
113
   }

yllen's avatar
yllen committed
114
115

   /**
Johan Cwiklinski's avatar
Johan Cwiklinski committed
116
    * Add new message
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
117
    *
118
    * @since 0.84
yllen's avatar
yllen committed
119
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
120
121
122
    * @param string $id Area ID
    *
    * @return void
yllen's avatar
yllen committed
123
   **/
124
   function addNewMessageArea($id) {
yllen's avatar
yllen committed
125

126
      if (!isCommandLine() && $id != $this->current_message_area_id) {
127
         $this->current_message_area_id = $id;
128
         echo "<div id='".$this->current_message_area_id."'></div>";
129
      }
130
131

      $this->displayMessage(__('Work in progress...'));
webmyster's avatar
webmyster committed
132
133
134
135
   }


   /**
Johan Cwiklinski's avatar
Johan Cwiklinski committed
136
    * Flush previous displayed message in log file
webmyster's avatar
webmyster committed
137
    *
138
    * @since 0.84
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
139
140
    *
    * @return void
webmyster's avatar
webmyster committed
141
142
143
144
145
146
147
148
   **/
   function flushLogDisplayMessage() {

      if (isset($this->lastMessage)) {
         $tps = Html::timestampToString(time() - $this->lastMessage['time']);
         $this->log($tps . ' for "' . $this->lastMessage['msg'] . '"', false);
         unset($this->lastMessage);
      }
149
   }
webmyster's avatar
webmyster committed
150
151


152
153
154
   /**
    * Additional message in global message
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
155
156
157
    * @param string $msg text  to display
    *
    * @return void
158
   **/
yllen's avatar
yllen committed
159
   function displayMessage($msg) {
160

Cédric Anne's avatar
Cédric Anne committed
161
162
      $this->flushLogDisplayMessage();

webmyster's avatar
webmyster committed
163
164
      $now = time();
      $tps = Html::timestampToString($now-$this->deb);
165
166

      $this->outputMessage("{$msg} ({$tps})", null, $this->current_message_area_id);
webmyster's avatar
webmyster committed
167

168
      $this->lastMessage = ['time' => time(),
169
                            'msg'  => $msg];
170
171
   }

yllen's avatar
yllen committed
172

173
   /**
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
174
    * Log message for this migration
yllen's avatar
yllen committed
175
    *
176
    * @since 0.84
yllen's avatar
yllen committed
177
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
178
179
180
181
    * @param string  $message Message to display
    * @param boolean $warning Is a warning
    *
    * @return void
182
   **/
webmyster's avatar
webmyster committed
183
184
185
186
187
188
189
   function log($message, $warning) {

      if ($warning) {
         $log_file_name = 'warning_during_migration_to_'.$this->version;
      } else {
         $log_file_name = 'migration_to_'.$this->version;
      }
190

191
192
      // Do not log if more than 3 log error
      if ($this->log_errors < 3
webmyster's avatar
webmyster committed
193
         && !Toolbox::logInFile($log_file_name, $message . ' @ ', true)) {
194
         $this->log_errors++;
195
      }
196
   }
webmyster's avatar
webmyster committed
197

yllen's avatar
yllen committed
198

199
200
201
   /**
    * Display a title
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
202
203
204
    * @param string $title Title to display
    *
    * @return void
yllen's avatar
yllen committed
205
   **/
206
   function displayTitle($title) {
Cédric Anne's avatar
Cédric Anne committed
207
208
      $this->flushLogDisplayMessage();

209
      $this->outputMessage($title, 'title');
210
211
   }

yllen's avatar
yllen committed
212

213
214
215
   /**
    * Display a Warning
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
216
217
218
219
    * @param string  $msg Message to display
    * @param boolean $red Displays with red class (false by default)
    *
    * @return void
yllen's avatar
yllen committed
220
   **/
221
   function displayWarning($msg, $red = false) {
222
      $this->outputMessage($msg, $red ? 'warning' : 'strong');
webmyster's avatar
webmyster committed
223
      $this->log($msg, true);
224
225
   }

yllen's avatar
yllen committed
226

yllen's avatar
yllen committed
227
   /**
228
    * Define field's format
yllen's avatar
yllen committed
229
    *
Johan Cwiklinski's avatar
Johan Cwiklinski committed
230
    * @param string  $type          can be bool, char, string, integer, date, datetime, text, longtext or autoincrement
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
231
232
233
234
235
    * @param string  $default_value new field's default value,
    *                               if a specific default value needs to be used
    * @param boolean $nodefault     No default value (false by default)
    *
    * @return string
yllen's avatar
yllen committed
236
   **/
237
   private function fieldFormat($type, $default_value, $nodefault = false) {
yllen's avatar
yllen committed
238

yllen's avatar
yllen committed
239
      $format = '';
Cédric Anne's avatar
Cédric Anne committed
240
      $collate = DBConnection::getDefaultCollation();
241
      switch ($type) {
yllen's avatar
yllen committed
242
         case 'bool' :
243
         case 'boolean' :
244
            $format = "TINYINT NOT NULL";
245
246
247
            if (!$nodefault) {
               if (is_null($default_value)) {
                  $format .= " DEFAULT '0'";
248
               } else if (in_array($default_value, ['0', '1'])) {
249
250
251
252
                  $format .= " DEFAULT '$default_value'";
               } else {
                  trigger_error(__('default_value must be 0 or 1'), E_USER_ERROR);
               }
253
254
            }
            break;
yllen's avatar
yllen committed
255

256
         case 'char' :
257
         case 'character' :
258
            $format = "CHAR(1)";
259
260
261
262
            if (!$nodefault) {
               if (is_null($default_value)) {
                  $format .= " DEFAULT NULL";
               } else {
263
                  $format .= " NOT NULL DEFAULT '$default_value'";
264
               }
265
266
267
            }
            break;

268
         case 'str' :
yllen's avatar
yllen committed
269
         case 'string' :
Cédric Anne's avatar
Cédric Anne committed
270
            $format = "VARCHAR(255) COLLATE $collate";
271
272
273
274
            if (!$nodefault) {
               if (is_null($default_value)) {
                  $format .= " DEFAULT NULL";
               } else {
275
                  $format .= " NOT NULL DEFAULT '$default_value'";
276
               }
277
278
            }
            break;
yllen's avatar
yllen committed
279

280
         case 'int' :
yllen's avatar
yllen committed
281
         case 'integer' :
282
            $format = "INT NOT NULL";
283
284
285
286
287
288
289
290
            if (!$nodefault) {
               if (is_null($default_value)) {
                  $format .= " DEFAULT '0'";
               } else if (is_numeric($default_value)) {
                  $format .= " DEFAULT '$default_value'";
               } else {
                  trigger_error(__('default_value must be numeric'), E_USER_ERROR);
               }
291
292
            }
            break;
yllen's avatar
yllen committed
293

294
         case 'date':
yllen's avatar
yllen committed
295
            $format = "DATE";
296
297
298
299
300
301
            if (!$nodefault) {
               if (is_null($default_value)) {
                  $format.= " DEFAULT NULL";
               } else {
                  $format.= " DEFAULT '$default_value'";
               }
yllen's avatar
yllen committed
302
            }
303
            break;
yllen's avatar
yllen committed
304

305
306
307
         case 'timestamp':
         case 'datetime':
            $format = "TIMESTAMP";
308
309
            if (!$nodefault) {
               if (is_null($default_value)) {
310
                  $format.= " NULL DEFAULT NULL";
311
312
313
               } else {
                  $format.= " DEFAULT '$default_value'";
               }
yllen's avatar
yllen committed
314
            }
315
            break;
yllen's avatar
yllen committed
316
317

         case 'text' :
318
         case 'mediumtext' :
yllen's avatar
yllen committed
319
         case 'longtext' :
320
            $format = sprintf('%s COLLATE %s', strtoupper($type), $collate);
321
322
323
324
            if (!$nodefault) {
               if (is_null($default_value)) {
                  $format .= " DEFAULT NULL";
               } else {
325
326
327
328
329
                  if (empty($default_value)) {
                     $format .= " NOT NULL";
                  } else {
                     $format .= " NOT NULL DEFAULT '$default_value'";
                  }
330
               }
yllen's avatar
yllen committed
331
            }
332
            break;
yllen's avatar
yllen committed
333
334
335

         // for plugins
         case 'autoincrement' :
336
            $format = "INT NOT NULL AUTO_INCREMENT";
337
            break;
yllen's avatar
yllen committed
338

339
340
341
         default :
            // for compatibility with old 0.80 migrations
            $format = $type;
342
343
            break;
      }
344
345
346
347
348
349
350
      return $format;
   }


   /**
    * Add a new GLPI normalized field
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
351
352
353
354
355
356
357
358
359
    * @param string $table   Table name
    * @param string $field   Field name
    * @param string $type    Field type, @see Migration::fieldFormat()
    * @param array  $options Options:
    *                         - update    : if not empty = value of $field (must be protected)
    *                         - condition : if needed
    *                         - value     : default_value new field's default value, if a specific default value needs to be used
    *                         - nodefault : do not define default value (default false)
    *                         - comment   : comment to be added during field creation
360
    *                         - first     : add the new field at first column
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
361
362
363
364
    *                         - after     : where adding the new field
    *                         - null      : value could be NULL (default false)
    *
    * @return boolean
365
   **/
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
366
   function addField($table, $field, $type, $options = []) {
yllen's avatar
yllen committed
367
      global $DB;
368
369
370

      $params['update']    = '';
      $params['condition'] = '';
371
      $params['value']     = null;
372
      $params['nodefault'] = false;
373
374
      $params['comment']   = '';
      $params['after']     = '';
btry's avatar
btry committed
375
      $params['first']     = '';
376
      $params['null']      = false;
377
378
379
380
381
382
383

      if (is_array($options) && count($options)) {
         foreach ($options as $key => $val) {
            $params[$key] = $val;
         }
      }

384
      $format = $this->fieldFormat($type, $params['value'], $params['nodefault']);
yllen's avatar
yllen committed
385

386
      if (!empty($params['comment'])) {
yllen's avatar
yllen committed
387
         $params['comment'] = " COMMENT '".addslashes($params['comment'])."'";
yllen's avatar
yllen committed
388
389
      }

390
      if (!empty($params['after'])) {
yllen's avatar
yllen committed
391
         $params['after'] = " AFTER `".$params['after']."`";
392
      } else if (!empty($params['first'])) {
btry's avatar
btry committed
393
         $params['first'] = " FIRST ";
394
395
      }

396
397
398
399
      if ($params['null']) {
         $params['null'] = 'NULL ';
      }

400
      if ($format) {
401
         if (!$DB->fieldExists($table, $field, false)) {
yllen's avatar
yllen committed
402
            $this->change[$table][] = "ADD `$field` $format ".$params['comment'] ." ".
403
                                      $params['null'].$params['first'].$params['after'];
yllen's avatar
yllen committed
404

405
            if ($params['update'] !== '') {
yllen's avatar
yllen committed
406
407
               $this->migrationOneTable($table);
               $query = "UPDATE `$table`
408
409
                        SET `$field` = ".$params['update']." ".
                        $params['condition']."";
410
               $DB->queryOrDie($query, $this->version." set $field in $table");
yllen's avatar
yllen committed
411
412
413
414
            }
            return true;
         }
         return false;
415
416
      }
   }
yllen's avatar
yllen committed
417
418


yllen's avatar
yllen committed
419
420
421
   /**
    * Modify field for migration
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
422
423
424
425
426
    * @param string $table    Table name
    * @param string $oldfield Old name of the field
    * @param string $newfield New name of the field
    * @param string $type     Field type, @see Migration::fieldFormat()
    * @param array  $options  Options:
427
    *                         - value     : new field's default value, if a specific default value needs to be used
428
429
430
    *                         - first     : add the new field at first column
    *                         - after     : where adding the new field
    *                         - null      : value could be NULL (default false)
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
431
432
433
434
    *                         - comment comment to be added during field creation
    *                         - nodefault : do not define default value (default false)
    *
    * @return boolean
yllen's avatar
yllen committed
435
   **/
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
436
   function changeField($table, $oldfield, $newfield, $type, $options = []) {
437
      global $DB;
438

439
      $params['value']     = null;
440
      $params['nodefault'] = false;
441
      $params['comment']   = '';
442
443
444
      $params['after']     = '';
      $params['first']     = '';
      $params['null']      = false;
445
446
447
448
449
450
451

      if (is_array($options) && count($options)) {
         foreach ($options as $key => $val) {
            $params[$key] = $val;
         }
      }

452
      $format = $this->fieldFormat($type, $params['value'], $params['nodefault']);
453
454

      if ($params['comment']) {
yllen's avatar
yllen committed
455
         $params['comment'] = " COMMENT '".addslashes($params['comment'])."'";
456
457
      }

458
459
460
461
462
463
464
465
466
467
      if (!empty($params['after'])) {
         $params['after'] = " AFTER `".$params['after']."`";
      } else if (!empty($params['first'])) {
         $params['first'] = " FIRST ";
      }

      if ($params['null']) {
         $params['null'] = 'NULL ';
      }

468
      if ($DB->fieldExists($table, $oldfield, false)) {
469
470
         // in order the function to be replayed
         // Drop new field if name changed
yllen's avatar
yllen committed
471
         if (($oldfield != $newfield)
472
             && $DB->fieldExists($table, $newfield)) {
473
            $this->change[$table][] = "DROP `$newfield` ";
474
         }
475

476
         if ($format) {
477
478
            $this->change[$table][] = "CHANGE `$oldfield` `$newfield` $format ".$params['comment']." ".
                                      $params['null'].$params['first'].$params['after'];
479
         }
yllen's avatar
yllen committed
480
481
         return true;
      }
482

yllen's avatar
yllen committed
483
484
485
486
487
488
489
      return false;
   }


   /**
    * Drop field for migration
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
490
491
492
493
    * @param string $table Table name
    * @param string $field Field name
    *
    * @return void
yllen's avatar
yllen committed
494
495
   **/
   function dropField($table, $field) {
496
      global $DB;
yllen's avatar
yllen committed
497

498
      if ($DB->fieldExists($table, $field, false)) {
yllen's avatar
yllen committed
499
500
501
502
         $this->change[$table][] = "DROP `$field`";
      }
   }

503

504
505
   /**
    * Drop immediatly a table if it exists
yllen's avatar
yllen committed
506
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
507
508
509
    * @param string $table Table name
    *
    * @return void
510
   **/
511
512
   function dropTable($table) {
      global $DB;
513

514
      if ($DB->tableExists($table)) {
515
516
517
         $DB->query("DROP TABLE `$table`");
      }
   }
yllen's avatar
yllen committed
518

519

yllen's avatar
yllen committed
520
521
522
   /**
    * Add index for migration
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
523
524
525
526
527
528
529
    * @param string       $table     Table name
    * @param string|array $fields    Field(s) name(s)
    * @param string       $indexname Index name, $fields if empty, defaults to empty
    * @param string       $type      Index type (index or unique - default 'INDEX')
    * @param integer      $len       Field length (default 0)
    *
    * @return void
yllen's avatar
yllen committed
530
   **/
531
   function addKey($table, $fields, $indexname = '', $type = 'INDEX', $len = 0) {
yllen's avatar
yllen committed
532
533
534
535

      // si pas de nom d'index, on prend celui du ou des champs
      if (!$indexname) {
         if (is_array($fields)) {
536
            $indexname = implode("_", $fields);
yllen's avatar
yllen committed
537
538
539
540
541
         } else {
            $indexname = $fields;
         }
      }

542
      if (!isIndex($table, $indexname)) {
yllen's avatar
yllen committed
543
         if (is_array($fields)) {
remi's avatar
remi committed
544
            if ($len) {
545
               $fields = "`".implode("`($len), `", $fields)."`($len)";
remi's avatar
remi committed
546
            } else {
547
               $fields = "`".implode("`, `", $fields)."`";
remi's avatar
remi committed
548
549
550
551
552
            }
         } else if ($len) {
            $fields = "`$fields`($len)";
         } else {
            $fields = "`$fields`";
yllen's avatar
yllen committed
553
554
         }

555
556
         if ($type == 'FULLTEXT') {
            $this->fulltexts[$table][] = "ADD $type `$indexname` ($fields)";
557
558
         } else if ($type == 'UNIQUE') {
            $this->uniques[$table][] = "ADD $type `$indexname` ($fields)";
559
560
561
         } else {
            $this->change[$table][] = "ADD $type `$indexname` ($fields)";
         }
yllen's avatar
yllen committed
562
563
564
565
566
567
568
      }
   }


   /**
    * Drop index for migration
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
569
570
571
572
    * @param string $table     Table name
    * @param string $indexname Index name
    *
    * @return void
yllen's avatar
yllen committed
573
574
575
   **/
   function dropKey($table, $indexname) {

576
      if (isIndex($table, $indexname)) {
yllen's avatar
yllen committed
577
578
579
580
581
         $this->change[$table][] = "DROP INDEX `$indexname`";
      }
   }


yllen's avatar
yllen committed
582
583
584
   /**
    * Rename table for migration
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
585
586
587
588
    * @param string $oldtable Old table name
    * @param string $newtable new table name
    *
    * @return void
yllen's avatar
yllen committed
589
590
   **/
   function renameTable($oldtable, $newtable) {
yllen's avatar
yllen committed
591
      global $DB;
yllen's avatar
yllen committed
592

593
      if (!$DB->tableExists("$newtable") && $DB->tableExists("$oldtable")) {
yllen's avatar
yllen committed
594
         $query = "RENAME TABLE `$oldtable` TO `$newtable`";
595
         $DB->queryOrDie($query, $this->version." rename $oldtable");
596
597
598
599
600
601
602

         // Clear possibly forced value of table name.
         // Actually the only forced value in core is for config table.
         $itemtype = getItemTypeForTable($newtable);
         if (class_exists($itemtype)) {
            $itemtype::forceTable($newtable);
         }
603
604
605
606
607
608
609
610
611
612
613
614
615
616

         // Update target of "buffered" schema updates
         if (isset($this->change[$oldtable])) {
            $this->change[$newtable] = $this->change[$oldtable];
            unset($this->change[$oldtable]);
         }
         if (isset($this->fulltexts[$oldtable])) {
            $this->fulltexts[$newtable] = $this->fulltexts[$oldtable];
            unset($this->fulltexts[$oldtable]);
         }
         if (isset($this->uniques[$oldtable])) {
            $this->uniques[$newtable] = $this->uniques[$oldtable];
            unset($this->uniques[$oldtable]);
         }
Johan Cwiklinski's avatar
Johan Cwiklinski committed
617
      } else {
618
619
620
621
622
         if (Toolbox::startsWith($oldtable, 'glpi_plugin_')
            || Toolbox::startsWith($newtable, 'glpi_plugin_')
         ) {
            return;
         }
Johan Cwiklinski's avatar
Johan Cwiklinski committed
623
624
625
626
627
628
629
630
631
         $message = sprintf(
            __('Unable to rename table %1$s (%2$s) to %3$s (%4$s)!'),
            $oldtable,
            ($DB->tableExists($oldtable) ? __('ok') : __('nok')),
            $newtable,
            ($DB->tableExists($newtable) ? __('nok') : __('ok'))
         );
         Toolbox::logError($message);
         die(1);
yllen's avatar
yllen committed
632
633
634
      }
   }

yllen's avatar
yllen committed
635

moyooo's avatar
moyooo committed
636
637
638
   /**
    * Copy table for migration
    *
639
    * @since 0.84
yllen's avatar
yllen committed
640
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
641
642
    * @param string $oldtable The name of the table already inside the database
    * @param string $newtable The copy of the old table
643
    * @param bool   $insert   Copy content ? True by default
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
644
645
    *
    * @return void
moyooo's avatar
moyooo committed
646
   **/
647
   function copyTable($oldtable, $newtable, bool $insert = true) {
yllen's avatar
yllen committed
648
      global $DB;
moyooo's avatar
moyooo committed
649

650
651
      if (!$DB->tableExists($newtable)
          && $DB->tableExists($oldtable)) {
yllen's avatar
yllen committed
652

653
654
655
         // Try to do a flush tables if RELOAD privileges available
         // $query = "FLUSH TABLES `$oldtable`, `$newtable`";
         // $DB->query($query);
yllen's avatar
yllen committed
656

moyooo's avatar
moyooo committed
657
         $query = "CREATE TABLE `$newtable` LIKE `$oldtable`";
658
         $DB->queryOrDie($query, $this->version." create $newtable");
yllen's avatar
yllen committed
659

660
661
662
663
664
         if ($insert) {
            //needs DB::insert to support subqueries to get migrated
            $query = "INSERT INTO `$newtable` (SELECT * FROM `$oldtable`)";
            $DB->queryOrDie($query, $this->version." copy from $oldtable to $newtable");
         }
moyooo's avatar
moyooo committed
665
666
      }
   }
yllen's avatar
yllen committed
667

yllen's avatar
yllen committed
668

669
670
671
   /**
    * Insert an entry inside a table
    *
672
    * @since 0.84
yllen's avatar
yllen committed
673
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
674
675
    * @param string $table The table to alter
    * @param array  $input The elements to add inside the table
yllen's avatar
yllen committed
676
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
677
    * @return integer id of the last item inserted by mysql
678
   **/
yllen's avatar
yllen committed
679
   function insertInTable($table, array $input) {
yllen's avatar
yllen committed
680
      global $DB;
681

682
      if ($DB->tableExists("$table")
yllen's avatar
yllen committed
683
684
          && is_array($input) && (count($input) > 0)) {

685
         $values = [];
686
         foreach ($input as $field => $value) {
687
            if ($DB->fieldExists($table, $field)) {
688
               $values[$field] = $value;
689
            }
690
         }
691
692

         $DB->insertOrDie($table, $values, $this->version." insert in $table");
693

Johan Cwiklinski's avatar
Johan Cwiklinski committed
694
         return $DB->insertId();
695
696
697
      }
   }

yllen's avatar
yllen committed
698

yllen's avatar
yllen committed
699
700
701
   /**
    * Execute migration for only one table
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
702
703
704
    * @param string $table Table name
    *
    * @return void
yllen's avatar
yllen committed
705
706
   **/
   function migrationOneTable($table) {
yllen's avatar
yllen committed
707
      global $DB;
yllen's avatar
yllen committed
708
709

      if (isset($this->change[$table])) {
710
         $query = "ALTER TABLE `$table` ".implode(" ,\n", $this->change[$table])." ";
711
         $this->displayMessage( sprintf(__('Change of the database layout - %s'), $table));
712
         $DB->queryOrDie($query, $this->version." multiple alter in $table");
yllen's avatar
yllen committed
713
714
         unset($this->change[$table]);
      }
715
716

      if (isset($this->fulltexts[$table])) {
Johan Cwiklinski's avatar
Wording    
Johan Cwiklinski committed
717
         $this->displayMessage( sprintf(__('Adding fulltext indices - %s'), $table));
718
719
720
721
722
723
724
         foreach ($this->fulltexts[$table] as $idx) {
            $query = "ALTER TABLE `$table` ".$idx;
            $DB->queryOrDie($query, $this->version." $idx");
         }
         unset($this->fulltexts[$table]);
      }

725
      if (isset($this->uniques[$table])) {
Johan Cwiklinski's avatar
Wording    
Johan Cwiklinski committed
726
         $this->displayMessage( sprintf(__('Adding unicity indices - %s'), $table));
727
728
729
730
731
732
         foreach ($this->uniques[$table] as $idx) {
            $query = "ALTER TABLE `$table` ".$idx;
            $DB->queryOrDie($query, $this->version." $idx");
         }
         unset($this->uniques[$table]);
      }
yllen's avatar
yllen committed
733
734
735
736
737
   }


   /**
    * Execute global migration
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
738
739
    *
    * @return void
yllen's avatar
yllen committed
740
741
   **/
   function executeMigration() {
742
      global $DB;
743
744
745
746

      foreach ($this->queries[self::PRE_QUERY] as $query) {
         $DB->queryOrDie($query['query'], $query['message']);
      }
747
      $this->queries[self::PRE_QUERY] = [];
yllen's avatar
yllen committed
748

749
750
      $tables = array_merge(
         array_keys($this->change),
751
752
         array_keys($this->fulltexts),
         array_keys($this->uniques)
753
754
      );
      foreach ($tables as $table) {
yllen's avatar
yllen committed
755
         $this->migrationOneTable($table);
756
757
      }

758
759
760
      foreach ($this->queries[self::POST_QUERY] as $query) {
         $DB->queryOrDie($query['query'], $query['message']);
      }
761
      $this->queries[self::POST_QUERY] = [];
762

763
      $this->storeConfig();
764
      $this->migrateSearchOptions();
765

766
      // end of global message
moyooo's avatar
moyooo committed
767
      $this->displayMessage(__('Task completed.'));
yllen's avatar
yllen committed
768
769
   }

yllen's avatar
yllen committed
770

771
772
773
   /**
    * Register a new rule
    *
774
775
    * @since 0.84
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
776
777
778
    * @param array $rule     Array of fields of glpi_rules
    * @param array $criteria Array of Array of fields of glpi_rulecriterias
    * @param array $actions  Array of Array of fields of glpi_ruleactions
779
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
780
    * @return integer new rule id
781
782
783
784
785
   **/
   function createRule(Array $rule, Array $criteria, Array $actions) {
      global $DB;

      // Avoid duplicate - Need to be improved using a rule uuid of other
786
      if (countElementsInTable('glpi_rules', ['name' => $DB->escape($rule['name'])])) {
787
788
         return 0;
      }
789
790
      $rule['comment']     = sprintf(__('Automatically generated by GLPI %s'), $this->version);
      $rule['description'] = '';
yllen's avatar
yllen committed
791

792
      // Compute ranking
793
      $sql = "SELECT MAX(`ranking`) AS `rank`
794
795
796
797
798
799
              FROM `glpi_rules`
              WHERE `sub_type` = '".$rule['sub_type']."'";
      $result = $DB->query($sql);

      $ranking = 1;
      if ($DB->numrows($result) > 0) {
Johan Cwiklinski's avatar
Johan Cwiklinski committed
800
         $datas = $DB->fetchAssoc($result);
801
802
803
804
         $ranking = $datas["rank"] + 1;
      }

      // The rule itself
805
      $values = ['ranking' => $ranking];
806
      foreach ($rule as $field => $value) {
807
         $values[$field] = $value;
808
      }
809
      $DB->insertOrDie('glpi_rules', $values);
Johan Cwiklinski's avatar
Johan Cwiklinski committed
810
      $rid = $DB->insertId();
811
812
813

      // The rule criteria
      foreach ($criteria as $criterion) {
814
         $values = ['rules_id' => $rid];
815
         foreach ($criterion as $field => $value) {
816
            $values[$field] = $value;
817
         }
818
         $DB->insertOrDie('glpi_rulecriterias', $values);
819
820
821
822
      }

      // The rule criteria actions
      foreach ($actions as $action) {
823
         $values = ['rules_id' => $rid];
824
         foreach ($action as $field => $value) {
825
            $values[$field] = $value;
826
         }
827
         $DB->insertOrDie('glpi_ruleactions', $values);
828
829
      }
   }
moyooo's avatar
moyooo committed
830
831
832
833
834


   /**
    * Update display preferences
    *
835
    * @since 0.85
moyooo's avatar
moyooo committed
836
    *
Johan Cwiklinski's avatar
PHPDoc    
Johan Cwiklinski committed
837
838
839
840
    * @param array $toadd items to add : itemtype => array of values
    * @param array $todel items to del : itemtype => array of values
    *
    * @return void
moyooo's avatar
moyooo committed
841
   **/
842
   function updateDisplayPrefs($toadd = [], $todel = []) {
moyooo's avatar
moyooo committed
843
844
845
846
847
848
      global $DB;

      //TRANS: %s is the table or item to migrate
      $this->displayMessage(sprintf(__('Data migration - %s'), 'glpi_displaypreferences'));
      if (count($toadd)) {
         foreach ($toadd as $type => $tab) {
849
            $iterator = $DB->request([
850
851
               'SELECT'          => 'users_id',
               'DISTINCT'        => true,
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
               'FROM'            => 'glpi_displaypreferences',
               'WHERE'           => ['itemtype' => $type]
            ]);

            if (count($iterator) > 0) {
               while ($data = $iterator->next()) {
                  $query = "SELECT MAX(`rank`)
                              FROM `glpi_displaypreferences`
                              WHERE `users_id` = '".$data['users_id']."'
                                    AND `itemtype` = '$type'";
                  $result = $DB->query($query);
                  $rank   = $DB->result($result, 0, 0);
                  $rank++;

                  foreach ($tab as $newval) {
                     $query = "SELECT *
                                 FROM `glpi_displaypreferences`
                                 WHERE `users_id` = '".$data['users_id']."'
                                       AND `num` = '$newval'
                                       AND `itemtype` = '$type'";
                     if ($result2 = $DB->query($query)) {
                        if ($DB->numrows($result2) == 0) {
874
875
876
877
878
879
880
881
                              $DB->insert(
                                 'glpi_displaypreferences', [
                                    'itemtype'  => $type,
                                    'num'       => $newval,
                                    'rank'      => $rank++,
                                    'users_id'  => $data['users_id']
                                 ]
                              );
moyooo's avatar
moyooo committed
882
883
884
                        }
                     }
                  }
885
               }
moyooo's avatar
moyooo committed
886

887
888
889
            } else { // Add for default user
               $rank = 1;
               foreach ($tab as $newval) {
890
891
892
893
894
895
896
897
                     $DB->insert(
                        'glpi_displaypreferences', [
                           'itemtype'  => $type,
                           'num'       => $newval,
                           'rank'      => $rank++,
                           'users_id'  => 0
                        ]
                     );
moyooo's avatar
moyooo committed
898
899
900
901
902
903
904
905
906
               }
            }
         }
      }

      if (count($todel)) {
         // delete display preferences
         foreach ($todel as $type => $tab) {
            if (count($tab)) {
907
908
909
910
911
912
               $DB->delete(
                  'glpi_displaypreferences', [
                     'itemtype'  => $type,
                     'num'       => $tab
                  ]
               );
moyooo's avatar
moyooo committed
913
914
915
916
917
            }
         }
      }
   }

918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
   /**
    * Add a migration SQL query
    *
    * @param string $type    Either self::PRE_QUERY or self::POST_QUERY
    * @param string $query   Query to execute
    * @param string $message Mesage to display on error, defaults to null
    *
    * @return Migration
    */
   private function addQuery($type, $query, $message = null) {
      $this->queries[$type][] =  [
         'query'     => $query,
         'message'   => $message
      ];
      return $this;
   }

   /**
    * Add a pre migration SQL query
    *
    * @param string $query   Query to execute
    * @param string $message Mesage to display on error, defaults to null
    *
    * @return Migration
    */
   public function addPreQuery($query, $message = null) {
      return $this->addQuery(self::PRE_QUERY, $query, $message);
   }

   /**
    * Add a post migration SQL query
    *
    * @param string $query   Query to execute
    * @param string $message Mesage to display on error, defaults to null
    *
    * @return Migration
    */
   public function addPostQuery($query, $message = null) {
      return $this->addQuery(self::POST_QUERY, $query, $message);
   }
958
959
960
961
962
963
964
965
966

   /**
    * Backup existing tables
    *
    * @param array $tables Existing tables to backup
    *
    * @return boolean
    */
   public function backupTables($tables) {
967
      global $DB;
yllen's avatar
yllen committed
968

969
970
971
      $backup_tables = false;
      foreach ($tables as $table) {
         // rename new tables if exists ?
972
         if ($DB->tableExists($table)) {
973
            $this->dropTable("backup_$table");
yllen's avatar
yllen committed
974
975
            $this->displayWarning(sprintf(__('%1$s table already exists. A backup have been done to %2$s'),
                                          $table, "backup_$table"));
976
977
978
979
            $backup_tables = true;
            $this->renameTable("$table", "backup_$table");
         }
      }
yllen's avatar
yllen committed
980
981
982
      if ($backup_tables) {
         $this->displayWarning("You can delete backup tables if you have no need of them.", true);
      }
983
984
      return $backup_tables;
   }
985
986

   /**
987
    * Add configuration value(s) to current context; @see Migration::addConfig()
988
989
990
    *
    * @since 9.2
    *
991
992
    * @param string|array $values  Value(s) to add
    * @param string       $context Context to add on (optional)
993
994
995
    *
    * @return Migration
    */
996
997
998
999
1000
1001
   public function addConfig($values, $context = null) {
      $context = $context ?? $this->context;
      if (!isset($this->configs[$context])) {
         $this->configs[$context] = [];
      }
      $this->configs[$context] += (array)$values;
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
      return $this;
   }

   /**
    * Store configuration values that does not exists
    *
    * @since 9.2
    *
    * @return boolean
    */
   private function storeConfig() {
      global $DB;

1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
      foreach ($this->configs as $context => $config) {
         if (count($config)) {
            $existing = $DB->request(
               "glpi_configs", [
                  'context'   => $context,
                  'name'      => array_keys(