You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by er...@apache.org on 2013/12/03 01:55:25 UTC

svn commit: r1547251 - in /lucene/dev/trunk/solr: ./ core/src/java/org/apache/solr/handler/admin/ core/src/test-files/solr/collection1/conf/ core/src/test/org/apache/solr/cloud/ core/src/test/org/apache/solr/schema/ example/solr/collection1/conf/

Author: erick
Date: Tue Dec  3 00:55:25 2013
New Revision: 1547251

URL: http://svn.apache.org/r1547251
Log:
SOLR-5518: Move editing files to a separte request handler

Added:
    lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/admin/EditFileRequestHandler.java   (with props)
Modified:
    lucene/dev/trunk/solr/CHANGES.txt
    lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/admin/ShowFileRequestHandler.java
    lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/solrconfig-tlog.xml
    lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/solrconfig.xml
    lucene/dev/trunk/solr/core/src/test/org/apache/solr/cloud/TestModifyConfFiles.java
    lucene/dev/trunk/solr/core/src/test/org/apache/solr/schema/ModifyConfFileTest.java
    lucene/dev/trunk/solr/example/solr/collection1/conf/solrconfig.xml

Modified: lucene/dev/trunk/solr/CHANGES.txt
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/CHANGES.txt?rev=1547251&r1=1547250&r2=1547251&view=diff
==============================================================================
--- lucene/dev/trunk/solr/CHANGES.txt (original)
+++ lucene/dev/trunk/solr/CHANGES.txt Tue Dec  3 00:55:25 2013
@@ -117,6 +117,9 @@ New Features
 
 * SOLR-5506: Support docValues in CollationField and ICUCollationField.
   (Robert Muir)
+  
+ * SOLR-5518: Added EditFileRequestHandler to deal with security issues around modifying
+   solr configuration files.
 
 * SOLR-5023: Add support for deleteInstanceDir to be passed from SolrJ for Core
   Unload action. (Lyubov Romanchuk, shalin)

