mirror of
https://github.com/FreshRSS/FreshRSS.git
synced 2026-11-05 00:52:57 -05:00
* Replace bcrypt challenge auth with direct password comparison Closes #2428 Highlights: * Replaced the bcrypt challenge authentication flow with direct password comparison against the bcrypt hash stored in the user configuration. * Removed the `bcrypt.js` dependency, so authentication no longer depends on JavaScript. * Removed the nonce and challenge mechanism and the `nonce` endpoint from the `javascript` controller. * Removed the JavaScript and HTML code used for the previous authentication flow. * Removed the `<noscript>` blocks that were only needed for the previous authentication flow. * Updated `FreshRSS_FormAuth::checkCredentials()` to accept the username, stored bcrypt hash, and plain-text password directly. * The stored password hash must still use the expected bcrypt format. * Empty or whitespace-only passwords are rejected even if the hash would otherwise match. * The bcrypt 72-byte limit is enforced during normal authentication. * Existing bcrypt passwords are handled correctly when the 72-byte limit cuts through a UTF-8 character. * Password input is no longer trimmed before authentication or password changes. Leading and trailing whitespace is therefore kept as part of the password. * Added `FreshRSS_FormAuth::passwordRequirementsMet()` to replace `FreshRSS_password_Util::check()`. * New passwords must be between 7 and 72 bytes by default. * The minimum length check can be disabled where needed. * Password requirements are now checked server-side for all password creation and update paths. * Added client-side byte-length validation for password fields using `TextEncoder` and `data-max-bytes`. * This gives better feedback for UTF-8 passwords. * Server-side validation remains authoritative. * Updated password handling in authentication, re-authentication, self-deletion, user creation, and user update paths so passwords are not trimmed automatically. * Added `FormAuthTest` to cover: * password length limits in bytes * UTF-8 passwords around the 72-byte limit * bcrypt truncation behavior during authentication * malformed and non-bcrypt hashes * empty passwords * authentication failures and log messages * Removed `bcrypt.js` from the documented dependencies and updated the authentication documentation. * Updated the installation and authentication views and JavaScript to match the new password handling. This simplifies the authentication flow, removes the client-side bcrypt challenge, makes password validation consistent between the client and server, and adds more test coverage while keeping compatibility with existing bcrypt password hashes. Because the password is now sent directly to the server instead of being processed by `bcrypt.js`, deployments should use HTTPS. Documentation for HTTPS setup can be handled separately. * Add test case with password truncated down to 71 characters * Compare log messages too during form auth tests * Mark the `getLastLog()` method as impure * Fix: Check password reqs against new password instead of current * Fix: Add custom JS validator for UTF-8, handle cut-off UTF-8 during login * Fix: Enforce password requirements everywhere server-side * Add TODO for updateUser in CLI * make readme * Fix extra.js in install.php * Don't report validity yet if the password is too short or not filled out * Simplify checking of whether to report validity * Fix: Do not trim password from user's input * Also `trim: false` in some userController paths * Use the proper argument order for PHPUnit's `assertSame()` * Remove `maxlength` in favor of `data-max-bytes` But add `pattern` again for some nojs support * make readme * composer run-script translations * Update i18n string for web form admin setting * i18n: fr * Minor whitespace --------- Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
620 lines
18 KiB
JavaScript
620 lines
18 KiB
JavaScript
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
|
|
'use strict';
|
|
/* globals context, notifs_html5_is_supported */
|
|
|
|
function forgetOpenCategories() {
|
|
localStorage.removeItem('FreshRSS_open_categories');
|
|
}
|
|
|
|
function init_display(parent) {
|
|
const theme = parent.querySelector('select#theme');
|
|
if (!theme) {
|
|
return;
|
|
}
|
|
theme.addEventListener('change', (e) => {
|
|
const picked = parent.querySelector('.preview-container.picked');
|
|
picked.classList.remove('picked');
|
|
parent.querySelector(`[data-theme-preview="${e.target.value}"]`).classList.add('picked');
|
|
});
|
|
}
|
|
|
|
// <password forms>
|
|
function togglePW(btn) {
|
|
if (btn.classList.contains('active')) {
|
|
hidePW(btn);
|
|
} else {
|
|
showPW(btn);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function showPW(btn) {
|
|
const passwordField = btn.previousElementSibling;
|
|
passwordField.setAttribute('type', 'text');
|
|
btn.classList.add('active');
|
|
clearTimeout(btn.timeoutHide);
|
|
btn.timeoutHide = setTimeout(function () { hidePW(btn); }, 5000);
|
|
return false;
|
|
}
|
|
|
|
function hidePW(btn) {
|
|
clearTimeout(btn.timeoutHide);
|
|
const passwordField = btn.previousElementSibling;
|
|
passwordField.setAttribute('type', 'password');
|
|
passwordField.nextElementSibling.classList.remove('active');
|
|
return false;
|
|
}
|
|
|
|
function init_password_observers(parent) {
|
|
parent.querySelectorAll('.toggle-password').forEach(function (btn) {
|
|
btn.onclick = () => togglePW(btn);
|
|
});
|
|
}
|
|
|
|
function init_password_byte_length_validation(parent) {
|
|
parent.querySelectorAll('input[type="password"][data-max-bytes]').forEach(input => {
|
|
function validate() {
|
|
// Display custom validation error for the password input or unset any custom errors.
|
|
if (new TextEncoder().encode(input.value).length > +input.dataset.maxBytes) {
|
|
input.setCustomValidity(context.i18n.provided_password_too_long);
|
|
} else {
|
|
input.setCustomValidity('');
|
|
}
|
|
|
|
if (input.validity.customError) {
|
|
// Update the displayed validation status immediately before
|
|
// the user attempts to submit the form.
|
|
input.reportValidity();
|
|
}
|
|
}
|
|
|
|
input.addEventListener('input', validate);
|
|
validate();
|
|
});
|
|
}
|
|
// </password forms>
|
|
|
|
function init_archiving(parent) {
|
|
parent.addEventListener('change', function (e) {
|
|
if (e.target.id === 'use_default_purge_options') {
|
|
parent.querySelectorAll('.archiving').forEach(function (element) {
|
|
element.hidden = e.target.checked;
|
|
if (!e.target.checked) element.style.visibility = 'visible'; // Help for Edge 44
|
|
});
|
|
}
|
|
});
|
|
parent.addEventListener('click', function (e) {
|
|
if (e.target.closest('button[type=reset]')) {
|
|
const archiving = document.getElementById('use_default_purge_options');
|
|
if (archiving) {
|
|
parent.querySelectorAll('.archiving').forEach(function (element) {
|
|
element.hidden = archiving.getAttribute('data-leave-validation') == 1;
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function init_update_feed() {
|
|
const feed_update = document.querySelector('div.post#feed_update');
|
|
if (!feed_update) {
|
|
return;
|
|
}
|
|
|
|
const faviconUpload = feed_update.querySelector('#favicon-upload');
|
|
const resetFavicon = feed_update.querySelector('#reset-favicon');
|
|
const faviconError = feed_update.querySelector('#favicon-error');
|
|
const faviconExt = feed_update.querySelector('#favicon-ext');
|
|
const extension = faviconExt.querySelector('b');
|
|
const faviconExtBtn = feed_update.querySelector('#favicon-ext-btn');
|
|
const favicon = feed_update.querySelector('.favicon');
|
|
|
|
function clearUploadedIcon() {
|
|
faviconUpload.value = '';
|
|
}
|
|
function discardIconChange() {
|
|
const resetField = feed_update.querySelector('input[name="resetFavicon"]');
|
|
if (resetField) {
|
|
resetField.remove();
|
|
}
|
|
const extBtn = feed_update.querySelector('input#extBtn');
|
|
if (extBtn) {
|
|
extBtn.remove();
|
|
}
|
|
if (faviconExtBtn) {
|
|
faviconExtBtn.disabled = false;
|
|
extension.innerText = extension.dataset.initialExt ?? extension.innerText;
|
|
}
|
|
if (extension.innerText == '') {
|
|
faviconExt.classList.add('hidden');
|
|
}
|
|
clearUploadedIcon();
|
|
favicon.src = favicon.dataset.initialSrc;
|
|
|
|
const isCustomFavicon = favicon.getAttribute('src') !== favicon.dataset.originalIcon;
|
|
resetFavicon.disabled = !isCustomFavicon;
|
|
}
|
|
|
|
faviconUpload.onchange = function () {
|
|
if (faviconUpload.files.length === 0) {
|
|
return;
|
|
}
|
|
|
|
faviconExt.classList.add('hidden');
|
|
if (faviconUpload.files[0].size > context.max_favicon_upload_size) {
|
|
faviconError.innerHTML = context.i18n.favicon_size_exceeded;
|
|
discardIconChange();
|
|
return;
|
|
}
|
|
if (faviconExtBtn) {
|
|
faviconExtBtn.disabled = false;
|
|
extension.innerText = extension.dataset.initialExt ?? extension.innerText;
|
|
}
|
|
faviconError.innerHTML = '';
|
|
|
|
const resetField = feed_update.querySelector('input[name="resetFavicon"]');
|
|
if (resetField) {
|
|
resetField.remove();
|
|
}
|
|
const extBtn = feed_update.querySelector('input#extBtn');
|
|
if (extBtn) {
|
|
extBtn.remove();
|
|
}
|
|
resetFavicon.disabled = false;
|
|
favicon.src = URL.createObjectURL(faviconUpload.files[0]);
|
|
};
|
|
|
|
resetFavicon.onclick = function (e) {
|
|
e.preventDefault();
|
|
if (resetFavicon.disabled) {
|
|
return;
|
|
}
|
|
if (faviconExtBtn) {
|
|
faviconExtBtn.disabled = false;
|
|
extension.innerText = extension.dataset.initialExt ?? extension.innerText;
|
|
}
|
|
|
|
faviconExt.classList.add('hidden');
|
|
faviconError.innerHTML = '';
|
|
clearUploadedIcon();
|
|
resetFavicon.insertAdjacentHTML('afterend', '<input type="hidden" name="resetFavicon" value="1" data-leave-validation="" />');
|
|
resetFavicon.disabled = true;
|
|
|
|
favicon.src = favicon.dataset.originalIcon;
|
|
};
|
|
|
|
feed_update.querySelector('form').addEventListener('reset', () => {
|
|
faviconExt.classList.remove('hidden');
|
|
faviconError.innerHTML = '';
|
|
discardIconChange();
|
|
});
|
|
|
|
if (faviconExtBtn) {
|
|
faviconExtBtn.onclick = async function (e) {
|
|
e.preventDefault();
|
|
faviconExtBtn.disabled = true;
|
|
try {
|
|
const resp = await fetch(faviconExtBtn.dataset.extensionUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json; charset=UTF-8',
|
|
},
|
|
body: JSON.stringify({
|
|
'_csrf': context.csrf,
|
|
'extAction': 'query_icon_info',
|
|
'id': +feed_update.dataset.feedId
|
|
}),
|
|
});
|
|
if (!resp.ok) {
|
|
faviconExtBtn.disabled = false;
|
|
throw new Error(`Custom favicons HTTP error ${resp.status}: ${resp.statusText}`);
|
|
}
|
|
const json = await resp.json();
|
|
clearUploadedIcon();
|
|
const resetField = feed_update.querySelector('input[name="resetFavicon"]');
|
|
if (resetField) {
|
|
resetField.remove();
|
|
}
|
|
faviconExtBtn.insertAdjacentHTML('afterend', '<input type="hidden" id="extBtn" value="1" data-leave-validation="" />');
|
|
resetFavicon.disabled = false;
|
|
faviconError.innerHTML = '';
|
|
faviconExt.classList.remove('hidden');
|
|
extension.dataset.initialExt = extension.innerText;
|
|
extension.innerText = json.extName;
|
|
favicon.src = json.iconUrl;
|
|
} catch (error) {
|
|
faviconExtBtn.disabled = false;
|
|
}
|
|
};
|
|
faviconExtBtn.form.onsubmit = async function (e) {
|
|
const extChanged = faviconExtBtn.disabled;
|
|
const isSubmit = !e.submitter.hasAttribute('formaction');
|
|
|
|
if (extChanged && isSubmit) {
|
|
e.preventDefault();
|
|
faviconExtBtn.form.querySelectorAll('[type="submit"]').forEach(el => {
|
|
el.disabled = true;
|
|
});
|
|
await fetch(faviconExtBtn.dataset.extensionUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json; charset=utf-8'
|
|
},
|
|
body: JSON.stringify({
|
|
'_csrf': context.csrf,
|
|
'extAction': 'update_icon',
|
|
'id': +feed_update.dataset.feedId
|
|
}),
|
|
});
|
|
faviconExtBtn.form.onsubmit = null;
|
|
faviconExtBtn.form.submit();
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
// <slider>
|
|
const freshrssSliderLoadEvent = new Event('freshrss:slider-load');
|
|
|
|
function open_slider_listener(ev) {
|
|
if (ev.ctrlKey || ev.shiftKey) {
|
|
return;
|
|
}
|
|
const a = ev.target.closest('.open-slider');
|
|
if (a) {
|
|
if (!context.ajax_loading) {
|
|
context.ajax_loading = true;
|
|
const slider = document.getElementById('slider');
|
|
const slider_content = document.getElementById('slider-content');
|
|
const req = new XMLHttpRequest();
|
|
slider_content.innerHTML = '';
|
|
slider.classList.add('sliding');
|
|
const ahref = a.href + '&ajax=1#slider';
|
|
req.open('GET', ahref, true);
|
|
req.responseType = 'document';
|
|
req.onload = function (e) {
|
|
if (this.status === 403) {
|
|
// Redirect to reauth page (or fail if session expired)
|
|
location.href = a.href;
|
|
return;
|
|
}
|
|
location.href = '#slider'; // close menu/dropdown
|
|
document.documentElement.classList.add('slider-active');
|
|
slider.classList.add('active');
|
|
slider.scrollTop = 0;
|
|
slider_content.innerHTML = this.response.body.innerHTML;
|
|
data_auto_leave_validation(slider);
|
|
init_update_feed();
|
|
slider_content.querySelectorAll('form').forEach(function (f) {
|
|
f.insertAdjacentHTML('afterbegin', '<input type="hidden" name="slider" value="1" />');
|
|
});
|
|
context.ajax_loading = false;
|
|
|
|
window.addEventListener('hashchange', () => {
|
|
if (location.hash.substr(1) === 'slider') {
|
|
// Triggers when slider is closed
|
|
window.addEventListener('hashchange', () => {
|
|
location.hash = 'close';
|
|
slider.querySelectorAll('form').forEach(function (f) { f.reset(); });
|
|
document.documentElement.classList.remove('slider-active');
|
|
}, { once: true });
|
|
}
|
|
}, { once: true });
|
|
|
|
slider.dispatchEvent(freshrssSliderLoadEvent);
|
|
};
|
|
req.send();
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
function init_slider(slider) {
|
|
window.onclick = open_slider_listener;
|
|
|
|
document.getElementById('close-slider').addEventListener('click', close_slider_listener);
|
|
document.querySelector('#slider .toggle_aside').addEventListener('click', close_slider_listener);
|
|
slider.addEventListener('click', ev => {
|
|
if (ev.target.closest('.extension-configure button[type="reset"], .extension-configure input[type="reset"]') && close_slider_listener(ev)) {
|
|
location.hash = 'close';
|
|
}
|
|
});
|
|
|
|
if (slider.children.length > 0) {
|
|
const slider_scrollTop = sessionStorage.getItem('FreshRSS_slider_scrollTop');
|
|
if (slider_scrollTop) {
|
|
slider.scrollTop = +slider_scrollTop;
|
|
sessionStorage.removeItem('FreshRSS_slider_scrollTop');
|
|
}
|
|
slider.submitListener = () => sessionStorage.setItem('FreshRSS_slider_scrollTop', slider.scrollTop);
|
|
slider.addEventListener('submit', slider.submitListener);
|
|
slider.dispatchEvent(freshrssSliderLoadEvent);
|
|
}
|
|
}
|
|
|
|
function close_slider_listener(ev) {
|
|
const slider = document.getElementById('slider');
|
|
if (data_leave_validation(slider) || confirm(context.i18n.confirm_exit_slider)) {
|
|
slider.removeEventListener('submit', slider.submitListener);
|
|
return true;
|
|
}
|
|
if (ev) {
|
|
ev.preventDefault();
|
|
}
|
|
return false;
|
|
}
|
|
// </slider>
|
|
|
|
// updates href from the input value, with optional prefix/encoding
|
|
function updateHref(ev) {
|
|
const urlField = document.getElementById(this.getAttribute('data-input'));
|
|
const rawUrl = urlField.value;
|
|
const prefix = this.getAttribute('data-prefix') || '';
|
|
const shouldEncode = this.getAttribute('data-encode') === '1';
|
|
const url = prefix + (shouldEncode ? encodeURIComponent(rawUrl) : rawUrl);
|
|
if (rawUrl.length > 0) {
|
|
this.href = url;
|
|
return true;
|
|
} else {
|
|
urlField.focus();
|
|
this.removeAttribute('href');
|
|
ev.preventDefault();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// set event listener on "show url" buttons
|
|
function init_url_observers(parent) {
|
|
parent.querySelectorAll('.open-url').forEach(function (btn) {
|
|
btn.addEventListener('mouseover', updateHref);
|
|
btn.addEventListener('click', updateHref);
|
|
});
|
|
}
|
|
|
|
function init_select_observers() {
|
|
document.querySelectorAll('.select-change').forEach(function (s) {
|
|
s.onchange = function (ev) {
|
|
const opt = s.options[s.selectedIndex];
|
|
const url = opt.getAttribute('data-url');
|
|
if (url) {
|
|
s.disabled = true;
|
|
s.value = '';
|
|
if (s.form) {
|
|
s.form.querySelectorAll('[type=submit]').forEach(function (b) {
|
|
b.disabled = true;
|
|
});
|
|
}
|
|
location.href = url;
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns true when no input element is changed, false otherwise.
|
|
* When excludeForm is defined, will only report changes outside the specified form.
|
|
*/
|
|
function data_leave_validation(parent, excludeForm = null) {
|
|
const ds = parent.querySelectorAll('[data-leave-validation]');
|
|
|
|
for (let i = ds.length - 1; i >= 0; i--) {
|
|
const input = ds[i];
|
|
if (excludeForm && excludeForm === input.form) {
|
|
continue;
|
|
}
|
|
if (input.type === 'checkbox' || input.type === 'radio') {
|
|
if (input.checked != input.getAttribute('data-leave-validation')) {
|
|
return false;
|
|
}
|
|
} else if (input.value != input.getAttribute('data-leave-validation')) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Automatically sets the `data-leave-validation` attribute for input, textarea, select elements for a given parent, if it's not set already.
|
|
* Ignores elements with the `data-no-leave-validation` attribute set.
|
|
*/
|
|
function data_auto_leave_validation(parent) {
|
|
parent.querySelectorAll(`[data-auto-leave-validation] input,
|
|
[data-auto-leave-validation] textarea,
|
|
[data-auto-leave-validation] select`).forEach(el => {
|
|
if (el.dataset.leaveValidation || el.dataset.noLeaveValidation) {
|
|
return;
|
|
}
|
|
|
|
if (el.type === 'checkbox' || el.type === 'radio') {
|
|
el.dataset.leaveValidation = +el.checked;
|
|
} else if (el.type !== 'hidden') {
|
|
el.dataset.leaveValidation = el.value;
|
|
}
|
|
});
|
|
}
|
|
|
|
function init_2stateButton() {
|
|
const btns = document.getElementsByClassName('btn-state1');
|
|
Array.prototype.forEach.call(btns, function (el) {
|
|
el.addEventListener('click', function () {
|
|
const btnState2 = document.getElementById(el.dataset.state2Id);
|
|
btnState2.classList.add('show');
|
|
this.classList.add('hide');
|
|
});
|
|
});
|
|
}
|
|
|
|
function init_configuration_alert() {
|
|
window.addEventListener('submit', function (e) {
|
|
window.hasSubmit = data_leave_validation(document.body, e.target);
|
|
});
|
|
window.addEventListener('beforeunload', function (e) {
|
|
if (window.hasSubmit) {
|
|
return true;
|
|
}
|
|
if (!data_leave_validation(document.body)) {
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
function init_details_attributes() {
|
|
function toggleRequired(details) {
|
|
details.querySelectorAll('[data-required-if-open]').forEach(el => {
|
|
if (details.open) {
|
|
el.setAttribute('required', 'required');
|
|
} else {
|
|
el.removeAttribute('required');
|
|
}
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('details').forEach(details => {
|
|
details.addEventListener('toggle', () => {
|
|
toggleRequired(details);
|
|
});
|
|
toggleRequired(details);
|
|
});
|
|
}
|
|
|
|
function init_user_stats() {
|
|
const active = new Set();
|
|
const queue = [];
|
|
const limit = 10; // Ensure not too many concurrent requests
|
|
|
|
const processQueue = () => {
|
|
while (queue.length > 0 && active.size < limit) {
|
|
const row = queue.shift();
|
|
const promise = (async () => {
|
|
row.removeAttribute('data-need-ajax');
|
|
try {
|
|
const username = row.querySelector('.username').textContent.trim();
|
|
const url = '?c=user&a=details&username=' + encodeURIComponent(username) + '&ajax=1';
|
|
const response = await fetch(url);
|
|
const html = await response.text();
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(html, 'text/html');
|
|
row.querySelector('.feed-count').innerHTML = doc.querySelector('.feed_count').innerHTML;
|
|
row.querySelector('.article-count').innerHTML = doc.querySelector('.article_count').innerHTML;
|
|
row.querySelector('.database-size').innerHTML = doc.querySelector('.database_size').innerHTML;
|
|
} catch (err) {
|
|
console.error('Error fetching user stats', err);
|
|
}
|
|
})();
|
|
|
|
promise.finally(() => {
|
|
active.delete(promise);
|
|
processQueue();
|
|
});
|
|
active.add(promise);
|
|
}
|
|
};
|
|
|
|
// Retrieve user stats when the row becomes visible
|
|
const timers = new WeakMap();
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const timer = setTimeout(() => {
|
|
// But wait a bit to avoid triggering on fast scrolls
|
|
observer.unobserve(entry.target);
|
|
queue.push(entry.target);
|
|
processQueue();
|
|
}, 100);
|
|
timers.set(entry.target, timer);
|
|
} else {
|
|
clearTimeout(timers.get(entry.target));
|
|
}
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('tr[data-need-ajax]').forEach(row => observer.observe(row));
|
|
}
|
|
|
|
function init_enable_notify_button() {
|
|
const notify_button = document.getElementById('html5_enable_notif');
|
|
if (!notify_button) return;
|
|
// it means unsupported in browser
|
|
if (!notifs_html5_is_supported()) {
|
|
notify_button.checked = false;
|
|
return;
|
|
}
|
|
|
|
// Not granted, uncheck even if it is saved in server so browser asks for permission
|
|
if (Notification.permission !== 'granted') {
|
|
notify_button.checked = false;
|
|
}
|
|
|
|
notify_button.addEventListener('change', async function () {
|
|
if (this.checked) {
|
|
const permission = await Notification.requestPermission();
|
|
context.notifs_html5_permission = permission;
|
|
// Uncheck if user denied
|
|
if (permission !== 'granted') {
|
|
notify_button.checked = false;
|
|
}
|
|
} else {
|
|
// User disabled notifications
|
|
context.notifs_html5_permission = 'denied';
|
|
}
|
|
});
|
|
}
|
|
|
|
function init_extra_afterDOM() {
|
|
if (!window.context) {
|
|
if (window.console) {
|
|
console.log('FreshRSS extra waiting for JS…');
|
|
}
|
|
setTimeout(init_extra_afterDOM, 50);
|
|
return;
|
|
}
|
|
const loginButton = document.querySelector('#loginButton');
|
|
if (loginButton) {
|
|
loginButton.addEventListener('click', forgetOpenCategories);
|
|
}
|
|
init_password_observers(document.body);
|
|
init_password_byte_length_validation(document.body);
|
|
init_select_observers();
|
|
init_configuration_alert();
|
|
init_2stateButton();
|
|
init_update_feed();
|
|
init_details_attributes();
|
|
init_user_stats();
|
|
init_enable_notify_button();
|
|
|
|
data_auto_leave_validation(document.body);
|
|
|
|
const slider = document.getElementById('slider');
|
|
if (slider) {
|
|
slider.addEventListener('freshrss:slider-load', function (e) {
|
|
init_password_observers(slider);
|
|
init_password_byte_length_validation(slider);
|
|
});
|
|
init_slider(slider);
|
|
init_archiving(slider);
|
|
init_url_observers(slider);
|
|
} else {
|
|
init_display(document.body);
|
|
init_archiving(document.body);
|
|
init_url_observers(document.body);
|
|
}
|
|
|
|
if (window.console) {
|
|
console.log('FreshRSS extra init done.');
|
|
}
|
|
}
|
|
|
|
if (document.readyState && document.readyState !== 'loading') {
|
|
init_extra_afterDOM();
|
|
} else {
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
if (window.console) {
|
|
console.log('FreshRSS extra waiting for DOMContentLoaded…');
|
|
}
|
|
init_extra_afterDOM();
|
|
}, false);
|
|
}
|
|
// @license-end
|