/* * ThemeUtils.js * * Copyright © 2021 JezerM * * This file is part of Web Greeter. * * Web Greeter is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * Web Greeter is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * The following additional terms are in effect as per Section 7 of the license: * * The preservation of all legal notices and author attributions in * the material or in the Appropriate Legal Notices displayed * by works containing it is required. * * You should have received a copy of the GNU General Public License * along with web-greeter; If not, see . */ class LightDMLanguage { constructor( {code, name, territory} ) { this.code = code; this.name = name; this.territory = territory; } } class LightDMLayout { constructor( {name, description, short_description} ) { this.name = name; this.description = description; this.short_description = short_description; } } class LightDMSession { constructor( {key, name, comment} ) { this.key = key; this.name = name; this.comment = comment; } } class LightDMUser { constructor( {display_name, username, language, layout, layouts, image, home_directory, session, logged_in, background} ) { this.display_name = display_name; this.username = username; this.language = language; this.layout = layout; this.layouts = layouts; this.image = image; this.home_directory = home_directory; this.background = background; this.session = session; this.logged_in = logged_in; } } class LightDMBattery { constructor( {name, level, status, ac_status}) { this.name = name; this.level = level; this.status = status; this.ac_status = ac_status; this.capacity = 100; this.time = "00:00"; this.watt = 0; } } let allSignals = []; class Signal { constructor(name) { this._name = name; this._callbacks = []; allSignals.push(this); } /** * Connects a callback to the signal. * @param {Function} callback The callback to attach. */ connect(callback) { if (typeof callback !== 'function') return; this._callbacks.push(callback); } /** * Disconnects a callback to the signal. * @param {Function} callback The callback to disattach. */ disconnect(callback) { var ind = this._callbacks.findIndex( (cb) => {return cb === callback}); if (ind == -1) return; this._callbacks.splice(ind, 1); } _run() { this._callbacks.forEach( (cb) => { if (typeof cb !== 'function') return; cb() }) } } /** * Emits a signal. * @param {String} name The signal's name. */ function emitSignal(name) { var signal = allSignals.find( (s) => { return s._name === name; }) signal._run() } const _mockData = { languages: [ new LightDMLanguage({ name: 'English', code: 'en_US.utf8', territory: 'USA' }), new LightDMLanguage({ name: 'Catalan', code: 'ca_ES.utf8', territory: 'Spain' }), new LightDMLanguage({ name: 'French', code: 'fr_FR.utf8', territory: 'France' }) ], layouts: [ new LightDMLayout({ name: 'us', short_description: 'en', description: 'English (US)' }), new LightDMLayout({ name: 'at', short_description: 'de', description: 'German (Austria)' }), new LightDMLayout({ name: 'us rus', short_description: 'ru', description: 'Russian (US, phonetic)' }) ], sessions: [ new LightDMSession({ key: 'gnome', name: 'GNOME', comment: 'This session logs you into GNOME' }), new LightDMSession({ key: 'cinnamon', name: 'Cinnamon', comment: 'This session logs you into Cinnamon' }), new LightDMSession({ key: 'plasma', name: 'Plasma', comment: 'Plasma by KDE' }), new LightDMSession({ key: 'awesome', name: 'Awesome wm', comment: 'An Awesome WM' }), new LightDMSession({ key: 'mate', name: 'MATE', comment: 'This session logs you into MATE' }), new LightDMSession({ key: 'openbox', name: 'Openbox', comment: 'This session logs you into Openbox' }) ], users: [ new LightDMUser({ display_name: 'Clark Kent', username: 'superman', language: null, layout: null, image: '/usr/share/web-greeter/themes/default/img/antergos-logo-user', home_directory: '/home/superman', session: 'gnome', logged_in: false, background: "", layouts: [], }), new LightDMUser({ display_name: 'Bruce Wayne', username: 'batman', language: null, layout: null, image: '/usr/share/web-greeter/themes/default/img/antergos-logo-user', home_directory: '/home/batman', session: 'cinnamon', logged_in: false, background: "", layouts: [], }), new LightDMUser({ display_name: 'Peter Parker', username: 'spiderman', language: null, layout: null, image: '/usr/share/web-greeter/themes/default/img/antergos-logo-user', home_directory: '/home/spiderman', session: 'MATE', logged_in: false, background: "", layouts: [], }) ], battery: new LightDMBattery({ name: "Battery 0", level: 85, status: "Discharging", ac_status: 1, }), } class Greeter { constructor() { if ('lightdm' in window) { return window.lightdm; } window.lightdm = this; return window.lightdm; } mock = true; _authentication_user = null; /** * The username of the user being authenticated or {@link null} * if no authentication is in progress * @type {String|Null} * @readonly */ get authentication_user() { return this._authentication_user; } _autologin_guest = false; /** * Whether or not the guest account should be automatically logged * into when the timer expires. * @type {Boolean} * @readonly */ get autologin_guest() { return this._autologin_guest; } _autologin_timeout = 100; /** * The number of seconds to wait before automatically logging in. * @type {Number} * @readonly */ get autologin_timeout() { return this._autologin_timeout; } _autologin_user = false; /** * The username with which to automattically log in when the timer expires. * @type {String} * @readonly */ get autologin_user() { return this._autologin_user; } _batteryData = _mockData.battery; /** * Gets the battery data. * @type {LightDMBattery} * @readonly */ get batteryData() { return this._batteryData; } _brightness = 50; /** * Gets the brightness * @type {Number} */ get brightness() { return this._brightness; } /** * Sets the brightness * @param {Number} quantity The quantity to set */ set brightness( quantity ) { if (quantity > 100) quantity = 100; if (quantity < 0) quantity = 0; this._brightness = quantity; emitSignal("brightness_update"); } _can_access_battery = true; /** * Whether or not the greeter can access to battery data. * @type {boolean} * @readonly */ get can_access_battery() { return this._can_access_battery; } _can_access_brightness = true; /** * Whether or not the greeter can control display brightness. * @type {boolean} * @readonly */ get can_access_brightness() { return this._can_access_brightness; } _can_hibernate = true; /** * Whether or not the greeter can make the system hibernate. * @type {Boolean} * @readonly */ get can_hibernate() { return this._can_hibernate; } _can_restart = true; /** * Whether or not the greeter can make the system restart. * @type {Boolean} * @readonly */ get can_restart() { return this._can_restart; } _can_shutdown = true; /** * Whether or not the greeter can make the system shutdown. * @type {Boolean} * @readonly */ get can_shutdown() { return this._can_shutdown; } _can_suspend = true; /** * Whether or not the greeter can make the system suspend/sleep. * @type {Boolean} * @readonly */ get can_suspend() { return this._can_suspend; } _default_session = "awesome"; /** * The name of the default session. * @type {String} * @readonly */ get default_session() { return this._default_session; } _has_guest_account = false; /** * Whether or not guest sessions are supported. * @type {Boolean} * @readonly */ get has_guest_account() { return this._has_guest_account; } _hide_users_hint = false; /** * Whether or not user accounts should be hidden. * @type {Boolean} * @readonly */ get hide_users_hint() { return this._hide_users_hint; } _hostname = "Web browser"; /** * The system's hostname. * @type {String} * @readonly */ get hostname() { return this._hostname; } _in_authentication = false; /** * Whether or not the greeter is in the process of authenticating. * @type {Boolean} * @readonly */ get in_authentication() { return this._in_authentication; } _is_authenticated = true; /** * Whether or not the greeter has successfully authenticated. * @type {Boolean} * @readonly */ get is_authenticated() { return this._is_authenticated; } _language = null; /** * The current language or {@link null} if no language. * @type {LightDMLanguage|null} * @readonly */ get language() { return this._language; } _languages = _mockData.languages; /** * A list of languages to present to the user. * @type {LightDMLanguage[]} * @readonly */ get languages() { return this._languages; } _layout = _mockData.layouts[0]; /** * The currently active layout for the selected user. * @type {LightDMLayout} */ get layout() { return this._layout; } _layouts = _mockData.layouts; /** * A list of keyboard layouts to present to the user. * @type {LightDMLayout[]} * @readonly */ get layouts() { return this._layouts; } _lock_hint = false; /** * Whether or not the greeter was started as a lock screen. * @type {Boolean} * @readonly */ get lock_hint() { return this._lock_hint; } _select_guest_hint = false; /** * Whether or not the guest account should be selected by default. * @type {Boolean} * @readonly */ get select_guest_hint() { return this.select_guest_hint; } _select_user_hint = ""; /** * The username to select by default. * @type {String} * @readonly */ get select_user_hint() { return this.select_user_hint; } _sessions = _mockData.sessions; /** * List of available sessions. * @type {LightDMSession[]} * @readonly */ get sessions() { return this._sessions; } _show_manual_login_hint = false; /** * Check if a manual login option should be shown. If {@link true}, the theme should * provide a way for a username to be entered manually. Otherwise, themes that show * a user list may limit logins to only those users. * @type {Boolean} * @readonly */ get show_manual_login_hint() { return this._show_manual_login_hint; } _show_remote_login_hint = false; /** * Check if a remote login option should be shown. If {@link true}, the theme should provide * a way for a user to log into a remote desktop server. * @type {Boolean} * @readonly * @internal */ get show_remote_login_hint() { return this._show_remote_login_hint; } _users = _mockData.users; /** * List of available users. * @type {LightDMUser[]} * @readonly */ get users() { return this._users; } /** * Starts the authentication procedure for a user. * * @param {String|null} username A username or {@link null} to prompt for a username. */ authenticate( username ) { this._in_authentication = true; this._authentication_user = username; } /** * Starts the authentication procedure for the guest user. */ authenticate_as_guest() { this._in_authentication = true; this._authentication_user = "guest"; } /** * Set the brightness to quantity * @param {Number} quantity The quantity to set */ brightnessSet( quantity ) { this.brightness = quantity; } /** * Increase the brightness by quantity * @param {Number} quantity The quantity to increase */ brightnessIncrease( quantity ) { this.brightness += quantity; } /** * Decrease the brightness by quantity * @param {Number} quantity The quantity to decrease */ brightnessDecrease( quantity ) { this.brightness -= quantity; } /** * Cancel user authentication that is currently in progress. */ cancel_authentication() { this._in_authentication = false; this._authentication_user = ""; } /** * Cancel the automatic login. */ cancel_autologin() { } /** * Triggers the system to hibernate. * @returns {Boolean} {@link true} if hibernation initiated, otherwise {@link false} */ hibernate() { alert("Hibernating system"); location.reload(); } _default_password = "justice"; /** * Provide a response to a prompt. * @param {*} response */ respond( response ) { console.log("Response:", response); if (response === this._default_password) { this._is_authenticated = true; emitSignal("authentication_complete") } else { this._is_authenticated = false; setTimeout(() => { emitSignal("authentication_complete") }, 2000) } } /** * Triggers the system to restart. * @returns {Boolean} {@link true} if restart initiated, otherwise {@link false} */ restart() { alert("Restarting system"); location.reload(); } /** * Set the language for the currently authenticated user. * @param {String} language The language in the form of a locale specification (e.g. * 'de_DE.UTF-8') * @returns {Boolean} {@link true} if successful, otherwise {@link false} */ set_language( language ) { } /** * Triggers the system to shutdown. * @returns {Boolean} {@link true} if shutdown initiated, otherwise {@link false} */ shutdown() { alert("Shutting down system"); location.reload(); } /** * Start a session for the authenticated user. * @param {String|null} session The session to log into or {@link null} to use the default. * @returns {Boolean} {@link true} if successful, otherwise {@link false} */ start_session( session ) { if (!this._is_authenticated) return; alert(`Session started with: "${session}"`); location.reload(); } /** * Triggers the system to suspend/sleep. * @returns {Boolean} {@link true} if suspend/sleep initiated, otherwise {@link false} */ suspend() { alert("Suspending system"); location.reload(); } authentication_complete = new Signal("authentication_complete"); autologin_timer_expired = new Signal("autologin_timer_expired"); brightness_update = new Signal("brightness_update"); battery_update = new Signal("battery_update"); idle = new Signal("idle"); reset = new Signal("reset"); show_message = new Signal("show_message"); show_prompt = new Signal("show_prompt"); } class GreeterConfig { constructor() { if ('greeter_config' in window) { return window.greeter_config; } window.greeter_config = this; } _branding = { background_images_dir: "/usr/share/backgrounds", logo_image: "/usr/share/web-greeter/themes/default/img/antergos-logo-user.png", user_image: "/usr/share/web-greeter/themes/default/img/antergos.png" } _greeter = { debug_mode: true, detect_theme_errors: true, screensaver_timeout: 300, secure_mode: true, time_language: "", theme: "none" } _features = { battery: true, backlight: { enabled: true, value: 10, steps: 0 } } _layouts = _mockData.layouts /** * Holds keys/values from the `branding` section of the config file. * * @type {object} branding * @property {string} background_images_dir Path to directory that contains background images * for use in greeter themes. * @property {string} logo Path to distro logo image for use in greeter themes. * @property {string} user_image Default user image/avatar. This is used by greeter themes * for users that have not configured a `.face` image. * @readonly */ get branding() { return this._branding; } /** * Holds keys/values from the `greeter` section of the config file. * * @type {object} greeter * @property {boolean} debug_mode Greeter theme debug mode. * @property {boolean} detect_theme_errors Provide an option to load a fallback theme when theme * errors are detected. * @property {number} screensaver_timeout Blank the screen after this many seconds of inactivity. * @property {boolean} secure_mode Don't allow themes to make remote http requests. * generate localized time for display. * @property {string} time_language Language to use when displaying the time or "" * to use the system's language. * @property {string} theme The name of the theme to be used by the greeter. * @readonly */ get greeter() { return this._greeter; } /** * Holds keys/values from the `features` section of the config file. * * @type {Object} features * @property {Boolean} battery Enable greeter and themes to ger battery status. * @property {Object} backlight * @property {Boolean} enabled Enable greeter and themes to control display backlight. * @property {Number} value The amount to increase/decrease brightness by greeter. * @property {Number} steps How many steps are needed to do the change. */ get features() { return this._features; } get layouts() { return this._layouts; } } let time_language = null class ThemeUtils { constructor() { if ("theme_utils" in window) { return window.theme_utils; } window.theme_utils = this } /** * Binds `this` to class, `context`, for all of the class's methods. * * @arg {object} context An ES6 class instance with at least one method. * * @return {object} `context` with `this` bound to it for all of its methods. */ bind_this( context ) { let excluded_methods = ['constructor']; function not_excluded( _method, _context ) { let is_excluded = excluded_methods.findIndex( excluded_method => _method === excluded_method ) > -1, is_method = 'function' === typeof _context[_method]; return is_method && !is_excluded; } for ( let obj = context; obj; obj = Object.getPrototypeOf( obj ) ) { // Stop once we have traveled all the way up the inheritance chain if ( 'Object' === obj.constructor.name ) { break; } for ( let method of Object.getOwnPropertyNames( obj ) ) { if ( not_excluded( method, context ) ) { context[method] = context[method].bind( context ); } } } return context; } /** * Returns the contents of directory found at `path` provided that the (normalized) `path` * meets at least one of the following conditions: * * Is located within the greeter themes' root directory. * * Has been explicitly allowed in the greeter's config file. * * Is located within the greeter's shared data directory (`/var/lib/lightdm-data`). * * Is located in `/tmp`. * * @param {String} path The abs path to desired directory. * @param {Boolean} only_images Include only images in the results. Default `true`. * @param {function(String[])} callback Callback function to be called with the result. */ dirlist( path, only_images = true, callback ) { if ( '' === path || 'string' !== typeof path ) { console.error(`theme_utils.dirlist(): path must be a non-empty string!`); return callback([]); } else if ( null !== path.match(/^[^/].+/) ) { console.error(`theme_utils.dirlist(): path must be absolute!`); return callback([]); } if ( null !== path.match(/\/\.+(?=\/)/) ) { // No special directory names allowed (eg ../../) path = path.replace(/\/\.+(?=\/)/g, '' ); } try { return callback([]) } catch( err ) { console.error(`theme_utils.dirlist(): ${err}`); return callback([]); } } /** * Binds `this` to class, `context`, for all of the class's methods. * * @param {Object} context An ES6 class instance with at least one method. * * @return {Object} `context` with `this` bound to it for all of its methods. */ bind_this( context ) { let excluded_methods = ['constructor']; function not_excluded( _method, _context ) { let is_excluded = excluded_methods.findIndex( excluded_method => _method === excluded_method ) > -1, is_method = 'function' === typeof _context[_method]; return is_method && !is_excluded; } for ( let obj = context; obj; obj = Object.getPrototypeOf( obj ) ) { // Stop once we have traveled all the way up the inheritance chain if ( 'Object' === obj.constructor.name ) { break; } for ( let method of Object.getOwnPropertyNames( obj ) ) { if ( not_excluded( method, context ) ) { context[method] = context[method].bind( context ); } } } return context; } /** * Get the current date in a localized format. Local language is autodetected by default, but can be set manually in the greeter config file. * * `language` defaults to the system's language, but can be set manually in the config file. * * @returns {Object} The current date. */ get_current_localized_date() { let config = greeter_config.greeter var locale = [] if (time_language === null) { time_language = config.time_language || "" } if (time_language != "") { locale.push(time_language) } let optionsDate = { day: "2-digit", month: "2-digit", year: "2-digit" } let fmtDate = Intl.DateTimeFormat(locale, optionsDate) let now = new Date() var date = fmtDate.format(now) return date } /** * Get the current time in a localized format. Local language is autodetected by default, but can be set manually in the greeter config file. * * `language` defaults to the system's language, but can be set manually in the config file. * * @returns {Object} The current time. */ get_current_localized_time() { let config = greeter_config.greeter var locale = [] if (time_language === null) { time_language = config.time_language || "" } if (time_language != "") { locale.push(time_language) } let optionsTime = { hour: "2-digit", minute: "2-digit" } let fmtTime = Intl.DateTimeFormat(locale, optionsTime) let now = new Date() var time = fmtTime.format(now) return time } } new ThemeUtils(); new GreeterConfig(); new Greeter(); window._ready_event = new Event("GreeterReady") window.addEventListener("load", () => {window.dispatchEvent(_ready_event)})