Allow to configure an alternative image server

This plugins implements an image server that stores uploaded images in
a git repository (by default All-Images) inside of refs/images/...
refs. Now there is a new configuration setting 'enableImageServer'
that allows disabling this image server. Instead an external image
server can be configured by defining a pattern for URLs of images that
should be embedded. Optionally also an URL for uploading images can be
configured. This is useful for companies that already run an image
server and rather want to use this server than storing the images in
Gerrit.

E.g. to configure imgur.com as image server you can add the following
configuration in gerrit.config:

  [plugin "imagare"]
    enableImageServer = false
    pattern = http:\\/\\/i\\.imgur\\.com\\/.*\\.png
    uploadUrl = http://imgur.com

Disabling the plugin image server means that the servlet to serve
images and the REST endpoints to upload/delete/list images are not
registered. Changing this configuration parameter requires a reload of
the plugin.

Images from an external image server are embedded in the UI even if
their mime type is not configured as safe in the Gerrit configuration.

Change-Id: I5022d421c5fd3ed9fdbdcf4374b6faffd9a7ff78
Signed-off-by: Edwin Kempin <ekempin@google.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/GetConfig.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/GetConfig.java
index 52b225d..903c859 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/GetConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/GetConfig.java
@@ -15,6 +15,7 @@
 package com.googlesource.gerrit.plugins.imagare;
 
 import com.google.common.base.MoreObjects;
+import com.google.gerrit.extensions.annotations.PluginCanonicalWebUrl;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.config.ConfigResource;
@@ -25,11 +26,16 @@
 public class GetConfig implements RestReadView<ConfigResource> {
 
   private final PluginConfig cfg;
+  private final String pluginName;
+  private final String canonicalWebUrl;
 
   @Inject
   public GetConfig(PluginConfigFactory cfgFactory,
-      @PluginName String pluginName) {
+      @PluginName String pluginName,
+      @PluginCanonicalWebUrl String canonicalWebUrl) {
     this.cfg = cfgFactory.getFromGerritConfig(pluginName);
+    this.pluginName = pluginName;
+    this.canonicalWebUrl = canonicalWebUrl;
   }
 
   @Override
@@ -45,12 +51,40 @@
     if (!info.stage) {
       info.stage = null;
     }
+    boolean enableImageServer = cfg.getBoolean("enableImageServer", true);
+    info.enableImageServer = enableImageServer;
+    if (!info.enableImageServer) {
+      info.enableImageServer = null;
+    }
+
+    if (enableImageServer) {
+      info.pattern = escapeRegexpForJavaScript(canonicalWebUrl)
+          + "project/.*/rev/.*/.*\\.(jpg|jpeg|png|gif|bmp|ico|svg|tif|tiff)";
+      info.uploadUrl = "#/x/" + pluginName + "/upload";
+    } else {
+      info.pattern = cfg.getString("pattern");
+      info.uploadUrl = cfg.getString("uploadUrl");
+    }
+
     return info;
   }
 
+  /**
+   * Escapes a string for being used in a JavaScript regexp.
+   * The following characters must be escaped: . * + ? ^ $ { } ( ) | [ ] / \
+   * @param s string to be escaped
+   * @return the escaped string
+   */
+  private String escapeRegexpForJavaScript(String s) {
+    return s.replaceAll("([.*+?^${}()|\\[\\]\\/\\\\])", "\\\\$1");
+  }
+
   public static class ConfigInfo {
     String defaultProject;
     LinkDecoration linkDecoration;
     Boolean stage;
+    Boolean enableImageServer;
+    String pattern;
+    String uploadUrl;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/GetPreference.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/GetPreference.java
index c470254..0711af9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/GetPreference.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/GetPreference.java
@@ -83,6 +83,8 @@
       info.stage = null;
     }
 
+    info.pattern = globalCfg.pattern;
+
     return info;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java
index daf5355..f83a243 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java
@@ -14,18 +14,34 @@
 
 package com.googlesource.gerrit.plugins.imagare;
 
