$info) { $items['spaces_'. $type] = array( 'name' => $info['title'], 'description' => t('Sets a spaces context.'), 'callback' => 'spaces_init_context', 'callback arguments' => array($type), 'example' => 'my-space', ); } return $items; } /** * Context prefix provider callback. */ function spaces_init_context($type, $sid) { static $once; if (!isset($once)) { $once = FALSE; } if (!$once) { // @TODO: remove the hardcoded space type here and attempt to // discover through the context prefix param : ( context_set('spaces', 'sid', $sid); $space = spaces_load($type, $sid, TRUE); spaces_set_space($space); $once = TRUE; } } /** * Implementation of hook_menu(). */ function spaces_menu() { $items = array(); $items['spaces-access-denied'] = array( 'title' => t('Access denied'), 'description' => t('Page callback for early-stack spaces router access denied.'), 'page callback' => 'spaces_access_denied', 'page arguments' => array(), 'access callback' => FALSE, 'type' => MENU_CALLBACK, ); $items['admin/build/spaces'] = array( 'title' => t('Spaces presets'), 'description' => t('Create and configure spaces for your site.'), 'page callback' => 'drupal_get_form', 'page arguments' => array('spaces_preset_default_form'), 'access callback' => 'user_access', 'access arguments' => array('administer spaces'), 'file' => 'spaces_admin.inc', 'type' => MENU_NORMAL_ITEM, ); $items['admin/build/spaces/presets'] = array( 'title' => t('Presets'), 'page callback' => 'drupal_get_form', 'page arguments' => array('spaces_preset_default_form'), 'access callback' => 'user_access', 'access arguments' => array('administer spaces'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -1, ); $items['admin/build/spaces/presets/add'] = array( 'title' => t('Add'), 'page callback' => 'drupal_get_form', 'page arguments' => array('spaces_preset_form', 'add'), 'access callback' => 'user_access', 'access arguments' => array('administer spaces'), 'type' => MENU_CALLBACK, ); $items['admin/build/spaces/presets/edit'] = array( 'page callback' => 'drupal_get_form', 'page arguments' => array('spaces_preset_form', 'edit'), 'access callback' => 'user_access', 'access arguments' => array('administer spaces'), 'type' => MENU_CALLBACK, ); $items['admin/build/spaces/presets/export'] = array( 'title' => t('Export preset'), 'page callback' => 'drupal_get_form', 'page arguments' => array('spaces_preset_export'), 'access callback' => 'user_access', 'access arguments' => array('administer spaces'), 'type' => MENU_CALLBACK, ); $items['admin/build/spaces/presets/delete'] = array( 'page callback' => 'drupal_get_form', 'page arguments' => array('spaces_preset_delete_form', 5, 6), 'access callback' => 'user_access', 'access arguments' => array('administer spaces'), 'type' => MENU_CALLBACK, ); $items['admin/build/spaces/presets/disable'] = array( 'page callback' => '_spaces_preset_disable_page', 'access callback' => 'user_access', 'access arguments' => array('administer spaces'), 'type' => MENU_CALLBACK, ); $items['admin/build/spaces/presets/enable'] = array( 'page callback' => '_spaces_preset_enable_page', 'access callback' => 'user_access', 'access arguments' => array('administer spaces'), 'type' => MENU_CALLBACK, ); return $items; } /** * Menu callback for early access_denied calls. Going through the a menu * callback allows Drupal bootstrap to complete. */ function spaces_access_denied() { drupal_access_denied(); exit; } /** * Implementation of hook_help(). */ function spaces_help($path, $arg) { switch ($path) { case 'admin/build/spaces/presets/export': return "

". t('You can use exported presets in your modules by returning an array of presets in hook_spaces_presets().') ."

"; } } /** * Implementation of hook_theme(). */ function spaces_theme() { $items = array(); $items['spaces_form'] = array(); $items['spaces_features_form'] = array(); $items['spaces_customize_item'] = array(); $items['spaces_preset_default_form'] = array(); $items['spaces_form_presets'] = array(); $items['spaces_block_customizer_settings_form'] = array(); return $items; } /** * Implementation of hook_theme_registry_alter(). */ function spaces_theme_registry_alter(&$theme_registry) { if (!in_array('spaces_preprocess_page', $theme_registry['page']['preprocess functions'])) { $theme_registry['page']['preprocess functions'][] = 'spaces_preprocess_page'; } } /** * Implementation of hook_user(). */ function spaces_user($op, &$edit, &$account, $category = NULL) { switch ($op) { case 'view': spaces_router('user view', $account); break; case 'form': spaces_router('user form', $account); break; } } /** * Implementation of hook_nodeapi(). */ function spaces_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) { switch ($op) { case 'view': if ($page && !$teaser && arg(0) == 'node' && arg(1) == $node->nid) { spaces_router('node view', $node); } break; } } /** * Implementation of hook_form_alter(). */ function spaces_form_alter(&$form, $form_state, $form_id) { if ($form['#id'] == 'node-form' && (arg(0) .'/'. arg(1) != 'admin/content')) { spaces_router('node form', $form['#node']); } // Blow away the customization form for the spaces menu -- // we don't want users messing with this... for now. if ($form_id == 'menu_overview_form' && $form['#menu']['menu_name'] == 'spaces') { $form = array(); $form['message'] = array( '#type' => 'markup', '#value' => '

'. t('The spaces menu is meant to be customized on a per-space basis - you can customize it on the features page of any space.') .'

', ); } } /** * Implementation of hook_block() */ function spaces_block($op = 'list', $delta = 0) { if ($op == 'list') { $blocks[1]['info'] = t('Spaces: Contextual Tools'); $blocks[2]['info'] = t('Spaces: Navigation'); $blocks[3]['info'] = t('Spaces: Utility links'); return $blocks; } else if ($op == 'view') { switch ($delta) { case 1: return _spaces_block_tools(); case 2: return _spaces_block_nav(); case 3: return _spaces_block_utility_links(); } } } /** * Implementation of hook_context_ui_define() * * hook_context_define provides a central method to define contextual behavior. The spaces * module extends this hook in the "spaces" key namespace. Available attributes are: * 'label', 'description', 'options', 'options_function', '#weight' * * @return * Keyed array which defines features. */ function spaces_context_ui_define() { $items = array(); return $items; } /** * Implementation of hook_spaces_settings(). */ function spaces_spaces_settings() { return array( 'home' => new space_setting_home(), ); } /** * Implementation of hook_spaces_customizers(). */ function spaces_spaces_customizers() { return array( 'menu' => new space_customizer_menu(), 'views' => new space_customizer_views(), 'block' => new space_customizer_block(), ); } /** * Implementation of hook_views_api(). */ function spaces_views_api() { return array( 'api' => 2, 'path' => drupal_get_path('module', 'spaces') .'/includes', ); } /** * Implementation of hook_views_pre_query(). */ function spaces_views_pre_query(&$view) { // Make spaces filter'd views non-cacheable foreach ($view->filter as $filter) { if ($filter['field'] == 'spaces.type') { $view->is_cacheable = false; } } if ($view->build_type == 'page' && $space = spaces_get_space()) { if ($feature = context_get('spaces', 'feature')) { $view = space_customizer_views::customize($space, $feature, $view); } } } /** * Implementation of hook_context_ui_setters(). */ function spaces_context_ui_setters() { $type_options = array(); foreach (spaces_types() as $type => $info) { $type_options[$type] = $info['title']; } $items = array(); $items['spaces_type'] = array( '#title' => t('Spaces type'), '#type' => 'checkboxes', '#options' => $type_options, '#description' => t('Set this context when the selected space types are active.'), ); return $items; } /** * Implementation of hook_context_ui_getters(). */ function spaces_context_ui_getters() { $type_options = array(); foreach (spaces_types() as $type => $info) { $type_options[$type] = $info['title']; } $items = array(); $items['spaces'] = array( '#title' => t('Spaces feature'), '#tree' => 'true', 'label' => array( '#title' => t('Label'), '#description' => t('The name of your feature.'), '#type' => 'textfield', ), 'description' => array( '#title' => t('Description'), '#description' => t('A short description to display with your feature.'), '#type' => 'textfield', ), // Support only 1 menu item currently 'menu' => array( '#tree' => TRUE, 0 => array( '#tree' => TRUE, 'title' => array( '#title' => t('Menu link'), '#description' => t('The link text for your feature.'), '#type' => 'textfield', ), 'href' => array( '#title' => t('Menu path'), '#description' => t('The path to your feature. Example: blog'), '#type' => 'textfield', ), ), ), 'types' => array( '#title' => t('Enabled types'), '#description' => t('Choose the space types for which this feature can be enabled. Leave empty to allow this feature to be used with any space type.'), '#type' => 'checkboxes', '#options' => $type_options, '#multiple' => TRUE, ), ); return $items; } /** * SPACES API ========================================================= */ /** * Interface for space objects. */ interface space { /** * Core API-related functions */ function __construct($type, $sid = NULL, $is_active = FALSE); function save(); function delete(); /** * Method that defines the feature options available for this space * type. * * @return array * A FormAPI suitable options array of feature options. */ function feature_options(); /** * Method that allows the space type to add to/modify the utility * "links" provided to visitors in a space. * * @param $links * The current space utility links (for use with theme_links) array * passed by reference. */ function links(&$links); /** * Method that provides a space-type specific check for whether the * provided feature is accessible by the current user. * * @param $feature * The feature identifier string. * * @return bool * TRUE if the user has access to this feature. FALSE if not. */ function feature_access($feature = NULL); /** * Method that provides a space-type specific check for whether the * the space can be administered by the current user. * * @return bool * TRUE if the user has admin access to this space. FALSE if not. */ function admin_access(); /** * Master router method that allows the space type to define routing * workflow rules. Currently called at the following hooks: * * hook_menu(), where $may_cache == FALSE * hook_nodeapi(), where $op == 'view' * hook_form_alter(), when editing a node * hook_user(), where $op == 'view' * * @param $op * The current hook from which the router is being called. * Can be one of the following: 'menu', 'node view', 'node form', * 'user view' * @param $object * The object relevant to the current $op called. e.g. when * $op == 'node view', $object is the node object. * @param $is_active * Boolean for whether this router has been called as part of a * fully instantiated space object. If FALSE, the router should * not assume the space has been fully constructed and should take * the appropriate actions as necessary. * * @return bool * TRUE to allow the user to pass through. FALSE to return a * drupal_access_denied() page to the user. */ function router($op, $object = NULL, $is_active = TRUE); /** * Redirect handler that abstracts redirect logic and allows space * types to define their own definitions of various spaces concepts. * * @param $op * The page being redirected to. Currently only $op value is 'home' */ function redirect($op = 'home'); /** * Allows the space type to implement custom form options on the * feature/preset form. * * @return array * A FormAPI element array. It will be included as a child element * of the master space feature form. */ function form(); /** * Validate handler for the space type's custom form. * * @param $values * The submitted values. */ function validate($values); /** * Custom submit handler for the space type's custom form. For * example, allows the space type to process values in preparation * for spaces_save(). * * @param $values * The submitted values. * * @return array * An array of values. */ function submit($values); /** * Views filter callback that allows the space type to filter views * when it is the current active space. * * @param $query * The views query object. */ function views_filter($is_active, &$query); /** * Allows the space type to take additional action when enforcing a * preset against the current space. * * @param $preset * A spaces preset definition. */ function preset_enforce($preset); } /** * Interface for space settings. */ interface space_setting { function __construct($id = NULL); function form($space, $value = array()); function validate($space, $value); function submit($space, $value); } /** * Interface for space customizers. */ interface space_customizer { function form($space, $feature); function validate($space, $feature, $value); function submit($space, $feature, $value); function customize($space, $feature, &$object = NULL); } /** * Provides a homepage setting for each space. */ class space_setting_home implements space_setting { var $id; function __construct($id = NULL) { if ($id) { $this->id = $id; } else { $this->id = 'home'; } } function form($space, $value = array()) { $options = array(0 => '---'); foreach (spaces_features($space->type) as $f => $feature) { if ($feature->spaces['menu'] && isset($space->features[$f]) && $space->features[$f] != SPACES_FEATURE_DISABLED) { $options[$f] = $feature->spaces['label']; } } $form = array( '#title' => t('Homepage'), '#description' => t('The default page for this space.'), '#type' => 'select', '#options' => $options, '#validate' => array('spaces_setting_validate' => array($this->id)), '#default_value' => $value ? $value : 0, ); return $form; } function validate($space, $value) { // Exclude space "prototypes" (like that used for the preset form) if ($space->sid) { if (!$value && is_array($space->features) && (array_sum($space->features) != 0)) { form_set_error('settings]['. $this->id, t('You must select a homepage for this space.')); } } } function submit($space, $value) { return $value; } } /** * Customizer for feature menus. */ class space_customizer_menu implements space_customizer { var $name = 'Menu'; function form($space, $feature) { $features = spaces_features(); $f = $features[$feature]; $form = array(); // Customize menus if (isset($f->spaces['menu']) && count($f->spaces['menu'])) { $feature_menu = array(); // Get customized values $feature_menu = $f->spaces['menu']; $feature_menu = $this->customize($space, $feature, $feature_menu); foreach ($feature_menu as $key => $item) { $form[$item['href']] = array( '#title' => $item['href'], '#type' => 'fieldset', '#tree' => TRUE, ); $form[$item['href']]['title'] = array( '#title' => t('Title'), '#type' => 'textfield', '#size' => 40, '#maxlength' => 255, '#default_value' => $item['title'], ); } } return $form; } function validate($space, $feature, $value) { return; } function submit($space, $feature, $value) { $features = spaces_features(); $feature_menu = $features[$feature]->spaces['menu']; foreach ($value as $path => $item) { if ($item == $feature_menu[$path]) { unset($value[$path]); } } return $value; } function customize($space, $feature, &$menu = NULL) { $customizer = array(); foreach ($space->customizer as $c) { $customizer = array_merge($customizer, $c['menu']); } foreach ($menu as $k => $item) { if (isset($customizer[$item['href']])) { $menu[$k]['title'] = $customizer[$item['href']]['title']; } } return $menu; } } /** * Customizer for views titles and headers. */ class space_customizer_views implements space_customizer { var $name = 'Views'; function form($space, $feature) { $features = spaces_features(); $f = $features[$feature]; $form = array(); if (isset($f->views) && is_array($f->views)) { foreach ($f->views as $view_name) { $view = views_get_view($view_name); $view = $this->customize($space, $feature, $view); // Only allow customization of page views for now // @TODO: add input format checking on headers/footers/etc. if ($view && $view->page == TRUE) { $form[$view_name] = array( '#title' => $view_name, '#type' => 'fieldset', '#tree' => TRUE, ); // $default_page_title = isset($customizer['views'][$view_name]['page_title']) ? $customizer['views'][$view_name]['page_title'] : $view->page_title; $form[$view_name]['page_title'] = array( '#title' => t('Page title'), '#type' => 'textfield', '#size' => 40, '#default_value' => $view->page_title, ); // $default_page_header = isset($customizer['views'][$view_name]['page_header']) ? $customizer['views'][$view_name]['page_header'] : $view->page_header; $form[$view_name]['page_header'] = array( '#title' => t('Page header'), '#type' => 'textarea', '#rows' => 2, '#cols' => 40, '#default_value' => $view->page_header, ); } } } return $form; } function validate($space, $feature, $value) { return; } function submit($space, $feature, $value) { // Would like to prune unset values, but it appears that views // caching makes it unusable... return $value; } function customize($space, $feature, &$view = NULL) { if (isset($space->customizer[$feature]['views'][$view->name])) { // Apply customizations to the view object $customizer = $space->customizer[$feature]['views'][$view->name]; foreach ($customizer as $property => $value) { $view->{$property} = $value; } } return $view; } } /** * Customizer for feature blocks. */ class space_customizer_block implements space_customizer { var $name = 'Blocks'; /** * Implementation of form(). */ function form($space, $feature) { $form = array('#theme' => 'spaces_block_customizer_settings_form', '#tree' => TRUE); $features = spaces_features(); $f = $features[$feature]; $customizer = !empty($space->customizer[$feature]['block']) ? $space->customizer[$feature]['block'] : array(); $info = array(); global $theme_key; init_theme(); $regions = system_region_list($theme_key); if (!empty($f->block)) { foreach ($f->block as $block) { // @TODO: remove this once context_ui is less confused about itself. $block = (array) $block; $bid = "{$block['module']}-{$block['delta']}"; if (!empty($block['region'])) { if (!isset($info[$block['module']])) { $info[$block['module']] = module_invoke($block['module'], 'block', 'list'); // If the block is provided by Views, doctor the info to strip out the // view name leaving only the block's display name. if ($block['module'] == 'views') { foreach ($info[$block['module']] as $k => $v) { $viewname = strpos($v['info'], ':'); if ($viewname !== FALSE) { $v['info'] = substr($v['info'], $viewname + 2); $info[$block['module']][$k] = $v; } } } } // Sanity check that this region exists $region = $block['region']; if (!empty($regions[$region])) { if (!isset($form[$region])) { $form[$region] = array( '#title' => $regions[$region], '#tree' => TRUE, ); } $block_details = module_invoke($block['module'], 'block', 'view', $block['delta']); if (!empty($info[$block['module']][$block['delta']])) { $default_weight = isset($block['weight']) ? $block['weight'] : 0; $default_status = isset($block['status']) ? $block['status'] : 1; $default_subject = !empty($block_details['subject']) ? $block_details['subject'] : ''; $form[$region][$bid] = array( '#tree' => TRUE, '#weight' => isset($customizer[$region][$bid]['weight']) ? $customizer[$region][$bid]['weight'] : $default_weight, ); $form[$region][$bid]['weight'] = array( '#type' => 'weight', '#delta' => 25, '#default_value' => isset($customizer[$region][$bid]['weight']) ? $customizer[$region][$bid]['weight'] : $default_weight, ); $form[$region][$bid]['status'] = array( '#type' => 'checkbox', '#default_value' => isset($customizer[$region][$bid]['status']) ? $customizer[$region][$bid]['status'] : $default_status, ); // If the block subject is empty, it's likely to be for a good reason // e.g. the subject is generated dynamically or the block is slimmed // down. Let's respect that. if (!empty($default_subject)) { $form[$region][$bid]['subject'] = array( '#type' => 'textfield', '#default_value' => !empty($customizer[$region][$bid]['subject']) ? $customizer[$region][$bid]['subject'] : $default_subject, '#description' => $info[$block['module']][$block['delta']]['info'], ); } else { $form[$region][$bid]['subject'] = array( '#type' => 'markup', '#value' => $info[$block['module']][$block['delta']]['info'], ); } // Pass default values to the submit handler so it can omit them. $form[$region][$bid]['default_weight'] = array( '#type' => 'value', '#value' => $default_weight, ); $form[$region][$bid]['default_status'] = array( '#type' => 'value', '#value' => $default_status, ); $form[$region][$bid]['default_subject'] = array( '#type' => 'value', '#value' => $default_subject, ); } } } } } return $form; } /** * Implementation of validate(). */ function validate($space, $feature, $value) { return; } /** * Implementation of submit(). * Iterate through and only record the aspects of each block that have been customized. */ function submit($space, $feature, $value) { foreach ($value as $region => $blocks) { foreach ($blocks as $bid => $block) { foreach (array('weight', 'status', 'subject') as $key) { if (isset($block[$key])) { if ($block[$key] == $block["default_{$key}"]) { unset($value[$region][$bid][$key]); } } unset($value[$region][$bid]["default_{$key}"]); } } } return $value; } /** * Implementation of customize(). */ function customize($space, $feature, &$block = NULL) { // Unset disabled blocks and change weights based on customizer settings. See spaces_context_ui_active_contexts_alter. if (!empty($space->customizer[$feature]['block'])) { $customizer = $space->customizer[$feature]['block']; foreach ($block as $key => $b) { // @TODO: remove this once context_ui is less confused about itself. $b = (array) $b; $block[$key] = (array) $block[$key]; $bid = "{$b['module']}-{$b['delta']}"; // If the block is not enabled, yank it out of the blocks array if (!empty($customizer[$b['region']][$bid])) { if (isset($customizer[$b['region']][$bid]['status']) && $customizer[$b['region']][$bid]['status'] === 0) { unset($block[$key]); } if (isset($customizer[$b['region']][$bid]['weight'])) { $block[$key]['weight'] = $customizer[$b['region']][$bid]['weight']; } } } } } /** * Additional method: customize_subject() for customizing a block subject. */ function customize_subject($space, $feature, &$block) { if (!empty($space->customizer[$feature]['block'])) { $customizer = $space->customizer[$feature]['block']; $bid = "{$block->module}-{$block->delta}"; if (!empty($customizer[$block->region][$bid]['subject'])) { $block->subject = $customizer[$block->region][$bid]['subject']; } } } } /** * Implementation of hook_context_ui_active_contexts_alter(). */ function spaces_context_ui_active_contexts_alter(&$contexts) { $space = spaces_get_space(); foreach ($contexts as $context) { if ($context->block && $context->attribute == 'feature') { $feature = $context->value; space_customizer_block::customize($space, $feature, $context->block); } } } /** * Load a space. * * @param $type * The type of the space to be loaded. Must be one of the keys in the * array returned by spaces_types(). * @param $sid * The id of the space to be loaded. If omitted, a "prototype" space * will be constructed. * @param $is_active * Optional boolean flag for whether this space is active or not. * Defaults to FALSE. * * @return * The requested space object or FALSE if something went wrong. */ function spaces_load($type, $sid = NULL, $is_active = FALSE) { $types = spaces_types(); if (isset($types[$type])) { $class = $types[$type]['class']; // Create a new space object $space = new $class($type, $sid, $is_active); // Initialize various space variables $space->type = $type; $space->features = array(); $space->settings = array(); $space->customizer = array(); // Initialize space specific settings if $sid is provided if ($sid) { $space->sid = $sid; if ($prefix = context_prefix_api('load', array('provider' => 'spaces_'. $type, 'id' => $sid))) { $space->prefix = $prefix['prefix']; } // Load features $result = db_query('SELECT id, value FROM {spaces_features} WHERE sid = %d AND type = "%s" ORDER BY weight ASC', $sid, $type); while ($row = db_fetch_object($result)) { $space->features[$row->id] = $row->value; } // Load settings $result = db_query('SELECT id, value FROM {spaces_settings} WHERE sid = %d AND type = "%s"', $sid, $type); while ($row = db_fetch_object($result)) { $space->settings[$row->id] = unserialize($row->value); } // Load customizer & preset $row = db_fetch_object(db_query("SELECT customizer, preset FROM {spaces} WHERE sid = %d AND type = '%s'", $space->sid, $space->type)); $space->customizer = $row->customizer ? unserialize($row->customizer) : array(); // Enforce preset or use default if not found $default_presets = variable_get('spaces_default_presets', array()); if ($row->preset) { $space->preset = $row->preset; } else if (empty($space->preset) && isset($default_presets[$type])) { $space->preset = $default_presets[$type]; } if (!empty($space->preset)) { spaces_preset_enforce($space); } } return $space; } return false; } /** * Saves a space object's feature/setting values. * * @param $space * The space object to save. * * @return * Returns TRUE on success, FALSE on failure. */ function spaces_save($space) { if ($space->sid) { // Enforce the preset spaces_preset_enforce($space); // Update features db_query("DELETE FROM {spaces_features} WHERE sid = %d AND type = '%s'", $space->sid, $space->type); $valid_features = spaces_features($space->type); $weight = -10; foreach ($space->features as $feature => $value) { if (isset($valid_features[$feature])) { $values = array($space->sid, $space->type, $feature, $value, $weight); db_query('INSERT INTO {spaces_features} (sid, type, id, value, weight) VALUES (%d, "%s", "%s", "%s", %d)', $values); $weight++; } } // Update settings db_query("DELETE FROM {spaces_settings} WHERE sid = %d AND type = '%s'", $space->sid, $space->type); // Build list of valid settings including those associated with features. $valid_settings = spaces_settings($space->type); foreach (spaces_features() as $feature) { if (isset($feature->spaces['settings'])) { foreach ($feature->spaces['settings'] as $id => $setting) { $valid_settings[$id] = $setting; } } } foreach ($space->settings as $setting => $value) { if (isset($valid_settings[$setting]) && !empty($value)) { $value = serialize($value); $values = array($space->sid, $space->type, $setting, $value); db_query('INSERT INTO {spaces_settings} (sid, type, id, value) VALUES (%d, "%s", "%s", "%s")', $values); } } // Update preset & customizer $exists = db_result(db_query("SELECT count(sid) FROM {spaces} WHERE sid = %d AND type = '%s'", $space->sid, $space->type)); if ($exists) { db_query("UPDATE {spaces} SET preset = '%s', customizer = '%s' WHERE sid = %d AND type = '%s'", $space->preset, serialize($space->customizer), $space->sid, $space->type); } else { db_query("INSERT INTO {spaces} (sid, type, preset, customizer) VALUES(%d, '%s', '%s', '%s')", $space->sid, $space->type, $space->preset, serialize($space->customizer)); } // Save context prefix if space type allows prefix customization $types = spaces_types(); $save_prefix = isset($types[$space->type]['custom prefixes']) && $types[$space->type]['custom prefixes']; if ($space->prefix && $save_prefix) { // We need to concatenate the type/sid so that collisions between // space types do not occur. $prefix = array( 'provider' => 'spaces_'. $space->type, 'id' => $space->sid, ); context_prefix_api('delete', $prefix); $prefix['prefix'] = $space->prefix; context_prefix_api('insert', $prefix); } // Allow space type to do its own saving $space->save(); return true; } return false; } /** * Deletes a space object's records in the database. * * @param $space * The space object to delete. * * @return * Returns TRUE for now. */ function spaces_delete($space) { // Remove all features and settings db_query("DELETE FROM {spaces} WHERE sid = %d AND type = '%s'", $space->sid, $space->type); db_query("DELETE FROM {spaces_features} WHERE sid = %d AND type = '%s'", $space->sid, $space->type); db_query("DELETE FROM {spaces_settings} WHERE sid = %d AND type = '%s'", $space->sid, $space->type); // Clear the prefix path from the context_prefix table $prefix = array('provider' => 'spaces_'. $space->type, 'id' => $space->sid); context_prefix_api('delete', $prefix); // Allow space type to do its own deleting $space->delete(); return true; } /** * Enforces a spaces preset. */ function spaces_preset_enforce(&$space) { $presets = spaces_presets($space->type); if (isset($space->preset) && isset($presets[$space->preset])) { $preset = $presets[$space->preset]['preset']; // Enforce features, settings, customizer $keys = array('features', 'settings', 'customizer'); foreach ($keys as $key) { if (isset($preset[$key]) && is_array($preset[$key])) { foreach ($preset[$key] as $k => $v) { if ($preset['locked'][$key][$k] || !isset($space->{$key}[$k])) { $space->{$key}[$k] = $v; } } } } // Type-specific presets $space->preset_enforce($preset); } } /** * Invokes hook_spaces_types() to gather an array of space types and * associated classes. * * Implementing modules should provide an associate array in * hook_spaces_types() of the following format: * * return array( * $id => array( * 'class' => $space_class, * 'title' => $space_type_name, * 'custom prefixes' => $bool, * ), * ); * * Where: * * $id: A unique string identifier for this space type. e.g. "og" * $space_class: The PHP class to construct for this space. e.g. "spaces_og" * $space_type_name: The human-readable name of your space type. e.g. "Group space" * $bool: TRUE or FALSE for whether spaces should provide a UI for * users to provide a custom prefix path for each space. If FALSE, * the implementing module should implement * hook_context_prefix_prefixes() in order to provide prefixes * programmatically. * * @param $reset * Optional reset flag for clearing the static cache. * * @return * An array of space types where $key => $value corresponds to the space type => space class. */ function spaces_types($reset = false) { static $spaces_types; if (!isset($spaces_types) || $reset) { $spaces_types = module_invoke_all('spaces_types'); } return $spaces_types; } /** * Gather an array of spaces presets from the DB. * * @param $type * Optional space type to filter results by. * @param $include_disabled * Optional flag to return all presets, including disabled ones. * @param $reset * Optional reset flag for clearing the static cache. * * @return * An array of space types where $key => $value corresponds to the space type => space class. */ function spaces_presets($type = NULL, $include_disabled = FALSE, $reset = FALSE) { static $presets; if (!isset($presets)) { $presets = array(); $result = db_query("SELECT * FROM {spaces_presets}"); $disabled = variable_get('spaces_disabled_presets', array()); while ($row = db_fetch_object($result)) { $presets[$row->type][$row->id] = array( 'name' => $row->name, 'description' => $row->description, 'preset' => unserialize($row->value), 'disabled' => isset($disabled[$row->type][$row->id]), ); } // Collect presets provided by modules in code foreach(module_implements('spaces_presets') as $module) { $items = call_user_func($module .'_spaces_presets'); foreach($items as $id => $preset) { $presets[$preset['type']][$id] = array( 'name' => $preset['name'], 'description' => $preset['description'], 'preset' => $preset['preset'], 'disabled' => isset($disabled[$preset['type']][$id]), 'module' => $module, ); } } } // Move filtering outside main condition in order to hit the DB only once $return = $presets; if (!$include_disabled) { foreach (array_keys($return) as $preset_type) { foreach ($return[$preset_type] as $id => $preset) { if ($preset['disabled']) { unset($return[$preset_type][$id]); } } } } if ($type) { return isset($return[$type]) ? $return[$type] : array(); } return $return; } /** * Wrapper function around spaces_set_space(). Retrieves the current * active space. */ 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. * @param $reset * Optional flag to reset the static cache. * * @return * The active space object or FALSE if there is no active space. */ function spaces_set_space($space = NULL, $reset = FALSE) { static $current_space; if (!isset($current_space) || $reset) { $current_space = $space; if ($space) { // Context UI integration with spaces_type setter. context_ui_set('spaces_type', $space->type); } } return $current_space ? $current_space : FALSE; } /** * Wrapper around implementations of $space->router. Provides * additional intelligence, including a global killswitch and routing * when no spaces are active. * * @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) { // Check global killswitch if (spaces_router_get()) { $access = true; $types = spaces_types(); // Run the router for the active space if ($space = spaces_get_space()) { $access = $access && $space->router($op, $object); unset($types[$space->type]); } // Run each non-active space type's router foreach ($types as $type => $info) { $access = $access && call_user_func(array($info['class'], 'router'), $op, $object, FALSE); } if (!$access && !user_access('administer spaces')) { // We use a menu callback here rather than drupal_access_denied() // because spaces_router() is called at times from hook_init(). // Using drupal_access_denied() can terminate the page request before // bootstrap completes, leading to all sorts of havoc. menu_set_active_item('spaces-access-denied'); } } } /** * Wrapper around spaces_router_get(). */ function spaces_router_set($status) { return spaces_router_get($status, TRUE); } /** * Sets a static variable that is used to disable spaces routing * altogether -- e.g. for install/update scripts, migrations, etc. * * @param $enabled * Optional boolean for enabling/disabling spaces routing. * @param $reset * Optional boolean for resetting the static cache. * * @return * Returns a boolean for whether routing is enabled/disabled. */ function spaces_router_get($enabled = 1, $reset = FALSE) { static $status; if (!isset($status) || $reset) { $status = $enabled; } return $status; } /** * 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 $spaces_features; if (!isset($spaces_features) || $reset) { $spaces_features = array('all' => array(), 'common' => array()); foreach (context_ui_enabled_contexts('spaces') as $feature) { if (!empty($feature->spaces)) { if (!empty($feature->spaces['types'])) { foreach ($feature->spaces['types'] as $t) { $spaces_features[$t][$feature->value] = $feature; } } else { $spaces_features['common'][$feature->value] = $feature; } $spaces_features['all'][$feature->value] = $feature; } } foreach (array_keys($spaces_features) as $t) { if ($t != 'all' && $t != 'common') { $spaces_features[$t] = array_merge($spaces_features[$t], $spaces_features['common']); } } } if ($type) { return !empty($spaces_features[$type]) ? $spaces_features[$type] : $spaces_features['common']; } return $spaces_features['all']; } /** * Retrieve all available settings. * * @param $reset * Optional boolean flag for resetting the static cache. * * @return * Keyed array of potential settings. */ function spaces_settings($type = NULL, $reset = FALSE) { static $spaces_settings; if (!isset($spaces_settings) || $reset) { $spaces_settings = array('all' => array(), 'common' => array()); foreach (module_implements('spaces_settings') as $module) { $function = $module .'_spaces_settings'; $settings = $function(); foreach ($settings as $setting) { if (!empty($setting->types)) { foreach ($setting->types as $t) { if (!isset($spaces_settings[$t])) { $spaces_settings[$t] = array(); } $spaces_settings[$t][$setting->id] = $setting; } } else { $spaces_settings['common'][$setting->id] = $setting; } $spaces_settings['all'][$setting->id] = $setting; } } foreach (array_keys($spaces_settings) as $t) { if ($t != 'all' && $t != 'common') { $spaces_settings[$t] = array_merge($spaces_settings[$t], $spaces_settings['common']); } } } if ($type) { return !empty($spaces_settings[$type]) ? $spaces_settings[$type] : $spaces_settings['common']; } return $spaces_settings['all']; } /** * Retrieve all available customizers. * * @param $reset * Optional boolean flag for resetting the static cache. * * @return * Keyed array of customizers. */ function spaces_customizers($reset = FALSE) { static $customizers; if (!isset($customizers) || $reset) { $customizers = array(); $customizers = module_invoke_all('spaces_customizers'); } return $customizers; } /** * Returns a content type => features map. * * @param $reset * Optional boolean flag for resetting the static cache. * * @return * Keyed array where $nodetype => $feature. */ function spaces_content_types($reset = FALSE) { static $map; if (!isset($map) || $reset) { $map = array(); $features = spaces_features(); foreach ($features as $id => $feature) { if (is_array($feature->node)) { foreach ($feature->node as $type) { $map[$type] = $id; } } } } return $map; } /** * Rebuilds the spaces menu stored in the menu_links tables via the menu API. */ function spaces_menu_rebuild() { // @TODO there is probably a better API-based way to achieve this ... $spaces_menu = array(); $result = db_query("SELECT * FROM {menu_links} WHERE menu_name = 'spaces' AND module = 'spaces'"); while ($item = db_fetch_object($result)) { $spaces_menu[$item->link_path] = $item; } $cleanup = $spaces_menu; // Loop through features and create or update menu items. $cache = array(); $features = spaces_features(); foreach ($features as $feature) { if (isset($feature->spaces['menu']) && count($feature->spaces['menu'])) { foreach ($feature->spaces['menu'] as $key => $item) { // Use the array key as the menu path if not set explicitly -- // should allow backwards compatibility with old feature // defintions. $path = !empty($item['href']) ? $item['href'] : $key; // Remove this item from list of menu items to clean up unset($cleanup[$path]); // Load an existing menu item if it already exists $existing_item = isset($spaces_menu[$path]) ? menu_link_load($spaces_menu[$path]->mlid) : array(); $customized = $existing_item['customized']; $attributes = isset($item['attributes']) ? array('attributes' => $item['attributes']) : array(); $new_item = array( 'link_title' => ($customized && isset($existing_item['title'])) ? $existing_item['title'] : $item['title'], 'link_path' => $path, 'options' => ($customized && isset($existing_item['options'])) ? $existing_item['options'] : $attributes, 'menu_name' => 'spaces', 'module' => 'spaces', ); $new_item = array_merge($existing_item, $new_item); $mlid = menu_link_save($new_item); if ($mlid) { $cache[$new_item['link_path']] = $feature->value; } } } } // Clean up stale items for features that no longer exist foreach ($cleanup as $item) { menu_link_delete($item->mlid); } cache_set('spaces_menu', $cache, 'cache'); return $cache; } /** * Returns a links array in the theme_links() format of the current * space's menu items for features accessible to the current user. Each * item has a keyed array of children items if applicable. * * @return * Array of links. */ function spaces_features_menu($space = NULL, $reset = FALSE) { static $cache; $cache = !isset($cache) ? array() : $cache; $space = empty($space) ? spaces_get_space() : $space; // Sanity check on our space object. if ($space && !empty($space->sid)) { // Cache by space type $cache[$space->type] = !isset($cache[$space->type]) ? array() : $cache[$space->type]; if (!isset($cache[$space->type][$space->sid]) || $reset) { // Load up the spaces menu links $menu = menu_navigation_links('spaces'); $menu = space_customizer_menu::customize($space, '', $menu); // Retrieve the menu cache for a path to feature mapping $menu_cache = cache_get('spaces_menu'); $menu_cache = !$menu_cache ? spaces_menu_rebuild() : $menu_cache->data; // Sort the menu by feature weight & hide any items // for features that are not available (this **should**) // be handled by access control on the links' respective // menu callbacks, but awkward permissioning sometimes // makes it nice for these to be hidden manually. $weights = array_flip(array_keys($space->features)); $weighted = array(); foreach ($menu as $k => $item) { $feature = $menu_cache[$item['href']]; if ($space->feature_access($feature)) { // Retrieve path > feature > weight $menu[$k]['#weight'] = $weights[$feature]; } else { unset($menu[$k]); } } uasort($menu, 'element_sort'); $cache[$space->type][$space->sid] = $menu; } if (!empty($cache[$space->type][$space->sid])) { return $cache[$space->type][$space->sid]; } } return array(); } /** * Preset options form that can be reused by implementing modules. * * @param $space * A space object. * * @return * A FormAPI array structure. */ function spaces_form_presets($space) { $presets = spaces_presets($space->type); if ($presets) { $default_presets = variable_get('spaces_default_presets', array()); if (isset($space->preset) && !empty($presets[$space->preset])) { $default_preset = $space->preset; } else if (isset($default_presets[$space->type])) { $default_preset = $default_presets[$space->type]; } else { $default_preset = NULL; } // Radios for presets $form = array( '#tree' => false, '#theme' => 'spaces_form_presets', ); $form['preset'] = array( '#title' => t('Preset'), '#type' => 'radios', '#required' => true, '#options' => array(), '#default_value' => $default_preset, ); $form['info'] = array(); foreach ($presets as $id => $preset) { $form['preset']['#options'][$id] = $preset['name']; $form['info'][$id] = array( '#type' => 'item', '#title' => $preset['name'], '#description' => $preset['description'], ); } return $form; } return array(); } /** * Menu admin access wrapper. */ function spaces_admin_access($type = NULL, $op = NULL) { $return = false; if ($space = spaces_get_space()) { if (isset($type)) { if ($space->type == $type && $space->admin_access()) { $return = true; } } else if ($space->admin_access()) { $return = true; } } if ($op == 'features') { return $return && (user_access('administer spaces') || user_access('configure spaces features')); } return $return; } /** * Menu feature access wrapper. */ function spaces_feature_access($feature = NULL) { if ($space = spaces_get_space()) { if (!$space->feature_access($feature)) { return false; } if (user_access('access content')) { return true; } } return false; } /** * A mild abstraction of hook_menu() items that can be used by * implementing modules to embed/graft relevant spaces items into the * menu tree. Should only be used when the $may_cache argument of * hook_menu() is false. * * @param $space * A space object. * @param $local_tasks * Optional boolean flag for whether the items are fully rendered as * local tasks. * @param $path_prefix * A path to prefix the menu item paths by. * * @return * An array of menu items. */ function spaces_active_space_menu($type, $local_tasks = FALSE, $path_prefix = '') { $types = spaces_types(); $path_prefix = !empty($path_prefix) ? $path_prefix .'/' : ''; if ($local_tasks == FALSE) { $items[$path_prefix. 'spaces'] = array( 'title' => t('!space_type settings', array('!space_type' => $types[$type]['title'])), 'page callback' => 'drupal_get_form', 'page arguments' => array('spaces_basic_form'), 'access callback' => 'spaces_admin_access', 'access arguments' => array($type), 'type' => MENU_NORMAL_ITEM, ); } else { $items[$path_prefix. 'spaces'] = array( 'title' => t('Spaces'), 'page callback' => 'drupal_get_form', 'page arguments' => array('spaces_basic_form'), 'access callback' => 'spaces_admin_access', 'access arguments' => array($type), 'type' => MENU_LOCAL_TASK, 'weight' => 1, ); } $items[$path_prefix. 'spaces/setup'] = array( 'title' => t('Basic setup'), 'page callback' => 'drupal_get_form', 'page arguments' => array('spaces_basic_form'), 'access callback' => 'spaces_admin_access', 'access arguments' => array($type), 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => 0, ); $items[$path_prefix. 'spaces/features'] = array( 'title' => t('Features'), 'page callback' => 'spaces_features_page', 'page arguments' => array(), 'access callback' => 'spaces_admin_access', 'access arguments' => array($type, 'features'), 'type' => MENU_LOCAL_TASK, 'weight' => 1, ); return $items; } /** * THEME FUNCTIONS ==================================================== */ /** * Generates an array of utility links for the current space suitable * for use in theme_links(). */ function spaces_space_links() { $links = array(); if ($space = spaces_get_space()) { $space->links($links); drupal_alter('space_links', $links); } return $links; } /** * Implementation of hook_context_ui_node_links_alter(). * @TODO: complain to the author of context_ui that this is too long. */ function spaces_context_ui_node_links_alter(&$links) { $output = ''; // Perform additional logic if a spaces feature is active. if ($feature = context_get('spaces', 'feature')) { $space = spaces_get_space(); // Feature is inaccessible if ($space && !$space->feature_access($feature)) { $features = spaces_features($space->type); if (isset($features[$feature]->node)) { foreach ($features[$feature]->node as $type) { unset($links[$type]); } } } } } /** * Form theming for the block customizer settings form. */ function theme_spaces_block_customizer_settings_form($form) { // Add draggable weights drupal_add_js('misc/tableheader.js'); $output = ''; // List of block regions that should force an empty display $force_empty = array('content'); global $theme_key; init_theme(); $regions = system_region_list($theme_key); foreach ($force_empty as $region) { if (empty($form[$region]) && !empty($regions[$region])) { $output .= "
"; $output .= "

{$regions[$region]}

"; $output .= "
". t('There are no options available for this region.') ."
"; $output .= "
"; } } foreach (element_children($form) as $a) { drupal_add_tabledrag('spaces-customizer-blocks-'. $a, 'order', 'sibling', 'block-weight'); $rows = array(); uasort($form[$a], 'element_sort'); foreach (element_children($form[$a]) as $b) { $form[$a][$b]['weight']['#attributes'] = array('class' => 'block-weight'); $row = array( 'dummy' => '', 'status' => drupal_render($form[$a][$b]['status']), 'title' => array('data' => drupal_render($form[$a][$b]['subject']), 'class' => 'fill'), 'weight' => drupal_render($form[$a][$b]['weight']), ); $rows[] = array('data' => $row, 'class' => 'draggable'); } $output .= "
"; $output .= "

{$form[$a]['#title']}

"; $output .= theme('table', array(), $rows, array('id' => 'spaces-customizer-blocks-'. $a)); $output .= "
"; } $output .= drupal_render($form); return $output; } /** * Form theme function for spaces presets. */ function theme_spaces_form_presets($form) { $output = ''; // Render presets in a table $rows = array(); foreach (element_children($form['info']) as $id) { unset($form['preset'][$id]['#title']); $row = array( drupal_render($form['preset'][$id]), drupal_render($form['info'][$id]), ); $rows[] = $row; } $output .= theme('table', array(array('data' => $form['preset']['#title'], 'colspan' => 2)), $rows); drupal_render($form['preset']); // Throw out the rest of this element $output .= drupal_render($form); return $output; } function spaces_preprocess_page(&$vars) { if (variable_get('menu_primary_links_source', 'primary-links') == 'spaces') { $vars['primary_links'] = spaces_features_menu(); } if (variable_get('menu_secondary_links_source', 'secondary-links') == 'spaces') { $vars['secondary_links'] = spaces_features_menu(); } } /** * BLOCKS ============================================================= */ /** * Contextual tools */ function _spaces_block_tools() { $block['content'] = theme('context_ui_node_links'); return $block; } /** * Default navigation menu - you can create your own customized version * using the spaces_features_menu() and theme_links() functions. */ function _spaces_block_nav() { $block = array(); if ($space = spaces_get_space()) { $links = spaces_features_menu(); $block['subject'] = $space->title; $block['content'] = theme('links', $links); } return $block; } /** * Utility links */ function _spaces_block_utility_links() { $block = array(); if ($space = spaces_get_space()) { } return $block; } /** * theme_block() preprocessor. */ function spaces_preprocess_block(&$vars) { $space = spaces_get_space(); if ($feature = context_get('spaces', 'feature')) { if (!empty($vars['block'])) { space_customizer_block::customize_subject($space, $feature, $vars['block']); } } }