config.inc 9.88 KB
Newer Older
chx's avatar
chx committed
1
2
<?php

gdd's avatar
gdd committed
3
4
5
6
7
/**
 * @file
 * This is the API for configuration storage.
 */

8
9
10
11
12
13
/**
 * Get the randomly generated config directory name.
 *
 * @return
 *   The directory name.
 */
14
15
function config_get_config_directory() {
  global $drupal_config_directory_name;
16
  
17
  return conf_path() . '/files/' . $drupal_config_directory_name;
18
19
}

20
/**
gdd's avatar
gdd committed
21
22
23
 * Retrieve an iterable array which lists the children under a config 'branch'.
 *
 * Given the following configuration files:
24
25
 *   core.entity.node_type.article.xml
 *   core.entity.node_type.page.xml
gdd's avatar
gdd committed
26
27
28
29
30
31
32
33
 *
 * You can pass a prefix 'core.entity.node_type' and get back an array of the
 * filenames that match. This allows you to iterate through all files in a 
 * branch. Note that this will only work on the level above the tips, so
 * a prefix of 'core.entity' would return an empty array.
 *
 * @param $prefix
 *   The prefix of the files we are searching for.
34
 * @return
gdd's avatar
gdd committed
35
 *   An array of file names under a branch.
36
37
 */
function config_get_signed_file_storage_names_with_prefix($prefix = '') {
38
  $files = glob(config_get_config_directory() . '/' . $prefix . '*.xml');
39
  $clean_name = function ($value) {
40
    return basename($value, '.xml');
41
  };
42
  return array_map($clean_name, $files);
David Strauss's avatar
David Strauss committed
43
44
}

gdd's avatar
gdd committed
45
46
47
48
49
50
51
52
/**
 * Generate a hash of a config file's contents using our encryption key.
 * 
 * @param $data
 *   The contents of a configuration file.
 * @return
 *   A hash of the data.
 */
David Strauss's avatar
David Strauss committed
53
function config_sign_data($data) {
54
55
  // The configuration key is loaded from settings.php and imported into the global namespace
  global $drupal_config_key;
gdd's avatar
gdd committed
56
  
chx's avatar
chx committed
57
  // SHA-512 is both secure and very fast on 64 bit CPUs.
58
  return hash_hmac('sha512', $data, $drupal_config_key);
David Strauss's avatar
David Strauss committed
59
60
}

61
62
63
64
65
66
class ConfigException extends Exception {}

class ConfigFileStorageException extends ConfigException {}
class ConfigFileStorageReadException extends ConfigFileStorageException {}
class ConfigFileStorageSignatureException extends ConfigFileStorageException {}

David Strauss's avatar
David Strauss committed
67
class SignedFileStorage {
68
69
  /**
   * Constructor for the signed file storage interface.
Crell's avatar
Crell committed
70
   *
71
72
   * This class allows reading and writing configuration data from/to the
   * disk while automatically managing and verifying cryptographic signatures.
Crell's avatar
Crell committed
73
   *
74
75
76
   * param @name
   *   Lowercase string, the name for the configuration data.
   */
David Strauss's avatar
David Strauss committed
77
  public function __construct($name) {
David Strauss's avatar
David Strauss committed
78
79
80
    $this->name = $name;
  }

81
  /**
82
   * Read and return a signed file and its signature.
83
84
85
86
87
88
89
   *
   * @return
   *   An array with "signature" and "data" keys.
   *
   * @throws
   *   Exception
   */
90
  protected function readWithSignature() {
David Strauss's avatar
David Strauss committed
91
    // TODO: Optimize with explicit offsets?
Crell's avatar
Crell committed
92
    $content = file_get_contents($this->getFilePath());
93
94
    if ($content === FALSE) {
      throw new Exception('Read file is invalid.');
95
    }
gdd's avatar
gdd committed
96
    $signature = file_get_contents($this->getFilePath() . '.sig');
97
98
    if ($signature === FALSE) {
      throw new Exception('Signature file is invalid.');
99
    }
100
    return array('data' => $content, 'signature' => $signature);
David Strauss's avatar
David Strauss committed
101
102
  }

103
104
105
106
  protected function exists() {
    return file_exists($this->getFilePath());
  }
  
107
108
  public function getFilePath() {
    return config_get_config_directory() . '/' . $this->name  . '.xml';
David Strauss's avatar
David Strauss committed
109
110
111
  }

  public function resign() {
112
    if ($this->exists()) {
113
      $parts = $this->readWithSignature();
114
115
      $this->write($parts['data']);
    }
David Strauss's avatar
David Strauss committed
116
117
118
  }

  public function verify($contentOnSuccess = FALSE) {
119
    if ($this->exists()) {
120
      $split = $this->readWithSignature();
121
      print_r($split);
122
123
124
125
126
127
      $expected_signature = config_sign_data($split['data']);
      if ($expected_signature === $split['signature']) {
        if ($contentOnSuccess) {
          return $split['data'];
        }
        return TRUE;
David Strauss's avatar
David Strauss committed
128
129
130
131
132
133
134
      }
    }
    return FALSE;
  }

