Backup of db before drupal security update
[yaffs-website] / web / core / modules / locale / src / StringDatabaseStorage.php
1 <?php
2
3 namespace Drupal\locale;
4
5 use Drupal\Core\Database\Connection;
6
7 /**
8  * Defines a class to store localized strings in the database.
9  */
10 class StringDatabaseStorage implements StringStorageInterface {
11
12   /**
13    * The database connection.
14    *
15    * @var \Drupal\Core\Database\Connection
16    */
17   protected $connection;
18
19   /**
20    * Additional database connection options to use in queries.
21    *
22    * @var array
23    */
24   protected $options = [];
25
26   /**
27    * Constructs a new StringDatabaseStorage class.
28    *
29    * @param \Drupal\Core\Database\Connection $connection
30    *   A Database connection to use for reading and writing configuration data.
31    * @param array $options
32    *   (optional) Any additional database connection options to use in queries.
33    */
34   public function __construct(Connection $connection, array $options = []) {
35     $this->connection = $connection;
36     $this->options = $options;
37   }
38
39   /**
40    * {@inheritdoc}
41    */
42   public function getStrings(array $conditions = [], array $options = []) {
43     return $this->dbStringLoad($conditions, $options, 'Drupal\locale\SourceString');
44   }
45
46   /**
47    * {@inheritdoc}
48    */
49   public function getTranslations(array $conditions = [], array $options = []) {
50     return $this->dbStringLoad($conditions, ['translation' => TRUE] + $options, 'Drupal\locale\TranslationString');
51   }
52
53   /**
54    * {@inheritdoc}
55    */
56   public function findString(array $conditions) {
57     $values = $this->dbStringSelect($conditions)
58       ->execute()
59       ->fetchAssoc();
60
61     if (!empty($values)) {
62       $string = new SourceString($values);
63       $string->setStorage($this);
64       return $string;
65     }
66   }
67
68   /**
69    * {@inheritdoc}
70    */
71   public function findTranslation(array $conditions) {
72     $values = $this->dbStringSelect($conditions, ['translation' => TRUE])
73       ->execute()
74       ->fetchAssoc();
75
76     if (!empty($values)) {
77       $string = new TranslationString($values);
78       $this->checkVersion($string, \Drupal::VERSION);
79       $string->setStorage($this);
80       return $string;
81     }
82   }
83
84   /**
85    * {@inheritdoc}
86    */
87   public function getLocations(array $conditions = []) {
88     $query = $this->connection->select('locales_location', 'l', $this->options)
89       ->fields('l');
90     foreach ($conditions as $field => $value) {
91       // Cast scalars to array so we can consistently use an IN condition.
92       $query->condition('l.' . $field, (array) $value, 'IN');
93     }
94     return $query->execute()->fetchAll();
95   }
96
97   /**
98    * {@inheritdoc}
99    */
100   public function countStrings() {
101     return $this->dbExecute("SELECT COUNT(*) FROM {locales_source}")->fetchField();
102   }
103
104   /**
105    * {@inheritdoc}
106    */
107   public function countTranslations() {
108     return $this->dbExecute("SELECT t.language, COUNT(*) AS translated FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid GROUP BY t.language")->fetchAllKeyed();
109   }
110
111   /**
112    * {@inheritdoc}
113    */
114   public function save($string) {
115     if ($string->isNew()) {
116       $result = $this->dbStringInsert($string);
117       if ($string->isSource() && $result) {
118         // Only for source strings, we set the locale identifier.
119         $string->setId($result);
120       }
121       $string->setStorage($this);
122     }
123     else {
124       $this->dbStringUpdate($string);
125     }
126     // Update locations if they come with the string.
127     $this->updateLocation($string);
128     return $this;
129   }
130
131   /**
132    * Update locations for string.
133    *
134    * @param \Drupal\locale\StringInterface $string
135    *   The string object.
136    */
137   protected function updateLocation($string) {
138     if ($locations = $string->getLocations(TRUE)) {
139       $created = FALSE;
140       foreach ($locations as $type => $location) {
141         foreach ($location as $name => $lid) {
142           // Make sure that the name isn't longer than 255 characters.
143           $name = substr($name, 0, 255);
144           if (!$lid) {
145             $this->dbDelete('locales_location', ['sid' => $string->getId(), 'type' => $type, 'name' => $name])
146               ->execute();
147           }
148           elseif ($lid === TRUE) {
149             // This is a new location to add, take care not to duplicate.
150             $this->connection->merge('locales_location', $this->options)
151               ->keys(['sid' => $string->getId(), 'type' => $type, 'name' => $name])
152               ->fields(['version' => \Drupal::VERSION])
153               ->execute();
154             $created = TRUE;
155           }
156           // Loaded locations have 'lid' integer value, nor FALSE, nor TRUE.
157         }
158       }
159       if ($created) {
160         // As we've set a new location, check string version too.
161         $this->checkVersion($string, \Drupal::VERSION);
162       }
163     }
164   }
165
166   /**
167    * Checks whether the string version matches a given version, fix it if not.
168    *
169    * @param \Drupal\locale\StringInterface $string
170    *   The string object.
171    * @param string $version
172    *   Drupal version to check against.
173    */
174   protected function checkVersion($string, $version) {
175     if ($string->getId() && $string->getVersion() != $version) {
176       $string->setVersion($version);
177       $this->connection->update('locales_source', $this->options)
178         ->condition('lid', $string->getId())
179         ->fields(['version' => $version])
180         ->execute();
181     }
182   }
183
184   /**
185    * {@inheritdoc}
186    */
187   public function delete($string) {
188     if ($keys = $this->dbStringKeys($string)) {
189       $this->dbDelete('locales_target', $keys)->execute();
190       if ($string->isSource()) {
191         $this->dbDelete('locales_source', $keys)->execute();
192         $this->dbDelete('locales_location', $keys)->execute();
193         $string->setId(NULL);
194       }
195     }
196     else {
197       throw new StringStorageException('The string cannot be deleted because it lacks some key fields: ' . $string->getString());
198     }
199     return $this;
200   }
201
202   /**
203    * {@inheritdoc}
204    */
205   public function deleteStrings($conditions) {
206     $lids = $this->dbStringSelect($conditions, ['fields' => ['lid']])->execute()->fetchCol();
207     if ($lids) {
208       $this->dbDelete('locales_target', ['lid' => $lids])->execute();
209       $this->dbDelete('locales_source', ['lid' => $lids])->execute();
210       $this->dbDelete('locales_location', ['sid' => $lids])->execute();
211     }
212   }
213
214   /**
215    * {@inheritdoc}
216    */
217   public function deleteTranslations($conditions) {
218     $this->dbDelete('locales_target', $conditions)->execute();
219   }
220
221   /**
222    * {@inheritdoc}
223    */
224   public function createString($values = []) {
225     return new SourceString($values + ['storage' => $this]);
226   }
227
228   /**
229    * {@inheritdoc}
230    */
231   public function createTranslation($values = []) {
232     return new TranslationString($values + [
233       'storage' => $this,
234       'is_new' => TRUE,
235     ]);
236   }
237
238   /**
239    * Gets table alias for field.
240    *
241    * @param string $field
242    *   One of the field names of the locales_source, locates_location,
243    *   locales_target tables to find the table alias for.
244    *
245    * @return string
246    *   One of the following values:
247    *   - 's' for "source", "context", "version" (locales_source table fields).
248    *   - 'l' for "type", "name" (locales_location table fields)
249    *   - 't' for "language", "translation", "customized" (locales_target
250    *     table fields)
251    */
252   protected function dbFieldTable($field) {
253     if (in_array($field, ['language', 'translation', 'customized'])) {
254       return 't';
255     }
256     elseif (in_array($field, ['type', 'name'])) {
257       return 'l';
258     }
259     else {
260       return 's';
261     }
262   }
263
264   /**
265    * Gets table name for storing string object.
266    *
267    * @param \Drupal\locale\StringInterface $string
268    *   The string object.
269    *
270    * @return string
271    *   The table name.
272    */
273   protected function dbStringTable($string) {
274     if ($string->isSource()) {
275       return 'locales_source';
276     }
277     elseif ($string->isTranslation()) {
278       return 'locales_target';
279     }
280   }
281
282   /**
283    * Gets keys values that are in a database table.
284    *
285    * @param \Drupal\locale\StringInterface $string
286    *   The string object.
287    *
288    * @return array
289    *   Array with key fields if the string has all keys, or empty array if not.
290    */
291   protected function dbStringKeys($string) {
292     if ($string->isSource()) {
293       $keys = ['lid'];
294     }
295     elseif ($string->isTranslation()) {
296       $keys = ['lid', 'language'];
297     }
298     if (!empty($keys) && ($values = $string->getValues($keys)) && count($keys) == count($values)) {
299       return $values;
300     }
301     else {
302       return [];
303     }
304   }
305
306   /**
307    * Loads multiple string objects.
308    *
309    * @param array $conditions
310    *   Any of the conditions used by dbStringSelect().
311    * @param array $options
312    *   Any of the options used by dbStringSelect().
313    * @param string $class
314    *   Class name to use for fetching returned objects.
315    *
316    * @return \Drupal\locale\StringInterface[]
317    *   Array of objects of the class requested.
318    */
319   protected function dbStringLoad(array $conditions, array $options, $class) {
320     $strings = [];
321     $result = $this->dbStringSelect($conditions, $options)->execute();
322     foreach ($result as $item) {
323       /** @var \Drupal\locale\StringInterface $string */
324       $string = new $class($item);
325       $string->setStorage($this);
326       $strings[] = $string;
327     }
328     return $strings;
329   }
330
331   /**
332    * Builds a SELECT query with multiple conditions and fields.
333    *
334    * The query uses both 'locales_source' and 'locales_target' tables.
335    * Note that by default, as we are selecting both translated and untranslated
336    * strings target field's conditions will be modified to match NULL rows too.
337    *
338    * @param array $conditions
339    *   An associative array with field => value conditions that may include
340    *   NULL values. If a language condition is included it will be used for
341    *   joining the 'locales_target' table.
342    * @param array $options
343    *   An associative array of additional options. It may contain any of the
344    *   options used by Drupal\locale\StringStorageInterface::getStrings() and
345    *   these additional ones:
346    *   - 'translation', Whether to include translation fields too. Defaults to
347    *     FALSE.
348    *
349    * @return \Drupal\Core\Database\Query\Select
350    *   Query object with all the tables, fields and conditions.
351    */
352   protected function dbStringSelect(array $conditions, array $options = []) {
353     // Start building the query with source table and check whether we need to
354     // join the target table too.
355     $query = $this->connection->select('locales_source', 's', $this->options)
356       ->fields('s');
357
358     // Figure out how to join and translate some options into conditions.
359     if (isset($conditions['translated'])) {
360       // This is a meta-condition we need to translate into simple ones.
361       if ($conditions['translated']) {
362         // Select only translated strings.
363         $join = 'innerJoin';
364       }
365       else {
366         // Select only untranslated strings.
367         $join = 'leftJoin';
368         $conditions['translation'] = NULL;
369       }
370       unset($conditions['translated']);
371     }
372     else {
373       $join = !empty($options['translation']) ? 'leftJoin' : FALSE;
374     }
375
376     if ($join) {
377       if (isset($conditions['language'])) {
378         // If we've got a language condition, we use it for the join.
379         $query->$join('locales_target', 't', "t.lid = s.lid AND t.language = :langcode", [
380           ':langcode' => $conditions['language'],
381         ]);
382         unset($conditions['language']);
383       }
384       else {
385         // Since we don't have a language, join with locale id only.
386         $query->$join('locales_target', 't', "t.lid = s.lid");
387       }
388       if (!empty($options['translation'])) {
389         // We cannot just add all fields because 'lid' may get null values.
390         $query->fields('t', ['language', 'translation', 'customized']);
391       }
392     }
393
394     // If we have conditions for location's type or name, then we need the
395     // location table, for which we add a subquery. We cast any scalar value to
396     // array so we can consistently use IN conditions.
397     if (isset($conditions['type']) || isset($conditions['name'])) {
398       $subquery = $this->connection->select('locales_location', 'l', $this->options)
399         ->fields('l', ['sid']);
400       foreach (['type', 'name'] as $field) {
401         if (isset($conditions[$field])) {
402           $subquery->condition('l.' . $field, (array) $conditions[$field], 'IN');
403           unset($conditions[$field]);
404         }
405       }
406       $query->condition('s.lid', $subquery, 'IN');
407     }
408
409     // Add conditions for both tables.
410     foreach ($conditions as $field => $value) {
411       $table_alias = $this->dbFieldTable($field);
412       $field_alias = $table_alias . '.' . $field;
413       if (is_null($value)) {
414         $query->isNull($field_alias);
415       }
416       elseif ($table_alias == 't' && $join === 'leftJoin') {
417         // Conditions for target fields when doing an outer join only make
418         // sense if we add also OR field IS NULL.
419         $query->condition(db_or()
420           ->condition($field_alias, (array) $value, 'IN')
421           ->isNull($field_alias)
422         );
423       }
424       else {
425         $query->condition($field_alias, (array) $value, 'IN');
426       }
427     }
428
429     // Process other options, string filter, query limit, etc.
430     if (!empty($options['filters'])) {
431       if (count($options['filters']) > 1) {
432         $filter = db_or();
433         $query->condition($filter);
434       }
435       else {
436         // If we have a single filter, just add it to the query.
437         $filter = $query;
438       }
439       foreach ($options['filters'] as $field => $string) {
440         $filter->condition($this->dbFieldTable($field) . '.' . $field, '%' . db_like($string) . '%', 'LIKE');
441       }
442     }
443
444     if (!empty($options['pager limit'])) {
445       $query = $query->extend('Drupal\Core\Database\Query\PagerSelectExtender')->limit($options['pager limit']);
446     }
447
448     return $query;
449   }
450
451   /**
452    * Creates a database record for a string object.
453    *
454    * @param \Drupal\locale\StringInterface $string
455    *   The string object.
456    *
457    * @return bool|int
458    *   If the operation failed, returns FALSE.
459    *   If it succeeded returns the last insert ID of the query, if one exists.
460    *
461    * @throws \Drupal\locale\StringStorageException
462    *   If the string is not suitable for this storage, an exception is thrown.
463    */
464   protected function dbStringInsert($string) {
465     if ($string->isSource()) {
466       $string->setValues(['context' => '', 'version' => 'none'], FALSE);
467       $fields = $string->getValues(['source', 'context', 'version']);
468     }
469     elseif ($string->isTranslation()) {
470       $string->setValues(['customized' => 0], FALSE);
471       $fields = $string->getValues(['lid', 'language', 'translation', 'customized']);
472     }
473     if (!empty($fields)) {
474       return $this->connection->insert($this->dbStringTable($string), $this->options)
475         ->fields($fields)
476         ->execute();
477     }
478     else {
479       throw new StringStorageException('The string cannot be saved: ' . $string->getString());
480     }
481   }
482
483   /**
484    * Updates string object in the database.
485    *
486    * @param \Drupal\locale\StringInterface $string
487    *   The string object.
488    *
489    * @return bool|int
490    *   If the record update failed, returns FALSE. If it succeeded, returns
491    *   SAVED_NEW or SAVED_UPDATED.
492    *
493    * @throws \Drupal\locale\StringStorageException
494    *   If the string is not suitable for this storage, an exception is thrown.
495    */
496   protected function dbStringUpdate($string) {
497     if ($string->isSource()) {
498       $values = $string->getValues(['source', 'context', 'version']);
499     }
500     elseif ($string->isTranslation()) {
501       $values = $string->getValues(['translation', 'customized']);
502     }
503     if (!empty($values) && $keys = $this->dbStringKeys($string)) {
504       return $this->connection->merge($this->dbStringTable($string), $this->options)
505         ->keys($keys)
506         ->fields($values)
507         ->execute();
508     }
509     else {
510       throw new StringStorageException('The string cannot be updated: ' . $string->getString());
511     }
512   }
513
514   /**
515    * Creates delete query.
516    *
517    * @param string $table
518    *   The table name.
519    * @param array $keys
520    *   Array with object keys indexed by field name.
521    *
522    * @return \Drupal\Core\Database\Query\Delete
523    *   Returns a new Delete object for the injected database connection.
524    */
525   protected function dbDelete($table, $keys) {
526     $query = $this->connection->delete($table, $this->options);
527     foreach ($keys as $field => $value) {
528       $query->condition($field, $value);
529     }
530     return $query;
531   }
532
533   /**
534    * Executes an arbitrary SELECT query string with the injected options.
535    */
536   protected function dbExecute($query, array $args = []) {
537     return $this->connection->query($query, $args, $this->options);
538   }
539
540 }