Project

General

Profile

Paste
Download (17.4 KB) Statistics
| Branch: | Revision:

root / drupal7 / sites / all / modules / ctools / includes / css.inc @ 560c3060

1
<?php
2

    
3
/*
4
 * @file
5
 * CSS filtering functions. Contains a disassembler, filter, compressor, and
6
 * decompressor.
7
 *
8
 * The general usage of this tool is:
9
 *
10
 * To simply filter CSS:
11
 * @code
12
 *   $filtered_css = ctools_css_filter($css, TRUE);
13
 * @endcode
14
 *
15
 * In the above, if the second argument is TRUE, the returned CSS will
16
 * be compressed. Otherwise it will be returned in a well formatted
17
 * syntax.
18
 *
19
 * To cache unfiltered CSS in a file, which will be filtered:
20
 *
21
 * @code
22
 *   $filename = ctools_css_cache($css, TRUE);
23
 * @endcode
24
 *
25
 * In the above, if the second argument is FALSE, the CSS will not be filtered.
26
 *
27
 * This file will be cached within the Drupal files system. This system cannot
28
 * detect when this file changes, so it is YOUR responsibility to remove and
29
 * re-cache this file when the CSS is changed. Your system should also contain
30
 * a backup method of re-generating the CSS cache in case it is removed, so
31
 * that it is easy to force a re-cache by simply deleting the contents of the
32
 * directory.
33
 *
34
 * Finally, if for some reason your application cannot store the filename
35
 * (which is true of Panels where the style can't force the display to
36
 * resave unconditionally) you can use the ctools storage mechanism. You
37
 * simply have to come up with a unique Id:
38
 *
39
 * @code
40
 *   $filename = ctools_css_store($id, $css, TRUE);
41
 * @endcode
42
 *
43
 * Then later on:
44
 * @code
45
 *   $filename = ctools_css_retrieve($id);
46
 *   drupal_add_css($filename);
47
 * @endcode
48
 *
49
 * The CSS that was generated will be stored in the database, so even if the
50
 * file was removed the cached CSS will be used. If the CSS cache is
51
 * cleared you may be required to regenerate your CSS. This will normally
52
 * only be cleared by an administrator operation, not during normal usage.
53
 *
54
 * You may remove your stored CSS this way:
55
 *
56
 * @code
57
 *   ctools_css_clear($id);
58
 * @endcode
59
 */
60

    
61
/**
62
 * Store CSS with a given id and return the filename to use.
63
 *
64
 * This function associates a piece of CSS with an id, and stores the
65
 * cached filename and the actual CSS for later use with
66
 * ctools_css_retrieve.
67
 */
68
function ctools_css_store($id, $css, $filter = TRUE) {
69
  $filename = db_query('SELECT filename FROM {ctools_css_cache} WHERE cid = :cid', array(':cid' => $id))->fetchField();
70
  if ($filename && file_exists($filename)) {
71
    file_unmanaged_delete($filename);
72
  }
73
  // Remove any previous records.
74
  db_delete('ctools_css_cache')
75
    ->condition('cid', $id)
76
    ->execute();
77

    
78
  $filename = ctools_css_cache($css, $filter);
79

    
80
  db_merge('ctools_css_cache')
81
    ->key(array('cid' => $id))
82
    ->fields(array(
83
      'filename' => $filename,
84
      'css' => $css,
85
      'filter' => intval($filter),
86
    ))
87
    ->execute();
88

    
89
  return $filename;
90
}
91

    
92
/**
93
 * Retrieve a filename associated with an id of previously cached CSS.
94
 *
95
 * This will ensure the file still exists and, if not, create it.
96
 */
97
function ctools_css_retrieve($id) {
98
  $cache = db_query('SELECT * FROM {ctools_css_cache} WHERE cid = :cid', array(':cid' => $id))->fetchObject();
99
  if (!$cache) {
100
    return;
101
  }
102

    
103
  if (!file_exists($cache->filename)) {
104
    $filename = ctools_css_cache($cache->css, $cache->filter);
105
    if ($filename != $cache->filename) {
106
      db_update('ctools_css_cache')
107
        ->fields(array('filename' => $filename))
108
        ->condition('cid', $id)
109
        ->execute();
110
      $cache->filename = $filename;
111
    }
112
  }
113

    
114
  return $cache->filename;
115
}
116

    
117
/**
118
 * Remove stored CSS and any associated file.
119
 */
