Skip to content
plugins.inc 47.2 KiB
Newer Older
/**
 * @file plugins.inc
 *
 * Contains helper code for plugins and contexts.
 */

/**
 * Forms the basis for describing and storing a context that a display
 * might be running in.
 */
class panels_context {
  var $type = NULL;
  var $data = NULL;
  // The title of this object.
  var $title = '';
  // The title of the page if this object exists
  var $page_title = '';
  // The identifier (in the UI) of this object
  var $identifier = '';
  var $argument = NULL;
  var $keyword = '';

  function panels_context($type = 'none', $data = NULL) {
    $this->type  = $type;
    $this->data  = $data;
    $this->title = t('Unknown context');
  }

  function is_type($type) {
    if ($type == 'any' || $this->type == 'any') {
      return TRUE;
    }

    $a = is_array($type) ? $type : array($type);
    $b = is_array($this->type) ? $this->type : array($this->type);
    return (bool) array_intersect($a, $b);
  }

  function get_argument() {
    return $this->argument;
  }

  function get_keyword() {
    return $this->keyword;
  }

  function get_identifier() {
    return $this->identifier;
  }

  function get_title() {
    return $this->title;
  }

  function get_page_title() {
    return $this->page_title;
  }
}

/**
 * This is used to explain to Panels what context is required.
 */
class panels_required_context {
  var $keywords = '';

  function panels_required_context() {
    $args = func_get_args();
    $this->title = array_shift($args);
    if (count($args) == 1) {
      $args = array_shift($args);
    }
    $this->keywords = $args;
  }

  function filter($contexts) {
    $result = array();

    // See which of these contexts are valid
    foreach ($contexts as $cid => $context) {
      if ($context->is_type($this->keywords)) {
        $result[$cid] = $context;
      }
    }

    return $result;
  }

  function select($contexts, $context) {
    if (empty($context) || empty($contexts[$context])) {
Earl Miles's avatar
Earl Miles committed
      return FALSE;
    }
    return $contexts[$context];
  }
}

class panels_optional_context extends panels_required_context {
  function panels_optional_context() {
    $args = func_get_args();
    call_user_func_array(array($this, 'panels_required_context'), $args);
  }

  /**
   * Add the 'empty' context which is possible for optional
   */
  function add_empty(&$contexts) {
    $context = new panels_context('any');
    $context->identifier = t('No context');
    $contexts = array_merge(array('empty' => $context), $contexts);
  }

  function filter($contexts) {
    $this->add_empty($contexts);
    return parent::filter($contexts);
  }

