mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-19 15:18:02 -04:00
Merge branch 'main' into 620-add-browser-extension-download-links-to-client
This commit is contained in:
@@ -9,12 +9,13 @@ import CredentialsList from './pages/CredentialsList';
|
||||
import EmailsList from './pages/EmailsList';
|
||||
import LoadingSpinner from './components/LoadingSpinner';
|
||||
import Home from './pages/Home';
|
||||
import './style.css';
|
||||
import CredentialDetails from './pages/CredentialDetails';
|
||||
import EmailDetails from './pages/EmailDetails';
|
||||
import Settings from './pages/Settings';
|
||||
import GlobalStateChangeHandler from './components/GlobalStateChangeHandler';
|
||||
import { useLoading } from './context/LoadingContext';
|
||||
import Logout from './pages/Logout';
|
||||
import './style.css';
|
||||
|
||||
/**
|
||||
* Route configuration.
|
||||
@@ -44,6 +45,7 @@ const App: React.FC = () => {
|
||||
{ path: '/emails', element: <EmailsList />, showBackButton: false },
|
||||
{ path: '/emails/:id', element: <EmailDetails />, showBackButton: true, title: 'Email details' },
|
||||
{ path: '/settings', element: <Settings />, showBackButton: false },
|
||||
{ path: '/logout', element: <Logout />, showBackButton: false },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -47,10 +47,7 @@ export const UserMenu: React.FC = () => {
|
||||
*/
|
||||
const onLogout = async () : Promise<void> => {
|
||||
showLoading();
|
||||
await authContext.logout();
|
||||
navigate('/', { replace: true });
|
||||
// Delay for 100ms for improved UX
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
navigate('/logout', { replace: true });
|
||||
hideLoading();
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { createContext, useContext, useState, useEffect, useCallback, use
|
||||
import SqliteClient from '../../shared/SqliteClient';
|
||||
import { VaultResponse } from '../../shared/types/webapi/VaultResponse';
|
||||
import EncryptionUtility from '../../shared/EncryptionUtility';
|
||||
|
||||
type DbContextType = {
|
||||
sqliteClient: SqliteClient | null;
|
||||
dbInitialized: boolean;
|
||||
|
||||
@@ -16,7 +16,13 @@ export const WebApiProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||
*/
|
||||
useEffect(() : void => {
|
||||
const service = new WebApiService(
|
||||
logout
|
||||
(statusError: string | null) => {
|
||||
if (statusError) {
|
||||
logout(statusError);
|
||||
} else {
|
||||
logout();
|
||||
}
|
||||
}
|
||||
);
|
||||
setWebApiService(service);
|
||||
}, [logout]);
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useLoading } from '../context/LoadingContext';
|
||||
import { useWebApi } from '../context/WebApiContext';
|
||||
import { VaultResponse } from '../../shared/types/webapi/VaultResponse';
|
||||
import ReloadButton from '../components/ReloadButton';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
import { useMinDurationLoading } from '../hooks/useMinDurationLoading';
|
||||
|
||||
@@ -21,7 +20,6 @@ const CredentialsList: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const navigate = useNavigate();
|
||||
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
|
||||
const authContext = useAuth();
|
||||
|
||||
/**
|
||||
* Loading state with minimum duration for more fluid UX.
|
||||
@@ -40,7 +38,7 @@ const CredentialsList: React.FC = () => {
|
||||
const statusResponse = await webApi.getStatus();
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError !== null) {
|
||||
authContext.logout(statusError);
|
||||
await webApi.logout(statusError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,7 +58,7 @@ const CredentialsList: React.FC = () => {
|
||||
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson);
|
||||
if (vaultError) {
|
||||
authContext.logout(vaultError);
|
||||
await webApi.logout(vaultError);
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
@@ -73,7 +71,7 @@ const CredentialsList: React.FC = () => {
|
||||
} catch (err) {
|
||||
console.error('Refresh error:', err);
|
||||
}
|
||||
}, [dbContext, webApi, authContext, hideLoading]);
|
||||
}, [dbContext, webApi, hideLoading]);
|
||||
|
||||
/**
|
||||
* Manually refresh the credentials list.
|
||||
|
||||
32
browser-extensions/chrome/src/app/pages/Logout.tsx
Normal file
32
browser-extensions/chrome/src/app/pages/Logout.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { useWebApi } from '../context/WebApiContext';
|
||||
|
||||
/**
|
||||
* Logout page.
|
||||
*/
|
||||
const Logout: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const webApi = useWebApi();
|
||||
const navigate = useNavigate();
|
||||
/**
|
||||
* Logout and navigate to home page.
|
||||
*/
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Perform logout via async method to ensure logout is completed before navigating to home page.
|
||||
*/
|
||||
const performLogout = async () : Promise<void> => {
|
||||
await webApi.logout();
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
performLogout();
|
||||
}, [authContext, navigate, webApi]);
|
||||
|
||||
// Return null since this is just a functional component that handles logout.
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Logout;
|
||||
@@ -31,7 +31,7 @@ const Unlock: React.FC = () => {
|
||||
const statusResponse = await webApi.getStatus();
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError !== null) {
|
||||
authContext.logout(statusError);
|
||||
await webApi.logout(statusError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -81,26 +81,6 @@ const Unlock: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle logout
|
||||
*/
|
||||
const handleLogout = async () : Promise<void> => {
|
||||
showLoading();
|
||||
try {
|
||||
await webApi.logout();
|
||||
} catch (err) {
|
||||
console.error('Logout error:', err);
|
||||
}
|
||||
|
||||
try {
|
||||
await authContext.logout();
|
||||
} catch (err) {
|
||||
console.error('Logout error:', err);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md">
|
||||
<form onSubmit={handleSubmit} className="bg-white dark:bg-gray-700 w-full shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
@@ -136,7 +116,7 @@ const Unlock: React.FC = () => {
|
||||
</Button>
|
||||
|
||||
<div className="text-sm font-medium text-gray-500 dark:text-gray-200 mt-6">
|
||||
Switch accounts? <a href="#" onClick={handleLogout} className="text-primary-700 hover:underline dark:text-primary-500">Log out</a>
|
||||
Switch accounts? <a href="/logout" className="text-primary-700 hover:underline dark:text-primary-500">Log out</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -354,7 +354,7 @@ function triggerInputEvents(element: HTMLInputElement | HTMLSelectElement) : voi
|
||||
const rect = element.getBoundingClientRect();
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 999999999;
|
||||
z-index: 999999991;
|
||||
pointer-events: none;
|
||||
top: ${rect.top}px;
|
||||
left: ${rect.left}px;
|
||||
|
||||
@@ -34,7 +34,7 @@ export function createBasePopup(input: HTMLInputElement) : HTMLElement {
|
||||
popup.style.cssText = `
|
||||
all: unset;
|
||||
position: absolute;
|
||||
z-index: 999999999;
|
||||
z-index: 999999991;
|
||||
background: ${isDarkMode() ? '#1f2937' : 'white'};
|
||||
border: 1px solid ${isDarkMode() ? '#374151' : '#ccc'};
|
||||
border-radius: 4px;
|
||||
@@ -228,7 +228,14 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
|
||||
createButton.style.backgroundColor = isDarkMode() ? '#374151' : '#f3f4f6';
|
||||
});
|
||||
|
||||
createButton.addEventListener('click', async () => {
|
||||
/**
|
||||
* Handle create button click
|
||||
*/
|
||||
const handleCreateClick = async (e: Event) : Promise<void> => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
// Determine service name based on conditions
|
||||
let suggestedName = document.title;
|
||||
|
||||
@@ -331,7 +338,7 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
|
||||
};
|
||||
|
||||
chrome.runtime.sendMessage({ type: 'CREATE_IDENTITY', credential }, () => {
|
||||
// Refresh the popup to show new identity
|
||||
// Refresh the popup to show new identity.
|
||||
openAutofillPopup(input);
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -345,9 +352,32 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
|
||||
removeExistingPopup();
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// Add click listener with capture and prevent removal.
|
||||
createButton.addEventListener('click', handleCreateClick, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
|
||||
// Create search input instead of button
|
||||
// Backup click handling using mousedown/mouseup if needed.
|
||||
let isMouseDown = false;
|
||||
createButton.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
isMouseDown = true;
|
||||
}, { capture: true });
|
||||
|
||||
createButton.addEventListener('mouseup', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isMouseDown) {
|
||||
handleCreateClick(e);
|
||||
}
|
||||
isMouseDown = false;
|
||||
}, { capture: true });
|
||||
|
||||
// Create search input.
|
||||
const searchInput = document.createElement('input');
|
||||
searchInput.type = 'text';
|
||||
searchInput.dataset.aliasvaultIgnore = 'true';
|
||||
@@ -370,7 +400,7 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
|
||||
text-align: left;
|
||||
`;
|
||||
|
||||
// Add focus styles
|
||||
// Add focus styles.
|
||||
searchInput.addEventListener('focus', () => {
|
||||
searchInput.style.borderColor = '#2563eb';
|
||||
searchInput.style.boxShadow = '0 0 0 2px rgba(37, 99, 235, 0.2)';
|
||||
@@ -381,7 +411,7 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
|
||||
searchInput.style.boxShadow = 'none';
|
||||
});
|
||||
|
||||
// Handle search input
|
||||
// Handle search input.
|
||||
let searchTimeout: NodeJS.Timeout;
|
||||
|
||||
searchInput.addEventListener('input', () => {
|
||||
@@ -654,8 +684,11 @@ function createCredentialList(credentials: Credential[], input: HTMLInputElement
|
||||
// Handle base64 image data
|
||||
if (cred.Logo) {
|
||||
try {
|
||||
const base64Logo = base64Encode(cred.Logo as Uint8Array<ArrayBufferLike>);
|
||||
imgElement.src = `data:image/x-icon;base64,${base64Logo}`;
|
||||
const logoBytes = toUint8Array(cred.Logo);
|
||||
const base64Logo = base64Encode(logoBytes);
|
||||
// Detect image type from first few bytes
|
||||
const mimeType = detectMimeType(logoBytes);
|
||||
imgElement.src = `data:${mimeType};base64,${base64Logo}`;
|
||||
} catch (error) {
|
||||
console.error('Error setting logo:', error);
|
||||
imgElement.src = `data:image/x-icon;base64,${placeholderBase64}`;
|
||||
@@ -837,7 +870,7 @@ export async function createEditNamePopup(defaultName: string): Promise<string |
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999999999;
|
||||
z-index: 999999995;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -1042,23 +1075,33 @@ export function openAutofillPopup(input: HTMLInputElement) : void {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert various binary data formats to Uint8Array
|
||||
*/
|
||||
function toUint8Array(buffer: Uint8Array | number[] | {[key: number]: number}): Uint8Array {
|
||||
if (buffer instanceof Uint8Array) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
if (Array.isArray(buffer)) {
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
const length = Object.keys(buffer).length;
|
||||
const arr = new Uint8Array(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
arr[i] = buffer[i];
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 encode binary data.
|
||||
*/
|
||||
function base64Encode(buffer: Uint8Array | number[] | {[key: number]: number}): string | null {
|
||||
try {
|
||||
// Handle object with numeric keys
|
||||
if (typeof buffer === 'object' && !Array.isArray(buffer) && !(buffer instanceof Uint8Array)) {
|
||||
const length = Object.keys(buffer).length;
|
||||
const arr = new Uint8Array(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
arr[i] = buffer[i];
|
||||
}
|
||||
buffer = arr;
|
||||
}
|
||||
|
||||
// Convert to array if Uint8Array
|
||||
const arr = Array.from(buffer as Uint8Array | number[]);
|
||||
const arr = Array.from(toUint8Array(buffer));
|
||||
return btoa(arr.reduce((data, byte) => data + String.fromCharCode(byte), ''));
|
||||
} catch (error) {
|
||||
console.error('Error encoding to base64:', error);
|
||||
@@ -1115,3 +1158,42 @@ async function getFaviconBytes(document: Document): Promise<Uint8Array | null> {
|
||||
|
||||
return null; // Return null if no favicon could be downloaded
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect MIME type from file signature (magic numbers)
|
||||
*/
|
||||
function detectMimeType(bytes: Uint8Array): string {
|
||||
/**
|
||||
* Check if the file is an SVG file.
|
||||
*/
|
||||
const isSvg = () : boolean => {
|
||||
const header = new TextDecoder().decode(bytes.slice(0, 5)).toLowerCase();
|
||||
return header.includes('<?xml') || header.includes('<svg');
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the file is an ICO file.
|
||||
*/
|
||||
const isIco = () : boolean => {
|
||||
return bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0x01 && bytes[3] === 0x00;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the file is an PNG file.
|
||||
*/
|
||||
const isPng = () : boolean => {
|
||||
return bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47;
|
||||
};
|
||||
|
||||
if (isSvg()) {
|
||||
return 'image/svg+xml';
|
||||
}
|
||||
if (isIco()) {
|
||||
return 'image/x-icon';
|
||||
}
|
||||
if (isPng()) {
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
return 'image/x-icon';
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ export class WebApiService {
|
||||
/**
|
||||
* Constructor for the WebApiService class.
|
||||
*
|
||||
* @param {Function} handleLogout - Function to handle logout.
|
||||
* @param {Function} authContextLogout - Function to handle logout.
|
||||
*/
|
||||
public constructor(private readonly handleLogout: () => void) { }
|
||||
public constructor(private readonly authContextLogout: (statusError: string | null) => void) { }
|
||||
|
||||
/**
|
||||
* Get the base URL for the API from settings.
|
||||
@@ -79,7 +79,7 @@ export class WebApiService {
|
||||
|
||||
return parseJson ? retryResponse.json() : retryResponse as unknown as T;
|
||||
} else {
|
||||
this.handleLogout();
|
||||
this.authContextLogout(null);
|
||||
throw new Error('Session expired');
|
||||
}
|
||||
}
|
||||
@@ -106,11 +106,13 @@ export class WebApiService {
|
||||
|
||||
try {
|
||||
const baseUrl = await this.getBaseUrl();
|
||||
|
||||
const response = await fetch(`${baseUrl}Auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Ignore-Failure': 'true',
|
||||
'X-AliasVault-Client': `${AppInfo.CLIENT_NAME}-${AppInfo.VERSION}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: await this.getAccessToken(),
|
||||
@@ -126,7 +128,7 @@ export class WebApiService {
|
||||
this.updateTokens(tokenResponse.token, tokenResponse.refreshToken);
|
||||
return tokenResponse.token;
|
||||
} catch {
|
||||
this.handleLogout();
|
||||
this.authContextLogout('Your session has expired. Please login again.');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -146,7 +148,7 @@ export class WebApiService {
|
||||
const response = await this.fetch<Response>(endpoint, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/octet-stream'
|
||||
'Accept': 'application/octet-stream',
|
||||
}
|
||||
}, false);
|
||||
|
||||
@@ -197,18 +199,26 @@ export class WebApiService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout and revoke tokens via WebApi.
|
||||
* Logout and revoke tokens via WebApi and remove local storage tokens via AuthContext.
|
||||
*/
|
||||
public async logout(): Promise<void> {
|
||||
const refreshToken = await this.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
return;
|
||||
public async logout(statusError: string | null = null): Promise<void> {
|
||||
// Logout and revoke tokens via WebApi.
|
||||
try {
|
||||
const refreshToken = await this.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.post('Auth/revoke', {
|
||||
token: await this.getAccessToken(),
|
||||
refreshToken: refreshToken,
|
||||
}, false);
|
||||
} catch (err) {
|
||||
console.error('WebApi logout error:', err);
|
||||
}
|
||||
|
||||
await this.post('Auth/revoke', {
|
||||
token: await this.getAccessToken(),
|
||||
refreshToken: refreshToken,
|
||||
}, false);
|
||||
// Logout and remove tokens from local storage via AuthContext.
|
||||
this.authContextLogout(statusError);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -308,7 +318,7 @@ export class WebApiService {
|
||||
/**
|
||||
* When the reader has finished loading, convert the result to a Base64 string.
|
||||
*/
|
||||
reader.onloadend = () : void => {
|
||||
reader.onloadend = (): void => {
|
||||
const result = reader.result;
|
||||
if (typeof result === 'string') {
|
||||
resolve(result.split(',')[1]); // Remove the data URL prefix
|
||||
@@ -320,7 +330,7 @@ export class WebApiService {
|
||||
/**
|
||||
* If the reader encounters an error, reject the promise with a proper Error object.
|
||||
*/
|
||||
reader.onerror = () : void => {
|
||||
reader.onerror = (): void => {
|
||||
reject(new Error('Failed to read blob as Data URL'));
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
|
||||
@@ -101,15 +101,33 @@ export class FormDetector {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for parent label
|
||||
// Check for parent label and table cell structure
|
||||
let currentElement = input;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
// Check for parent label
|
||||
const parentLabel = currentElement.closest('label');
|
||||
if (parentLabel) {
|
||||
attributes.push(parentLabel.textContent?.toLowerCase() ?? '');
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for table cell structure
|
||||
const parentTd = currentElement.closest('td');
|
||||
if (parentTd) {
|
||||
// Get the parent row
|
||||
const parentTr = parentTd.closest('tr');
|
||||
if (parentTr) {
|
||||
// Check all sibling cells in the row
|
||||
const siblingTds = parentTr.querySelectorAll('td');
|
||||
for (const td of siblingTds) {
|
||||
if (td !== parentTd) { // Skip the cell containing the input
|
||||
attributes.push(td.textContent?.toLowerCase() ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
break; // Found table structure, no need to continue up the tree
|
||||
}
|
||||
|
||||
if (currentElement.parentElement) {
|
||||
currentElement = currentElement.parentElement as HTMLInputElement;
|
||||
} else {
|
||||
|
||||
@@ -44,6 +44,13 @@ describe('FormDetector English tests', () => {
|
||||
testField(FormField.LastName, 'fbclc_lName', htmlFile);
|
||||
});
|
||||
|
||||
describe('English registration form 5 detection', () => {
|
||||
const htmlFile = 'en-registration-form5.html';
|
||||
|
||||
testField(FormField.Username, 'aliasvault-input-7owmnahd9', htmlFile);
|
||||
testField(FormField.Password, 'aliasvault-input-ienw3qgxv', htmlFile);
|
||||
});
|
||||
|
||||
describe('English email form 1 detection', () => {
|
||||
const htmlFile = 'en-email-form1.html';
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<html lang="en"><head><meta name="referrer" content="origin"><meta name="viewport" content="width=device-width, initial-scale=1.0"><link rel="icon" href="y18.svg"><style>com-strongbox-extension {visibility: visible !important; /* Ensure visibility for Strongbox Extension */}</style><style>com-strongbox-extension {visibility: visible !important; /* Ensure visibility for Strongbox Extension */}</style><style>com-strongbox-extension {visibility: visible !important; /* Ensure visibility for Strongbox Extension */}</style></head><body><b>Login</b><br><br>
|
||||
<form action="login" method="post"><input type="hidden" name="goto" value="news"><table border="0"><tbody><tr><td>username:</td><td><input type="text" name="acct" size="20" autocorrect="off" spellcheck="false" autocapitalize="off" autofocus="true" id="aliasvault-input-xahcz4tlf" autocomplete="false"></td></tr><tr><td>password:</td><td><input type="password" name="pw" size="20"></td></tr></tbody></table><br>
|
||||
<input type="submit" value="login"></form><a href="forgot">Forgot your password?</a><br><br>
|
||||
<b>Create Account</b><br><br>
|
||||
<form action="login" method="post"><input type="hidden" name="goto" value="news"><input type="hidden" name="creating" value="t"><table border="0"><tbody><tr><td>username:</td><td><input type="text" name="acct" size="20" autocorrect="off" spellcheck="false" autocapitalize="off" id="aliasvault-input-7owmnahd9" autocomplete="false"></td></tr><tr><td>password:</td><td><input type="password" name="pw" size="20" id="aliasvault-input-ienw3qgxv" autocomplete="false"></td></tr></tbody></table><br>
|
||||
<input type="submit" value="create account"></form></body></html>
|
||||
@@ -56,3 +56,5 @@ The following websites have been known to cause issues in the past (but should b
|
||||
| --- | --- |
|
||||
| https://www.paprika-shopping.nl/nieuwsbrief/newsletter-register-landing.html | Popup CSS style conflicts |
|
||||
| https://bloshing.com/inschrijven-nieuwsbrief | Popup CSS style conflicts |
|
||||
| https://gamefaqs.gamespot.com/user | Popup buttons not working |
|
||||
| https://news.ycombinator.com/login?goto=news | Popup and client favicon not showing due to SVG format |
|
||||
|
||||
@@ -26,6 +26,9 @@ else
|
||||
[Parameter]
|
||||
public bool Padding { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The data URL of the favicon.
|
||||
/// </summary>
|
||||
private string? _faviconDataUrl;
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -33,8 +36,46 @@ else
|
||||
{
|
||||
if (FaviconBytes is not null)
|
||||
{
|
||||
string mimeType = DetectMimeType(FaviconBytes);
|
||||
string base64String = Convert.ToBase64String(FaviconBytes);
|
||||
_faviconDataUrl = $"data:image/x-icon;base64,{base64String}";
|
||||
_faviconDataUrl = $"data:{mimeType};base64,{base64String}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect the mime type of the favicon.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes of the favicon.</param>
|
||||
/// <returns>The mime type of the favicon.</returns>
|
||||
private static string DetectMimeType(byte[] bytes)
|
||||
{
|
||||
// Check for SVG.
|
||||
if (bytes.Length >= 5)
|
||||
{
|
||||
string header = System.Text.Encoding.ASCII.GetString(bytes.Take(5).ToArray()).ToLower();
|
||||
if (header.Contains("<?xml") || header.Contains("<svg"))
|
||||
{
|
||||
return "image/svg+xml";
|
||||
}
|
||||
}
|
||||
|
||||
// Check for ICO.
|
||||
if (bytes.Length >= 4 &&
|
||||
bytes[0] == 0x00 && bytes[1] == 0x00 &&
|
||||
bytes[2] == 0x01 && bytes[3] == 0x00)
|
||||
{
|
||||
return "image/x-icon";
|
||||
}
|
||||
|
||||
// Check for PNG.
|
||||
if (bytes.Length >= 4 &&
|
||||
bytes[0] == 0x89 && bytes[1] == 0x50 &&
|
||||
bytes[2] == 0x4E && bytes[3] == 0x47)
|
||||
{
|
||||
return "image/png";
|
||||
}
|
||||
|
||||
// Default to x-icon if unknown.
|
||||
return "image/x-icon";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1342,6 +1342,11 @@ video {
|
||||
border-color: rgb(191 219 254 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-blue-300 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(147 197 253 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-blue-700 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(29 78 216 / var(--tw-border-opacity));
|
||||
@@ -1392,6 +1397,16 @@ video {
|
||||
border-color: rgb(239 68 68 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-blue-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(191 219 254 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-gray-100 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(243 244 246 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.bg-amber-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 251 235 / var(--tw-bg-opacity));
|
||||
@@ -1729,6 +1744,10 @@ video {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.align-text-bottom {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
|
||||
}
|
||||
@@ -1942,6 +1961,11 @@ video {
|
||||
color: rgb(133 77 14 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-primary-800 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(154 93 38 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.opacity-0 {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -2322,6 +2346,11 @@ video {
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-blue-800:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(30 64 175 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-gray-400:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(156 163 175 / var(--tw-border-opacity));
|
||||
@@ -2381,6 +2410,11 @@ video {
|
||||
background-color: rgb(30 58 138 / 0.2);
|
||||
}
|
||||
|
||||
.dark\:bg-blue-900\/50:is(.dark *) {
|
||||
background-color: rgb(30 58 138 / 0.5);
|
||||
|
||||
}
|
||||
|
||||
.dark\:bg-gray-500:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
||||
@@ -2465,6 +2499,15 @@ video {
|
||||
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-blue-900\/20:is(.dark *) {
|
||||
background-color: rgb(30 58 138 / 0.2);
|
||||
}
|
||||
|
||||
.dark\:bg-primary-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(123 74 30 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-opacity-80:is(.dark *) {
|
||||
--tw-bg-opacity: 0.8;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user