Port 4ducky to Rust (Axum + SQLite)

Same design, layout, URLs and features as the PHP version: browse grid,
image view with AJAX comments, BBCode, likes, uploads (file, URL and
bookmarklet), quicktagger, profile, block list, inbox, invites, Hall of
Ducks, Truhe, abuse reports and the admin tools.

- Parameterized SQL, escaped templates (askama), argon2 passwords,
  server-side sessions, CSRF tokens on every write, strict script CSP
- SSRF-safe remote image fetching, magic-byte upload checks
- Self-hosted captcha replaces reCAPTCHA v1; analytics/social widgets removed
- Vanilla JS replaces jQuery and its plugins with the same DOM behaviour
- Reverse proxy support (frontend_path, trust_proxy, secure_cookies)
- Admin CLI: admin, settings, invites

See README.md for running it and the full list of changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 02:15:05 +02:00
co-authored by Claude Opus 5.5
parent 664d8c6117
commit d0f2d526df
56 changed files with 11133 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
// Bookmarklet script (was media/post.js.php). Injected into third party pages.
// The dialog iframe only shows instructions; posting happens in a first-party
// popup window so the session cookie (SameSite) and CSRF token work.
function RP_RemotePost( postURL, stylesheet ) {
this.postURL = postURL;
this.stylesheet = stylesheet;
this.visible = false;
this.dialog = null;
this.iframe = null;
this.minImageSize = 32;
this.create = function(){
var that = this;
var css = document.createElement('link');
css.type = 'text/css';
css.rel = 'stylesheet';
css.href = this.stylesheet;
(document.head || document.body).appendChild( css );
var closeButton = document.createElement('a');
closeButton.appendChild( document.createTextNode("x") );
closeButton.className = 'close';
closeButton.onclick = function() { return that.toggle(); };
closeButton.href = '#';
this.dialog = document.createElement('div');
this.dialog.id = 'RP_Dialog';
this.iframe = document.createElement('iframe');
this.iframe.src = this.postURL + '?frame=1';
this.dialog.appendChild( this.iframe );
this.dialog.appendChild( closeButton );
document.body.appendChild( this.dialog );
};
this.selectImage = function( image ) {
var imageSrc = image.src;
if(
image.parentNode.tagName.match(/^a$/i) &&
image.parentNode.href &&
image.parentNode.href.match(/\.(jpe?g|gif|png)$/i)
) {
imageSrc = image.parentNode.href;
}
window.open(
this.postURL + '?url=' + encodeURIComponent( imageSrc ),
'RP_Post',
'width=520,height=260,scrollbars=yes'
);
this.hide();
return false;
};
this.show = function() {
this.visible = true;
var that = this;
this.dialog.style.display = 'block';
var images = document.getElementsByTagName('img');
for( var i=0; i<images.length; i++ ) {
var img = images[i];
if( img && img.src && img.src.match(/(space|blank)[^\/]*\.gif$/i) ) {
img.style.display = 'none';
}
else if( img && img.src && img.width > this.minImageSize && img.height > this.minImageSize) {
img.onclick = function( ev ) {
if( ev && ev.stopPropagation ) {
ev.stopPropagation();
}
return that.selectImage(this);
};
img.className = img.className ? img.className + ' RP_PostImage' : 'RP_PostImage';
}
}
};
this.hide = function() {
this.visible = false;
this.dialog.style.display = 'none';
var images = document.getElementsByTagName('img');
for( var i=0; i<images.length; i++ ) {
var img = images[i];
if( img && img.src && img.width > this.minImageSize && img.height > this.minImageSize) {
img.onclick = null;
img.className = img.className.replace(/\s*RP_PostImage/, '');
}
}
};
this.toggle = function() {
if( !this.visible ) {
this.show();
} else {
this.hide();
}
return false;
};
this.create();
}
if( typeof(RP_Instance) == 'undefined' ) {
var RP_Instance = new RP_RemotePost( %POST_URL%, %STYLESHEET% );
}
RP_Instance.toggle();