load()) { return $space; } } return FALSE; } /** * Remove all override values for a space. */ function spaces_delete($type, $id) { return db_query("DELETE FROM {spaces_overrides} WHERE type = '%s' AND id = '%s'", $type, $id); } /** * Initialize a space and set it to be the current active space. */ function spaces_init_space($type, $id) { $space = spaces_load($type, $id); if ($space) { if ($space->activate()) { // If there is already an active space we need to deactivate it. // Note that this often causes a redirect. if ($active = spaces_get_space()) { if ($active->type != $space->type || $active->id != $space->id) { $active->deactivate(); } } spaces_set_space($space); } else { $space->deactivate(); } } } /** * Wrapper function around spaces_set_space(). Retrieves the current * active space for a given space type. */ function spaces_get_space() { return spaces_set_space(); } /** * Sets the specified space as the current active space. Returns the * active space if no space is provided. * * @param $space * The space object to set as the active space. Optional. * * @return * The active space object or FALSE if there is no active space. */ function spaces_set_space($space = NULL) { static $active_space; if (isset($space)) { $active_space = $space; if (module_exists('context') && $plugin = context_get_plugin('condition', 'spaces_type')) { $plugin->execute($space); } } return isset($active_space) ? $active_space : FALSE; } /** * Implementation of hook_ctools_plugin_api(). */ function spaces_ctools_plugin_api($module, $api) { if ($module == 'spaces' && $api == 'plugins') { return array('version' => 3); } } /** * Implementation of hook_ctools_plugin_plugins(). */ function spaces_ctools_plugin_plugins() { return array( 'cache' => TRUE, 'use hooks' => TRUE, ); } /** * Implementation of hook_spaces_plugins(). */ function spaces_spaces_plugins() { $plugins = array(); $plugins['space'] = array( 'handler' => array( 'path' => drupal_get_path('module', 'spaces') .'/plugins', 'file' => 'space.inc', 'class' => 'space', ), ); $plugins['space_type'] = array( 'handler' => array( 'path' => drupal_get_path('module', 'spaces') .'/plugins', 'file' => 'space_type.inc', 'class' => 'space_type', 'parent' => 'space', ), ); $plugins['space_type_purl'] = array( 'handler' => array( 'path' => drupal_get_path('module', 'spaces') .'/plugins', 'file' => 'space_type_purl.inc', 'class' => 'space_type_purl', 'parent' => 'space_type', ), ); $plugins['spaces_controller'] = array( 'handler' => array( 'path' => drupal_get_path('module', 'spaces') .'/plugins', 'file' => 'spaces_controller.inc', 'class' => 'spaces_controller', ), ); $plugins['spaces_controller_variable'] = array( 'handler' => array( 'path' => drupal_get_path('module', 'spaces') .'/plugins', 'file' => 'spaces_controller_variable.inc', 'class' => 'spaces_controller_variable', 'parent' => 'spaces_controller', ), ); $plugins['spaces_controller_context'] = array( 'handler' => array( 'path' => drupal_get_path('module', 'spaces') .'/plugins', 'file' => 'spaces_controller_context.inc', 'class' => 'spaces_controller_context', 'parent' => 'spaces_controller', ), ); return $plugins; } /** * Implementation of hook_spaces_registry(). */ function spaces_spaces_registry() { return array( 'controllers' => array( 'variable' => array( 'title' => t('Variable'), 'plugin' => 'spaces_controller_variable', ), 'context' => array( 'title' => t('Context'), 'plugin' => 'spaces_controller_context', ), ), ); } /** * Get the registry of spaces types. */ function spaces_types($reset = FALSE) { return _spaces_registry('types', $reset); } /** * Get the registry of spaces controllers. */ function spaces_controllers($reset = FALSE) { return _spaces_registry('controllers', $reset); } /** * Registry retrieval and caching. */ function _spaces_registry($key = NULL, $reset = FALSE) { static $registry; if (!isset($registry) || $reset) { if (!$reset && $cache = cache_get('spaces_registry', 'cache')) { $registry = $cache->data; } else { $registry = module_invoke_all('spaces_registry'); drupal_alter('spaces_registry', $registry); cache_set('spaces_registry', $registry); } } if (isset($key)) { return isset($registry[$key]) ? $registry[$key] : array(); } return $registry; } /** * Presets ============================================================ */ /** * Preset loader. * * @param $name * Optional name for the preset to load. * @param $name * Optional spaces_type of the the presets to load. * * @return * Returns a fully-loaded preset. */ function spaces_preset_load($name = NULL, $space_type = NULL, $reset = FALSE) { ctools_include('export'); static $presets; if (!isset($presets) || $reset) { if (!$reset && $cache = cache_get('spaces_presets', 'cache')) { $presets = $cache->data; } else { if ($reset) { ctools_export_load_object_reset('spaces_presets'); } $presets = ctools_export_load_object('spaces_presets', 'all'); cache_set('spaces_presets', $presets); } } if (!isset($name) && !isset($space_type)) { return $presets; } if (isset($name)) { return isset($presets[$name]) ? $presets[$name] : FALSE; } if (isset($space_type)) { $return = array(); foreach ($presets as $key => $preset) { if ($preset->space_type === $space_type) { $return[$key] = $preset; } } return $return; } } /** * Inserts or updates a spaces preset into the database. * * @param $preset * The preset object to be inserted. */ function spaces_preset_save($preset) { $existing = spaces_preset_load($preset->name, NULL, TRUE); if ($existing && ($existing->export_type & EXPORT_IN_DATABASE)) { drupal_write_record('spaces_presets', $preset, 'name'); } else { drupal_write_record('spaces_presets', $preset); } spaces_preset_load(NULL, NULL, TRUE); // Reset static cache return TRUE; } /** * Save a preset's values from a given space object. * * @param $preset * The preset object to be saved. * @param $space * The space object whose values should be used for the preset. */ function spaces_preset_save_from_space($preset, $space) { $space->activate(); foreach (array_keys(spaces_controllers()) as $controller) { $preset->value[$controller] = array_merge( $space->controllers->{$controller}->get(NULL, 'preset'), $space->controllers->{$controller}->get(NULL, 'space') ); } spaces_preset_save($preset); } /** * Deletes an existing preset. * * @param $preset * The preset object to be deleted. * * @return * Returns true on success, false on failure. */ function spaces_preset_delete($preset) { if (isset($preset->name) && ($preset->export_type & EXPORT_IN_DATABASE)) { db_query("DELETE FROM {spaces_presets} WHERE name = '%s'", $preset->name); spaces_preset_load(NULL, NULL, TRUE); return TRUE; } return FALSE; } /** * CTools export function. * * Includes workaround for var_export() #fail with stdClass objects. See: * http://bugs.php.net/bug.php?id=48016&edit=1. A proper patch is in the * CTools issue queue here: http://drupal.org/node/661332. Once this is * committed & included in a CTools release, this workaround will no * longer be necessary. */ function spaces_export_spaces_presets($object, $indent = '') { $export = ctools_export_object('spaces_presets', $object, $indent); $strings = array(); foreach (array('title', 'description') as $id) { if (!empty($object->$id)) { $strings[] = $indent . " t('" . str_replace("'", "\'", $object->$id) . "'),"; } } sort($strings); $extra = implode("\n", array_unique($strings)); $extra = "\n". $indent ."\$translatables['" . $object->name . "'] = array(\n". $extra . "\n" . $indent .");\n"; return str_replace('stdClass::__set_state(', '(object) (', $export) . $extra; } /** * Other spaces actions =============================================== */ /** * Calls the router method on all space types, giving them a chance to * route requests accordingly. * * @param $op * The current "hook" or "hook op" identifier for the $space->router * to act on. * @param $object * Optional object to pass to the $space->router. */ function spaces_router($op, $object = NULL) { static $router; if (!isset($router)) { $router = array(); // Add the active space. if ($space = spaces_get_space()) { $router[$space->type] = $space; } // Iterate over space types and create instances of each. ctools_include('plugins'); $plugins = ctools_get_plugins('spaces', 'plugins'); foreach (spaces_types() as $type => $info) { if (!isset($router[$type]) && isset($plugins[$info['plugin']]) && $class = ctools_plugin_get_class($plugins[$info['plugin']], 'handler')) { $router[$type] = new $class($type); } } } foreach ($router as $space) { $space->router($op, $object); } } /** * Integration with Features ========================================== */ /** * Retrieve all available features. * * @param $type * Optional type flag by which to filter available features. * @param $reset * Optional boolean flag for resetting the static cache. * * @return * Keyed array of potential features. */ function spaces_features($type = NULL, $reset = FALSE) { static $features; static $by_type; if (!isset($features) || $reset) { $features = array(); $by_type = array(); // Include the 'site' key. $types = array_merge(array_keys(spaces_types()), array('site')); foreach (features_get_features() as $feature) { if (module_exists($feature->name) && !empty($feature->info['spaces']['types'])) { $features[$feature->name] = $feature; foreach ($types as $t) { switch (gettype($feature->info['spaces']['types'])) { case 'array': if (in_array($t, $feature->info['spaces']['types'], TRUE)) { $by_type[$t][$feature->name] = $feature; } break; case 'string': if ($feature->info['spaces']['types'] === 'all' || $feature->info['spaces']['types'] === $t) { $by_type[$t][$feature->name] = $feature; } break; } } } } } if (isset($type)) { return !empty($by_type[$type]) ? $by_type[$type] : array(); } return $features; } /** * Caches a map of feature component => feature name. */ function spaces_features_map($type = NULL, $reset = FALSE) { static $map; if (!isset($map) || $reset) { $map = array(); $cache = cache_get('spaces_map', 'cache'); if ($cache && !$reset) { $map = $cache->data; } else { // Generate component to feature map based on feature info files. // Only use enabled features. $features = features_get_features(NULL, TRUE); foreach ($features as $feature_name => $feature) { if (module_exists($feature_name)) { foreach ($feature->info['features'] as $component => $items) { if (is_array($items)) { foreach ($items as $item) { if (!isset($map[$component][$item])) { $map[$component][$item] = $feature_name; } } } } } } cache_set('spaces_map', $map, 'cache'); } } if (!empty($type)) { return isset($map[$type]) ? $map[$type] : array(); } return $map; } /** * Access check for the current space. */ function spaces_access_space($account = NULL) { $space = spaces_get_space(); return $space ? $space->access_space() : TRUE; } /** * Access check for administrative access to the current space. * An optional set of permissions can be provided to be added as AND * conditions to the access check. * * @param $account * User account to check access for. Optional. * @param $space * The space to check access against. */ function spaces_access_admin($account = NULL, $space = NULL) { if (empty($space)) { $space = spaces_get_space(); } if ($space) { return $space->access_admin($account); } return user_access('administer site configuration', $account); } /** * Access callback for spaces, with extra permission checking. */ function spaces_access_admin_perms($perms = array(), $account = NULL, $space = NULL) { $perm_access = TRUE; if (!empty($perms)) { foreach ($perms as $perm) { $perm_access = $perm_access && user_access($perm, $account); } } return ($perm_access ? spaces_access_admin($account, $space) : FALSE); } /** * Access check for a given feature in the current space. * * @param $op * The operation to test access for. Optional, defaults to 'view'. * @param $feature * Feature to test access against. * @param $account * User account to test access for. * @param $space * The space to check access against. */ function spaces_access_feature($op = 'view', $feature, $account = NULL, $space = NULL) { $features = spaces_features(); // Allow access to features that are not spaces-compatible. // Allow access to features in the "admin" section of the site so menu items // are available on pages like 'admin/build/menu' and 'admin/build/context'. if (!isset($features[$feature]) || arg(0) === 'admin') { return TRUE; } if (empty($space)) { $space = spaces_get_space(); } if ($space) { return $space->access_feature($op, $feature, $account); } $features = variable_get('spaces_features', array()); return user_access('access content', $account) && !empty($features[$feature]); } /** * Access check for a user account page in the current space. */ function spaces_access_user($op = 'view', $account = NULL) { $space = spaces_get_space(); return $space ? $space->access_user($op, $account) : TRUE; } /** * Implemented hooks ================================================== */ /** * Implementation of hook_menu(). */ function spaces_menu() { $items = array(); $items['spaces-access-denied'] = array( 'access callback' => FALSE, 'type' => MENU_CALLBACK, ); $items['spaces-frontpage'] = array( 'page callback' => 'spaces_frontpage', 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); $items['features'] = array( 'title' => 'Features', 'description' => 'Configure features for this space.', 'page callback' => 'drupal_get_form', 'page arguments' => array('spaces_features_form'), 'access callback' => 'spaces_access_admin', 'access arguments' => array(), 'type' => MENU_NORMAL_ITEM, 'file' => 'spaces.admin.inc', ); return $items; } /** * Implementation of hook_menu_alter(). */ function spaces_menu_alter(&$items) { $router_items = array( 'node/%node', 'node/%node/edit', 'user/%user/view', 'user/%user_uid_optional', 'user/%user_category/edit', ); foreach (node_get_types('types', NULL, TRUE) as $type) { $type_url_str = str_replace('_', '-', $type->type); $router_items[] = 'node/add/'. $type_url_str; } foreach ($router_items as $path) { if (isset($items[$path])) { $arguments = isset($items[$path]['access arguments']) ? $items[$path]['access arguments'] : array(); $arguments[] = isset($items[$path]['access callback']) ? $items[$path]['access callback'] : NULL; $items[$path]['access callback'] = 'spaces_menu_access'; $items[$path]['access arguments'] = $arguments; } } // Graft space-specific settings pages into each space type's menu tree. $graft = array(); $graft['features'] = $items['features']; foreach ($items as $path => $item) { if (strpos($path, 'features/') === 0) { $graft[$path] = $item; } } foreach (spaces_types() as $type => $info) { if (isset($info['path'])) { $graft['features']['type'] = MENU_LOCAL_TASK; $graft['features']['access callback'] = 'spaces_access_admin'; $graft['features']['access arguments'] = array(); // For any child pages of the graft, increment load arguments by the // number of args present in the path. $argcount = count(explode('/', $info['path'])); foreach ($graft as $path => $item) { $newitem = $item; foreach (array('page arguments', 'access arguments', 'title arguments') as $key) { if (!empty($newitem[$key])) { foreach ($newitem[$key] as $position => $argument) { if (is_numeric($argument)) { $newitem[$key][$position] = $newitem[$key][$position] + $argcount; } } } } $items["{$info['path']}/{$path}"] = $newitem; } } $items["{$info['path']}/overrides"] = array( 'title' => 'Overrides', 'description' => 'Manage override values for this space.', 'page callback' => 'drupal_get_form', 'page arguments' => array('spaces_overrides_form'), 'access callback' => 'spaces_access_admin_perms', 'access arguments' => array(array('administer spaces')), 'type' => MENU_LOCAL_TASK, 'file' => 'spaces.admin.inc', 'module' => 'spaces', ); } // Overrides of autocomplete callbacks using Views. // @TODO: Implement taoxnomy autocomplete callback. if (module_exists('views') && isset($items['user/autocomplete'])) { $items['user/autocomplete']['page callback'] = 'spaces_user_autocomplete'; $items['user/autocomplete']['module'] = 'spaces'; $items['user/autocomplete']['file'] = 'spaces.admin.inc'; } } /** * Spaces menu access callback. Allows space types to manage access as * related to their space workflow. See hook_menu_alter() for how * menu access callbacks / arguments get passed. */ function spaces_menu_access() { // Run the standard Drupal access check. $args = func_get_args(); $access_callback = array_pop($args); if (empty($access_callback) || call_user_func_array($access_callback, $args)) { $map = spaces_features_map(); // Determine the access callback to use by inspecting args. if ($access_callback == 'node_access' && $args[0] == 'create') { $node_type = str_replace('-', '_', $args[1]); $feature = isset($map['node'][$node_type]) ? $map['node'][$node_type] : NULL; if ($feature) { return spaces_access_feature('create', $feature); } } else { foreach ($args as $arg) { if (is_object($arg)) { if (isset($object->nid)) { $feature = isset($map['node'][$object->type]) ? $map['node'][$object->type] : NULL; if ($feature) { return spaces_access_feature('view', $feature); } } else if (isset($object->uid)) { return spaces_access_user('view', $object); } break; } } } return TRUE; } return FALSE; } /** * Route the user to the proper homepage for this space. * * Assumes that the router path provided in hook_spaces_registry() for the * space type describes a path with a loader argument to be replaced by the * $space->id. See _menu_router_build() for the origin of the regex. */ function spaces_frontpage() { $space = spaces_get_space(); if ($space) { $types = spaces_types(); $type_info = $types[$space->type]; if (isset($type_info['path'])) { $path = preg_replace('/%(|[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$/', $space->id, $type_info['path']); menu_set_active_item($path); return menu_execute_active_handler(); } } drupal_not_found(); exit; } /** * Implementation of hook_theme(). */ function spaces_theme() { $items = array(); $base = array( 'arguments' => array('form' => array()), 'file' => 'spaces.theme.inc', 'path' => drupal_get_path('module', 'spaces'), ); $items['spaces_features_form'] = $items['spaces_preset_form'] = $items['spaces_overrides_form'] = $base; return $items; } /** * Implementation of hook_views_api(). */ function spaces_views_api() { return array('api' => 2, 'path' => drupal_get_path('module', 'spaces') .'/includes'); } /** * Implementation of hook_perm(). */ function spaces_perm() { return array('administer spaces'); } /** * Implementation of hook_init(). */ function spaces_init() { if (module_exists('spaces_customtext')) { global $language; $language->language = ($language->language === 'spaces_customtext') ? spaces_customtext_cache() : $language->language; } if (!spaces_access_space()) { menu_set_active_item('spaces-access-denied'); } spaces_router('init'); } /** * Implementation of hook_flush_caches(). */ function spaces_flush_caches() { cache_clear_all('spaces_map', 'cache', TRUE); cache_clear_all('spaces_presets', 'cache', TRUE); cache_clear_all('spaces_registry', 'cache', TRUE); } /** * Implementation of hook_user(). */ function spaces_user($op, &$edit, &$account, $category = NULL) { switch ($op) { case 'view': case 'form': spaces_router('user', $account); break; } } /** * Implementation of hook_nodeapi(). */ function spaces_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) { if ($op == 'view' && $page && menu_get_object() === $node) { spaces_router('node', $node); } if ($op == 'prepare') { spaces_router('node', $node); } } /** * Implementation of hook_block(). */ function spaces_block($op, $delta = 0) { if (module_exists('jquery_ui') && module_exists('context_ui')) { if ($op == 'list') { return array( 'menu_editor' => array( 'info' => t('Reorder menu'), 'admin' => TRUE, ), ); } elseif ($op == 'view' && $delta == 'menu_editor') { if (spaces_access_admin()) { // Check for User space type which doesn't support this feature. $space = spaces_get_space(); if ($space && $space->type == 'user') { return array(); } return array( 'subject' => t('Reorder menu'), 'content' => drupal_get_form('spaces_get_menu_editor'), ); } } } } // Retrieve a link to open the spaces menu editor. function spaces_get_menu_editor(&$form_state) { jquery_ui_add(array('ui.draggable', 'ui.droppable', 'ui.sortable')); // @TODO: Use Libraries API to split these JS libraries out. drupal_add_js(drupal_get_path('module', 'context_ui') .'/json2.js'); drupal_add_js(drupal_get_path('module', 'context_ui') . '/jquery.pageEditor.js'); drupal_add_js(drupal_get_path('module', 'spaces') . '/spaces.js'); drupal_add_css(drupal_get_path('module', 'spaces') . '/spaces.css'); // Start from a system_settings_form. $form = system_settings_form(array()); unset($form['#theme']); $form['#attributes'] = array('class' => 'spaces-menu-editor'); $form['space_menu_items'] = array( '#type' => 'hidden', '#element_validate' => array('spaces_menu_items_validate'), ); $form['buttons']['edit'] = array( '#submit' => array(), '#type' => 'submit', '#value' => t('Edit'), '#attributes' => array('class' => 'spaces-menu-editor'), ); $form['buttons']['reset']['#access'] = FALSE; $form['buttons']['submit']['#value'] = t('Save'); $form['buttons']['submit']['#attributes'] = array('class' => 'spaces-menu-save', 'style' => 'display: none;'); $form['buttons']['cancel'] = array( '#submit' => array(), '#value' => t('Cancel'), '#type' => 'button', '#attributes' => array('class' => 'spaces-menu-cancel', 'style' => 'display: none;'), ); return $form; } /** * Validate callback for spaces menu editor. */ function spaces_menu_items_validate($element, &$form_state) { $base_path = base_path(); $items = array(); // @TODO: Split out json_decode() into a library. // We only check for context here because the block reaction plugin has good // PHP backwards compatibility support for json_decode(). Ideally, this would // either be part of Drupal core or split out into a utility module/library. if (module_exists('context') && $plugin = context_get_plugin('reaction', 'block')) { $weight = -20; if (!empty($form_state['values']['space_menu_items'])) { foreach (context_reaction_block::json_decode($form_state['values']['space_menu_items']) as $i) { if (strpos($i, $base_path) === 0) { $path = substr($i, strlen($base_path)); $path = array_shift(explode('#', $path)); $path = drupal_get_normal_path($path); $items[$path] = $weight; $weight++; } } } } $form_state['values']['space_menu_items'] = $items; // Remove button values. unset($form_state['values']['edit']); unset($form_state['values']['cancel']); } /** * Order a navigation links menu according to the order customized for * this space. */ function spaces_features_order_menu(&$links) { $weights = variable_get('space_menu_items', array()); // Mark the first link with a class that will allow the spaces menu editor // to find the menu in the DOM. $first = TRUE; foreach ($links as $k => $v) { if ($first) { $first = FALSE; if (isset($links[$k]['attributes']['class'])) { $links[$k]['attributes']['class'] .= ' spaces-menu-editable'; } else { $links[$k]['attributes']['class'] = 'spaces-menu-editable'; } } if (isset($weights[$v['href']])) { $links[$k]['#weight'] = $weights[$v['href']]; } } if (!empty($weights)) { uasort($links, 'element_sort'); } } /** * Reorder primary menu links. */ function spaces_preprocess_page(&$vars) { spaces_features_order_menu($vars['primary_links']); }