  public function write($data) {
    $signature = config_sign_data($data);
135
    if (!file_put_contents($this->getFilePath(), $data)) {
136
137
      throw new Exception('Failed to write configuration file.');
    }
gdd's avatar
gdd committed
138
    if (!file_put_contents($this->getFilePath() . '.sig', $signature)) {
139
      throw new Exception('Failed to write signature file.');
David Strauss's avatar
David Strauss committed
140
141
142
143
    }
  }

  public function read() {
144
145
146
147
148
149
    if ($this->exists()) {
      $verification = $this->verify(TRUE);
      if ($verification === FALSE) {
        throw new Exception('Invalid signature in file header.');
      }
      return $verification;
David Strauss's avatar
David Strauss committed
150
151
152
153
    }
  }
}

154
155
156
157
interface DrupalConfigVerifiedStorageInterface {

  /**
   * Constructor for the verified storage manipulation class.
Crell's avatar
Crell committed
158
   *
chx's avatar
chx committed
159
160
161
   * This class allows reading and writing configuration data from/to the
   * verified storage and copying to/from the signed file storing the same
   * data.
Crell's avatar
Crell committed
162
   *
163
164
165
   * param @name
   *   Lowercase string, the name for the configuration data.
   */
David Strauss's avatar
David Strauss committed
166
  function __construct($name);
chx's avatar
chx committed
167

168
169
170
  /**
   * Read the configuration data from the verified storage.
   */
chx's avatar
chx committed
171
172
  function read();

173
174
175
176
177
178
179
180
181
182
183
184
  /**
   * Copy the configuration data from the verified storage into a file.
   */
  function copyToFile();

  /**
   * Copy the configuration data from the file into the verified storage.
   */
  function copyFromFile();

  /**
   * Check whether the file and the verified storage is in sync.
Crell's avatar
Crell committed
185
   *
186
187
188
189
   * @return
   *   TRUE if the file and the verified storage contains the same data, FALSE
   *   if not.
   */
chx's avatar
chx committed
190
191
  function isOutOfSync();

192
193
  /**
   * Write the configuration data into the active storage but not the file.
Crell's avatar
Crell committed
194
   *
195
196
197
   * Use this function if you need to make temporary changes to your
   * configuration.
   */
chx's avatar
chx committed
198
  function writeToActive($data);
chx's avatar
chx committed
199

200
201
202
  /**
   * Write the configuration data into the active storage and the file.
   */
chx's avatar
chx committed
203
  function write($data);
chx's avatar
chx committed
204

205
206
207
  /**
   * Get names starting with this prefix.
   */
chx's avatar
chx committed
208
  static function getNamesWithPrefix($prefix);
chx's avatar
chx committed
209
210
}

211
abstract class DrupalConfigVerifiedStorage implements DrupalConfigVerifiedStorageInterface {
David Strauss's avatar
David Strauss committed
212
  function __construct($name) {
chx's avatar
chx committed
213
214
215
216
    $this->name = $name;
  }

  protected function signedFileStorage() {
David Strauss's avatar
David Strauss committed
217
    return new SignedFileStorage($this->name);
chx's avatar
chx committed
218
219
  }

Crell's avatar
Crell committed
220
  public function copyTofile() {
221
    return $this->signedFileStorage()->write($this->read());
chx's avatar
chx committed
222
223
  }

Crell's avatar
Crell committed
224
  public function copyFromFile() {
chx's avatar
chx committed
225
226
227
    return $this->writeToActive($this->readFromFile());
  }

Crell's avatar
Crell committed
228
  public function readFromFile() {
chx's avatar
chx committed
229
    return $this->signedFileStorage()->read($this->name);
chx's avatar
chx committed
230
231
  }

Crell's avatar
Crell committed
232
  public function isOutOfSync() {
chx's avatar
chx committed
233
    return $this->read() !== $this->readFromFile();
chx's avatar
chx committed
234
235
  }

236
237
  public function write($data) {
    $this->writeToActive($data);
238
    $this->copyToFile();
239
240
241
242
243
  }
}

class DrupalVerifiedStorageSQL extends DrupalConfigVerifiedStorage {

David Strauss's avatar
David Strauss committed
244
  public function read() {
David Strauss's avatar
David Strauss committed
245
    return db_query('SELECT data FROM {config} WHERE name = :name', array(':name' => $this->name))->fetchField();
246
247
  }

Crell's avatar
Crell committed
248
  public function writeToActive($data) {
chx's avatar
chx committed
249
    return db_merge('config')
David Strauss's avatar
David Strauss committed
250
      ->key(array('name' => $this->name))
chx's avatar
chx committed
251
      ->fields(array('data' => $data))
chx's avatar
chx committed
252
253
254
      ->execute();
  }

David Strauss's avatar
David Strauss committed
255
  static public function getNamesWithPrefix($prefix = '') {
256
    return db_query('SELECT name FROM {config} WHERE name LIKE :name', array(':name' => db_like($prefix) . '%'))->fetchCol();
chx's avatar
chx committed
257
258
259
  }
}