Added: lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/admin/EditFileRequestHandler.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/admin/EditFileRequestHandler.java?rev=1547251&view=auto
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/admin/EditFileRequestHandler.java (added)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/admin/EditFileRequestHandler.java Tue Dec  3 00:55:25 2013
@@ -0,0 +1,357 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.handler.admin;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.solr.cloud.ZkController;
+import org.apache.solr.cloud.ZkSolrResourceLoader;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.common.cloud.SolrZkClient;
+import org.apache.solr.common.util.ContentStream;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.Config;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.CoreDescriptor;
+import org.apache.solr.core.SolrConfig;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.handler.RequestHandlerBase;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.RawResponseWriter;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.xml.sax.InputSource;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * This handler uses the RawResponseWriter to give client access to
+ * files inside ${solr.home}/conf
+ * <p/>
+ * If you want to selectively restrict access some configuration files, you can list
+ * these files in the hidden invariants.  For example to hide
+ * synonyms.txt and anotherfile.txt, you would register:
+ * <p/>
+ * <pre>
+ * &lt;requestHandler name="/admin/fileupdate" class="org.apache.solr.handler.admin.EditFileRequestHandler" &gt;
+ *   &lt;lst name="defaults"&gt;
+ *    &lt;str name="echoParams"&gt;explicit&lt;/str&gt;
+ *   &lt;/lst&gt;
+ *   &lt;lst name="invariants"&gt;
+ *    &lt;str name="hidden"&gt;synonyms.txt&lt;/str&gt;
+ *    &lt;str name="hidden"&gt;anotherfile.txt&lt;/str&gt;
+ *    &lt;str name="hidden"&gt;*&lt;/str&gt;
+ *   &lt;/lst&gt;
+ * &lt;/requestHandler&gt;
+ * </pre>
+ * <p/>
+ * At present, there is only explicit file names (including path) or the glob '*' are supported. Variants like '*.xml'
+ * are NOT supported.ere
+ * <p/>
+ * <p/>
+ * The EditFileRequestHandler uses the {@link RawResponseWriter} (wt=raw) to return
+ * file contents.  If you need to use a different writer, you will need to change
+ * the registered invariant param for wt.
+ * <p/>
+ * If you want to override the contentType header returned for a given file, you can
+ * set it directly using: CONTENT_TYPE.  For example, to get a plain text
+ * version of schema.xml, try:
+ * <pre>
+ *   http://localhost:8983/solr/admin/fileedit?file=schema.xml&contentType=text/plain
+ * </pre>
+ *
+ * @since solr 4.7
+ *        <p/>
+ *        <p/>
+ *        You can use this handler to modify any files in the conf directory, e.g. solrconfig.xml
+ *        or schema.xml, or even in sub-directories (e.g. velocity/error.vm) by POSTing a file. Here's an example cURL command
+ *        <pre>
+ *                                            curl -X POST --form "fileupload=@schema.new" 'http://localhost:8983/solr/collection1/admin/fileedit?op=write&file=schema.xml'
+ *                                           </pre>
+ *
+ *        or
+ *        <pre>
+ *                                            curl -X POST --form "fileupload=@error.new" 'http://localhost:8983/solr/collection1/admin/file?op=write&file=velocity/error.vm'
+ *                                           </pre>
+ *
+ *        For the first iteration, this is probably going to be used from the Solr admin screen.
+ *
+ *        NOTE: Specifying a directory or simply leaving the any "file=XXX" parameters will list the contents of a directory.
+ *
+ *        NOTE: <b>You must reload the core/collection for any changes made via this handler to take effect!</b>
+ *
+ *        NOTE: <b>If the core does not load (say schema.xml is not well formed for instance) you may be unable to replace
+ *        the files with this interface.</b>
+ *
+ *        NOTE: <b>Leaving this handler enabled is a security risk! This handler should be disabled in all but trusted
+ *        (probably development only) environments!</b>
+ *
+ *        Configuration files in ZooKeeper are supported.
+ */
+public class EditFileRequestHandler extends RequestHandlerBase {
+
+  protected static final Logger log = LoggerFactory.getLogger(EditFileRequestHandler.class);
+
+  private final static String OP_PARAM = "op";
+  private final static String OP_WRITE = "write";
+  private final static String OP_TEST = "test";
+
+  ContentStream stream;
+  private byte[] data = null;
+  Set<String> hiddenFiles;
+
+  public EditFileRequestHandler() {
+    super();
+  }
+
+  @Override
+  public void init(NamedList args) {
+    super.init(args);
+    hiddenFiles = ShowFileRequestHandler.initHidden(invariants);
+  }
+
+  @Override
+  public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp)
+      throws InterruptedException, KeeperException, IOException {
+
+    CoreContainer coreContainer = req.getCore().getCoreDescriptor().getCoreContainer();
+    String op = req.getParams().get(OP_PARAM);
+    if (OP_WRITE.equalsIgnoreCase(op) || OP_TEST.equalsIgnoreCase(op)) {
+      String fname = req.getParams().get("file", null);
+      if (fname == null) {
+        rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "No file name specified for write operation."));
+      } else {
+        fname = fname.replace('\\', '/');
+        stream = getOneInputStream(req, rsp);
+        if (stream == null) {
+          return; // Error already in rsp.
+        }
+
+        data = IOUtils.toByteArray(new InputStreamReader(stream.getStream(), "UTF-8"), "UTF-8");
+
+        // If it's "solrconfig.xml", try parsing it as that object. Otherwise, if it ends in '.xml',
+        // see if it at least parses.
+        if ("solrconfig.xml".equals(fname)) {
+          try {
+            new SolrConfig("unused", new InputSource(new ByteArrayInputStream(data)));
+          } catch (Exception e) {
+            rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "Invalid solr config file: " + e.getMessage()));
+            return;
+          }
+        } else if (fname.endsWith(".xml")) { // At least do a rudimentary test, see if the thing parses.
+          try {
+            new Config(null, null, new InputSource(new ByteArrayInputStream(data)), null, false);
+          } catch (Exception e) {
+            rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "Invalid XML file: " + e.getMessage()));
+            return;
+          }
+        }
+        if (ShowFileRequestHandler.isHiddenFile(req, rsp, fname, true, hiddenFiles) == false) {
+          if (coreContainer.isZooKeeperAware()) {
+            writeToZooKeeper(req, rsp);
+          } else {
+            writeToFileSystem(req, rsp);
+          }
+        }
+      }
+    }
+  }
+
+  // write the file contained in the parameter "file=XXX" to ZooKeeper. The file may be a path, e.g.
+  // file=velocity/error.vm or file=schema.xml
+  //
+  // Important: Assumes that the file already exists in ZK, so far we aren't creating files there.
+  private void writeToZooKeeper(SolrQueryRequest req, SolrQueryResponse rsp)
+      throws KeeperException, InterruptedException, IOException {
+
+    CoreContainer coreContainer = req.getCore().getCoreDescriptor().getCoreContainer();
+    SolrZkClient zkClient = coreContainer.getZkController().getZkClient();
+
+    String adminFile = ShowFileRequestHandler.getAdminFileFromZooKeeper(req, rsp, zkClient, hiddenFiles);
+    String fname = req.getParams().get("file", null);
+    if (OP_TEST.equals(req.getParams().get(OP_PARAM))) {
+      testReloadSuccess(req, rsp);
+      return;
+    }
+    // Persist the managed schema
+    try {
+      // Assumption: the path exists
+      zkClient.setData(adminFile, data, true);
+      log.info("Saved " + fname + " to ZooKeeper successfully.");
+    } catch (KeeperException.BadVersionException e) {
+      log.error("Cannot save file: " + fname + " to Zookeeper, " +
+          "ZooKeeper error: " + e.getMessage());
+      rsp.setException(new SolrException(ErrorCode.SERVER_ERROR, "Cannot save file: " + fname + " to Zookeeper, " +
+          "ZooKeeper error: " + e.getMessage()));
+    }
+  }
+
+  // Used when POSTing the configuration files to Solr (either ZooKeeper or locally).
+  //
+  // It takes some effort to insure that there is one (and only one) stream provided, there's no provision for
+  // more than one stream at present.
+  private ContentStream getOneInputStream(SolrQueryRequest req, SolrQueryResponse rsp) {
+    String file = req.getParams().get("file");
+    if (file == null) {
+      log.error("You must specify a file for the write operation.");
+      rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "You must specify a file for the write operation."));
+      return null;
+    }
+
+    // Now, this is truly clumsy
+    Iterable<ContentStream> streams = req.getContentStreams();
+    if (streams == null) {
+      log.error("Input stream list was null for admin file write operation.");
+      rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "Input stream list was null for admin file write operation."));
+      return null;
+    }
+    Iterator<ContentStream> iter = streams.iterator();
+    if (!iter.hasNext()) {
+      log.error("No input streams were in the list for admin file write operation.");
+      rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "No input streams were in the list for admin file write operation."));
+      return null;
+    }
+    ContentStream stream = iter.next();
+    if (iter.hasNext()) {
+      log.error("More than one input stream was found for admin file write operation.");
+      rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "More than one input stream was found for admin file write operation."));
+      return null;
+    }
+    return stream;
+  }
+
+  // Write the data passed in from the stream to the file indicated by the file=XXX parameter on the local file system
+  private void writeToFileSystem(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {
+
+    File adminFile = ShowFileRequestHandler.getAdminFileFromFileSystem(req, rsp, hiddenFiles);
+    if (adminFile == null || adminFile.isDirectory()) {
+      String fname = req.getParams().get("file", null);
+
+      if (adminFile == null) {
+        log.error("File " + fname + " was not found.");
+        rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "File " + fname + " was not found."));
+        return;
+      }
+      log.error("File " + fname + " is a directory.");
+      rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "File " + fname + " is a directory."));
+      return;
+    }
+    if (OP_TEST.equals(req.getParams().get(OP_PARAM))) {
+      testReloadSuccess(req, rsp);
+      return;
+    }
+
+    FileUtils.copyInputStreamToFile(stream.getStream(), adminFile);
+    log.info("Successfully saved file " + adminFile.getAbsolutePath() + " locally");
+  }
+
+  private boolean testReloadSuccess(SolrQueryRequest req, SolrQueryResponse rsp) {
+    // Try writing the config to a temporary core and reloading to see that we don't allow people to shoot themselves
+    // in the foot.
+    File home = null;
+    try {
+      home = new File(FileUtils.getTempDirectory(), "SOLR_5459"); // Unlikely to name a core or collection this!
+      FileUtils.writeStringToFile(new File(home, "solr.xml"), "<solr></solr>", "UTF-8"); // Use auto-discovery
+      File coll = new File(home, "SOLR_5459");
+
+      SolrCore core = req.getCore();
+      CoreDescriptor desc = core.getCoreDescriptor();
+      CoreContainer coreContainer = desc.getCoreContainer();
+
+      if (coreContainer.isZooKeeperAware()) {
+        try {
+          String confPath = ((ZkSolrResourceLoader) core.getResourceLoader()).getCollectionZkPath();
+
+          ZkController.downloadConfigDir(coreContainer.getZkController().getZkClient(), confPath,
+              new File(coll, "conf"));
+        } catch (Exception ex) {
+          log.error("Error when attempting to download conf from ZooKeeper: " + ex.getMessage());
+          rsp.setException(new SolrException(ErrorCode.BAD_REQUEST,
+              "Error when attempting to download conf from ZooKeeper" + ex.getMessage()));
+          return false;
+        }
+      } else {
+        FileUtils.copyDirectory(new File(desc.getInstanceDir(), "conf"),
+            new File(coll, "conf"));
+      }
+
+      FileUtils.writeStringToFile(new File(coll, "core.properties"), "name=SOLR_5459", "UTF-8");
+
+      FileUtils.writeByteArrayToFile(new File(new File(coll, "conf"), req.getParams().get("file", null)), data);
+
+      return tryReloading(rsp, home);
+
+    } catch (IOException ex) {
+      log.warn("Caught IO exception when trying to verify configs. " + ex.getMessage());
+      rsp.setException(new SolrException(ErrorCode.SERVER_ERROR,
+          "Caught IO exception when trying to verify configs. " + ex.getMessage()));
+      return false;
+    } finally {
+      if (home != null) {
+        try {
+          FileUtils.deleteDirectory(home);
+        } catch (IOException e) {
+          log.warn("Caught IO exception trying to delete temporary directory " + home + e.getMessage());
+          return true; // Don't fail for this reason!
+        }
+      }
+    }
+  }
+
+  private boolean tryReloading(SolrQueryResponse rsp, File home) {
+    CoreContainer cc = null;
+    try {
+      cc = CoreContainer.createAndLoad(home.getAbsolutePath(), new File(home, "solr.xml"));
+      if (cc.getCoreInitFailures().size() > 0) {
+        for (Exception ex : cc.getCoreInitFailures().values()) {
+          log.error("Error when attempting to reload core: " + ex.getMessage());
+          rsp.setException(new SolrException(ErrorCode.BAD_REQUEST,
+              "Error when attempting to reload core after writing config" + ex.getMessage()));
+        }
+        return false;
+      }
+      return true;
+    } finally {
+      if (cc != null) {
+        cc.shutdown();
+      }
+    }
+  }
+
+  //////////////////////// SolrInfoMBeans methods //////////////////////
+
+  @Override
+  public String getDescription() {
+    return "Admin Config File -- update config files directly";
+  }
+
+  @Override
+  public String getSource() {
+    return "$URL: https://svn.apache.org/repos/asf/lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/admin/ShowFileRequestHandler.java $";
+  }
+}

