diff --git a/reviewboard/__init__.py b/reviewboard/__init__.py
index 92f93bf5bb59c1154fd0115f68803160a34416df..8a577536bb2e56a00cb3949adea2013184c11fd9 100644
--- a/reviewboard/__init__.py
+++ b/reviewboard/__init__.py
@@ -59,8 +59,12 @@ def initialize():
     import os
     import sys
 
+    import settings_local
+
     # Set PYTHONPATH to match sys.patch, for subprocesses.
-    os.environ['PYTHONPATH'] = ':'.join(sys.path)
+    os.environ['PYTHONPATH'] = '%s:%s' % \
+        (os.path.dirname(settings_local.__file__),
+            os.environ.get('PYTHONPATH', ''))
 
     from django.conf import settings
     from django.db import DatabaseError
diff --git a/reviewboard/extensionbrowser/base.py b/reviewboard/extensionbrowser/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..b6dbdb887d3586f4c54fb4752bec1f66b8222fc9
--- /dev/null
+++ b/reviewboard/extensionbrowser/base.py
@@ -0,0 +1,107 @@
+from urllib import urlencode
+from urllib2 import Request, urlopen, HTTPError
+
+import django.utils.simplejson as json
+
+from django.conf import settings
+
+
+class StoreExtensionInfo(object):
+    """A class for depicting an extension from the store."""
+    def __init__(self, id, name, description, version, author, package, installed):
+        self.id = id
+        self.name = name
+        self.description = description
+        self.version = version
+        self.author = author
+        self.package_name = package
+        self.installed = installed
+
+
+class ExtensionStoreQuery(object):
+    """Allows querying the extension store for a possible list of extensions.
+
+    A supported extension store can be queried (with or without parameters)
+    for a list of extensions available.
+    """
+    def __init__(self, extension_manager):
+        self.store_url = settings.EXTENSION_STORE_ENDPOINT
+        self.installed_extensions = None
+
+        # Populate package names of all extensions presently installed.
+        self.installed_extensions = [
+            ext.dist.project_name.lower()
+            for ext in extension_manager._entrypoint_iterator()
+        ]
+
+    def _query(self, endpoint, params=None):
+        """Query the store.
+
+        Perform the actual HTTP request to the store with parameters (if any).
+
+        Return object is a tuple, with the first index representing boolean
+        status of success or error and the second index representing the
+        response.
+
+        A JSON object representing the response is returned on success. On
+        errors, an appropriate error message is returned.
+        """
+        request_url = '%s/%s' % (self.store_url, endpoint)
+
+        if params:
+            url_params = urlencode(params)
+            request_url = '%s?%s' % (request_url, url_params)
+
+        try:
+            request = Request(request_url)
+            response = urlopen(request)
+            return True, json.loads(response.read())
+
+        except HTTPError:
+            return False, "Could not obtain resource"
+
+        except ValueError:
+            return False, "Could not parse response"
+
+    def populate_extensions(self, params):
+        """Query and populate the result of extensions.
+
+        Query for a list of extensions with the given params against the
+        extension store and return a list of StoreExtensionInfo class objects.
+        """
+        response = self._query("list", params)
+        if not response[0]:
+            return False, response[1]
+
+        extlist = []
+        extensions = response[1]['extensions']
+
+        for ext in extensions:
+            installed = ext['package_name'] in self.installed_extensions
+            extlist.append(StoreExtensionInfo(id=ext['id'],
+                                            name=ext['name'],
+                                            description=ext['description'],
+                                            version=ext['version'],
+                                            author=ext['author'],
+                                            package=ext['package_name'],
+                                            installed=installed))
+
+        return extlist, response[1].get('paging')
+
+    def get_extension_info(self, package_name):
+        """Fetch more information about an extension.
+
+        Information obtained includes parameters such as the
+        extension's description, category, author, compatibility,
+        depdendencies and screenshots.
+        """
+        response = self._query("details", {"ext": package_name})
+        if not response[0]:
+            return {"error": response[0]}
+        elif "error" in response[1]:
+            # Error from store response.
+            return {"error": response[1]["error"]}
+        else:
+            response[1]['extension_info']['installed'] = package_name in \
+                                                    self.installed_extensions
+            return response[1]['extension_info']
diff --git a/reviewboard/extensionbrowser/forms.py b/reviewboard/extensionbrowser/forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..b82541cdbbe096711da26018f70f4cc7075b8473
--- /dev/null
+++ b/reviewboard/extensionbrowser/forms.py
@@ -0,0 +1,7 @@
+from django import forms
+from django.utils.translation import ugettext as _
+
+
+class SearchForm(forms.Form):
+    """Search form for the extension browser."""
+    query = forms.CharField(label=_("Search"))
diff --git a/reviewboard/extensionbrowser/templates/extensionbrowser/extension_browser.html b/reviewboard/extensionbrowser/templates/extensionbrowser/extension_browser.html
new file mode 100644
index 0000000000000000000000000000000000000000..97fd903e0178ebf7d53a15781353283bc468ee72
--- /dev/null
+++ b/reviewboard/extensionbrowser/templates/extensionbrowser/extension_browser.html
@@ -0,0 +1,85 @@
+{% extends "admin/base_site.html" %}
+{% load adminmedia admin_list compressed djblets_extensions i18n staticfiles %}
+
+{% block title %}{% trans "Browse Extensions" %} {{block.super}}{% endblock %}
+
+{% block extrahead %}
+<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}"/>
+{% compressed_css "extensionbrowser" %}
+
+{{block.super}}
+{% endblock %}
+
+{% block bodyclass %}{{block.super}} change-form{% endblock %}
+
+{% block content %}
+<h1 class="title">{% trans "Browse Extensions" %}</h1>
+<div id="content-main">
+ <form action="{% url extensionbrowser %}" method="post">
+  <fieldset class="module aligned wide">
+    <div class="form-row search-query{% if form.query.errors %} error{% endif %}" id="row-query">
+     {{form.query.errors}}
+     <label for="id_query" class="required">{{form.query.label}}</label>
+     {{form.query}}
+     <p class="help">
+      {% trans "Enter some search keywords." %}
+     </p>
+   </div>
+
+   <div class="submit-row">
+    <input type="submit" value="{% trans "Search" %}" class="default" />
+   </div>
+  </fieldset>
+ </form>
+{% if error %}
+<div class="results-error">
+  {% trans "Error:" %} {% trans error %}
+</div>
+{% endif %}
+{% if results %}
+<div id="results_container">
+ <h2>{% trans "Search Results" %}</h2>
+ <ul id="results">
+  <li id="results_header" class="header clear">
+   <div class="item-name item-start">{% trans "Extension Name" %}</div>
+   <div class="item-version">{% trans "Version" %}</div>
+   <div class="item-author">{% trans "Author" %}</div>
+   <div class="item-description">{% trans "Description" %}</div>
+  </li>
+{%  for ext in results %}
+  <li class="clear">
+   <div class="item-name item-start">
+    {{ext.name}}
+     <span class="item-actions">
+      <a href="#" class="details" title="{{ext.package_name}}">{% trans "Details" %}</a>&nbsp;|&nbsp;
+{%      if not ext.installed %}
+      <a href="#" class="install" title="{{ext.package_name}}" id="action_install_{{id}}">{% trans "Install" %}</a>
+{%      else %}
+      <span class="item-installed">{% trans "Installed" %}</span>
+{%      endif %}
+     </span>
+    </div>
+     <div class="item-version">{{ext.version}}</div>
+     <div class="item-author">{{ext.author}}</div>
+     <div class="item-description">{{ ext.description|truncatechars:258 }}</div>
+  </li>
+{%  endfor %}
+ </ul>
+ </div>
+{% if paging %}
+ <div class="extensions-paging">
+{%  if paging.previous %}
+  <a href="./?q={{search_keyword}}&o={{paging.previous}}">Previous</a>
+{%  endif %}
+  &nbsp;|&nbsp;
+{%  if paging.next %}
+  <a href="./?q={{search_keyword}}&o={{paging.next}}">Next
+{%  endif %}
+ </div>
+{% endif %}
+{% endif %}
+</div>
+{% endblock %}
+{% block scripts-post %}
+{% compressed_js "extensionbrowser" %}
+{% endblock %}
\ No newline at end of file
diff --git a/reviewboard/extensionbrowser/urls.py b/reviewboard/extensionbrowser/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..5d1eab8fce10f401b37e0b325a48e8493cb6c363
--- /dev/null
+++ b/reviewboard/extensionbrowser/urls.py
@@ -0,0 +1,6 @@
+from django.conf.urls.defaults import patterns, url
+
+
+urlpatterns = patterns('reviewboard.extensionbrowser.views',
+    url(r'^$', 'browse_extensions', name="extensionbrowser"),
+)
diff --git a/reviewboard/extensionbrowser/views.py b/reviewboard/extensionbrowser/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..cfb862fe6265386ea9b2d0f5af15f50915fc39b3
--- /dev/null
+++ b/reviewboard/extensionbrowser/views.py
@@ -0,0 +1,56 @@
+from django.contrib.admin.views.decorators import staff_member_required
+from django.shortcuts import render_to_response
+from django.template.context import RequestContext
+from reviewboard.extensionbrowser.base import ExtensionStoreQuery
+from reviewboard.extensionbrowser.forms import SearchForm
+from reviewboard.extensions.base import get_extension_manager
+
+
+@staff_member_required
+def browse_extensions(request,
+                      template_name='extensionbrowser/extension_browser.html'):
+    """Main view for the extension browser module."""
+    extlist = None
+    paging = None
+    search_keyword = None
+    error = None
+
+    if request.method == 'POST':
+        form = SearchForm(request.POST)
+
+        if form.is_valid():
+            store = ExtensionStoreQuery(get_extension_manager())
+            results = store.populate_extensions(request.POST)
+            if results[0]:
+                extlist = results[0]
+                paging = results[1]
+            else:
+                error = results[1]
+
+            search_keyword = request.POST['query']
+
+    else:
+        form = SearchForm()
+
+        if "q" in request.GET and "o" in request.GET:
+            # Paged query is being requested.
+            params = {"query": request.GET["q"],
+                      "offset": request.GET["o"]}
+
+            store = ExtensionStoreQuery(get_extension_manager())
+            results = store.populate_extensions(params)
+            if results[0]:
+                extlist = results[0]
+                paging = results[1]
+            else:
+                error = results[1]
+
+            search_keyword = params["query"]
+
+    return render_to_response(template_name, RequestContext(request, {
+        'error': error,
+        'form': form,
+        'paging': paging,
+        'results': extlist,
+        'search_keyword': search_keyword
+    }))
diff --git a/reviewboard/settings.py b/reviewboard/settings.py
index 8a473297447f5c7f08a7c339564384bbb9531b6e..acd2b2bfa8c375a84b31115c7b8538e9afa61c12 100644
--- a/reviewboard/settings.py
+++ b/reviewboard/settings.py
@@ -105,6 +105,8 @@ ROOT_URLCONF = 'djblets.util.rooturl'
 
 REVIEWBOARD_ROOT = os.path.abspath(os.path.split(__file__)[0])
 
