mirror of
https://github.com/rystaf/mlmym.git
synced 2024-11-04 22:33:15 +00:00
csp and http only cookies
This commit is contained in:
parent
bf0bc421cd
commit
7ca1e23acf
|
@ -167,10 +167,10 @@ summary {
|
|||
|
||||
.comment {
|
||||
font-size: 14px;
|
||||
margin: 0px 0px 5px 15px;
|
||||
margin: 0px 0px 5px 0px;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 3px;
|
||||
padding: 5px 10px 5px 7px;
|
||||
padding: 10px 10px 0px 7px;
|
||||
}
|
||||
.dark .comment {
|
||||
border-color: #333;
|
||||
|
@ -182,7 +182,6 @@ summary {
|
|||
max-height: 300px;
|
||||
}
|
||||
.comment .comment {
|
||||
margin-left: 15px;
|
||||
}
|
||||
.comment .comment,
|
||||
.comment .comment .comment .comment,
|
||||
|
@ -243,18 +242,21 @@ summary {
|
|||
}
|
||||
.commentmenu {
|
||||
font-size: 16px;
|
||||
margin: 0px 0px 10px 10px;
|
||||
margin: 0px 0px 10px 0px;
|
||||
}
|
||||
.commentmenu div {
|
||||
border-top: 1px dotted gray;
|
||||
font-size: 12px;
|
||||
}
|
||||
form.savecomment {
|
||||
margin: 0px 0px 10px 10px;
|
||||
margin: 0px 0px 10px 0px;
|
||||
}
|
||||
.comment > .children > form.savecomment {
|
||||
margin: 0px 0px 10px 20px;
|
||||
}
|
||||
.comment .children {
|
||||
margin: 5px 0px 10px 15px;
|
||||
}
|
||||
.savecomment textarea {
|
||||
width: 500px;
|
||||
height: 100px;
|
||||
|
@ -275,11 +277,13 @@ form.savecomment {
|
|||
font-style: italic;
|
||||
font-weight: 700;
|
||||
}
|
||||
.comment.hidden .content, .comment.hidden .children {
|
||||
.comment.hidden .content, .comment.hidden .children, .comment.hidden .morecomments {
|
||||
display: none;
|
||||
}
|
||||
.children .morecomments {
|
||||
}
|
||||
.morecomments {
|
||||
margin: 10px 0px;
|
||||
margin: 0px 0px 10px 0px;
|
||||
font-size: 10px;
|
||||
}
|
||||
.morecomments a {
|
||||
|
@ -406,7 +410,7 @@ form.nsfw div {
|
|||
width: 100%;
|
||||
}
|
||||
.comment .buttons {
|
||||
margin: 3px 0px 10px 0px;
|
||||
margin: 3px 0px 0px 0px;
|
||||
}
|
||||
.comment.hidden .buttons {
|
||||
display: none;
|
||||
|
@ -481,7 +485,7 @@ form.nsfw div {
|
|||
position: relative;
|
||||
color: #000;
|
||||
}
|
||||
#settings {
|
||||
#settingspopup {
|
||||
background-color: white;
|
||||
border: 1px solid #888;
|
||||
display: none;
|
||||
|
@ -489,13 +493,13 @@ form.nsfw div {
|
|||
right: 10px;
|
||||
top: 45px;
|
||||
}
|
||||
#settings form {
|
||||
#settingspopup form {
|
||||
margin: 0px;
|
||||
}
|
||||
.dark #settings {
|
||||
.dark #settingspopup {
|
||||
background-color: #262626;
|
||||
}
|
||||
#settings.open {
|
||||
#settingspopup.open {
|
||||
display: inline-block;
|
||||
}
|
||||
.expando.open{
|
||||
|
@ -585,6 +589,7 @@ form.nsfw div {
|
|||
}
|
||||
main {
|
||||
position: relative;
|
||||
margin: 0px 10px;
|
||||
}
|
||||
@media (min-width: 900px) {
|
||||
.side {
|
||||
|
@ -594,8 +599,8 @@ main {
|
|||
right: 0;
|
||||
}
|
||||
main {
|
||||
padding-right: 310px;
|
||||
margin-left: 10px;
|
||||
padding-right: 316px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
.side form {
|
||||
|
|
|
@ -29,8 +29,8 @@ function postClick(e) {
|
|||
bdy.className = 'expando open';
|
||||
btn.className = "expando-button open"
|
||||
var url = targ.getElementsByClassName("url")[0].href
|
||||
if (id = parse_youtube(url)) {
|
||||
targ.getElementsByClassName("embed")[0].innerHTML = youtube_iframe(id)
|
||||
if (id = parseYoutube(url)) {
|
||||
targ.getElementsByClassName("embed")[0].innerHTML = youtubeIframe(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -114,23 +114,26 @@ function loadMore(e) {
|
|||
request(window.location.origin+window.location.pathname+"?"+urlParams.toString(), "",
|
||||
function(res){
|
||||
if (res.trim()) {
|
||||
e.target.outerHTML = res + '<input id="loadmore" type="submit" data-page="'+(parseInt(page)+1)+'" value="load more" onclick="loadMore(event)">'
|
||||
e.target.outerHTML = res + '<input id="loadmore" type="submit" data-page="'+(parseInt(page)+1)+'" value="load more">'
|
||||
if (showimages = document.getElementById("showimages")) {
|
||||
if (showimages.className == "selected") {
|
||||
toggle_images(true)
|
||||
toggleImages(true)
|
||||
}
|
||||
}
|
||||
var loadmore = document.getElementById("loadmore")
|
||||
if (loadmore) loadmore.className = "show"
|
||||
insert_youtube()
|
||||
loadmore.className = "show"
|
||||
loadmore.addEventListener("click", loadMore)
|
||||
setup()
|
||||
}
|
||||
else {
|
||||
e.target.outerHTML = '<input id="end" type="submit" value="" disabled>'
|
||||
}
|
||||
},
|
||||
function(res) {
|
||||
e.target.outerHTML = '<input id="loadmore" type="submit" data-page="'+parseInt(page)+'" value="loading failed" onclick="loadMore(event)">'
|
||||
document.getElementById("loadmore").className = "show"
|
||||
e.target.outerHTML = '<input id="loadmore" type="submit" data-page="'+parseInt(page)+'" value="loading failed">'
|
||||
var loadmore = document.getElementById("loadmore")
|
||||
loadmore.className = "show"
|
||||
loadmore.addEventListener("click", loadMore)
|
||||
}
|
||||
)
|
||||
return false;
|
||||
|
@ -177,9 +180,9 @@ function formSubmit(e) {
|
|||
return false
|
||||
}
|
||||
|
||||
function open_settings(e) {
|
||||
function openSettings(e) {
|
||||
e.preventDefault()
|
||||
var settings = document.getElementById("settings")
|
||||
var settings = document.getElementById("settingspopup")
|
||||
settings.className = "open"
|
||||
request(e.target.href + "?xhr=1", "", function(res) {
|
||||
settings.innerHTML = res
|
||||
|
@ -191,22 +194,23 @@ function open_settings(e) {
|
|||
input[0].checked = "checked"
|
||||
}
|
||||
}
|
||||
document.getElementById("settings").addEventListener("submit", saveSettings)
|
||||
document.getElementById("closesettings").addEventListener("click", closeSettings)
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
function close_settings(e) {
|
||||
function closeSettings(e) {
|
||||
e.preventDefault()
|
||||
var settings = document.getElementById("settings")
|
||||
var settings = document.getElementById("settingspopup")
|
||||
settings.className = ""
|
||||
return false
|
||||
}
|
||||
|
||||
function save_settings(e) {
|
||||
function saveSettings(e) {
|
||||
e = e || window.event;
|
||||
var targ = e.currentTarget || e.srcElement || e;
|
||||
var data = new FormData(targ)
|
||||
console.log(data)
|
||||
e.preventDefault()
|
||||
var params = new URLSearchParams(data).toString()
|
||||
request(targ.target, params, function(res) {
|
||||
|
@ -218,7 +222,7 @@ function save_settings(e) {
|
|||
return false;
|
||||
}
|
||||
|
||||
function parse_youtube(url){
|
||||
function parseYoutube(url){
|
||||
if (url.indexOf("youtu") == -1) return false
|
||||
var regExp = /^.*(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/|shorts\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/;
|
||||
var match = url.match(regExp);
|
||||
|
@ -227,27 +231,26 @@ function parse_youtube(url){
|
|||
}
|
||||
return false
|
||||
}
|
||||
function youtube_iframe(id) {
|
||||
function youtubeIframe(id) {
|
||||
return '<iframe width="560" height="315" src="https://www.youtube.com/embed/'+id+'" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>'
|
||||
}
|
||||
|
||||
function show_images(e) {
|
||||
function showImages(e) {
|
||||
e = e || window.event;
|
||||
e.preventDefault()
|
||||
var targ = e.currentTarget || e.srcElement || e;
|
||||
console.log(targ)
|
||||
var parent = targ.parentNode
|
||||
if (parent.className == "") {
|
||||
parent.className = "selected"
|
||||
toggle_images(true)
|
||||
toggleImages(true)
|
||||
} else {
|
||||
parent.className = ""
|
||||
toggle_images(false)
|
||||
toggleImages(false)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function toggle_images(open) {
|
||||
function toggleImages(open) {
|
||||
var posts = document.getElementsByClassName("post")
|
||||
for (var i = 0; i < posts.length; i++) {
|
||||
var btn = posts[i].getElementsByClassName("expando-button")[0]
|
||||
|
@ -265,27 +268,48 @@ function toggle_images(open) {
|
|||
}
|
||||
}
|
||||
|
||||
function insert_youtube() {
|
||||
function setup() {
|
||||
if (showimages = document.getElementById("se")) {
|
||||
showimages.addEventListener("click", showImages)
|
||||
}
|
||||
if (settings = document.getElementById("opensettings")) {
|
||||
settings.addEventListener("click", openSettings)
|
||||
}
|
||||
if (hidechildren = document.getElementById("hidechildren")){
|
||||
hidechildren.addEventListener("click", hideAllChildComments)
|
||||
}
|
||||
var posts = document.getElementsByClassName("post")
|
||||
for (var i = 0; i < posts.length; i++) {
|
||||
posts[i].addEventListener("click", postClick)
|
||||
var voteForm = posts[i].getElementsByClassName("link-btn")
|
||||
if (voteForm.length) {
|
||||
voteForm[0].addEventListener("submit", formSubmit)
|
||||
}
|
||||
var url = posts[i].getElementsByClassName("url")[0].href
|
||||
if (id = parse_youtube(url)) {
|
||||
if (id = parseYoutube(url)) {
|
||||
var btn = posts[i].getElementsByClassName("expando-button")[0]
|
||||
if (btn.className.indexOf("open") > -1) {
|
||||
posts[i].getElementsByClassName("embed")[0].innerHTML = youtube_iframe(id)
|
||||
posts[i].getElementsByClassName("embed")[0].innerHTML = youtubeIframe(id)
|
||||
} else {
|
||||
btn.className = "expando-button"
|
||||
}
|
||||
}
|
||||
}
|
||||
var comments = document.getElementsByClassName("comment")
|
||||
for (var i = 0; i < comments.length; i++) {
|
||||
comments[i].addEventListener("click", commentClick)
|
||||
}
|
||||
}
|
||||
insert_youtube()
|
||||
setup()
|
||||
|
||||
if (localStorage.getItem("endlessScrolling") == "true") {
|
||||
var pager = document.getElementsByClassName("pager")
|
||||
if (pager.length) pager[0].className = "pager hidden"
|
||||
var loadmore = document.getElementById("loadmore")
|
||||
if (loadmore) loadmore.className = "show"
|
||||
if (loadmore) {
|
||||
loadmore.className = "show"
|
||||
loadmore.addEventListener("click", loadMore)
|
||||
}
|
||||
|
||||
if (localStorage.getItem("autoLoad") == "true") {
|
||||
window.onscroll = function(e) {
|
||||
|
@ -299,4 +323,11 @@ if (localStorage.getItem("endlessScrolling") == "true") {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// delete cookies without HTTPOnly
|
||||
var cookies = document.cookie.split(";");
|
||||
for (var i = 0; i < cookies.length; i++) {
|
||||
var cookie = cookies[i];
|
||||
var eqPos = cookie.indexOf("=");
|
||||
var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
|
||||
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;SameSite=None;Secure";
|
||||
}
|
||||
|
|
15
routes.go
15
routes.go
|
@ -194,7 +194,7 @@ func Initialize(Host string, r *http.Request) (State, error) {
|
|||
token := getCookie(r, "jwt")
|
||||
user := getCookie(r, "user")
|
||||
parts := strings.Split(user, ":")
|
||||
if len(parts) == 2 {
|
||||
if len(parts) == 2 && token != "" {
|
||||
if id, err := strconv.Atoi(parts[1]); err == nil {
|
||||
state.Client.Token = token
|
||||
sess := Session{
|
||||
|
@ -248,6 +248,8 @@ func Render(w http.ResponseWriter, templateName string, state State) {
|
|||
if state.Status != http.StatusOK {
|
||||
w.WriteHeader(state.Status)
|
||||
}
|
||||
header := w.Header()
|
||||
header.Set("Content-Security-Policy", "script-src 'self'")
|
||||
err = tmpl.Execute(w, state)
|
||||
if err != nil {
|
||||
fmt.Println("execute fail", err)
|
||||
|
@ -504,10 +506,13 @@ func setCookie(w http.ResponseWriter, host string, name string, value string) {
|
|||
host = ""
|
||||
}
|
||||
cookie := http.Cookie{
|
||||
Name: name,
|
||||
Value: value,
|
||||
MaxAge: 86400 * 30,
|
||||
Path: "/" + host,
|
||||
Name: name,
|
||||
Value: value,
|
||||
MaxAge: 86400 * 30,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteNoneMode,
|
||||
Secure: true,
|
||||
Path: "/" + host,
|
||||
}
|
||||
http.SetCookie(w, &cookie)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<div class="comment{{if or (lt .P.Counts.Score -5) .P.Comment.Deleted }} hidden{{end}}" id="c{{.P.Comment.ID}}" onclick="commentClick(event)">
|
||||
<div class="meta">
|
||||
<div class="comment{{if or (lt .P.Counts.Score -5) .P.Comment.Deleted }} hidden{{end}}" id="c{{.P.Comment.ID}}">
|
||||
{{ if .State.Session }}
|
||||
<div class="score">
|
||||
<form class="link-btn{{ if eq .P.MyVote.String "1"}} like{{ else if eq .P.MyVote.String "-1"}} dislike{{end}}" method="POST">
|
||||
|
@ -14,6 +13,7 @@
|
|||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="meta">
|
||||
<a class="minimize" href="" for="c{{.P.Comment.ID}}">
|
||||
{{if or (lt .P.Counts.Score -5) .P.Comment.Deleted }}
|
||||
[+]
|
||||
|
@ -105,11 +105,11 @@
|
|||
</form>
|
||||
{{ end}}
|
||||
{{ range $ci, $child := .C }}{{ template "comment.html" $child }}{{end}}
|
||||
</div>
|
||||
{{ if ne .P.Counts.ChildCount .ChildCount}}
|
||||
<div class="morecomments">
|
||||
<a class="loadmore" for="c{{ .P.Comment.ID}}" href="/{{.State.Host}}/comment/{{.P.Comment.ID}}?">load more comments</a>
|
||||
<span class="gray">({{ sub .P.Counts.ChildCount .ChildCount}} replies)</span>
|
||||
</div>
|
||||
<div class="morecomments">
|
||||
<a class="loadmore" for="c{{ .P.Comment.ID}}" href="/{{.State.Host}}/comment/{{.P.Comment.ID}}?">load more comments</a>
|
||||
<span class="gray">({{ sub .P.Counts.ChildCount .ChildCount}} replies)</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<head>
|
||||
<title>{{ if and .Community (ne .Community.CommunityView.Community.Title "")}}{{.Community.CommunityView.Community.Title}}{{else if ne .CommunityName ""}}/c/{{.CommunityName}}{{ else if .User}}overview for {{.User.PersonView.Person.Name}}{{else}}{{ host .Host }}{{end}}</title>
|
||||
<link rel="shortcut icon" href="/{{.Host}}/icon.jpg">
|
||||
<link rel="stylesheet" href="/_/static/style.css?v=16">
|
||||
<link rel="stylesheet" href="/_/static/style.css?v=17">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
<body {{ if .Dark }}class="dark"{{end}}>
|
||||
|
@ -50,12 +50,12 @@
|
|||
<div class="pager">
|
||||
view more: {{if gt .Page 1 }}<a href="{{ .PrevPage }}">‹ prev</a>{{ end }} <a href="{{ .NextPage }}">next ›</a>
|
||||
</div>
|
||||
<input id="loadmore" type="submit" value="load more" onclick="loadMore(event)" data-page="2">
|
||||
<input id="loadmore" type="submit" value="load more" data-page="2">
|
||||
{{ end }}
|
||||
|
||||
{{ template "sidebar.html" . }}
|
||||
</main>
|
||||
{{ end }}
|
||||
<script src="/_/static/utils.js?v=9"></script>
|
||||
<script src="/_/static/utils.js?v=12"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<title>{{if and .Posts .PostID }}{{ (index .Posts 0).Post.Name}} : {{.CommunityName}}{{else if and .Community (ne .Community.CommunityView.Community.Title "")}}{{.Community.CommunityView.Community.Title}}{{else if ne .CommunityName ""}}/c/{{.CommunityName}}{{ else if .User}}overview for {{.User.PersonView.Person.Name}}{{else}}{{ host .Host }}{{end}}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="shortcut icon" href="/{{.Host}}/icon.jpg">
|
||||
<link rel="stylesheet" href="/_/static/style.css?v=16">
|
||||
<link rel="stylesheet" href="/_/static/style.css?v=17">
|
||||
</head>
|
||||
<body{{ if .Dark }} class="dark"{{end}}>
|
||||
<noscript>
|
||||
|
@ -122,10 +122,10 @@
|
|||
<div class="pager">
|
||||
view more: {{if gt .Page 1 }}<a href="{{ .PrevPage }}">‹ prev</a>{{ end }} <a href="{{ .NextPage }}">next ›</a>
|
||||
</div>
|
||||
<input id="loadmore" type="submit" value="load more" onclick="loadMore(event)" data-page="2">
|
||||
<input id="loadmore" type="submit" value="load more" data-page="2">
|
||||
{{ end }}
|
||||
|
||||
<script src="/_/static/utils.js?v=9"></script>
|
||||
<script src="/_/static/utils.js?v=12"></script>
|
||||
{{ template "sidebar.html" . }}
|
||||
</main>
|
||||
{{ end }}
|
||||
|
|
|
@ -17,16 +17,16 @@
|
|||
|
|
||||
<a href="/{{.Host}}/inbox" class="mailbox{{ if .UnreadCount }} orangered{{end}}">✉</a>
|
||||
|
|
||||
<a href="/{{.Host}}/settings" onclick="open_settings(event)">settings</a>
|
||||
<a id="opensettings" href="/{{.Host}}/settings">settings</a>
|
||||
|
|
||||
<form method="POST"><input type="submit" name="op" value="logout"></form>
|
||||
{{else}}
|
||||
<a href="/{{.Host}}/login">log in</a> or <a href="/{{.Host}}/login">sign up</a>
|
||||
|
|
||||
<a href="/{{.Host}}/settings" onclick="open_settings(event)">settings</a>
|
||||
<a id="opensettings" href="/{{.Host}}/settings">settings</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<div id="settings"></div>
|
||||
<div id="settingspopup"></div>
|
||||
<div class="spacer">
|
||||
<a href="/{{ .Host}}/">
|
||||
<img class="icon" src="{{ if .Site }}{{ .Site.SiteView.Site.Icon.String }}{{else}}/{{ .Host}}/icon.jpg{{end}}">
|
||||
|
@ -66,7 +66,7 @@
|
|||
<li{{ if eq .Sort "NewComments" }} class="selected"{{end}}><a href="{{ .SortBy "NewComments" }}">new comments</a></li>
|
||||
<li{{ if contains .Sort "Top" }} class="selected"{{end}}><a href="{{ .SortBy "TopDay" }}">top</a></li>
|
||||
{{ if .Posts }}
|
||||
<li id="showimages"><a href="" onclick="show_images(event)">show images</a></li>
|
||||
<li id="showimages"><a id="se" href="">show images</a></li>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</ul>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{{ if ne .State.Op "vote_post" }}
|
||||
<div class="post{{if .Post.Deleted}} deleted{{end}}{{ if or .Post.FeaturedCommunity .Post.FeaturedLocal }} distinguished{{end}}" onclick="postClick(event)">
|
||||
<div class="post{{if .Post.Deleted}} deleted{{end}}{{ if or .Post.FeaturedCommunity .Post.FeaturedLocal }} distinguished{{end}}">
|
||||
{{ if gt .Rank 0 }}
|
||||
<div class="rank"> {{ .Rank }} </div>
|
||||
{{ end }}
|
||||
<div class="score">
|
||||
{{ end }}
|
||||
{{ if .State.Session }}
|
||||
<form class="link-btn {{ if lt .Rank 1 }}squish{{end}}{{ if eq .MyVote.String "1" }} like{{else if eq .MyVote.String "-1"}} dislike{{end}}" method="POST" onsubmit="formSubmit(event)">
|
||||
<form class="link-btn {{ if lt .Rank 1 }}squish{{end}}{{ if eq .MyVote.String "1" }} like{{else if eq .MyVote.String "-1"}} dislike{{end}}" method="POST">
|
||||
<input type="submit" name="vote" value="▲">
|
||||
{{ if .MyVote.IsValid}}
|
||||
<input type="hidden" name="undo" value="{{.MyVote.String}}">
|
||||
|
@ -71,7 +71,7 @@
|
|||
</form>
|
||||
{{end}}
|
||||
{{ if .State.PostID }}
|
||||
<a class="hidechildren" onclick="hideAllChildComments(event)" href="">hide all child comments</a>
|
||||
<a id="hidechildren" class="scripting" href="">hide all child comments</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
<div class="error">{{.Error}}</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<form class="preferences" method="POST" target="/{{.Host}}/settings" onsubmit="save_settings(event)" >
|
||||
<form id="settings" class="preferences" method="POST" target="/{{.Host}}/settings">
|
||||
<div>
|
||||
<label>
|
||||
default listing
|
||||
|
@ -114,7 +114,7 @@
|
|||
<div>
|
||||
<label></label>
|
||||
<input type="submit" value="save">
|
||||
{{ if .XHR }}<input type="submit" value="close" onclick="close_settings(event)">{{ end }}
|
||||
{{ if .XHR }}<input id="closesettings" type="submit" value="close">{{ end }}
|
||||
</div>
|
||||
</form>
|
||||
{{ if not .XHR}}
|
||||
|
|
Loading…
Reference in a new issue