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