120
function ctools_css_clear($id) {
121
  $cache = db_query('SELECT * FROM {ctools_css_cache} WHERE cid = :cid', array(':cid' => $id))->fetchObject();
122
  if (!$cache) {
123
    return;
124
  }
125

    
126
  if (file_exists($cache->filename)) {
127
    file_unmanaged_delete($cache->filename);
128
    // If we remove an existing file, there may be cached pages that refer
129
    // to it. We must get rid of them: FIXME same format in D7?
130
    cache_clear_all();
131
  }
132

    
133
  db_delete('ctools_css_cache')
134
    ->condition('cid', $id)
135
    ->execute();
136
}
137

    
138
/**
139
 * Write a chunk of CSS to a temporary cache file and return the file name.
140
 *
141
 * This function optionally filters the CSS (always compressed, if so) and
142
 * generates a unique filename based upon md5. It returns that filename that
143
 * can be used with drupal_add_css(). Note that as a cache file, technically
144
 * this file is volatile so it should be checked before it is used to ensure
145
 * that it exists.
146
 *
147
 * You can use file_exists() to test for the file and file_delete() to remove
148
 * it if it needs to be cleared.
149
 *
150
 * @param $css
151
 *   A chunk of well-formed CSS text to cache.
152
 * @param $filter
153
 *   If TRUE the css will be filtered. If FALSE the text will be cached
154
 *   as-is.
155
 *
156
 * @return $filename
157
 *   The filename the CSS will be cached in.
158
 */
