'Map', 'page callback' => 'feedapi_mapper_page', 'page arguments' => array(1), 'type' => MENU_LOCAL_TASK, 'access callback' => 'feedapi_mapper_access_mapper', 'access arguments' => array(1), ); $items['node/%node/map/delete/%'] = array( 'title' => 'Delete', 'page callback' => 'drupal_get_form', 'page arguments' => array('feedapi_mapper_delete_form', 1, 4), 'type' => MENU_CALLBACK, 'access callback' => 'feedapi_mapper_access_mapper', 'access arguments' => array(1), ); foreach (node_get_types() as $type) { $type_url_str = str_replace('_', '-', $type->type); $items['admin/content/node-type/'. $type_url_str .'/map'] = array( 'title' => 'Map', 'page callback' => 'feedapi_mapper_page', 'page arguments' => array(3), 'load arguments' => array(3), 'type' => MENU_LOCAL_TASK, 'access callback' => 'feedapi_mapper_access_mapper', 'access arguments' => array(3), ); $items['admin/content/node-type/'. $type_url_str .'/map/delete/%'] = array( 'title' => 'Delete', 'page callback' => 'drupal_get_form', 'page arguments' => array('feedapi_mapper_delete_form', 3, 6), 'load arguments' => array(3), 'type' => MENU_LOCAL_TASK, 'access callback' => 'feedapi_mapper_access_mapper', 'access arguments' => array(3), ); } return $items; } /** * Implementation of hook_nodeapi(). */ function feedapi_mapper_nodeapi(&$node, $op, $teaser, $page) { switch ($op) { case 'prepare': if (isset($node->feedapi) && $node->feedapi->feed_item) { _feedapi_mapper_map($node); } break; } } /** * Implementation of hook_theme(). */ function feedapi_mapper_theme() { return array( 'feedapi_mapper_form' => array( 'arguments' => array('form'), ), 'feedapi_mapper_descriptions' => array( 'arguments' => array('descriptions'), ), ); } /** * Theming function for outputting result of _feedapi_mapper_get_field_mappers_descriptions(). * @param $descriptions * Result of _feedapi_mapper_get_field_mappers_descriptions(). * @return * HTML output. */ function theme_feedapi_mapper_descriptions($descriptions) { $output = '
'; foreach ($descriptions as $module => $description) { $output .= '
'. t('(!module_name module)', array('!module_name' => $module)) .'
'; $output .= '
'. $description .'
'; } $output .= '
'; return $output; } /** * Theming function for showing the current mapping. */ function theme_feedapi_mapper_form($form) { $output = ''; $url = isset($form['#node']->nid) ? 'node/' . $form['#node']->nid .'/map/delete/' : 'admin/content/node-type/'. $form['#node']->type .'/map/delete/'; foreach ($form['#mapping'] as $feed_path => $node_path) { $rows[] = array( $form['#feed_map'][$feed_path], $form['#field_map'][$node_path], l(t('Delete'), $url . base64_encode($feed_path)), ); } $rows[] = array( drupal_render($form['source']), drupal_render($form['target']), drupal_render($form['add']), ); $form['mapping']['table']['#value'] = theme('table', array(t('Feed item'), t('Node')), $rows); $output .= drupal_render($form); return $output; } /** * Determine whether current user can map on a given content type. * Primarily used as menu acess callback. @see feedapi_mapper_menu() * * @param $node * String that defines a content type or node object. * @return * TRUE if current user can map feed elements AND content type is feed enabled. * FALSE otherwise. */ function feedapi_mapper_access_mapper($node) { if (user_access('administer feedapi')) { if (is_string($node)) { // String comes from directly from path, substitute - with _. $node_type = str_replace('-', '_', $node); } elseif (is_string($node->type)) { $node_type = $node->type; } else { return FALSE; } $settings = feedapi_get_settings($node_type); if (!empty($settings['processors']['feedapi_node']['enabled'])) { return TRUE; } } return FALSE; } /** * Map feed item elements to node. */ function _feedapi_mapper_map(&$node) { // Load mapping stored for this node. $feed_node = node_load($node->feedapi->feed_nid); if (!$mapping = _feedapi_mapper_load_mapping($node->feedapi->feed_nid)) { $mapping = _feedapi_mapper_load_mapping($feed_node->type); } if ($mapping) { // Load available mappers. _feedapi_mapper_load_mappers(); // Convert item to array: $feed_item = _feedapi_mapper_obj2array($node->feedapi->feed_item); $feed_item = _feedapi_mapper_simplify_raw($feed_item, $feed_node->feed->parsers); foreach ($mapping as $element_path => $field) { if ($field) { // Double check if field is set - @todo: this step could be skipped. $field = unserialize($field); $element_path = unserialize($element_path); // Get the feed item element on $element_path and pass it into the mapping function. $feed_item_element = _feedapi_mapper_get_feed_item_element($element_path, $feed_item); $node = call_user_func($field[0] .'_feedapi_mapper', 'map', $node, $feed_item_element, $field[1], isset($field[2]) ? $field[2] : NULL); } } } } /** * Returns feed item element on given path. */ function _feedapi_mapper_get_feed_item_element($path, $item) { $p = array_shift($path); if (count($path) > 0) { return _feedapi_mapper_get_feed_item_element($path, $item[$p]); } else if (isset($item[$p])) { return $item[$p]; } } /** * Page callback for deleting field mapings. */ function feedapi_mapper_delete_form($form_state, $param, $encoded_key) { $form = array(); $key = base64_decode($encoded_key); if (is_string($param)) { $node = new stdClass(); $node->type = str_replace('-', '_', $param); $path = 'admin/content/node-type/'. $param .'/map'; } else { $node = $param; $path = "node/{$node->nid}/map"; } $form['#node'] = $node; $form['#redirect'] = $path; $form['#source'] = $key; return confirm_form($form, t('Are you sure you want to delete this mapping?'), $path, t('Are you sure you want to delete the mapping for the %path feed element? This cannot be undone.', array('%path' => join('->', unserialize($key)))), t('Delete'), t('Cancel') ); } function feedapi_mapper_delete_form_submit($form, &$form_state) { $param = isset($form['#node']->nid) ? $form['#node']->nid : $form['#node']->type; feedapi_mapper_delete_mapping($param, $form['#source']); drupal_set_message(t('The mapping entry has been deleted successfully')); } /** * Callback function for hook_menu(). */ function feedapi_mapper_page($node) { if (is_string($node)) { $node_type = $node; $node = new stdClass(); $node->type = str_replace('-', '_', $node_type); } $names = node_get_types('names'); drupal_set_title(check_plain($node->title ? $node->title : $names[$node->type])); $output = t('Map feed item elements to feed item node fields.'); $output .= drupal_get_form('feedapi_mapper_form', $node); return $output; } /** * Mapping form. */ function feedapi_mapper_form($form_state, $node) { // Get fields of node type with available feed element mappers. if (isset($node->feed->settings['processors']['feedapi_node']['content_type'])) { $settings = $node->feed->settings; } else { $settings = feedapi_get_settings($node->type); } $feed_item_type = $settings['processors']['feedapi_node']['content_type']; $field_map = _feedapi_mapper_get_field_mappers($feed_item_type); // Get elements of feed items. $elements = array(); if ($merged_item = _feedapi_mapper_get_items_merged($node)) { $merged_item = _feedapi_mapper_simplify_raw($merged_item, $node->feed->parsers); $elements = _feedapi_mapper_get_feed_elements($merged_item); } $elements = array_merge($elements, _feedapi_mapper_get_standard_elements()); asort($elements); $feed_map = array_flip($elements); // Load stored mapping. if (!$mapping = _feedapi_mapper_load_mapping($node->nid)) { $mapping = _feedapi_mapper_load_mapping($node->type); } $form['#node'] = $node; $form['#feed_map'] = $feed_map; $form['#field_map'] = $field_map; // Flatten array for theme_feedapi_mapper_form(). foreach ($form['#field_map'] as $k => $v) { if (is_array($v)) { unset($form['#field_map'][$k]); $form['#field_map'] = array_merge($form['#field_map'], $v); } } $form['#mapping'] = $mapping; // Create a placeholder for mapping form elements. This will be populated in // theme_feedapi_mapper_form(). $form['mapping'] = array( '#type' => 'fieldset', '#title' => t('Mapping'), ); // Filter out maps that already exist from our list since we don't support multiple mappings. $feed_map_options = array_merge(array(t('Select a source')), $feed_map); foreach ($mapping as $feed_path => $node_path) { if (isset($feed_map_options[$feed_path])) { unset($feed_map_options[$feed_path]); } } $field_map_options = array_merge(array(t('Select a target')), $field_map); foreach ($mapping as $feed_path => $node_path) { if (isset($field_map_options[$node_path])) { unset($field_map_options[$node_path]); } } $form['source'] = array( '#type' => 'select', '#options' => $feed_map_options, ); $form['target'] = array( '#type' => 'select', '#options' => $field_map_options, ); $form['add'] = array( '#type' => 'submit', '#value' => t('Add'), ); // Print descriptions for available mapping targets. // This is done by iterating through $field_map and extracting module names from it. $descriptions = _feedapi_mapper_get_field_mappers_descriptions($feed_item_type); $descriptions_filtered = array(); foreach ($field_map as $key => $target) { if (is_array($target)) { $key = unserialize(key($target)); } else { $key = unserialize($key); } if (is_array($key)) { $module = $key[0]; $descriptions_filtered[$module] = $descriptions[$module]; } } if ($merged_item) { $form['feed_item'] = array( '#type' => 'fieldset', '#title' => t('Feed item example'), '#collapsible' => TRUE, '#collapsed' => TRUE, '#description' => t('All feed items of this feed merged into one. Here you can see which feed item elements are available for mapping. As this view is derived from the actual feed items on this feed, there might be more mappable elements than those listed here.'), ); $form['feed_item']['item'] = array( '#type' => 'markup', '#value' => '
'. print_r(_feedapi_mapper_truncate_array($merged_item), TRUE) .'
', ); } if ($descriptions = theme('feedapi_mapper_descriptions', $descriptions_filtered)) { $form['descriptions'] = array( '#type' => 'fieldset', '#title' => t('Description of available mappers'), '#description' => t('This is a list of mappers available for the feed item content type for this feed.'), '#tree' => TRUE, ); $form['descriptions']['descriptions'] = array( '#type' => 'markup', '#value' => $descriptions, ); } return $form; } /** * Validate hook. */ function feedapi_mapper_form_validate($form, &$form_state) { if (empty($form_state['values']['source'])) { form_set_error('source', t('You need to specify a valid source from the dropdown list.')); } elseif (empty($form_state['values']['target'])) { form_set_error('target', t('You need to specify a valid destination from the dropdown list.')); } } /** * Submit hook. */ function feedapi_mapper_form_submit($form, &$form_state) { $param = $form['#node']->nid ? $form['#node']->nid : $form['#node']->type; feedapi_mapper_add_mapping($param, $form_state['values']['source'], $form_state['values']['target']); } /** * Add an additional map to a given mapping. * * @param $param * Either a node id of a node type. Used to determine storage. * @param $source * A serialized string mapping to the source feed element. * @param $target * A serialized string mapping to the target node element. */ function feedapi_mapper_add_mapping($param, $source, $target) { if (is_numeric($param)) { $map = unserialize(db_result(db_query('SELECT mapping FROM {feedapi_mapper} WHERE nid = %d', $param))); if (is_array($map)) { $map[$source] = $target; db_query('UPDATE {feedapi_mapper} SET mapping = \'%s\' WHERE nid = %d', serialize($map), $param); } else { $map = array($source => $target); db_query("INSERT INTO {feedapi_mapper} (nid, mapping) VALUES (%d, '%s')", $param, serialize($map)); } } else if (is_string($param)) { $variable = 'feedapi_mapper_mapping_'. $param; $map = variable_get($variable, array()); $map[$source] = $target; variable_set($variable, $map); } } function feedapi_mapper_delete_mapping($param, $source) { if (is_numeric($param)) { if ($map = unserialize(db_result(db_query('SELECT mapping FROM {feedapi_mapper} WHERE nid = %d', $param)))) { unset($map[$source]); db_query('UPDATE {feedapi_mapper} SET mapping = \'%s\' WHERE nid = %d', serialize($map), $param); } } else if (is_string($param)) { $variable = 'feedapi_mapper_mapping_'. $param; $map = variable_get($variable, array()); unset($map[$source]); variable_set($variable, $map); } } /** * Retrieve mapping from db. * @param $param * node id or node type * @return * Associative array in the format * array( * element_path1 => node_field1, * element_path2 => node_field2, * ); * where element_pathx and node_fieldx are both serialized arrays. */ function _feedapi_mapper_load_mapping($param) { static $mappings; if (is_numeric($param)) { if (isset($mappings[$param])) { return $mappings[$param]; } if ($mapping = db_result(db_query('SELECT mapping FROM {feedapi_mapper} WHERE nid = %d', $param))) { $mappings[$param] = unserialize($mapping); return $mappings[$param]; } } else if (is_string($param)) { return variable_get('feedapi_mapper_mapping_'. $param, array()); } } /** * Load mapper implementations. */ function _feedapi_mapper_load_mappers() { static $loaded = FALSE; if (!$loaded) { // Load all feedapi mapper implementations from ./mappers $path = drupal_get_path('module', 'feedapi_mapper') .'/mappers'; $files = drupal_system_listing('.*\.inc$', $path, 'name', 0); foreach ($files as $file) { require_once("./$file->filename"); } // Rebuild cache. module_implements('', FALSE, TRUE); } $loaded = TRUE; } /** * @todo: move this function to feedapi as feedapi_parse(). */ function _feedapi_parse($feed) { return _feedapi_call_parsers($feed, $feed->parsers, $feed->half_done ? FALSE : TRUE); } /** * Simplifies options->raw item on feed item. */ function _feedapi_mapper_simplify_raw($item, $parsers) { if (isset($item['options']['raw'])) { if (in_array('parser_simplepie', $parsers)) { $item['options']['raw'] = parser_simplepie_simplify_raw_item($item['options']['raw']); } } return $item; } /** * Returns all feed items on node as one merged item. */ function _feedapi_mapper_get_items_merged($node) { if ($node->feed) { $feed = _feedapi_parse($node->feed); // Convert items to array. $items = _feedapi_mapper_obj2array($feed->items); // Merge items to one item. $merged = NULL; if (is_array($items)) { $i = 0; foreach ($items as $item) { if ($i++ > 10) { break; } $merged = _feedapi_mapper_array_merge_recursive($item, $merged); } } return $merged; } } /** * Sister function of _feedapi_mapper_get_feed_elements(). * Returns array in same format. Only difference: does not take * a real feed that it analyzes, but returns some standard elements. * * Standard elements: * * options->original_author->name * options->original_author->picture * options->original_author * options->tags * * Implement hook_feedapi_mapper_elements() to add custom standard * elements. * * @see hook_feedapi_mapper_elements(). * * @return * Array in the format array('pathname' => 'serializedpath') */ function _feedapi_mapper_get_standard_elements() { $paths = array(); $paths[] = array('options', 'original_author', 'name'); $paths[] = array('options', 'original_author', 'picture'); $paths[] = array('options', 'original_author'); $paths[] = array('options', 'tags'); // hook_feedapi_mapper_elements() - allow other modules to add paths. // These should be in the exact same format as the paths above, i.e.: // array(array('foo', 'bar'), array('fnargle', 'bargle'),); foreach (module_implements('feedapi_mapper_elements') as $module) { $result = call_user_func($module .'_feedapi_mapper_elements'); if (is_array($result)) { $paths = array_merge($paths, $result); } } foreach ($paths as $path) { $elements[implode('->', $path)] = serialize($path); } return $elements; } /** * Takes a feed item and retrieves paths to all elements. * Use a merged feed item (_feedapi_mapper_get_items_merged()) for best results. * * @return * Array in the format array('pathname' => 'serializedpath') */ function _feedapi_mapper_get_feed_elements($merged_item) { // Retrieve paths to elements in merged item. // Stick them into an array where the key is the serialized path and the value is the element name. $elements = array(); while (count($merged_item)) { $path = array(); $path = _feedapi_mapper_next_element_path($merged_item, $path, TRUE); if ($path == FALSE) { break; } $elements[implode('->', $path)] = serialize($path); } return $elements; } /** * Traverse an associative array and look for path to first leaf. * If found, unset leaf and return path to it. * * @return * A path to a leaf element in the format * array(path, to, leaf, element); */ function _feedapi_mapper_next_element_path(&$items, &$path, $reset_count = FALSE) { // This recursion is a bit shaky. Put on breaks. static $i = 0; $i++; if ($reset_count) { $i = 0; } if ($i > 200) { drupal_set_message(t('Error in recursion _feedapi_mapper_next_element_path()'), 'error'); return FALSE; } foreach ($items as $key => $value) { $path[] = $key; // Recurse if value is array and if it contains elements. if (is_array($items[$key]) && count($items[$key])) { // Arrays with keys of 0 are not considered collections of same items - reached a leaf. if (isset($items[$key][0])) { unset($items[$key]); return $path; } else if ($result_path = _feedapi_mapper_next_element_path($items[$key], $path)) { // Leaf was found, pass it up. return $result_path; } } else { // Reached leaf, unset it and return path to it. unset($items[$key]); return $path; } } // No leaves found in this pass. return FALSE; } /** * Converts a multidemensional associative array with interdispersed objects into * an associative array only. */ function _feedapi_mapper_obj2array($items) { if (is_object($items)) { $items = (array) $items; } if (is_array($items)) { foreach ($items as $key => $value) { $items[$key] = _feedapi_mapper_obj2array($value); } } return $items; } /** * Truncates all strings in cascaded array. */ function _feedapi_mapper_truncate_array($array) { foreach ($array as $k => $v) { if (is_string($v)) { $array[$k] = strip_tags($v); $array[$k] = truncate_utf8($array[$k], 200, TRUE, TRUE); } else if (is_array($v)) { $array[$k] = _feedapi_mapper_truncate_array($v); } } return $array; } /** * Like array_merge_recursive. Only difference: does NOT merge * two different keys into an array, but merges key on key. * Argument 1 always has to be an array. */ function _feedapi_mapper_array_merge_recursive($array1, $array2) { $result = NULL; foreach ($array1 as $k => $v) { if (is_array($array1[$k])) { $result[$k] = _feedapi_mapper_array_merge_recursive($array1[$k], isset($array2[$k]) ? $array2[$k] : NULL); } else { $result[$k] = $array1[$k]; } } if (is_array($array2)) { foreach ($array2 as $k => $v) { if (is_array($array2[$k])) { $result[$k] = _feedapi_mapper_array_merge_recursive($array2[$k], isset($array1[$k]) ? $array1[$k] : NULL); } else { $result[$k] = $array2[$k]; } } } return $result; } /** * Get field mappers for a given node type. Returns an array of all * feed mappers that are applicable to this node type. * @param $node_type * Valid node type. * @return * Array of fields that are mappable for this content type. */ function _feedapi_mapper_get_field_mappers($node_type) { $node = new stdClass(); $node->type = $node_type; // Load all available mappers and create an array of fields available as mapping target. _feedapi_mapper_load_mappers(); $modules = module_implements('feedapi_mapper'); foreach ($modules as $module) { if ($fields = module_invoke($module, 'feedapi_mapper', 'list', $node)) { foreach ($fields as $field_name => $sub_fields) { $field_label = is_string($sub_fields) ? $sub_fields : $field_name; $field_category = t('Map to @field (@module)', array('@field' => $field_label, '@module' => $module)); if (is_array($sub_fields)) { foreach ($sub_fields as $sub_field_id => $sub_field_name) { $field_mappers[$field_category][serialize(array($module, $field_name, $sub_field_id))] = $field_category .': '. $sub_field_name; } } else { $field_mappers[serialize(array($module, $field_name))] = $field_category; } } } } return $field_mappers; } /** * Returns descriptions for all mappable fields of given node type. * @return * Array in the format * array('field_name_a' => * array('module_name_a' => 'Descriptive text'), * 'module_name_b' => ...), * 'field_name_b' => array(...), * ); */ function _feedapi_mapper_get_field_mappers_descriptions($node_type) { $node = new stdClass(); $node->type = $node_type; // Load all available mappers and create an array of fields available as mapping target. _feedapi_mapper_load_mappers(); $modules = module_implements('feedapi_mapper'); $descriptions = array(); foreach ($modules as $module) { if ($description = module_invoke($module, 'feedapi_mapper', 'describe', $node)) { $descriptions[$module] = $description; } } return $descriptions; }