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