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 @@ WeBWorK using host: $SITE_URL - +
@@ -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); + } + }); 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 @@ - +