Merge pull request #11226 from seamuslee001/CRM-21378
[civicrm-core.git] / CRM / Core / I18n / Schema.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
7e9e8871 4 | CiviCRM version 4.7 |
6a488035 5 +--------------------------------------------------------------------+
0f03f337 6 | Copyright CiviCRM LLC (c) 2004-2017 |
6a488035
TO
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
d25dd0ee 26 */
6a488035
TO
27
28/**
29 *
30 * @package CRM
0f03f337 31 * @copyright CiviCRM LLC (c) 2004-2017
6a488035
TO
32 */
33class CRM_Core_I18n_Schema {
34
35 /**
36 * Drop all views (for use by CRM_Core_DAO::dropAllTables() mostly).
6a488035 37 */
00be9182 38 public static function dropAllViews() {
6a488035
TO
39 $domain = new CRM_Core_DAO_Domain();
40 $domain->find(TRUE);
41 if (!$domain->locales) {
42 return;
43 }
44
45 $locales = explode(CRM_Core_DAO::VALUE_SEPARATOR, $domain->locales);
46 $tables = CRM_Core_I18n_SchemaStructure::tables();
47
48 foreach ($locales as $locale) {
49 foreach ($tables as $table) {
50 CRM_Core_DAO::executeQuery("DROP VIEW IF EXISTS {$table}_{$locale}");
51 }
52 }
53 }
54
55 /**
56 * Switch database from single-lang to multi (by adding
57 * the first language and dropping the original columns).
58 *
5a4f6742
CW
59 * @param string $locale
60 * the first locale to create (migrate to).
6a488035 61 */
00be9182 62 public static function makeMultilingual($locale) {
6a488035
TO
63 $domain = new CRM_Core_DAO_Domain();
64 $domain->find(TRUE);
65
66 // break early if the db is already multi-lang
67 if ($domain->locales) {
68 return;
69 }
70
71 $dao = new CRM_Core_DAO();
72
73 // build the column-adding SQL queries
74 $columns = CRM_Core_I18n_SchemaStructure::columns();
75 $indices = CRM_Core_I18n_SchemaStructure::indices();
76 $queries = array();
77 foreach ($columns as $table => $hash) {
78 // drop old indices
79 if (isset($indices[$table])) {
80 foreach ($indices[$table] as $index) {
15a68d41
SL
81 if (CRM_Core_BAO_SchemaHandler::checkIfIndexExists($table, $index['name'])) {
82 $queries[] = "DROP INDEX {$index['name']} ON {$table}";
83 }
6a488035
TO
84 }
85 }
86 // deal with columns
87 foreach ($hash as $column => $type) {
88 $queries[] = "ALTER TABLE {$table} ADD {$column}_{$locale} {$type}";
15a68d41
SL
89 if (CRM_Core_BAO_SchemaHandler::checkIfFieldExists($table, $column)) {
90 $queries[] = "UPDATE {$table} SET {$column}_{$locale} = {$column}";
91 $queries[] = "ALTER TABLE {$table} DROP {$column}";
92 }
6a488035
TO
93 }
94
95 // add view
96 $queries[] = self::createViewQuery($locale, $table, $dao);
97
98 // add new indices
99 $queries = array_merge($queries, array_values(self::createIndexQueries($locale, $table)));
100 }
101
102 // execute the queries without i18n rewriting
103 foreach ($queries as $query) {
104 $dao->query($query, FALSE);
105 }
106
107 // update civicrm_domain.locales
108 $domain->locales = $locale;
109 $domain->save();
110 }
111
112 /**
113 * Switch database from multi-lang back to single (by dropping
114 * additional columns and views and retaining only the selected locale).
115 *
5a4f6742
CW
116 * @param string $retain
117 * the locale to retain.
6a488035 118 */
00be9182 119 public static function makeSinglelingual($retain) {
3bdca100 120 $domain = new CRM_Core_DAO_Domain();
6a488035
TO
121 $domain->find(TRUE);
122 $locales = explode(CRM_Core_DAO::VALUE_SEPARATOR, $domain->locales);
123
124 // break early if the db is already single-lang
125 if (!$locales) {
126 return;
127 }
128
129 // lets drop all triggers first
3bdca100 130 $logging = new CRM_Logging_Schema();
481a74f4 131 $logging->dropTriggers();
6a488035
TO
132
133 // turn subsequent tables singlelingual
134 $tables = CRM_Core_I18n_SchemaStructure::tables();
135 foreach ($tables as $table) {
136 self::makeSinglelingualTable($retain, $table);
137 }
138
139 // update civicrm_domain.locales
140 $domain->locales = 'NULL';
141 $domain->save();
142
143 //CRM-6963 -fair assumption.
144 global $dbLocale;
145 $dbLocale = '';
146
147 // now lets rebuild all triggers
481a74f4 148 CRM_Core_DAO::triggerRebuild();
6a488035
TO
149 }
150
151 /**
152 * Switch a given table from multi-lang to single (by retaining only the selected locale).
153 *
5a4f6742
CW
154 * @param string $retain
155 * the locale to retain.
156 * @param string $table
157 * the table containing the column.
158 * @param string $class
159 * schema structure class to use to recreate indices.
6a488035 160 *
da6b46f4 161 * @param array $triggers
6a488035 162 */
3bdca100 163 public static function makeSinglelingualTable(
6a488035
TO
164 $retain,
165 $table,
166 $class = 'CRM_Core_I18n_SchemaStructure',
167 $triggers = array()
168 ) {
3bdca100 169 $domain = new CRM_Core_DAO_Domain();
6a488035
TO
170 $domain->find(TRUE);
171 $locales = explode(CRM_Core_DAO::VALUE_SEPARATOR, $domain->locales);
172
173 // break early if the db is already single-lang
174 if (!$locales) {
175 return;
176 }
177
0e6e8724
DL
178 $columns =& $class::columns();
179 $indices =& $class::indices();
6a488035
TO
180 $queries = array();
181 $dropQueries = array();
182 // drop indices
183 if (isset($indices[$table])) {
184 foreach ($indices[$table] as $index) {
185 foreach ($locales as $loc) {
186 $queries[] = "DROP INDEX {$index['name']}_{$loc} ON {$table}";
187 }
188 }
189 }
190
0e2b9f40 191 $dao = new CRM_Core_DAO();
6a488035
TO
192 // deal with columns
193 foreach ($columns[$table] as $column => $type) {
0e2b9f40 194 $queries[] = "ALTER TABLE {$table} CHANGE `{$column}_{$retain}` `{$column}` {$type}";
6a488035 195 foreach ($locales as $loc) {
40ee1db2 196 if (strcmp($loc, $retain) !== 0) {
0e2b9f40
MW
197 $dropQueries[] = "ALTER TABLE {$table} DROP {$column}_{$loc}";
198 }
6a488035
TO
199 }
200 }
201
202 // drop views
203 foreach ($locales as $loc) {
0e2b9f40 204 $queries[] = "DROP VIEW IF EXISTS {$table}_{$loc}";
6a488035
TO
205 }
206
207 // add original indices
208 $queries = array_merge($queries, self::createIndexQueries(NULL, $table));
209
210 // execute the queries without i18n rewriting
3bdca100 211 $dao = new CRM_Core_DAO();
6a488035
TO
212 foreach ($queries as $query) {
213 $dao->query($query, FALSE);
214 }
215
216 foreach ($dropQueries as $query) {
217 $dao->query($query, FALSE);
218 }
219
481a74f4 220 if (!empty($triggers)) {
6a488035 221 if (CRM_Core_Config::isUpgradeMode()) {
353ffa53
TO
222 foreach ($triggers as $triggerInfo) {
223 $when = $triggerInfo['when'];
224 $event = $triggerInfo['event'];
225 $triggerName = "{$table}_{$when}_{$event}";
226 CRM_Core_DAO::executeQuery("DROP TRIGGER IF EXISTS {$triggerName}");
227 }
6a488035 228 }
6a488035 229
353ffa53
TO
230 // invoke the meta trigger creation call
231 CRM_Core_DAO::triggerRebuild($table);
232 }
6a488035
TO
233 }
234
235 /**
236 * Add a new locale to a multi-lang db, setting
237 * its values to the current default locale.
238 *
5a4f6742
CW
239 * @param string $locale
240 * the new locale to add.
241 * @param string $source
242 * the locale to copy from.
6a488035 243 */
00be9182 244 public static function addLocale($locale, $source) {
6a488035
TO
245 // get the current supported locales
246 $domain = new CRM_Core_DAO_Domain();
247 $domain->find(TRUE);
248 $locales = explode(CRM_Core_DAO::VALUE_SEPARATOR, $domain->locales);
249
250 // break early if the locale is already supported
251 if (in_array($locale, $locales)) {
252 return;
253 }
254
255 $dao = new CRM_Core_DAO();
256
257 // build the required SQL queries
258 $columns = CRM_Core_I18n_SchemaStructure::columns();
259 $indices = CRM_Core_I18n_SchemaStructure::indices();
260 $queries = array();
261 foreach ($columns as $table => $hash) {
262 // add new columns
263 foreach ($hash as $column => $type) {
264 // CRM-7854: skip existing columns
265 if (CRM_Core_DAO::checkFieldExists($table, "{$column}_{$locale}", FALSE)) {
266 continue;
267 }
268 $queries[] = "ALTER TABLE {$table} ADD {$column}_{$locale} {$type}";
269 $queries[] = "UPDATE {$table} SET {$column}_{$locale} = {$column}_{$source}";
270 }
271
272 // add view
273 $queries[] = self::createViewQuery($locale, $table, $dao);
274
275 // add new indices
276 $queries = array_merge($queries, array_values(self::createIndexQueries($locale, $table)));
277 }
278
279 // execute the queries without i18n rewriting
280 foreach ($queries as $query) {
281 $dao->query($query, FALSE);
282 }
283
284 // update civicrm_domain.locales
285 $locales[] = $locale;
286 $domain->locales = implode(CRM_Core_DAO::VALUE_SEPARATOR, $locales);
287 $domain->save();
288
289 // invoke the meta trigger creation call
290 CRM_Core_DAO::triggerRebuild();
291 }
292
293 /**
294 * Rebuild multilingual indices, views and triggers (useful for upgrades)
295 *
5a4f6742
CW
296 * @param array $locales
297 * locales to be rebuilt.
298 * @param string $version
299 * version of schema structure to use.
41ace555
SL
300 * @param bool $isUpgradeMode
301 * Are we upgrading our database
6a488035 302 */
41ace555 303 public static function rebuildMultilingualSchema($locales, $version = NULL, $isUpgradeMode = FALSE) {
6a488035
TO
304 if ($version) {
305 $latest = self::getLatestSchema($version);
306 require_once "CRM/Core/I18n/SchemaStructure_{$latest}.php";
307 $class = "CRM_Core_I18n_SchemaStructure_{$latest}";
308 }
309 else {
310 $class = 'CRM_Core_I18n_SchemaStructure';
311 }
0e6e8724 312 $indices =& $class::indices();
353ffa53 313 $tables =& $class::tables();
6a488035 314 $queries = array();
3bdca100 315 $dao = new CRM_Core_DAO();
6a488035
TO
316
317 // get all of the already existing indices
318 $existing = array();
319 foreach (array_keys($indices) as $table) {
320 $existing[$table] = array();
321 $dao->query("SHOW INDEX FROM $table", FALSE);
322 while ($dao->fetch()) {
323 if (preg_match('/_[a-z][a-z]_[A-Z][A-Z]$/', $dao->Key_name)) {
324 $existing[$table][] = $dao->Key_name;
325 }
326 }
327 }
328
329 // from all of the CREATE INDEX queries fetch the ones creating missing indices
330 foreach ($locales as $locale) {
331 foreach (array_keys($indices) as $table) {
332 $allQueries = self::createIndexQueries($locale, $table, $class);
333 foreach ($allQueries as $name => $query) {
334 if (!in_array("{$name}_{$locale}", $existing[$table])) {
335 $queries[] = $query;
336 }
337 }
338 }
339 }
340
341 // rebuild views
342 foreach ($locales as $locale) {
343 foreach ($tables as $table) {
41ace555 344 $queries[] = self::createViewQuery($locale, $table, $dao, $class, $isUpgradeMode);
6a488035
TO
345 }
346 }
347
348 // rebuild triggers
349 $last = array_pop($locales);
350
351 foreach ($queries as $query) {
352 $dao->query($query, FALSE);
353 }
354
355 // invoke the meta trigger creation call
356 CRM_Core_DAO::triggerRebuild();
357 }
358
359 /**
360 * Rewrite SQL query to use views to access tables with localized columns.
361 *
5a4f6742
CW
362 * @param string $query
363 * the query for rewrite.
6a488035 364 *
a6c01b45
CW
365 * @return string
366 * the rewritten query
6a488035 367 */
00be9182 368 public static function rewriteQuery($query) {
6a488035
TO
369 global $dbLocale;
370 $tables = self::schemaStructureTables();
371 foreach ($tables as $table) {
7f7d306e
SV
372 // CRM-19093
373 // should match the civicrm table name such as: civicrm_event
374 // but must not match the table name if it's a substring of another table: civicrm_events_in_cart
bedb217f 375 $query = preg_replace("/([^'\"])({$table})(\z|[^a-z_'\"])/", "\\1\\2{$dbLocale}\\3", $query);
6a488035
TO
376 }
377 // uncomment the below to rewrite the civicrm_value_* queries
378 // $query = preg_replace("/(civicrm_value_[a-z0-9_]+_\d+)([^_])/", "\\1{$dbLocale}\\2", $query);
379 return $query;
380 }
381
a0ee3941
EM
382 /**
383 * @param null $version
384 * @param bool $force
385 *
386 * @return array
387 */
00be9182 388 public static function schemaStructureTables($version = NULL, $force = FALSE) {
6a488035
TO
389 static $_tables = NULL;
390 if ($_tables === NULL || $force) {
391 if ($version) {
392 $latest = self::getLatestSchema($version);
393 // FIXME: Doing require_once is a must here because a call like CRM_Core_I18n_SchemaStructure_4_1_0 makes
394 // class loader look for file like - CRM/Core/I18n/SchemaStructure/4/1/0.php which is not what we want to be loaded
395 require_once "CRM/Core/I18n/SchemaStructure_{$latest}.php";
396 $class = "CRM_Core_I18n_SchemaStructure_{$latest}";
353ffa53 397 $tables =& $class::tables();
6a488035
TO
398 }
399 else {
400 $tables = CRM_Core_I18n_SchemaStructure::tables();
401 }
402 $_tables = $tables;
403 }
404 return $_tables;
405 }
406
a0ee3941
EM
407 /**
408 * @param $version
409 *
410 * @return mixed
411 */
00be9182 412 public static function getLatestSchema($version) {
6a488035
TO
413 // remove any .upgrade sub-str from version. Makes it easy to do version_compare & give right result
414 $version = str_ireplace(".upgrade", "", $version);
415
416 // fetch all the SchemaStructure versions we ship and sort by version
417 $schemas = array();
418 foreach (scandir(dirname(__FILE__)) as $file) {
419 $matches = array();
420 if (preg_match('/^SchemaStructure_([0-9a-z_]+)\.php$/', $file, $matches)) {
421 $schemas[] = str_replace('_', '.', $matches[1]);
422 }
423 }
424 usort($schemas, 'version_compare');
425
426 // find the latest schema structure older than (or equal to) $version
427 do {
428 $latest = array_pop($schemas);
429 } while (version_compare($latest, $version, '>'));
430
431 return str_replace('.', '_', $latest);
432 }
433
434 /**
fe482240 435 * CREATE INDEX queries for a given locale and table.
6a488035 436 *
5a4f6742
CW
437 * @param string $locale
438 * locale for which the queries should be created (null to create original indices).
439 * @param string $table
440 * table for which the queries should be created.
441 * @param string $class
442 * schema structure class to use.
6a488035 443 *
a6c01b45
CW
444 * @return array
445 * array of CREATE INDEX queries
6a488035
TO
446 */
447 private static function createIndexQueries($locale, $table, $class = 'CRM_Core_I18n_SchemaStructure') {
0e6e8724
DL
448 $indices =& $class::indices();
449 $columns =& $class::columns();
6a488035
TO
450 if (!isset($indices[$table])) {
451 return array();
452 }
453
454 $queries = array();
455 foreach ($indices[$table] as $index) {
456 $unique = isset($index['unique']) && $index['unique'] ? 'UNIQUE' : '';
457 foreach ($index['field'] as $i => $col) {
458 // if a given column is localizable, extend its name with the locale
459 if ($locale and isset($columns[$table][$col])) {
460 $index['field'][$i] = "{$col}_{$locale}";
461 }
462 }
463 $cols = implode(', ', $index['field']);
464 $name = $index['name'];
465 if ($locale) {
466 $name .= '_' . $locale;
467 }
468 // CRM-7854: skip existing indices
469 if (CRM_Core_DAO::checkConstraintExists($table, $name)) {
470 continue;
471 }
472 $queries[$index['name']] = "CREATE {$unique} INDEX {$name} ON {$table} ({$cols})";
473 }
474 return $queries;
475 }
476
477 /**
fe482240 478 * CREATE VIEW query for a given locale and table.
6a488035 479 *
5a4f6742
CW
480 * @param string $locale
481 * locale of the view.
482 * @param string $table
483 * table of the view.
6a0b768e
TO
484 * @param CRM_Core_DAO $dao
485 * A DAO object to run DESCRIBE queries.
5a4f6742
CW
486 * @param string $class
487 * schema structure class to use.
41ace555
SL
488 * @param bool $isUpgradeMode
489 * Are we in upgrade mode therefore only build based off table not class
a6c01b45
CW
490 * @return array
491 * array of CREATE INDEX queries
6a488035 492 */
41ace555 493 private static function createViewQuery($locale, $table, &$dao, $class = 'CRM_Core_I18n_SchemaStructure', $isUpgradeMode = FALSE) {
0e6e8724 494 $columns =& $class::columns();
6a488035 495 $cols = array();
41ace555 496 $tableCols = array();
6a488035
TO
497 $dao->query("DESCRIBE {$table}", FALSE);
498 while ($dao->fetch()) {
499 // view non-internationalized columns directly
500 if (!in_array($dao->Field, array_keys($columns[$table])) and
501 !preg_match('/_[a-z][a-z]_[A-Z][A-Z]$/', $dao->Field)
502 ) {
503 $cols[] = $dao->Field;
504 }
41ace555 505 $tableCols[] = $dao->Field;
6a488035
TO
506 }
507 // view intrernationalized columns through an alias
508 foreach ($columns[$table] as $column => $_) {
41ace555
SL
509 if (!$isUpgradeMode) {
510 $cols[] = "{$column}_{$locale} {$column}";
511 }
512 elseif (in_array("{$column}_{$locale}", $tableCols)) {
513 $cols[] = "{$column}_{$locale} {$column}";
514 }
6a488035
TO
515 }
516 return "CREATE OR REPLACE VIEW {$table}_{$locale} AS SELECT " . implode(', ', $cols) . " FROM {$table}";
517 }
518
a0ee3941
EM
519 /**
520 * @param $info
521 * @param null $tableName
522 */
00be9182 523 public static function triggerInfo(&$info, $tableName = NULL) {
6a488035
TO
524 // get the current supported locales
525 $domain = new CRM_Core_DAO_Domain();
526 $domain->find(TRUE);
527 if (empty($domain->locales)) {
528 return;
529 }
530
531 $locales = explode(CRM_Core_DAO::VALUE_SEPARATOR, $domain->locales);
532 $locale = array_pop($locales);
533
534 // CRM-10027
535 if (count($locales) == 0) {
536 return;
537 }
538
539 $currentVer = CRM_Core_BAO_Domain::version(TRUE);
540
541 if ($currentVer && CRM_Core_Config::isUpgradeMode()) {
542 // take exact version so that proper schema structure file in invoked
543 $latest = self::getLatestSchema($currentVer);
544 require_once "CRM/Core/I18n/SchemaStructure_{$latest}.php";
545 $class = "CRM_Core_I18n_SchemaStructure_{$latest}";
546 }
547 else {
548 $class = 'CRM_Core_I18n_SchemaStructure';
549 }
550
0e6e8724 551 $columns =& $class::columns();
6a488035
TO
552
553 foreach ($columns as $table => $hash) {
554 if ($tableName &&
555 $tableName != $table
556 ) {
557 continue;
558 }
559
560 $trigger = array();
561
562 foreach ($hash as $column => $_) {
563 $trigger[] = "IF NEW.{$column}_{$locale} IS NOT NULL THEN";
564 foreach ($locales as $old) {
565 $trigger[] = "IF NEW.{$column}_{$old} IS NULL THEN SET NEW.{$column}_{$old} = NEW.{$column}_{$locale}; END IF;";
566 }
567 foreach ($locales as $old) {
568 $trigger[] = "ELSEIF NEW.{$column}_{$old} IS NOT NULL THEN";
569 foreach (array_merge($locales, array(
3bdca100 570 $locale,
353ffa53 571 )) as $loc) {
6a488035
TO
572 if ($loc == $old) {
573 continue;
574 }
575 $trigger[] = "IF NEW.{$column}_{$loc} IS NULL THEN SET NEW.{$column}_{$loc} = NEW.{$column}_{$old}; END IF;";
576 }
577 }
578 $trigger[] = 'END IF;';
579 }
580
581 $sql = implode(' ', $trigger);
6ea503d4
TO
582 $info[] = array(
583 'table' => array($table),
6a488035
TO
584 'when' => 'BEFORE',
585 'event' => array('UPDATE'),
586 'sql' => $sql,
587 );
588 }
589
590 // take care of the ON INSERT triggers
591 foreach ($columns as $table => $hash) {
592 $trigger = array();
593 foreach ($hash as $column => $_) {
594 $trigger[] = "IF NEW.{$column}_{$locale} IS NOT NULL THEN";
595 foreach ($locales as $old) {
596 $trigger[] = "SET NEW.{$column}_{$old} = NEW.{$column}_{$locale};";
597 }
598 foreach ($locales as $old) {
599 $trigger[] = "ELSEIF NEW.{$column}_{$old} IS NOT NULL THEN";
600 foreach (array_merge($locales, array(
3bdca100 601 $locale,
353ffa53 602 )) as $loc) {
6a488035
TO
603 if ($loc == $old) {
604 continue;
605 }
606 $trigger[] = "SET NEW.{$column}_{$loc} = NEW.{$column}_{$old};";
607 }
608 }
609 $trigger[] = 'END IF;';
610 }
611
612 $sql = implode(' ', $trigger);
6ea503d4
TO
613 $info[] = array(
614 'table' => array($table),
6a488035
TO
615 'when' => 'BEFORE',
616 'event' => array('INSERT'),
617 'sql' => $sql,
618 );
619 }
620 }
96025800 621
6a488035 622}