on beb293c help sign in
unity8/qml/Greeter/Greeter.qml
537 lines 18 kB history raw
1/*
2 * Copyright (C) 2013,2014,2015 Canonical, Ltd.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; version 3.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
17import QtQuick 2.4
18import AccountsService 0.1
19import Biometryd 0.0
20import GSettings 1.0
21import Ubuntu.Components 1.3
22import Ubuntu.SystemImage 0.1
23import Unity.Launcher 0.1
24import Unity.Session 0.1
25import "../Components"
27Showable {
28 id: root
29 created: loader.status == Loader.Ready
31 property real dragHandleLeftMargin: 0
33 property url background
35 // How far to offset the top greeter layer during a launcher left-drag
36 property real launcherOffset
38 readonly property bool active: required || hasLockedApp
39 readonly property bool fullyShown: loader.item ? loader.item.fullyShown : false
41 // True when the greeter is waiting for PAM or other setup process
42 readonly property alias waiting: d.waiting
44 property string lockedApp: ""
45 readonly property bool hasLockedApp: lockedApp !== ""
47 property bool forcedUnlock
48 readonly property bool locked: lightDM.greeter.active && !lightDM.greeter.authenticated && !forcedUnlock
50 property bool tabletMode
51 property url viewSource // only used for testing
53 property int maxFailedLogins: -1 // disabled by default for now, will enable via settings in future
54 property int failedLoginsDelayAttempts: 7 // number of failed logins
55 property real failedLoginsDelayMinutes: 5 // minutes of forced waiting
57 readonly property bool animating: loader.item ? loader.item.animating : false
59 signal tease()
60 signal sessionStarted()
61 signal emergencyCall()
63 function forceShow() {
64 forcedUnlock = false;
65 showNow();
66 d.selectUser(d.currentIndex, true);
67 }
69 function notifyAppFocusRequested(appId) {
70 if (!active) {
71 return;
72 }
74 if (hasLockedApp) {
75 if (appId === lockedApp) {
76 hide(); // show locked app
77 } else {
78 show();
79 d.startUnlock(false /* toTheRight */);
80 }
81 } else if (appId !== "unity8-dash") { // dash isn't started by user
82 d.startUnlock(false /* toTheRight */);
83 }
84 }
86 // Notify that the user has explicitly requested the given app through unity8 GUI.
87 function notifyUserRequestedApp(appId) {
88 if (!active) {
89 return;
90 }
92 // A hint that we're about to focus an app. This way we can look
93 // a little more responsive, rather than waiting for the above
94 // notifyAppFocusRequested call. We also need this in case we have a locked
95 // app, in order to show lockscreen instead of new app.
96 d.startUnlock(false /* toTheRight */);
97 }
99 // This is a just a glorified notifyUserRequestedApp(), but it does one
100 // other thing: it hides any cover pages to the RIGHT, because the user
101 // just came from a launcher drag starting on the left.
102 // It also returns a boolean value, indicating whether there was a visual
103 // change or not (the shell only wants to hide the launcher if there was
104 // a change).
105 function notifyShowingDashFromDrag() {
106 if (!active) {
107 return false;
108 }
110 return d.startUnlock(true /* toTheRight */);
111 }
113 LightDM{id:lightDM} // Provide backend access
114 QtObject {
115 id: d
117 readonly property bool multiUser: lightDM.users.count > 1
118 property int currentIndex
119 property bool waiting
120 property int fingerprintFailureCount
122 // We want 'launcherOffset' to animate down to zero. But not to animate
123 // while being dragged. So ideally we change this only when the user
124 // lets go and launcherOffset drops to zero. But we need to wait for
125 // the behavior to be enabled first. So we cache the last known good
126 // launcherOffset value to cover us during that brief gap between
127 // release and the behavior turning on.
128 property real lastKnownPositiveOffset // set in a launcherOffsetChanged below
129 property real launcherOffsetProxy: (shown && !launcherOffsetProxyBehavior.enabled) ? lastKnownPositiveOffset : 0
130 Behavior on launcherOffsetProxy {
131 id: launcherOffsetProxyBehavior
132 enabled: launcherOffset === 0
133 UbuntuNumberAnimation {}
134 }
136 onFingerprintFailureCountChanged: {
137 if (d.fingerprintFailureCount >= 3) {
138 d.startUnlock(false /* toTheRight */);
139 d.fingerprintFailureCount = 0;
140 }
141 }
143 function selectUser(uid, reset) {
144 d.waiting = true;
145 d.fingerprintFailureCount = 0;
146 if (reset) {
147 loader.item.reset();
148 }
149 currentIndex = uid;
150 var user = lightDM.users.data(uid, lightDM.userRoles.NameRole);
151 AccountsService.user = user;
152 LauncherModel.setUser(user);
153 lightDM.greeter.authenticate(user); // always resets auth state
154 }
156 function login() {
157 enabled = false;
158 if (lightDM.greeter.startSessionSync()) {
159 sessionStarted();
160 if (loader.item) {
161 loader.item.notifyAuthenticationSucceeded(false /* showFakePassword */);
162 }
163 } else if (loader.item) {
164 loader.item.notifyAuthenticationFailed();
165 }
166 enabled = true;
167 }
169 function startUnlock(toTheRight) {
170 if (loader.item) {
171 return loader.item.tryToUnlock(toTheRight);
172 } else {
173 return false;
174 }
175 }
177 function checkForcedUnlock() {
178 if (forcedUnlock && shown && loader.item) {
179 // pretend we were just authenticated
180 loader.item.notifyAuthenticationSucceeded(false /* showFakePassword */);
181 loader.item.hide();
182 }
183 }
184 }
186 onLauncherOffsetChanged: {
187 if (launcherOffset > 0) {
188 d.lastKnownPositiveOffset = launcherOffset;
189 }
190 }
192 onForcedUnlockChanged: d.checkForcedUnlock()
193 Component.onCompleted: d.checkForcedUnlock()
195 onRequiredChanged: {
196 if (required) {
197 d.waiting = true;
198 lockedApp = "";
199 }
200 }
202 GSettings {
203 id: greeterSettings
204 schema.id: "com.canonical.Unity8.Greeter"
205 }
207 Timer {
208 id: forcedDelayTimer
210 // We use a short interval and check against the system wall clock
211 // because we have to consider the case that the system is suspended
212 // for a few minutes. When we wake up, we want to quickly be correct.
213 interval: 500
215 property var delayTarget
216 property int delayMinutes
218 function forceDelay() {
219 // Store the beginning time for a lockout in GSettings, so that
220 // we still lock the user out if they reboot. And we store
221 // starting time rather than end-time or how-long because:
222 // - If storing end-time and on boot we have a problem with NTP,
223 // we might get locked out for a lot longer than we thought.
224 // - If storing how-long, and user turns their phone off for an
225 // hour rather than wait, they wouldn't expect to still be locked
226 // out.
227 // - A malicious actor could manipulate either of the above
228 // settings to keep the user out longer. But by storing
229 // start-time, we never make the user wait longer than the full
230 // lock out time.
231 greeterSettings.lockedOutTime = new Date().getTime();
232 checkForForcedDelay();
233 }
235 onTriggered: {
236 var diff = delayTarget - new Date();
237 if (diff > 0) {
238 delayMinutes = Math.ceil(diff / 60000);
239 start(); // go again
240 } else {
241 delayMinutes = 0;
242 }
243 }
245 function checkForForcedDelay() {
246 if (greeterSettings.lockedOutTime === 0) {
247 return;
248 }
250 var now = new Date();
251 delayTarget = new Date(greeterSettings.lockedOutTime + failedLoginsDelayMinutes * 60000);
253 // If tooEarly is true, something went very wrong. Bug or NTP
254 // misconfiguration maybe?
255 var tooEarly = now.getTime() < greeterSettings.lockedOutTime;
256 var tooLate = now >= delayTarget;
258 // Compare stored time to system time. If a malicious actor is
259 // able to manipulate time to avoid our lockout, they already have
260 // enough access to cause damage. So we choose to trust this check.
261 if (tooEarly || tooLate) {
262 stop();
263 delayMinutes = 0;
264 } else {
265 triggered();
266 }
267 }
269 Component.onCompleted: checkForForcedDelay()
270 }
272 // event eater
273 // Nothing should leak to items behind the greeter
274 MouseArea { anchors.fill: parent; hoverEnabled: true }
276 Loader {
277 id: loader
278 objectName: "loader"
280 anchors.fill: parent
282 active: root.required
283 source: root.viewSource.toString() ? root.viewSource :
284 (d.multiUser || root.tabletMode) ? "WideView.qml" : "NarrowView.qml"
286 onLoaded: {
287 root.lockedApp = "";
288 root.forceActiveFocus();
289 d.selectUser(d.currentIndex, true);
290 lightDM.infographic.readyForDataChange();
291 }
293 Connections {
294 target: loader.item
295 onSelected: {
296 d.selectUser(index, true);
297 }
298 onResponded: {
299 if (root.locked) {
300 lightDM.greeter.respond(response);
301 } else {
302 if (lightDM.greeter.active && !lightDM.greeter.authenticated) { // could happen if forcedUnlock
303 d.login();
304 }
305 loader.item.hide();
306 }
307 }
308 onTease: root.tease()
309 onEmergencyCall: root.emergencyCall()
310 onRequiredChanged: {
311 if (!loader.item.required) {
312 root.hide();
313 }
314 }
315 }
317 Binding {
318 target: loader.item
319 property: "backgroundTopMargin"
320 value: -root.y
321 }
323 Binding {
324 target: loader.item
325 property: "launcherOffset"
326 value: d.launcherOffsetProxy
327 }
329 Binding {
330 target: loader.item
331 property: "dragHandleLeftMargin"
332 value: root.dragHandleLeftMargin
333 }
335 Binding {
336 target: loader.item
337 property: "delayMinutes"
338 value: forcedDelayTimer.delayMinutes
339 }
341 Binding {
342 target: loader.item
343 property: "background"
344 value: root.background
345 }
347 Binding {
348 target: loader.item
349 property: "locked"
350 value: root.locked
351 }
353 Binding {
354 target: loader.item
355 property: "alphanumeric"
356 value: AccountsService.passwordDisplayHint === AccountsService.Keyboard
357 }
359 Binding {
360 target: loader.item
361 property: "currentIndex"
362 value: d.currentIndex
363 }
365 Binding {
366 target: loader.item
367 property: "userModel"
368 value: lightDM.users
369 }
371 Binding {
372 target: loader.item
373 property: "infographicModel"
374 value: lightDM.infographic
375 }
376 }
378 Connections {
379 target: lightDM.greeter
381 onShowGreeter: root.forceShow()
383 onHideGreeter: {
384 d.login();
385 loader.item.hide();
386 }
388 onShowMessage: {
389 if (!lightDM.greeter.active) {
390 return; // could happen if hideGreeter() comes in before we prompt
391 }
393 // inefficient, but we only rarely deal with messages
394 var html = text.replace(/&/g, "&amp;")
395 .replace(/</g, "&lt;")
396 .replace(/>/g, "&gt;")
397 .replace(/\n/g, "<br>");
398 if (isError) {
399 html = "<font color=\"#df382c\">" + html + "</font>";
400 }
402 loader.item.showMessage(html);
403 }
405 onShowPrompt: {
406 d.waiting = false;
408 if (!lightDM.greeter.active) {
409 return; // could happen if hideGreeter() comes in before we prompt
410 }
412 loader.item.showPrompt(text, isSecret, isDefaultPrompt);
413 }
415 onAuthenticationComplete: {
416 d.waiting = false;
418 if (lightDM.greeter.authenticated) {
419 AccountsService.failedLogins = 0;
420 d.login();
421 if (!lightDM.greeter.promptless) {
422 loader.item.hide();
423 }
424 } else {
425 if (!lightDM.greeter.promptless) {
426 AccountsService.failedLogins++;
427 }
429 // Check if we should initiate a factory reset
430 if (maxFailedLogins >= 2) { // require at least a warning
431 if (AccountsService.failedLogins === maxFailedLogins - 1) {
432 loader.item.showLastChance();
433 } else if (AccountsService.failedLogins >= maxFailedLogins) {
434 SystemImage.factoryReset(); // Ouch!
435 }
436 }
438 // Check if we should initiate a forced login delay
439 if (failedLoginsDelayAttempts > 0
440 && AccountsService.failedLogins > 0
441 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
442 forcedDelayTimer.forceDelay();
443 }
445 loader.item.notifyAuthenticationFailed();
446 if (!lightDM.greeter.promptless) {
447 d.selectUser(d.currentIndex, false);
448 }
449 }
450 }
452 onRequestAuthenticationUser: {
453 // Find index for requested user, if it exists
454 for (var i = 0; i < lightDM.users.count; i++) {
455 if (user === lightDM.users.data(i, lightDM.userRoles.NameRole)) {
456 d.selectUser(i, true);
457 return;
458 }
459 }
460 }
461 }
463 Connections {
464 target: DBusUnitySessionService
465 onLockRequested: root.forceShow()
466 }
468 Binding {
469 target: lightDM.greeter
470 property: "active"
471 value: root.active
472 }
474 Binding {
475 target: lightDM.infographic
476 property: "username"
477 value: AccountsService.statsWelcomeScreen ? lightDM.users.data(d.currentIndex, lightDM.userRoles.NameRole) : ""
478 }
480 Connections {
481 target: i18n
482 onLanguageChanged: lightDM.infographic.readyForDataChange()
483 }
485 Observer {
486 id: biometryd
487 objectName: "biometryd"
489 property var operation: null
490 readonly property bool idEnabled: root.active &&
491 Biometryd.defaultDevice &&
492 AccountsService.enableFingerprintIdentification
494 function cancelOperation() {
495 if (operation) {
496 operation.cancel();
497 operation = null;
498 }
499 }
501 function restartOperation() {
502 cancelOperation();
504 if (idEnabled) {
505 var identifier = Biometryd.defaultDevice.identifier;
506 operation = identifier.identifyUser();
507 operation.start(biometryd);
508 }
509 }
511 function failOperation(reason) {
512 console.log("Failed to identify user by fingerprint:", reason);
513 d.fingerprintFailureCount++;
514 restartOperation();
515 if (loader.item)
516 loader.item.showErrorMessage(i18n.tr("Try again"));
517 }
519 Component.onCompleted: restartOperation()
520 Component.onDestruction: cancelOperation()
521 onIdEnabledChanged: restartOperation()
523 onSucceeded: {
524 if (result.uid !== lightDM.users.data(d.currentIndex, lightDM.userRoles.UidRole)) {
525 failOperation("not the selected user");
526 return;
527 }
528 console.log("Identified user by fingerprint:", result.uid);
529 d.fingerprintFailureCount = 0;
530 if (loader.item)
531 loader.item.notifyAuthenticationSucceeded(true /* showFakePassword */);
532 if (root.active)
533 root.forcedUnlock = true;
534 }
535 onFailed: failOperation(reason)
536 }