? remove_from_database.patch
Index: API.txt
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/exportables/API.txt,v
retrieving revision 1.3
diff -u -p -r1.3 API.txt
--- API.txt	7 Oct 2009 18:25:15 -0000	1.3
+++ API.txt	27 Oct 2009 21:28:10 -0000
@@ -12,6 +12,7 @@ Returns a keyed array of information abo
 - save function: Function to save the object.
 - load all function: The function which returns an array of all valid items.
 - update function: Function to update in-db definition of the object. Defaults to save function if none specified.
+- delete function: Function to delete the object from the database.
 - id property: The property of the object defining it's ID.
 - title property: The property of the object defining it's title, from which the machine readable name is derived.
 - storage type: How the item is stored (array/object).
Index: exportables.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/exportables/exportables.module,v
retrieving revision 1.9
diff -u -p -r1.9 exportables.module
--- exportables.module	21 Oct 2009 18:10:10 -0000	1.9
+++ exportables.module	27 Oct 2009 21:28:10 -0000
@@ -7,6 +7,11 @@
  * @return void
  */
 function exportables_init() {
+  // Now that exportables_sync() isn't run on every page load unless the
+  // user has enabled it, we still need to run exportables_type_info() to
+  // ensure that the necessary files from modules/ are loaded by the time
+  // the features module starts looking for hooks.
+  exportables_type_info();
   if (variable_get('exportables_sync_on_init', 0)) {
     exportables_sync();
   }
@@ -39,6 +44,14 @@ function exportables_menu() {
     'access arguments' => array('administer site configuration'),
     'type' => MENU_LOCAL_TASK,
   );
+  $items['admin/build/exportables/unsync'] = array(
+    'title' => 'Un-synchronize',
+    'description' => 'Remove default items provided by the exportables module hooks.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('exportables_admin_unsync_form'),
+    'access arguments' => array('administer site configuration'),
+    'type' => MENU_LOCAL_TASK,
+  );
   return $items;
 }
 
@@ -89,33 +102,53 @@ function exportables_admin_sync_form($fo
     '#value' => t('Resync'),
   );
 
-  $form['#validate'][] = 'exportables_admin_sync_form_validate';
   $form['#submit'][] = 'exportables_admin_sync_form_submit';
 
   return $form;
 }
 
 /**
- * Validate function for exportables_admin_sync_form().
+ * Submit function for exportables_admin_sync_form().
  */
