diff --git a/lib/RenderApp.pm b/lib/RenderApp.pm
index 02e88b2cc..f63095074 100644
--- a/lib/RenderApp.pm
+++ b/lib/RenderApp.pm
@@ -96,6 +96,8 @@ sub startup {
 		$r->any('/render-api/cat')->to('IO#catalog');
 		$r->any('/render-api/find')->to('IO#search');
 		$r->post('/render-api/upload')->to('IO#upload');
+		$r->delete('/render-api/remove')->to('IO#remove');
+		$r->post('/render-api/clone')->to('IO#clone');
 		$r->post('/render-api/sma')->to('IO#findNewVersion');
 		$r->post('/render-api/unique')->to('IO#findUniqueSeeds');
 		$r->post('/render-api/tags')->to('IO#setTags');
diff --git a/lib/RenderApp/Controller/FormatRenderedProblem.pm b/lib/RenderApp/Controller/FormatRenderedProblem.pm
index 777bc3e2e..a73928381 100755
--- a/lib/RenderApp/Controller/FormatRenderedProblem.pm
+++ b/lib/RenderApp/Controller/FormatRenderedProblem.pm
@@ -102,7 +102,7 @@ sub formatRenderedProblem {
 	my $problemHeadText       = $rh_result->{header_text}//'';  ##head_text vs header_text
 	my $problemPostHeaderText = $rh_result->{post_header_text}//'';
 	my $rh_answers            = $rh_result->{answers}//{};
-	my $answerOrder           = $rh_result->{flags}->{ANSWER_ENTRY_ORDER}; #[sort keys %{ $rh_result->{answers} }];
+	my $answerOrder           = $rh_result->{flags}->{ANSWER_ENTRY_ORDER}//[]; #[sort keys %{ $rh_result->{answers} }];
 	my $encoded_source        = $self->encoded_source//'';
 	my $sourceFilePath        = $self->{sourceFilePath}//'';
 	my $problemSourceURL      = $self->{inputs_ref}->{problemSourceURL};
@@ -166,9 +166,9 @@ sub formatRenderedProblem {
 	my $sessionJWT         = $self->{return_object}{sessionJWT} // '';
 
 	my $previewMode     = defined( $self->{inputs_ref}{previewAnswers} )     || 0;
-	my $checkMode       = defined( $self->{inputs_ref}{checkAnswers} )       || 0;
-	my $submitMode      = defined( $self->{inputs_ref}{submitAnswers} )      || 0;
+	# showCorrectMode needs more security -- ww2 uses want/can/will
 	my $showCorrectMode = defined( $self->{inputs_ref}{showCorrectAnswers} ) || 0;
+	my $submitMode = defined($self->{inputs_ref}{submitAnswers}) || $self->{inputs_ref}{answersSubmitted} || 0;
 
 	# problemUUID can be added to the request as a parameter.  It adds a prefix
 	# to the identifier used by the  format so that several different problems
@@ -180,6 +180,8 @@ sub formatRenderedProblem {
 		// $rh_result->{flags}{showPartialCorrectAnswers};
 	my $showSummary   = $self->{inputs_ref}{showSummary} // 1;    #default to show summary for the moment
 	my $formLanguage  = $self->{inputs_ref}{language}    // 'en';
+	my $showTable = $self->{inputs_ref}{hideAttemptsTable} ? 0 : 1;
+	my $showMessages = $self->{inputs_ref}{hideMessages} ? 0 : 1;
 	my $scoreSummary  = '';
 
 	my $COURSE_LANG_AND_DIR = get_lang_and_dir($formLanguage);
@@ -191,24 +193,27 @@ sub formatRenderedProblem {
 	my $PROBLEM_LANG_AND_DIR = join(" ", map { qq{$_="$PROBLEM_LANG_AND_DIR{$_}"} } keys %PROBLEM_LANG_AND_DIR);
 	my $mt = WeBWorK::Localize::getLangHandle($self->{inputs_ref}{language} // 'en');
 
-	my $tbl = WeBWorK::Utils::AttemptsTable->new(
-		$rh_answers,
-		answersSubmitted       => $self->{inputs_ref}{answersSubmitted}//0,
-		answerOrder            => $answerOrder//[],
-		displayMode            => $self->{inputs_ref}{displayMode},
-		showAnswerNumbers      => 0,
-		showAttemptAnswers     => 0,
-		showAttemptPreviews    => ($previewMode or $submitMode or $showCorrectMode),
-		showAttemptResults     => ($submitMode and $showPartialCorrectAnswers),
-		showCorrectAnswers     => ($showCorrectMode),
-		showMessages           => ($previewMode or $submitMode or $showCorrectMode),
-		showSummary            => ( ($showSummary and ($submitMode or $showCorrectMode) )//0 )?1:0,
-		maketext               => WeBWorK::Localize::getLoc($formLanguage//'en'),
-		summary                => $problemResult->{summary} //'', # can be set by problem grader???
-	);
-
-	my $answerTemplate = $tbl->answerTemplate;
-	$tbl->imgGen->render(body_text => \$answerTemplate) if $tbl->displayMode eq 'images';
+	my $answerTemplate = '';
+	if ($submitMode && $showTable) {
+		my $tbl = WeBWorK::Utils::AttemptsTable->new(
+			$rh_answers,
+			answersSubmitted    => 1,
+			answerOrder         => $answerOrder,
+			displayMode         => $displayMode,
+			showAnswerNumbers   => 0,
+			showAttemptAnswers  => 0,
+			showAttemptPreviews => 1,
+			showAttemptResults  => $showPartialCorrectAnswers,
+			showCorrectAnswers  => $showCorrectMode,
+			showMessages        => $showMessages,
+			showSummary         => $showSummary,
+			maketext            => WeBWorK::Localize::getLoc($formLanguage),
+			summary             => $problemResult->{summary} // '',                     # can be set by problem grader???
+		);
+
+		$answerTemplate = $tbl->answerTemplate;
+		$tbl->imgGen->render(body_text => \$answerTemplate) if $tbl->displayMode eq 'images';
+	}
 
 	# warn "imgGen is ", $tbl->imgGen;
 	#warn "answerOrder ", $tbl->answerOrder;
diff --git a/lib/RenderApp/Controller/IO.pm b/lib/RenderApp/Controller/IO.pm
index e13bb039d..af0e4c2d2 100644
--- a/lib/RenderApp/Controller/IO.pm
+++ b/lib/RenderApp/Controller/IO.pm
@@ -118,6 +118,91 @@ sub upload {
     return $c->render( text => 'File successfully uploaded', status => 200 );
 }
 
+sub remove {
+  my $c = shift;
+  my $required = [];
+  push @$required,
+    {
+      field     => 'removeFilePath',
+      checkType => 'like',
+      check     => $regex->{privateOnly},
+    };
+  my $validatedInput = $c->validateRequest( { required => $required } );
+  return unless $validatedInput;
+
+  my $file_path = $validatedInput->{removeFilePath};
+  my $file = Mojo::File->new($file_path);
+
+  return $c->render( text => 'Path does not exist', status => 404 )
+    unless (-e $file);
+
+  if (-d $file) {
+    return $c->render( text => 'Directory is not empty', status => 400 )
+      unless ($file->list({ dir => 1 })->size == 0);
+
+    $file->remove_tree;
+  } else {
+    $file->remove;
+  }
+
+  return $c->render( text => 'Path deleted' );
+}
+
+sub clone {
+  my $c = shift;
+  my $required = [];
+  push @$required,
+    {
+      field     => 'sourceFilePath',
+      checkType => 'like',
+      check     => $regex->{privateOnly},
+    };
+  push @$required,
+    {
+      field     => 'targetFilePath',
+      checkType => 'like',
+      check     => $regex->{privateOnly},
+    };
+  my $validatedInput = $c->validateRequest( { required => $required } );
+  return unless $validatedInput;
+
+  my $source_path = $validatedInput->{sourceFilePath};
+  my $source_file = Mojo::File->new($source_path);
+  my $target_path = $validatedInput->{targetFilePath};
+  my $target_file = Mojo::File->new($target_path);
+  
+  return $c->render( text => 'source does not exist', status => 404 )
+    unless (-e $source_file);
+
+  return $c->render( text => 'target already exists', status => 400 )
+    if (-e $target_file);
+
+  # allow cloning of directories - problems with static assets
+  # no recursing through directories!
+  if (-d $source_file) {
+    return $c->render( text => 'source does not contain clone-able files', status => 400)
+      if ($source_file->list->size == 0);
+
+    return $c->render( text => 'target must also be a directory', status => 400)
+      unless ($target_path =~ m!.*/$!);
+    
+    $target_file->make_path;
+    for ($source_file->list->each) {
+      $_->copy_to($target_path . $_->basename);
+    }
+  } else {
+    return $c->render( text => 'you may not create new directories with this method', status => 400)
+      unless (-e $target_file->dirname);
+
+    return($c->render( text => 'file extensions do not match'))
+      unless ($source_file->extname eq $target_file->extname);
+
+    $source_file->copy_to($target_file);
+  }
+
+  return $c->render( text => 'clone successful' );
+}
+
 async sub catalog {
     my $c = shift;
     my $required = [];
@@ -169,13 +254,12 @@ sub depthSearch_p {
             my $wanted = sub {
                 # measure depth relative to root_path
                 ( my $rel = $File::Find::name ) =~ s!^\Q$root_path\E/?!!;
+                return unless $rel;
                 my $path = $File::Find::name;
                 $File::Find::prune = 1
                   if File::Spec::Functions::splitdir($rel) >= $depth;
                 $path = $path . '/' if -d $File::Find::name;
-                # only report .pg files and directories
-                $all{$rel} = $path
-                  if ( $rel =~ /\S/ && ( $path =~ m!.+/$! || $path =~ m!.+\.pg$! ) );
+                $all{$rel} = $path;
             };
             File::Find::find { wanted => $wanted, no_chdir => 1 }, $root_path;
             return \%all, 200;
diff --git a/lib/RenderApp/Controller/Render.pm b/lib/RenderApp/Controller/Render.pm
index 62de9e173..6e4966a16 100644
--- a/lib/RenderApp/Controller/Render.pm
+++ b/lib/RenderApp/Controller/Render.pm
@@ -154,24 +154,26 @@ async sub problem {
         my $response = shift->result;
 
         $answerJWTresponse->{status} = int($response->code);
-        if ($response->is_success) {
+        # answerURL responses are expected to be JSON
+        if ($response->json) {
+          # munge data with default response object
+          $answerJWTresponse = { %$answerJWTresponse, %{$response->json} };
+        } else {
+          # otherwise throw the whole body as the message
           $answerJWTresponse->{message} = $response->body;
         }
-        elsif ($response->is_error) {
-          $answerJWTresponse->{message} = '[' . $c->logID . '] ' . $response->message;
-        }
-
-        $answerJWTresponse->{message} =~ s/"/\\"/g;
-        $answerJWTresponse->{message} =~ s/'/\'/g;
       })->
       catch(sub {
-        my $response = shift;
-        $c->log->error($response);
+        my $err = shift;
+        $c->log->error($err);
 
         $answerJWTresponse->{status} = 500;
-        $answerJWTresponse->{message} = '[' . $c->logID . '] ' . $response;
+        $answerJWTresponse->{message} = '[' . $c->logID . '] ' . $err;
       });
+
     $answerJWTresponse = encode_json($answerJWTresponse);
+    # this will become a string literal, so single-quote characters must be escaped
+    $answerJWTresponse =~ s/'/\\'/g;
     $c->log->info("answerJWT response ".$answerJWTresponse);
 
     $ww_return_hash->{renderedHTML} =~ s/JWTanswerURLstatus/$answerJWTresponse/g;
diff --git a/lib/WebworkClient/jwe_secure_format.pl b/lib/WebworkClient/jwe_secure_format.pl
index c13914fb9..5511cbb4b 100644
--- a/lib/WebworkClient/jwe_secure_format.pl
+++ b/lib/WebworkClient/jwe_secure_format.pl
@@ -31,7 +31,7 @@
 
 <title>WeBWorK using host: $SITE_URL</title>
 </head>
-<body>
+<body onLoad="window.parent.postMessage('loaded', '*')" >
   <div class="container-fluid">
     <div class="row">
       <div class="col-12 problem">
@@ -56,6 +56,47 @@
       console.log("response message ", JSON.parse('JWTanswerURLstatus'));
       window.parent.postMessage('JWTanswerURLstatus', '*');
     }
+
+    window.addEventListener('message', event => {
+      let message;
+      try {
+        message = JSON.parse(event.data);
+      } 
+      catch (e) {
+        return;
+      }
+      
+      if (message.hasOwnProperty('elements')) {
+        message.elements.forEach((incoming) => {
+          let elements;
+          if (incoming.hasOwnProperty('selector')) {
+            elements = window.document.querySelectorAll(incoming.selector);
+            if (incoming.hasOwnProperty('style')) {
+              elements.forEach(el => {el.style.cssText = incoming.style});
+            }
+            if (incoming.hasOwnProperty('class')) {
+              elements.forEach(el => {el.className = incoming.class});
+            }
+          }
+        });
+        event.source.postMessage('updated elements', event.origin);
+      }
+
+      if (message.hasOwnProperty('templates')) {
+        message.templates.forEach((cssString) => {
+          const element = document.createElement('style');
+          element.innerText = cssString;
+          document.head.insertAdjacentElement('beforeend', element);
+        });
+        event.source.postMessage('updated templates', event.origin);
+      }
+
+      if (message.hasOwnProperty('showSolutions')) {
+        const elements = Array.from(window.document.querySelectorAll('.knowl[data-type="solution"]'));
+        const solutions = elements.map(el => el.dataset.knowlContents);
+        event.source.postMessage(JSON.stringify({solutions: solutions}), event.origin);
+      }
+    });
   </script>
 </body>
 </html>
diff --git a/lib/WebworkClient/standard_format.pl b/lib/WebworkClient/standard_format.pl
index b1fdd507c..bb9f2212c 100644
--- a/lib/WebworkClient/standard_format.pl
+++ b/lib/WebworkClient/standard_format.pl
@@ -68,7 +68,7 @@
 
     <input type="submit" name="previewAnswers" class="btn btn-primary" value="$STRING_Preview" />
     <input type="submit" name="submitAnswers" class="btn btn-primary" value="$STRING_Submit"/>
-    <input type="submit" name="showCorrectAns" class="btn btn-primary" value="$STRING_ShowCorrect"/>
+    <input type="submit" name="showCorrectAnswers" class="btn btn-primary" value="$STRING_ShowCorrect"/>
   </p>
 </form>
 <HR>