/* * Copyright (C) 2013,2014,2015 Canonical, Ltd. * * This program 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; version 3. * * This program 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. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.3 import AccountsService 0.1 import GSettings 1.0 import LightDM 0.1 as LightDM import Ubuntu.Components 1.1 import Ubuntu.SystemImage 0.1 import Unity.Launcher 0.1 import "../Components" Showable { id: root created: loader.status == Loader.Ready property real dragHandleLeftMargin: 0 property url background // How far to offset the top greeter layer during a launcher left-drag property real launcherOffset readonly property bool active: shown || hasLockedApp readonly property bool fullyShown: loader.item ? loader.item.fullyShown : false // True when the greeter is waiting for PAM or other setup process readonly property alias waiting: d.waiting property string lockedApp: "" readonly property bool hasLockedApp: lockedApp !== "" property bool forcedUnlock readonly property bool locked: LightDM.Greeter.active && !LightDM.Greeter.authenticated && !forcedUnlock property bool tabletMode property url viewSource // only used for testing property int maxFailedLogins: -1 // disabled by default for now, will enable via settings in future property int failedLoginsDelayAttempts: 7 // number of failed logins property real failedLoginsDelayMinutes: 5 // minutes of forced waiting readonly property bool animating: loader.item ? loader.item.animating : false signal tease() signal sessionStarted() signal emergencyCall() function forceShow() { showNow(); loader.item.reset(); } function notifyAppFocused(appId) { if (!active) { return; } if (hasLockedApp) { if (appId === lockedApp) { hide(); // show locked app } else { show(); d.startUnlock(false /* toTheRight */); } } else if (appId !== "unity8-dash") { // dash isn't started by user d.startUnlock(false /* toTheRight */); } } function notifyAboutToFocusApp(appId) { if (!active) { return; } // A hint that we're about to focus an app. This way we can look // a little more responsive, rather than waiting for the above // notifyAppFocused call. We also need this in case we have a locked // app, in order to show lockscreen instead of new app. d.startUnlock(false /* toTheRight */); } // This is a just a glorified notifyAboutToFocusApp(), but it does one // other thing: it hides any cover pages to the RIGHT, because the user // just came from a launcher drag starting on the left. // It also returns a boolean value, indicating whether there was a visual // change or not (the shell only wants to hide the launcher if there was // a change). function notifyShowingDashFromDrag() { if (!active) { return false; } return d.startUnlock(true /* toTheRight */); } QtObject { id: d readonly property bool multiUser: LightDM.Users.count > 1 property int currentIndex property bool waiting // We want 'launcherOffset' to animate down to zero. But not to animate // while being dragged. So ideally we change this only when the user // lets go and launcherOffset drops to zero. But we need to wait for // the behavior to be enabled first. So we cache the last known good // launcherOffset value to cover us during that brief gap between // release and the behavior turning on. property real lastKnownPositiveOffset // set in a launcherOffsetChanged below property real launcherOffsetProxy: (shown && !launcherOffsetProxyBehavior.enabled) ? lastKnownPositiveOffset : 0 Behavior on launcherOffsetProxy { id: launcherOffsetProxyBehavior enabled: launcherOffset === 0 UbuntuNumberAnimation {} } function selectUser(uid, reset) { d.waiting = true; if (reset) { loader.item.reset(); } currentIndex = uid; var user = LightDM.Users.data(uid, LightDM.UserRoles.NameRole); AccountsService.user = user; LauncherModel.setUser(user); LightDM.Greeter.authenticate(user); // always resets auth state } function login() { enabled = false; if (LightDM.Greeter.startSessionSync()) { sessionStarted(); if (loader.item) { loader.item.notifyAuthenticationSucceeded(); } } else if (loader.item) { loader.item.notifyAuthenticationFailed(); } enabled = true; } function startUnlock(toTheRight) { if (loader.item) { return loader.item.tryToUnlock(toTheRight); } else { return false; } } } onLauncherOffsetChanged: { if (launcherOffset > 0) { d.lastKnownPositiveOffset = launcherOffset; } } onForcedUnlockChanged: { if (forcedUnlock && shown) { // pretend we were just authenticated loader.item.notifyAuthenticationSucceeded(); } } onRequiredChanged: { if (required) { d.waiting = true; lockedApp = ""; } } GSettings { id: greeterSettings schema.id: "com.canonical.Unity8.Greeter" onLockedOutTimeChanged: forcedDelayTimer.checkForForcedDelay() } Timer { id: forcedDelayTimer // We use a short interval and check against the system wall clock // because we have to consider the case that the system is suspended // for a few minutes. When we wake up, we want to quickly be correct. interval: 500 property var delayTarget; property int delayMinutes; function forceDelay() { // Store the beginning time for a lockout in GSettings, so that // we still lock the user out if they reboot. And we store // starting time rather than end-time or how-long because: // - If storing end-time and on boot we have a problem with NTP, // we might get locked out for a lot longer than we thought. // - If storing how-long, and user turns their phone off for an // hour rather than wait, they wouldn't expect to still be locked // out. // - A malicious actor could manipulate either of the above // settings to keep the user out longer. But by storing // start-time, we never make the user wait longer than the full // lock out time. // The code below that watches GSettings will pick up the change // for us. greeterSettings.lockedOutTime = new Date().getTime(); } onTriggered: { var diff = delayTarget - new Date(); if (diff > 0) { delayMinutes = Math.ceil(diff / 60000); start(); // go again } else { delayMinutes = 0; } } function checkForForcedDelay() { var now = new Date(); delayTarget = now; delayTarget.setTime(greeterSettings.lockedOutTime + failedLoginsDelayMinutes * 60000); // If tooEarly is true, something went very wrong. Bug or NTP // misconfiguration maybe? var tooEarly = now.getTime() < greeterSettings.lockedOutTime; var tooLate = now > delayTarget; // Compare stored time to system time. If a malicious actor is // able to manipulate time to avoid our lockout, they already have // enough access to cause damage. So we choose to trust this check. if (tooEarly || tooLate) { stop(); delayMinutes = 0; } else { triggered(); } } Component.onCompleted: checkForForcedDelay() } // event eater // Nothing should leak to items behind the greeter MouseArea { anchors.fill: parent } Loader { id: loader objectName: "loader" anchors.fill: parent active: root.required source: root.viewSource.toString() ? root.viewSource : (d.multiUser || root.tabletMode) ? "WideView.qml" : "NarrowView.qml" onLoaded: { root.lockedApp = ""; root.forceActiveFocus(); d.selectUser(d.currentIndex, true); LightDM.Infographic.readyForDataChange(); } Connections { target: loader.item onSelected: { d.selectUser(index, true); } onResponded: { if (root.locked) { LightDM.Greeter.respond(response); } else { if (LightDM.Greeter.active && !LightDM.Greeter.authenticated) { // could happen if forcedUnlock d.login(); } loader.item.hide(); } } onTease: root.tease() onEmergencyCall: root.emergencyCall() onRequiredChanged: { if (!loader.item.required) { root.hide(); } } } Binding { target: loader.item property: "backgroundTopMargin" value: -root.y } Binding { target: loader.item property: "launcherOffset" value: d.launcherOffsetProxy } Binding { target: loader.item property: "dragHandleLeftMargin" value: root.dragHandleLeftMargin } Binding { target: loader.item property: "delayMinutes" value: forcedDelayTimer.delayMinutes } Binding { target: loader.item property: "background" value: root.background } Binding { target: loader.item property: "locked" value: root.locked } Binding { target: loader.item property: "alphanumeric" value: AccountsService.passwordDisplayHint === AccountsService.Keyboard } Binding { target: loader.item property: "currentIndex" value: d.currentIndex } Binding { target: loader.item property: "userModel" value: LightDM.Users } Binding { target: loader.item property: "infographicModel" value: LightDM.Infographic } } Connections { target: LightDM.Greeter onShowGreeter: root.forceShow() onHideGreeter: { d.login(); loader.item.hide(); } onShowMessage: { if (!LightDM.Greeter.active) { return; // could happen if hideGreeter() comes in before we prompt } // inefficient, but we only rarely deal with messages var html = text.replace(/&/g, "&") .replace(//g, ">") .replace(/\n/g, "
"); if (isError) { html = "" + html + ""; } loader.item.showMessage(html); } onShowPrompt: { d.waiting = false; if (!LightDM.Greeter.active) { return; // could happen if hideGreeter() comes in before we prompt } loader.item.showPrompt(text, isSecret, isDefaultPrompt); } onAuthenticationComplete: { d.waiting = false; if (LightDM.Greeter.authenticated) { AccountsService.failedLogins = 0; d.login(); if (!LightDM.Greeter.promptless) { loader.item.hide(); } } else { if (!LightDM.Greeter.promptless) { AccountsService.failedLogins++; } // Check if we should initiate a factory reset if (maxFailedLogins >= 2) { // require at least a warning if (AccountsService.failedLogins === maxFailedLogins - 1) { loader.item.showLastChance(); } else if (AccountsService.failedLogins >= maxFailedLogins) { SystemImage.factoryReset(); // Ouch! } } // Check if we should initiate a forced login delay if (failedLoginsDelayAttempts > 0 && AccountsService.failedLogins > 0 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) { forcedDelayTimer.forceDelay(); } loader.item.notifyAuthenticationFailed(); if (!LightDM.Greeter.promptless) { d.selectUser(d.currentIndex, false); } } } onRequestAuthenticationUser: { // Find index for requested user, if it exists for (var i = 0; i < LightDM.Users.count; i++) { if (user === LightDM.Users.data(i, LightDM.UserRoles.NameRole)) { d.selectUser(i, true); return; } } } } Binding { target: LightDM.Greeter property: "active" value: root.active } Binding { target: LightDM.Infographic property: "username" value: AccountsService.statsWelcomeScreen ? LightDM.Users.data(d.currentIndex, LightDM.UserRoles.NameRole) : "" } Connections { target: i18n onLanguageChanged: LightDM.Infographic.readyForDataChange() } }