on a142f21 help sign in
unity8/qml/Greeter/Greeter.qml
461 lines 15 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.3
18import AccountsService 0.1
19import GSettings 1.0
20import LightDM 0.1 as LightDM
21import Ubuntu.Components 1.1
22import Ubuntu.SystemImage 0.1
23import Unity.Launcher 0.1
24import "../Components"
26Showable {
27 id: root
28 created: loader.status == Loader.Ready
30 property real dragHandleLeftMargin: 0
32 property url background
34 // How far to offset the top greeter layer during a launcher left-drag
35 property real launcherOffset
37 readonly property bool active: shown || hasLockedApp
38 readonly property bool fullyShown: loader.item ? loader.item.fullyShown : false
40 // True when the greeter is waiting for PAM or other setup process
41 readonly property alias waiting: d.waiting
43 property string lockedApp: ""
44 readonly property bool hasLockedApp: lockedApp !== ""
46 property bool forcedUnlock
47 readonly property bool locked: LightDM.Greeter.active && !LightDM.Greeter.authenticated && !forcedUnlock
49 property bool tabletMode
50 property url viewSource // only used for testing
52 property int maxFailedLogins: -1 // disabled by default for now, will enable via settings in future
53 property int failedLoginsDelayAttempts: 7 // number of failed logins
54 property real failedLoginsDelayMinutes: 5 // minutes of forced waiting
56 readonly property bool animating: loader.item ? loader.item.animating : false
58 signal tease()
59 signal sessionStarted()
60 signal emergencyCall()
62 function forceShow() {
63 showNow();
64 loader.item.reset();
65 }
67 function notifyAppFocused(appId) {
68 if (!active) {
69 return;
70 }
72 if (hasLockedApp) {
73 if (appId === lockedApp) {
74 hide(); // show locked app
75 } else {
76 show();
77 d.startUnlock(false /* toTheRight */);
78 }
79 } else if (appId !== "unity8-dash") { // dash isn't started by user
80 d.startUnlock(false /* toTheRight */);
81 }
82 }
84 function notifyAboutToFocusApp(appId) {
85 if (!active) {
86 return;
87 }
89 // A hint that we're about to focus an app. This way we can look
90 // a little more responsive, rather than waiting for the above
91 // notifyAppFocused call. We also need this in case we have a locked
92 // app, in order to show lockscreen instead of new app.
93 d.startUnlock(false /* toTheRight */);
94 }
96 // This is a just a glorified notifyAboutToFocusApp(), but it does one
97 // other thing: it hides any cover pages to the RIGHT, because the user
98 // just came from a launcher drag starting on the left.
99 // It also returns a boolean value, indicating whether there was a visual
100 // change or not (the shell only wants to hide the launcher if there was
101 // a change).
102 function notifyShowingDashFromDrag() {
103 if (!active) {
104 return false;
105 }
107 return d.startUnlock(true /* toTheRight */);
108 }
110 QtObject {
111 id: d
113 readonly property bool multiUser: LightDM.Users.count > 1
114 property int currentIndex
115 property bool waiting
117 // We want 'launcherOffset' to animate down to zero. But not to animate
118 // while being dragged. So ideally we change this only when the user
119 // lets go and launcherOffset drops to zero. But we need to wait for
120 // the behavior to be enabled first. So we cache the last known good
121 // launcherOffset value to cover us during that brief gap between
122 // release and the behavior turning on.
123 property real lastKnownPositiveOffset // set in a launcherOffsetChanged below
124 property real launcherOffsetProxy: (shown && !launcherOffsetProxyBehavior.enabled) ? lastKnownPositiveOffset : 0
125 Behavior on launcherOffsetProxy {
126 id: launcherOffsetProxyBehavior
127 enabled: launcherOffset === 0
128 UbuntuNumberAnimation {}
129 }
131 function selectUser(uid, reset) {
132 d.waiting = true;
133 if (reset) {
134 loader.item.reset();
135 }
136 currentIndex = uid;
137 var user = LightDM.Users.data(uid, LightDM.UserRoles.NameRole);
138 AccountsService.user = user;
139 LauncherModel.setUser(user);
140 LightDM.Greeter.authenticate(user); // always resets auth state
141 }
143 function login() {
144 enabled = false;
145 if (LightDM.Greeter.startSessionSync()) {
146 sessionStarted();
147 if (loader.item) {
148 loader.item.notifyAuthenticationSucceeded();
149 }
150 } else if (loader.item) {
151 loader.item.notifyAuthenticationFailed();
152 }
153 enabled = true;
154 }
156 function startUnlock(toTheRight) {
157 if (loader.item) {
158 return loader.item.tryToUnlock(toTheRight);
159 } else {
160 return false;
161 }
162 }
163 }
165 onLauncherOffsetChanged: {
166 if (launcherOffset > 0) {
167 d.lastKnownPositiveOffset = launcherOffset;
168 }
169 }
171 onForcedUnlockChanged: {
172 if (forcedUnlock && shown) {
173 // pretend we were just authenticated
174 loader.item.notifyAuthenticationSucceeded();
175 }
176 }
178 onRequiredChanged: {
179 if (required) {
180 d.waiting = true;
181 lockedApp = "";
182 }
183 }
185 GSettings {
186 id: greeterSettings
187 schema.id: "com.canonical.Unity8.Greeter"
188 onLockedOutTimeChanged: forcedDelayTimer.checkForForcedDelay()
189 }
191 Timer {
192 id: forcedDelayTimer
194 // We use a short interval and check against the system wall clock
195 // because we have to consider the case that the system is suspended
196 // for a few minutes. When we wake up, we want to quickly be correct.
197 interval: 500
199 property var delayTarget;
200 property int delayMinutes;
202 function forceDelay() {
203 // Store the beginning time for a lockout in GSettings, so that
204 // we still lock the user out if they reboot. And we store
205 // starting time rather than end-time or how-long because:
206 // - If storing end-time and on boot we have a problem with NTP,
207 // we might get locked out for a lot longer than we thought.
208 // - If storing how-long, and user turns their phone off for an
209 // hour rather than wait, they wouldn't expect to still be locked
210 // out.
211 // - A malicious actor could manipulate either of the above
212 // settings to keep the user out longer. But by storing
213 // start-time, we never make the user wait longer than the full
214 // lock out time.
215 // The code below that watches GSettings will pick up the change
216 // for us.
217 greeterSettings.lockedOutTime = new Date().getTime();
218 }
220 onTriggered: {
221 var diff = delayTarget - new Date();
222 if (diff > 0) {
223 delayMinutes = Math.ceil(diff / 60000);
224 start(); // go again
225 } else {
226 delayMinutes = 0;
227 }
228 }
230 function checkForForcedDelay() {
231 var now = new Date();
232 delayTarget = now;
233 delayTarget.setTime(greeterSettings.lockedOutTime + failedLoginsDelayMinutes * 60000);
235 // If tooEarly is true, something went very wrong. Bug or NTP
236 // misconfiguration maybe?
237 var tooEarly = now.getTime() < greeterSettings.lockedOutTime;
238 var tooLate = now > delayTarget;
240 // Compare stored time to system time. If a malicious actor is
241 // able to manipulate time to avoid our lockout, they already have
242 // enough access to cause damage. So we choose to trust this check.
243 if (tooEarly || tooLate) {
244 stop();
245 delayMinutes = 0;
246 } else {
247 triggered();
248 }
249 }
251 Component.onCompleted: checkForForcedDelay()
252 }
254 // event eater
255 // Nothing should leak to items behind the greeter
256 MouseArea { anchors.fill: parent }
258 Loader {
259 id: loader
260 objectName: "loader"
262 anchors.fill: parent
264 active: root.required
265 source: root.viewSource.toString() ? root.viewSource :
266 (d.multiUser || root.tabletMode) ? "WideView.qml" : "NarrowView.qml"
268 onLoaded: {
269 root.lockedApp = "";
270 root.forceActiveFocus();
271 d.selectUser(d.currentIndex, true);
272 LightDM.Infographic.readyForDataChange();
273 }
275 Connections {
276 target: loader.item
277 onSelected: {
278 d.selectUser(index, true);
279 }
280 onResponded: {
281 if (root.locked) {
282 LightDM.Greeter.respond(response);
283 } else {
284 if (LightDM.Greeter.active && !LightDM.Greeter.authenticated) { // could happen if forcedUnlock
285 d.login();
286 }
287 loader.item.hide();
288 }
289 }
290 onTease: root.tease()
291 onEmergencyCall: root.emergencyCall()
292 onRequiredChanged: {
293 if (!loader.item.required) {
294 root.hide();
295 }
296 }
297 }
299 Binding {
300 target: loader.item
301 property: "backgroundTopMargin"
302 value: -root.y
303 }
305 Binding {
306 target: loader.item
307 property: "launcherOffset"
308 value: d.launcherOffsetProxy
309 }
311 Binding {
312 target: loader.item
313 property: "dragHandleLeftMargin"
314 value: root.dragHandleLeftMargin
315 }
317 Binding {
318 target: loader.item
319 property: "delayMinutes"
320 value: forcedDelayTimer.delayMinutes
321 }
323 Binding {
324 target: loader.item
325 property: "background"
326 value: root.background
327 }
329 Binding {
330 target: loader.item
331 property: "locked"
332 value: root.locked
333 }
335 Binding {
336 target: loader.item
337 property: "alphanumeric"
338 value: AccountsService.passwordDisplayHint === AccountsService.Keyboard
339 }
341 Binding {
342 target: loader.item
343 property: "currentIndex"
344 value: d.currentIndex
345 }
347 Binding {
348 target: loader.item
349 property: "userModel"
350 value: LightDM.Users
351 }
353 Binding {
354 target: loader.item
355 property: "infographicModel"
356 value: LightDM.Infographic
357 }
358 }
360 Connections {
361 target: LightDM.Greeter
363 onShowGreeter: root.forceShow()
365 onHideGreeter: {
366 d.login();
367 loader.item.hide();
368 }
370 onShowMessage: {
371 if (!LightDM.Greeter.active) {
372 return; // could happen if hideGreeter() comes in before we prompt
373 }
375 // inefficient, but we only rarely deal with messages
376 var html = text.replace(/&/g, "&amp;")
377 .replace(/</g, "&lt;")
378 .replace(/>/g, "&gt;")
379 .replace(/\n/g, "<br>");
380 if (isError) {
381 html = "<font color=\"#df382c\">" + html + "</font>";
382 }
384 loader.item.showMessage(html);
385 }
387 onShowPrompt: {
388 d.waiting = false;
390 if (!LightDM.Greeter.active) {
391 return; // could happen if hideGreeter() comes in before we prompt
392 }
394 loader.item.showPrompt(text, isSecret, isDefaultPrompt);
395 }
397 onAuthenticationComplete: {
398 d.waiting = false;
400 if (LightDM.Greeter.authenticated) {
401 AccountsService.failedLogins = 0;
402 d.login();
403 if (!LightDM.Greeter.promptless) {
404 loader.item.hide();
405 }
406 } else {
407 if (!LightDM.Greeter.promptless) {
408 AccountsService.failedLogins++;
409 }
411 // Check if we should initiate a factory reset
412 if (maxFailedLogins >= 2) { // require at least a warning
413 if (AccountsService.failedLogins === maxFailedLogins - 1) {
414 loader.item.showLastChance();
415 } else if (AccountsService.failedLogins >= maxFailedLogins) {
416 SystemImage.factoryReset(); // Ouch!
417 }
418 }
420 // Check if we should initiate a forced login delay
421 if (failedLoginsDelayAttempts > 0
422 && AccountsService.failedLogins > 0
423 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
424 forcedDelayTimer.forceDelay();
425 }
427 loader.item.notifyAuthenticationFailed();
428 if (!LightDM.Greeter.promptless) {
429 d.selectUser(d.currentIndex, false);
430 }
431 }
432 }
434 onRequestAuthenticationUser: {
435 // Find index for requested user, if it exists
436 for (var i = 0; i < LightDM.Users.count; i++) {
437 if (user === LightDM.Users.data(i, LightDM.UserRoles.NameRole)) {
438 d.selectUser(i, true);
439 return;
440 }
441 }
442 }
443 }
445 Binding {
446 target: LightDM.Greeter
447 property: "active"
448 value: root.active
449 }
451 Binding {
452 target: LightDM.Infographic
453 property: "username"
454 value: AccountsService.statsWelcomeScreen ? LightDM.Users.data(d.currentIndex, LightDM.UserRoles.NameRole) : ""
455 }
457 Connections {
458 target: i18n
459 onLanguageChanged: LightDM.Infographic.readyForDataChange()
460 }