From d6d50625af83883cb019918fc18303a0f8b61a34 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Fri, 20 Jun 2025 14:13:59 -0700 Subject: [PATCH] Get rid of ENV hacking and fix the baseURL setting. Add `getAssetURL` as a helper in the `RenderApp::Plugin::Assets` plugin module. This method should be used for any javascript or css files to ensure that the baseURL setting is correctly utilized. The language used for determining if a right-to-left css file is needed is an optional last argument. When rendering the `language` form parameter can set this, and in all cases the fall back is the `language` set in the app configuration. The only environment variables that are set are those that are needed for rendering. The renderer code itself should generally pass the controller object in where needed and configuration values should be extracted from there instead of setting environment variables corresponding to each configuration value. Also move the public/js/apps directories directly in public/js. This was done for webwork2 and PG a while ago, and should be done here as well. --- lib/Renderer.pm | 153 +++++++++--------- lib/Renderer/Controller/IO.pm | 15 +- lib/Renderer/Controller/Render.pm | 28 ++-- lib/Renderer/Model/Problem.pm | 41 +++-- lib/Renderer/Plugin/Assets.pm | 120 ++++++++++++++ lib/WeBWorK/FormatRenderedProblem.pm | 36 ++--- lib/WeBWorK/PreTeXt.pm | 5 +- lib/WeBWorK/RenderProblem.pm | 83 +++------- lib/WeBWorK/Utils.pm | 118 +------------- public/generate-assets.js | 4 +- .../js/{apps => }/CSSMessage/css-message.js | 0 .../MathJaxConfig/mathjax-config.js | 0 public/js/{apps => }/Problem/problem.js | 6 +- public/js/{apps => }/Problem/submithelper.js | 0 renderer.conf.dist | 3 + templates/RPCRenderFormats/default.html.ep | 2 +- templates/columns/editorIframe.html.ep | 2 +- templates/columns/editorUI.html.ep | 2 +- templates/columns/filebrowser.html.ep | 8 +- templates/columns/oplIframe.html.ep | 2 +- templates/columns/tags.html.ep | 14 +- templates/exception.html.ep | 9 +- templates/layouts/navbar.html.ep | 6 +- templates/pages/flex.html.ep | 4 +- templates/pages/oplUI.html.ep | 2 +- templates/pages/twocolumn.html.ep | 2 +- 26 files changed, 317 insertions(+), 348 deletions(-) create mode 100644 lib/Renderer/Plugin/Assets.pm rename public/js/{apps => }/CSSMessage/css-message.js (100%) rename public/js/{apps => }/MathJaxConfig/mathjax-config.js (100%) rename public/js/{apps => }/Problem/problem.js (97%) rename public/js/{apps => }/Problem/submithelper.js (100%) diff --git a/lib/Renderer.pm b/lib/Renderer.pm index 0cb1d6c3a..aaca5e2a0 100644 --- a/lib/Renderer.pm +++ b/lib/Renderer.pm @@ -1,30 +1,16 @@ package Renderer; use Mojo::Base 'Mojolicious'; -BEGIN { - use Mojo::File; - $main::libname = Mojo::File::curfile->dirname; - - # RENDER_ROOT is required for initializing conf files. - $ENV{RENDER_ROOT} = $main::libname->dirname - unless (defined($ENV{RENDER_ROOT})); - - # PG_ROOT is required for PG/lib/PGEnvironment.pm - $ENV{PG_ROOT} = $main::libname . '/PG'; +use Mojo::File; +use Env qw(RENDER_ROOT PG_ROOT baseURL); +use Date::Format; - # Used for reconstructing library paths from sym-links. - $ENV{OPL_DIRECTORY} = "$ENV{RENDER_ROOT}/webwork-open-problem-library"; - - $ENV{MOJO_CONFIG} = - (-r "$ENV{RENDER_ROOT}/renderer.conf") - ? "$ENV{RENDER_ROOT}/renderer.conf" - : "$ENV{RENDER_ROOT}/renderer.conf.dist"; - $ENV{MOJO_LOG_LEVEL} = 'debug'; +BEGIN { + # RENDER_ROOT and PG_ROOT are required for the WeBWorK::PG::Environment. + $RENDER_ROOT = Mojo::File::curfile->dirname->dirname; + $PG_ROOT = Mojo::File::curfile->dirname->child('PG'); } -use lib "$main::libname"; -print "using root directory: $ENV{RENDER_ROOT}\n"; - use Renderer::Model::Problem; use Renderer::Controller::IO; use WeBWorK::FormatRenderedProblem; @@ -32,18 +18,18 @@ use WeBWorK::FormatRenderedProblem; sub startup { my $self = shift; - # Merge environment variables with config file $self->plugin('Config'); - $self->plugin('TagHelpers'); $self->secrets($self->config('secrets')); - for (qw(problemJWTsecret webworkJWTsecret baseURL formURL SITE_HOST STRICT_JWT)) { - $ENV{$_} //= $self->config($_); - } - sanitizeHostURLs(); + $self->sanitizeHostURLs; + + # This is also required for the WeBWorK::PG::Environment, but is not needed at compile time. + $baseURL = $self->config->{baseURL}; - print "Renderer is based at $main::basehref\n"; - print "Problem attempts will be sent to $main::formURL\n"; + say 'Renderer is based at ' . $self->defaults->{baseHREF}; + say 'Problem attempts will be sent to ' . $self->defaults->{formURL}; + + $self->plugin('Renderer::Plugin::Assets'); # Handle optional CORS settings if (my $CORS_ORIGIN = $self->config('CORS_ORIGIN')) { @@ -63,8 +49,8 @@ sub startup { # Logging if ($ENV{MOJO_MODE} && $ENV{MOJO_MODE} eq 'production') { - my $logPath = "$ENV{RENDER_ROOT}/logs/error.log"; - print "[LOGS] Running in production mode, logging to $logPath\n"; + my $logPath = $self->home->child('logs', 'error.log'); + say "[LOGS] Running in production mode, logging to $logPath"; $self->log(Mojo::Log->new( path => $logPath, level => ($ENV{MOJO_LOG_LEVEL} || 'warn') @@ -72,33 +58,53 @@ sub startup { } if ($self->config('INTERACTION_LOG')) { - my $interactionLogPath = "$ENV{RENDER_ROOT}/logs/interactions.log"; - print "[LOGS] Saving interactions to $interactionLogPath\n"; + my $interactionLogPath = $self->home->child('logs', 'interactions.log'); + say "[LOGS] Saving interactions to $interactionLogPath"; my $resultsLog = Mojo::Log->new(path => $interactionLogPath, level => 'info'); $resultsLog->format(sub { my ($time, $level, @lines) = @_; my $start = shift(@lines); - my $msg = join ", ", @lines; - return sprintf "%s, %s, %s\n", $start, $time - $start, $msg; + return sprintf "%s, %s, %s\n", $start, $time - $start, join(', ', @lines); }); $self->helper(logAttempt => sub { shift; $resultsLog->info(@_); }); } + my $resourceUsageLog = Mojo::Log->new(path => $self->home->child('logs', 'resource_usage.log')); + $resourceUsageLog->format(sub { + my ($time, $level, @lines) = @_; + return '[' . time2str('%a %b %d %H:%M:%S %Y', time) . '] ' . join(', ', @lines) . "\n"; + }); + $self->helper(resourceUsageLog => sub { shift; return $resourceUsageLog->info(@_); }); + # Models - $self->helper(newProblem => sub { shift; Renderer::Model::Problem->new(@_) }); + $self->helper(newProblem => sub { my ($c, $args) = @_; Renderer::Model::Problem->new($c, $args) }); # Helpers - $self->helper(format => sub { WeBWorK::FormatRenderedProblem::formatRenderedProblem(@_) }); - $self->helper(validateRequest => sub { Renderer::Controller::IO::validate(@_) }); - $self->helper(parseRequest => sub { Renderer::Controller::Render::parseRequest(@_) }); - $self->helper(croak => sub { Renderer::Controller::Render::croak(@_) }); - $self->helper(logID => sub { shift->req->request_id }); - $self->helper(exception => sub { Renderer(@_) }); + $self->helper( + format => sub { + my ($c, $rh_result) = @_; + WeBWorK::FormatRenderedProblem::formatRenderedProblem($c, $rh_result); + } + ); + $self->helper(validateRequest => sub { my ($c, $options) = @_; Renderer::Controller::IO::validate($c, $options) }); + $self->helper(parseRequest => sub { my $c = shift; Renderer::Controller::Render::parseRequest($c) }); + $self->helper( + croak => sub { + my ($c, $exception, $depth) = @_; + Renderer::Controller::Render::croak($c, $exception, $depth); + } + ); + $self->helper(logID => sub { my $c = shift; $c->req->request_id }); + $self->helper( + exception => sub { + my ($c, $message, $status, %data) = @_; + Renderer::Controller::Render::exception($c, $message, $status, %data); + } + ); # Routes - # baseURL sets the root at which the renderer is listening, - # and is used in Environment for pg_root_url - my $r = $self->routes->under($ENV{baseURL}); + # baseURL is the root at which the renderer is listening. + my $r = $self->routes->under($self->config->{baseURL}); $r->any('/render-api')->to('render#problem'); $r->any('/render-ptx')->to('render#render_ptx'); @@ -108,10 +114,12 @@ sub startup { supplementalRoutes($r) if ($self->mode eq 'development' || $self->config('FULL_APP_INSECURE')); # Static file routes - $r->any('/pg_files/CAPA_Graphics/*static')->to('StaticFiles#CAPA_graphics_file'); - $r->any('/pg_files/tmp/*static')->to('StaticFiles#temp_file'); - $r->any('/pg_files/*static')->to('StaticFiles#pg_file'); - $r->any('/*static')->to('StaticFiles#public_file'); + $r->any('/pg_files/CAPA_Graphics/*static')->to('StaticFiles#CAPA_graphics_file')->name('capaFile'); + $r->any('/pg_files/tmp/*static')->to('StaticFiles#temp_file')->name('pgTempFile'); + $r->any('/pg_files/*static')->to('StaticFiles#pg_file')->name('pgFile'); + $r->any('/*static')->to('StaticFiles#public_file')->name('publicFile'); + + return; } sub supplementalRoutes { @@ -156,45 +164,40 @@ sub timeout { } sub sanitizeHostURLs { - $ENV{SITE_HOST} =~ s!/$!!; - - # set an absolute base href for asset urls under iframe embedding - if ($ENV{baseURL} =~ m!^https?://!) { + my $self = shift; - # this should only be used by MITM sites when proxying renderer assets - my $baseURL = $ENV{baseURL} =~ m!/$! ? $ENV{baseURL} : "$ENV{baseURL}/"; - $main::basehref = Mojo::URL->new($baseURL); + $self->config->{SITE_HOST} =~ s!/$!!; - # do NOT use the proxy address in our router! - $ENV{baseURL} = ''; - } elsif ($ENV{baseURL} =~ m!\S!) { + # Set an absolute base href for asset urls under iframe embedding. + if ($self->config->{baseURL} =~ m!^https?://!) { + # This should only be used by MITM sites when proxying renderer assets. + my $baseURL = $self->config->{baseURL} =~ m!/$! ? $self->config->{baseURL} : $self->config->{baseURL} . '/'; + $self->defaults->{baseHREF} = Mojo::URL->new($baseURL); - # ENV{baseURL} is used to build routes, so configure as "/extension" - $ENV{baseURL} = "/$ENV{baseURL}"; - warn "*** [CONFIG] baseURL should not end in a slash\n" - if $ENV{baseURL} =~ s!/$!!; - warn "*** [CONFIG] baseURL should begin with a slash\n" - unless $ENV{baseURL} =~ s!^//!/!; + # Do NOT use the proxy address for the router! + $self->config->{baseURL} = ''; + } elsif ($self->config->{baseURL} =~ m!\S!) { + # Ensure baseURL starts with a slash but doesn't end with a slash. + $self->config->{baseURL} = '/' . $self->config->{baseURL} unless $self->config->{baseURL} =~ m!^/!; + $self->config->{baseURL} =~ s!/$!!; - # base href must end in a slash when not hosting at the root - $main::basehref = - Mojo::URL->new($ENV{SITE_HOST})->path("$ENV{baseURL}/"); + # base href must end in a slash when not hosting at the root. + $self->defaults->{baseHREF} = Mojo::URL->new($self->config->{SITE_HOST})->path($self->config->{baseURL} . '/'); } else { # no proxy and service is hosted at the root of SITE_HOST - $main::basehref = Mojo::URL->new($ENV{SITE_HOST}); + $self->defaults->{baseHREF} = Mojo::URL->new($self->config->{SITE_HOST}); } - if ($ENV{formURL} =~ m!\S!) { - + if ($self->config->{formURL} =~ m!\S!) { # this should only be used by MITM - $main::formURL = Mojo::URL->new($ENV{formURL}); + $self->defaults->{formURL} = Mojo::URL->new($self->config->{formURL}); die '*** [CONFIG] if provided, formURL must be absolute' - unless $main::formURL->is_abs; + unless $self->defaults->{formURL}->is_abs; } else { # if using MITM proxy base href + renderer api not at SITE_HOST root # provide form url as absolute SITE_HOST/extension/render-api - $main::formURL = - Mojo::URL->new($ENV{SITE_HOST})->path("$ENV{baseURL}/render-api"); + $self->defaults->{formURL} = + Mojo::URL->new($self->config->{SITE_HOST})->path($self->config->{baseURL} . '/render-api'); } } diff --git a/lib/Renderer/Controller/IO.pm b/lib/Renderer/Controller/IO.pm index 7fbc510c4..d5688e068 100644 --- a/lib/Renderer/Controller/IO.pm +++ b/lib/Renderer/Controller/IO.pm @@ -1,6 +1,6 @@ package Renderer::Controller::IO; -use Mojo::Base -async_await; -use Mojo::Base 'Mojolicious::Controller'; +use Mojo::Base 'Mojolicious::Controller', -async_await; + use File::Spec::Functions qw(splitdir); use File::Find qw(find); use MIME::Base64 qw(decode_base64); @@ -37,7 +37,7 @@ sub raw { return unless $validatedInput; my $file_path = $validatedInput->{sourceFilePath}; - my $problem = $c->newProblem({ log => $c->log, read_path => $file_path }); + my $problem = $c->newProblem({ read_path => $file_path }); $problem->{action} = 'fetch source'; return $c->exception($problem->{_message}, $problem->{status}) unless $problem->success(); @@ -58,7 +58,6 @@ async sub writer { return unless $validatedInput; my $problem = $c->newProblem({ - log => $c->log, write_path => $validatedInput->{writeFilePath}, problem_contents => $validatedInput->{problemSource} }); @@ -362,7 +361,7 @@ async sub findNewVersion { my $avoidProblems = {}; $c->render_later; for my $seed (@avoidSeeds) { - my $problem = $c->newProblem({ log => $c->log, read_path => $filePath, random_seed => $seed }); + my $problem = $c->newProblem({ read_path => $filePath, random_seed => $seed }); my $renderedProblem = await $problem->render({}); next unless ($problem->success()); $avoidProblems->{$seed} = decode_json($renderedProblem); @@ -375,7 +374,7 @@ async sub findNewVersion { $newSeed = 1 + int rand(999999); } until (!exists($avoidProblems->{$newSeed})); - my $newProblemObj = $c->newProblem({ log => $c->log, read_path => $filePath, random_seed => $newSeed }); + my $newProblemObj = $c->newProblem({ read_path => $filePath, random_seed => $newSeed }); my $newProblemJson = await $newProblemObj->render({}); next unless ($newProblemObj->success()); $newProblem = decode_json($newProblemJson); @@ -453,7 +452,7 @@ async sub findUniqueSeeds { do { $newSeed = 1 + int rand(999999); } until (!exists($triedSeeds->{$newSeed})); - my $newProblemObj = $c->newProblem({ log => $c->log, read_path => $filePath, random_seed => $newSeed }); + my $newProblemObj = $c->newProblem({ read_path => $filePath, random_seed => $newSeed }); my $newProblemJson = await $newProblemObj->render({}); next unless ($newProblemObj->success()); $newProblem = decode_json($newProblemJson); @@ -519,7 +518,7 @@ async sub setTags { # the same holds for keywords $incomingTags->{keywords} = [ $incomingTags->{keywords} ] unless (ref($incomingTags->{keywords}) =~ /ARRAY/); - my $problem = $c->newProblem({ log => $c->log, read_path => $incomingTags->{file} }); + my $problem = $c->newProblem({ read_path => $incomingTags->{file} }); # wrap the get/update/write tags in a promise my $tags = WeBWorK::Utils::Tags->new($incomingTags->{file}); diff --git a/lib/Renderer/Controller/Render.pm b/lib/Renderer/Controller/Render.pm index 632384e3e..912ef0abf 100644 --- a/lib/Renderer/Controller/Render.pm +++ b/lib/Renderer/Controller/Render.pm @@ -15,8 +15,9 @@ sub parseRequest { // '' =~ s!^\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}).*$!$1!r; $originIP ||= $c->tx->remote_address || 'unknown-origin'; - if ($ENV{STRICT_JWT} && !(defined $params{problemJWT} || defined $params{sessionJWT})) { - return $c->exception('Not allowed to request problems with raw data.', 403); + if ($c->config->{STRICT_JWT} && !(defined $params{problemJWT} || defined $params{sessionJWT})) { + $c->exception('Not allowed to request problems with raw data.', 403); + return; } # protect against DOM manipulation @@ -41,8 +42,8 @@ sub parseRequest { eval { $claims = decode_jwt( token => $sessionJWT, - key => $ENV{webworkJWTsecret}, - verify_iss => $ENV{SITE_HOST}, + key => $c->config->{webworkJWTsecret}, + verify_iss => $c->config->{SITE_HOST}, ); 1; } or do { @@ -65,8 +66,8 @@ sub parseRequest { eval { $claims = decode_jwt( token => $problemJWT, - key => $ENV{problemJWTsecret}, - verify_aud => $ENV{SITE_HOST}, + key => $c->config->{problemJWTsecret}, + verify_aud => $c->config->{SITE_HOST}, ); 1; } or do { @@ -78,12 +79,12 @@ sub parseRequest { @params{ keys %$claims } = values %$claims; } elsif ($params{outputFormat} ne 'ptx') { # if no JWT is provided, create one (unless this is a pretext request) - $params{aud} = $ENV{SITE_HOST}; + $params{aud} = $c->config->{SITE_HOST}; $params{isInstructor} //= 0; $params{sessionID} ||= time; my $req_jwt = encode_jwt( payload => \%params, - key => $ENV{problemJWTsecret}, + key => $c->config->{problemJWTsecret}, alg => 'PBES2-HS512+A256KW', enc => 'A256GCM', auto_iat => 1 @@ -156,7 +157,6 @@ async sub problem { } my $problem = $c->newProblem({ - log => $c->log, read_path => $file_path, random_seed => $random_seed, problem_contents => $problem_contents @@ -232,7 +232,7 @@ async sub sendAnswerJWT { message => 'initial message' }; my $header = { - Origin => $ENV{SITE_HOST}, + Origin => $c->config->{SITE_HOST}, 'Content-Type' => 'text/plain', }; @@ -304,10 +304,10 @@ sub jweFromRequest { my $c = shift; my $inputs_ref = $c->parseRequest; return unless $inputs_ref; - $inputs_ref->{aud} = $ENV{SITE_HOST}; + $inputs_ref->{aud} = $c->config->{SITE_HOST}; my $req_jwt = encode_jwt( payload => $inputs_ref, - key => $ENV{problemJWTsecret}, + key => $c->config->{problemJWTsecret}, alg => 'PBES2-HS512+A256KW', enc => 'A256GCM', auto_iat => 1 @@ -319,10 +319,10 @@ sub jwtFromRequest { my $c = shift; my $inputs_ref = $c->parseRequest; return unless $inputs_ref; - $inputs_ref->{aud} = $ENV{SITE_HOST}; + $inputs_ref->{aud} = $c->config->{SITE_HOST}; my $req_jwt = encode_jwt( payload => $inputs_ref, - key => $ENV{problemJWTsecret}, + key => $c->config->{problemJWTsecret}, alg => 'HS256', auto_iat => 1 ); diff --git a/lib/Renderer/Model/Problem.pm b/lib/Renderer/Model/Problem.pm index 1b1b0ca58..29daddd05 100644 --- a/lib/Renderer/Model/Problem.pm +++ b/lib/Renderer/Model/Problem.pm @@ -41,21 +41,14 @@ our $codes = { }; sub new { - my $class = shift; - my $problem_ref = { + my ($class, $c, $args) = @_; + my $self = bless { + c => $c, _error => '', action => '', code_origin => '', - }; - bless $problem_ref, $class; - $problem_ref->{start} = time; - $problem_ref->_init(@_); - return $problem_ref; -} - -sub _init { - my ($self, $args) = @_; - $self->{log} = $args->{log} if $args->{log}; + start => time + }, $class; my $read_path = $args->{read_path} || ''; my $write_path = $args->{write_path} || ''; @@ -80,9 +73,13 @@ sub _init { my $path_info = $self->{code_origin}; my $seed_info = $args->{random_seed} ? "random seed #" . $args->{random_seed} : "no random seed."; - $self->{log}->info("CREATED: Problem created from $path_info with $seed_info"); + $self->c->log->info("CREATED: Problem created from $path_info with $seed_info"); + + return $self; } +sub c { my $self = shift; return $self->{c}; } + sub source { my $self = shift; if (scalar(@_) == 1) { @@ -110,18 +107,19 @@ sub seed { return $self->{random_seed}; } +my $oplRoot = Mojo::File::curfile->dirname->dirname->dirname->dirname->child('webwork-open-problem-library'); + sub path { my $self = shift; if (scalar(@_) >= 1) { my $read_path = shift; my $force = shift if @_; $read_path =~ s!\s+|\.\./!!g; # prevent backtracking and whitespace - my $opl_root = $ENV{OPL_DIRECTORY}; if ($read_path =~ m!^Library/!) { - $read_path =~ s!^Library/!$opl_root/OpenProblemLibrary/!; + $read_path =~ s!^Library/!$oplRoot/OpenProblemLibrary/!; $self->{write_allowed} = 0; } elsif ($read_path =~ m!^Contrib!) { - $read_path =~ s!^Contrib/!$opl_root/Contrib/!; + $read_path =~ s!^Contrib/!$oplRoot/Contrib/!; $self->{write_allowed} = 0; # eventually reconsider this? } else { # TODO: consider steps in pipeline towards OPL @@ -144,11 +142,10 @@ sub target { if (scalar(@_) == 1) { my $write_path = shift; $write_path =~ s!\s+|\.\./!!g; # prevent backtracking and whitespace - my $opl_root = $ENV{OPL_DIRECTORY}; if ($write_path =~ m!^Library/!) { - $write_path =~ s!^Library/!$opl_root/OpenProblemLibrary/!; + $write_path =~ s!^Library/!$oplRoot/OpenProblemLibrary/!; } elsif ($write_path =~ m!^Contrib!) { - $write_path =~ s!^Contrib/!$opl_root/Contrib/!; + $write_path =~ s!^Contrib/!$oplRoot/Contrib/!; } # TODO: include permission check to write to this path... @@ -223,7 +220,7 @@ sub render { sub success { my $self = shift; - $self->{log}->error($self->{exception}) if ($self->{log} && $self->{exception}); + $self->c->log->error($self->{exception}) if $self->{exception}; my $report = ($self->{_error} =~ /\S/) ? $self->{_error} : 'NO ERRORS'; return 1 unless $self->{_error} =~ /\S/; my ($code, $mesg) = split(/ /, $self->{_error}, 2); @@ -240,9 +237,9 @@ sub DESTROY { $logmsg .= $self->{action} . ' from '; $logmsg .= $self->{code_origin}; if ($self->{_error} && $self->{_error} =~ /\S/) { - $self->{log}->error("$logmsg failed with error: " . $self->{_error}); + $self->c->log->error("$logmsg failed with error: " . $self->{_error}); } else { - $self->{log}->info("$logmsg succeeded."); + $self->c->log->info("$logmsg succeeded."); } } diff --git a/lib/Renderer/Plugin/Assets.pm b/lib/Renderer/Plugin/Assets.pm new file mode 100644 index 000000000..05d3194dc --- /dev/null +++ b/lib/Renderer/Plugin/Assets.pm @@ -0,0 +1,120 @@ +package Renderer::Plugin::Assets; +use Mojo::Base 'Mojolicious::Plugin', -signatures; + +use Mojo::File qw(path); +use Mojo::JSON qw(decode_json); + +my $staticRendererAssets; +my $staticPGAssets; +my $thirdPartyRendererDependencies; +my $thirdPartyPGDependencies; + +sub register ($plugin, $app, $conf) { + $app->helper( + getAssetURL => sub ($c, $file, $language = $app->config->{language}) { + my $renderRoot = $app->home; + + # Load the static files list generated by `npm install` the first time this method is called. + unless ($staticRendererAssets) { + my $staticAssetsList = $renderRoot->child('public', 'static-assets.json'); + $staticRendererAssets = readJSON($staticAssetsList); + unless ($staticRendererAssets) { + warn "ERROR: '$staticAssetsList' not found or not readable!\n" + . "You may need to run 'npm install' from '$renderRoot/public'."; + $staticRendererAssets = {}; + } + } + + unless ($staticPGAssets) { + my $staticAssetsList = path($ENV{PG_ROOT})->child('htdocs', 'static-assets.json'); + $staticPGAssets = readJSON($staticAssetsList); + unless ($staticPGAssets) { + warn "ERROR: '$staticAssetsList' not found or not readable!\n" + . "You may need to run 'npm install' from '$ENV{PG_ROOT}/htdocs'."; + $staticPGAssets = {}; + } + } + + unless ($thirdPartyRendererDependencies) { + my $packageJSON = $renderRoot->child('public', 'package.json'); + my $data = readJSON($packageJSON); + warn "ERROR: '$packageJSON' not found or not readable!\n" + unless $data && defined $data->{dependencies}; + $thirdPartyRendererDependencies = $data->{dependencies} // {}; + } + + unless ($thirdPartyPGDependencies) { + my $packageJSON = path($ENV{PG_ROOT})->child('htdocs', 'package.json'); + my $data = readJSON($packageJSON); + warn "ERROR: '$packageJSON' not found or not readable!\n" + unless $data && defined $data->{dependencies}; + $thirdPartyPGDependencies = $data->{dependencies} // {}; + } + + # Check to see if this is a third party asset file in node_modules (either in renderer/public or pg/htdocs). + # If so, then either serve it from a CDN if requested, or serve it directly with the library version + # appended as a URL parameter. + if ($file =~ /^node_modules/) { + my $rendererFile = getThirdPartyAssetURL($c, $file, $thirdPartyRendererDependencies, 'publicFile'); + return $rendererFile if $rendererFile; + + my $pgFile = getThirdPartyAssetURL($c, $file, $thirdPartyPGDependencies, 'pgFile',); + return $pgFile if $pgFile; + } + + # If a right-to-left language is enabled (Hebrew or Arabic) and this is a css file that is not a third party + # asset, then determine the rtl varaint file name. This will be looked for first in the asset lists. + my $rtlfile = + ($language =~ /^(he|ar)/ && $file !~ /node_modules/ && $file =~ /\.css$/) + ? $file =~ s/\.css$/.rtl.css/r + : undef; + + # First check to see if this is a file in the renderer public location with a rtl variant. + return $c->url_for('publicFile', static => $staticRendererAssets->{$rtlfile})->to_string + if defined $rtlfile && defined $staticRendererAssets->{$rtlfile}; + + # Next check to see if this is a file in the renderer public location. + return $c->url_for('publicFile', static => $staticRendererAssets->{$file})->to_string + if defined $staticRendererAssets->{$file}; + + # Now check to see if this is a file in the pg htdocs location with a rtl variant. + return $c->url_for('pgFile', static => $staticPGAssets->{$rtlfile})->to_string + if defined $rtlfile && defined $staticPGAssets->{$rtlfile}; + + # Next check to see if this is a file in the pg htdocs location. + return $c->url_for('pgFile', static => $staticPGAssets->{$file})->to_string + if defined $staticPGAssets->{$file}; + + # If the file was not found in the lists, then just use the given file and assume its path is relative to the + # render app public folder. + return $c->url_for('publicFile', static => $file)->to_string; + } + ); + return; +} + +sub readJSON ($filePath) { + return unless -r $filePath; + my $data = decode_json($filePath->slurp('UTF-8')); + die "FATAL: Unable to open '$filePath'!" if $@; + return $data; +} + +sub getThirdPartyAssetURL ($c, $file, $dependencies, $routeName) { + for (keys %$dependencies) { + if ($file =~ /^node_modules\/$_\/(.*)$/) { + if ($c->config->{thirdPartyAssetsUseCDN}) { + return + "https://cdn.jsdelivr.net/npm/$_\@" + . substr($dependencies->{$_}, 1) . '/' + . ($1 =~ s/(?:\.min)?\.(js|css)$/.min.$1/gr); + } else { + return $c->url_for($routeName, static => $file)->query(version => $dependencies->{$_} =~ s/#/@/gr) + ->to_string; + } + } + } + return; +} + +1; diff --git a/lib/WeBWorK/FormatRenderedProblem.pm b/lib/WeBWorK/FormatRenderedProblem.pm index bc7fcd0f0..f8d1eb447 100644 --- a/lib/WeBWorK/FormatRenderedProblem.pm +++ b/lib/WeBWorK/FormatRenderedProblem.pm @@ -16,7 +16,6 @@ use Mojo::DOM; use Mojo::URL; use WeBWorK::Localize; -use WeBWorK::Utils qw(getAssetURL); use WeBWorK::Utils::LanguageAndDirection; sub formatRenderedProblem { @@ -35,28 +34,23 @@ sub formatRenderedProblem { } # TODO: add configuration to disable these overrides - my $SITE_URL = $inputs_ref->{baseURL} ? Mojo::URL->new($inputs_ref->{baseURL}) : $main::basehref; - my $FORM_ACTION_URL = $inputs_ref->{formURL} ? Mojo::URL->new($inputs_ref->{formURL}) : $main::formURL; + my $SITE_URL = $inputs_ref->{baseURL} ? Mojo::URL->new($inputs_ref->{baseURL}) : $c->stash->{baseHREF}; + my $FORM_ACTION_URL = $inputs_ref->{formURL} ? Mojo::URL->new($inputs_ref->{formURL}) : $c->stash->{formURL}; my $displayMode = $inputs_ref->{displayMode} // 'MathJax'; # HTML document language setting - my $formLanguage = $inputs_ref->{language} // 'en'; + my $formLanguage = $inputs_ref->{language} // $c->config->{language}; # Third party CSS - my @third_party_css = map { getAssetURL($formLanguage, $_->[0]) } ( - [ 'css/bootstrap.css', ], - [ 'node_modules/jquery-ui-dist/jquery-ui.min.css', ], + my @third_party_css = map { $c->getAssetURL($_->[0], $formLanguage) } ( + ['css/bootstrap.css'], + ['node_modules/jquery-ui-dist/jquery-ui.min.css'], ['node_modules/@fortawesome/fontawesome-free/css/all.min.css'], ); - # Add CSS files requested by problems via ADD_CSS_FILE() in the PG file - # or via a setting of $ce->{pg}{specialPGEnvironmentVars}{extra_css_files} - # which can be set in course.conf (the value should be an anonomous array). + # Add CSS files requested by problems via ADD_CSS_FILE() in the PG file. my @cssFiles; - # if (ref($ce->{pg}{specialPGEnvironmentVars}{extra_css_files}) eq 'ARRAY') { - # push(@cssFiles, { file => $_, external => 0 }) for @{ $ce->{pg}{specialPGEnvironmentVars}{extra_css_files} }; - # } if (ref($rh_result->{flags}{extra_css_files}) eq 'ARRAY') { push @cssFiles, @{ $rh_result->{flags}{extra_css_files} }; } @@ -68,22 +62,22 @@ sub formatRenderedProblem { if ($_->{external}) { push(@extra_css_files, $_); } else { - push(@extra_css_files, { file => getAssetURL($formLanguage, $_->{file}), external => 0 }); + push(@extra_css_files, { file => $c->getAssetURL($_->{file}, $formLanguage), external => 0 }); } } # Third party JavaScript # The second element is a hash containing the necessary attributes for the script tag. - my @third_party_js = map { [ getAssetURL($formLanguage, $_->[0]), $_->[1] ] } ( + my @third_party_js = map { [ $c->getAssetURL($_->[0], $formLanguage), $_->[1] ] } ( [ 'node_modules/jquery/dist/jquery.min.js', {} ], [ 'node_modules/jquery-ui-dist/jquery-ui.min.js', {} ], [ 'node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js', {} ], - [ "js/apps/MathJaxConfig/mathjax-config.js", { defer => undef } ], + [ "js/MathJaxConfig/mathjax-config.js", { defer => undef } ], [ 'node_modules/mathjax/es5/tex-svg.js', { defer => undef, id => 'MathJax-script' } ], [ 'node_modules/bootstrap/dist/js/bootstrap.bundle.min.js', { defer => undef } ], - [ "js/apps/Problem/problem.js", { defer => undef } ], - [ "js/apps/Problem/submithelper.js", { defer => undef } ], - [ "js/apps/CSSMessage/css-message.js", { defer => undef } ], + [ "js/Problem/problem.js", { defer => undef } ], + [ "js/Problem/submithelper.js", { defer => undef } ], + [ "js/CSSMessage/css-message.js", { defer => undef } ], ); # Get the requested format. (outputFormat or outputformat) @@ -103,7 +97,7 @@ sub formatRenderedProblem { push( @extra_js_files, { - file => getAssetURL($formLanguage, $_->{file}), + file => $c->getAssetURL($_->{file}, $formLanguage), external => 0, attributes => $_->{attributes} } @@ -209,7 +203,7 @@ sub formatRenderedProblem { template => $formatName eq 'ptx' ? 'RPCRenderFormats/ptx' : 'RPCRenderFormats/default', $formatName eq 'json' ? (format => 'json') : (), formatName => $formatName, - lh => WeBWorK::Localize::getLangHandle($inputs_ref->{language} // 'en'), + lh => WeBWorK::Localize::getLangHandle($formLanguage), rh_result => $rh_result, SITE_URL => $SITE_URL, FORM_ACTION_URL => $FORM_ACTION_URL, diff --git a/lib/WeBWorK/PreTeXt.pm b/lib/WeBWorK/PreTeXt.pm index 5a557bcbe..a6d9abd8c 100644 --- a/lib/WeBWorK/PreTeXt.pm +++ b/lib/WeBWorK/PreTeXt.pm @@ -3,11 +3,13 @@ package WeBWorK::PreTeXt; use strict; use warnings; +use Mojo::File; use Mojo::DOM; use Mojo::IOLoop; use Data::Structure::Util qw(unbless); -use lib "$ENV{PG_ROOT}/lib"; +use lib Mojo::File::curfile->dirname->dirname->child('PG', 'lib'); + use WeBWorK::PG; sub render_ptx { @@ -43,4 +45,5 @@ sub render_ptx { return "error: $err"; }); } + 1; diff --git a/lib/WeBWorK/RenderProblem.pm b/lib/WeBWorK/RenderProblem.pm index 78fc8992c..10c4d2eb5 100644 --- a/lib/WeBWorK/RenderProblem.pm +++ b/lib/WeBWorK/RenderProblem.pm @@ -6,32 +6,18 @@ use warnings; # for logs use Time::HiRes qw/time/; use Proc::ProcessTable; -use Date::Format; +use Mojo::File; use Mojo::JSON qw( encode_json ); use Crypt::JWT qw( encode_jwt ); use Digest::MD5 qw( md5_hex ); -use lib "$ENV{PG_ROOT}/lib"; +use lib Mojo::File::curfile->dirname->dirname->child('PG', 'lib'); use WeBWorK::PG; use WeBWorK::Utils::Tags; -################################################## -# create log files :: expendable -################################################## - -my $path_to_log_file = "$ENV{RENDER_ROOT}/logs/resource_usage.log"; - -eval { # attempt to create log file - local (*FH); - open(FH, '>>:encoding(UTF-8)', $path_to_log_file) - or die "Can't open file $path_to_log_file for writing"; - close(FH); -}; - -die "You must first create an output file at $path_to_log_file with permissions 777 " - unless -w $path_to_log_file; +my $renderRoot = Mojo::File::curfile->dirname->dirname->dirname; ################################################## # define universal TO_JSON for JSON::XS unbless @@ -78,7 +64,7 @@ sub process_pg_file { my $log_file_path = $inputs_ref->{sourceFilePath} || 'source provided without path'; my $memory_use_end = get_current_process_memory(); my $memory_use = $memory_use_end - $memory_use_start; - writeRenderLogEntry( + $problem->c->resourceUsageLog( sprintf("(duration: %.3f sec) ", $pg_duration) . sprintf("{memory: %6d bytes} ", $memory_use) . "file: $log_file_path" @@ -140,7 +126,7 @@ sub process_problem { $error_string = ''; # can include @args as third input below - $return_object = standaloneRenderer(\$source, $inputs_ref); + $return_object = renderPG($problem->c, \$source, $inputs_ref); # stash assets list in $return_object $return_object->{pgResources} = \@pgResources; @@ -151,7 +137,7 @@ sub process_problem { # if this is a preview, leave session unmodified, and no answerJWT $return_object->{sessionJWT} = $inputs_ref->{sessionJWT}; } elsif ($inputs_ref->{problemJWT}) { - my ($sessionJWT, $answerJWT) = generateJWTs($return_object, $inputs_ref); + my ($sessionJWT, $answerJWT) = generateJWTs($problem->c, $return_object, $inputs_ref); $return_object->{sessionJWT} = $sessionJWT; $return_object->{answerJWT} = $answerJWT; } @@ -182,7 +168,8 @@ sub process_problem { # standalonePGproblemRenderer ########################################### -sub standaloneRenderer { +sub renderPG { + my $c = shift; my $problemFile = shift // ''; my $inputs_ref = shift // {}; my %args = @_; @@ -221,9 +208,9 @@ sub standaloneRenderer { psvn => $inputs_ref->{psvn}, problemUUID => $inputs_ref->{problemUUID}, language => $inputs_ref->{language} // 'en', - templateDirectory => "$ENV{RENDER_ROOT}/", - htmlURL => 'pg_files/', - tempURL => 'pg_files/tmp/', + templateDirectory => "$renderRoot/", + htmlURL => $c->url_for('pgFile', static => '')->to_string, + tempURL => $c->url_for('pgTempFile', static => '')->to_string, debuggingOptions => { show_resource_info => $inputs_ref->{show_resource_info}, view_problem_debugging_info => $inputs_ref->{view_problem_debugging_info} @@ -287,10 +274,12 @@ sub get_current_process_memory { # expects a pg/result_object and a ref to submitted formdata # generates a sessionJWT and an answerJWT sub generateJWTs { - my $pg = shift; - my $inputs_ref = shift; + my $c = shift; + my $pg = shift; + my $inputs_ref = shift; + my $sessionHash = { - iss => $ENV{SITE_HOST}, + iss => $c->config->{SITE_HOST}, answersSubmitted => 1, sessionID => $inputs_ref->{sessionID}, problemUUID => $inputs_ref->{problemUUID}, @@ -301,13 +290,6 @@ sub generateJWTs { answers => unbless($pg->{answers}), }; - # proposed restructuring of the answerJWT -- prepare with LibreTexts - # my %studentKeys = qw(student_value value student_formula formula student_ans answer original_student_ans original); - # my %previewKeys = qw(preview_text_string text preview_latex_string latex); - # my %correctKeys = qw(correct_value value correct_formula formula correct_ans ans); - # my %messageKeys = qw(ans_message answer error_message error); - # my @resultKeys = qw(score weight); - # once the correct answers are shown, this setting is permanent if ($inputs_ref->{showCorrectAnswers} && !$inputs_ref->{isInstructor}) { $sessionHash->{showCorrectAnswers} = 1; @@ -315,15 +297,8 @@ sub generateJWTs { } # store the current answer/response state for each entry - foreach my $ans (@{ $pg->{flags}{KEPT_EXTRA_ANSWERS} }) { + for my $ans (@{ $pg->{flags}{KEPT_EXTRA_ANSWERS} }) { $sessionHash->{$ans} = $inputs_ref->{$ans}; - -# More restructuring -- confirm with LibreTexts -# $scoreHash->{$ans}{student} = { map {exists $answers{$ans}{$_} ? ($studentKeys{$_} => $answers{$ans}{$_}) : ()} keys %studentKeys }; -# $scoreHash->{$ans}{preview} = { map {exists $answers{$ans}{$_} ? ($previewKeys{$_} => $answers{$ans}{$_}) : ()} keys %previewKeys }; -# $scoreHash->{$ans}{correct} = { map {exists $answers{$ans}{$_} ? ($correctKeys{$_} => $answers{$ans}{$_}) : ()} keys %correctKeys }; -# $scoreHash->{$ans}{message} = { map {exists $answers{$ans}{$_} ? ($messageKeys{$_} => $answers{$ans}{$_}) : ()} keys %messageKeys }; -# $scoreHash->{$ans}{result} = { map {exists $answers{$ans}{$_} ? ($_ => $answers{$ans}{$_}) : ()} @resultKeys }; } # update the number of correct/incorrect submissions if answers were 'submitted' @@ -338,19 +313,21 @@ sub generateJWTs { : ($inputs_ref->{numIncorrect} // 0); # create the session JWT - my $sessionJWT = encode_jwt(payload => $sessionHash, auto_iat => 1, alg => 'HS256', key => $ENV{webworkJWTsecret}); + my $sessionJWT = + encode_jwt(payload => $sessionHash, auto_iat => 1, alg => 'HS256', key => $c->config->{webworkJWTsecret}); # form answerJWT my $responseHash = { - iss => $ENV{SITE_HOST}, + iss => $c->config->{SITE_HOST}, aud => $inputs_ref->{JWTanswerURL}, score => $scoreHash, sessionJWT => $sessionJWT, - platform => 'standaloneRenderer' + platform => 'renderer' }; # Can instead use alg => 'PBES2-HS512+A256KW', enc => 'A256GCM' for JWE - my $answerJWT = encode_jwt(payload => $responseHash, alg => 'HS256', key => $ENV{problemJWTsecret}, auto_iat => 1); + my $answerJWT = + encode_jwt(payload => $responseHash, alg => 'HS256', key => $c->config->{problemJWTsecret}, auto_iat => 1); return ($sessionJWT, $answerJWT); } @@ -371,7 +348,7 @@ sub pretty_print_rh { if (ref($rh) =~ /HASH/) { $out .= "{\n"; $indent++; - foreach my $key (sort keys %{$rh}) { + for my $key (sort keys %{$rh}) { $out .= " " x $indent . "$key => " . pretty_print_rh($rh->{$key}, $indent) . "\n"; } $indent--; @@ -395,16 +372,4 @@ sub pretty_print_rh { return $out . " "; } -sub writeRenderLogEntry($) { - my $message = shift; - - local *LOG; - if (open LOG, ">>", $path_to_log_file) { - print LOG "[", time2str("%a %b %d %H:%M:%S %Y", time), "] $message\n"; - close LOG; - } else { - warn "failed to open $path_to_log_file for writing: $!"; - } -} - 1; diff --git a/lib/WeBWorK/Utils.pm b/lib/WeBWorK/Utils.pm index 3db77b82d..13b0c3b4c 100644 --- a/lib/WeBWorK/Utils.pm +++ b/lib/WeBWorK/Utils.pm @@ -4,12 +4,7 @@ use base qw(Exporter); use strict; use warnings; -use Mojo::JSON qw(decode_json); - -our @EXPORT_OK = qw( - wwRound - getAssetURL -); +our @EXPORT_OK = qw(wwRound); # usage wwRound($places,$float) # return $float rounded up to number of decimal places given by $places @@ -20,115 +15,4 @@ sub wwRound(@) { return int($float * $factor + 0.5) / $factor; } -my $staticWWAssets; -my $staticPGAssets; -my $thirdPartyWWDependencies; -my $thirdPartyPGDependencies; - -sub readJSON { - my $fileName = shift; - - return unless -r $fileName; - - open(my $fh, "<:encoding(UTF-8)", $fileName) or die "FATAL: Unable to open '$fileName'!"; - local $/; - my $data = <$fh>; - close $fh; - - return decode_json($data); -} - -sub getThirdPartyAssetURL { - my ($file, $dependencies, $baseURL, $useCDN) = @_; - - for (keys %$dependencies) { - if ($file =~ /^node_modules\/$_\/(.*)$/) { - if ($useCDN && $1 !~ /mathquill/) { - return - "https://cdn.jsdelivr.net/npm/$_\@" - . substr($dependencies->{$_}, 1) . '/' - . ($1 =~ s/(?:\.min)?\.(js|css)$/.min.$1/gr); - } else { - return Mojo::URL->new("${baseURL}$file")->query(version => $dependencies->{$_} =~ s/#/@/gr)->to_string; - } - } - } - return; -} - -# Get the url for static assets. -sub getAssetURL { - my ($language, $file) = @_; - - # Load the static files list generated by `npm install` the first time this method is called. - unless ($staticWWAssets) { - my $staticAssetsList = "$ENV{RENDER_ROOT}/public/static-assets.json"; - $staticWWAssets = readJSON($staticAssetsList); - unless ($staticWWAssets) { - warn "ERROR: '$staticAssetsList' not found or not readable!\n" - . "You may need to run 'npm install' from '$ENV{RENDER_ROOT}/public'."; - $staticWWAssets = {}; - } - } - - unless ($staticPGAssets) { - my $staticAssetsList = "$ENV{PG_ROOT}/htdocs/static-assets.json"; - $staticPGAssets = readJSON($staticAssetsList); - unless ($staticPGAssets) { - warn "ERROR: '$staticAssetsList' not found or not readable!\n" - . "You may need to run 'npm install' from '$ENV{PG_ROOT}/htdocs'."; - $staticPGAssets = {}; - } - } - - unless ($thirdPartyWWDependencies) { - my $packageJSON = "$ENV{RENDER_ROOT}/public/package.json"; - my $data = readJSON($packageJSON); - warn "ERROR: '$packageJSON' not found or not readable!\n" unless $data && defined $data->{dependencies}; - $thirdPartyWWDependencies = $data->{dependencies} // {}; - } - - unless ($thirdPartyPGDependencies) { - my $packageJSON = "$ENV{PG_ROOT}/htdocs/package.json"; - my $data = readJSON($packageJSON); - warn "ERROR: '$packageJSON' not found or not readable!\n" unless $data && defined $data->{dependencies}; - $thirdPartyPGDependencies = $data->{dependencies} // {}; - } - - # Check to see if this is a third party asset file in node_modules (either in webwork2/htdocs or pg/htdocs). - # If so, then either serve it from a CDN if requested, or serve it directly with the library version - # appended as a URL parameter. - if ($file =~ /^node_modules/) { - my $wwFile = getThirdPartyAssetURL($file, $thirdPartyWWDependencies, '', 0); - return $wwFile if $wwFile; - - my $pgFile = getThirdPartyAssetURL($file, $thirdPartyPGDependencies, 'pg_files/', 1); - return $pgFile if $pgFile; - } - - # If a right-to-left language is enabled (Hebrew or Arabic) and this is a css file that is not a third party asset, - # then determine the rtl varaint file name. This will be looked for first in the asset lists. - my $rtlfile = - ($language =~ /^(he|ar)/ && $file !~ /node_modules/ && $file =~ /\.css$/) - ? $file =~ s/\.css$/.rtl.css/r - : undef; - - # First check to see if this is a file in the webwork htdocs location with a rtl variant. - return "$staticWWAssets->{$rtlfile}" - if defined $rtlfile && defined $staticWWAssets->{$rtlfile}; - - # Next check to see if this is a file in the webwork htdocs location. - return "$staticWWAssets->{$file}" if defined $staticWWAssets->{$file}; - - # Now check to see if this is a file in the pg htdocs location with a rtl variant. - return "pg_files/$staticPGAssets->{$rtlfile}" if defined $rtlfile && defined $staticPGAssets->{$rtlfile}; - - # Next check to see if this is a file in the pg htdocs location. - return "pg_files/$staticPGAssets->{$file}" if defined $staticPGAssets->{$file}; - - # If the file was not found in the lists, then just use the given file and assume its path is relative to the - # render app public folder. - return "$file"; -} - 1; diff --git a/public/generate-assets.js b/public/generate-assets.js index 4d68de265..48cf572ad 100755 --- a/public/generate-assets.js +++ b/public/generate-assets.js @@ -196,7 +196,7 @@ const processFile = async (file, _details) => { if (ready) fs.writeFileSync(assetFile, JSON.stringify(assets)); }; -const jsDir = path.resolve(__dirname, 'js/apps'); +const jsDir = path.resolve(__dirname, 'js'); const cssDir = path.resolve(__dirname, 'css'); // Remove generated files from previous builds. @@ -208,7 +208,7 @@ if (argv.clean) process.exit(); // Set up the watcher. if (argv.watchFiles) console.log('\x1b[32mEstablishing watches and performing initial build.\x1b[0m'); chokidar - .watch(['js/apps', 'css'], { + .watch(['js', 'css'], { ignored: /layouts|\.min\.(js|css)$/, cwd: __dirname, // Make sure all paths are given relative to the htdocs directory. usePolling: true, // Needed to get changes to symlinks. diff --git a/public/js/apps/CSSMessage/css-message.js b/public/js/CSSMessage/css-message.js similarity index 100% rename from public/js/apps/CSSMessage/css-message.js rename to public/js/CSSMessage/css-message.js diff --git a/public/js/apps/MathJaxConfig/mathjax-config.js b/public/js/MathJaxConfig/mathjax-config.js similarity index 100% rename from public/js/apps/MathJaxConfig/mathjax-config.js rename to public/js/MathJaxConfig/mathjax-config.js diff --git a/public/js/apps/Problem/problem.js b/public/js/Problem/problem.js similarity index 97% rename from public/js/apps/Problem/problem.js rename to public/js/Problem/problem.js index c204f37d4..4ce896d64 100644 --- a/public/js/apps/Problem/problem.js +++ b/public/js/Problem/problem.js @@ -27,7 +27,7 @@ // set up listeners on knowl hints and solutions document.querySelectorAll('.knowl[data-type="hint"]').forEach((hint) => { - hint.addEventListener('click', (event) => { + hint.addEventListener('click', () => { window.parent.postMessage( JSON.stringify({ type: 'webwork.interaction.hint', @@ -41,7 +41,7 @@ }); document.querySelectorAll('.knowl[data-type="solution"]').forEach((solution) => { - solution.addEventListener('click', (event) => { + solution.addEventListener('click', () => { window.parent.postMessage( JSON.stringify({ type: 'webwork.interaction.solution', @@ -166,7 +166,7 @@ // we also need to trigger the submit when the user clicks the button // or when they hit enter in the input field const creditButton = document.getElementById('creditModalSubmitBtn'); - creditButton.addEventListener('click', (event) => { + creditButton.addEventListener('click', () => { creditForm.dispatchEvent(new Event('submit')); }); const creditInput = document.getElementById('creditModalEmail'); diff --git a/public/js/apps/Problem/submithelper.js b/public/js/Problem/submithelper.js similarity index 100% rename from public/js/apps/Problem/submithelper.js rename to public/js/Problem/submithelper.js diff --git a/renderer.conf.dist b/renderer.conf.dist index 084609d5a..c568b1d94 100644 --- a/renderer.conf.dist +++ b/renderer.conf.dist @@ -9,6 +9,9 @@ STRICT_JWT => 0, FULL_APP_INSECURE => 0, INTERACTION_LOG => 0, + thirdPartyAssetsUseCDN => 0, + language => 'en', + hypnotoad => { listen => ['http://*:3000'], accepts => 400, diff --git a/templates/RPCRenderFormats/default.html.ep b/templates/RPCRenderFormats/default.html.ep index 81e24bf57..5b70c3441 100644 --- a/templates/RPCRenderFormats/default.html.ep +++ b/templates/RPCRenderFormats/default.html.ep @@ -1,4 +1,4 @@ -% use WeBWorK::Utils qw(getAssetURL wwRound); +% use WeBWorK::Utils qw(wwRound); % > diff --git a/templates/columns/editorIframe.html.ep b/templates/columns/editorIframe.html.ep index e13c9bd58..b7d40ce24 100644 --- a/templates/columns/editorIframe.html.ep +++ b/templates/columns/editorIframe.html.ep @@ -1,4 +1,4 @@ -%= javascript 'node_modules/iframe-resizer/js/iframeResizer.min.js' +%= javascript getAssetURL('node_modules/iframe-resizer/js/iframeResizer.min.js')
diff --git a/templates/columns/editorUI.html.ep b/templates/columns/editorUI.html.ep index e55188ae3..274826c62 100644 --- a/templates/columns/editorUI.html.ep +++ b/templates/columns/editorUI.html.ep @@ -1,4 +1,4 @@ -%= javascript 'node_modules/@openwebwork/pg-codemirror-editor/dist/pg-codemirror-editor.js' +%= javascript getAssetURL('node_modules/@openwebwork/pg-codemirror-editor/dist/pg-codemirror-editor.js')
Editing problem:
diff --git a/templates/columns/filebrowser.html.ep b/templates/columns/filebrowser.html.ep index 3eecaf43d..53a3f2074 100644 --- a/templates/columns/filebrowser.html.ep +++ b/templates/columns/filebrowser.html.ep @@ -1,15 +1,15 @@ -%= stylesheet 'css/filebrowser.css' -%= javascript 'js/filebrowser.js' +%= stylesheet getAssetURL('css/filebrowser.css') +%= javascript getAssetURL('js/filebrowser.js')
Current directory path:
- %= form_for 'render-api/cat' => ( method => 'POST', id => 'BackNavigation') => begin + %= form_for 'renderapicat' => ( method => 'POST', id => 'BackNavigation') => begin %= hidden_field maxDepth => 1 %= select_field basePath => ['/'], id => 'back-nav', class => 'back-nav dropdown', onchange => "updateBrowser('BackNavigation', backOut)" %= end
- %= form_for 'render-api/cat' => ( method => 'POST', id => 'FileBrowserForm', class => 'fill-height' ) => begin + %= form_for 'renderapicat' => ( method => 'POST', id => 'FileBrowserForm', class => 'fill-height' ) => begin %= hidden_field maxDepth => 1 %= select_field basePath => [[Contrib => 'Contrib/'], [Library => 'Library/'], [Pending => 'Pending/'], [private => 'private/']], id => 'file-list', multiple => undef, class => 'file-list', ondblclick => "updateBrowser('FileBrowserForm', diveIn)" %= end diff --git a/templates/columns/oplIframe.html.ep b/templates/columns/oplIframe.html.ep index c4a9efdcb..bb37f2237 100644 --- a/templates/columns/oplIframe.html.ep +++ b/templates/columns/oplIframe.html.ep @@ -1,4 +1,4 @@ -%= javascript 'node_modules/iframe-resizer/js/iframeResizer.min.js' +%= javascript getAssetURL('node_modules/iframe-resizer/js/iframeResizer.min.js') %= stylesheet begin .pgfile-header { max-height: 30%; diff --git a/templates/columns/tags.html.ep b/templates/columns/tags.html.ep index 3d6261162..7e9e00e9b 100644 --- a/templates/columns/tags.html.ep +++ b/templates/columns/tags.html.ep @@ -1,13 +1,13 @@ -%= stylesheet 'css/tags.css' +%= stylesheet getAssetURL('css/tags.css') % my $taxo = ''; -% if ( open(TAXONOMY, "<:encoding(utf8)", $c->app->home->child('tmp/tagging-taxonomy.json')) ) { -% $taxo = join("", ); -% close TAXONOMY; +% if (open(my $taxonomy, '<:encoding(utf8)', $c->app->home->child('tmp/tagging-taxonomy.json'))) { + % $taxo = join('', <$taxonomy>); + % close $taxonomy; % } else { die "Could not open Taxonomy!"; }
- %= form_for 'render-api/tags' => ( method => 'POST', id => 'problem-tags' ) => begin + %= form_for 'renderapitags' => ( method => 'POST', id => 'problem-tags' ) => begin %= hidden_field 'file' => '', id => 'tag-filename'
@@ -79,7 +79,7 @@
- %= submit_button 'Save Tags' + %= submit_button 'Save Tags'
%= end
@@ -87,4 +87,4 @@ %= javascript begin taxo = <%== $taxo %>; %= end -%= javascript 'js/tags.js' +%= javascript getAssetURL('js/tags.js') diff --git a/templates/exception.html.ep b/templates/exception.html.ep index 206b5ce05..9839d3590 100644 --- a/templates/exception.html.ep +++ b/templates/exception.html.ep @@ -1,5 +1,5 @@ -%= stylesheet "$ENV{baseURL}/css/typing-sim.css" -%= stylesheet "$ENV{baseURL}/css/crt-display.css" +%= stylesheet getAssetURL('css/typing-sim.css') +%= stylesheet getAssetURL('css/crt-display.css') %= javascript begin window.onload = function() { let i = 0; @@ -19,9 +19,10 @@ window.onload = function() { % end % my $message = $c->stash('message') // $c->stash('exception')->message; -% $message =~ s!$ENV{RENDER_ROOT}/!!g; +% my $renderRoot = $c->app->home; +% $message =~ s!$renderRoot/!!g;

>

- \ No newline at end of file + diff --git a/templates/layouts/navbar.html.ep b/templates/layouts/navbar.html.ep index 04bd44455..37e404637 100644 --- a/templates/layouts/navbar.html.ep +++ b/templates/layouts/navbar.html.ep @@ -1,12 +1,12 @@ - + WeBWorK Standalone Renderer - %= stylesheet 'css/navbar.css' + %= stylesheet getAssetURL('css/navbar.css')
@@ -29,6 +29,6 @@
<%= content %> - %= javascript 'js/navbar.js' + %= javascript getAssetURL('js/navbar.js') diff --git a/templates/pages/flex.html.ep b/templates/pages/flex.html.ep index 5ade66483..3cc7b3b35 100644 --- a/templates/pages/flex.html.ep +++ b/templates/pages/flex.html.ep @@ -1,4 +1,4 @@ -%= stylesheet 'css/opl-flex.css' +%= stylesheet getAssetURL('css/opl-flex.css')
@@ -16,4 +16,4 @@
-
\ No newline at end of file +
diff --git a/templates/pages/oplUI.html.ep b/templates/pages/oplUI.html.ep index ea33dcf2d..43df27653 100644 --- a/templates/pages/oplUI.html.ep +++ b/templates/pages/oplUI.html.ep @@ -1,4 +1,4 @@ -%= stylesheet 'css/opl-flex.css' +%= stylesheet getAssetURL('css/opl-flex.css')
diff --git a/templates/pages/twocolumn.html.ep b/templates/pages/twocolumn.html.ep index bca5fee3b..888524a09 100644 --- a/templates/pages/twocolumn.html.ep +++ b/templates/pages/twocolumn.html.ep @@ -1,5 +1,5 @@ % layout 'navbar'; -%= stylesheet 'css/twocolumn.css' +%= stylesheet getAssetURL('css/twocolumn.css') %= javascript 'https://cdn.jsdelivr.net/npm/js-base64@3.5.2/base64.min.js'