-function exportables_admin_sync_form_validate($form, $form_state) {
-  if (array_key_exists('values', $form_state) && array_key_exists('types', $form_state['values'])) {
-    $type_info = exportables_type_info();
-    foreach ($form_state['values']['types'] as $type => $value) {
-      if (!$value) {
-        continue;
-      }
-      if (!array_key_exists($type, $type_info)) {
-        form_set_error('types', t('The requested type !type is not valid.', array('!type' => $type)));
-      }
+function exportables_admin_sync_form_submit($form, $form_state) {
+  $types = array();
+  foreach ($form_state['values']['types'] as $type => $value) {
+    if (!$value) {
+      continue;
     }
+    $types[] = $type;
+  }
+  if (count($types) > 0) {
+    exportables_sync($types);
+  }
+  else {
+    // Nothing explicitly checked, so sync everything.
+    exportables_sync();
   }
 }
 
 /**
- * Submit function for exportables_admin_sync_form().
+ * Administration form for un-synchronizing exportables defaults.
+ *
+ * @param array $form_state
+ * @return array FormAPI array describing the form
  */
-function exportables_admin_sync_form_submit($form, $form_state) {
+function exportables_admin_unsync_form($form_state) {
+  $form = exportables_admin_sync_form($form_state);
+
+  $form['types']['#description'] = t('Select which type(s) of exportables defaults you would like to remove from the database.  If you do not check anything here, all types will be removed on submit.');
+  $form['submit']['#title'] = t('Un-sync');
+  $form['submit']['#value'] = t('Un-sync');
+
+  $form['#submit'] = array('exportables_admin_unsync_form_submit'); // override the sync form's submit callback
+
+  return $form;
+}
+
+/**
+ * Submit function for exportables_admin_unsync_form().
+ */
+function exportables_admin_unsync_form_submit($form, $form_state) {
   $types = array();
   foreach ($form_state['values']['types'] as $type => $value) {
     if (!$value) {
@@ -124,10 +157,10 @@ function exportables_admin_sync_form_sub
     $types[] = $type;
   }
   if (count($types) > 0) {
-    exportables_sync($types);
-  } else {
-    // Nothing explicitly checked, so sync everything.
-    exportables_sync();
+    exportables_unsync($types);
+  }
+  else {
+    exportables_unsync();
   }
 }
 
@@ -264,25 +297,21 @@ function exportables_var_export($var, $p
 /**
  * Sync in-code and database versions.
  * @param string|array $type array of types to sync, or single string type
+ * @param string|array $module array of modules to sync, or single string module name
  * @return void
  */
-function exportables_sync($type = '') {
-  if (!$type) {
-    $types = array_keys(exportables_type_info());
-  }
-  else if (is_string($type)) {
-    $types = array($type);
-  }
-  else {
-    $types = (array) $type;
-  }
+function exportables_sync($type = '', $module = '') {
+  $types = _exportables_get_type_array($type);
+
   foreach ($types as $type) {
     $info = exportables_type_info($type);
-    $items = module_invoke_all($info['hook name']);
+    $items = exportables_get_default_items($type, $module);
     foreach ($items as $item) {
       $item = (object) $item;
       if (!property_exists($item, 'machine') || empty($item->machine)) {
-        drupal_set_message(t('One of the default !type items is missing its required machine-readable ID.', array('!type' => $type)), 'warning');
+        drupal_set_message(t('One of the default !type items is missing its required machine-readable ID.', array(
+          '!type' => $type
+        )), 'warning');
         continue;
       }
       $machine = $item->machine;
@@ -290,33 +319,30 @@ function exportables_sync($type = '') {
       // database.
       $id = exportables_machine_load_id($type, $machine);
       if (!$id) {
-        // Save the entry.
-        settype($item, $info['storage type']);
-        $function = $info['save function'];
-        $result = $function($item);
-        if (!$result) {
-          drupal_set_message(t('The default !type item !machine could not be synchronized to the database.', array('!type' => $type, '!machine' => $machine)), 'warning');
-          continue;
-        }
-        // Associate the machine-readable name with the ID.
-        $result = exportables_machine_save($type, $item, $machine);
+        // Has not been saved yet; save it.
+        $result = exportables_insert_object($type, $item, $machine);
         if (!$result) {
-          drupal_set_message(t('The default !type item !machine was synchronized to the database, but its machine-readable name could not be saved.', array('!type' => $type, '!machine' => $machine)), 'warning');
+          drupal_set_message(t('An error occurred while trying to sync !type item !machine to the database.', array(
+            '!type' => $type, 
+            '!machine' => $machine
+          )), 'warning');
           continue;
         }
       }
       else {
-        $load = $info['load function']($id);
+        $load = exportables_load_object($type, $id);
         // If it's loaded, it exists.
         if ($load) {
           // See if the loaded version is the same as a previouly stored in-DB default.
-          $object = unserialize(db_result(db_query("SELECT object FROM {exportables_object_cache} WHERE type = '%s' AND machine = '%s'", $type, $item->machine)));
-          if (exportables_compare_object($object, $load) && !exportables_compare_object($object, $item)) {
+          $cached_default = exportables_load_object_from_cache($type, $machine);
+          if (exportables_compare_object($cached_default, $load) && !exportables_compare_object($cached_default, $item)) {
             // If so, replace it.
-            $function = $info['update function'];
-            $result = $function(exportables_prepare_object($type, $id, $item));
+            $result = exportables_update_object($type, $item, $machine);
             if (!$result) {
-              drupal_set_message(t('The !type item !machine is out of sync with the database, and could not be updated.', array('!type' => $type, '!machine' => $machine)), 'warning');
+              drupal_set_message(t('An error occurred while replacing modified !type item !machine.', array(
+                '!type' => $type,
+                '!machine' => $machine,
+              )), 'warning');
               continue;
             }
           }
@@ -326,41 +352,132 @@ function exportables_sync($type = '') {
         else {
           $result = exportables_machine_delete($type, $id, $machine);
           if (!$result) {
-            drupal_set_message(t('The machine-readable name for previously-deleted !type item !machine could not be deleted.', array('!type' => $type, '!machine' => $machine)), 'warning');
-            continue;
-          }
-          settype($item, $info['storage type']);
-          $function = $info['save function'];
-          $result = $function($item);
-          if (!$result) {
-            drupal_set_message(t('The previously-deleted !type item !machine could not be re-saved.', array('!type' => $type, '!machine' => $machine)), 'warning');
+            drupal_set_message(t('The machine-readable name for previously-deleted !type item !machine could not be deleted.', array(
+              '!type' => $type, 
+              '!machine' => $machine
+            )), 'warning');
             continue;
           }
-          // Generate a machine-readable name.
-          $result = exportables_machine_save($type, $item, $machine);
+          $result = exportables_insert_object($type, $item, $machine);
           if (!$result) {
-            drupal_set_message(t('The machine-readable name for previously-deleted !type item !machine could not be re-saved.', array('!type' => $type, '!machine' => $machine)), 'warning');
+            drupal_set_message(t('An error occurred while trying to re-sync previously-deleted !type item !machine.', array(
+              '!type' => $type,
+              '!machine' => $machine,
+            )), 'warning');
             continue;
           }
         }
       }
+      $result = exportables_cache_object($type, $item, $machine);
+      if (!$result) {
+        drupal_set_message(t('An error occurred while trying to cache the current state of !type item !machine.', array(
+          '!type' => $type,
+          '!machine' => $machine,
+        )), 'warning');
+        continue;
+      }
+    }
+  }
+}
+
+function exportables_unsync($type = '', $module = '') {
+  $types = _exportables_get_type_array($type);
+
+  foreach ($types as $type) {
+    $info = exportables_type_info($type);
+    $items = exportables_get_default_items($type, $module);
+    foreach ($items as $item) {
       $item = (object) $item;
-      // Set the in-DB default to be the in-code default.
       $machine = $item->machine;
-      settype($item, $info['storage type']);
-      $array = array("type" => $type, "machine" => $machine, 'object' => serialize($item));
-      if (db_result(db_query("SELECT * FROM {exportables_object_cache} WHERE type = '%s' AND machine = '%s'", $type, $machine))) {
-        $result = drupal_write_record('exportables_object_cache', $array, array('type', 'machine'));
-      }
-      else {
-        $result = drupal_write_record('exportables_object_cache', $array);
+
+      if (exportables_object_is_overridden($type, $item, $machine)) {
+        // The item has been overridden; do not delete it.
+        continue;
       }
+
+      $result = exportables_delete_object($type, $item, $machine);
       if (!$result) {
-        drupal_set_message(t('The !type item !machine could not be written to the exportables object cache.', array('!type' => $type, '!machine' => $machine)), 'warning');
+        drupal_set_message(t('Default !type item !machine could not be deleted.', array(
+          '!type' => $type,
+          '!machine' => $machine,
+        )), 'warning');
+        continue;
+      }
+    }
+  }
+}
+
+/**
+ * Gets default items of a particular type, optionally filtered by module.
+ * @param string $type
+ * @param string|array $module
+ * @return array
+ */
+function exportables_get_default_items($type, $module = '') {
+  $modules = _exportables_get_module_array($module);
+  $info = exportables_type_info($type);
+
+  if (!$modules) {
+    $items = module_invoke_all($info['hook name']);
+  }
+  else {
+    $items = array();
+    foreach ($modules as $module) {
+      $module_items = module_invoke($module, $info['hook name']);
+      if (!is_array($module_items)) {
+        // If it didn't return an array, there's not much we can do with it.
         continue;
       }
+      $items = array_merge($items, $module_items);
     }
   }
+
+  return $items;
+}
+
+/**
+ * Utility function to convert type argument into normalized array.
+ * @param string|array $type
+ *   The $type argument can be an array of type names, a single string type,
+ *   or anything that evaluates FALSE. If it evaluates to FALSE, the return
+ *   value will be all possible types.
+ * @return array
+ *   Returns an array of type names.
+ */
+function _exportables_get_type_array($type = '') {
+  if (!$type) {
+    $types = array_keys(exportables_type_info());
+  }
+  else if (is_string($type)) {
+    $types = array($type);
+  }
+  else {
+    $types = (array) $type;
+  }
+  return $types;
+}
+
+/**
+ * Utility function to convert module argument into normalized array.
+ * @param string|array $module
+ *   The $module argument can be an array of module names, a single string
+ *   module name, or anything that evaluates FALSE. If it evaluates to FALSE,
+ *   the return value will also be FALSE, indicating that no particular
+ *   module was specified.
+ * @return array
+ *   Returns an array of module names.
+ */
+function _exportables_get_module_array($module = '') {
+  if (!$module) {
+    return FALSE;
+  }
+  else if (is_string($module)) {
+    $modules = array($module);
+  }
+  else {
+    $modules = (array) $module;
+  }
+  return $modules;
 }
 
 /**
@@ -400,6 +517,199 @@ function exportables_prepare_object($typ
   return $item;
 }
 
+/**
+ * Load an object from the database.
+ * @param string $type
+ * @param integer $id
+ * @return mixed
+ */
+function exportables_load_object($type, $id) {
+  $info = exportables_type_info($type);
+  return $info['load function']($id);
+}
+
+/**
+ * Insert a new object into the database.
+ * @param string $type
+ * @param mixed $object
+ * @return boolean
+ *   Returns true if succeeded, false if failed.
+ */
+function exportables_insert_object($type, $object, $machine) {
+  $info = exportables_type_info($type);
+  settype($object, $info['storage type']);
+  $function = $info['save function'];
+  $result = $function($object);
+  if (!$result) {
+    drupal_set_message(t('The default !type item !machine could not be synchronized to the database.', array(
+      '!type' => $type, 
+      '!machine' => $machine
+    )), 'warning');
+    return FALSE;
+  }
+  // Associate the machine-readable name with the ID.
+  $result = exportables_machine_save($type, $object, $machine);
+  if (!$result) {
+    drupal_set_message(t('The default !type item !machine was synchronized to the database, but its machine-readable name could not be saved.', array(
+      '!type' => $type, 
+      '!machine' => $machine
+    )), 'warning');
+    return FALSE;
+  }
+  return TRUE;
+}
+
+/**
+ * Update an existing object in the database.
+ * @param string $type
+ * @param mixed $object
+ * @param string $machine
+ * @return array
+ *   Returns true if succeeded, false if failed.
+ */
+function exportables_update_object($type, $object, $machine) {
+  $info = exportables_type_info($type);
+  $function = $info['update function'];
+  $result = $function(exportables_prepare_object($type, $id, $object));
+  if (!$result) {
+    drupal_set_message(t('The default !type item !machine could not be updated in the database.', array(
+      '!type' => $type, 
+      '!machine' => $machine
+    )), 'warning');
+    return FALSE;
+  }
+  return TRUE;
+}
+
+/**
+ * Delete an object from the database.
+ * @param string $type
+ * @param mixed $object
+ * @param string $machine
+ * @return array
+ *   Returns true if succeeded, false if failed.
+ */
+function exportables_delete_object($type, $object, $machine) {
+  $object = (object) $object;
+  $info = exportables_type_info($type);
+
+  $id = exportables_machine_load_id($type, $machine);
+  if (!$id) {
+    // Doesn't exist anyway.
+    return TRUE;
+  }
+
+  // Delete the item itself.
+  $function = $info['delete function'];
+  $result = $function($id);
+  if (!$result) {
+    drupal_set_message(t('The default !type item !machine could not be deleted from the database.', array(
+      '!type' => $type,
+      '!machine' => $machine,
+    )), 'warning');
+    return FALSE;
+  }
+
+  // Delete the item's machine-readable name.
+  $result = exportables_machine_delete($type, $id, $machine);
+  if (!$result) {
+    drupal_set_message(t('The default !type item !machine was successfully deleted, but its machine-readable name could not be deleted.', array(
+      '!type' => $type,
+      '!machine' => $machine,
+    )), 'warning');
+    return FALSE;
+  }
+
+  // Delete any cached versions of the object.
+  $result = exportables_delete_object_from_cache($type, $machine);
+  if (!$result) {
+    drupal_set_message(t('The default !type item !machine was successfully deleted, but its cached version could not be deleted.', array(
+      '!type' => $type,
+      '!machine' => $machine,
+    )), 'warning');
+    return FALSE;
+  }
+
+  return TRUE;
+}
+
+/**
+ * Check to see if an object is in an overridden state.
+ * @param string $type
+ * @param mixed $object
+ * @param string $machine
+ * @return boolean
+ *   Returns true if overridden (database version is different from
+ *   default), false if not.
+ */
+function exportables_object_is_overridden($type, $object, $machine) {
+  $load = exportables_load_object($type, exportables_machine_load_id($type, $machine));
+  if (exportables_compare_object($object, $load)) {
+    // In-database object is identical to provided default; it is not overridden.
+    return FALSE;
+  } else {
+    // In-database object differs from provided default; it is overridden.
+    return TRUE;
+  }
+}
+
+/**
+ * Store an object in the exportables cache.
+ * @param string $type
+ * @param mixed $object
+ * @param string $machine
+ * @return array
+ *   Returns true if succeeded, false if failed.
+ */
+function exportables_cache_object($type, $object, $machine) {
+  $info = exportables_type_info($type);
+  settype($object, $info['storage type']);
+  $array = array(
+    'type' => $type, 
+    'machine' => $machine, 
+    'object' => serialize($object)
+  );
+  if (db_result(db_query("SELECT * FROM {exportables_object_cache} WHERE type = '%s' AND machine = '%s'", $type, $machine))) {
+    $result = drupal_write_record('exportables_object_cache', $array, array('type', 'machine'));
+  }
+  else {
+    $result = drupal_write_record('exportables_object_cache', $array);
+  }
+  if (!$result) {
+    drupal_set_message(t('The !type item !machine could not be written to the exportables object cache.', array(
+      '!type' => $type, 
+      '!machine' => $machine
+    )), 'warning');
+    return FALSE;
+  }
+  return TRUE;
+}
+
+/**
+ * Loads an object from cache.
+ * @param string $type
+ * @param string $machine
+ * @return mixed
+ */
+function exportables_load_object_from_cache($type, $machine) {
+  $object = unserialize(db_result(db_query("SELECT object FROM {exportables_object_cache} WHERE type = '%s' AND machine = '%s'", $type, $machine)));
+  return $object;
+}
+
+/**
+ * Deletes an object from cache.
+ * @param string $type
+ * @param string $machine
+ * @return boolean
+ */
+function exportables_delete_object_from_cache($type, $machine) {
+  $result = db_query("DELETE FROM {exportables_object_cache} WHERE type = '%s' AND machine = '%s'", $type, $machine);
+  if (FALSE === $result) {
+    return FALSE;
+  }
+  return TRUE;
+}
+
 
 
 // ---------- MACHINE-READABLE NAMES ----------
@@ -550,6 +860,10 @@ function _exportables_features_export($t
     }
   }
 
+  if (!in_array('exportables', $export['dependencies'])) {
+    $export['dependencies'][] = 'exportables';
+  }
+
   return $pipe;
 }
 
@@ -612,5 +926,6 @@ function _exportables_features_export_re
   $output = implode(PHP_EOL, $output);
 
   $type_info = exportables_type_info($type);
+
   return array($type_info['hook name'] => $output);
 }
Index: modules/taxonomy.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/exportables/modules/taxonomy.inc,v
retrieving revision 1.4
diff -u -p -r1.4 taxonomy.inc
--- modules/taxonomy.inc	7 Oct 2009 18:25:15 -0000	1.4
+++ modules/taxonomy.inc	27 Oct 2009 21:28:10 -0000
@@ -12,6 +12,7 @@ function taxonomy_exportables() {
     'hook name'         => 'taxonomy_default_vocabularies',
     'load function'     => 'taxonomy_vocabulary_load',
     'save function'     => 'taxonomy_save_vocabulary',
+    'delete function'   => 'taxonomy_del_vocabulary',
     'load all function' => 'taxonomy_get_vocabularies',
     'id property'       => 'vid',
     'title property'    => 'name',
@@ -63,6 +64,10 @@ function taxonomy_vocabulary_features_ex
   // first do the standard exportables export
   $pipe = _exportables_features_export('taxonomy_vocabulary', $data, $export, $module_name);
 
+  if (!in_array('taxonomy', $export['dependencies'])) {
+    $export['dependencies'][] = 'taxonomy';
+  }
+
   // then loop through the data again so we can add taxonomy-specific
   // dependency components to the pipe
   foreach ($data as $type) {