260
function config_get_verified_storage_names_with_prefix($prefix = '') {
David Strauss's avatar
David Strauss committed
261
  return DrupalVerifiedStorageSQL::getNamesWithPrefix($prefix);
chx's avatar
chx committed
262
}
David Strauss's avatar
David Strauss committed
263

chx's avatar
chx committed
264
function config_get_names_with_prefix($prefix) {
265
  return config_get_verified_storage_names_with_prefix($prefix);
chx's avatar
chx committed
266
267
}

chx's avatar
chx committed
268
function config($name, $class = 'DrupalConfig') {
David Strauss's avatar
David Strauss committed
269
270
  static $overrides;
  if (!isset($overrides)) {
271
    $storage = new SignedFileStorage('local');
272
    $overrides = (array) config_decode($storage->read());
David Strauss's avatar
David Strauss committed
273
274
  }
  $key_overrides = isset($overrides[$name]) ? $overrides[$name] : array();
Crell's avatar
Crell committed
275
  // @TODO Replace this with the appropriate factory.
276
  return new $class(new DrupalVerifiedStorageSQL($name), $key_overrides);
chx's avatar
chx committed
277
278
}

279
280
281
282
283
284
285
286
287
/**
 * Decode configuration data from its native format to an associative array.
 *
 * @param $data
 *   Configuration data
 * @return
 *   An associative array representation of the data.
 */
function config_decode($data) {
288
289
290
291
292
  if (empty($data)) {
    return array();
  }
  print_r(debug_backtrace());
  print $data;
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
  $xml = new SimpleXMLElement($data);
  $json = json_encode($xml);
  return json_decode($json);
}

/**
 * Encode an array into the native configuration format.
 *
 * @param $data
 *   An associative array or an object
 * @return
 *   A representation of this array or object in the native configuration
 *   format. 
 * @todo
 *   This needs to work for objects as well and currently doesn't
 */
309
function config_encode($data) {
310
	return config_array_to_xml($data);
311
312
}

313
314
315
316
317
318
319
320
321
322
/**
 * Encode an array or object into the native configuration format.
 *
 * @param $data
 *   An associative array or an object
 * @return
 *   A representation of this array or object in the native configuration
 *   format. 
 */
function config_array_to_xml($arr, $tab_count = 0) {
323
  $xml = '<?xml version="1.0" encoding="UTF8"?>' . PHP_EOL . '<data>';
324
325
326
327
328
329
330
331
332
  foreach ($arr as $tag => $val) { 
    if (!is_array($val)) { 
      $xml .= PHP_EOL . '<' . $tag . '>' . htmlentities($val) . '</' . $tag . '>'; 
    } else { 
      $tab_count++; 
      $xml .= PHP_EOL . '<' . $tag . '>' . config_array_to_xml($val, $tab_count); 
      $xml .= PHP_EOL . '</' . $tag . '>'; 
    } 
  } 
333
  $xml .= PHP_EOL . '</data>';
334
	return $xml; 
335
336
}

337
338
class DrupalConfig {

Crell's avatar
Crell committed
339
340
341
342
343
344
  /**
   * The storage engine to save this config object to.
   *
   * @var DrupalConfigVerifiedStorageInterface
   */
  protected $_verifiedStorage;
345

Crell's avatar
Crell committed
346
347
  protected $_overrides;

David Strauss's avatar
David Strauss committed
348
  public function __construct(DrupalConfigVerifiedStorageInterface $verified_storage, $overrides = array()) {
Crell's avatar
Crell committed
349
    $this->_verifiedStorage = $verified_storage;
350
    $original_keys = (array) config_decode($this->_verifiedStorage->read());
Crell's avatar
Crell committed
351
    $this->_overrides = $overrides;
David Strauss's avatar
David Strauss committed
352
    $active = array_merge($original_keys, $overrides);
353
    foreach ($active as $key => $value) {
354
355
356
357
      $this->$key = $value;
    }
  }

Crell's avatar
Crell committed
358
359
360
361
  public function isOverridden($key) {
    return isset($this->_overrides[$key]);
  }

chx's avatar
chx committed
362
  public function save() {
Crell's avatar
Crell committed
363
364
365
366
367
368
    $obj = new stdClass();
    foreach (get_object_vars($this) as $key => $val) {
      if ($key[0] != '_') {
        $obj->$key = $val;
      }
    }
369
    $this->_verifiedStorage->write(config_encode($obj));
370
  }
chx's avatar
chx committed
371
}