Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions cms/io/web_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import hashlib
import logging
import importlib.resources

import collections
try:
Expand All @@ -36,6 +38,7 @@

from cms.db.filecacher import FileCacher
from cms.server.file_middleware import FileServerMiddleware
from cms.server.util import Url
from .service import Service
from .web_rpc import RPCMiddleware

Expand All @@ -45,6 +48,58 @@

SECONDS_IN_A_YEAR = 365 * 24 * 60 * 60

class StaticFileHasher:
"""
Constructs URLs to static files. The result of make() is similar to the
url() function that's used in the templates, in that it constructs a
relative URL, but it also adds a "?h=12345678" query parameter which forces
browsers to reload the resource when it has changed.
"""
def __init__(self, files: list[tuple[str, str]]):
"""
Initialize.

files: list of static file locations, each in the format that would be
passed to SharedDataMiddleware.
"""
# Cache of the hashes of files, to prevent re-hashing them on every request.
self.cache: dict[tuple[str, ...], str] = {}
# We reverse the order, because in WSGI later-added middlewares
# override earlier ones, but here we iterate the locations and use the
# first found match.
self.static_locations = files[::-1]

def make(self, base_url: Url):
"""
Create a new url helper function (called once per request).

The returned function takes arguments in the same format as `Url`, and
returns a string in the same format as `Url` except with a hash
appended as a query string.
"""
def inner_func(*paths: str):
# WebService always serves the static files under /static.
assert paths[0] == "static"

url_path_part = base_url(*paths)

if paths in self.cache:
return url_path_part + self.cache[paths]

for module_name, dir in self.static_locations:
resource = importlib.resources.files(module_name).joinpath(dir, *paths[1:])
if resource.is_file():
with resource.open('rb') as file:
hash = hashlib.file_digest(file, hashlib.sha256).hexdigest()
result = "?h=" + hash[:24]
break
else:
logger.warning(f"Did not find path passed to static_url(): {paths}")
result = ""

self.cache[paths] = result
return url_path_part + result
return inner_func

