=== modified file 'modules/comment/comment.module' --- modules/comment/comment.module 2009-10-17 05:50:27 +0000 +++ modules/comment/comment.module 2009-10-18 21:30:46 +0000 @@ -2494,3 +2494,35 @@ function comment_filter_format_delete($f ->condition('format', $format->format) ->execute(); } + +function comment_rdf_mapping() { + return array( + array( + 'type' => 'comment', + 'bundle' => RDF_DEFAULT_BUNDLE, + 'mapping' => array( + 'rdftype' => array('sioct:Post'), + 'title' => array( + 'property' => array('dc:title'), + ), + 'created' => array( + 'property' => array('dc:date', 'dc:created'), + 'datatype' => 'xsd:dateTime', + 'callback' => 'date_iso8601', + ), + 'body' => array( + 'property' => array('content:encoded'), + ), + 'pid' => array( + 'property' => array('sioc:reply_of'), + ), + 'uid' => array( + 'property' => array('sioc:has_creator'), + ), + 'name' => array( + 'property' => array('foaf:name'), + ), + ), + ), + ); +} === modified file 'modules/comment/comment.test' --- modules/comment/comment.test 2009-10-16 23:48:37 +0000 +++ modules/comment/comment.test 2009-10-18 23:57:41 +0000 @@ -864,3 +864,52 @@ class CommentRSSUnitTest extends Comment $this->assertNoRaw($raw, t('Hidden comments is not a part of RSS feed.')); } } + +class RdfaCommentTestCase extends CommentHelperCase { + + public static function getInfo() { + return array( + 'name' => t('RDFa comment markup'), + 'description' => t('Test RDFa markup in comments.'), + 'group' => t('RDF'), + ); + } + + function setUp() { + parent::setUp('comment', 'rdf'); + + $this->admin_user = $this->drupalCreateUser(array('administer content types', 'administer comments', 'administer permissions', 'administer blocks')); + $this->web_user = $this->drupalCreateUser(array('access comments', 'post comments', 'create article content')); + + $this->drupalLogin($this->web_user); + $this->node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1)); + $this->drupalLogout(); + } + + function testAttributesInMarkup() { + // Set comments to not have subject. + $this->drupalLogin($this->admin_user); + $this->setCommentPreview(FALSE); + $this->setCommentForm(TRUE); + $this->setCommentSubject(TRUE); + $this->setCommentSettings('comment_default_mode', COMMENT_MODE_THREADED, t('Comment paging changed.')); + $this->drupalLogout(); + + // Post comment without subject. + $this->drupalLogin($this->web_user); + $subject_text = 'foo'; + $comment_text = 'bar'; + $comment = $this->postComment($this->node, $comment_text, $subject_text, FALSE); + $this->drupalGet('node/' . $this->node->nid); + + $comment_container = $this->xpath("//div[contains(@class, 'comment') and @typeof='sioct:Post']"); + $this->assertFalse(empty($comment_container)); + $comment_title = $this->xpath("//h3[@property='dc:title']"); + $this->assertEqual((string)$comment_title[0]->a, 'foo'); + $comment_date = $this->xpath("//div[@typeof='sioct:Post']//*[contains(@property, 'dc:date') and contains(@property, 'dc:created')]"); + $this->assertFalse(empty($comment_date)); + $comment_author = $this->xpath("//div[@typeof='sioct:Post']//*[contains(@property, 'foaf:name')]"); + $this->assertEqual((string)$comment_author[0], $this->web_user->name); + } + +} === modified file 'modules/node/node.module' --- modules/node/node.module 2009-10-17 05:50:27 +0000 +++ modules/node/node.module 2009-10-19 01:34:17 +0000 @@ -754,6 +754,47 @@ function node_type_set_defaults($info = } /** + * Define the default RDF mapping for the node entity type. + * + * These default mapping properties are used by rdf_save_mapping() to populate + * non-existing properties before they are saved to the database. + * + * @return + * A list of default mapping properties for the node entity type. + */ +function node_rdf_mapping() { + return array( + array( + 'type' => 'node', + 'bundle' => RDF_DEFAULT_BUNDLE, + 'mapping' => array( + 'rdftype' => array('sioc:Item', 'foaf:Document'), + 'title' => array( + 'property' => array('dc:title'), + ), + 'created' => array( + 'property' => array('dc:date', 'dc:created'), + 'datatype' => 'xsd:dateTime', + 'callback' => 'date_iso8601', + ), + 'changed' => array( + 'property' => array('dc:modified'), + ), + 'body' => array( + 'property' => array('content:encoded'), + ), + 'uid' => array( + 'property' => array('sioc:has_creator'), + ), + 'name' => array( + 'property' => array('foaf:name'), + ), + ), + ), + ); +} + +/** * Determine whether a node hook exists. * * @param $node === modified file 'modules/node/node.tpl.php' --- modules/node/node.tpl.php 2009-10-19 01:30:06 +0000 +++ modules/node/node.tpl.php 2009-10-19 03:00:50 +0000 @@ -13,7 +13,7 @@ * given element. * - $user_picture: The node author's picture from user-picture.tpl.php. * - $date: Formatted creation date (use $created to reformat with - * format_date()). + * format_date()). This data is excepted to be sanitized beforehand. * - $name: Themed username of node author output from theme_username(). * - $node_url: Direct url of the current node. * - $terms: the themed list of taxonomy term links output from theme_links(). @@ -88,8 +88,8 @@ $name, '@datetime' => $date)); + print t('Submitted by !username on !datetime', + array('!username' => $name, '!datetime' => $date)); ?> === added directory 'modules/rdf' === added file 'modules/rdf/rdf.api.php' --- modules/rdf/rdf.api.php 1970-01-01 00:00:00 +0000 +++ modules/rdf/rdf.api.php 2009-10-17 01:50:01 +0000 @@ -0,0 +1,44 @@ + 'node', + 'bundle' => 'blog', + 'mapping' => array( + 'rdftype' => array('sioct:Weblog'), + ) + ), + ); +} + +/** + * @} End of "addtogroup hooks". + */ === added file 'modules/rdf/rdf.info' --- modules/rdf/rdf.info 1970-01-01 00:00:00 +0000 +++ modules/rdf/rdf.info 2009-10-17 20:46:53 +0000 @@ -0,0 +1,9 @@ +; $Id$ +name = RDF +description = Allows to map the site data structure to RDF and export it in RDFa. +package = Core +version = VERSION +core = 7.x +files[] = rdf.install +files[] = rdf.module +files[] = rdf.test === added file 'modules/rdf/rdf.install' --- modules/rdf/rdf.install 1970-01-01 00:00:00 +0000 +++ modules/rdf/rdf.install 2009-10-17 01:07:07 +0000 @@ -0,0 +1,50 @@ + 'Stores custom RDF mappings for user defined content types or overriden module-defined mappings', + 'fields' => array( + 'type' => array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'description' => 'The name of the entity type a mapping applies to (node, user, comment, etc.).', + ), + 'bundle' => array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'description' => 'The name of the bundle a mapping applies to.', + ), + 'mapping' => array( + 'description' => 'The serialized mapping of the bundle type and fields to RDF terms.', + 'type' => 'text', + 'not null' => FALSE, + 'size' => 'big', + 'serialize' => TRUE, + ), + ), + 'primary key' => array('type', 'bundle'), + ); + + return $schema; +} + +/** + * Implements hook_install(). + */ +function rdf_install() { + // The installer does not trigger hook_modules_installed() so it needs to + // triggered programmatically on the modules which defined RDF mappings. + $modules = module_implements('rdf_mapping'); + rdf_modules_installed($modules); +} === added file 'modules/rdf/rdf.module' --- modules/rdf/rdf.module 1970-01-01 00:00:00 +0000 +++ modules/rdf/rdf.module 2009-10-19 02:36:35 +0000 @@ -0,0 +1,557 @@ + 'node', + * 'bundle' => RDF_DEFAULT_BUNDLE, + * 'mapping' => array( + * 'rdftype' => array('sioc:Item', 'foaf:Document'), + * 'title' => array( + * 'property' => array('dc:title'), + * ), + * 'created' => array( + * 'property' => array('dc:date', 'dc:created'), + * 'datatype' => 'xsd:dateTime', + * 'callback' => 'date_iso8601', + * ), + * 'body' => array( + * 'property' => array('content:encoded'), + * ), + * 'uid' => array( + * 'property' => array('sioc:has_creator'), + * ), + * 'name' => array( + * 'property' => array('foaf:name'), + * ), + * ), + * ); + */ + +/** + * Defines the empty string as the name of the bundle to store default + * RDF mappings of a type's properties (fields, et. al.). + */ +define('RDF_DEFAULT_BUNDLE', ''); + +/** + * Implements hook_theme(). + */ +function rdf_theme() { + return array( + 'rdf_template_variable_wrapper' => array( + 'arguments' => array('content' => NULL, 'attributes' => array(), 'context' => array(), 'inline' => TRUE), + ), + 'rdf_metadata' => array( + 'arguments' => array('metadata' => array()), + ), + ); +} + + /** + * Wraps a template variable in an HTML element with the desired attributes. + * + * @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'] ? "$output" : "$output"; + } + 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. + * + * @ingroup themeable + */ +function theme_rdf_metadata($variables) { + $output = ''; + foreach ($variables['metadata'] as $attributes) { + $output .= ''; + } + return $output; +} + + /** + * Process function for wrapping some content with an extra tag. + */ +function rdf_process(&$variables, $hook) { + if (!empty($variables['rdf_variable_attributes_array'])) { + foreach ($variables['rdf_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)); + } + } + + if (!empty($variables['metadata_attributes_array'])) { + if (!isset($variables['content']['#prefix'])) { + $variables['content']['#prefix'] = ''; + } + $variables['content']['#prefix'] = theme('rdf_metadata', array('metadata' => $variables['metadata_attributes_array'])) . $variables['content']['#prefix']; + } + + +} + +/** + * Returns the mapping for the attributes of the given type, bundle pair. + * + * @param $type + * An entity type. + * @param $bundle + * A bundle name. + * @return array + * The mapping corresponding to the requested type, bundle pair or an empty + * array. + */ +function rdf_get_mapping($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); + } +} + +/** + * 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_save_mapping($mapping) { + // Adds default values for non-existent keys. + $new_mapping = $mapping['mapping'] + _rdf_get_default_mapping($mapping['type']); + $exists = (bool)rdf_read_mapping($mapping['type'], $mapping['bundle']); + + if ($exists) { + rdf_update_mapping($mapping['type'], $mapping['bundle'], $new_mapping); + return SAVED_UPDATED; + } + else { + rdf_create_mapping($mapping['type'], $mapping['bundle'], $new_mapping); + return SAVED_NEW; + } + + cache_clear_all('entity_info', 'cache'); + drupal_static_reset('entity_get_info'); +} + +/** + * 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) { + if (function_exists($module . '_rdf_mapping')) { + $mapping_array = call_user_func($module . '_rdf_mapping'); + foreach ($mapping_array as $mapping) { + // Only the bundle mappings are saved in the database. + if ($mapping['bundle'] != RDF_DEFAULT_BUNDLE) { + rdf_save_mapping($mapping); + } + } + } + } +} + +/** + * Implements hook_modules_uninstalled(). + */ +function rdf_modules_uninstalled($modules) { +// @todo remove the RDF mappings. +} + +/** + * 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_read_mapping($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); + } + } + } + } +} + +/** + * Returns ready to render RDFa attributes for the given mapping. + * + * @param $mapping + * An array containing a mandatory property key and optional datatype and + * callback keys. + * Example: + * array( + * 'property' => array('dc:created'), + * 'datatype' => 'xsd:dateTime', + * 'callback' => 'date_iso8601', + * ) + * @param $data + * A value that needs to be converted by the provided callback function. + * @return array + * An array containing RDFa attributes ready for rendering. + */ +function drupal_rdfa_attributes($mapping, $data = NULL) { + $attributes['property'] = $mapping['property']; + + if (isset($mapping['callback']) && isset($data)) { + $callback = $mapping['callback']; + + if (function_exists($callback)) { + $attributes['content'] = call_user_func($callback, $data); + } + if (isset($mapping['datatype'])) { + $attributes['datatype'] = $mapping['datatype']; + } + } + + return $attributes; +} + + +/** + * 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) = field_extract_ids($type, $entity); + $entity->rdf_mapping = rdf_get_mapping($type, $bundle); + } +} + +/** + * Implements MODULE_preprocess_HOOK(). + */ +function rdf_preprocess_node(&$variables) { + // Add 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']; + + // Add 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']['property']) ? NULL : $variables['node']->rdf_mapping['title']['property']; + $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']) { + $title_attributes['property'] = empty($variables['node']->rdf_mapping['title']['property']) ? NULL : $variables['node']->rdf_mapping['title']['property']; + $title_attributes['content'] = $variables['node_title']; + $title_attributes['about'] = $variables['node_url']; + drupal_add_html_head(''); + } + + // Add RDFa markup for the date. + if (!empty($variables['rdf_mapping']['created'])) { + $date_attributes_array = drupal_rdfa_attributes($variables['rdf_mapping']['created'], $variables['created']); + $variables['rdf_variable_attributes_array']['date'] = $date_attributes_array; + } +} + +/** + * Implements MODULE_preprocess_HOOK(). + */ +function rdf_preprocess_field(&$variables) { + $entity_type = $variables['element']['#object_type']; + $instance = $variables['instance']; + $mapping = rdf_get_mapping($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] = drupal_rdfa_attributes($mapping[$field_name], $item['#item']); + } + } + } +} + +/** + * Implements MODULE_preprocess_HOOK(). + */ +function rdf_preprocess_username(&$variables) { + $account = $variables['account']; + if (!empty($account->rdf_mapping['name'])) { + if ($account->uid != 0) { + // The following RDFa construct allows to fit all the needed information + // into the a tag and avoids having to wrap it with an extra span. + + // 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. + $variables['attributes_array']['about'] = url('user/' . $account->uid); + // The 'typeof' attribute specifies the RDF type(s) of this resource. They + // are defined in the 'rdftype' property of the user object RDF mapping. + // Since the full user object is not available in $variables, it needs to + // be loaded. This is due to the collision between the node and user + // when they are merged into $account and some properties are overridden. + $variables['attributes_array']['typeof'] = user_load($account->uid)->rdf_mapping['rdftype']; + + // This first thing we are describing is the relation between the user and + // the parent resource (e.g. a node). Because the set of predicate link + // the parent to the user, we must use the 'rev' RDFa attribute to specify + // that the relationship is reverse. + if (!empty($account->rdf_mapping['uid']['property'])) { + $variables['attributes_array']['rev'] = $account->rdf_mapping['uid']['property']; + // We indicate the parent identifier in the 'resource' attribute, + // typically this is the entity URI. This is the object in RDF. + $parent_uri = ''; + if (!empty($account->path['source'])) { + $parent_uri = url($account->path['source']); + } + elseif (!empty($account->cid)) { + $parent_uri = url('comment/' . $account->cid, array('fragment' => 'comment-' . $account->cid)); + } + $variables['attributes_array']['resource'] = $parent_uri; + } + + // The second information we annotate is the name of the user with the + // 'property' attribute. We do not need to specify the RDF object here + // because it's the value inside the a tag which will be used + // automatically according to the RDFa parsing rules. + $variables['attributes_array']['property'] = $account->rdf_mapping['name']['property']; + } + } +} + +/** + * Implements MODULE_preprocess_HOOK(). + */ +function rdf_preprocess_comment(&$variables) { + $comment = $variables['comment']; + if (!empty($comment->rdf_mapping['rdftype'])) { + // Add 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']; + } + + // RDFa markup for the date of the comment. + if (!empty($comment->rdf_mapping['created'])) { + $date_attributes_array = drupal_rdfa_attributes($comment->rdf_mapping['created'], $comment->created); + $variables['rdf_variable_attributes_array']['created'] = $date_attributes_array; + } + if (!empty($comment->rdf_mapping['title'])) { + // Add 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']['property']; + $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']['property']; + } + + // 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']['property']; + $parent_node_attributes['resource'] = url('node/' . $comment->nid); + $variables['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']['property']; + $parent_comment_attributes['resource'] = url('comment/' . $comment->pid, array('fragment' => 'comment-' . $comment->pid)); + $variables['metadata_attributes_array'][] = $parent_comment_attributes; + } + } +} + +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']['property'])) { + $variables['link_options']['attributes']['property'] = $term->rdf_mapping['name']['property']; + } +} + +/** + * Returns the default RDF mapping for the given entity type. + * + * @param $type + * An entity type. + * @return array + * The RDF mapping or an empty array. + */ +function _rdf_get_default_mapping($type) { + $default_mappings = &drupal_static(__FUNCTION__, array()); + + if (empty($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 empty($default_mappings[$type]) ? array() : $default_mappings[$type]; +} + +/** + * Create an RDF mapping binded to a bundle and an entity type. + * + * RDF CRUD API, handling RDF mapping creation and deletion. + * + * @param $type + * The entity type the mapping refers to (node, user, comment, term, etc.). + * @param $bundle + * The bundle the mapping refers to. + * @param $mapping + * An associative array represeting an RDF mapping structure. + * @return array + * The stored mapping. + */ +function rdf_create_mapping($type, $bundle, $mapping) { + $fields = array( + 'type' => $type, + 'bundle' => $bundle, + 'mapping' => serialize($mapping) + ); + + db_insert('rdf_mapping')->fields($fields)->execute(); + + return $mapping; +} + +/** + * Read an RDF mapping record directly from the database. + * + * RDF CRUD API, handling RDF mapping creation and deletion. + * + * @param $type + * The entity type the mapping refers to. + * @param $bundle + * The bundle the mapping refers to. + * @return array + * An RDF mapping structure or FALSE if the mapping could not be found. + */ +function rdf_read_mapping($type, $bundle) { + $query = db_select('rdf_mapping')->fields(NULL, array('mapping')) + ->condition('type', $type)->condition('bundle', $bundle)->execute(); + + $mapping = unserialize($query->fetchField()); + + if (!is_array($mapping)) { + $mapping = array(); + } + + return $mapping; +} + +/** + * Update an RDF mapping binded to a bundle and an entity type. + * + * RDF CRUD API, handling RDF mapping creation and deletion. + * + * @param $type + * The entity type the mapping refers to. + * @param $bundle + * The bundle the mapping refers to. + * @param $mapping + * An associative array representing an RDF mapping structure. + * @return bool + * Return boolean TRUE if mapping updated, FALSE if not. + */ +function rdf_update_mapping($type, $bundle, $mapping) { + $fields = array('mapping' => serialize($mapping)); + $num_rows = db_update('rdf_mapping')->fields($fields) + ->condition('type', $type)->condition('bundle', $bundle)->execute(); + + return (bool) ($num_rows > 0); +} + +/** + * Delete the mapping for the given pair of type and bundle from the database. + * + * RDF CRUD API, handling RDF mapping creation and deletion. + * + * @param $type + * The entity type the mapping refers to. + * @param $bundle + * The bundle the mapping refers to. + * @return bool + * Return boolean TRUE if mapping deleted, FALSE if not. + */ +function rdf_delete_mapping($type, $bundle) { + $num_rows = db_delete('rdf_mapping')->condition('type', $type) + ->condition('bundle', $bundle)->execute(); + + return (bool) ($num_rows > 0); +} === added file 'modules/rdf/rdf.test' --- modules/rdf/rdf.test 1970-01-01 00:00:00 +0000 +++ modules/rdf/rdf.test 2009-10-17 22:55:50 +0000 @@ -0,0 +1,193 @@ + t('RDF mapping hook'), + 'description' => t('Test hook_rdf_mapping().'), + 'group' => t('RDF'), + ); + } + + function setUp() { + parent::setUp('rdf', 'rdf_test', 'field_test'); + // We need to trigger rdf_modules_installed() because + // hook_modules_installed() is not automatically invoked during testing. + rdf_modules_installed(array('rdf_test')); + } + + /** + * Test that hook_rdf_mapping() correctly returns and processes mapping. + */ + function testMapping() { + // Test that the mapping is returned correctly by the hook. + $mapping = rdf_get_mapping('test_entity', 'test_bundle'); + $this->assertIdentical($mapping['rdftype'], array('sioc:Post'), t('Mapping for rdftype is sioc:Post.')); + $this->assertIdentical($mapping['title'], array('property' => array('dc:title')), t('Mapping for title is dc:title.')); + $this->assertIdentical($mapping['created'], array( + 'property' => array('dc:created'), + 'datatype' => 'xsd:dateTime', + 'callback' => 'date_iso8601', + ), t('Mapping for created is dc:created with datatype xsd:dateTime and callback date_iso8601.')); + $this->assertIdentical($mapping['uid'], array('property' => array('sioc:has_creator', 'dc:creator')), t('Mapping for uid is sioc:has_creator and dc:creator.')); + + $mapping = rdf_get_mapping('test_entity', 'test_bundle_no_mapping'); + $this->assertEqual($mapping, array(), t('Empty array returned when an entity type, bundle pair has no mapping.')); + } +} + +class RdfMarkupTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => t('RDFa markup'), + 'description' => t('Test RDFa markup generation.'), + 'group' => t('RDF'), + ); + } + + function setUp() { + parent::setUp('rdf', 'field_test', 'rdf_test'); + rdf_modules_installed(array('field_test', 'rdf_test')); + } + + /** + * Test drupal_rdfa_attributes(). + */ + function testDrupalRdfaAtributes() { + $date = 1252750327; + $isoDate = date('c', $date); + + $expected_type = 'xsd:dateTime'; + $expected_property = array('dc:created'); + $expected_value = $isoDate; + + $mapping = rdf_get_mapping('test_entity', 'test_bundle'); + $attributes = drupal_rdfa_attributes($mapping['created'], $date); + + $this->assertEqual($expected_type, $attributes['datatype']); + $this->assertEqual($expected_property, $attributes['property']); + $this->assertEqual($expected_value, $attributes['content']); + } + +} + +class RdfCrudTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'RDF mapping CRUD functions', + 'description' => 'Test the RDF mapping CRUD functions.', + 'group' => 'RDF', + ); + } + + function setUp() { + parent::setUp('rdf', 'rdf_test'); + } + + function testCreateReadUpdateWrite() { + $test_mapping = rdf_test_rdf_mapping(); + + $this->assertTrue(is_array(rdf_read_mapping('test_entity', 'test_bundle'))); + $this->assertEqual(count(rdf_read_mapping('test_entity', 'test_bundle')), 0); + $this->assertEqual( + rdf_create_mapping('test_entity', 'test_bundle', $test_mapping[0]['mapping']), + $test_mapping[0]['mapping'] + ); + + try { + rdf_create_mapping('test_entity', 'test_bundle', $test_mapping[0]['mapping']); + $this->fail('No Exception thrown when attempting to insert the same mapping another time.'); + } + catch (Exception $e) { + $this->pass('Exception thrown when attempting to insert the same mapping another time.'); + } + + $this->assertEqual($test_mapping[0]['mapping'], + rdf_read_mapping('test_entity', 'test_bundle')); + $this->assertTrue(rdf_update_mapping('test_entity', 'test_bundle', + $test_mapping[1]['mapping'])); + $this->assertEqual($test_mapping[1]['mapping'], + rdf_read_mapping('test_entity', 'test_bundle')); + $this->assertTrue(rdf_delete_mapping('test_entity', 'test_bundle')); + $this->assertFalse(rdf_read_mapping('test_entity', 'test_bundle')); + } + + function testSaveMapping() { + $test_mapping = rdf_test_rdf_mapping(); + rdf_save_mapping($test_mapping[0]); + + $this->assertEqual($test_mapping[0]['mapping'], + rdf_read_mapping('test_entity', 'test_bundle')); + } + +} + +class RdfMappingDefinitionTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => t('RDF mapping definition functionality'), + 'description' => t('Test the different types of RDF mappings and ensure the proper RDFa markup in included in node pages.'), + 'group' => t('RDF'), + ); + } + + function setUp() { + parent::setUp('rdf', 'rdf_test', 'blog'); + // We need to trigger rdf_modules_installed() because + // hook_modules_installed() is not automatically invoked during testing. + rdf_modules_installed(array('rdf_test', 'node')); + // entity_info caches must be cleared during testing. This is done + // automatically during the manual installation. + cache_clear_all('entity_info', 'cache'); + drupal_static_reset('entity_get_info'); + } + + /** + * Create a node of type blog and test whether the RDF mapping defined for + * this node type in rdf_test.module is used in the node page. + */ + function testAttributesInMarkup1() { + $node = $this->drupalCreateNode(array('type' => 'blog')); + $this->drupalGet('node/' . $node->nid); + + $this->assertRaw('typeof="sioct:Weblog"'); + // Ensure the default bundle mapping for node is used. These attributes come + // from the node default bundle definition. + $this->assertRaw('property="dc:title"'); + $this->assertRaw('property="dc:date dc:created"'); + + } + + /** + * Create a content type and a node of type test_bundle_hook_install and test + * whether the RDF mapping defined in rdf_test.install is used. + */ + function testAttributesInMarkup2() { + $type = $this->drupalCreateContentType(array('type' => 'test_bundle_hook_install')); + $node = $this->drupalCreateNode(array('type' => 'test_bundle_hook_install')); + $this->drupalGet('node/' . $node->nid); + + $this->assertRaw('typeof="foo:mapping_install1 bar:mapping_install2"'); + // Ensure the default bundle mapping for node is used. These attributes come + // from the node default bundle definition. + $this->assertRaw('property="dc:title"'); + $this->assertRaw('property="dc:date dc:created"'); + } + + /** + * Create a random content type and node and ensure the default mapping for + * node is used. + */ + function testAttributesInMarkup3() { + $type = $this->drupalCreateContentType(); + $node = $this->drupalCreateNode(array('type' => $type->type)); + $this->drupalGet('node/' . $node->nid); + + $this->assertRaw('typeof="sioc:Item foaf:Document"'); + // Ensure the default bundle mapping for node is used. These attributes come + // from the node default bundle definition. + $this->assertRaw('property="dc:title"'); + $this->assertRaw('property="dc:date dc:created"'); + } +} === added directory 'modules/rdf/tests' === added file 'modules/rdf/tests/rdf_test.info' --- modules/rdf/tests/rdf_test.info 1970-01-01 00:00:00 +0000 +++ modules/rdf/tests/rdf_test.info 2009-10-08 23:37:04 +0000 @@ -0,0 +1,9 @@ +; $Id$ +name = "RDF module tests" +description = "Support module for RDF module testing." +package = Testing +version = VERSION +core = 7.x +files[] = rdf_test.install +files[] = rdf_test.module +hidden = TRUE === added file 'modules/rdf/tests/rdf_test.install' --- modules/rdf/tests/rdf_test.install 1970-01-01 00:00:00 +0000 +++ modules/rdf/tests/rdf_test.install 2009-10-14 10:48:10 +0000 @@ -0,0 +1,26 @@ + 'node', + 'bundle' => 'test_bundle_hook_install', + 'mapping' => array( + 'rdftype' => array('foo:mapping_install1', 'bar:mapping_install2'), + ), + ), + ); + + foreach ($rdf_mappings as $rdf_mapping) { + rdf_save_mapping($rdf_mapping); + } +} === added file 'modules/rdf/tests/rdf_test.module' --- modules/rdf/tests/rdf_test.module 1970-01-01 00:00:00 +0000 +++ modules/rdf/tests/rdf_test.module 2009-10-14 10:48:14 +0000 @@ -0,0 +1,44 @@ + array( + 'type' => 'test_entity', + 'bundle' => 'test_bundle', + 'mapping' => array( + 'rdftype' => array('sioc:Post'), + 'title' => array( + 'property' => array('dc:title'), + ), + 'created' => array( + 'property' => array('dc:created'), + 'datatype' => 'xsd:dateTime', + 'callback' => 'date_iso8601', + ), + 'uid' => array( + 'property' => array('sioc:has_creator', 'dc:creator'), + ), + 'foobar' => array( + 'property' => array('foo:bar'), + ), + ), + ), + 1 => array( + 'type' => 'node', + 'bundle' => 'blog', + 'mapping' => array( + 'rdftype' => array('sioct:Weblog'), + ) + ), + ); +} === modified file 'modules/system/html.tpl.php' --- modules/system/html.tpl.php 2009-09-15 17:10:38 +0000 +++ modules/system/html.tpl.php 2009-10-17 13:24:43 +0000 @@ -35,7 +35,7 @@ */ ?> -> === modified file 'modules/system/system.module' --- modules/system/system.module 2009-10-18 11:34:44 +0000 +++ modules/system/system.module 2009-10-18 14:29:52 +0000 @@ -265,15 +265,18 @@ function system_rdf_namespaces() { return array( 'admin' => 'http://webns.net/mvcb/', 'content' => 'http://purl.org/rss/1.0/modules/content/', - 'dc' => 'http://purl.org/dc/elements/1.1/', - 'dcterms' => 'http://purl.org/dc/terms/', + 'dc' => 'http://purl.org/dc/terms/', 'foaf' => 'http://xmlns.com/foaf/0.1/', 'owl' => 'http://www.w3.org/2002/07/owl#', 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#', 'rss' => 'http://purl.org/rss/1.0/', + 'tags' => 'http://www.holygoat.co.uk/owl/redwood/0.1/tags/', 'sioc' => 'http://rdfs.org/sioc/ns#', - 'xsd' => 'http://www.w3.org/2001/XMLSchema', + 'sioct' => 'http://rdfs.org/sioc/types#', + 'ctag' => 'http://commontag.org/ns#', + 'skos' => 'http://www.w3.org/2004/02/skos/core#', + 'xsd' => 'http://www.w3.org/2001/XMLSchema#', ); } === modified file 'modules/taxonomy/taxonomy.module' --- modules/taxonomy/taxonomy.module 2009-10-18 04:50:08 +0000 +++ modules/taxonomy/taxonomy.module 2009-10-18 15:35:52 +0000 @@ -1102,7 +1102,8 @@ function taxonomy_field_formatter_info() */ function theme_field_formatter_taxonomy_term_link($variables) { $term = $variables['element']['#item']['taxonomy_term']; - return l($term->name, taxonomy_term_path($term)); + $attributes = empty($variables['link_options']) ? array() : $variables['link_options']; + return l($term->name, taxonomy_term_path($term), $attributes); } /** @@ -1406,6 +1407,47 @@ function taxonomy_field_settings_form($f } /** + * Implement hook_rdf_mapping(). + * + * @return array + * The rdf mapping for vocabularies and terms. + */ +function taxonomy_rdf_mapping() { + return array( + array( + 'type' => 'taxonomy_term', + 'bundle' => RDF_DEFAULT_BUNDLE, + 'mapping' => array( + 'rdftype' => array('skos:Concept'), + 'name' => array( + 'property' => array('skos:prefLabel'), + ), + 'description' => array( + 'property' => array('skos:definition'), + ), + // The vocabulary this term belongs to. + 'vid' => array( + 'property' => array('skos:member'), + ), + ), + ), + array( + 'type' => 'taxonomy_vocabulary', + 'bundle' => RDF_DEFAULT_BUNDLE, + 'mapping' => array( + 'rdftype' => array('skos:Collection'), + 'name' => array( + 'property' => array('rdfs:label'), + ), + 'description' => array( + 'property' => array('rdfs:comment'), + ), + ), + ), + ); +} + +/** * @defgroup taxonomy indexing Taxonomy functions maintaining {taxonomy_index}. * * Taxonomy uses default field storage to store canonical relationships @@ -1495,4 +1537,3 @@ function taxonomy_taxonomy_term_delete($ /** * @} End of "defgroup taxonomy indexing" */ - === modified file 'modules/user/user.module' --- modules/user/user.module 2009-10-18 11:34:44 +0000 +++ modules/user/user.module 2009-10-18 16:37:16 +0000 @@ -3255,3 +3255,21 @@ function user_cookie_save(array $values, } } } + +/** + * Implementation of hook_rdf_mapping(). + */ +function user_rdf_mapping() { + return array( + 'user' => array( + 'type' => 'user', + 'bundle' => RDF_DEFAULT_BUNDLE, + 'mapping' => array( + 'rdftype' => array('sioc:User'), + 'name' => array( + 'property' => array('foaf:name'), + ), + ), + ), + ); +} === modified file 'profiles/default/default.info' --- profiles/default/default.info 2009-10-17 01:15:39 +0000 +++ profiles/default/default.info 2009-10-17 06:32:28 +0000 @@ -18,4 +18,5 @@ dependencies[] = shortcut dependencies[] = toolbar dependencies[] = field_ui dependencies[] = file +dependencies[] = rdf files[] = default.profile === modified file 'profiles/default/default.install' --- profiles/default/default.install 2009-10-18 07:50:45 +0000 +++ profiles/default/default.install 2009-10-18 14:29:52 +0000 @@ -166,6 +166,28 @@ function default_install() { node_type_save($type); } + // Insert default user-defined RDF mapping into the database. + $rdf_mappings = array( + array( + 'type' => 'node', + 'bundle' => 'page', + 'mapping' => array( + 'rdftype' => array('foaf:Document'), + ), + ), + array( + 'type' => 'node', + 'bundle' => 'article', + 'mapping' => array( + 'rdftype' => array('sioc:Item', 'foaf:Document'), + ), + ), + ); + + foreach ($rdf_mappings as $rdf_mapping) { + rdf_save_mapping($rdf_mapping); + } + // Default page to not be promoted and have comments disabled. variable_set('node_options_page', array('status')); variable_set('comment_page', COMMENT_NODE_HIDDEN);