-
Notifications
You must be signed in to change notification settings - Fork 4
/
xpath_field.module
612 lines (544 loc) · 19.5 KB
/
xpath_field.module
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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
<?php
/*
* Field API funcitons.
*/
/**
* Implements hook_field_info().
*
* Provides the description of the field.
*/
function xpath_field_field_info() {
return array(
'xpath_field_xpath' => array(
'label' => 'XPath Fragment',
'description' => t('Pick out a fragment of XML from an XML field instance.'),
'default_widget' => 'text_textfield',
'default_formatter' => 'xpath_field_safe',
'storage' => array(
'type' => 'xpath_field_storage',
'settings' => array(),
'module' => 'xpath_field',
'active' => 1,
),
),
);
}
/**
* Implementation of hook_field_create_field.
*
* Sets the field's storage backend to our custom, non-persisting, backend.
* @param Array $field
*/
function xpath_field_field_create_field($field) {
if ($field['type'] == 'xpath_field_xpath') {
$field['storage'] = array(
'type' => 'xpath_field_storage',
'settings' => array(),
'module' => 'xpath_field',
'active' => 1,
);
// Build record to update.
$record = array(
'id' => $field['id'],
'storage_type' => $field['storage']['type'],
'storage_module' => $field['storage']['module'],
'storage_active' => $field['storage']['active'],
);
// Update the field storage.
$primary_key = array('id');
drupal_write_record('field_config', $record, $primary_key);
}
}
/**
* Implements hook_field_update_field().
*/
function xpath_field_field_update_field($field) {
xpath_field_field_create_field($field);
}
/**
* Implementation of hook_storage_info
*
* This info isn't properly saved by Drupal when a new vield instance is created.
* @see xpath_field_field_create_field for a workaround.
*
* @return Array
*/
function xpath_field_field_storage_info() {
return array(
'xpath_field_storage' => array(
'label' => t('XPath derived field storage'),
'description' => t('Transparent storage of derived XPath fragments.'),
'settings' => array(),
),
);
}
function xpath_field_field_storage_query(EntityFieldQuery $query) {
}
/**
* Implementation of hook_field_storage_load().
*
* Loads the XML field that this field derives its data from and runs the XPath query
* to build the field data that attaches to an entity.
*
* @param string $entity_type
* @param array $entities
* @param int $age
* @param array $fields
* @param array $options
*/
function xpath_field_field_attach_load($entity_type, $entities, $age, $options) {
$load_current = $age == FIELD_LOAD_CURRENT;
$map = field_info_field_map();
foreach ($entities as $entity) {
$bundle_name = property_exists($entity, 'type') ? $entity->type : NULL;
$entity_info = field_info_instances($entity_type, $bundle_name);
foreach($entity as $field_name => $field) {
if (is_array($entity->{$field_name})
&& array_key_exists($field_name, $map)
&& $map[$field_name]['type'] == 'xpath_field_xpath') {
$fragments = xpath_field_extract_fragments($entity_type, $entity, $field_name, $field);
if (!empty($fragments)) {
// Add the item to the field values for the entity.
foreach ($fragments as $fragment_index => $fragment) {
$entity->{$field_name}[$entity->language][] = array('value' => $fragment);
}
}
}
}
}
}
function xpath_field_get_xml_data_from_field($xml_field_data, $settings) {
// XML FIeld puts its data in a key named 'xml'. All other supported
// fields store their data in 'value'.
return array_key_exists('xml', $xml_field_data) ? $xml_field_data['xml'] : $xml_field_data['value'];
}
function xpath_field_get_xml_data_from_remote_field($xml_field_data, $settings) {
$prefix = $settings['url_prefix']['use_default'] ? variable_get('xpath_field_default_url_prefix') : $settings['url_prefix']['prefix'];
$response = drupal_http_request($prefix . $xml_field_data['value']);
if ($response->code == '200') {
return $response->data;
}
else {
watchdog('xpath_field', t('Got error @code form XML data source. Message: @message', array('@code' => $response->code, '@message' => $response->error)), NULL, WATCHDOG_ERROR);
return FALSE;
}
}
/**
* Implementation of hook_widget_info().
*
* @return array
*/
function xpath_field_field_widget_info() {
return array(
'xpath_field_xpath' => array(
'label' => t('XPath text'),
'field types' => array('xpath_field_xpath'),
),
);
}
function _xpath_field_instance_xpath(array $instance) {
return isset($instance['widget']['settings']['xpath']) ? $instance['widget']['settings']['xpath'] : '';
}
/**
* Implementation of hook_field_widget_form().
*
* Adds extra form elements to the node edit form. These elements are read-only and reflect
* the result of the field instance's XPath query on the XML field it's been configured to point to.
*
* @param array $form
* @param array $form_state
* @param array $field
* @param array $instance
* @param string $langcode
* @param array $items
* @param int $delta
* @param array $element
*/
function xpath_field_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
$xpath = _xpath_field_instance_xpath($instance);
switch ($instance['widget']['type']) {
case 'xpath_field_xpath':
$entity = isset($element['#entity']) ? $element['#entity'] : NULL;
if ($entity) {
// We are attached to an entity, and not in the widget instance settings form.
$element['xpath_element'] = array(
'#type' => 'textfield',
'#title' => "{$instance['label']} (Derrived with $xpath)",
'#value' => $entity->{$instance['field_name']}[$element['#language']][$delta],
'#attributes' => array('readonly' => 'readonly'),
);
break;
}
}
return $element;
}
/**
* Implementation of hook_field_formatter_info().
*
* @return array
*/
function xpath_field_field_formatter_info() {
return array(
// This formatter just displays the hex value in the color indicated.
'xpath_field_safe' => array(
'label' => t('Simple text-based formatter'),
'field types' => array('xpath_field_xpath'),
),
);
}
/**
* Implementation of hook_field_validate().
*
* Always returns true because the user doens't input this field directly.
*/
function xpath_field_field_validate($element, $form_state) {
return TRUE;
}
/*
* Field formatter functions.
*/
function xpath_field_field_formatter_info_alter(&$info) {
$info['text_default']['field types'][] = 'xpath_field_xpath';
}
/**
* Implementation of hook_field_formatter_view().
*
* Puts the XPath field data into a simple markup field.
*
* @param string $entity_type
* @param StdClass $entity
* @param array $field
* @param array $instance
* @param string $langcode
* @param array $items
* @param int $display
* @return array
*/
function xpath_field_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
$element = array();
switch ($display['type']) {
case 'xpath_field_safe':
foreach ($items as $index => $instance) {
$element[$index] = array(
'#type' => 'markup',
'#markup' => $items[$index]['value'],
);
}
break;
}
return $element;
}
/**
* Implement hook_field_widget_settings_form().
*
* Adds settings for XML Field and XPath value.
*/
function xpath_field_field_widget_settings_form($field, $instance) {
$widget = $instance['widget'];
$settings = $widget['settings'];
$form = array();
$form['xml_field_instance'] = array(
'#type' => 'select',
'#title' => t('Attach to XML Field'),
'#description' => t('Select the XML field instance on this content type to grab an XPath value from.'),
);
$xml_field_instances = array();
foreach (field_info_field_map() as $index => $other_field) {
if (in_array($other_field['type'], array('xml_field_xml', 'text', 'text_long', 'text_with_summary'))) {
foreach ($other_field['bundles'] as $other_field_bundle_type_name => $other_field_bundle_type) {
foreach ($other_field_bundle_type as $other_field_bundle) {
if ($other_field_bundle == $instance['bundle']) {
$field_info = field_info_instance($other_field_bundle_type_name, $index, $other_field_bundle);
$form['xml_field_instance']['#options'][$index] = $field_info['label'] . " ($index)";
}
}
}
}
}
$form['is_remote_path'] = array(
'#type' => 'checkbox',
'#title' => 'Field is remote path.',
'#description' => 'This field contains the path identifier from which to retrieve the XML content.',
'#default_value' => isset($settings['is_remote_path']) ? $settings['is_remote_path'] : FALSE,
);
$form['url_prefix'] = array(
'#type' => 'fieldset',
'#title' => 'Remote field URL Prefix',
);
$form['url_prefix']['use_default'] = array(
'#type' => 'checkbox',
'#title' => t('Use default (@default)', array('@default' => variable_get('xpath_field_default_url_prefix'))),
'#default_value' => isset($settings['url_prefix']['use_default']) ? $settings['url_prefix']['use_default'] : TRUE,
);
$form['url_prefix']['prefix'] = array(
'#type' => 'textfield',
'#title' => t('Or, use a custom URL prefix for this field instance'),
'#description' => t('If this is a remote XML path, this is the prefix to attach the path to to retrieve the document.'),
'#default_value' => isset($settings['url_prefix']['prefix']) ? $settings['url_prefix']['prefix'] : '',
);
if (array_key_exists('xml_field_instance', $settings)) {
$form['xml_field_instance']['#default_value'] = $settings['xml_field_instance'];
}
$xsl_formatter_enabled = module_exists('xsl_formatter');
$form['use_xslt'] = array(
'#title' => t('Use an XSL transformation.'),
'#type' => 'select',
'#description' => t('Choose an XSLT stylesheet to use to transform the XML content.'),
'#options' => array(
'xpath' => 'XPath query',
'xslt' => 'XSLT transformation',
),
'#access' => $xsl_formatter_enabled,
'#default_value' => isset($settings['use_xslt']) ? $settings['use_xslt'] : 'xpath',
);
$xsl_enabled_states = array(
'enabled' => array(
':input[name="instance[widget][settings][use_xslt]"]' => array('value' => 'xslt'),
),
);
$xsls = $xsl_formatter_enabled ? xsl_formatter_enumerate_xsls(): array();
$form['xsl_path'] = array(
'#title' => t('XSL path'),
'#type' => 'select',
'#default_value' => $settings['xsl_path'],
'#element_validate' => array('xpath_field_xsl_path_validate'),
'#description' => t("Path to the location of the XSL file. Search will be made relative to the files/xsl directory, then the module directory."),
'#options' => $xsls,
'#required' => FALSE,
'#states' => $xsl_enabled_states,
'#access' => $xsl_formatter_enabled,
);
// file upload needs an explicit name. This is horrid sorry
$upload_field_id = 'files[' . drupal_clean_css_identifier("files[instance][widget][settings][xsl_upload]") . ']';
$form['xsl_upload'] = array(
'#type' => 'file',
'#title' => t('Upload XSL file'),
'#maxlength' => 40,
'#description' => t("This will be placed in your files/xsl folder where it can be found and re-used."),
'#element_validate' => $xsl_formatter_enabled ? array('xsl_formatter_xsl_upload_validate') : array(),
'#name' => $upload_field_id,
'#states' => $xsl_enabled_states,
'#access' => $xsl_formatter_enabled,
);
$module_path = drupal_get_path('module', 'xsl_formatter');
$form['xsl_params'] = array(
'#title' => t('Additional params'),
'#type' => 'textarea',
'#rows' => 2,
'#cols' => 24,
'#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>"),
'#default_value' => $settings['xsl_params'],
'#element_validate' => $xsl_formatter_enabled ? array('xsl_formatter_xsl_params_validate') : array(),
'#states' => $xsl_enabled_states,
'#access' => $xsl_formatter_enabled,
);
$form['debug'] = array(
'#title' => t('Show XML parsing warnings'),
'#type' => 'checkbox',
'#description' => t("Bad XML data input will trigger warnings that may show on screen. Disable this for a public site."),
'#default_value' => $settings['debug'],
'#states' => $xsl_enabled_states,
'#access' => $xsl_formatter_enabled,
);
$form['xpath'] = array(
'#type' => 'textfield',
'#title' => t('XPath value'),
'#description' => t('Enter the XPath to select a fragment from the XML Field instance.'),
'#default_value' => array_key_exists('xpath', $settings) ? $settings['xpath'] : '',
'#states' => array(
'enabled' => array(
':input[name="instance[widget][settings][use_xslt]"]' => array('value' => 'xpath'),
),
),
);
return $form;
}
/**
* Ensure the named path exists. This includes a small search lookup.
*/
function xpath_field_xsl_path_validate($element, &$form_state, $form) {
$settings = $form_state['values']['instance']['widget']['settings'];
if ($settings['use_xslt'] === 'xslt') {
xsl_formatter_xsl_path_validate($element, $form_state, $form);
}
}
/**
* Implementation of hook_form_field_ui_field_edit_form_alter().
* Add a submit handler so we can clear Drupal's field cache.
* @param Array $form
* @param Array $form_state
*/
function xpath_field_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
if ($form['#field']['module'] == 'xpath_field') {
$form['#submit'][] = 'xpath_field_widget_settings_form_submit';
}
}
/**
* Since Drupal caches field contents we need to clear the field cache when the
* user changes the field settings.
* @param Array $form
* @param Array $form_state
*/
function xpath_field_widget_settings_form_submit($form, &$form_state) {
drupal_set_message(t('The field cache has been cleared.'));
field_cache_clear();
}
/**
* Implementation of hook_field_is_empty().
*
* Always FALSE because the field's value is populated dynamically.
*/
function xpath_field_field_is_empty($item, $field) {
return FALSE;
}
/*
* XML utility functions.
*/
/**
* Runs an XPath query against the XML Field and returns the SimpleXMLElement containing
* the results.
*
* @param string $xml_string
* @param string $xpath
* @return string The XML result.
*/
function xpath_field_get_xpath_fragment($xml_string, $xpath) {
$results = array();
$resultsXML = array();
try {
$xml = new SimpleXMLElement($xml_string);
$results = $xml->xpath($xpath);
} catch (Exception $e) {
drupal_set_message($e->getMessage());
}
if (!empty($results)) {
foreach($results as $result) {
$resultsXML[] = $result->asXml();
}
}
return $resultsXML;
}
function xpath_field_apply_xsl_transformation($xml_string, $xsl_path, $xsl_params, $debug = FALSE) {
$xml_doc = new DomDocument;
try {
if ($debug) {
$xml_doc->loadXml($xml_string);
}
else {
@$xml_doc->loadXml($xml_string);
}
$xsl_doc = xsl_formatter_get_xml_doc($xsl_path);
$params = (array)json_decode($xsl_params);
// 'base' can be used for supporting relative css links.
$params['base'] = url(dirname($xsl_doc->documenturi));
// Transform
$result = xsl_formatter_xmldoc_plus_xsldoc($xml_doc, $xsl_doc, $params);
}
catch (Exception $e) {
watchdog('xpath_field', "Unable to parse the XML data or transform it. %message", array('%message' => $e->getMessage()), E_USER_ERROR);
}
return $result;
}
function xpath_field_get_xml_extractor($settings) {
if ($settings['is_remote_path'] == True) {
$xml_extractor = 'xpath_field_get_xml_data_from_remote_field';
}
else {
$xml_extractor = 'xpath_field_get_xml_data_from_field';
}
return $xml_extractor;
}
function xpath_field_extract_fragments($entity_type, $entity, $field_name, $field) {
$field_instance = field_info_instance($entity_type, $field_name, $entity->type);
$settings = $field_instance['widget']['settings'];
$xml_extractor = xpath_field_get_xml_extractor($settings);
$xml_field_instance = isset($settings['xml_field_instance']) ? $settings['xml_field_instance'] : NULL;
if ($xml_field_instance) {
$lang = property_exists($entity, 'language') ? $entity->language : NULL;
$xpath = _xpath_field_instance_xpath($field_instance);
if ($lang !== NULL) {
foreach ($entity->{$xml_field_instance}[$lang] as $xml_field_data) {
$xml = $xml_extractor($xml_field_data, $settings);
switch($settings['use_xslt']) {
case 'xpath':
$fragments = xpath_field_get_xpath_fragment($xml, $xpath);
break;
case 'xslt':
$fragments = array(xpath_field_apply_xsl_transformation($xml, $settings['xsl_path'], $settings['xsl_params']));
break;
}
}
}
}
return $fragments;
}
/**
* If we upload our own xsl, Make sure it gets saved.
*
* Place it in the public xsl foilder and refer to it.
*/
function xpath_field_xsl_upload_validate($element, &$form_state, $form) {
// Check for a new uploaded xsl.
// Figure out what the big ID was. This is wierd.
$upload_field_id = 'files-' . substr($element['#id'], strlen('edit-'));
// Get it. Temporary at first.
$validators = array('file_validate_extensions' => array('xsl','xslt'));
$file = file_save_upload($upload_field_id, $validators);
if (!empty($file)) {
// File upload was attempted.
if ($file) {
$save_dir = "public://xsl";
file_prepare_directory($save_dir, FILE_CREATE_DIRECTORY);
$save_filepath = $save_dir . '/' . $file->filename;
$filename = file_unmanaged_copy($file->uri, $save_filepath, FILE_EXISTS_REPLACE);
// Set xsl_path to the newly uploaded value.
// The #parents array is important.
// Find the nearby xsl_path element with the same ancestry as me.
$parents = $element['#parents'];
array_pop($parents);
array_push($parents, 'xsl_path');
$xsl_path_element = array('#parents' => $parents);
form_set_value($xsl_path_element, $save_filepath, $form_state);
}
else {
// File upload failed.
form_set_error('xsl_upload', t('The xsl could not be uploaded.'));
}
}
}
/*
* Drupal admin menu functions.
*/
/**
* Implementation of hook_menu().
*/
function xpath_field_menu() {
$menu = array(
'admin/config/content/xpath-field' => array(
'title' => t('XPath Field'),
'description' => t('XPath Field global settings.'),
'page callback' => 'drupal_get_form',
'page arguments' => array('xpath_field_settings_form'),
'access arguments' => array('access administration pages'),
),
);
return $menu;
}
function xpath_field_settings_form($form, &$form_state) {
$form['xpath_field_default_url_prefix'] = array(
'#type' => 'textfield',
'#title' => t('Default URL prefix'),
'#description' => t('For XML fields stored on remote servers, you can provide a default root URL that paths in field data will be appended to. E.g., "http://www.example.com/xml". If the field value contains a leading slash, then don\'t include a trailing slash in this field.'),
'#default_value' => variable_get('xpath_field_default_url_prefix'),
'#required' => FALSE,
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Save'),
);
return $form;
}
function xpath_field_settings_form_submit($form, &$form_state) {
variable_set('xpath_field_default_url_prefix', $form_state['values']['xpath_field_default_url_prefix']);
drupal_set_message(t('Saved XPath field settings.'), 'status');
}