drupal/modules/rdf/rdf.module

659 lines
24 KiB
Plaintext

<?php
// $Id$
/**
* @file
* Enables semantically enriched output for Drupal sites.
*/
/**
* @defgroup rdf RDFa API
* @{
* Functions to describe entities and bundles for RDFa.
*
* RDF module introduces RDFa to Drupal, which provides a set of XHTML
* attributes to augment visual data with machine-readable hints.
* @see http://www.w3.org/TR/xhtml-rdfa-primer/
*
* Modules can provide mappings of their bundles' data and metadata to RDFa
* properties using the appropriate vocabularies. This module takes care of
* injecting that data into variables available to themers in the .tpl files.
* Drupal core themes ship with RDFa output enabled.
*
* Example mapping from node.module:
* @code
* array(
* 'type' => 'node',
* 'bundle' => RDF_DEFAULT_BUNDLE,
* 'mapping' => array(
* 'rdftype' => array('sioc:Item', 'foaf:Document'),
* 'title' => array(
* 'predicates' => array('dc:title'),
* ),
* 'created' => array(
* 'predicates' => array('dc:date', 'dc:created'),
* 'datatype' => 'xsd:dateTime',
* 'callback' => 'date_iso8601',
* ),
* 'body' => array(
* 'predicates' => array('content:encoded'),
* ),
* 'uid' => array(
* 'predicates' => array('sioc:has_creator'),
* ),
* 'name' => array(
* 'predicates' => array('foaf:name'),
* ),
* ),
* );
* @endcode
*/
/**
* RDF bundle flag: Default bundle.
*
* Defines an empty string as the name of the bundle to store default
* RDF mappings of a type's properties (fields, etc.).
*/
define('RDF_DEFAULT_BUNDLE', '');
/**
* Returns the mapping for attributes of a given type/bundle pair.
*
* @param $type
* An entity type.
* @param $bundle
* (optional) A bundle name.
*
* @return
* The mapping corresponding to the requested type/bundle pair or an empty
* array.
*/
function rdf_mapping_load($type, $bundle = RDF_DEFAULT_BUNDLE) {
// Retrieve the mapping from the entity info.
$entity_info = entity_get_info($type);
if (!empty($entity_info['bundles'][$bundle]['rdf_mapping'])) {
return $entity_info['bundles'][$bundle]['rdf_mapping'];
}
else {
return _rdf_get_default_mapping($type);
}
}
/**
* Returns the default RDF mapping for a given entity type.
*
* @param $type
* An entity type, e.g. 'node' or 'comment'.
*
* @return
* The RDF mapping or an empty array.
*/
function _rdf_get_default_mapping($type) {
$default_mappings = &drupal_static(__FUNCTION__);
if (!isset($default_mappings)) {
// Get all modules implementing hook_rdf_mapping().
$modules = module_implements('rdf_mapping');
// Only consider the default entity mapping definitions.
foreach ($modules as $module) {
$mappings = module_invoke($module, 'rdf_mapping');
foreach ($mappings as $mapping) {
if ($mapping['bundle'] === RDF_DEFAULT_BUNDLE) {
$default_mappings[$mapping['type']] = $mapping['mapping'];
}
}
}
}
return isset($default_mappings[$type]) ? $default_mappings[$type] : array();
}
/**
* Helper function to retrieve a RDF mapping from the database.
*
* @param $type
* The entity type the mapping refers to.
* @param $bundle
* The bundle the mapping refers to.
*
* @return
* A RDF mapping structure or FALSE if no record was found.
*/
function _rdf_mapping_load($type, $bundle) {
$mapping = db_select('rdf_mapping')
->fields(NULL, array('mapping'))
->condition('type', $type)
->condition('bundle', $bundle)
->execute()
->fetchField();
if (!$mapping) {
return array();
}
return unserialize($mapping);
}
/**
* Saves an RDF mapping to the database.
*
* Takes a mapping structure returned by hook_rdf_mapping() implementations
* and creates or updates a record mapping for each encountered
* type, bundle pair. If available, adds default values for non-existent
* mapping keys.
*
* @param $mapping
* The RDF mapping to save, as an array.
*
* @return
* Status flag indicating the outcome of the operation.
*/
function rdf_mapping_save(&$mapping) {
// Adds default values for non-existent keys.
$mapping['mapping'] += _rdf_get_default_mapping($mapping['type']);
$status = db_merge('rdf_mapping')
->key(array(
'type' => $mapping['type'],
'bundle' => $mapping['bundle'],
))
->fields(array(
'mapping' => serialize($mapping['mapping']),
))
->execute();
cache_clear_all('entity_info', 'cache');
drupal_static_reset('entity_get_info');
return $status;
}
/**
* Deletes the mapping for the given pair of type and bundle from the database.
*
* @param $type
* The entity type the mapping refers to.
* @param $bundle
* The bundle the mapping refers to.
*
* @return
* Return boolean TRUE if mapping deleted, FALSE if not.
*/
function rdf_mapping_delete($type, $bundle) {
$num_rows = db_delete('rdf_mapping')
->condition('type', $type)
->condition('bundle', $bundle)
->execute();
return (bool) ($num_rows > 0);
}
/**
* Builds an array of RDFa attributes for a given mapping.
*
* @param $mapping
* An array containing a mandatory 'predicates' key and optional 'datatype',
* 'callback' and 'type' keys. For example:
* @code
* array(
* 'predicates' => array('dc:created'),
* 'datatype' => 'xsd:dateTime',
* 'callback' => 'date_iso8601',
* ),
* );
* @endcode
* @param $data
* A value that needs to be converted by the provided callback function.
*
* @return
* An array containing RDFa attributes suitable for drupal_attributes().
*/
function rdf_rdfa_attributes($mapping, $data = NULL) {
// The type of mapping defaults to 'property'.
$type = isset($mapping['type']) ? $mapping['type'] : 'property';
switch ($type) {
// The mapping expresses the relationship between two resources.
case 'rel':
case 'rev':
$attributes[$type] = $mapping['predicates'];
break;
// The mapping expresses the relationship between a resource and some
// literal text.
case 'property':
$attributes['property'] = $mapping['predicates'];
if (isset($mapping['callback']) && isset($data)) {
$callback = $mapping['callback'];
if (function_exists($callback)) {
$attributes['content'] = $callback($data);
}
if (isset($mapping['datatype'])) {
$attributes['datatype'] = $mapping['datatype'];
}
}
break;
}
return $attributes;
}
/**
* @} End of "defgroup rdf".
*/
/**
* Implements hook_modules_installed().
*
* Checks if the installed modules have any RDF mapping definitions to declare
* and stores them in the rdf_mapping table.
*
* While both default entity mappings and specific bundle mappings can be
* defined in hook_rdf_mapping(), we do not want to save the default entity
* mappings in the database because users are not expected to alter these.
* Instead they should alter specific bundle mappings which are stored in the
* database so that they can be altered via the RDF CRUD mapping API.
*/
function rdf_modules_installed($modules) {
// We need to clear the caches of entity_info as this is not done right
// during the tests. see http://drupal.org/node/594234
cache_clear_all('entity_info', 'cache');
drupal_static_reset('entity_get_info');
foreach ($modules as $module) {
$function = $module . '_rdf_mapping';
if (function_exists($function)) {
foreach ($function() as $mapping) {
// Only the bundle mappings are saved in the database.
if ($mapping['bundle'] !== RDF_DEFAULT_BUNDLE) {
rdf_mapping_save($mapping);
}
}
}
}
}
/**
* Implements hook_modules_uninstalled().
*/
function rdf_modules_uninstalled($modules) {
// @todo Remove RDF mappings of uninstalled modules.
}
/**
* Implements hook_entity_info_alter().
*
* Adds the proper RDF mapping to each entity type, bundle pair.
*/
function rdf_entity_info_alter(&$entity_info) {
// Loop through each entity type and its bundles.
foreach ($entity_info as $entity_type => $entity_type_info) {
if (isset($entity_type_info['bundles'])) {
foreach ($entity_type_info['bundles'] as $bundle => $bundle_info) {
if ($mapping = _rdf_mapping_load($entity_type, $bundle)) {
$entity_info[$entity_type]['bundles'][$bundle]['rdf_mapping'] = $mapping;
}
else {
// If no mapping was found in the database, assign the default RDF
// mapping for this entity type.
$entity_info[$entity_type]['bundles'][$bundle]['rdf_mapping'] = _rdf_get_default_mapping($entity_type);
}
}
}
}
}
/**
* Implements hook_entity_load().
*/
function rdf_entity_load($entities, $type) {
foreach ($entities as $entity) {
// Extracts the bundle of the entity being loaded.
list($id, $vid, $bundle) = entity_extract_ids($type, $entity);
$entity->rdf_mapping = rdf_mapping_load($type, $bundle);
}
}
/**
* Implements hook_theme().
*/
function rdf_theme() {
return array(
'rdf_template_variable_wrapper' => array(
'variables' => array('content' => NULL, 'attributes' => array(), 'context' => array(), 'inline' => TRUE),
),
'rdf_metadata' => array(
'variables' => array('metadata' => array()),
),
);
}
/**
* Template process function for adding extra tags to hold RDFa attributes.
*
* Since template files already have built-in support for $attributes,
* $title_attributes, and $content_attributes, and field templates have support
* for $item_attributes, we try to leverage those as much as possible. However,
* in some cases additional attributes are needed not covered by these. We deal
* with those here.
*/
function rdf_process(&$variables, $hook) {
// Handles attributes needed for content not covered by title, content,
// and field items. Does this by adjusting the variable sent to the template
// so that the template doesn't have to worry about it.
// @see theme_rdf_template_variable_wrapper()
if (!empty($variables['rdf_template_variable_attributes_array'])) {
foreach ($variables['rdf_template_variable_attributes_array'] as $variable_name => $attributes) {
$context = array(
'hook' => $hook,
'variable_name' => $variable_name,
'variables' => $variables,
);
$variables[$variable_name] = theme('rdf_template_variable_wrapper', array('content' => $variables[$variable_name], 'attributes' => $attributes, 'context' => $context));
}
}
// Handles additional attributes about a template entity that for RDF parsing
// reasons, can't be placed into that template's $attributes variable. This
// is "meta" information that is related to particular content, so render it
// close to that content.
if (!empty($variables['rdf_metadata_attributes_array'])) {
if (!isset($variables['content']['#prefix'])) {
$variables['content']['#prefix'] = '';
}
$variables['content']['#prefix'] = theme('rdf_metadata', array('metadata' => $variables['rdf_metadata_attributes_array'])) . $variables['content']['#prefix'];
}
}
/**
* Implements MODULE_preprocess_HOOK().
*/
function rdf_preprocess_node(&$variables) {
// Adds RDFa markup to the node container. The about attribute specifies the
// URI of the resource described within the HTML element, while the typeof
// attribute indicates its RDF type (foaf:Document, or sioc:User, etc.).
$variables['attributes_array']['about'] = empty($variables['node_url']) ? NULL: $variables['node_url'];
$variables['attributes_array']['typeof'] = empty($variables['node']->rdf_mapping['rdftype']) ? NULL : $variables['node']->rdf_mapping['rdftype'];
// Adds RDFa markup to the title of the node. Because the RDFa markup is added
// to the h2 tag which might contain HTML code, we specify an empty datatype
// to ensure the value of the title read by the RDFa parsers is a literal.
$variables['title_attributes_array']['property'] = empty($variables['node']->rdf_mapping['title']['predicates']) ? NULL : $variables['node']->rdf_mapping['title']['predicates'];
$variables['title_attributes_array']['datatype'] = '';
// In full node mode, the title is not displayed by node.tpl.php so it is
// added in the head tag of the HTML page.
if ($variables['page']) {
$element = array(
'#tag' => 'meta',
'#attributes' => array(
'content' => $variables['node_title'],
'about' => $variables['node_url'],
),
);
if (!empty($variables['node']->rdf_mapping['title']['predicates'])) {
$element['#attributes']['property'] = $variables['node']->rdf_mapping['title']['predicates'];
}
drupal_add_html_head($element, 'rdf_node');
}
// Adds RDFa markup for the date.
if (!empty($variables['rdf_mapping']['created'])) {
$date_attributes_array = rdf_rdfa_attributes($variables['rdf_mapping']['created'], $variables['created']);
$variables['rdf_template_variable_attributes_array']['date'] = $date_attributes_array;
}
// Adds RDFa markup for the relation between the node and its author.
if (!empty($variables['rdf_mapping']['uid'])) {
$variables['rdf_template_variable_attributes_array']['name']['rel'] = $variables['rdf_mapping']['uid']['predicates'];
}
}
/**
* Implements MODULE_preprocess_HOOK().
*/
function rdf_preprocess_field(&$variables) {
$entity_type = $variables['element']['#object_type'];
$instance = $variables['instance'];
$mapping = rdf_mapping_load($entity_type, $instance['bundle']);
$field_name = $instance['field_name'];
if (!empty($mapping) && !empty($mapping[$field_name])) {
foreach ($variables['items'] as $delta => $item) {
if (!empty($item['#item'])) {
$variables['item_attributes_array'][$delta] = rdf_rdfa_attributes($mapping[$field_name], $item['#item']);
}
}
}
}
/**
* Implements MODULE_preprocess_HOOK().
*/
function rdf_preprocess_user_profile(&$variables) {
// Adds RDFa markup to the user profile page. Fields displayed in this page
// will automatically describe the user.
// @todo move to user.module
$account = user_load($variables['user']->uid);
if (!empty($account->rdf_mapping['rdftype'])) {
$variables['attributes_array']['typeof'] = $account->rdf_mapping['rdftype'];
$variables['attributes_array']['about'] = url('user/' . $account->uid);
}
}
/**
* Implements MODULE_preprocess_HOOK().
*/
function rdf_preprocess_username(&$variables) {
// $variables['account'] is a pseudo account object, and as such, does not
// contain the rdf mappings for the user; in the case of nodes and comments,
// it contains the mappings for the node or comment object instead. Therefore,
// the real account object for the user is needed. The real account object is
// needed for this function only; it should not replace $variables['account'].
if ($account = user_load($variables['uid'])) {
// An RDF resource for the user is created with the 'about' attribute and
// the profile URI is used to identify this resource. Even if the user
// profile is not accessible, we generate its URI regardless in order to
// be able to identify the user in RDF. We do not use this attribute for
// the anonymous user because we do not have a user profile URI for it (only
// a homepage which cannot be used as user profile in RDF).
if ($account->uid > 0) {
$variables['attributes_array']['about'] = url('user/' . $account->uid);
}
// The remaining attributes are defined by RDFa as lists
// (http://www.w3.org/TR/rdfa-syntax/#rdfa-attributes). Therefore, merge
// rather than override, so as not to clobber values set by earlier
// preprocess functions.
$attributes = array();
// The 'typeof' attribute specifies the RDF type(s) of this resource. They
// are defined in the 'rdftype' property of the user object RDF mapping.
if (!empty($account->rdf_mapping['rdftype'])) {
$attributes['typeof'] = $account->rdf_mapping['rdftype'];
}
// Annotate the user name in RDFa. The attribute 'property' is used here
// because the user name is a literal.
if (!empty($account->rdf_mapping['name'])) {
$attributes['property'] = $account->rdf_mapping['name']['predicates'];
}
// Add the homepage RDFa markup if present.
if (!empty($variables['homepage']) && !empty($account->rdf_mapping['homepage'])) {
$attributes['rel'] = $account->rdf_mapping['homepage']['predicates'];
}
$variables['attributes_array'] = array_merge_recursive($variables['attributes_array'], $attributes);
}
}
/**
* Implements MODULE_preprocess_HOOK().
*/
function rdf_preprocess_comment(&$variables) {
$comment = $variables['comment'];
if (!empty($comment->rdf_mapping['rdftype'])) {
// Adds RDFa markup to the comment container. The about attribute specifies
// the URI of the resource described within the HTML element, while the
// typeof attribute indicates its RDF type (e.g. sioc:Post, etc.).
$variables['attributes_array']['about'] = url('comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid));
$variables['attributes_array']['typeof'] = $comment->rdf_mapping['rdftype'];
}
// Adds RDFa markup for the date of the comment.
if (!empty($comment->rdf_mapping['created'])) {
$date_attributes_array = rdf_rdfa_attributes($comment->rdf_mapping['created'], $comment->created);
$variables['rdf_template_variable_attributes_array']['created'] = $date_attributes_array;
}
// Adds RDFa markup for the relation between the comment and its author.
if (!empty($comment->rdf_mapping['uid'])) {
$variables['rdf_template_variable_attributes_array']['author']['rel'] = $comment->rdf_mapping['uid']['predicates'];
}
if (!empty($comment->rdf_mapping['title'])) {
// Adds RDFa markup to the subject of the comment. Because the RDFa markup is
// added to an h3 tag which might contain HTML code, we specify an empty
// datatype to ensure the value of the title read by the RDFa parsers is a
// literal.
$variables['title_attributes_array']['property'] = $comment->rdf_mapping['title']['predicates'];
$variables['title_attributes_array']['datatype'] = '';
}
if (!empty($comment->rdf_mapping['body'])) {
// We need a special case here since the comment body is not a field. Note
// that for that reason, fields attached to comment will be ignored by RDFa
// parsers since we set the property attribute here.
// @todo Use fields instead, see http://drupal.org/node/538164
$variables['content_attributes_array']['property'] = $comment->rdf_mapping['body']['predicates'];
}
// Annotates the parent relationship between the current comment and the node
// it belongs to. If available, the parent comment is also annotated.
if (!empty($comment->rdf_mapping['pid'])) {
// Relation to parent node.
$parent_node_attributes['rel'] = $comment->rdf_mapping['pid']['predicates'];
$parent_node_attributes['resource'] = url('node/' . $comment->nid);
$variables['rdf_metadata_attributes_array'][] = $parent_node_attributes;
// Relation to parent comment if it exists.
if ($comment->pid != 0) {
$parent_comment_attributes['rel'] = $comment->rdf_mapping['pid']['predicates'];
$parent_comment_attributes['resource'] = url('comment/' . $comment->pid, array('fragment' => 'comment-' . $comment->pid));
$variables['rdf_metadata_attributes_array'][] = $parent_comment_attributes;
}
}
}
/**
* Implements MODULE_preprocess_HOOK().
*/
function rdf_preprocess_field_formatter_taxonomy_term_link(&$variables) {
$term = $variables['element']['#item']['taxonomy_term'];
if (!empty($term->rdf_mapping['rdftype'])) {
$variables['link_options']['attributes']['typeof'] = $term->rdf_mapping['rdftype'];
}
if (!empty($term->rdf_mapping['name']['predicates'])) {
$variables['link_options']['attributes']['property'] = $term->rdf_mapping['name']['predicates'];
}
}
/**
* Wraps a template variable in an HTML element with the desired attributes.
*
* This is called by rdf_process() shortly before the theme system renders
* a template file. It is called once for each template variable for which
* additional attributes are needed. While template files are responsible for
* rendering the attributes for the template's primary object (via the
* $attributes variable), title (via the $title_attributes variable), and
* content (via the $content_attributes variable), additional template variables
* that need containing attributes are routed through this function, allowing
* the template file to receive properly wrapped variables.
*
* @param $variables
* An associative array containing:
* - content: A string of content to be wrapped with attributes.
* - attributes: An array of attributes desired on the wrapping element.
* - context: An array of context information about the content to be wrapped:
* - hook: The theme hook that will use the wrapped content. This
* corresponds to the key within the theme registry for this template.
* For example, if this content is about to be used in node.tpl.php or
* node-TYPE.tpl.php, then the 'hook' is 'node'.
* - variable_name: The name of the variable, by which the template will
* refer to this content. Each template file has documentation about
* the variables it uses. For example, if this function is called in
* preparing the $author variable for comment.tpl.php, then the
* 'variable_name' is 'author'.
* - variables: The full array of variables about to be passed to the
* template.
* - inline: TRUE if the content contains only inline HTML elements and
* therefore can be validly wrapped by a 'span' tag. FALSE if the content
* might contain block level HTML elements and therefore cannot be validly
* wrapped by a 'span' tag. Modules implementing preprocess functions that
* set 'rdf_template_variable_attributes_array' for a particular template
* variable that might contain block level HTML must also implement
* hook_preprocess_rdf_template_variable_wrapper() and set 'inline' to FALSE
* for that context. Themes that render normally inline content with block
* level HTML must similarly implement
* hook_preprocess_rdf_template_variable_wrapper() and set 'inline'
* accordingly.
*
* @return
* A string containing the wrapped content. The template receives the for its
* variable instead of the original content.
*
* Tip for themers: if you're already outputting a wrapper element around a
* particular template variable in your template file and if you don't want
* an extra wrapper element, you can override this function to not wrap that
* variable and instead print the following inside your template file:
* @code
* drupal_attributes($rdf_template_variable_attributes_array[$variable_name])
* @endcode
*
* @see rdf_process()
*
* @ingroup themeable
*/
function theme_rdf_template_variable_wrapper($variables) {
$output = $variables['content'];
if (!empty($output) && !empty($variables['attributes'])) {
$attributes = drupal_attributes($variables['attributes']);
$output = $variables['inline'] ? "<span$attributes>$output</span>" : "<div$attributes>$output</div>";
}
return $output;
}
/**
* Outputs a series of empty spans for exporting RDF metadata in RDFa.
*
* Sometimes it is useful to export data which is not semantically present in
* the HTML output. For example, a hierarchy of comments is visible for a human
* but not for machines because this hiearchy is not present in the DOM tree.
* We can express it in RDFa via empty span tags. These won't be visible and
* will give machines extra information about the content and its structure.
*
* @param $variables
* An associative array containing:
* - metadata: An array of attribute arrays. Each item in the array
* corresponds to its own set of attributes, and therefore, needs its own
* element.
*
* @return
* A string of HTML containing markup that can be understood by an RDF parser.
*
* Tip for themers: while this default implementation results in valid markup
* for the XHTML+RDFa doctype, you may need to override this in your theme to be
* valid for doctypes that don't support empty spans. Or, if empty spans create
* visual problems in your theme, you may want to override this to set a
* class on them, and apply a CSS rule of display:none for that class.
*
* @see rdf_process()
*
* @ingroup themeable
*/
function theme_rdf_metadata($variables) {
$output = '';
foreach ($variables['metadata'] as $attributes) {
$output .= '<span' . drupal_attributes($attributes) . ' />';
}
return $output;
}