Projet

Général

Profil

Paste
Télécharger (17 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / ctools / includes / css.inc @ 7e72b748

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 bool $filter
153
 *   If TRUE the css will be filtered. If FALSE the text will be cached
154
 *   as-is.
155
 *
156
 * @return string
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
    drupal_set_message(t('Unable to create CTools CSS cache directory. Check the permissions on your files directory.'), 'error');
168
    return;
169
  }
170

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

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

    
181
  return $filename;
182
}
183

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

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

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

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

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

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

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

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

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

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

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

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

    
369
/**
370
 * Run disassembled $css through the filter.
371
 *
372
 * @param $css
373
 *   CSS code disassembled by ctools_dss_disassemble().
374
 * @param $allowed_properties
375
 *   A list of properties that are allowed by the filter. If empty
376
 *   ctools_css_filter_default_allowed_properties() will provide the
377
 *   list.
378
 * @param $allowed_values
379
 *   A list of values that are allowed by the filter. If empty
380
 *   ctools_css_filter_default_allowed_values() will provide the
381
 *   list.
382
 *
383
 * @return
384
 *   An array of disassembled, filtered CSS.
385
 */
386
function ctools_css_filter_css_data($css, $allowed_properties = array(), $allowed_values = array(), $allowed_values_regex = '', $disallowed_values_regex = '') {
387
  // Retrieve the default list of allowed properties if none is provided.
388
  $allowed_properties = !empty($allowed_properties) ? $allowed_properties : ctools_css_filter_default_allowed_properties();
389
  // Retrieve the default list of allowed values if none is provided.
390
  $allowed_values = !empty($allowed_values) ? $allowed_values : ctools_css_filter_default_allowed_values();
391
  // Define allowed values regex if none is provided.
392
  $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|%|,|\))?)/';
393
  // Define disallowed url() value contents, if none is provided.
394
  $disallowed_values_regex = !empty($disallowed_values_regex) ? $disallowed_values_regex : '/(url|expression)/';
395

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

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

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

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

    
571
  db_delete('ctools_css_cache')->execute();
572
}