  function select($contexts, $context) {
    $this->add_empty($contexts);
    if (empty($context)) {
      return $contexts['empty'];
    }

Earl Miles's avatar
Earl Miles committed
    $result = parent::select($contexts, $context);

    // Don't flip out if it can't find the context; this is optional, put
    // in an empty.
    if ($result == FALSE) {
      $result = $contexts['empty'];
Earl Miles's avatar
Earl Miles committed
    }
    return $result;
 * Master pane access function; combines all the relevant parameters that
 * natively used by the Panels API to determine a pane's access. Called from
 * panels_render_panes().
 *   The pane object to test for access.
 * @param $display
 *   The display object containing the pane object to be tested.
function panels_pane_access($pane, $display) {
  global $user;
  if (user_access('view all pane', $user)) {
  $role_access = _panels_pane_access_role($pane, $user);
  $type = panels_get_content_type($pane->type);
  if (!$visibility_check = panels_plugin_get_function('content_types', $type, 'visibility check')) {
    return $role_access;
  // Call the content type's custom-defined visibility rendering check.
  // Pass as much possibly relevant data as possible.
  $visibility_access = $visibility_check($pane, $display, $user);
  // Content type marked both access modes to be ANDed together.
  if (!empty($type['roles and visibility'])) {
    return ($visibility_access === TRUE && $role_access === TRUE) ? TRUE : FALSE;
  }
  // Err on the safe side: if EVERYTHING else has failed, then don't render the pane.
  return isset($visibility_access) ? $visibility_access : FALSE;
}
/**
 * Determine role-based access to a panel pane for the current user
 *
 * @param object $pane
 *  The pane object to test.
 * @param object $account
 *  The current $user object.
 * @return bool $role_access
 *  The boolean result of the roles-based segment of the Panels access system.
 */
function _panels_pane_access_role($pane, $account) {
  // All views with an empty access setting are available to all roles.
  if (!$pane->access || !is_array($pane->access)) {
    return TRUE;
  }

  // Otherwise, check roles
  static $roles = array();
  if (!isset($roles[$account->uid])) {
    $roles[$account->uid] = array_keys($account->roles);
    $roles[$account->uid][] = $account->uid ? DRUPAL_AUTHENTICATED_RID : DRUPAL_ANONYMOUS_RID;
  }

  return array_intersect($pane->access, $roles[$account->uid]) ? TRUE : FALSE;
}

/**
 * Get the content from a given content type.
 *
 * @param $type
 *   The content type. May be the name or an already loaded content type object.
 * @param $conf
 *   The configuration for the content type.
 * @param $args
 *   The arguments to the content type.
 * @param $context
 *   The panels_context object.
 * @Param $incoming_content
 *   Any incoming content, if this display is a wrapper.
 */
function panels_ct_get_content($type, $conf, $args, $context, $incoming_content) {
  if ($function = panels_plugin_get_function('content_types', $type, 'render callback')) {
    $content = $function($conf, $args, $context, $incoming_content);
    if (empty($content->title) && !empty($content->subject)) {
      $content->title = $content->subject;
    }

    if (!empty($content->title) && empty($content->subject)) {
      $content->subject = $content->title;
    }

    return $content;
  }
}

/**
 * Get the title from a given content type.
 *
 * @param $type
 *   The content type. May be the name or an already loaded content type object.
 * @param $conf
 *   The configuration for the content type.
 * @param $context
 *   The panels_context object.
 * @param $incoming_content
 *   Any incoming content, if this display is a wrapper.
 */
function panels_ct_get_title($type, $conf, $context = NULL, $incoming_content = NULL) {
  if ($function = panels_plugin_get_function('content_types', $type, 'title callback')) {
    return $function($conf, $context, $incoming_content);
  }
Earl Miles's avatar
Earl Miles committed
  return t('Deleted/missing content type @type', array('@type' => $type));
 * Add the default FAPI elements to the content type configuration form
function panels_ct_conf_form($ct_data, $contexts, $conf) {
  list($subtype, $content_type) = array($ct_data['subtype'], $ct_data['type']);
  if (!empty($subtype['required context']) && is_array($contexts)) {
    $form['context'] = panels_context_selector($contexts, $subtype['required context'], isset($conf['context']) ? $conf['context'] : array());
  $style_options = array('default' => t('Default'));
  foreach (panels_get_styles() as $name => $properties) {
    if (empty($properties['hidden']) && (!empty($properties['render pane']))) {
      $style_options[$name] = $properties['title'];
    }
  }

  asort($style_options);

  if (empty($conf['style'])) {
    $conf['style'] = 'default';
  }

  if ($style_options) {
    $form['style'] = array(
      '#type' => 'select',
      '#title' => t('Pane style'),
      '#default_value' => $conf['style'],
      '#options' => $style_options,
    );
  }

  // Unless we're not allowed to overide the title on this content type, add this
  // gadget to all panes.
  if (empty($content_type['no title override'])) {
    $form['aligner_start'] = array(
      '#value' => '<div class="option-text-aligner">',
    );
    $form['override_title'] = array(
      '#type' => 'checkbox',
      '#default_value' => isset($conf['override_title']) ? $conf['override_title'] : '',
      '#title' => t('Override title'),
      '#id' => 'override-title-checkbox',
    );
    $form['override_title_text'] = array(
      '#type' => 'textfield',
      '#default_value' => isset($conf['override_title_text']) ? $conf['override_title_text'] : '',
      '#size' => 35,
      '#id' => 'override-title-textfield',
    );
    $form['aligner_stop'] = array(
      '#value' => '</div><div style="clear: both; padding: 0; margin: 0"></div>',
    );
    $form['override_title_markup'] = array(
      '#prefix' => '<div class="description">',
      '#suffix' => '</div>',
      '#value' => t('You may use %keywords from contexts, as well as %title to contain the original title.'),
  // Add CSS class and ID gadgets to all panes if advanced pane settings is set.
  if (user_access('administer advanced pane settings')) {
    $form['css_id'] = array(
      '#type' => 'textfield',
      '#default_value' => isset($conf['css_id']) ? $conf['css_id'] : '',
      '#title' => t('CSS ID'),
      '#description' => t('CSS ID to apply to this content. This may be blank.'),
      '#weight' => 2,
      '#size' => 15,
    );
    $form['css_class'] = array(
      '#type' => 'textfield',
      '#default_value' => isset($conf['css_class']) ? $conf['css_class'] : '',
      '#title' => t('CSS class'),
      '#description' => t('CSS class to apply to this content. This may be blank.'),
      '#weight' => 2,
      '#size' => 15,
    );
 * Call any content type-defined add/edit callbacks so that additions
 * to $form['configuration'] can be made.
 * @param object $pane
 *   The $pane object currently being edited.
 * @param $contexts
 *   A list of possible contexts.
 * @param $parents
 *   The #parents to be used on the form, because some form gadgets need to
 *   know where they live.
 * @param string $op
 *  Either 'add' or 'edit' depending on the operation being performed.
 * @param mixed $content_type
 *  This variable should only be passed if the calling function already has access to the
 *  relevant content_type data and wants to save a little on performance. If so, then the
 *  fully-loaded content type plugin declaration array should be passed.
function panels_ct_pane_config_form($pane, $contexts, $parents, $op, $content_type = 'content_types') {
  if ($function = panels_plugin_get_function($content_type, $pane->type, "$op callback")) {
    return $function($pane->subtype, $parents, $pane->configuration);
 * Call any add/edit validators defined by the content type.
 * @see panels_content_config_form_validate()
 * @param $type
 *   The content type. May be the name or an already loaded content type object.
 * @param $form
 *   The actual Forms API form that is being validated.
 * @param $form_values
 *   The actual Forms API values being validated.
 * @param string $op
 *  Either 'add' or 'edit' depending on the operation being performed.
function panels_ct_pane_validate_form($content_type, $form, $form_values, $op) {
  if ($function = panels_plugin_get_function('content_types', $content_type, "$op validate callback")) {
    return $function($form, $form_values);
  }
}
 * Call any add/edit submit handlers defined by the content type.
 *
 * @param string $content_type
 *  A string value containing the name of the content type.
 *  The $form_values['configuration'] sub-array generated by FAPI for the
 *  overall ct add/edit submit handler.
 * @param string $op
 *  Either 'add' or 'edit' depending on the operation being performed.
function panels_ct_pane_submit_form($content_type, $form_values, $op) {
  if ($function = panels_plugin_get_function('content_types', $content_type, "$op submit callback")) {
    return $function($form_values);
  }
}

/**
 * Get all of the individual types provided by a given content types. This
 * would be all of the blocks for the block type, or all of the views for
 * the view type.
 *
 * @param $type
 *   The content type to load.
 */
function panels_ct_get_types($type) {
  if (is_array($type)) {
    $content_type = $type;
  }
  else {
    $content_type = panels_get_content_type($type);
  }

  $function = $content_type['content_types'];
  if (is_array($function)) {
Earl Miles's avatar
Earl Miles committed
    return (array) $function;
Earl Miles's avatar
Earl Miles committed
  return array();
 * Get a list of panels available in the layout.
 */
function panels_get_panels($layout, $display) {
  if (!empty($layout['panels function']) && function_exists($layout['panels function'])) {
    return $layout['panels function']($display, $display->layout_settings);
  }
  if (!empty($layout['panels'])) {
    return $layout['panels'];
  }
  return array();
}

/**
 * Get an array of all available content types that can be fed into the
 * display editor for the add content list.
 *
 * @param $context
 *   If a context is provided, content that requires that context can apepar.
 * @param $has_content
 *   Whether or not the display will have incoming content
 * @param $allowed_types
 *   An array of allowed content types (pane types) keyed by content_type . '-' . sub_type
 * @param $default_types
 *   A default allowed/denied status for content that isn't known about
 */
function panels_get_available_content_types($contexts = NULL, $has_content = FALSE, $allowed_types = NULL, $default_types = NULL) {
  $content_types = panels_get_content_types();
  $available = array();

  foreach ($content_types as $id => $type) {
    foreach (panels_ct_get_types($type) as $cid => $cinfo) {
      // exclude items that require content if we're saying we don't
      // provide it.
      if (!empty($cinfo['requires content']) && !$has_content) {
        continue;
      }

      // Check to see if the content type can be used in this context.
      if (!empty($cinfo['required context'])) {
        if (!panels_context_filter($contexts, $cinfo['required context'])) {
          continue;
        }
      }

      // Check to see if the passed-in allowed types allows this content.
      if ($allowed_types) {
        $key = $id . '-' . $cid;
        if (!isset($allowed_types[$key])) {
          $allowed_types[$key] = isset($default_types[$id]) ? $default_types[$id] : $default_types['other'];
        }
        if (!$allowed_types[$key]) {
          continue;
        }
      }

      // If we made it through all the tests, then we can use this content.
      $available[$id][$cid] = $cinfo;
    }
  }
  return $available;
}

/**
 * Get an array of all content types that can be fed into the
 * display editor for the add content list, regardless of
 * availability.
 *
 */
function panels_get_all_content_types() {
  $content_types = panels_get_content_types();
  $available = array();

  foreach ($content_types as $id => $type) {
    foreach (panels_ct_get_types($type) as $cid => $cinfo) {
      // If we made it through all the tests, then we can use this content.
      $available[$id][$cid] = $cinfo;
    }
  }
  return $available;
}

// ------------------------------------------------------------------
// Functions to provide information about a panel or part of a panel.

/**
 * Get the content from a given pane.
 *
Sam Boyer's avatar
Sam Boyer committed
 * @ingroup HookInvokers
 *
 * @param $pane
 *   The pane to retrieve content from.
 * @param $args
 *   The arguments sent to the display.
 * @param $context
 *   The panels context.
 * @param $incoming_content
 *   Any incoming content if this display is a wrapper.
 */
function panels_get_pane_content($display, $pane, $args = array(), $context = NULL, $incoming_content = '') {
  if (!$context) {
    $context = new panels_context;
  }
  // FIXME misplaced bang?
  if (!$incoming_content === '') {
    $incoming_content = t('Incoming content will be displayed here.');
  }

  $caching = !empty($pane->cache['method']) ? TRUE : FALSE;
  if ($caching && ($cache = panels_get_cached_content($display, $args, $context, $pane))) {
    $content = $cache->content;
  }
  else {
    $content = panels_ct_get_content($pane->type, $pane->configuration, $args, $context, $incoming_content);
    foreach (module_implements('panels_pane_content_alter') as $module) {
      // TODO This makes the third hook invocation on the render path. How badly is this hindering performance?
      $function = $module . '_panels_pane_content_alter';
      $function($content, $pane, $args, $context);
    }
      panels_set_cached_content($cache, $display, $args, $context, $pane);
    }
  }

  return $content;
}

/**
 * Get cached content for a given display and possibly pane.
 *
 * @return
 *   The cached content, or FALSE to indicate no cached content exists.
 */
function panels_get_cached_content($display, $args, $context, $pane = NULL) {
  $method = $pane ? $pane->cache['method'] : $display->cache['method'];
  $function = panels_plugin_get_function('cache', $method, 'cache get');

  if (!$function) {
    return FALSE;
  }

  $conf = $pane ? $pane->cache['settings'] : $display->cache['settings'];
  $cache = $function($conf, $display, $args, $context, $pane);
  if (empty($cache)) {
    return FALSE;
  }

  // restore it.
  $cache->restore();
  return $cache;
}

/**
 * Store cached content for a given display and possibly pane.
 */
function panels_set_cached_content($cache, $display, $args, $context, $pane = NULL) {
  $method = $pane ? $pane->cache['method'] : $display->cache['method'];
  $function = panels_plugin_get_function('cache', $method, 'cache set');

  if (!$function) {
    return FALSE;
  }

  $conf = $pane ? $pane->cache['settings'] : $display->cache['settings'];

  // snapshot it.
  $cache->cache();
  return $function($conf, $cache, $display, $args, $context, $pane);
}

/**
 * Clear all cached content for a display.
 */
function panels_clear_cached_content($display) {
  // Figure out every method we might be using to cache content in this display:
  $methods = array();
  if (!empty($display->cache['method'])) {
    $methods[$display->cache['method']] = TRUE;
  }

  foreach ($display->content as $pane) {
    if (!empty($pane->cache['method'])) {
      $methods[$pane->cache['method']] = TRUE;
    }
  }

  foreach (array_keys($methods) as $method) {
    $function = panels_plugin_get_function('cache', $method, 'cache clear');
    if ($function) {
      $function($display);
    }
  }
}

/**
 * An object to hold caching information while it is happening.
 */
class panels_cache_object {
  var $content = '';
  var $head = NULL;
  var $css = NULL;
  var $js = NULL;
  var $ready = FALSE;

  /**
   * When constructed, take a snapshot of our existing out of band data.
   */
  function panels_cache_object() {
    $this->head = drupal_set_html_head();
    $this->css = drupal_add_css();
Earl Miles's avatar
Earl Miles committed

    foreach (array('header', 'footer') as $scope) {
      $this->js[$scope] = drupal_add_js(NULL, NULL, $scope);
    }
  }

  /**
   * Add content to the cache. This assumes a pure stream;
   * use set_content() if it's something else.
   */
  function add_content($content) {
    $this->content .= $content;
  }

  function set_content($content) {
    $this->content = $content;
  }

  /**
   * Set the object for storing. This overwrites.
   */
  function cache() {
Earl Miles's avatar
Earl Miles committed
    if ($this->ready) {
Earl Miles's avatar
Earl Miles committed
    $this->ready = TRUE;

    // Simple replacement for head
    $this->head = str_replace($this->head, '', drupal_set_html_head());

    // Slightly less simple for CSS:
    $css = drupal_add_css();
    $start = $this->css;
    $this->css = array();

    foreach ($css as $media => $medias) {
      foreach ($medias as $type => $types) {
        foreach ($types as $path => $preprocess) {
          if (!isset($start[$media][$type][$path])) {
            $this->css[] = array($path, $type, $media, $preprocess);
          }
        }
      }
    }

Earl Miles's avatar
Earl Miles committed
    $js = array();
Earl Miles's avatar
Earl Miles committed
    foreach (array('header', 'footer') as $scope) {
      $js[$scope] = drupal_add_js(NULL, NULL, $scope);
Earl Miles's avatar
Earl Miles committed
    $start = $this->js;
    $this->js = array();

    foreach ($js as $scope => $scopes) {
      foreach ($scopes as $type => $types) {
        foreach ($types as $id => $info) {
          if (!isset($start[$scope][$type][$id])) {
            switch ($type) {
              case 'setting':
                $this->js[] = array($info, $type, $scope);
                break;
              case 'inline':
                $this->js[] = array($info['code'], $type, $scope, $info['defer']);
                break;
              default:
                $this->js[] = array($id, $type, $scope, $info['defer'], $info['cache']);
            }
          }
        }
      }
    }
  }
  /**
   * Restore out of band data saved to cache.
   */
  function restore() {
    if (!empty($this->head)) {
      drupal_set_html_head($this->head);
    }
    if (!empty($this->css)) {
      foreach ($this->css as $args) {
        call_user_func_array('drupal_add_css', $args);
      }
    }
    if (!empty($this->js)) {
      foreach ($this->js as $args) {
        call_user_func_array('drupal_add_js', $args);
      }
    }
  }
}

/**
 * Get the title of a pane.
 *
 * @param $pane
 *   The $pane object.
 */
function panels_get_pane_title(&$pane, $context = array(), $incoming_content = NULL) {
Earl Miles's avatar
Earl Miles committed
    $pane->context = panels_pane_select_context($pane, $context);
      return t('Does not meet context requirements');
    }
  }
  return panels_ct_get_title($pane->type, $pane->configuration, $pane->context, $incoming_content);
}

// ---------------------------------------------------------------------------
// panels argument helpers

/**
 * Get a context from an argument
 */
function panels_argument_get_context($argument, $arg, $empty = FALSE) {
  if ($function = panels_plugin_get_function('arguments', $argument['name'], 'context')) {
    $context = $function($arg, $argument['argument_settings'], $empty);
    if ($context) {
      $context->identifier = $argument['identifier'];
      $context->page_title = $argument['title'];
      $context->keyword    = $argument['keyword'];
      return $context;
    }
  }
}

/**
 * Pick which display an argument wants to use
 */
function panels_argument_choose_display($type, $conf, $context) {
  if ($function = panels_plugin_get_function('arguments', $type, 'choose display')) {
    return $function($conf, $context);
  }
}

/**
 * Determine a unique context ID for an argument
 */
function panels_argument_context_id($argument) {
  return "argument_$argument[name]_$argument[id]";
}

/**
 * Retrieve a list of empty contexts for all arguments
 */
function panels_argument_get_contexts($arguments) {
  $contexts = array();
  foreach ($arguments as $argument) {
    $context = panels_argument_get_context($argument, NULL, true);
    if ($context) {
      $contexts[panels_argument_context_id($argument)] = $context;
    }
  }
  return $contexts;
}

/**
 * Load the contexts for a given panel.
 *
 * @param $arguments
 *   The array of argument definitions
 * @param &$contexts
 *   The array of existing contexts.
 * @param $args
 *   The arguments to load
 *
 * @return
 *   FALSE if an argument wants to 404.
 */
function panels_argument_load_contexts($arguments, &$contexts, $args) {
  foreach ($arguments as $argument) {
    // pull the argument off the list.
    $arg = array_shift($args);
    $id = panels_argument_context_id($argument);

    // For % arguments embedded in the URL, our context is already loaded.
    // There is no need to go and load it again.
    if (empty($contexts[$id])) {
      if ($context = panels_argument_get_context($argument, $arg)) {
        $contexts[$id] = $context;
      }
    }
    else {
      $context = $contexts[$id];
    }

    if ((empty($context) || empty($context->data)) && $argument['default'] == '404') {
      return FALSE;
    }
  }
  return TRUE;
}

/**
 * Choose a display based upon arguments and loaded contexts.
 *
 * @param $arguments
 *   The array of argument definitions
 *   The array of existing contexts.
 *
 * @return
 *   The identification of the display to use
 */
function panels_argument_get_display($arguments, $contexts) {
  $display_candidate = NULL;
    $id = panels_argument_context_id($argument);
    if (!empty($contexts[$id]) && !empty($contexts[$id]->data)) {
      $context = $contexts[$id];
      $d = panels_argument_choose_display($argument['name'], $argument['argument_settings'], $context);
      if ($d) {
        $display_candidate = "argument_$i" . '-' . $d;
      }
    }
  }

  return $display_candidate;
}

// ---------------------------------------------------------------------------
// panels relationship helpers

/**
 * Fetch all relevant relationships
 *
 * @param $contexts
 *   An array of contexts used to figure out which relationships are relevant.
 *
 * @return
 *   An array of relationship keys that are relevant for the given set of
 * arguments.
 */
function panels_get_relevant_relationships($contexts) {
  $relevant = array();
  $relationships = panels_get_relationships();

  // Go through each relationship
  foreach ($relationships as $rid => $relationship) {
    // For each relationship, see if there is a context that satisfies it.
    if (panels_context_filter($contexts, $relationship['required context'])) {
      $relevant[$rid] = $relationship['title'];
    }
  }

  return $relevant;
}

/**
 * Fetch all active relationships
 *
 * @param $relationships
 *   An keyed array of relationship data including:
 *   - name: name of relationship
 *   - context: context id relationship belongs to.
 *
 * @param $contexts
 *   A keyed array of contexts used to figure out which relationships
 *   are relevant. New contexts will be added to this.
 *
 */
function panels_relationship_get_contexts($relationships, &$contexts) {
  $return = array();

  foreach ($relationships as $rdata) {
    if (empty($contexts[$rdata['context']])) {
      continue;
    }
    $relationship = panels_get_relationship($rdata['name']);
    // If the relationship can't be found or its context can't be found,
    // ignore.
    if (!$relationship) {
      continue;
    }

    $cid = panels_relationship_context_id($rdata);
    if ($context = panels_relationship_get_context($rdata, $contexts[$rdata['context']])) {
      $contexts[$cid] = $context;
    }
  }
}

/**
 * Determine a unique context ID for an argument
 */
function panels_relationship_context_id($relationship) {
  return "relationship_$relationship[name]_$relationship[id]";
}

/**
 * Fetch a context from a relationship, given the context input.
 */
function panels_relationship_get_context($relationship, $arg) {
  if ($function = panels_plugin_get_function('relationships', $relationship['name'], 'context')) {
    $context = $function($arg, $relationship['relationship_settings']);
    if ($context) {
      $context->identifier = $relationship['identifier'];
      $context->page_title = $relationship['title'];
      $context->keyword    = $relationship['keyword'];
      return $context;
    }
  }
}

// ---------------------------------------------------------------------------
// panels context helpers

/**
 * Return a keyed array of context that match the given 'required context'
 * filters.
 *
 * @param $contexts
 *   A keyed array of all available contexts.
 * @param $required
 *   The required context string or array.
 *
 * @return
 *   A keyed array of contexts.
 */
function panels_context_filter($contexts, $required) {
  if (is_array($required)) {
    $result = array();
    foreach ($required as $r) {
      $result = array_merge($result, _panels_context_filter($contexts, $r));
    }
    return $result;
  }

  return _panels_context_filter($contexts, $required);
}

function _panels_context_filter($contexts, $required) {
  // TODO: Multiples
  $result = array();

  if (is_object($required)) {
    $result = $required->filter($contexts);
  }

  return $result;
}

/**
 * Create a select box to choose possible contexts. This only creates a
 * selector if there is actually a choice.
 *
 * @param $contexts
 *   A keyed array of all available contexts.
 * @param $required
 *   The required context string or array.
 *
 * @return
 *   A form element, or NULL if there are no contexts that satisfy the
 *   requirements.
 */
function panels_context_selector($contexts, $required, $default) {
  if (is_array($required)) {
    $result = array();
    $count = 1;
    foreach ($required as $id => $r) {
      $result[] = _panels_context_selector($contexts, $r, $default[$id], $count++);