diff --git a/djblets/extensions/extension.py b/djblets/extensions/extension.py
index de47e203798757fb8a7eb525240242cee8a24ca7..775ca746122f9ce6675a13884187568f5c106ee6 100644
--- a/djblets/extensions/extension.py
+++ b/djblets/extensions/extension.py
@@ -122,6 +122,19 @@ class Extension(object):
     to a list of class names. This extension's middleware will be loaded after
     any middleware belonging to any extensions in the :py:attr:`requirements`
     list.
+
+
+    Template Context Processors
+    ---------------------------
+
+    Extensions may need to provide additional context variables to templates.
+    This can usually be accomplished through a TemplateHook, but sometimes
+    it's necessary to provide context variables for other pages (such as
+    those controlled by a third-party module).
+
+    To add additional context processors, set :py:attr:`context_processors`
+    to a list of class names. They will be added to
+    ``settings.TEMPLATE_CONTEXT_PROCESSORS`` automatically.
     """
     metadata = None
     is_configurable = False
@@ -130,6 +143,7 @@ class Extension(object):
     requirements = []
     resources = []
     apps = []
+    context_processors = []
     middleware = []
 
     css_bundles = {}
diff --git a/djblets/extensions/manager.py b/djblets/extensions/manager.py
index 85e78c3cc5698fd08ee4c45d42500cb3f69f1c01..ce450dc0efdf6ad8c8b17966938c763fa6f44ecd 100644
--- a/djblets/extensions/manager.py
+++ b/djblets/extensions/manager.py
@@ -59,6 +59,84 @@ from djblets.extensions.signals import (extension_initialized,
 from djblets.urls.resolvers import DynamicURLResolver
 
 
+class SettingListWrapper(object):
+    """Wraps list-based settings to provide management and ref counting.
+
+    This can be used instead of direct access to a list in Django
+    settings to ensure items are never added more than once, and only
+    removed when nothing needs it anymore.
+
+    Each item in the list is ref-counted. The initial items from the
+    setting are populated and start with a ref count of 1. Adding items
+    will increment a ref count for the item, adding it to the list
+    if it doesn't already exist. Removing items reduces the ref count,
+    removing when it hits 0.
+    """
+    def __init__(self, setting_name, display_name):
+        self.setting_name = setting_name
+        self.display_name = display_name
+        self.ref_counts = {}
+
+        self.setting = getattr(settings, setting_name)
+
+        if isinstance(self.setting, tuple):
+            self.setting = list(self.setting)
+            setattr(settings, setting_name, self.setting)
+
+        for item in self.setting:
+            self.ref_counts[item] = 1
+
+    def add(self, item):
+        """Adds an item to the setting.
+
+        If the item is already in the list, it won't be added again.
+        The ref count will just be incremented.
+
+        If it's a new item, it will be added to the list with a ref count
+        of 1.
+        """
+        if item in self.ref_counts:
+            self.ref_counts[item] += 1
+        else:
+            assert item not in self.setting, \
+                   ("%s extension's %s %s is already in settings.%s, with "
+                    "ref count of 0."
+                    % (extension.id, self.display_name, item,
+                       self.setting_name))
+
+            self.ref_counts[item] = 1
+            self.setting.append(item)
+
+    def add_list(self, items):
+        """Adds a list of items to the setting."""
+        for item in items:
+            self.add(item)
+
+    def remove(self, item):
+        """Removes an item from the setting.
+
+        The item's ref count will be decremented. If it hits 0, it will
+        be removed from the list.
+        """
+        assert item in self.ref_counts, \
+               ("%s extension's %s %s is missing a ref count."
+                % (extension.id, self.display_name, item))
+        assert item in self.setting, \
+               ("%s extension's %s %s is not in settings.%s"
+                % (extension.id, self.display_name, item, self.setting_name))
+
+        if self.ref_counts[item] == 1:
+            del self.ref_counts[item]
+            self.setting.remove(item)
+        else:
+            self.ref_counts[item] -= 1
+
+    def remove_list(self, items):
+        """Removes a list of items from the setting."""
+        for item in items:
+            self.remove(item)
+
+
 class ExtensionManager(object):
     """A manager for all extensions.
 
@@ -96,6 +174,15 @@ class ExtensionManager(object):
         # Extension middleware instances, ordered by dependencies.
         self.middleware = []
 
+        # Wrap the INSTALLED_APPS and TEMPLATE_CONTEXT_PROCESSORS settings
+        # to allow for ref-counted add/remove operations.
+        self._installed_apps_setting = SettingListWrapper(
+            'INSTALLED_APPS',
+            'installed app')
+        self._context_processors_setting = SettingListWrapper(
+            'TEMPLATE_CONTEXT_PROCESSORS',
+            'context processor')
+
         _extension_managers.append(self)
 
     def get_url_patterns(self):
@@ -409,6 +496,7 @@ class ExtensionManager(object):
         extension.info.installed = extension.registration.installed
         extension.info.enabled = True
         self._add_to_installed_apps(extension)