+import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.webui.GwtPlugin;
 import com.google.gerrit.extensions.webui.JavaScriptPlugin;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
 import com.google.gerrit.httpd.plugins.HttpPluginModule;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.Inject;
 
 public class HttpModule extends HttpPluginModule {
 
+  private final PluginConfigFactory cfgFactory;
+  private final String pluginName;
+
+  @Inject
+  HttpModule(PluginConfigFactory cfgFactory,
+      @PluginName String pluginName) {
+    this.cfgFactory = cfgFactory;
+    this.pluginName = pluginName;
+  }
+
   @Override
   protected void configureServlets() {
-    serveRegex("^" + ImageServlet.PATH_PREFIX + "(.+)?$")
-        .with(ImageServlet.class);
+    if (cfgFactory.getFromGerritConfig(pluginName, true)
+        .getBoolean("enableImageServer", true)) {
+      serveRegex("^" + ImageServlet.PATH_PREFIX + "(.+)?$")
+          .with(ImageServlet.class);
+    }
 
     DynamicSet.bind(binder(), WebUiPlugin.class)
         .toInstance(new GwtPlugin("imagare"));
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/ImagareMenu.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/ImagareMenu.java
index 804538e..975c6ae 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/ImagareMenu.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/ImagareMenu.java
@@ -16,6 +16,8 @@
 
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.webui.TopMenu;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.inject.Inject;
 
 import java.util.ArrayList;
@@ -26,10 +28,20 @@
   private final List<MenuEntry> menuEntries;
 
   @Inject
-  public ImagareMenu(@PluginName String pluginName) {
+  public ImagareMenu(PluginConfigFactory cfgFactory,
+      @PluginName String pluginName) {
     menuEntries = new ArrayList<>();
-    menuEntries.add(new MenuEntry("Tools", Collections
-        .singletonList(new MenuItem("Image Upload", "#/x/" + pluginName + "/upload", ""))));
+    PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName, true);
+    if (cfg.getBoolean("enableImageServer", true)) {
+      menuEntries.add(new MenuEntry("Tools", Collections
+          .singletonList(new MenuItem("Image Upload", "#/x/" + pluginName + "/upload", ""))));
+    } else {
+      String uploadUrl = cfg.getString("uploadUrl");
+      if (uploadUrl != null) {
+        menuEntries.add(new MenuEntry("Tools", Collections
+            .singletonList(new MenuItem("Image Upload", uploadUrl))));
+      }
+    }
   }
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/Module.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/Module.java
index 309e5fd..11c221a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/Module.java
@@ -21,27 +21,49 @@
 import static com.googlesource.gerrit.plugins.imagare.ImageResource.IMAGE_KIND;
 
 import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.RestApiModule;
 import com.google.gerrit.extensions.webui.TopMenu;
+import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
 
 public class Module extends AbstractModule {
 
+  private final PluginConfigFactory cfgFactory;
+  private final String pluginName;
+
+  @Inject
+  Module(PluginConfigFactory cfgFactory,
+      @PluginName String pluginName) {
+    this.cfgFactory = cfgFactory;
+    this.pluginName = pluginName;
+  }
+
   @Override
   protected void configure() {
-    DynamicSet.bind(binder(), TopMenu.class).to(ImagareMenu.class);
-    bind(com.google.gerrit.extensions.config.CapabilityDefinition.class)
+    if (cfgFactory.getFromGerritConfig(pluginName, true)
+        .getBoolean("enableImageServer", true)) {
+      bind(com.google.gerrit.extensions.config.CapabilityDefinition.class)
         .annotatedWith(Exports.named(DELETE_OWN_IMAGES))
         .to(DeleteOwnImagesCapability.class);
+      install(new RestApiModule() {
+        @Override
+        protected void configure() {
+          DynamicMap.mapOf(binder(), IMAGE_KIND);
+          bind(ImagesCollection.class);
+          child(PROJECT_KIND, "images").to(ImagesCollection.class);
+          delete(IMAGE_KIND).to(DeleteImage.class);
+        }
+      });
+    }
+
+    DynamicSet.bind(binder(), TopMenu.class).to(ImagareMenu.class);
     install(new RestApiModule() {
       @Override
       protected void configure() {
-        DynamicMap.mapOf(binder(), IMAGE_KIND);
-        bind(ImagesCollection.class);
-        child(PROJECT_KIND, "images").to(ImagesCollection.class);
-        delete(IMAGE_KIND).to(DeleteImage.class);
         get(CONFIG_KIND, "config").to(GetConfig.class);
         put(CONFIG_KIND, "config").to(PutConfig.class);
         get(ACCOUNT_KIND, "preference").to(GetPreference.class);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/PutConfig.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/PutConfig.java
index 026d7aa..1227810 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/PutConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/PutConfig.java
@@ -43,6 +43,8 @@
     public String defaultProject;
     public LinkDecoration linkDecoration;
     public Boolean stage;
+    public String pattern;
+    public String uploadUrl;
   }
 
   private final PluginConfigFactory cfgFactory;
@@ -95,6 +97,14 @@
       }
     }
 
+    if (input.pattern != null) {
+      cfg.setString("plugin", pluginName, "pattern", input.pattern);
+    }
+
+    if (input.uploadUrl != null) {
+      cfg.setString("plugin", pluginName, "uploadUrl", input.uploadUrl);
+    }
+
     cfg.save();
     cfgFactory.getFromGerritConfig(pluginName, true);
     return Response.<String> ok("OK");
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ConfigInfo.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ConfigInfo.java
index f503031..901856a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ConfigInfo.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ConfigInfo.java
@@ -28,6 +28,7 @@
   private final native String link_decoration() /*-{ return this.link_decoration; }-*/;
 
   final native boolean stage() /*-{ return this.stage ? true : false; }-*/;
+  final native boolean enableImageServer() /*-{ return this.enable_image_server ? true : false; }-*/;
 
   final native void setDefaultProject(String p) /*-{ this.default_project = p; }-*/;
   final native void setLinkDecoration(String d) /*-{ this.link_decoration = d; }-*/;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagareAdminScreen.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagareAdminScreen.java
index b6d77f2..a0092d6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagareAdminScreen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagareAdminScreen.java
@@ -21,15 +21,22 @@
 public class ImagareAdminScreen extends ImagareConfigScreen {
 
   static class Factory implements Screen.EntryPoint {
+    private final boolean enableImageServer;
+
+    Factory(boolean enableImageServer) {
+      this.enableImageServer = enableImageServer;
+    }
+
     @Override
     public void onLoad(Screen screen) {
       screen.setPageTitle("Imagare Admin");
-      screen.show(new ImagareAdminScreen());
+      screen.show(new ImagareAdminScreen(enableImageServer));
     }
   }
 
-  ImagareAdminScreen() {
-    super(new RestApi("config").id("server").view(Plugin.get().getPluginName(),
-        "config"));
+  ImagareAdminScreen(boolean enableImageServer) {
+    super(enableImageServer,
+        new RestApi("config").id("server").view(
+            Plugin.get().getPluginName(), "config"));
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagareConfigScreen.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagareConfigScreen.java
index f554fea..0a4f83d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagareConfigScreen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagareConfigScreen.java
@@ -29,6 +29,7 @@
 import com.google.gwt.user.client.ui.VerticalPanel;
 
 public abstract class ImagareConfigScreen extends VerticalPanel {
+  protected final boolean enableImageServer;
   private final RestApi restApi;
 
   private TextBox projectBox;
@@ -36,7 +37,8 @@
   private CheckBox stageBox;
   private Button saveButton;
 
-  protected ImagareConfigScreen(RestApi restApi) {
+  protected ImagareConfigScreen(boolean enableImageServer, RestApi restApi) {
+    this.enableImageServer = enableImageServer;
     this.restApi = restApi;
     setStyleName("imagare-config-screen");
     restApi.get(new AsyncCallback<ConfigInfo>() {
@@ -55,18 +57,6 @@
   protected void display(ConfigInfo info) {
     HorizontalPanel p = new HorizontalPanel();
     p.setStyleName("imagare-label-panel");
-    p.add(new Label("Project"));
-    Image projectInfo = new Image(ImagarePlugin.RESOURCES.info());
-    projectInfo.setTitle("The default project for the image upload.");
-    p.add(projectInfo);
-    p.add(new Label(":"));
-    projectBox = new TextBox();
-    projectBox.setValue(info.getDefaultProject());
-    p.add(projectBox);
-    add(p);
-
-    p = new HorizontalPanel();
-    p.setStyleName("imagare-label-panel");
     p.add(new Label("Link Decoration"));
     Image linkDecorationInfo = new Image(ImagarePlugin.RESOURCES.info());
     linkDecorationInfo.setTitle("Decoration for image links in the Gerrit WebUI."
@@ -87,16 +77,30 @@
     p.add(linkDecorationBox);
     add(p);
 
-    p = new HorizontalPanel();
-    p.setStyleName("imagare-label-panel");
-    stageBox = new CheckBox("Stage images before upload");
-    stageBox.setValue(info.stage());
-    p.add(stageBox);
-    Image stageInfo = new Image(ImagarePlugin.RESOURCES.info());
-    stageInfo.setTitle("Images are not uploaded immediately but put into a "
-        + "staging area. The upload must be triggered explicitely.");
-    p.add(stageInfo);
-    add(p);
+    if (enableImageServer) {
+      p = new HorizontalPanel();
+      p.setStyleName("imagare-label-panel");
+      p.add(new Label("Project"));
+      Image projectInfo = new Image(ImagarePlugin.RESOURCES.info());
+      projectInfo.setTitle("The default project for the image upload.");
+      p.add(projectInfo);
+      p.add(new Label(":"));
+      projectBox = new TextBox();
+      projectBox.setValue(info.getDefaultProject());
+      p.add(projectBox);
+      add(p);
+
+      p = new HorizontalPanel();
+      p.setStyleName("imagare-label-panel");
+      stageBox = new CheckBox("Stage images before upload");
+      stageBox.setValue(info.stage());
+      p.add(stageBox);
+      Image stageInfo = new Image(ImagarePlugin.RESOURCES.info());
+      stageInfo.setTitle("Images are not uploaded immediately but put into a "
+          + "staging area. The upload must be triggered explicitely.");
+      p.add(stageInfo);
+      add(p);
+    }
 
     HorizontalPanel buttons = new HorizontalPanel();
     add(buttons);
@@ -111,9 +115,11 @@
     });
     buttons.add(saveButton);
     saveButton.setEnabled(false);
-    OnEditEnabler onEditEnabler = new OnEditEnabler(saveButton, projectBox);
-    onEditEnabler.listenTo(linkDecorationBox);
-    onEditEnabler.listenTo(stageBox);
+    OnEditEnabler onEditEnabler = new OnEditEnabler(saveButton, linkDecorationBox);
+    if (enableImageServer) {
+      onEditEnabler.listenTo(projectBox);
+      onEditEnabler.listenTo(stageBox);
+    }
 
     projectBox.setFocus(true);
     saveButton.setEnabled(false);
@@ -121,9 +127,11 @@
 
   private void doSave() {
     ConfigInfo in = ConfigInfo.create();
-    in.setDefaultProject(projectBox.getValue());
     in.setLinkDecoration(linkDecorationBox.getValue(linkDecorationBox.getSelectedIndex()));
-    in.setStage(stageBox.getValue());
+    if (enableImageServer) {
+      in.setDefaultProject(projectBox.getValue());
+      in.setStage(stageBox.getValue());
+    }
     restApi.put(in, new AsyncCallback<JavaScriptObject>() {
         @Override
         public void onSuccess(JavaScriptObject result) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagarePlugin.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagarePlugin.java
index 240c770..5b04b81 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagarePlugin.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagarePlugin.java
@@ -16,17 +16,35 @@
 
 import com.google.gerrit.plugin.client.Plugin;
 import com.google.gerrit.plugin.client.PluginEntryPoint;
+import com.google.gerrit.plugin.client.rpc.RestApi;
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 
 public class ImagarePlugin extends PluginEntryPoint {
   public static final Resources RESOURCES = GWT.create(Resources.class);
 
   @Override
   public void onPluginLoad() {
-    Plugin.get().screen("upload", new ImageUploadScreen.Factory());
-    Plugin.get().screen("settings", new ImagareAdminScreen.Factory());
-    Plugin.get().settingsScreen("preferences",
-        Plugin.get().getName() + " Preferences",
-        new ImagarePreferenceScreen.Factory());
+    new RestApi("config").id("server").view(Plugin.get().getPluginName(),
+        "config").get(new AsyncCallback<ConfigInfo>() {
+          @Override
+          public void onSuccess(ConfigInfo info) {
+            if (info.enableImageServer()) {
+              Plugin.get().screen("upload", new ImageUploadScreen.Factory());
+            }
+
+            Plugin.get().screen("settings",
+                new ImagareAdminScreen.Factory(info.enableImageServer()));
+            Plugin.get().settingsScreen("preferences",
+                Plugin.get().getName() + " Preferences",
+                new ImagarePreferenceScreen.Factory(info.enableImageServer()));
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            // never invoked
+          }
+        });
+
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagarePreferenceScreen.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagarePreferenceScreen.java
index 9d63ecd..bae87be 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagarePreferenceScreen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagarePreferenceScreen.java
@@ -24,28 +24,37 @@
 public class ImagarePreferenceScreen extends ImagareConfigScreen {
 
   static class Factory implements Screen.EntryPoint {
+    private final boolean enableImageServer;
+
+    Factory(boolean enableImageServer) {
+      this.enableImageServer = enableImageServer;
+    }
+
     @Override
     public void onLoad(Screen screen) {
       screen.setPageTitle("Imagare Preferences");
-      screen.show(new ImagarePreferenceScreen());
+      screen.show(new ImagarePreferenceScreen(enableImageServer));
     }
   }
 
-  ImagarePreferenceScreen() {
-    super(new RestApi("accounts").id("self")
-        .view(Plugin.get().getPluginName(), "preference"));
+  ImagarePreferenceScreen(boolean enableImageServer) {
+    super(enableImageServer,
+        new RestApi("accounts").id("self").view(
+            Plugin.get().getPluginName(), "preference"));
   }
 
   @Override
   protected void display(ConfigInfo info) {
-    HorizontalPanel p = new HorizontalPanel();
-    p.setStyleName("imagare-menu-panel");
-    Anchor prefsAnchor = new Anchor(new ImageResourceRenderer().render(
-        ImagarePlugin.RESOURCES.image()),
-        "#/x/" + Plugin.get().getPluginName() + "/upload");
-    prefsAnchor.setTitle("Upload Image");
-    p.add(prefsAnchor);
-    add(p);
+    if (enableImageServer) {
+      HorizontalPanel p = new HorizontalPanel();
+      p.setStyleName("imagare-menu-panel");
+      Anchor uploadAnchor = new Anchor(new ImageResourceRenderer().render(
+          ImagarePlugin.RESOURCES.image()),
+          "#/x/" + Plugin.get().getPluginName() + "/upload");
+      uploadAnchor.setTitle("Upload Image");
+      p.add(uploadAnchor);
+      add(p);
+    }
 
     super.display(info);
   }
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index c8fc4bd..9279cb3 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -5,9 +5,12 @@
 the uploaded image is available. This is useful for sharing screenshots
 and linking them from review comments.
 
+You can either [use the images server that comes with the plugin](#setup)
+or [configure an external image server](#external).
+
 <a id="setup"></a>
-Setup
------
+Setup of Image Server
+---------------------
 The uploaded images are stored in the `refs/images/*` namespace of the
 `All-Projects` project.
 
@@ -31,3 +34,24 @@
 To allow the deletion of any uploaded image the
 [Force Push](../../../Documentation/access-control.html#category_push)
 access right can be assigned on the images namespace.
+
+<a id="external"></a>
+Configure external Image Server
+-------------------------------
+
+To configure an external image server the image server that comes with
+the plugin must be disabled. This is done by setting the
+[enableImageServer](config.html#enable-image-server) to `false`.
+
+A regular expression to match URLs of images that should be embedded
+must be configured as [pattern](config.html#pattern).
+
+Optionally also a [URL for uploading images](config.html#upload-url)
+can be defined.
+
+```
+  [plugin "imagare"]
+    enableImageServer = false
+    pattern = http:\\/\\/i\\.imgur\\.com\\/.*\\.png
+    uploadUrl = http://imgur.com
+```
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index c001bb4..5fbbfc7 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -23,3 +23,27 @@
 <a id="stage">
 `plugin.@PLUGIN@.stage`
 :	Whether images should be staged before upload.
+
+<a id="enable-image-server">
+`plugin.@PLUGIN@.enableImageServer`
+:	Whether image server functionality should be enabled.
+    If `true` images can be uploaded to Gerrit.
+    When this parameter is changed the plugin must be reloaded.
+    By default `true`.
+
+<a id="pattern">
+`plugin.@PLUGIN@.pattern`
+:	URL pattern for image links. Matching images will be decorated.
+    The URL pattern is a JavaScript regexp, the following characters
+    must be escaped: `.`, `*`, `+`, `?`, `^`, `$`, `{`, `}`, `(`,
+    `)`, `|`, `[`, `]`, `/`, `\`
+    Images that match this pattern are rendered in the UI even when the
+    corresponding mime type is not configured as safe in the Gerrit
+    configuration.
+    Must be set if an external image server is used.
+
+<a id="upload-url">
+`plugin.@PLUGIN@.uploadUrl`
+:	Optional, URL for uploading images.
+    May be set if an external image server is used.
+    If Gerrit is used as image server the value is ignored.
diff --git a/src/main/resources/Documentation/rest-api-accounts.md b/src/main/resources/Documentation/rest-api-accounts.md
index 7ba476d..e1952b3 100644
--- a/src/main/resources/Documentation/rest-api-accounts.md
+++ b/src/main/resources/Documentation/rest-api-accounts.md
@@ -21,9 +21,8 @@
   GET /accounts/self/@PLUGIN@~preference HTTP/1.0
 ```
 
-As response a [ConfigInfo](rest-api-config.html#config-info) entity is
-returned that contains the preferences of a user for the @PLUGIN@
-plugin.
+As response a [PreferenceInfo](#preference-info) entity is returned
+that contains the preferences of a user for the @PLUGIN@ plugin.
 
 #### Response
 
@@ -35,7 +34,8 @@
   )]}'
   {
     "default_project": "All-Images",
-    "link_decoration": "INLINE"
+    "link_decoration": "INLINE",
+    "enable\_image_server": true
   }
 ```
 
@@ -44,7 +44,7 @@
 
 Sets the configuration of the @PLUGIN@ plugin.
 
-The new preferences must be specified as a [ConfigInfo](rest-api-config.html#config-info)
+The new preferences must be specified as a [PreferenceInfo](#preference-info)
 entity in the request body. Not setting a parameter means that the
 parameter is unset and that the global setting for this parameter
 applies again.
@@ -60,6 +60,24 @@
   }
 ```
 
+<a id="json-entities">JSON Entities
+-----------------------------------
+
+### <a id="preference-info"></a>PreferenceInfo
+
+The `PreferenceInfo` entity contains the configuration of the
+@PLUGIN@ plugin.
+
+* _default\_project_: The project to which images should be uploaded by
+  default.
+* _link\_decoration_: Decoration for image links in the Gerrit WebUI.
+  `NONE`: no decoration, `TOOLTIP`: the image is shown as tooltip on
+  mouse over an image link, `INLINE`: the image is inlined instead of
+  the URL.
+* _stage_: Whether images should be staged before upload.
+* _pattern_: JavaScript Regular expression to match URLs of images
+  that should be embedded (read-only).
+
 SEE ALSO
 --------
 
diff --git a/src/main/resources/Documentation/rest-api-config.md b/src/main/resources/Documentation/rest-api-config.md
index 23a959c..2d223cc 100644
--- a/src/main/resources/Documentation/rest-api-config.md
+++ b/src/main/resources/Documentation/rest-api-config.md
@@ -33,7 +33,8 @@
 
   )]}'
   {
-    "default_project": "All-Images"
+    "default_project": "All-Images",
+    "enable\_image_server": true
   }
 ```
 
@@ -72,6 +73,10 @@
   mouse over an image link, `INLINE`: the image is inlined instead of
   the URL.
 * _stage_: Whether images should be staged before upload.
+* _enable\_image\_server_: Whether Gerrit is used as image server.
+* _pattern_: JavaScript Regular expression to match URLs of images
+  that should be embedded.
+* _upload\_url_: URL to upload images.
 
 SEE ALSO
 --------
diff --git a/src/main/resources/Documentation/rest-api-projects.md b/src/main/resources/Documentation/rest-api-projects.md
index 83bf26e..9e738eb 100644
--- a/src/main/resources/Documentation/rest-api-projects.md
+++ b/src/main/resources/Documentation/rest-api-projects.md
@@ -22,6 +22,8 @@
 [Create Reference](../../../Documentation/access-control.html#category_create)
 access right on the `refs/images/*` namespace of the project.
 
+Only available if [image server is enabled](config.html#enable-image-server).
+
 #### Request
 
 ```
@@ -50,6 +52,8 @@
 granted the `Delete Own Images` global capability which allows deleting
 own images.
 
+Only available if [image server is enabled](config.html#enable-image-server).
+
 #### Request
 
 ```
diff --git a/src/main/resources/static/imagare.js b/src/main/resources/static/imagare.js
index 244350b..e9d0705 100644
--- a/src/main/resources/static/imagare.js
+++ b/src/main/resources/static/imagare.js
@@ -15,18 +15,22 @@
 Gerrit.install(function(self) {
     function onHistory(t) {
       Gerrit.get('/accounts/self/preference', function(r) {
+        if (!r.pattern) {
+          return;
+        }
+
         if ('TOOLTIP' === r.link_decoration) {
-          addTooltips();
+          addTooltips(r.pattern);
         } else if ('INLINE' === r.link_decoration) {
-          inlineImages();
+          inlineImages(r.pattern);
         }
       });
     }
 
-    function inlineImages() {
+    function inlineImages(pattern) {
       var l = document.links;
       for(var i = 0; i < l.length; i++) {
-        if (isImage(l[i].href)) {
+        if (l[i].href.match(pattern)) {
           var a = document.createElement('a');
           a.setAttribute('href', l[i].href);
           var img = document.createElement('img');
@@ -38,10 +42,10 @@
       }
     }
 
-    function addTooltips() {
+    function addTooltips(pattern) {
       var l = document.links;
       for(var i = 0; i < l.length; i++) {
-        if (isImage(l[i].href)) {
+        if (l[i].href.match(pattern)) {
           l[i].onmouseover = function (evt) {
             var img = document.createElement('img');
             img.setAttribute('src', this.href);
@@ -55,9 +59,5 @@
       }
     }
 
-    function isImage(href) {
-      return href.match(window.location.hostname + '.*project/.*/rev/.*/.*\.(jpg|jpeg|png|gif|bmp|ico|svg|tif|tiff)')
-    }
-
     Gerrit.on('history', onHistory);
   });