Modified: lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/admin/ShowFileRequestHandler.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/admin/ShowFileRequestHandler.java?rev=1547251&r1=1547250&r2=1547251&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/admin/ShowFileRequestHandler.java (original)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/admin/ShowFileRequestHandler.java Tue Dec  3 00:55:25 2013
@@ -17,21 +17,17 @@
 
 package org.apache.solr.handler.admin;
 
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.io.IOUtils;
-import org.apache.solr.cloud.ZkController;
 import org.apache.solr.cloud.ZkSolrResourceLoader;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.common.cloud.SolrZkClient;
 import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
-import org.apache.solr.common.util.ContentStream;
+import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.ContentStreamBase;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.common.util.SimpleOrderedMap;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.core.CoreDescriptor;
 import org.apache.solr.core.SolrCore;
 import org.apache.solr.core.SolrResourceLoader;
 import org.apache.solr.handler.RequestHandlerBase;
@@ -46,12 +42,10 @@ import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.IOException;
-import java.io.InputStreamReader;
 import java.io.UnsupportedEncodingException;
 import java.net.URISyntaxException;
 import java.util.Date;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
@@ -94,46 +88,16 @@ import java.util.Set;
  *
  *
  * @since solr 1.3
- *
- *
- * As of Solr 4.7, you can use this handler to modify any files in the conf directory, e.g. solrconfig.xml
- * or schema.xml, or even in sub-directories (e.g. velocity/error.vm) by POSTing a file. Here's an example cURL command
- * <pre>
- *   curl -X POST --form "fileupload=@schema.new" 'http://localhost:8983/solr/collection1/admin/file?op=write&file=schema.xml'
- * </pre>
- *
- * or
- * <pre>
- * curl -X POST --form "fileupload=@error.new" 'http://localhost:8983/solr/collection1/admin/file?op=write&file=velocity/error.vm'
- * </pre>
- *
- * For the first iteration, this is probably going to be used from the Solr admin screen.
- *
- * NOTE: Specifying a directory or simply leaving the any "file=XXX" parameters will list the contents of a directory.
- *
- * NOTE: <b>You must reload the core/collection for any changes made via this handler to take effect!</b>
- *
- * NOTE: <b>If the core does not load (say schema.xml is not well formed for instance) you may be unable to replace
- * the files with this interface.</b>
- *
- * Configuration files in ZooKeeper are supported.
- *
- * Writing files out, @since solr 4.7
  */
 public class ShowFileRequestHandler extends RequestHandlerBase
 {
-
-  protected static final Logger log = LoggerFactory
-      .getLogger(ShowFileRequestHandler.class);
-
   public static final String HIDDEN = "hidden";
   public static final String USE_CONTENT_TYPE = "contentType";
-  
+
   protected Set<String> hiddenFiles;
 
-  private final static String OP_PARAM = "op";
-  private final static String OP_WRITE = "write";
-  private final static String OP_TEST = "test";
+  protected static final Logger log = LoggerFactory
+      .getLogger(ShowFileRequestHandler.class);
 
 
   public ShowFileRequestHandler()
@@ -144,161 +108,33 @@ public class ShowFileRequestHandler exte
   @Override
   public void init(NamedList args) {
     super.init( args );
+    hiddenFiles = initHidden(invariants);
+  }
 
+  public static Set<String> initHidden(SolrParams invariants) {
+
+    Set<String> hiddenRet = new HashSet<String>();
     // Build a list of hidden files
-    hiddenFiles = new HashSet<String>();
-    if( invariants != null ) {
-      String[] hidden = invariants.getParams( HIDDEN );
-      if( hidden != null ) {
-        for( String s : hidden ) {
-          hiddenFiles.add( s.toUpperCase(Locale.ROOT) );
+    if (invariants != null) {
+      String[] hidden = invariants.getParams(HIDDEN);
+      if (hidden != null) {
+        for (String s : hidden) {
+          hiddenRet.add(s.toUpperCase(Locale.ROOT));
         }
       }
     }
+    return hiddenRet;
   }
-  
-  public Set<String> getHiddenFiles()
-  {
-    return hiddenFiles;
-  }
-  
+
   @Override
   public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp)
       throws InterruptedException, KeeperException, IOException {
 
     CoreContainer coreContainer = req.getCore().getCoreDescriptor().getCoreContainer();
-    String op = req.getParams().get(OP_PARAM);
-    if (op == null) {
-      if (coreContainer.isZooKeeperAware()) {
-        showFromZooKeeper(req, rsp, coreContainer);
-      } else {
-        showFromFileSystem(req, rsp);
-      }
-    } else if (OP_WRITE.equalsIgnoreCase(op) || OP_TEST.equalsIgnoreCase(op)) {
-      String fname = req.getParams().get("file", null);
-      if (fname == null) {
-        rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "No file name specified for write operation."));
-      } else {
-        fname = fname.replace('\\', '/');
-        if (isHiddenFile(req, rsp, fname, true) == false) {
-          if (coreContainer.isZooKeeperAware()) {
-            writeToZooKeeper(req, rsp);
-          } else {
-            writeToFileSystem(req, rsp);
-          }
-        }
-      }
-    }
-  }
-
-  // See if we should deal with this file
-
-  private boolean isHiddenFile(SolrQueryRequest req, SolrQueryResponse rsp, String fnameIn, boolean reportError) {
-    String fname = fnameIn.toUpperCase(Locale.ROOT);
-    if (hiddenFiles.contains(fname) || hiddenFiles.contains("*")) {
-      if (reportError) {
-        log.error("Cannot access " + fname);
-        rsp.setException(new SolrException(ErrorCode.FORBIDDEN, "Can not access: " + fnameIn));
-      }
-      return true;
-    }
-
-    // This is slightly off, a valid path is something like ./schema.xml. I don't think it's worth the effort though
-    // to fix it to handle all possibilities though.
-    if (fname.indexOf("..") >= 0 || fname.startsWith(".")) {
-      if (reportError) {
-        log.error("Invalid path: " + fname);
-        rsp.setException(new SolrException(ErrorCode.FORBIDDEN, "Invalid path: " + fnameIn));
-      }
-      return true;
-    }
-
-    // Make sure that if the schema is managed, we don't allow editing. Don't really want to put
-    // this in the init since we're not entirely sure when the managed schema will get initialized relative to this
-    // handler.
-    SolrCore core = req.getCore();
-    IndexSchema schema = core.getLatestSchema();
-    if (schema instanceof ManagedIndexSchema) {
-      String managed = schema.getResourceName();
-
-      if (fname.equalsIgnoreCase(managed)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  // Refactored to be usable from multiple methods. Gets the path of the requested file from ZK.
-  // Returns null if the file is not found.
-  //
-  // Assumes that the file is in a parameter called "file".
-
-  private String getAdminFileFromZooKeeper(SolrQueryRequest req, SolrQueryResponse rsp, SolrZkClient zkClient)
-      throws KeeperException, InterruptedException {
-    String adminFile = null;
-    SolrCore core = req.getCore();
-
-    final ZkSolrResourceLoader loader = (ZkSolrResourceLoader) core
-        .getResourceLoader();
-    String confPath = loader.getCollectionZkPath();
-
-    String fname = req.getParams().get("file", null);
-    if (fname == null) {
-      adminFile = confPath;
+    if (coreContainer.isZooKeeperAware()) {
+      showFromZooKeeper(req, rsp, coreContainer);
     } else {
-      fname = fname.replace('\\', '/'); // normalize slashes
-      if (isHiddenFile(req, rsp, fname, true)) {
-        return null;
-      }
-      if (fname.startsWith("/")) { // Only files relative to conf are valid
-        fname = fname.substring(1);
-      }
-      adminFile = confPath + "/" + fname;
-    }
-
-    // Make sure the file exists, is readable and is not a hidden file
-    if (!zkClient.exists(adminFile, true)) {
-      log.error("Can not find: " + adminFile);
-      rsp.setException(new SolrException(ErrorCode.NOT_FOUND, "Can not find: "
-          + adminFile));
-      return null;
-    }
-
-    return adminFile;
-  }
-
-  // write the file contained in the parameter "file=XXX" to ZooKeeper. The file may be a path, e.g.
-  // file=velocity/error.vm or file=schema.xml
-  //
-  // Important: Assumes that the file already exists in ZK, so far we aren't creating files there.
-  private void writeToZooKeeper(SolrQueryRequest req, SolrQueryResponse rsp)
-      throws KeeperException, InterruptedException, IOException {
-
-    CoreContainer coreContainer = req.getCore().getCoreDescriptor().getCoreContainer();
-    SolrZkClient zkClient = coreContainer.getZkController().getZkClient();
-
-    String adminFile = getAdminFileFromZooKeeper(req, rsp, zkClient);
-    ContentStream stream = getOneInputStream(req, rsp);
-    if (stream == null) {
-      return; // Error already in rsp.
-    }
-
-    byte[] data = IOUtils.toByteArray(new InputStreamReader(stream.getStream(), "UTF-8"), "UTF-8");
-    String fname = req.getParams().get("file", null);
-    if (OP_TEST.equals(req.getParams().get(OP_PARAM)))  {
-      testReloadSuccess(req, rsp, stream);
-      return;
-    }
-    // Persist the managed schema
-    try {
-      // Assumption: the path exists
-      zkClient.setData(adminFile, data, true);
-      log.info("Saved " + fname + " to ZooKeeper successfully.");
-    } catch (KeeperException.BadVersionException e) {
-      log.error("Cannot save file: " + fname + " to Zookeeper, " +
-          "ZooKeeper error: " + e.getMessage());
-      rsp.setException(new SolrException(ErrorCode.SERVER_ERROR, "Cannot save file: " + fname + " to Zookeeper, " +
-          "ZooKeeper error: " + e.getMessage()));
+      showFromFileSystem(req, rsp);
     }
   }
 
@@ -309,7 +145,7 @@ public class ShowFileRequestHandler exte
 
     SolrZkClient zkClient = coreContainer.getZkController().getZkClient();
 
-    String adminFile = getAdminFileFromZooKeeper(req, rsp, zkClient);
+    String adminFile = getAdminFileFromZooKeeper(req, rsp, zkClient, hiddenFiles);
 
     if (adminFile == null) {
       return;
@@ -321,7 +157,7 @@ public class ShowFileRequestHandler exte
       
       NamedList<SimpleOrderedMap<Object>> files = new SimpleOrderedMap<SimpleOrderedMap<Object>>();
       for (String f : children) {
-        if (isHiddenFile(req, rsp, f, false)) {
+        if (isHiddenFile(req, rsp, f, false, hiddenFiles)) {
           continue;
         }
 
@@ -352,185 +188,9 @@ public class ShowFileRequestHandler exte
     rsp.setHttpCaching(false);
   }
 
-
-  // Used when POSTing the configuration files to Solr (either ZooKeeper or locally).
-  //
-  // It takes some effort to insure that there is one (and only one) stream provided, there's no provision for
-  // more than one stream at present.
-  private ContentStream getOneInputStream(SolrQueryRequest req, SolrQueryResponse rsp) {
-    String file = req.getParams().get("file");
-    if (file == null) {
-      log.error("You must specify a file for the write operation.");
-      rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "You must specify a file for the write operation."));
-      return null;
-    }
-
-    // Now, this is truly clumsy
-    Iterable<ContentStream> streams = req.getContentStreams();
-    if (streams == null) {
-      log.error("Input stream list was null for admin file write operation.");
-      rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "Input stream list was null for admin file write operation."));
-      return null;
-    }
-    Iterator<ContentStream> iter = streams.iterator();
-    if (!iter.hasNext()) {
-      log.error("No input streams were in the list for admin file write operation.");
-      rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "No input streams were in the list for admin file write operation."));
-      return null;
-    }
-    ContentStream stream = iter.next();
-    if (iter.hasNext()) {
-      log.error("More than one input stream was found for admin file write operation.");
-      rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "More than one input stream was found for admin file write operation."));
-      return null;
-    }
-    return stream;
-  }
-
-
-  // Write the data passed in from the stream to the file indicated by the file=XXX parameter on the local file system
-  private void writeToFileSystem(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {
-    ContentStream stream = getOneInputStream(req, rsp);
-    if (stream == null) {
-      return; // Error should already have been logged.
-    }
-
-    File adminFile = getAdminFileFromFileSystem(req, rsp);
-    if (adminFile == null || adminFile.isDirectory()) {
-      String fname = req.getParams().get("file", null);
-
-      if (adminFile == null) {
-        log.error("File " + fname + " was not found.");
-        rsp.setException(new SolrException( ErrorCode.BAD_REQUEST, "File " + fname + " was not found."));
-        return;
-      }
-      log.error("File " + fname + " is a directory.");
-      rsp.setException(new SolrException( ErrorCode.BAD_REQUEST, "File " + fname + " is a directory."));
-      return;
-    }
-    if (OP_TEST.equals(req.getParams().get(OP_PARAM))) {
-      testReloadSuccess(req, rsp, stream);
-      return;
-    }
-
-    FileUtils.copyInputStreamToFile(stream.getStream(), adminFile);
-    log.info("Successfully saved file " + adminFile.getAbsolutePath() + " locally");
-  }
-
-  private boolean testReloadSuccess(SolrQueryRequest req, SolrQueryResponse rsp, ContentStream stream) {
-    // Try writing the config to a temporary core and reloading to see that we don't allow people to shoot themselves
-    // in the foot.
-    File home = null;
-    try {
-      home = new File(FileUtils.getTempDirectory(), "SOLR_5459"); // Unlikely to name a core or collection this!
-      FileUtils.writeStringToFile(new File(home, "solr.xml"), "<solr></solr>", "UTF-8"); // Use auto-discovery
-      File coll = new File(home, "SOLR_5459");
-
-      SolrCore core = req.getCore();
-      CoreDescriptor desc = core.getCoreDescriptor();
-      CoreContainer coreContainer = desc.getCoreContainer();
-
-      if (coreContainer.isZooKeeperAware()) {
-        try {
-          String confPath = ((ZkSolrResourceLoader) core.getResourceLoader()).getCollectionZkPath();
-
-          ZkController.downloadConfigDir(coreContainer.getZkController().getZkClient(), confPath,
-              new File(coll, "conf"));
-        } catch (Exception ex) {
-          log.error("Error when attempting to download conf from ZooKeeper: " + ex.getMessage());
-          rsp.setException(new SolrException(ErrorCode.BAD_REQUEST,
-              "Error when attempting to download conf from ZooKeeper" + ex.getMessage()));
-          return false;
-        }
-      } else {
-        FileUtils.copyDirectory(new File(desc.getInstanceDir(), "conf"),
-            new File(coll, "conf"));
-      }
-
-      FileUtils.writeStringToFile(new File(coll, "core.properties"), "name=SOLR_5459", "UTF-8");
-
-      FileUtils.copyInputStreamToFile(stream.getStream(),
-          new File(new File(coll, "conf"), req.getParams().get("file", null)));
-
-      return tryReloading(rsp, home);
-
-    } catch (IOException ex) {
-      log.warn("Caught IO exception when trying to verify configs. " + ex.getMessage());
-      rsp.setException(new SolrException(ErrorCode.SERVER_ERROR,
-          "Caught IO exception when trying to verify configs. " + ex.getMessage()));
-      return false;
-    } finally {
-      if (home != null) {
-        try {
-          FileUtils.deleteDirectory(home);
-        } catch (IOException e) {
-          log.warn("Caught IO exception trying to delete temporary directory " + home + e.getMessage());
-          return true; // Don't fail for this reason!
-        }
-      }
-    }
-  }
-
-  private boolean tryReloading(SolrQueryResponse rsp, File home) {
-    CoreContainer cc = null;
-    try {
-      cc = CoreContainer.createAndLoad(home.getAbsolutePath(), new File(home, "solr.xml"));
-      if (cc.getCoreInitFailures().size() > 0) {
-        for (Exception ex : cc.getCoreInitFailures().values()) {
-          log.error("Error when attempting to reload core: " + ex.getMessage());
-          rsp.setException(new SolrException( ErrorCode.BAD_REQUEST,
-              "Error when attempting to reload core after writing config" + ex.getMessage()));
-        }
-        return false;
-      }
-      return true;
-    } finally {
-      if (cc != null) {
-        cc.shutdown();
-      }
-    }
-  }
-
-  // Find the file indicated by the "file=XXX" parameter or the root of the conf directory on the local
-  // file system. Respects all the "interesting" stuff around what the resource loader does to find files.
-  private File getAdminFileFromFileSystem(SolrQueryRequest req, SolrQueryResponse rsp) {
-    File adminFile = null;
-    final SolrResourceLoader loader = req.getCore().getResourceLoader();
-    File configdir = new File( loader.getConfigDir() );
-    if (!configdir.exists()) {
-      // TODO: maybe we should just open it this way to start with?
-      try {
-        configdir = new File( loader.getClassLoader().getResource(loader.getConfigDir()).toURI() );
-      } catch (URISyntaxException e) {
-        log.error("Can not access configuration directory!");
-        rsp.setException(new SolrException( ErrorCode.FORBIDDEN, "Can not access configuration directory!", e));
-        return null;
-      }
-    }
-    String fname = req.getParams().get("file", null);
-    if( fname == null ) {
-      adminFile = configdir;
-    }
-    else {
-      fname = fname.replace( '\\', '/' ); // normalize slashes
-      if( hiddenFiles.contains( fname.toUpperCase(Locale.ROOT) ) ) {
-        log.error("Can not access: "+ fname);
-        rsp.setException(new SolrException( ErrorCode.FORBIDDEN, "Can not access: "+fname ));
-        return null;
-      }
-      if( fname.indexOf( ".." ) >= 0 ) {
-        log.error("Invalid path: "+ fname);
-        rsp.setException(new SolrException( ErrorCode.FORBIDDEN, "Invalid path: "+fname ));
-        return null;
-      }
-      adminFile = new File( configdir, fname );
-    }
-    return adminFile;
-  }
-
   // Return the file indicated (or the directory listing) from the local file system.
   private void showFromFileSystem(SolrQueryRequest req, SolrQueryResponse rsp) {
-    File adminFile = getAdminFileFromFileSystem(req, rsp);
+    File adminFile = getAdminFileFromFileSystem(req, rsp, hiddenFiles);
 
     if (adminFile == null) { // exception already recorded
       return;
@@ -561,7 +221,7 @@ public class ShowFileRequestHandler exte
         String path = f.getAbsolutePath().substring( basePath );
         path = path.replace( '\\', '/' ); // normalize slashes
 
-        if (isHiddenFile(req, rsp, f.getName().replace('\\', '/'), false)) {
+        if (isHiddenFile(req, rsp, f.getName().replace('\\', '/'), false, hiddenFiles)) {
           continue;
         }
 
@@ -593,6 +253,127 @@ public class ShowFileRequestHandler exte
     rsp.setHttpCaching(false);
   }
 
+  //////////////////////// Static methods //////////////////////////////
+
+  public static boolean isHiddenFile(SolrQueryRequest req, SolrQueryResponse rsp, String fnameIn, boolean reportError,
+                                     Set<String> hiddenFiles) {
+    String fname = fnameIn.toUpperCase(Locale.ROOT);
+    if (hiddenFiles.contains(fname) || hiddenFiles.contains("*")) {
+      if (reportError) {
+        log.error("Cannot access " + fname);
+        rsp.setException(new SolrException(SolrException.ErrorCode.FORBIDDEN, "Can not access: " + fnameIn));
+      }
+      return true;
+    }
+
+    // This is slightly off, a valid path is something like ./schema.xml. I don't think it's worth the effort though
+    // to fix it to handle all possibilities though.
+    if (fname.indexOf("..") >= 0 || fname.startsWith(".")) {
+      if (reportError) {
+        log.error("Invalid path: " + fname);
+        rsp.setException(new SolrException(SolrException.ErrorCode.FORBIDDEN, "Invalid path: " + fnameIn));
+      }
+      return true;
+    }
+
+    // Make sure that if the schema is managed, we don't allow editing. Don't really want to put
+    // this in the init since we're not entirely sure when the managed schema will get initialized relative to this
+    // handler.
+    SolrCore core = req.getCore();
+    IndexSchema schema = core.getLatestSchema();
+    if (schema instanceof ManagedIndexSchema) {
+      String managed = schema.getResourceName();
+
+      if (fname.equalsIgnoreCase(managed)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  // Refactored to be usable from multiple methods. Gets the path of the requested file from ZK.
+  // Returns null if the file is not found.
+  //
+  // Assumes that the file is in a parameter called "file".
+
+  public static String getAdminFileFromZooKeeper(SolrQueryRequest req, SolrQueryResponse rsp, SolrZkClient zkClient,
+                                                 Set<String> hiddenFiles)
+      throws KeeperException, InterruptedException {
+    String adminFile = null;
+    SolrCore core = req.getCore();
+
+    final ZkSolrResourceLoader loader = (ZkSolrResourceLoader) core
+        .getResourceLoader();
+    String confPath = loader.getCollectionZkPath();
+
+    String fname = req.getParams().get("file", null);
+    if (fname == null) {
+      adminFile = confPath;
+    } else {
+      fname = fname.replace('\\', '/'); // normalize slashes
+      if (isHiddenFile(req, rsp, fname, true, hiddenFiles)) {
+        return null;
+      }
+      if (fname.startsWith("/")) { // Only files relative to conf are valid
+        fname = fname.substring(1);
+      }
+      adminFile = confPath + "/" + fname;
+    }
+
+    // Make sure the file exists, is readable and is not a hidden file
+    if (!zkClient.exists(adminFile, true)) {
+      log.error("Can not find: " + adminFile);
+      rsp.setException(new SolrException(SolrException.ErrorCode.NOT_FOUND, "Can not find: "
+          + adminFile));
+      return null;
+    }
+
+    return adminFile;
+  }
+
+
+  // Find the file indicated by the "file=XXX" parameter or the root of the conf directory on the local
+  // file system. Respects all the "interesting" stuff around what the resource loader does to find files.
+  public static File getAdminFileFromFileSystem(SolrQueryRequest req, SolrQueryResponse rsp,
+                                                Set<String> hiddenFiles) {
+    File adminFile = null;
+    final SolrResourceLoader loader = req.getCore().getResourceLoader();
+    File configdir = new File( loader.getConfigDir() );
+    if (!configdir.exists()) {
+      // TODO: maybe we should just open it this way to start with?
+      try {
+        configdir = new File( loader.getClassLoader().getResource(loader.getConfigDir()).toURI() );
+      } catch (URISyntaxException e) {
+        log.error("Can not access configuration directory!");
+        rsp.setException(new SolrException( SolrException.ErrorCode.FORBIDDEN, "Can not access configuration directory!", e));
+        return null;
+      }
+    }
+    String fname = req.getParams().get("file", null);
+    if( fname == null ) {
+      adminFile = configdir;
+    }
+    else {
+      fname = fname.replace( '\\', '/' ); // normalize slashes
+      if( hiddenFiles.contains( fname.toUpperCase(Locale.ROOT) ) ) {
+        log.error("Can not access: "+ fname);
+        rsp.setException(new SolrException( SolrException.ErrorCode.FORBIDDEN, "Can not access: "+fname ));
+        return null;
+      }
+      if( fname.indexOf( ".." ) >= 0 ) {
+        log.error("Invalid path: "+ fname);
+        rsp.setException(new SolrException( SolrException.ErrorCode.FORBIDDEN, "Invalid path: "+fname ));
+        return null;
+      }
+      adminFile = new File( configdir, fname );
+    }
+    return adminFile;
+  }
+
+  public final Set<String> getHiddenFiles() {
+    return hiddenFiles;
+  }
+
   //////////////////////// SolrInfoMBeans methods //////////////////////
 
   @Override

Modified: lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/solrconfig-tlog.xml
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/solrconfig-tlog.xml?rev=1547251&r1=1547250&r2=1547251&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/solrconfig-tlog.xml (original)
+++ lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/solrconfig-tlog.xml Tue Dec  3 00:55:25 2013
@@ -92,6 +92,11 @@
     </lst>
   </requestHandler>
 
+  <requestHandler name="/admin/fileedit" class="solr.admin.EditFileRequestHandler" >
+    <lst name="invariants">
+      <str name="hidden">bogus.txt</str>
+    </lst>
+  </requestHandler>
 
   <updateRequestProcessorChain name="distrib-dup-test-chain-explicit">
     <!-- explicit test using processors before and after distrib -->

Modified: lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/solrconfig.xml
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/solrconfig.xml?rev=1547251&r1=1547250&r2=1547251&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/solrconfig.xml (original)
+++ lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/solrconfig.xml Tue Dec  3 00:55:25 2013
@@ -240,6 +240,12 @@
     </lst>
   </requestHandler>
 
+  <requestHandler name="/admin/fileedit" class="solr.admin.EditFileRequestHandler" >
+    <lst name="invariants">
+      <str name="hidden">bogus.txt</str>
+    </lst>
+  </requestHandler>
+
   <!-- test query parameter defaults -->
   <requestHandler name="defaults" class="solr.StandardRequestHandler">
     <lst name="defaults">

Modified: lucene/dev/trunk/solr/core/src/test/org/apache/solr/cloud/TestModifyConfFiles.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test/org/apache/solr/cloud/TestModifyConfFiles.java?rev=1547251&r1=1547250&r2=1547251&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/test/org/apache/solr/cloud/TestModifyConfFiles.java (original)
+++ lucene/dev/trunk/solr/core/src/test/org/apache/solr/cloud/TestModifyConfFiles.java Tue Dec  3 00:55:25 2013
@@ -16,6 +16,8 @@ package org.apache.solr.cloud;
  * limitations under the License.
  */
 
+import org.apache.commons.io.FileUtils;
+import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.solrj.impl.HttpSolrServer;
 import org.apache.solr.client.solrj.request.QueryRequest;
 import org.apache.solr.common.cloud.SolrZkClient;
@@ -23,6 +25,8 @@ import org.apache.solr.common.params.Mod
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.common.util.SimpleOrderedMap;
 
+import java.io.File;
+
 public class TestModifyConfFiles extends AbstractFullDistribZkTestBase {
 
   public TestModifyConfFiles() {
@@ -38,7 +42,7 @@ public class TestModifyConfFiles extends
     params.set("op", "write");
     params.set("file", "schema.xml");
     QueryRequest request = new QueryRequest(params);
-    request.setPath("/admin/file");
+    request.setPath("/admin/fileedit");
     try {
       client.request(request);
       fail("Should have caught exception");
@@ -50,7 +54,7 @@ public class TestModifyConfFiles extends
     params.set("stream.body", "Testing rewrite of schema.xml file.");
     params.set("op", "test");
     request = new QueryRequest(params);
-    request.setPath("/admin/file");
+    request.setPath("/admin/fileedit");
     try {
       client.request(request);
       fail("Should have caught exception");
@@ -61,7 +65,7 @@ public class TestModifyConfFiles extends
     params.set("op", "write");
     params.set("file", "bogus.txt");
     request = new QueryRequest(params);
-    request.setPath("/admin/file");
+    request.setPath("/admin/fileedit");
     try {
       client.request(request);
       fail("Should have caught exception");
@@ -69,16 +73,29 @@ public class TestModifyConfFiles extends
       assertEquals(e.getMessage(), "Can not access: bogus.txt");
     }
 
+    try {
+      params.set("file", "schema.xml");
+      request = new QueryRequest(params);
+      request.setPath("/admin/fileedit");
+      client.request(request);
+      fail("Should have caught exception since it's mal-formed XML");
+    } catch (Exception e) {
+      assertTrue("Should have a sax parser exception here!",
+          e.getMessage().contains("Invalid XML file: org.xml.sax.SAXParseException"));
+    }
+
+    String top = SolrTestCaseJ4.TEST_HOME() + "/collection1/conf";
+    params.set("stream.body", FileUtils.readFileToString(new File(top, "schema-tiny.xml"), "UTF-8"));
     params.set("file", "schema.xml");
     request = new QueryRequest(params);
-    request.setPath("/admin/file");
+    request.setPath("/admin/fileedit");
 
     client.request(request);
 
     SolrZkClient zkClient = cloudClient.getZkStateReader().getZkClient();
     String contents = new String(zkClient.getData("/configs/conf1/schema.xml", null, null, true), "UTF-8");
 
-    assertTrue("Schema contents should have changed!", "Testing rewrite of schema.xml file.".equals(contents));
+    assertTrue("Schema contents should have changed!", contents.contains("<schema name=\"tiny\" version=\"1.1\">"));
 
     // Create a velocity/whatever node. Put a bit of data in it. See if you can change it.
     zkClient.makePath("/configs/conf1/velocity/test.vm", false, true);
@@ -86,7 +103,7 @@ public class TestModifyConfFiles extends
     params.set("stream.body", "Some bogus stuff for a test.");
     params.set("file", "velocity/test.vm");
     request = new QueryRequest(params);
-    request.setPath("/admin/file");
+    request.setPath("/admin/fileedit");
 
     client.request(request);
 

Modified: lucene/dev/trunk/solr/core/src/test/org/apache/solr/schema/ModifyConfFileTest.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test/org/apache/solr/schema/ModifyConfFileTest.java?rev=1547251&r1=1547250&r2=1547251&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/test/org/apache/solr/schema/ModifyConfFileTest.java (original)
+++ lucene/dev/trunk/solr/core/src/test/org/apache/solr/schema/ModifyConfFileTest.java Tue Dec  3 00:55:25 2013
@@ -21,6 +21,7 @@ import com.carrotsearch.randomizedtestin
 import org.apache.commons.codec.Charsets;
 import org.apache.commons.io.FileUtils;
 import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.client.solrj.request.QueryRequest;
 import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.util.ContentStream;
 import org.apache.solr.common.util.ContentStreamBase;
@@ -41,6 +42,7 @@ import java.util.ArrayList;
 
 public class ModifyConfFileTest extends SolrTestCaseJ4 {
   private File solrHomeDirectory = new File(TEMP_DIR, this.getClass().getName());
+
   @Rule
   public TestRule solrTestRules = RuleChain.outerRule(new SystemPropertiesRestoreRule());
 
@@ -69,26 +71,42 @@ public class ModifyConfFileTest extends 
 
       SolrCore core = cc.getCore("core1");
       SolrQueryResponse rsp = new SolrQueryResponse();
-      SolrRequestHandler handler = core.getRequestHandler("/admin/file");
+      SolrRequestHandler handler = core.getRequestHandler("/admin/fileedit");
 
       ModifiableSolrParams params = params("file","schema.xml", "op","write");
       core.execute(handler, new LocalSolrQueryRequest(core, params), rsp);
       assertEquals(rsp.getException().getMessage(), "Input stream list was null for admin file write operation.");
 
-      params = params("op", "write", "stream.body", "Testing rewrite of schema.xml file.");
+      params = params("op", "write");
       core.execute(handler, new LocalSolrQueryRequest(core, params), rsp);
       assertEquals(rsp.getException().getMessage(), "No file name specified for write operation.");
 
+      ArrayList<ContentStream> streams = new ArrayList<ContentStream>( 2 );
+      streams.add(new ContentStreamBase.StringStream("Testing rewrite of schema.xml file." ) );
 
       params = params("op", "write", "file", "bogus.txt");
-      core.execute(handler, new LocalSolrQueryRequest(core, params), rsp);
+      LocalSolrQueryRequest locReq = new LocalSolrQueryRequest(core, params);
+      locReq.setContentStreams(streams);
+      core.execute(handler, locReq, rsp);
       assertEquals(rsp.getException().getMessage(), "Can not access: bogus.txt");
 
-      ArrayList<ContentStream> streams = new ArrayList<ContentStream>( 2 );
-      streams.add( new ContentStreamBase.StringStream( "Testing rewrite of schema.xml file." ) );
+      String top = SolrTestCaseJ4.TEST_HOME() + "/collection1/conf";
+      String badConf = FileUtils.readFileToString(new File(top, "solrconfig-minimal.xml"), "UTF-8").replace("</dataDir>", "");
+
+      params = params("op", "write", "file", "solrconfig.xml");
+      locReq = new LocalSolrQueryRequest(core, params);
+      streams.clear();
+      streams.add(new ContentStreamBase.StringStream(badConf));
+      locReq.setContentStreams(streams);
+      core.execute(handler, locReq, rsp);
+      assertTrue("should have detected an error early!",
+          rsp.getException().getMessage().contains("\"dataDir\""));
+
+      assertTrue("should have detected an error early!",
+          rsp.getException().getMessage().contains("\"</dataDir>\""));
 
       params = params("op", "test", "file", "schema.xml", "stream.body", "Testing rewrite of schema.xml file.");
-      LocalSolrQueryRequest locReq = new LocalSolrQueryRequest(core, params);
+      locReq = new LocalSolrQueryRequest(core, params);
       locReq.setContentStreams(streams);
       core.execute(handler, locReq, rsp);
 
@@ -116,7 +134,8 @@ public class ModifyConfFileTest extends 
       streams.clear();
       params = params();
       locReq = new LocalSolrQueryRequest(core, params);
-      core.execute(handler, locReq, rsp);
+
+      core.execute(core.getRequestHandler("/admin/file"), locReq, rsp);
 
       NamedList<Object> res = rsp.getValues();
 

Modified: lucene/dev/trunk/solr/example/solr/collection1/conf/solrconfig.xml
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/example/solr/collection1/conf/solrconfig.xml?rev=1547251&r1=1547250&r2=1547251&view=diff
==============================================================================
--- lucene/dev/trunk/solr/example/solr/collection1/conf/solrconfig.xml (original)
+++ lucene/dev/trunk/solr/example/solr/collection1/conf/solrconfig.xml Tue Dec  3 00:55:25 2013
@@ -1139,6 +1139,19 @@
      </requestHandler>
     -->
 
+  <!--
+    Enabling this request handler (which is NOT a default part of the admin handler) will allow the Solr UI to edit
+    all the config files. This is intended for secure/development use ONLY! Leaving available and publically
+    accessible is a security vulnerability and should be done with extreme caution!
+  -->
+  <!--
+  <requestHandler name="/admin/fileedit" class="solr.admin.EditFileRequestHandler" >
+    <lst name="invariants">
+         <str name="hidden">synonyms.txt</str>
+         <str name="hidden">anotherfile.txt</str>
+    </lst>
+  </requestHandler>
+  -->
   <!-- ping/healthcheck -->
   <requestHandler name="/admin/ping" class="solr.PingRequestHandler">
     <lst name="invariants">