+        self._context_processors_setting.add_list(extension.context_processors)
         self._reset_templatetags_cache()
         extension_initialized.send(self, ext_class=extension)
 
@@ -434,6 +522,8 @@ class ExtensionManager(object):
         if extension.has_admin_site:
             del extension.admin_site
 
+        self._context_processors_setting.remove_list(
+            extension.context_processors)
         self._remove_from_installed_apps(extension)
         self._reset_templatetags_cache()
         extension.info.enabled = False
@@ -671,14 +761,12 @@ class ExtensionManager(object):
                     % extension.info.app_name)
 
     def _add_to_installed_apps(self, extension):
-        for app in extension.apps or [extension.info.app_name]:
-            if app not in settings.INSTALLED_APPS:
-                settings.INSTALLED_APPS.append(app)
+        self._installed_apps_setting.add_list(
+            extension.apps or [extension.info.app_name])
 
     def _remove_from_installed_apps(self, extension):
-        for app in extension.apps or [extension.info.app_name]:
-            if app in settings.INSTALLED_APPS:
-                settings.INSTALLED_APPS.remove(app)
+        self._installed_apps_setting.remove_list(
+            extension.apps or [extension.info.app_name])
 
     def _entrypoint_iterator(self):
         return pkg_resources.iter_entry_points(self.key)
diff --git a/djblets/extensions/tests.py b/djblets/extensions/tests.py
index 507a042ffb849fceadb66900d46c74996f2bd7ce..29d0a4d07271c7a9d22652cf02d7e44bb6f3a434 100644
--- a/djblets/extensions/tests.py
+++ b/djblets/extensions/tests.py
@@ -35,7 +35,8 @@ from mock import Mock
 from djblets.extensions.extension import Extension, ExtensionInfo
 from djblets.extensions.hooks import (ExtensionHook, ExtensionHookPoint,
                                       TemplateHook, URLHook)
-from djblets.extensions.manager import _extension_managers, ExtensionManager
+from djblets.extensions.manager import (_extension_managers, ExtensionManager,
+                                        SettingListWrapper)
 from djblets.extensions.settings import Settings
 from djblets.testing.testcases import TestCase
 from djblets.util.compat import six
@@ -544,6 +545,58 @@ class ExtensionManagerTest(TestCase):
         self.assertEqual(extension2.settings[setting_key], setting_val)
 
 
+class SettingListWrapperTests(TestCase):
+    """Unit tests for djblets.extensions.manager.SettingListWrapper."""
+    def test_loading_from_setting(self):
+        """Testing SettingListWrapper constructor loading from settings"""
+        settings.TEST_SETTING_LIST = ['item1', 'item2']
+        wrapper = SettingListWrapper('TEST_SETTING_LIST', 'test setting list')
+
+        self.assertEqual(wrapper.ref_counts.get('item1'), 1)
+        self.assertEqual(wrapper.ref_counts.get('item2'), 1)
+
+    def test_add_with_new_item(self):
+        """Testing SettingListWrapper.add with new item"""
+        settings.TEST_SETTING_LIST = []
+        wrapper = SettingListWrapper('TEST_SETTING_LIST', 'test setting list')
+        wrapper.add('item1')
+
+        self.assertEqual(settings.TEST_SETTING_LIST, ['item1'])
+        self.assertEqual(wrapper.ref_counts.get('item1'), 1)
+
+    def test_add_with_existing_item(self):
+        """Testing SettingListWrapper.add with existing item"""
+        settings.TEST_SETTING_LIST = ['item1']
+        wrapper = SettingListWrapper('TEST_SETTING_LIST', 'test setting list')
+        wrapper.add('item1')
+
+        self.assertEqual(settings.TEST_SETTING_LIST, ['item1'])
+        self.assertEqual(wrapper.ref_counts.get('item1'), 2)
+
+    def test_remove_with_ref_count_1(self):
+        """Testing SettingListWrapper.remove with ref_count == 1"""
+        settings.TEST_SETTING_LIST = ['item1']
+        wrapper = SettingListWrapper('TEST_SETTING_LIST', 'test setting list')
+
+        self.assertEqual(wrapper.ref_counts.get('item1'), 1)
+        wrapper.remove('item1')
+
+        self.assertEqual(settings.TEST_SETTING_LIST, [])
+        self.assertFalse('item1' in wrapper.ref_counts)
+
+    def test_remove_with_ref_count_gt_1(self):
+        """Testing SettingListWrapper.remove with ref_count > 1"""
+        settings.TEST_SETTING_LIST = ['item1']
+        wrapper = SettingListWrapper('TEST_SETTING_LIST', 'test setting list')
+        wrapper.add('item1')
+
+        self.assertEqual(wrapper.ref_counts.get('item1'), 2)
+        wrapper.remove('item1')
+
+        self.assertEqual(settings.TEST_SETTING_LIST, ['item1'])
+        self.assertEqual(wrapper.ref_counts.get('item1'), 1)
+
+
 class URLHookTest(TestCase):
     def setUp(self):
         manager = ExtensionManager('')
