/* * greeter.js * * Copyright © 2015-2017 Antergos * * 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 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 this program. If not, see . */ /** * This is used to access our classes from within jQuery callbacks. */ let _self = null, _bg_self = null, _config = null; /** * Capitalize a string. * * @returns {string} */ String.prototype.capitalize = function () { return this.charAt( 0 ).toUpperCase() + this.slice( 1 ); }; function is_empty( value ) { if ( value instanceof Array ) { return value.length === 0; } return ['', null, 'null', undefined, 'undefined', false, 'false'].includes( value ); } /** * Add text to the debug log element (accessible from the login screen). * * @param {string} text - To be added to the log. */ function log( text ) { if ( _config.debug ) { console.debug( text ); } $( '#logArea' ).append( `${text}
` ); } /** * The base class for all theme components. It handles accessing cached settings in * local storage and also initializes config values. */ class ThemeConfig { constructor() { if ( null !== _config ) { return _config; } _config = theme_utils.bind_this( this ); this.debug = false; this.lang = window.navigator.language.split( '-' )[0].toLowerCase(); this.translations = window.ant_translations; this.$log_container = $( '#logArea' ); this.cache_backend = ''; this.setup_cache_backend(); return _config; } /** * Determine which background image should be displayed and apply it. * * @return {Promise} */ initialize() { return new Promise( resolve => this.init_config_values( resolve ) ); } setup_cache_backend() { // Do we have access to localStorage? try { localStorage.setItem( 'testing', 'test' ); if ( 'test' === localStorage.getItem( 'testing' ) ) { // We have access to localStorage this.cache_backend = 'localStorage'; } localStorage.removeItem( 'testing' ); } catch ( err ) { // We do not have access to localStorage. Use cookies instead. log( err ); log( 'INFO: localStorage is not available. Using cookies for cache backend.' ); this.cache_backend = 'Cookies'; } log( `AntergosThemeUtils.cache_backend is: ${this.cache_backend}` ); } /** * Get a key's value from localStorage. Keys can have two or more parts. * For example: "ant:user:john:session". * * @param {...string} key_parts Strings that are combined to form the key. * * @return {string|null} */ _get( ...key_parts ) { let key = `ant`, value = null; for ( let part of key_parts ) { key += `:${part}`; } if ( 'localStorage' === this.cache_backend ) { value = localStorage.getItem( key ); } else if ( 'Cookies' === this.cache_backend ) { value = Cookies.get( key ) || null; } log( `ThemeSettings._get() key: ${key} value is: ${value}` ); return value; } /** * Set a key's value in localStorage. Keys can have two or more parts. * For example: "ant:user:john:session". * * @param {string} value The value to set. * @param {...string} key_parts Strings that are combined to form the key. * * @return {string} Always returns `value` (regardless of whether it was set successfully). */ _set( value, ...key_parts ) { let key = `ant`; for ( let part of key_parts ) { key += `:${part}`; } log( `ThemeSettings._set() called with key: ${key} and value: ${value}` ); if ( 'localStorage' === this.cache_backend ) { localStorage.setItem( key, value ); } else if ( 'Cookies' === this.cache_backend ) { Cookies.set( key, value ); } return value; } /** * Get some values from `web-greeter.conf` and save them for later. * * @param {Promise.resolve} callback */ init_config_values( callback ) { if ( 'undefined' === typeof( greeter_config ) ) { window.greeter_config = { branding: {}, greeter: {} }; } this.logo = greeter_config.branding.logo || 'img/antergos.png'; this.user_image = greeter_config.branding.user_image || 'img/antergos-logo-user.png'; this.background_images_dir = greeter_config.branding.background_images_dir || '/usr/share/backgrounds'; this.debug = greeter_config.greeter.debug_mode || false; this.background_images = this._get( 'background_manager', 'background_images' ); this.images_cache_expires = moment.unix( parseInt( this._get( 'background_manager', 'cache_expires' ) ) ); let expired = ( null === this.background_images || ! this.images_cache_expires.isValid() || moment().isAfter( this.images_cache_expires ) ); if ( this.background_images && this.background_images.length > 0 && ! expired ) { this.background_images = JSON.parse( this.background_images ); return callback(); } theme_utils.dirlist( this.background_images_dir, true, result => { this.cache_background_images( result ); return callback(); } ); } cache_background_images( result ) { this.background_images = result; if ( is_empty( this.background_images ) ) { return; } this._set( JSON.stringify( this.background_images ), 'background_manager', 'background_images' ); let expires = moment().add( 7, 'days' ).unix().toString(); this.images_cache_expires = this._set( expires, 'background_manager', 'cache_expires' ); } } /** * This class handles the theme's background switcher. */ class BackgroundManager { constructor() { if ( null !== _bg_self ) { return _bg_self; } _bg_self = theme_utils.bind_this( this ); this.current_background = _config._get( 'background_manager', 'current_background' ); this.random_background = _config._get( 'background_manager', 'random_background' ); if ( is_empty( _config.background_images ) ) { log( 'BackgroundManager: [ERROR] No background images detected.' ); $( '.header' ).fadeTo( 300, 0.5, function () { $( '.header' ).css( 'background-image', 'url(img/fallback_bg.jpg)' ); } ).fadeTo( 300, 1 ); } return _bg_self; } /** * Determine which background image should be displayed and apply it. * * @return {Promise} */ initialize() { return new Promise( (resolve, reject) => { if ( is_empty( _bg_self.current_background ) ) { _bg_self.random_background = _config._set( 'true', 'background_manager', 'random_background' ); } if ( 'true' === _bg_self.random_background ) { _bg_self.current_background = _config._set( _bg_self.get_random_image(), 'background_manager', 'current_background' ); } return _bg_self.do_background( resolve ); }); } /** * Sets the background image to the value of {@link this.current_background}. * * @param {?Promise.resolve} resolve */ do_background( resolve = null ) { let background = _bg_self.current_background, tpl = background.startsWith( 'url(' ) ? background : `url(${background})`; $( '.header' ).css( 'background-image', tpl ); if ( null !== resolve ) { return resolve(); } } /** * Get a random background image from our images array. * * @returns {string} The absolute path to a background image. */ get_random_image() { let random_bg; if ( is_empty( _config.background_images ) ) { return 'img/fallback_bg.jpg'; } random_bg = Math.floor( Math.random() * _config.background_images.length ); let image = _config.background_images[random_bg]; return image; } /** * Setup the background switcher widget. */ setup_background_thumbnails() { if ( _config.background_images && _config.background_images.length ) { let current_bg_url = `url(${this.current_background})`, $random_thumbnail = $( '[data-img="random"]' ), $thumbs_container = $( '.bgs' ); // TODO: Implement some form of pagination if ( _config.background_images.length > 100 ) { _config.background_images = _config.background_images.splice( 0, 100 ); } $random_thumbnail.on( 'click', event => this.background_selected_handler(event) ); for ( let image_file of _config.background_images ) { let $link = $( '
' ), $img_el = $link.children( 'div' ), img_url = `url(web-greeter://${image_file})`; if ( image_file === this.current_background || img_url === current_bg_url ) { $link.addClass( 'active' ); } $img_el.css( 'background-image', img_url ); $link.addClass( 'bg clearfix' ).attr( 'data-img', img_url ); $link.appendTo( $thumbs_container ).on( 'click', event => this.background_selected_handler(event) ); } if ( ! $( '.bg.active' ).length ) { $random_thumbnail.addClass( 'active' ); } } } /** * Handle background image selected event. * * @param {Object} event jQuery event object. */ background_selected_handler( event ) { let $target = $( event.target ), image = event.target.parentElement.dataset.img $( '.bg.active' ).removeClass( 'active' ); $target.addClass( 'active' ); if ( 'random' === image ) { _bg_self.random_background = _config._set( 'true', 'background_manager', 'random_background' ); _bg_self.current_background = _bg_self.get_random_image(); } else { _bg_self.random_background = _config._set( 'false', 'background_manager', 'random_background' ); _bg_self.current_background = image; } _config._set( image, 'background_manager', 'current_background' ); this.do_background(); } } /** * This is the theme's main class object. It contains most of the theme's logic. */ class Theme { constructor() { if ( null !== _self ) { return _self; } _self = theme_utils.bind_this( this ); this.tux = 'img/antergos-logo-user.png'; this.user_list_visible = false; this.auth_pending = false; this.showing_message = false; this.selected_user = null; this.$user_list = $( '#user-list2' ); this.$session_list = $( '#sessions' ); this.$clock_container = $( '#collapseOne' ); this.$clock = $( '#current_time' ); this.$actions_container = $( '#actionsArea' ); this.$msg_area_container = $( '#statusArea' ); this.$alert_msg_tpl = this.$msg_area_container.children( '.alert-dismissible' ).clone(); this.background_manager = new BackgroundManager(); this.background_manager.initialize().then( () => this.initialize() ); return _self; } /** * Initialize the theme. */ initialize() { this.prepare_translations(); this.do_static_translations(); this.initialize_clock(); this.prepare_login_panel_header(); this.prepare_system_action_buttons(); $( '#login' ).css( 'opacity', '1' ); this.prepare_user_list(); this.prepare_session_list(); this.register_callbacks(); this.background_manager.setup_background_thumbnails(); } /** * Register callbacks for the LDM Greeter as well as any others that haven't * been registered elsewhere. */ register_callbacks() { this.$user_list .parents( '.collapse' ) .on( 'shown.bs.collapse hidden.bs.collapse', event => this.user_list_collapse_handler(event) ); $( document ).on( 'keydown', event => this.key_press_handler(event) ); $( '.cancel_auth:not(.alert .cancel_auth)' ).on( 'click', event => this.cancel_authentication(event) ); $( '.submit_passwd' ).on( 'click', event => this.submit_password(event) ); $( '[data-i18n="debug_log"]' ).on( 'click', event => this.show_log_handler(event) ); lightdm.show_prompt.connect( (prompt, type) => this.show_prompt(prompt, type) ); lightdm.show_message.connect( (msg, type) => this.show_message(msg, type) ); window.start_authentication = event => this.start_authentication(event); window.cancel_authentication = event => this.cancel_authentication(event); lightdm.authentication_complete.connect( () => this.authentication_complete() ); lightdm.autologin_timer_expired.connect( event => this.cancel_authentication(event) ); } /** * Initialize the user list. */ prepare_user_list() { let template; // Loop through the array of LightDMUser objects to create our user list. for ( let user of lightdm.users ) { let last_session = _config._get( 'user', user.username, 'session' ), image_src = ( user.image && user.image.length ) ? user.image : _config.user_image; if ( null === last_session ) { // This user has never logged in before let's enable the system's default session. last_session = _config._set( lightdm.default_session, 'user', user.username, 'session' ); } log( `Last session for ${user.username} was: ${last_session}` ); template = ` ${user.display_name} ${user.display_name} `; // Insert the template into the DOM and then register event handlers so we don't // have to iterate over the users again later. $( template ) .appendTo( this.$user_list ) .on( 'click', event => this.start_authentication(event) ) .on( 'error.antergos', err => this.user_image_error_handler(err) ); } // END for ( let user of lightdm.users ) if ( this.$user_list.children().length > 3 ) { // Make the user list two columns instead of one. this.$user_list.css( 'column-count', '2' ).parent().css( 'max-width', '85%' ); } } /** * Initialize the session selection dropdown. */ prepare_session_list() { // Loop through the array of LightDMSession objects to create our session list. for ( let session of lightdm.sessions ) { let css_class = session.name.replace( / /g, '' ), template; log( `Adding ${session.name} to the session list...` ); template = `
  • ${session.name}
  • `; $( template ) .appendTo( this.$session_list ) .on( 'click', event => this.session_toggle_handler(event) ); } // END for (var session of lightdm.sessions) $( '.dropdown-toggle' ).dropdown(); } /** * Initialize the system action buttons */ prepare_system_action_buttons() { let template, actions = { shutdown: 'power-off', hibernate: 'asterisk', suspend: 'arrow-down', restart: 'refresh' }; for ( let action of Object.keys( actions ) ) { let cmd = `can_${action}`; template = ` `; if ( ! lightdm[cmd] ) { // This action is either not available on this system or we don't have permission to use it. continue; } $( template ) .appendTo( this.$actions_container ) .on( 'click', event => this.system_action_handler(event) ); } // END for (let action of actions) $( '[data-toggle=tooltip]' ).tooltip(); $( '.modal' ).modal( { show: false } ); } /** * Setup the clock widget. */ initialize_clock() { this.$clock.html( theme_utils.get_current_localized_time() ); setInterval( () => _self.$clock.html( theme_utils.get_current_localized_time() ), 60000 ); } /** * Show the user list if its not already shown. This is used to allow the user to * display the user list by pressing Enter or Spacebar. */ show_user_list( show = true ) { let delay = 0; if ( false === show ) { return; } if ( this.$clock_container.hasClass( 'in' ) ) { delay = 500; $( '#trigger' ).trigger( 'click' ); } if ( this.$user_list.children().length <= 1 ) { setTimeout( () => this.$user_list.find( 'a' ).trigger( 'click', this ), delay ); } } prepare_login_panel_header() { let greeting = _config.translations.greeting ? _config.translations.greeting : 'Welcome!', logo = is_empty( _config.logo ) ? 'img/antergos.png' : _config.logo; $( '.welcome' ).text( greeting ); $( '#hostname' ).append( lightdm.hostname ); $( '[data-greeter-config="logo"]' ).attr( 'src', logo ); } prepare_translations() { if ( ! _config.translations.hasOwnProperty( this.lang ) ) { this.lang = 'en'; for ( let lang of window.navigator.languages ) { if ( _config.translations.hasOwnProperty( lang ) ) { this.lang = lang; break; } } } _config.translations = _config.translations[ this.lang ]; } /** * Replace '${i18n}' with translated string for all elements that * have the data-i18n attribute. This is for elements that are not generated * dynamically (they can be found in index.html). */ do_static_translations() { $( '[data-i18n]' ).each( function () { let key = $( this ).attr( 'data-i18n' ), html = $( this ).html(), translated = _config.translations[ key ], new_html = html.replace( '${i18n}', translated ); $( this ).html( new_html ); } ); } /** * Start the authentication process for the selected user. * * @param {object} event - jQuery.Event object from 'click' event. */ start_authentication( event ) { let user_id = $( event.target ).attr( 'id' ), selector = `.${user_id}`, user_session_cached = _config._get( 'user', user_id, 'session' ), user_session = is_empty( user_session_cached ) ? lightdm.default_session : user_session_cached; if ( this.auth_pending || null !== this.selected_user ) { lightdm.cancel_authentication(); log( `Authentication cancelled for ${this.selected_user}` ); this.selected_user = null; this.auth_pending = false; } log( `Starting authentication for ${user_id}.` ); this.selected_user = user_id; if ( this.$user_list.children().length > 3 ) { // Reset columns since only the selected user is visible right now. this.$user_list.css( 'column-count', 'initial' ).parent().css( 'max-width', '50%' ); } $( selector ).addClass( 'hovered' ).siblings().hide(); $( '.fa-toggle-down' ).hide(); log( `Session for ${user_id} is ${user_session}` ); $( `[data-session-id="${user_session}"]` ).parent().trigger( 'click', this ); $( '#session-list' ).removeClass( 'hidden' ).show(); $( '#passwordArea' ).show(); $('#passwordField').focus(); $( '.dropdown-toggle' ).dropdown(); this.auth_pending = true; lightdm.authenticate( user_id ); } /** * Cancel the pending authentication. * * @param {object} event - jQuery.Event object from 'click' event. */ cancel_authentication( event ) { let selectors = ['#statusArea', '#timerArea', '#passwordArea', '#session-list']; selectors.forEach( selector => $( selector ).hide() ); lightdm.cancel_authentication(); log( 'Cancelled authentication.' ); this.selected_user = null; this.auth_pending = false; if ( event && $( event.target ).hasClass( 'alert' ) ) { /* We were triggered by the authentication failed message being dismissed. * Keep the same account selected so user can retry without re-selecting an account. */ $( '#collapseTwo .user-wrap2' ).show( () => { $( '.list-group-item.hovered' ).trigger( 'click' ); } ); } else { if ( this.$user_list.children().length > 3 ) { // Make the user list two columns instead of one. this.$user_list.css( 'column-count', '2' ).parent().css( 'max-width', '85%' ); } $( '.hovered' ).removeClass( 'hovered' ).siblings().show(); $( '.fa-toggle-down' ).show(); } } /** * Called when the user attempts to authenticate (submits password). * We check to see if the user successfully authenticated and if so tell the LDM * Greeter to log them in with the session they selected. */ authentication_complete() { let selected_session = $( '#session-list .selected' ).attr( 'data-session-id' ), err_msg = _config.translations.auth_failed; this.auth_pending = false; _config._set( selected_session, 'user', lightdm.authentication_user, 'session' ); $( '#timerArea' ).hide(); if ( lightdm.is_authenticated ) { // The user entered the correct password. Let's start the session. $( 'body' ).fadeOut( 1000, () => lightdm.start_session( selected_session ) ); } else { // The user did not enter the correct password. Show error message. this.showing_message = true; this.show_message( err_msg, 'error' ); } } submit_password( event ) { let passwd = $( '#passwordField' ).val(); console.log(lightdm.authentication_user); $( '#passwordArea' ).hide(); $( '#timerArea' ).show(); lightdm.respond( passwd ); } session_toggle_handler( event ) { let $session = $( event.target ), session_name = $session.text(), session_key = $session.attr( 'data-session-id' ); $session .parents( '.btn-group' ) .find( '.selected' ) .attr( 'data-session-id', session_key ) .html( session_name ); } key_press_handler( event ) { let action = this.showing_message ? this.dismiss_message : null; if ( null === action ) { switch ( event.which ) { case 13: if ( this.auth_pending ) { action = this.submit_password; } else if ( ! this.user_list_visible ) { action = this.show_user_list; } break; case 27: action = this.auth_pending ? this.cancel_authentication : null; break; case 32: action = ( !this.user_list_visible && !this.auth_pending ) ? this.show_user_list : null; break; } } if ( null !== action ) { action(); } } system_action_handler( event ) { let action = $( event.currentTarget ).attr( 'id' ), $modal = $( '.modal' ); $modal .find( '.btn-primary' ) .text( _config.translations[ action ] ) .on( 'click', action, function( event ) { $( this ).off( 'click' ); $( 'body' ).fadeOut( 1000, () => lightdm[ event.data ]() ); } ); $modal .find( '.btn-default' ) .on( 'click', function( event ) { $( this ).next().off( 'click' ); } ); $modal.modal( 'toggle' ); } user_list_collapse_handler( event ) { this.user_list_visible = $( event.target ).hasClass( 'in' ); this.show_user_list( this.user_list_visible ); } user_image_error_handler( event ) { $( event.target ).off( 'error.antergos' ); $( event.target ).attr( 'src', this.tux ); } show_log_handler( event ) { if ( _config.$log_container.is( ':visible' ) ) { _config.$log_container.hide(); } else { _config.$log_container.show(); } } /** * LightDM Callback - Show prompt to user. * * @param text * @param type */ show_prompt( text, type ) { if ( 'password' === type ) { $( '#passwordField' ).val( '' ); $( '#passwordArea' ).show(); $( '#passwordField' ).focus(); } } /** * LightDM Callback - Show message to user. * * @param text * @param type */ show_message( text, type ) { if ( ! text.length ) { log( 'show_message() called without a message to show!' ); return; } let $msg_container = this.$msg_area_container.children( '.alert-dismissible' ); if ( ! $msg_container.length ) { $msg_container = this.$alert_msg_tpl.clone(); $msg_container.appendTo( this.$msg_area_container ); } $msg_container.on( 'closed.bs.alert', event => this.cancel_authentication( event ) ); $msg_container.html( $msg_container.html() + text ); $( '#collapseTwo .user-wrap2' ).hide(); this.$msg_area_container.show(); } dismiss_message() { this.$msg_area_container .children( '.alert-dismissible' ) .find('.close') .trigger('click'); this.showing_message = false; } } /** * Initialize the theme once the window has loaded. */ $( window ).on( 'GreeterReady', () => { const config = new ThemeConfig(); config.initialize().then( () => new Theme() ); } );