summaryrefslogtreecommitdiff
path: root/includes/registry.inc
blob: a8a89f2179acb7e1000e2d9c2519330705834264 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
<?php
// $Id$

/**
 * @file
 * This file contains the code registry parser engine.
 */

/**
 * @defgroup registry Code registry
 * @{
 * The code registry engine.
 *
 * Drupal maintains an internal registry of all functions or classes in the
 * system, allowing it to lazy-load code files as needed (reducing the amount
 * of code that must be parsed on each request). The list of included files is
 * cached per menu callback for subsequent loading by the menu router. This way,
 * a given page request will have all the code it needs but little else, minimizing
 * time spent parsing unneeded code.
 */

/**
 * @see registry_rebuild.
 */
function _registry_rebuild() {

  // The registry serves as a central autoloader for all classes, including
  // the database query builders.  However, the registry rebuild process
  // requires write ability to the database, which means having access to the
  // query builders that require the registry in order to be loaded.  That
  // causes a fatal race condition.  Therefore we manually include the
  // appropriate query builders for the currently active database before the
  // registry rebuild process runs.
  $connection_info = Database::getConnectionInfo();
  $driver = $connection_info['default']['driver'];
  require_once DRUPAL_ROOT . '/includes/database/query.inc';
  require_once DRUPAL_ROOT . '/includes/database/select.inc';
  require_once DRUPAL_ROOT . '/includes/database/' . $driver . '/query.inc';

  // Reset the resources cache.
  _registry_get_resource_name();
  // Get the list of files we are going to parse.
  $files = array();
  foreach (module_rebuild_cache() as $module) {
    if ($module->status) {
      $dir = dirname($module->filename);
      foreach ($module->info['files'] as $file) {
        $files["$dir/$file"] = array('module' => $module->name, 'weight' => $module->weight);
      }
    }
  }
  foreach (file_scan_directory('includes', '/\.inc$/') as $filename => $file) {
    $files["$filename"] = array('module' => '', 'weight' => 0);
  }

  foreach (registry_get_parsed_files() as $filename => $file) {
    // Add the md5 to those files we've already parsed.
    if (isset($files[$filename])) {
      $files[$filename]['md5'] = $file['md5'];
    }
    else {
      // Flush the registry of resources in files that are no longer on disc
      // or don't belong to installed modules.
      db_delete('registry')
        ->condition('filename', $filename)
        ->execute();
      db_delete('registry_file')
        ->condition('filename', $filename)
        ->execute();
    }
  }
  $parsed_files = _registry_parse_files($files);

  $unchanged_resources = array();
  $lookup_cache = array();
  if ($cache = cache_get('lookup_cache', 'cache_registry')) {
    $lookup_cache = $cache->data;
  }
  foreach ($lookup_cache as $key => $file) {
    // If the file for this cached resource is carried over unchanged from
    // the last registry build, then we can safely re-cache it.
    if ($file && in_array($file, array_keys($files)) && !in_array($file, $parsed_files)) {
      $unchanged_resources[$key] = $file;
    }
  }
  _registry_check_code(REGISTRY_RESET_LOOKUP_CACHE);

  module_implements(MODULE_IMPLEMENTS_CLEAR_CACHE);
  cache_clear_all('*', 'cache_registry', TRUE);

  // We have some unchanged resources, warm up the cache - no need to pay
  // for looking them up again.
  if (count($unchanged_resources) > 0) {
    cache_set('lookup_cache', $unchanged_resources, 'cache_registry');
  }
}

/**
 * Return the list of files in registry_file
 */
function registry_get_parsed_files() {
  $files = array();
  // We want the result as a keyed array.
  $files = db_query("SELECT * FROM {registry_file}")->fetchAllAssoc('filename', PDO::FETCH_ASSOC);
  return $files;
}

/**
 * Parse all files that have changed since the registry was last built, and save their function and class listings.
 *
 * @param $files
 *  The list of files to check and parse.
 */