+EXTENSION_STORE_ENDPOINT = None
+
 # where is the site on your server ? - add the trailing slash.
 SITE_ROOT = '/'
 
@@ -150,6 +152,7 @@ RB_BUILTIN_APPS = [
     'reviewboard.changedescs',
     'reviewboard.diffviewer',
     'reviewboard.extensions',
+    'reviewboard.extensionbrowser',
     'reviewboard.hostingsvcs',
     'reviewboard.notifications',
     'reviewboard.reviews',
@@ -327,6 +330,14 @@ PIPELINE_JS = {
         ),
         'output_filename': 'rb/js/base.min.js',
     },
+    'extensionbrowser': {
+        'source_filenames': (
+            'rb/js/models/extensionInformationModel.js',
+            'rb/js/views/extensionBrowserView.js',
+            'rb/js/extensionbrowser.js',
+        ),
+        'output_filename': 'rb/js/extensionbrowser.min.js',
+    },
     'reviews': {
         'source_filenames': (
             # Note: These are roughly in dependency order.
@@ -383,6 +394,14 @@ PIPELINE_CSS = {
         'output_filename': 'rb/css/common.min.css',
         'absolute_paths': False,
     },
+    'extensionbrowser': {
+        'source_filenames': (
+            'djblets/css/admin.css',
+            'rb/css/extensionbrowser.less',
+            'rb/css/jquery-ui-1.7.1.custom.css',
+        ),
+        'output_filename': 'rb/css/extensionbrowser.min.css'
+    },
     'js-tests': {
         'source_filenames': (
             'rb/css/js-tests.less',
diff --git a/reviewboard/static/rb/css/common.less b/reviewboard/static/rb/css/common.less
index 18b57043591ae238b5d6e86dd2aa37bf1d94d5b2..3078a648df04d1c736dc5a66f1cadacf61efafcc 100644
--- a/reviewboard/static/rb/css/common.less
+++ b/reviewboard/static/rb/css/common.less
@@ -866,33 +866,4 @@ body.admin #navbar {
    width: 100%;
 }
 
-
-/****************************************************************************
- * clearfix hacks
- ****************************************************************************/
-
-/*
- * clearfix hack. See http://www.webtoolkit.info/css-clearfix.html
- */
-.clearfix {
-  display: inline-block;
-
-  &:after {
-    content: ".";
-    display: block;
-    clear: both;
-    visibility: hidden;
-    line-height: 0;
-    height: 0;
-  }
-}
-
-html[xmlns] .clearfix {
-  display: block;
-}
-
-* html .clearfix {
-  height: 1%;
-}
-
 // vim: set et ts=2 sw=2:
diff --git a/reviewboard/static/rb/css/defs.less b/reviewboard/static/rb/css/defs.less
index 830a40c3197223c6a0abb3355ea48891a1857bfd..9f389dfc3f40c0fa054b41dd794437c1fd9330a4 100644
--- a/reviewboard/static/rb/css/defs.less
+++ b/reviewboard/static/rb/css/defs.less
@@ -181,5 +181,32 @@
   }
 }
 
+/****************************************************************************
+ * clearfix hacks
+ ****************************************************************************/
+
+/*
+ * clearfix hack. See http://www.webtoolkit.info/css-clearfix.html
+ */
+.clearfix {
+  display: inline-block;
+
+  &:after {
+    content: ".";
+    display: block;
+    clear: both;
+    visibility: hidden;
+    line-height: 0;
+    height: 0;
+  }
+}
+
+html[xmlns] .clearfix {
+  display: block;
+}
+
+* html .clearfix {
+  height: 1%;
+}
 
 // vim: set et ts=2 sw=2:
diff --git a/reviewboard/static/rb/css/extensionbrowser.less b/reviewboard/static/rb/css/extensionbrowser.less
new file mode 100644
index 0000000000000000000000000000000000000000..9cc6e6d805a3e2974f9a85095dbb1eb51e671bed
--- /dev/null
+++ b/reviewboard/static/rb/css/extensionbrowser.less
@@ -0,0 +1,141 @@
+@import "defs.less";
+
+
+@extension-browser-border: #888A85;
+@grayed: #999999;
+
+#results_container {
+  height: 400px;
+  margin: 0 auto;
+  width: 85%;
+}
+
+#results {
+  height: 100%;
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+
+  li {
+    height: 8%;
+  }
+
+  li.header div {
+    background-color: #C6DCF3;
+    font-weight: bold;
+    height: 80%;
+  }
+
+  li div {
+    border-bottom: 1px solid @extension-browser-border;
+    border-right: 1px solid @extension-browser-border;
+    display: block;
+    float: left;
+    height: 85%;
+    padding: 3px 5px;
+  }
+}
+
+.item-start {
+  border-left: 1px solid @extension-browser-border;
+}
+
+.clear {
+  clear: both;
+}
+
+.item-name, .item-version, .item-author {
+  width: 15%;
+}
+
+.item-description {
+  width: 50%;
+}
+
+.item-actions {
+  display: block;
+  margin-top: 5px;
+}
+
+.item-installed {
+  color: @grayed;
+}
+
+.extension-info-wrapper {
+  margin: 0;
+  width: 600px;
+
+  label {
+    font-weight: bold;
+  }
+
+  li {
+    &:link, &:visited {
+      color: @grayed;
+    }
+
+    &:hover{
+      text-decoration: underline;
+    }
+  }
+}
+
+.extension-info-main {
+  float: left;
+}
+
+.extension-info-sidebar {
+  border-left: 1px solid @extension-browser-border;
+  float: left;
+  margin-left: 10px;
+  padding-left: 10px;
+  width: 150px;
+
+  div {
+    margin-bottom: 15px;
+  }
+}
+
+.extension-modalbox {
+  height: 350px;
+  width: 700px;
+}
+
+.extension-screenshots {
+  max-height: 350px;
+  overflow: auto;
+  text-align: center;
+}
+
+.extension-screenshot-img {
+  height: 250px;
+  margin-bottom: 2px;
+  margin-right: 2px;
+  width: 250px;
+}
+
+.extension-title {
+  display: block;
+  font-size: 15px;
+  font-weight: bold;
+  margin-bottom: 15px;
+  text-align: center;
+}
+
+.extension-text {
+  margin-bottom: 15px;
+  margin-left: 15px;
+  text-align: justify;
+  width: 400px;
+}
+
+.extensions-paging {
+  margin-bottom: 10px;
+  margin-top: 10px;
+  text-align: center;
+}
+
+.results-error {
+  text-align: center;
+  font-weight: bold;
+}
\ No newline at end of file
diff --git a/reviewboard/static/rb/css/images/animated-overlay.gif b/reviewboard/static/rb/css/images/animated-overlay.gif
new file mode 100644
index 0000000000000000000000000000000000000000..d441f75ebfbdf26a265dfccd670120d25c0a341c
Binary files /dev/null and b/reviewboard/static/rb/css/images/animated-overlay.gif differ
diff --git a/reviewboard/static/rb/css/images/ui-bg_flat_0_aaaaaa_40x100.png b/reviewboard/static/rb/css/images/ui-bg_flat_0_aaaaaa_40x100.png
new file mode 100644
index 0000000000000000000000000000000000000000..e078a34902954e006f754d3d65e98196063be7f0
Binary files /dev/null and b/reviewboard/static/rb/css/images/ui-bg_flat_0_aaaaaa_40x100.png differ
diff --git a/reviewboard/static/rb/css/images/ui-bg_flat_55_fbec88_40x100.png b/reviewboard/static/rb/css/images/ui-bg_flat_55_fbec88_40x100.png
new file mode 100644
index 0000000000000000000000000000000000000000..48232503cf965a426755fdc0c23fffc9453f6b60
Binary files /dev/null and b/reviewboard/static/rb/css/images/ui-bg_flat_55_fbec88_40x100.png differ
diff --git a/reviewboard/static/rb/css/images/ui-bg_glass_75_d0e5f5_1x400.png b/reviewboard/static/rb/css/images/ui-bg_glass_75_d0e5f5_1x400.png
new file mode 100644
index 0000000000000000000000000000000000000000..788a36006172f0e230d6e251ef5740f08eaa22b4
Binary files /dev/null and b/reviewboard/static/rb/css/images/ui-bg_glass_75_d0e5f5_1x400.png differ
diff --git a/reviewboard/static/rb/css/images/ui-bg_glass_85_dfeffc_1x400.png b/reviewboard/static/rb/css/images/ui-bg_glass_85_dfeffc_1x400.png
new file mode 100644
index 0000000000000000000000000000000000000000..a33cdc2a72c74c1641f7b4d2d9adbc5ce29fe649
Binary files /dev/null and b/reviewboard/static/rb/css/images/ui-bg_glass_85_dfeffc_1x400.png differ
diff --git a/reviewboard/static/rb/css/images/ui-bg_glass_95_fef1ec_1x400.png b/reviewboard/static/rb/css/images/ui-bg_glass_95_fef1ec_1x400.png
new file mode 100644
index 0000000000000000000000000000000000000000..f2785a430d7b1ed8365b0547d41c49bd6f09dd7e
Binary files /dev/null and b/reviewboard/static/rb/css/images/ui-bg_glass_95_fef1ec_1x400.png differ
diff --git a/reviewboard/static/rb/css/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png b/reviewboard/static/rb/css/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png
new file mode 100644
index 0000000000000000000000000000000000000000..ad2132a415027ccd556dbb4d4d63bee32a9d67b6
Binary files /dev/null and b/reviewboard/static/rb/css/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png differ
diff --git a/reviewboard/static/rb/css/images/ui-bg_inset-hard_100_f5f8f9_1x100.png b/reviewboard/static/rb/css/images/ui-bg_inset-hard_100_f5f8f9_1x100.png
new file mode 100644
index 0000000000000000000000000000000000000000..e2c64d1685ffef4b78e898a83c38ac8e62f51971
Binary files /dev/null and b/reviewboard/static/rb/css/images/ui-bg_inset-hard_100_f5f8f9_1x100.png differ
diff --git a/reviewboard/static/rb/css/images/ui-bg_inset-hard_100_fcfdfd_1x100.png b/reviewboard/static/rb/css/images/ui-bg_inset-hard_100_fcfdfd_1x100.png
new file mode 100644
index 0000000000000000000000000000000000000000..813aa49f20dbb1be6ace02c3bf546c7a153d9bf7
Binary files /dev/null and b/reviewboard/static/rb/css/images/ui-bg_inset-hard_100_fcfdfd_1x100.png differ
diff --git a/reviewboard/static/rb/css/images/ui-icons_217bc0_256x240.png b/reviewboard/static/rb/css/images/ui-icons_217bc0_256x240.png
new file mode 100644
index 0000000000000000000000000000000000000000..8d2b7e57044465bac0008ff88d23cdebeab6a8b8
Binary files /dev/null and b/reviewboard/static/rb/css/images/ui-icons_217bc0_256x240.png differ
diff --git a/reviewboard/static/rb/css/images/ui-icons_2e83ff_256x240.png b/reviewboard/static/rb/css/images/ui-icons_2e83ff_256x240.png
new file mode 100644
index 0000000000000000000000000000000000000000..84b601bf0f726bf95801da487deaf2344a32e4b8
Binary files /dev/null and b/reviewboard/static/rb/css/images/ui-icons_2e83ff_256x240.png differ
diff --git a/reviewboard/static/rb/css/images/ui-icons_469bdd_256x240.png b/reviewboard/static/rb/css/images/ui-icons_469bdd_256x240.png
new file mode 100644
index 0000000000000000000000000000000000000000..5dff3f962cd744033b2aef575491451a8b2ce6a8
Binary files /dev/null and b/reviewboard/static/rb/css/images/ui-icons_469bdd_256x240.png differ
diff --git a/reviewboard/static/rb/css/images/ui-icons_6da8d5_256x240.png b/reviewboard/static/rb/css/images/ui-icons_6da8d5_256x240.png
new file mode 100644
index 0000000000000000000000000000000000000000..f7809f8566cd0aaef9af00e7caeca9d3720cf1b0
Binary files /dev/null and b/reviewboard/static/rb/css/images/ui-icons_6da8d5_256x240.png differ
diff --git a/reviewboard/static/rb/css/images/ui-icons_cd0a0a_256x240.png b/reviewboard/static/rb/css/images/ui-icons_cd0a0a_256x240.png
new file mode 100644
index 0000000000000000000000000000000000000000..ed5b6b0930f672fa08e9b9bdbe5e55370fd1dc30
Binary files /dev/null and b/reviewboard/static/rb/css/images/ui-icons_cd0a0a_256x240.png differ
diff --git a/reviewboard/static/rb/css/images/ui-icons_d8e7f3_256x240.png b/reviewboard/static/rb/css/images/ui-icons_d8e7f3_256x240.png
new file mode 100644
index 0000000000000000000000000000000000000000..9b46228fb1e80406b2a9a65b694e5674494c2775
Binary files /dev/null and b/reviewboard/static/rb/css/images/ui-icons_d8e7f3_256x240.png differ
diff --git a/reviewboard/static/rb/css/images/ui-icons_f9bd01_256x240.png b/reviewboard/static/rb/css/images/ui-icons_f9bd01_256x240.png
new file mode 100644
index 0000000000000000000000000000000000000000..f1f0531ad5b02b7f891d84a6b6db6ce7290b65de
Binary files /dev/null and b/reviewboard/static/rb/css/images/ui-icons_f9bd01_256x240.png differ
diff --git a/reviewboard/static/rb/css/jquery-ui-1.7.1.custom.css b/reviewboard/static/rb/css/jquery-ui-1.7.1.custom.css
new file mode 100644
index 0000000000000000000000000000000000000000..38f976209a4e34959fc2e380571c4192105c9625
--- /dev/null
+++ b/reviewboard/static/rb/css/jquery-ui-1.7.1.custom.css
@@ -0,0 +1,117 @@
+/*
+* jQuery UI CSS Framework
+* Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses.
+*/
+
+/* Layout helpers
+----------------------------------*/
+.ui-helper-hidden { display: none; }
+.ui-helper-hidden-accessible { position: absolute; left: -99999999px; }
+.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
+.ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; }
+.ui-helper-clearfix { display: inline-block; }
+/* required comment for clearfix to work in Opera \*/
+* html .ui-helper-clearfix { height:1%; }
+.ui-helper-clearfix { display:block; }
+/* end clearfix */
+.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
+
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled { cursor: default !important; }
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Overlays */
+.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }/* Accordion
+----------------------------------*/
+
+----------------------------------*/
+.ui-tabs { padding: .2em; zoom: 1; }
+.ui-tabs .ui-tabs-nav { list-style: none; position: relative; padding: .2em .2em 0; }
+.ui-tabs .ui-tabs-nav li { position: relative; float: left; border-bottom-width: 0 !important; margin: 0 .2em -1px 0; padding: 0; }
+.ui-tabs .ui-tabs-nav li a { float: left; text-decoration: none; padding: .5em 1em; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-selected { padding-bottom: 1px; border-bottom-width: 0; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; }
+.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */
+.ui-tabs .ui-tabs-panel { padding: 1em 1.4em; display: block; border-width: 0; background: none; }
+.ui-tabs .ui-tabs-hide { display: none !important; }
+
+
+/*
+* jQuery UI CSS Framework
+* Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses.
+* To view and modify this theme, visit http://jqueryui.com/themeroller/?tr=&ffDefault=Lucida%20Grande,%20Lucida%20Sans,%20Arial,%20sans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=5px&bgColorHeader=5c9ccc&bgTextureHeader=12_gloss_wave.png&bgImgOpacityHeader=55&borderColorHeader=4297d7&fcHeader=ffffff&iconColorHeader=d8e7f3&bgColorContent=fcfdfd&bgTextureContent=06_inset_hard.png&bgImgOpacityContent=100&borderColorContent=a6c9e2&fcContent=222222&iconColorContent=469bdd&bgColorDefault=dfeffc&bgTextureDefault=02_glass.png&bgImgOpacityDefault=85&borderColorDefault=c5dbec&fcDefault=2e6e9e&iconColorDefault=6da8d5&bgColorHover=d0e5f5&bgTextureHover=02_glass.png&bgImgOpacityHover=75&borderColorHover=79b7e7&fcHover=1d5987&iconColorHover=217bc0&bgColorActive=f5f8f9&bgTextureActive=06_inset_hard.png&bgImgOpacityActive=100&borderColorActive=79b7e7&fcActive=e17009&iconColorActive=f9bd01&bgColorHighlight=fbec88&bgTextureHighlight=01_flat.png&bgImgOpacityHighlight=55&borderColorHighlight=fad42e&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=02_glass.png&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
+*/
+
+
+/* Component containers
+----------------------------------*/
+.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-size: 1em; }
+.ui-widget-content { border: 1px solid #a6c9e2; background: #fcfdfd url(images/ui-bg_inset-hard_100_fcfdfd_1x100.png) 50% bottom repeat-x; color: #222222; }
+.ui-widget-content a { color: #222222; }
+.ui-widget-header { border: 1px solid #4297d7; background: #5c9ccc url(images/ui-bg_gloss-wave_55_5c9ccc_500x100.png) 50% 50% repeat-x; color: #ffffff; font-weight: bold; }
+.ui-widget-header a { color: #ffffff; }
+
+/* Interaction states
+----------------------------------*/
+.ui-state-default, .ui-widget-content .ui-state-default { border: 1px solid #c5dbec; background: #dfeffc url(images/ui-bg_glass_85_dfeffc_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #2e6e9e; outline: none; }
+.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #2e6e9e; text-decoration: none; outline: none; }
+.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus { border: 1px solid #79b7e7; background: #d0e5f5 url(images/ui-bg_glass_75_d0e5f5_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #1d5987; outline: none; }
+.ui-state-hover a, .ui-state-hover a:hover { color: #1d5987; text-decoration: none; outline: none; }
+.ui-state-active, .ui-widget-content .ui-state-active { border: 1px solid #79b7e7; background: #f5f8f9 url(images/ui-bg_inset-hard_100_f5f8f9_1x100.png) 50% 50% repeat-x; font-weight: bold; color: #e17009; outline: none; }
+.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #e17009; outline: none; text-decoration: none; }
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-highlight, .ui-widget-content .ui-state-highlight {border: 1px solid #fad42e; background: #fbec88 url(images/ui-bg_flat_55_fbec88_40x100.png) 50% 50% repeat-x; color: #363636; }
+.ui-state-highlight a, .ui-widget-content .ui-state-highlight a { color: #363636; }
+.ui-state-error, .ui-widget-content .ui-state-error {border: 1px solid #cd0a0a; background: #fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x; color: #cd0a0a; }
+.ui-state-error a, .ui-widget-content .ui-state-error a { color: #cd0a0a; }
+.ui-state-error-text, .ui-widget-content .ui-state-error-text { color: #cd0a0a; }
+.ui-state-disabled, .ui-widget-content .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
+.ui-priority-primary, .ui-widget-content .ui-priority-primary { font-weight: bold; }
+.ui-priority-secondary, .ui-widget-content .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_469bdd_256x240.png); }
+.ui-widget-content .ui-icon {background-image: url(images/ui-icons_469bdd_256x240.png); }
+.ui-widget-header .ui-icon {background-image: url(images/ui-icons_d8e7f3_256x240.png); }
+.ui-state-default .ui-icon { background-image: url(images/ui-icons_6da8d5_256x240.png); }
+.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_217bc0_256x240.png); }
+.ui-state-active .ui-icon {background-image: url(images/ui-icons_f9bd01_256x240.png); }
+.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_2e83ff_256x240.png); }
+.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_cd0a0a_256x240.png); }
+
+/* Misc visuals
+----------------------------------*/
+
+/* Corner radius */
+.ui-corner-tl { -moz-border-radius-topleft: 5px; -webkit-border-top-left-radius: 5px; }
+.ui-corner-tr { -moz-border-radius-topright: 5px; -webkit-border-top-right-radius: 5px; }
+.ui-corner-bl { -moz-border-radius-bottomleft: 5px; -webkit-border-bottom-left-radius: 5px; }
+.ui-corner-br { -moz-border-radius-bottomright: 5px; -webkit-border-bottom-right-radius: 5px; }
+.ui-corner-top { -moz-border-radius-topleft: 5px; -webkit-border-top-left-radius: 5px; -moz-border-radius-topright: 5px; -webkit-border-top-right-radius: 5px; }
+.ui-corner-bottom { -moz-border-radius-bottomleft: 5px; -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomright: 5px; -webkit-border-bottom-right-radius: 5px; }
+.ui-corner-right {  -moz-border-radius-topright: 5px; -webkit-border-top-right-radius: 5px; -moz-border-radius-bottomright: 5px; -webkit-border-bottom-right-radius: 5px; }
+.ui-corner-left { -moz-border-radius-topleft: 5px; -webkit-border-top-left-radius: 5px; -moz-border-radius-bottomleft: 5px; -webkit-border-bottom-left-radius: 5px; }
+.ui-corner-all { -moz-border-radius: 5px; -webkit-border-radius: 5px; }
+
+/* Overlays */
+.ui-widget-overlay { background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); }
+.ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); -moz-border-radius: 8px; -webkit-border-radius: 8px; }
\ No newline at end of file
diff --git a/reviewboard/static/rb/js/extensionbrowser.js b/reviewboard/static/rb/js/extensionbrowser.js
new file mode 100644
index 0000000000000000000000000000000000000000..29dcd86a1de864cbc450273b53bda5d5351afb33
--- /dev/null
+++ b/reviewboard/static/rb/js/extensionbrowser.js
@@ -0,0 +1,33 @@
+var action_nodes = $("#results_container").find(".item-actions");
+
+// Listener for details link over each extension.
+action_nodes.find('a.details').click(function() {
+    show_details(this.title, false);
+});
+
+// Listener for install link over each extension.
+action_nodes.find('a.install').click(function() {
+    show_details(this.title, true);
+});
+
+/*
+ * Invoke a modal box and show more details about an extension.
+ * If the user intends to install the extension, show the installation
+ * tab directly.
+ *
+ * @param {string} package_name The package name of the extension.
+ * @param {boolean} open_install_tab Whether to open with the installation
+ * tab by default.
+ */
+function show_details(package_name, open_install_tab) {
+    var extension_info = new ExtensionInfo({id: package_name});
+    extension_info.ready({
+        ready: function() {
+            var modalBoxView = new TabbedModalBoxView({model: extension_info});
+            modalBoxView.render(open_install_tab);
+        },
+        error: function(model, text, statusText) {
+            alert("An error occurred: " + text);
+        }
+    });
+}
\ No newline at end of file
diff --git a/reviewboard/static/rb/js/models/extensionInformationModel.js b/reviewboard/static/rb/js/models/extensionInformationModel.js
new file mode 100644
index 0000000000000000000000000000000000000000..9def97ac0fea5cc945c98b4779a79dc596cb41d3
--- /dev/null
+++ b/reviewboard/static/rb/js/models/extensionInformationModel.js
@@ -0,0 +1,72 @@
+/*
+ * Model associated with various information for an extension.
+ */
+ExtensionInfo = RB.BaseResource.extend({
+    rspNamespace: 'extension',
+    id: null,
+
+    defaults: _.defaults({
+        name: null,
+        description: null,
+        version: null,
+        author: null,
+        rating: null,
+        category: null,
+        compatibility: null,
+        dependencies: null,
+        screenshots: null,
+        preinstall: null,
+        installed: null
+    }, RB.BaseResource.prototype.defaults),
+
+    url: function() {
+        return '../../../api/extensionbrowser/extensions/' + this.id + '/';
+    },
+
+    parse: function(rsp) {
+
+        var result = RB.BaseResource.prototype.parse.call(this, rsp),
+            rspData = rsp[this.rspNamespace];
+
+        result.name = rspData.name;
+        result.description = rspData.description;
+        result.version = rspData.version;
+        result.author = rspData.author;
+        result.rating = rspData.rating;
+        result.category = rspData.category;
+        result.compatibility  = rspData.compatibility;
+        result.dependencies = rspData.dependencies;
+        result.screenshots = rspData.screenshots;
+        result.preinstall = rspData.preinstall;
+        result.installed = rspData.installed;
+        result.install_url = rspData.install_url;
+
+        return result;
+    }
+});
+
+/*
+ * Model associated with installing an extension from the store.
+ */
+ExtensionInstallInfo = RB.BaseResource.extend({
+    rspNamespace: "install",
+
+    defaults: _.defaults({
+        package_name: null,
+        install_url: null
+    }, RB.BaseResource.prototype.defaults),
+
+    url: function() {
+        return '../../../api/extensionbrowser/install/';
+    },
+
+    toJSON: function() {
+        return {
+            package_name: this.get('package_name'),
+            install_url: this.get('install_url')
+        };
+    },
+
+    parse: function(rsp) {
+    }
+});
\ No newline at end of file
diff --git a/reviewboard/static/rb/js/views/extensionBrowserView.js b/reviewboard/static/rb/js/views/extensionBrowserView.js
new file mode 100644
index 0000000000000000000000000000000000000000..5fe68d3717db877ae7f4f493c264462c71f914e0
--- /dev/null
+++ b/reviewboard/static/rb/js/views/extensionBrowserView.js
@@ -0,0 +1,175 @@
+/*
+ * Displays a modal box with various tab concering different
+ * information about an extension.
+ *
+ * By default the first tab is rendered unless the caller specifies
+ * to open the installation tab by default. This happens when the user
+ * directly clicks on the installation link.
+ *
+ * Each tab itself has a view of type InfoView (as defined below) which
+ * handles the rendering associated with a tab.
+ */
+var TabbedModalBoxView = Backbone.View.extend({
+
+    el: $("<div/>"),
+
+    initialize: function() {
+        _.bindAll(this, 'render');
+    },
+
+    render: function(show_install_tab) {
+        var template_html = '\
+         <div id="extension_info_tabs" class="extensionbrowser">\
+          <ul>\
+           <li><a href="#ext-information">Information</a></li>\
+           <li><a href="#ext-screenshots">Screenshots</a></li>\
+           <li><a href="#ext-dependencies">Dependencies</a></li>\
+           <li><a href="#ext-preinstall">Install</a></li>\
+          </ul>\
+          <div id="ext-information">\
+          </div>\
+          <div id="ext-screenshots">\
+          </div>\
+          <div id="ext-dependencies">\
+          </div>\
+          <div id="ext-preinstall">\
+          </div>\
+         </div>';
+
+        var template = _.template(template_html, {name: this.model.get('name')});
+        $(this.el).html(template);
+        $(this.el).addClass("extension-modalbox");
+        $(this.el).modalBox();
+
+        var info_view = new InfoView({ model: this.model });
+        var current_tab = show_install_tab ? "ext-preinstall" : "ext-information";
+        var current_tab_index = show_install_tab ? 3 : 0;
+        // Populate the first or install tab.
+        info_view.render(current_tab);
+
+        // Set up a listener for tab clicks and render the contents
+        // of the tab when needed.
+        $("#extension_info_tabs").tabs({
+            selected: current_tab_index,
+
+            select: function(e, ui) {
+                info_view.render(ui.panel.id);
+            }
+        });
+
+        return this;
+    }
+});
+
+/*
+ * Display details of an extension within a particular tab.
+ *
+ * If within the "install" tab, there is an installation action
+ * permitted which installs an extension.
+ */
+var InfoView = Backbone.View.extend({
+
+    // Templates for InfoViews.
+    target_templates: {
+      "ext-information": [
+        '<div class="extension-info-wrapper clearfix">',
+         '<div class="extension-info-main">',
+          '<span class="extension-title"><%= name %></span>',
+          '<label>Description</label>:',
+          '<div class="extension-text">',
+           '<%= description %>',
+          '</div>',
+         '</div>',
+         '<div class="extension-info-sidebar">',
+          '<div><label>Author</label>: <%= author %></div>',
+          '<div><label>Version</label>: <%= version %></div>',
+          '<div><label>Rating</label>: <%= rating %></div>',
+          '<div><label>Category</label>: <%= category %></div>',
+          '<div><label>Compatibility</label>: <%= compatibility.start %> - <%= compatibility.end %></div>',
+         '</div>',
+        '</div>',
+      ].join(''),
+
+      "ext-screenshots": [
+        '<div class="extension-info-wrapper">',
+         '<div class="extension-screenshots">',
+         '<span class="extension-title"><%= name %></span>',
+         '<% _.each(screenshots, function(screenshot){ %>',
+          '<a href="<%= screenshot.image %>" target="_blank">',
+           '<img src="<%= screenshot.thumbnail %>" class="extension-screenshot-img" />',
+          '</a>',
+         '<% }); %>',
+         '</div>',
+        '</div>',
+      ].join(''),
+
+      "ext-dependencies": [
+        '<div class="extension-info-wrapper">',
+         '<span class="extension-title"><%= name %></span>',
+         '<ul>',
+          '<% _.each(dependencies, function(dependency){ %>',
+            '<li>',
+             '<%= dependency.name %> (<a href="<%= dependency.url %>"><%= dependency.package %></a>)',
+            '</li>',
+          '<% }); %>',
+         '</ul>',
+        '</div>',
+      ].join(''),
+
+      "ext-preinstall": [
+        '<div class="extension-info-wrapper">',
+         '<span class="extension-title"><%= name %></span>',
+          '<label>Pre-Install Information</label>:',
+          '<div class="extension-text">',
+           '<%= preinstall %>',
+          '</div>',
+          '<% if(installed){ %>',
+            '<button disabled="disabled">Installed</b>',
+          '<% }else{ %>',
+            '<button>Install</button>',
+          '<% } %>',
+        '</div>',
+      ].join('')
+    },
+
+    el: $("<div/>"),
+
+    initialize: function() {
+        _.bindAll(this, 'render');
+    },
+
+    events: {
+        "click .extension-info-wrapper > button": "install"
+    },
+
+    render: function(id) {
+        var template = _.template(this.target_templates[id], this.model.attributes);
+        $(this.el).html(template);
+        $("#" + id).html(this.el);
+    },
+
+    /*
+     * Install an extension.
+     *
+     * Populate the ExtensionInstallInfo model and save it; saving it
+     * installs the extension.
+     */
+    install: function() {
+        var extension_install = new ExtensionInstallInfo({
+                                    package_name: this.model.get('id'),
+                                    install_url: this.model.get('install_url')
+                                });
+        var self = this;
+
+        extension_install.save({
+            success: function() {
+                // Change the 'installed' attribute and re-render the tab.
+                self.model.set('installed', true);
+                self.render("ext-preinstall");
+            },
+            error: function(model, text, statusText) {
+               alert("An error occurred: " + text);
+            }
+        });
+    },
+});
\ No newline at end of file
diff --git a/reviewboard/urls.py b/reviewboard/urls.py
index b27f97efe044ff2639da0db363d073fe1707d13b..73402ef8c76dd6aeeaf8e12f7c4f72f6daea002e 100644
--- a/reviewboard/urls.py
+++ b/reviewboard/urls.py
@@ -26,6 +26,8 @@ if not admin.site._registry:
 urlpatterns = patterns('',
     (r'^admin/extensions/', include('djblets.extensions.urls'),
      {'extension_manager': extension_manager}),
+    (r'^admin/extensions/browse/',
+        include('reviewboard.extensionbrowser.urls')),
     (r'^admin/', include('reviewboard.admin.urls')),
 )
 
diff --git a/reviewboard/webapi/resources.py b/reviewboard/webapi/resources.py
index edaa046169079ecd8d7e8cd38ccffc5828bbbc02..c76128cfc1901f7a4b74e10cd2302605bf92e1d8 100644
--- a/reviewboard/webapi/resources.py
+++ b/reviewboard/webapi/resources.py
@@ -18,6 +18,7 @@ from django.utils.formats import localize
 from django.utils.translation import ugettext as _
 from djblets.extensions.base import RegisteredExtension
 from djblets.extensions.resources import ExtensionResource
+from djblets.extensions.errors import InstallExtensionError
 from djblets.gravatars import get_gravatar_url
 from djblets.siteconfig.models import SiteConfiguration
 from djblets.util.decorators import augment_method_from
@@ -29,8 +30,11 @@ from djblets.webapi.core import WebAPIResponsePaginated, \
 from djblets.webapi.decorators import webapi_login_required, \
                                       webapi_response_errors, \
                                       webapi_request_fields
-from djblets.webapi.errors import DOES_NOT_EXIST, INVALID_FORM_DATA, \
-                                  NOT_LOGGED_IN, PERMISSION_DENIED
+from djblets.webapi.errors import DOES_NOT_EXIST, EXTENSION_INSTALLED, \
+                                  INSTALL_EXTENSION_FAILED, \
+                                  INVALID_FORM_DATA, NOT_LOGGED_IN, \
+                                  PERMISSION_DENIED
+
 from djblets.webapi.resources import WebAPIResource as DjbletsWebAPIResource, \
                                      UserResource as DjbletsUserResource, \
                                      RootResource as DjbletsRootResource, \
@@ -47,6 +51,7 @@ from reviewboard.diffviewer.diffutils import get_diff_files, \
                                              get_patched_file, \
                                              populate_diff_chunks
 from reviewboard.diffviewer.forms import EmptyDiffError, DiffTooBigError
+from reviewboard.extensionbrowser.base import ExtensionStoreQuery
 from reviewboard.extensions.base import get_extension_manager
 from reviewboard.hostingsvcs.errors import AuthorizationError
 from reviewboard.hostingsvcs.models import HostingServiceAccount
@@ -7204,6 +7209,109 @@ class ServerInfoResource(WebAPIResource):
 server_info_resource = ServerInfoResource()
 
 
+class ExtensionInfo(WebAPIResource):
+    """Information about an extension from an extension store.
+
+    This includes information such as description, author,
+    version, rating, category, compatibility, screenshots,
+    dependencies and any pre-instal warnings/information.
+
+    Only administrators can access this endpoint.
+    """
+    name = "extension"
+    uri_object_key = "package_name"
+    uri_object_key_regex = '[A-Za-z0-9_-]+'
+
+    @webapi_login_required
+    @webapi_response_errors(DOES_NOT_EXIST, PERMISSION_DENIED)
+    def get(self, request, *args, **kwargs):
+        """Get information on an extension addressed by its
+        package name."""
+        if not request.user.is_superuser:
+            return PERMISSION_DENIED
+
+        store = ExtensionStoreQuery(get_extension_manager())
+        data = store.get_extension_info(kwargs["package_name"])
+        if "error" in data:
+            logging.error("Extension does not exist: " + data["error"])
+            return DOES_NOT_EXIST
+
+        data["links"] = self.get_links(request=request, *args, **kwargs)
+
+        print request.user
+
+        return 200, {self.name: data}
+
+extension_info = ExtensionInfo()
+
+
+class ExtensionInstall(WebAPIResource):
+    """Install an extension from the store.
+
+    Only administrators can access this endpoint.
+    """
+    name = "install"
+    singleton = True
+    allowed_methods = ('POST')
+
+    @webapi_login_required
+    @webapi_response_errors(EXTENSION_INSTALLED,
+                            INSTALL_EXTENSION_FAILED,
+                            PERMISSION_DENIED)
+    @webapi_request_fields(required={
+        'install_url': {
+            'type': str,
+            'description': 'URL to the archive containting an egg to install.',
+        },
+        'package_name': {
+            'type': str,
+            'description': 'Package name of the extension.'
+        }
+    })
+    def create(self, request, package_name, install_url, *args, **kwargs):
+        """Install an extension.
+
+        Extension must be an archive of a compatible egg located at
+        install_url and with package name package_name.
+        """
+        if not request.user.is_superuser:
+            return PERMISSION_DENIED
+
+        installed_extensions = [
+            ext.dist.project_name.lower()
+            for ext in get_extension_manager()._entrypoint_iterator()
+            ]
+
+        if package_name in installed_extensions:
+            return EXTENSION_INSTALLED
+
+        try:
+            get_extension_manager().\
+                install_extension(install_url, package_name)
+        except InstallExtensionError, e:
+            logging.error("Extension installation failed: " + str(e))
+            return INSTALL_EXTENSION_FAILED
+
+        return 201, {}
+
+extension_install = ExtensionInstall()
+
+
+class ExtensionBrowser(WebAPIResource):
+    """Endpoints for seeking for information about an extension from
+    the store and installting an extension.
+    """
+    name = "extensionbrowser"
+    singleton = True
+
+    list_child_resources = [
+        extension_info,
+        extension_install,
+    ]
+
+extension_browser = ExtensionBrowser()
+
+
 class SessionResource(WebAPIResource):
     """Information on the active user's session.
 
@@ -7278,6 +7386,7 @@ class RootResource(DjbletsRootResource):
     def __init__(self, *args, **kwargs):
         super(RootResource, self).__init__([
             default_reviewer_resource,
+            extension_browser,
             extension_resource,
             hosting_service_account_resource,
             repository_resource,