class WebService(Service):
"""RPC service with Web server capabilities.
Expand Down Expand Up @@ -78,6 +133,8 @@ def __init__(
cache=True, cache_timeout=SECONDS_IN_A_YEAR,
fallback_mimetype="application/octet-stream")

self.static_file_hasher = StaticFileHasher(static_files)

self.file_cacher = FileCacher(self)
self.wsgi_app = FileServerMiddleware(self.file_cacher, self.wsgi_app)

Expand Down
1 change: 1 addition & 0 deletions cms/server/admin/handlers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ def render_params(self) -> dict:
params["timestamp"] = make_datetime()
params["contest"] = self.contest
params["url"] = self.url
params["static_url"] = self.static_url_helper
params["xsrf_form_html"] = self.xsrf_form_html()
# FIXME These objects provide too broad an access: their usage
# should be extracted into with narrower-scoped parameters.
Expand Down
24 changes: 12 additions & 12 deletions cms/server/admin/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="shortcut icon" href="{{ url("static", "favicon.ico") }}" />
<link rel="stylesheet" type="text/css" href="{{ url("static", "reset.css") }}">
<link rel="stylesheet" type="text/css" href="{{ url("static", "aws_style.css") }}">
<script src="{{ url("static", "web_rpc.js") }}"></script>
<script src="{{ url("static", "aws_utils.js") }}"></script>
<script src="{{ url("static", "jq", "jquery-3.6.0.min.js") }}"></script>
<script src="{{ url("static", "jq", "jquery.jqplot.min.js") }}"></script>
<script src="{{ url("static", "jq", "jqplot.dateAxisRenderer.min.js") }}"></script>
<script src="{{ url("static", "jq", "jqplot.enhancedLegendRenderer.min.js") }}"></script>
<link rel="stylesheet" type="text/css" href="{{ url("static", "jq", "jquery.jqplot.min.css") }}"/>
<link rel="stylesheet" type="text/css" href="{{ url("static", "prism.css") }}">
<script src="{{ url("static", "prism.js") }}" data-manual></script>
<link rel="shortcut icon" href="{{ static_url("static", "favicon.ico") }}" />
<link rel="stylesheet" type="text/css" href="{{ static_url("static", "reset.css") }}">
<link rel="stylesheet" type="text/css" href="{{ static_url("static", "aws_style.css") }}">
<script src="{{ static_url("static", "web_rpc.js") }}"></script>
<script src="{{ static_url("static", "aws_utils.js") }}"></script>
<script src="{{ static_url("static", "jq", "jquery-3.6.0.min.js") }}"></script>
<script src="{{ static_url("static", "jq", "jquery.jqplot.min.js") }}"></script>
<script src="{{ static_url("static", "jq", "jqplot.dateAxisRenderer.min.js") }}"></script>
<script src="{{ static_url("static", "jq", "jqplot.enhancedLegendRenderer.min.js") }}"></script>
<link rel="stylesheet" type="text/css" href="{{ static_url("static", "jq", "jquery.jqplot.min.css") }}"/>
<link rel="stylesheet" type="text/css" href="{{ static_url("static", "prism.css") }}">
<script src="{{ static_url("static", "prism.js") }}" data-manual></script>

{% if contest is none %}
<title>Admin</title>
Expand Down
6 changes: 3 additions & 3 deletions cms/server/admin/templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="shortcut icon" href="{{ url("static", "favicon.ico") }}" />
<link rel="stylesheet" type="text/css" href="{{ url("static", "reset.css") }}">
<link rel="stylesheet" type="text/css" href="{{ url("static", "aws_style.css") }}">
<link rel="shortcut icon" href="{{ static_url("static", "favicon.ico") }}" />
<link rel="stylesheet" type="text/css" href="{{ static_url("static", "reset.css") }}">
<link rel="stylesheet" type="text/css" href="{{ static_url("static", "aws_style.css") }}">
<title>Admin</title>
</head>
<body class="admin">
Expand Down
8 changes: 4 additions & 4 deletions cms/server/admin/templates/overview.html
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ <h2 id="title_submissions_status" class="toggling_on">Submissions status</h2>
</tr>
</thead>
<tbody>
<tr><td style="text-align: center;" colspan="2"><img src="{{ url("static", "loading.gif") }}" alt="loading..." /></td></tr>
<tr><td style="text-align: center;" colspan="2"><img src="{{ static_url("static", "loading.gif") }}" alt="loading..." /></td></tr>
</tbody>
</table>
<div class="hr"></div>
Expand All @@ -277,7 +277,7 @@ <h2 id="title_queue_status" class="toggling_on">Queue status</h2>
</tr>
</thead>
<tbody>
<tr><td style="text-align: center;" colspan="4"><img src="{{ url("static", "loading.gif") }}" alt="loading..." /></td></tr>
<tr><td style="text-align: center;" colspan="4"><img src="{{ static_url("static", "loading.gif") }}" alt="loading..." /></td></tr>
</tbody>
</table>
<div class="hr"></div>
Expand All @@ -296,7 +296,7 @@ <h2 id="title_workers_status" class="toggling_on">Workers status</h2>
</tr>
</thead>
<tbody>
<tr><td style="text-align: center;" colspan="5"><img src="{{ url("static", "loading.gif") }}" alt="loading..." /></td></tr>
<tr><td style="text-align: center;" colspan="5"><img src="{{ static_url("static", "loading.gif") }}" alt="loading..." /></td></tr>
</tbody>
</table>
<div class="hr"></div>
Expand All @@ -316,7 +316,7 @@ <h2 id="title_logs" class="toggling_on">Logs</h2>
</tr>
</thead>
<tbody>
<tr><td style="text-align: center;" colspan="5"><img src="{{ url("static", "loading.gif") }}" alt="loading..." /></td></tr>
<tr><td style="text-align: center;" colspan="5"><img src="{{ static_url("static", "loading.gif") }}" alt="loading..." /></td></tr>
</tbody>
</table>
<div class="hr"></div>
Expand Down
4 changes: 2 additions & 2 deletions cms/server/admin/templates/resources.html
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@

kill_service: function(s, link)
{
link.parentNode.innerHTML = '<img src="{{ url("static", "loading.gif") }}" alt="loading..." />';
link.parentNode.innerHTML = '<img src="{{ static_url("static", "loading.gif") }}" alt="loading..." />';
cmsrpc_request("ResourceService", this.shard,
"kill_service",
{"service": s});
Expand Down Expand Up @@ -302,7 +302,7 @@ <h2 id="title_machine_{{ i }}" class="toggling_on">Machine {{ i }} ({{ resource_
</tr>
</thead>
<tbody>
<tr><td style="text-align: center;" colspan="8"><img src="{{ url("static", "loading.gif") }}" alt="loading..." /></td></tr>
<tr><td style="text-align: center;" colspan="8"><img src="{{ static_url("static", "loading.gif") }}" alt="loading..." /></td></tr>
</tbody>
</table>

Expand Down
1 change: 1 addition & 0 deletions cms/server/contest/handlers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ def render_params(self) -> dict:
ret["now"] = self.timestamp
ret["utc"] = utc_tzinfo
ret["url"] = self.url
ret["static_url"] = self.static_url_helper

ret["available_translations"] = self.available_translations

Expand Down
14 changes: 7 additions & 7 deletions cms/server/contest/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@

<title>{% block title %}{% endblock title %}</title>

<link rel="shortcut icon" href="{{ url("static", "favicon.ico") }}" />
<link rel="stylesheet" href="{{ url("static", "css", "bootstrap.css") }}">
<link rel="stylesheet" href="{{ url("static", "cws_style.css") }}">
<link rel="shortcut icon" href="{{ static_url("static", "favicon.ico") }}" />
<link rel="stylesheet" href="{{ static_url("static", "css", "bootstrap.css") }}">
<link rel="stylesheet" href="{{ static_url("static", "cws_style.css") }}">

<script src="{{ url("static", "jq", "jquery-3.6.0.min.js") }}"></script>
<script src="{{ static_url("static", "jq", "jquery-3.6.0.min.js") }}"></script>
{# For compatibility with Bootstrap 2.x #}
<script src="{{ url("static", "jq", "jquery-migrate-3.3.2.min.js") }}"></script>
<script src="{{ url("static", "js", "bootstrap.js") }}"></script>
<script src="{{ url("static", "cws_utils.js") }}"></script>
<script src="{{ static_url("static", "jq", "jquery-migrate-3.3.2.min.js") }}"></script>
<script src="{{ static_url("static", "js", "bootstrap.js") }}"></script>
<script src="{{ static_url("static", "cws_utils.js") }}"></script>

{% block js %}{% endblock js %}
</head>
Expand Down
13 changes: 8 additions & 5 deletions cms/server/contest/templates/macro/submission.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{% macro rows(url, contest_url, translation, xsrf_form_html,
{% macro rows(url, static_url, contest_url, translation, xsrf_form_html,
actual_phase, task, submissions,
can_use_tokens, can_play_token, can_play_token_now,
submissions_download_allowed, official) -%}
{#
Render a submission table with all (un)official submissions passed.

url (Url): the URL instance referring to the root of CWS.
static_url: static url helper constructed from url
contest_url (Url): the URL instance referring to the main contest page.
translation (Translation): locale to use to show messages.
xsrf_form_html (str): input element for the XSRF protection.
Expand Down Expand Up @@ -90,6 +91,7 @@
{# loop.revindex is broken: https://github.com/pallets/jinja/issues/794 #}
{{ row(
url,
static_url,
contest_url,
translation,
xsrf_form_html,
Expand All @@ -108,14 +110,15 @@
</table>
{%- endmacro %}

{% macro row(url, contest_url, translation, xsrf_form_html,
{% macro row(url, static_url, contest_url, translation, xsrf_form_html,
actual_phase, s, opaque_id, show_date,
can_use_tokens, can_play_token, can_play_token_now,
submissions_download_allowed) -%}
{#
Render a row in a submission table.

url (Url): the URL instance referring to the root of CWS.
static_url: static url helper constructed from url
contest_url (Url): the URL instance referring to the main contest page.
translation (Translation): locale to use to show messages.
xsrf_form_html (str): input element for the XSRF protection.
Expand Down Expand Up @@ -146,16 +149,16 @@
<td class="status">
{% if status == SubmissionResult.COMPILING %}
{% trans %}Compiling...{% endtrans %}
<img class="details" src="{{ url("static", "loading.gif") }}" />
<img class="details" src="{{ static_url("static", "loading.gif") }}" />
{% elif status == SubmissionResult.COMPILATION_FAILED %}
{% trans %}Compilation failed{% endtrans %}
<a class="details">{% trans %}details{% endtrans %}</a>
{% elif status == SubmissionResult.EVALUATING %}
{% trans %}Evaluating...{% endtrans %}
<img class="details" src="{{ url("static", "loading.gif") }}" />
<img class="details" src="{{ static_url("static", "loading.gif") }}" />
{% elif status == SubmissionResult.SCORING %}
{% trans %}Scoring...{% endtrans %}
<img class="details" src="{{ url("static", "loading.gif") }}" />
<img class="details" src="{{ static_url("static", "loading.gif") }}" />
{% elif status == SubmissionResult.SCORED %}
{% trans %}Evaluated{% endtrans %}
<a class="details">{% trans %}details{% endtrans %}</a>
Expand Down
4 changes: 2 additions & 2 deletions cms/server/contest/templates/task_description.html
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,9 @@ <h2>{% trans %}Attachments{% endtrans %}</h2>
<li>
<a href="{{ contest_url("tasks", task.name, "attachments", filename) }}" class="btn">
{% if type_icon is not none %}
<img src="{{ url("static", "img", "mimetypes", "%s.png"|format(type_icon)) }}" alt="{{ mime_type }}" />
<img src="{{ static_url("static", "img", "mimetypes", "%s.png"|format(type_icon)) }}" alt="{{ mime_type }}" />
{% else %}
<img src="{{ url("static", "img", "mimetypes", "unknown.png") }}" alt="{% trans %}unknown{% endtrans %}" />
<img src="{{ static_url("static", "img", "mimetypes", "unknown.png") }}" alt="{% trans %}unknown{% endtrans %}" />
{% endif %}
<span class="first_line">
<span class="name">{{ filename }}</span>
Expand Down
12 changes: 7 additions & 5 deletions cms/server/contest/templates/task_submissions.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
var submission_id = $(this).parent().parent().attr("data-submission");
var modal = $("#submission_detail");
var modal_body = modal.children(".modal-body");
modal_body.html('<div class="loading"><img src="{{ url("static", "loading.gif") }}"/>{% trans %}loading...{% endtrans %}</div>');
modal_body.html('<div class="loading"><img src="{{ static_url("static", "loading.gif") }}"/>{% trans %}loading...{% endtrans %}</div>');
modal_body.load(utils.contest_url("tasks", "{{ task.name }}", "submissions", submission_id, "details"), function(response, status, xhr) {
if(status != "success") {
$(this).html("{% trans %}Error loading details, please refresh the page.{% endtrans %}");
Expand Down Expand Up @@ -108,7 +108,7 @@
task_score_span.text(task_score_message);
if (task_score_is_partial) {
task_score_span.append(
$("<img class=\"details\" src=\"{{ url("static", "loading.gif") }}\"/>"));
$("<img class=\"details\" src=\"{{ static_url("static", "loading.gif") }}\"/>"));
}
task_score_elem.removeClass("undefined");
task_score_elem.removeClass("score_0");
Expand All @@ -124,7 +124,7 @@
var terminal_status = is_status_terminal(data["status"]);
if (!terminal_status) {
row.children("td.status").append(
$("<img class=\"details\" src=\"{{ url("static", "loading.gif") }}\"/>"));
$("<img class=\"details\" src=\"{{ static_url("static", "loading.gif") }}\"/>"));
} else {
row.children("td.status").append(
$("<a class=\"details\">{% trans %}details{% endtrans %}</a>"));
Expand Down Expand Up @@ -205,7 +205,7 @@ <h1>{% trans name=task.title, short_name=task.name %}{{ name }} ({{ short_name }
<span class="score">
{{ score_type.format_score(public_score, score_type.max_public_score, none, task.score_precision, translation=translation) }}
{% if is_score_partial %}
<img src="{{ url("static", "loading.gif") }}" />
<img src="{{ static_url("static", "loading.gif") }}" />
{% endif %}
</span>
</div>
Expand All @@ -227,7 +227,7 @@ <h1>{% trans name=task.title, short_name=task.name %}{{ name }} ({{ short_name }
{% if can_use_tokens %}
{{ score_type.format_score(tokened_score, score_type.max_score, none, task.score_precision, translation=translation) }}
{% if is_score_partial %}
<img src="{{ url("static", "loading.gif") }}" />
<img src="{{ static_url("static", "loading.gif") }}" />
{% endif %}
{% else %}
{% trans %}N/A{% endtrans %}
Expand Down Expand Up @@ -366,6 +366,7 @@ <h2 style="margin: 40px 0 10px">{% trans %}Previous submissions{% endtrans %}</h
<h3>{% trans %}Unofficial submissions{% endtrans %}</h3>
{{ macro_submission.rows(
url,
static_url,
contest_url,
translation,
xsrf_form_html,
Expand All @@ -382,6 +383,7 @@ <h3>{% trans %}Official submissions{% endtrans %}</h3>

{{ macro_submission.rows(
url,
static_url,
contest_url,
translation,
xsrf_form_html,
Expand Down
2 changes: 1 addition & 1 deletion cms/server/contest/templates/test_interface.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
var user_test_id = $this.parent().parent().attr("data-user-test");
var modal = $("#user_test_detail");
var modal_body = modal.children(".modal-body");
modal_body.html('<div class="loading"><img src="{{ url("static", "loading.gif") }}"/>{% trans %}loading...{% endtrans %}</div>');
modal_body.html('<div class="loading"><img src="{{ static_url("static", "loading.gif") }}"/>{% trans %}loading...{% endtrans %}</div>');
modal_body.load(utils.contest_url("tasks", task_id, "tests", user_test_id, "details"), function(response, status, xhr) {
if(status != "success") {
$(this).html("{% trans %}Error loading details, please refresh the page.{% endtrans %}");
Expand Down
6 changes: 5 additions & 1 deletion cms/server/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
from cms.server.file_middleware import FileServerMiddleware
from cmscommon.datetime import make_datetime

if typing.TYPE_CHECKING:
from cms.io.web_service import WebService

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -183,13 +185,15 @@ def __init__(self, *args, **kwargs):
self.r_params = None
self.contest = None
self.url: Url = None
self.static_url_helper = None

def prepare(self):
"""This method is executed at the beginning of each request.

"""
super().prepare()
self.url = Url(get_url_root(self.request.path))
self.static_url_helper = self.service.static_file_hasher.make(self.url)
self.set_header("Cache-Control", "no-cache, must-revalidate")

def finish(self, *args, **kwargs):
Expand All @@ -216,5 +220,5 @@ def finish(self, *args, **kwargs):
logger.debug("Connection closed before our reply.")

@property
def service(self):
def service(self) -> "WebService":
return self.application.service