function _registry_parse_files($files) {
  $parsed_files = array();
  foreach ($files as $filename => $file) {
    $contents = file_get_contents($filename);
    $md5 = md5($contents);
    $new_file = !isset($file['md5']);
    if ($new_file || $md5 != $file['md5']) {
      $parsed_files[] = $filename;
      // We update the md5 after we've saved the files resources rather than here, so if we
      // don't make it through this rebuild, the next run will reparse the file.
      _registry_parse_file($filename, $contents, $file['module'], $file['weight']);
      $file['md5'] = $md5;
      db_merge('registry_file')
        ->key(array('filename' => $filename))
        ->fields(array('md5' => $md5))
        ->execute();
    }
  }
  return $parsed_files;
}

/**
 * Parse a file and save its function and class listings.
 *
 * @param $filename
 *  Name of the file we are going to parse.
 * @param $contents
 *  Contents of the file we are going to parse as a string.
 * @param $module
 *   (optional) Name of the module this file belongs to.
 * @param $weight
 *   (optional) Weight of the module.
 */
function _registry_parse_file($filename, $contents, $module = '', $weight = 0) {
  static $map = array(T_FUNCTION => 'function', T_CLASS => 'class', T_INTERFACE => 'interface');
  // Delete registry entries for this file, so we can insert the new resources.
  db_delete('registry')
    ->condition('filename', $filename)
    ->execute();
  $tokens = token_get_all($contents);
  while ($token = next($tokens)) {
    // Ignore all tokens except for those we are specifically saving.
    if (is_array($token) && isset($map[$token[0]])) {
      $type = $map[$token[0]];
      if ($resource_name = _registry_get_resource_name($tokens, $type)) {
        $suffix = '';
        // Collect the part of the function name after the module name,
        // so that we can query the registry for possible hook implementations.
        if ($type == 'function' && !empty($module)) {
          $n = strlen($module);
          if (substr($resource_name, 0, $n) == $module) {
            $suffix = substr($resource_name, $n + 1);
          }
        }
        $fields = array(
          'filename' => $filename,
          'module' => $module,
          'suffix' => $suffix,
          'weight' => $weight,
        );
        // Because some systems, such as cache, currently use duplicate function
        // names in separate files an insert query cannot be used here as it
        // would cause a key constraint violation.  Instead we use a merge query.
        // In practice this should not be an issue as those systems all initialize
        // pre-registry and therefore are never loaded by the registry so it
        // doesn't matter if those records in the registry table point to one
        // filename instead of another.
        // TODO: Convert this back to an insert query after all duplicate
        // function names have been purged from Drupal.
        db_merge('registry')
          ->key(array('name' => $resource_name, 'type' => $type))
          ->fields($fields)
          ->execute();

        // We skip the body because classes may contain functions.
        _registry_skip_body($tokens);
      }
    }
  }
}

/**
 * Derive the name of the next resource in the token stream.
 *
 * When called without arguments, it resets its static cache.
 *
 * @param $tokens
 *  The collection of tokens for the current file being parsed.
 * @param $type
 *  The human-readable token name, either: "function", "class", or "interface".
 * @return
 *  The name of the resource, or FALSE if the resource has already been processed.
 */
function _registry_get_resource_name(&$tokens = NULL, $type = NULL) {
  // Keep a running list of all resources we've saved so far, so that we never
  // save one more than once.
  static $resources;

  if (!isset($tokens)) {
    $resources = array();
    return;
  }
  // Determine the name of the resource.
  next($tokens); // Eat a space.
  $token = next($tokens);
  if ($token == '&') {
    $token = next($tokens);
  }
  $resource_name = $token[1];

  // Ensure that we never save it more than once.
  if (isset($resources[$type][$resource_name])) {
    return FALSE;
  }
  $resources[$type][$resource_name] = TRUE;

  return $resource_name;
}

/**
 * Skip the body of a code block, as defined by { and }.
 *
 * This function assumes that the body starts at the next instance
 * of { from the current position.
 *
 * @param $tokens
 */
function _registry_skip_body(&$tokens) {
  $num_braces = 1;

  $token = '';
  // Get to the first open brace.
  while ($token != '{' && ($token = next($tokens)));

  // Scan through the rest of the tokens until we reach the matching
  // end brace.
  while ($num_braces && ($token = next($tokens))) {
    if ($token == '{') {
      ++$num_braces;
    }
    elseif ($token == '}') {
      --$num_braces;
    }
  }
}

/**
 * @} End of "defgroup registry".
 */