3 * @file a formatter that runs given XML content through a defined
4 * XSL stylesheet before rendering.
6 * When enabled, this provides an additional field formatter available
7 * through 'manage display' for use entity displays or views.
8 * The data source should be a textarea witch contains the raw XML
9 * and configuration options on the display widget will allow you to
10 * define the XSL that should be run over it.
12 * The textarea should contain valid XML, and the XSL should produce an HTML
13 * snippet, as the result will be displayed inline in the page.
15 * This process can ALSO run identically over a link field.
16 * Add a 'link' field to your page, and choose 'Transformed by XSL' as the
18 * On each request, that file will be fetched, parsed, transformed and
24 * Built with *some* comparison to
26 * http://drupal.org/project/cck_xslt D6 unreleased
27 * - which made its own field and data storage instead of re-using a text field.
28 * - Has the XSL selection a per-node choice, not a field formatter.
29 * - did demonstrate how easy it was to extend to reading an URL instead of text.
31 * http://drupal.org/project/feeds_xsltparser D7 dev
32 * - Not incredibly similar, but nice to see!
34 * http://drupal.org/node/1476774 "XML Transform" Never released
35 * - Uses the Drupal text filter system to process a textarea with XSL.
36 * - Actually the first way I started thinking about it, as I've done multiple
37 * XML-based text filters already.
40 * @author Dan Morrison (dman) dan@coders.co.nz
41 * @version 2012-11-27 (1:00AM -3:30AM)
46 * Implements hook_field_formatter_info().
48 * Declares the existance of this formatter.
49 * We can do similar things to local textareas, remote URLs, or uploaded files!
51 function xsl_formatter_field_formatter_info() {
53 'xsl_formatter' => array(
54 'label' => t('Transformed by XSL'),
55 'field types' => array('text_long', 'link_field', 'file'),
57 'xsl_path' => 'xsl/xmlverbatim.xsl',
66 * Implements hook_field_formatter_settings_summary().
68 * Summarizes the settings for display on the UI.
70 function xsl_formatter_field_formatter_settings_summary($field, $instance, $view_mode) {
71 $display = $instance['display'][$view_mode];
72 $settings = $display['settings'];
73 return t('XSL process, using : @xsl_filename', array('@xsl_filename' => basename($settings['xsl_path'])));
77 * Implements hook_field_formatter_settings_form().
79 * Settings for the display options.
81 function xsl_formatter_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
82 $display = $instance['display'][$view_mode];
83 $settings = $display['settings'];
85 // Originally text, try a drop-down instead
87 $element['xsl_path'] = array(
88 '#title' => t('XSL path'),
89 '#type' => 'textfield',
90 '#default_value' => $settings['xsl_path'],
91 '#element_validate' => array('xsl_formatter_xsl_path_validate'),
92 '#description' => t("Path to the location of the XSL file. Search will be made relative to the module directory, the site directory and the file directory. eg <code>xsl/xmlverbatimwrapper.xsl</code>, <code>xsl/prettyprint.xsl</code>"),
93 '#autocomplete_path' => 'admin/xsl_path'
96 $xsls = xsl_formatter_enumerate_xsls();
97 $element['xsl_path'] = array(
98 '#title' => t('XSL path'),
100 '#default_value' => $settings['xsl_path'],
101 '#element_validate' => array('xsl_formatter_xsl_path_validate'),
102 '#description' => t("Path to the location of the XSL file. Search will be made relative to the files/xsl directory, then the module directory."),
106 // file upload needs an explicit name. This is horrid sorry
107 $upload_field_id = 'files[' .
drupal_clean_css_identifier("files[fields][{$instance['field_name']}][settings_edit_form][settings][xsl_upload]") .
']';
108 $element['xsl_upload'] = array(
110 '#title' => t('Upload XSL file'),
112 '#description' => t("This will be placed in your files/xsl folder where it can be found and re-used."),
113 '#element_validate' => array('xsl_formatter_xsl_upload_validate'),
114 '#name' => $upload_field_id,
116 $module_path = drupal_get_path('module', 'xsl_formatter');
117 $element['xsl_params'] = array(
118 '#title' => t('Additional params'),
119 '#type' => 'textarea',
122 '#description' => t("Additional parameters that the Transformation stylesheet may expect. Use JSON format, eg <pre>{\"indent-elements\":true, \"css-stylesheet\":\"$module_path/xsl/xmlverbatim.css\"}</pre>"),
123 '#default_value' => $settings['xsl_params'],
124 '#element_validate' => array('xsl_formatter_xsl_params_validate'),
126 $element['debug'] = array(
127 '#title' => t('Show XML parsing warnings'),
128 '#type' => 'checkbox',
129 '#description' => t("Bad XML data input will trigger warnings that may show on screen. Disable this for a public site."),
130 '#default_value' => $settings['debug'],
136 * Ensure the named path exists. This includes a small search lookup.
138 function xsl_formatter_xsl_path_validate($element, &$form_state, $form) {
140 $xsl_doc = xsl_formatter_get_xml_doc($element['#value']);
141 } catch (Exception
$e) {
142 $element_id = join('][', $element['#parents']);
143 form_set_error($element_id, $e->getMessage());
148 * Ensure the params are valid. Checks that the JSON parses into something.
150 function xsl_formatter_xsl_params_validate($element, &$form_state, $form) {
151 // json_decode doesn't throw many parse errors, so look at the results.
152 $value = trim($element['#value']);
153 $params = json_decode($value);
154 if (!empty($value) && $params == NULL
) {
155 // This means some sort of failure
156 $element_id = join('][', $element['#parents']);
157 form_set_error($element_id, 'Failed to parse the JSON. Check your syntax. You must quote all strings with double-quotes.');
162 * If we upload our own xsl, Make sure it gets saved.
164 * Place it in the public xsl foilder and refer to it.
166 function xsl_formatter_xsl_upload_validate($element, &$form_state, $form) {
167 // Check for a new uploaded xsl.
168 // Figure out what the big ID was. This is wierd.
169 $upload_field_id = 'files-' .
substr($element['#id'], strlen('edit-'));
171 // Get it. Temporary at first.
172 $validators = array('file_validate_extensions' => array('xsl','xslt'));
173 $file = file_save_upload($upload_field_id, $validators);
176 // File upload was attempted.
178 $save_dir = "public://xsl";
179 file_prepare_directory($save_dir, FILE_CREATE_DIRECTORY
);
180 $save_filepath = $save_dir .
'/' .
$file->filename
;
181 $filename = file_unmanaged_copy($file->uri
, $save_filepath, FILE_EXISTS_REPLACE
);
183 // Set xsl_path to the newly uploaded value.
184 // The #parents array is important.
185 // Find the nearby xsl_path element with the same ancestry as me.
186 $parents = $element['#parents'];
188 array_push($parents, 'xsl_path');
189 $xsl_path_element = array('#parents' => $parents);
190 form_set_value($xsl_path_element, $save_filepath, $form_state);
193 // File upload failed.
194 form_set_error('xsl_upload', t('The xsl could not be uploaded.'));
201 * Implements hook_field_formatter_view().
203 * Does the process here, to generate the result.
204 * Delegates the final layout to the theme func
206 function xsl_formatter_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
208 foreach ($items as
$delta => $item) {
210 $result = "Can't parse the XML input";
211 $data = @
$item['value'];
212 $xml_doc = new domdocument
;
214 // Alternate field types. Local data (valus) is easiest, but
215 // If the field type is a link, go get that data now
216 // DESPERATELY need caching or something here.
217 if ($field['type'] == 'link_field') {
218 $url = url($item['url'], $item);
219 $data = file_get_contents($url);
221 // Or files, why not?
222 if ($field['type'] == 'file') {
224 # $url = url($item['url'], $item);
225 $data = file_get_contents($item['uri']);
229 // Tricky to catch errors.
230 // Do this to toggle debug mode.
231 if (!empty($display['settings']['debug'])) {
232 // Warnings may go to the screen.
233 $xml_doc->loadXML($data);
236 // Suppress warnings.
237 @
$xml_doc->loadXML($data);
240 // XML Loaded OK. Now load the stylesheet.
241 $xsl_path = $display['settings']['xsl_path'];
242 $xsl_doc = xsl_formatter_get_xml_doc($xsl_path);
243 // Pass through any params that the XSLT may want.
244 $params = (array)json_decode($display['settings']['xsl_params']);
245 // 'base' can be used for supporting relative css links.
246 $params['base'] = url(dirname($xsl_doc->documenturi
));
248 $result = xsl_formatter_xmldoc_plus_xsldoc($xml_doc, $xsl_doc, $params);
250 catch (Exception
$e) {
251 throw new
Exception("Unable to parse the data. Probably invalid XML.", E_USER_ERROR
);
254 $element[$delta] = array(
255 '#theme' => 'xsl_formatter',
257 '#settings' => $display['settings'],
258 '#result' => $result,
266 * Implements hook_theme().
268 * Advertises our theme function.
270 function xsl_formatter_theme() {
272 'xsl_formatter' => array(
273 'variables' => array(
283 * Returns HTML from passing the input through the XSL process
286 * An associative array containing:
287 * - item: An array of field data.
288 * - settings: used to do the transform, including the xsl path
289 * - result: the rendered result.
293 function theme_xsl_formatter($variables) {
294 // The data is already cooked, just use this theme func to stick a
295 // wrapper around it if you want.
296 return $variables['result'];
300 * Return a list of xsl files, as found in the search locations
301 * Over-engineerd for now, anticipating 'module:// as a file scheme
302 * and keeping the UI simpler (?)
304 function xsl_formatter_enumerate_xsls() {
305 $paths = array('public://xsl', 'module://xsl_formatter/xsl');
307 foreach ($paths as
$base) {
310 // Just me being cute here...
311 if (file_uri_scheme($base) == 'module') {
312 $target_path = file_uri_target($base);
313 $split_path = explode('/', $target_path);
314 $module_name = array_shift($split_path);
315 $base = drupal_get_path('module' , $module_name) .
'/'.
join('/', $split_path);
318 $files = file_scan_directory($base, '/.*\.xsl[t]?/');
319 foreach ($files as
$file) {
320 $found[$file->uri
] = $label .
'/'.
$file->filename
;
326 /////////////////////////
327 // XML utilities below.
328 // Ultra-paranoid and layered with pessimism.
329 // Because XML always goes wrong.
332 * Find and initialize the transformation template.
334 * LOTS of error checking.
336 * Allows you to define the path relative to the module, the site,
339 * Includes caching retrieval for a bit of speed-up over bulks.
340 * XSL is expensive, so if we find ourselves doing it more than once,
341 * the parsed file is retained for next time.
343 * Throws an exception if anything goes wrong.
345 * @return XML Document
347 function xsl_formatter_get_xml_doc($xml_file, $description = "XML file") {
348 // Check cache first-off.
350 if (isset($xmldocs[$xml_file])) {
351 return $xmldocs[$xml_file];
353 if (empty($xml_file)) {
354 throw new
Exception("Null $description? Cannot proceed", E_USER_ERROR
);
357 // Check if and where filepath can be found.
358 // Search first under full path, then under files dir, then module dir.
360 # TODO - check if this could be used as an attack vector?
361 # Sanitize the fetch path.
363 $xml_filepath = $xml_file;
364 if (!is_file($xml_filepath)) {
365 $xml_filepath = 'public://' .
$xml_file;
367 if (!is_file($xml_filepath)) {
368 $xml_filepath = drupal_get_path('module', 'xsl_formatter') .
"/$xml_file";
371 if (is_file($xml_filepath)) {
372 #watchdog(__FUNCTION__, "Loading $description from $xml_filepath", array(), WATCHDOG_DEBUG);
373 $xml_doc = new domdocument
;
374 if ($xml_doc->load($xml_filepath) ) {
379 $xmldocs[$xml_file] = FALSE
;
380 throw new
Exception("Unable to parse the $description '$xml_filepath' ", E_USER_ERROR
);
383 $xml_docs[$xml_file] = $xml_doc;
386 $xmldocs[$xml_file] = FALSE
;
387 throw new
Exception("Unable to locate the $description '$xml_file' ", E_USER_ERROR
);
390 // This is required/helpful to support relative includes
391 $xml_doc->documenturi
= $xml_filepath;
397 * Do the actual conversion between XML+XSL
399 * Input and output are full DOM objects in PHP5
400 * We return the result STRING, as that's what
401 * the process gives us :-/
402 * Need to parse it back in again for pipelining.
404 * Support for PHP4 XSL removed.
406 * @param domdocument or string $xmldoc
408 * @param domdocument or string $xsldoc . If it uses includes, the xsl must have
409 * had its documenturi set correctly prior to this, but it can be set in the
412 * @param array $parameters To be passed into the xslt_process()
414 * @returns string The result.
416 function xsl_formatter_xmldoc_plus_xsldoc($xml_doc, $xsl_doc, $parameters = array()) {
417 $xsltproc = new XSLTProcessor
;
419 // Attach the xsl rules.
420 $xsltproc->importStyleSheet($xsl_doc);
421 // Set any processing parameters and flags.
423 foreach ($parameters as
$param => $value) {
424 $xsltproc->setParameter("", $param, $value);
428 $out = $xsltproc->transformToXml($xml_doc);
430 if (function_exists('charset_decode_utf_8')) {
431 // I just CAN'T trust XML not to have squashed the entities into bytecodes.
432 // Expand them before returning or I can never trust that my result here
433 // is actually valid to put in anywhere else again.
434 return charset_decode_utf_8($out);