159
function ctools_css_cache($css, $filter = TRUE) {
160
  if ($filter) {
161
    $css = ctools_css_filter($css);
162
  }
163

    
164
  // Create the css/ within the files folder.
165
  $path = 'public://ctools/css';
166
  if (!file_prepare_directory($path, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
167
//  if (!file_prepare_directory($path, FILE_CREATE_DIRECTORY)) {
168
    drupal_set_message(t('Unable to create CTools CSS cache directory. Check the permissions on your files directory.'), 'error');
169
    return;
170
  }
171

    
172
  // @todo Is this slow? Does it matter if it is?
173
  $filename = $path . '/' . md5($css) . '.css';
174

    
175
  // Generally md5 is considered unique enough to sign file downloads.
176
  // So this replaces already existing files based on the assumption that two
177
  // files with the same hash are identical content wise.
178
  // If we rename, the cache folder can potentially fill up with thousands of
179
  // files with the same content.
180
  $filename = file_unmanaged_save_data($css, $filename, FILE_EXISTS_REPLACE);
181

    
182
  return $filename;
183
}
184

    
185
/**
186
 * Filter a chunk of CSS text.
187
 *
188
 * This function disassembles the CSS into a raw format that makes it easier
189
 * for our tool to work, then runs it through the filter and reassembles it.
190
 * If you find that you want the raw data for some reason or another, you
191
 * can use the disassemble/assemble functions yourself.
192
 *
193
 * @param $css
194
 *   The CSS text to filter.
195
 * @param $compressed
196
 *   If true, generate compressed output; if false, generate pretty output.
197
 *   Defaults to TRUE.
198
 */
199
function ctools_css_filter($css, $compressed = TRUE) {
200
  $css_data = ctools_css_disassemble($css);
201

    
202
  // Note: By using this function yourself you can control the allowed
203
  // properties and values list.
204
  $filtered = ctools_css_filter_css_data($css_data);
205

    
206
  return $compressed ? ctools_css_compress($filtered) : ctools_css_assemble($filtered);
207
}
208

    
209
/**
210
 * Re-assemble a css string and format it nicely.
211
 *
212
 * @param array $css_data
213
 *   An array of css data, as produced by @see ctools_css_disassemble()
214
 *   disassembler and the @see ctools_css_filter_css_data() filter.
215
 *
216
 * @return string $css
217
 *   css optimized for human viewing.
218
 */
219
function ctools_css_assemble($css_data) {
220
  // Initialize the output.
221
  $css = '';
222
  // Iterate through all the statements.
223
  foreach ($css_data as $selector_str => $declaration) {
224
    // Add the selectors, separating them with commas and line feeds.
225
    $css .= strpos($selector_str, ',') === FALSE ? $selector_str : str_replace(", ", ",\n", $selector_str);
226
    // Add the opening curly brace.
227
    $css .= " {\n";
228
    // Iterate through all the declarations.
229
    foreach ($declaration as $property => $value) {
230
      $css .= "  " . $property . ": " . $value . ";\n";
231
    }
232
    // Add the closing curly brace.
233
    $css .= "}\n\n";
234
  }
235
  // Return the output.
236
  return $css;
237
}
238

    
239
/**
240
 * Compress css data (filter it first!) to optimize for use on view.
241
 *
242
 * @param array $css_data
243
 *   An array of css data, as produced by @see ctools_css_disassemble()
244
 *   disassembler and the @see ctools_css_filter_css_data() filter.
245
 *
246
 * @return string $css
247
 *   css optimized for use.
248
 */
249
function ctools_css_compress($css_data) {
250
  // Initialize the output.
251
  $css = '';
252
  // Iterate through all the statements.
253
  foreach ($css_data as $selector_str => $declaration) {
254
    if (empty($declaration)) {
255
      // Skip this statement if filtering removed all parts of the declaration.
256
      continue;
257
    }
258
    // Add the selectors, separating them with commas.
259
    $css .= $selector_str;
260
    // And, the opening curly brace.
261
    $css .= "{";
262
    // Iterate through all the statement properties.
263
    foreach ($declaration as $property => $value) {
264
      $css .= $property . ':' . $value . ';';
265
    }
266
    // Add the closing curly brace.
267
    $css .= "}";
268
  }
269
  // Return the output.
270
  return $css;
271
}
272

    
273
/**
274
 * Disassemble the css string.
275
 *
276
 * Strip the css of irrelevant characters, invalid/malformed selectors and
277
 * declarations, and otherwise prepare it for processing.
278
 *
279
 * @param string $css
280
 *   A string containing the css to be disassembled.
281
 *
282
 * @return array $disassembled_css
283
 *   An array of disassembled, slightly cleaned-up/formatted css statements.
284
 */
285
function ctools_css_disassemble($css) {
286
  $disassembled_css = array();
287
  // Remove comments.
288
  $css = preg_replace("/\/\*(.*)?\*\//Usi", "", $css);
289
  // Split out each statement. Match either a right curly brace or a semi-colon
290
  // that precedes a left curly brace with no right curly brace separating them.
291
  $statements = preg_split('/}|;(?=[^}]*{)/', $css);
292

    
293
  // If we have any statements, parse them.
294
  if (!empty($statements)) {
295
    // Iterate through all of the statements.
296
    foreach ($statements as $statement) {
297
      // Get the selector(s) and declaration.
298
      if (empty($statement) || !strpos($statement, '{')) {
299
        continue;
300
      }
301

    
302
      list($selector_str, $declaration) = explode('{', $statement);
303

    
304
      // If the selector exists, then disassemble it, check it, and regenerate
305
      // the selector string.
306
      $selector_str = empty($selector_str) ? FALSE : _ctools_css_disassemble_selector($selector_str);
307
      if (empty($selector_str)) {
308
        // No valid selectors. Bomb out and start the next item.
309
        continue;
310
      }
311

    
312
      // Disassemble the declaration, check it and tuck it into an array.
313
      if (!isset($disassembled_css[$selector_str])) {
314
        $disassembled_css[$selector_str] = array();
315
      }
316
      $disassembled_css[$selector_str] += _ctools_css_disassemble_declaration($declaration);
317
    }
318
  }
319
  return $disassembled_css;
320
}
321

    
322
function _ctools_css_disassemble_selector($selector_str) {
323
  // Get all selectors individually.
324
  $selectors = explode(",", trim($selector_str));
325
  // Iterate through all the selectors, sanity check them and return if they
326
  // pass. Note that this handles 0, 1, or more valid selectors gracefully.
327
  foreach ($selectors as $key => $selector) {
328
    // Replace un-needed characters and do a little cleanup.
329
    $selector = preg_replace("/[\n|\t|\\|\s]+/", ' ', trim($selector));
330
    // Make sure this is still a real selector after cleanup.
331
    if (!empty($selector)) {
332
      $selectors[$key] = $selector;
333
    }
334
    else {
335
      // Selector is no good, so we scrap it.
336
      unset($selectors[$key]);
337
    }
338
  }
339
  // Check for malformed selectors; if found, we skip this declaration.
340
  if (empty($selectors)) {
341
    return FALSE;
342
  }
343
  return implode(', ', $selectors);
344
}
345

    
346
function _ctools_css_disassemble_declaration($declaration) {
347
  $formatted_statement = array();
348
  $propval_pairs = explode(";", $declaration);
349
  // Make sure we actually have some properties to work with.
350
  if (!empty($propval_pairs)) {
351
    // Iterate through the remains and parse them.
352
    foreach ($propval_pairs as $key => $propval_pair) {
353
      // Check that we have a ':', otherwise it's an invalid pair.
354
      if (strpos($propval_pair, ':') === FALSE) {
355
        continue;
356
      }
357
      // Clean up the current property-value pair.
358
      $propval_pair = preg_replace("/[\n|\t|\\|\s]+/", ' ', trim($propval_pair));
359
      // Explode the remaining fragements some more, but clean them up first.
360
      list($property, $value) = explode(':', $propval_pair, 2);
361
      // If the property survived, toss it onto the stack.
362
      if (!empty($property)) {
363
        $formatted_statement[trim($property)] = trim($value);
364
      }
365
    }
366
  }
367
  return $formatted_statement;
368
}
369

    
370
/**
371
 * Run disassembled $css through the filter.
372
 *
373
 * @param $css
374
 *   CSS code disassembled by ctools_dss_disassemble().
375
 * @param $allowed_properties
376
 *   A list of properties that are allowed by the filter. If empty
377
 *   ctools_css_filter_default_allowed_properties() will provide the
378
 *   list.
379
 * @param $allowed_values
380
 *   A list of values that are allowed by the filter. If empty
381
 *   ctools_css_filter_default_allowed_values() will provide the
382
 *   list.
383
 *
384
 * @return
385
 *   An array of disassembled, filtered CSS.
386
 */
387
function ctools_css_filter_css_data($css, $allowed_properties = array(), $allowed_values = array(), $allowed_values_regex = '', $disallowed_values_regex = '') {
388
//function ctools_css_filter_css_data($css, &$filtered = NULL, $allowed_properties = array(), $allowed_values = array(), $allowed_values_regex = '', $disallowed_values_regex = '') {
389
  // Retrieve the default list of allowed properties if none is provided.
390
  $allowed_properties = !empty($allowed_properties) ? $allowed_properties : ctools_css_filter_default_allowed_properties();
391
  // Retrieve the default list of allowed values if none is provided.
392
  $allowed_values = !empty($allowed_values) ? $allowed_values : ctools_css_filter_default_allowed_values();
393
  // Define allowed values regex if none is provided.
394
  $allowed_values_regex = !empty($allowed_values_regex) ? $allowed_values_regex : '/(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)/';
395
  // Define disallowed url() value contents, if none is provided.
396
  // $disallowed_values_regex = !empty($disallowed_values_regex) ? $disallowed_values_regex : '/[url|expression]\s*\(\s*[^\s)]+?\s*\)\s*/';
397
  $disallowed_values_regex = !empty($disallowed_values_regex) ? $disallowed_values_regex : '/(url|expression)/';
398

    
399
  foreach ($css as $selector_str => $declaration) {
400
    foreach ($declaration as $property => $value) {
401
      if (!in_array($property, $allowed_properties)) {
402
        // $filtered['properties'][$selector_str][$property] = $value;
403
        unset($css[$selector_str][$property]);
404
        continue;
405
      }
406
      $value = str_replace('!important', '', $value);
407
      if (preg_match($disallowed_values_regex, $value) || !(in_array($value, $allowed_values) || preg_match($allowed_values_regex, $value))) {
408
        // $filtered['values'][$selector_str][$property] = $value;
409
        unset($css[$selector_str][$property]);
410
        continue;
411
      }
412
    }
413
  }
414
  return $css;
415
}
416

    
417
/**
418
 * Provide a deafult list of allowed properties by the filter.
419
 */
420
function ctools_css_filter_default_allowed_properties() {
421
  return array(
422
    'azimuth',
423
    'background',
424
    'background-color',
425
    'background-image',
426
    'background-repeat',
427
    'background-attachment',
428
    'background-position',
429
    'border',
430
    'border-top-width',
431
    'border-right-width',
432
    'border-bottom-width',
433
    'border-left-width',
434
    'border-width',
435
    'border-top-color',
436
    'border-right-color',
437
    'border-bottom-color',
438
    'border-left-color',
439
    'border-color',
440
    'border-top-style',
441
    'border-right-style',
442
    'border-bottom-style',
443
    'border-left-style',
444
    'border-style',
445
    'border-top',
446
    'border-right',
447
    'border-bottom',
448
    'border-left',
449
    'clear',
450
    'color',
451
    'cursor',
452
    'direction',
453
    'display',
454
    'elevation',
455
    'float',
456
    'font',
457
    'font-family',
458
    'font-size',
459
    'font-style',
460
    'font-variant',
461
    'font-weight',
462
    'height',
463
    'letter-spacing',
464
    'line-height',
465
    'margin',
466
    'margin-top',
467
    'margin-right',
468
    'margin-bottom',
469
    'margin-left',
470
    'overflow',
471
    'padding',
472
    'padding-top',
473
    'padding-right',
474
    'padding-bottom',
475
    'padding-left',
476
    'pause',
477
    'pause-after',
478
    'pause-before',
479
    'pitch',
480
    'pitch-range',
481
    'richness',
482
    'speak',
483
    'speak-header',
484
    'speak-numeral',
485
    'speak-punctuation',
486
    'speech-rate',
487
    'stress',
488
    'text-align',
489
    'text-decoration',
490
    'text-indent',
491
    'text-transform',
492
    'unicode-bidi',
493
    'vertical-align',
494
    'voice-family',
495
    'volume',
496
    'white-space',
497
    'width',
498
    'fill',
499
    'fill-opacity',
500
    'fill-rule',
501
    'stroke',
502
    'stroke-width',
503
    'stroke-linecap',
504
    'stroke-linejoin',
505
    'stroke-opacity',
506
  );
507
}
508

    
509
/**
510
 * Provide a default list of allowed values by the filter.
511
 */
512
function ctools_css_filter_default_allowed_values() {
513
  return array(
514
    'auto',
515
    'aqua',
516
    'black',
517
    'block',
518
    'blue',
519
    'bold',
520
    'both',
521
    'bottom',
522
    'brown',
523
    'capitalize',
524
    'center',
525
    'collapse',
526
    'dashed',
527
    'dotted',
528
    'fuchsia',
529
    'gray',
530
    'green',
531
    'italic',
532
    'inherit',
533
    'left',
534
    'lime',
535
    'lowercase',
536
    'maroon',
537
    'medium',
538
    'navy',
539
    'normal',
540
    'nowrap',
541
    'olive',
542
    'pointer',
543
    'purple',
544
    'red',
545
    'right',
546
    'solid',
547
    'silver',
548
    'teal',
549
    'top',
550
    'transparent',
551
    'underline',
552
    'uppercase',
553
    'white',
554
    'yellow',
555
  );
556
}
557

    
558
/**
559
 * Delegated implementation of hook_flush_caches()
560
 */
561
function ctools_css_flush_caches() {
562
  // Remove all generated files.
563
  // @see http://drupal.org/node/573292
564
  // file_unmanaged_delete_recursive('public://render');
565
  $filedir = file_default_scheme() . '://ctools/css';
566
  if (drupal_realpath($filedir) && file_exists($filedir)) {
567
    // We use the @ because it's possible that files created by the webserver
568
    // cannot be deleted while using drush to clear the cache. We don't really
569
    // care that much about that, to be honest, so we use the @ to suppress
570
    // the error message.
571
    @file_unmanaged_delete_recursive($filedir);
572
  }
573

    
574
  db_delete('ctools_css_cache')->execute();
575
}