on fdefead help sign in
unity8/qml/Greeter/Greeter.qml
462 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.4
18import AccountsService 0.1
19import GSettings 1.0
20import Ubuntu.Components 1.3
21import Ubuntu.SystemImage 0.1
22import Unity.Launcher 0.1
23import "../Components"
25Showable {
26 id: root
27 created: loader.status == Loader.Ready
29 property real dragHandleLeftMargin: 0
31 property url background
33 // How far to offset the top greeter layer during a launcher left-drag
34 property real launcherOffset
36 readonly property bool active: required || hasLockedApp
37 readonly property bool fullyShown: loader.item ? loader.item.fullyShown : false
39 // True when the greeter is waiting for PAM or other setup process
40 readonly property alias waiting: d.waiting
42 property string lockedApp: ""
43 readonly property bool hasLockedApp: lockedApp !== ""
45 property bool forcedUnlock
46 readonly property bool locked: lightDM.greeter.active && !lightDM.greeter.authenticated && !forcedUnlock
48 property bool tabletMode
49 property url viewSource // only used for testing
51 property int maxFailedLogins: -1 // disabled by default for now, will enable via settings in future
52 property int failedLoginsDelayAttempts: 7 // number of failed logins
53 property real failedLoginsDelayMinutes: 5 // minutes of forced waiting
55 readonly property bool animating: loader.item ? loader.item.animating : false
57 signal tease()
58 signal sessionStarted()
59 signal emergencyCall()
61 function forceShow() {
62 showNow();
63 loader.item.reset();
64 }
66 function notifyAppFocused(appId) {
67 if (!active) {
68 return;
69 }
71 if (hasLockedApp) {
72 if (appId === lockedApp) {
73 hide(); // show locked app
74 } else {
75 show();
76 d.startUnlock(false /* toTheRight */);
77 }
78 } else if (appId !== "unity8-dash") { // dash isn't started by user
79 d.startUnlock(false /* toTheRight */);
80 }
81 }
83 function notifyAboutToFocusApp(appId) {
84 if (!active) {
85 return;
86 }
88 // A hint that we're about to focus an app. This way we can look
89 // a little more responsive, rather than waiting for the above
90 // notifyAppFocused call. We also need this in case we have a locked
91 // app, in order to show lockscreen instead of new app.
92 d.startUnlock(false /* toTheRight */);
93 }
95 // This is a just a glorified notifyAboutToFocusApp(), but it does one
96 // other thing: it hides any cover pages to the RIGHT, because the user
97 // just came from a launcher drag starting on the left.
98 // It also returns a boolean value, indicating whether there was a visual
99 // change or not (the shell only wants to hide the launcher if there was
100 // a change).
101 function notifyShowingDashFromDrag() {
102 if (!active) {
103 return false;
104 }
106 return d.startUnlock(true /* toTheRight */);
107 }
109 LightDM{id:lightDM} // Provide backend access
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 }
190 Timer {
191 id: forcedDelayTimer
193 // We use a short interval and check against the system wall clock
194 // because we have to consider the case that the system is suspended
195 // for a few minutes. When we wake up, we want to quickly be correct.
196 interval: 500
198 property var delayTarget
199 property int delayMinutes
201 function forceDelay() {
202 // Store the beginning time for a lockout in GSettings, so that
203 // we still lock the user out if they reboot. And we store
204 // starting time rather than end-time or how-long because:
205 // - If storing end-time and on boot we have a problem with NTP,
206 // we might get locked out for a lot longer than we thought.
207 // - If storing how-long, and user turns their phone off for an
208 // hour rather than wait, they wouldn't expect to still be locked
209 // out.
210 // - A malicious actor could manipulate either of the above
211 // settings to keep the user out longer. But by storing
212 // start-time, we never make the user wait longer than the full
213 // lock out time.
214 greeterSettings.lockedOutTime = new Date().getTime();
215 checkForForcedDelay();
216 }
218 onTriggered: {
219 var diff = delayTarget - new Date();
220 if (diff > 0) {
221 delayMinutes = Math.ceil(diff / 60000);
222 start(); // go again
223 } else {
224 delayMinutes = 0;
225 }
226 }
228 function checkForForcedDelay() {
229 if (greeterSettings.lockedOutTime === 0) {
230 return;
231 }
233 var now = new Date();
234 delayTarget = new Date(greeterSettings.lockedOutTime + failedLoginsDelayMinutes * 60000);
236 // If tooEarly is true, something went very wrong. Bug or NTP
237 // misconfiguration maybe?
238 var tooEarly = now.getTime() < greeterSettings.lockedOutTime;
239 var tooLate = now >= delayTarget;
241 // Compare stored time to system time. If a malicious actor is
242 // able to manipulate time to avoid our lockout, they already have
243 // enough access to cause damage. So we choose to trust this check.
244 if (tooEarly || tooLate) {
245 stop();
246 delayMinutes = 0;
247 } else {
248 triggered();
249 }
250 }
252 Component.onCompleted: checkForForcedDelay()
253 }
255 // event eater
256 // Nothing should leak to items behind the greeter
257 MouseArea { anchors.fill: parent; hoverEnabled: true }
259 Loader {
260 id: loader
261 objectName: "loader"
263 anchors.fill: parent
265 active: root.required
266 source: root.viewSource.toString() ? root.viewSource :
267 (d.multiUser || root.tabletMode) ? "WideView.qml" : "NarrowView.qml"
269 onLoaded: {
270 root.lockedApp = "";
271 root.forceActiveFocus();
272 d.selectUser(d.currentIndex, true);
273 lightDM.infographic.readyForDataChange();
274 }
276 Connections {
277 target: loader.item
278 onSelected: {
279 d.selectUser(index, true);
280 }
281 onResponded: {
282 if (root.locked) {
283 lightDM.greeter.respond(response);
284 } else {
285 if (lightDM.greeter.active && !lightDM.greeter.authenticated) { // could happen if forcedUnlock
286 d.login();
287 }
288 loader.item.hide();
289 }
290 }
291 onTease: root.tease()
292 onEmergencyCall: root.emergencyCall()
293 onRequiredChanged: {
294 if (!loader.item.required) {
295 root.hide();
296 }
297 }
298 }
300 Binding {
301 target: loader.item
302 property: "backgroundTopMargin"
303 value: -root.y
304 }
306 Binding {
307 target: loader.item
308 property: "launcherOffset"
309 value: d.launcherOffsetProxy
310 }
312 Binding {
313 target: loader.item
314 property: "dragHandleLeftMargin"
315 value: root.dragHandleLeftMargin
316 }
318 Binding {
319 target: loader.item
320 property: "delayMinutes"
321 value: forcedDelayTimer.delayMinutes
322 }
324 Binding {
325 target: loader.item
326 property: "background"
327 value: root.background
328 }
330 Binding {
331 target: loader.item
332 property: "locked"
333 value: root.locked
334 }
336 Binding {
337 target: loader.item
338 property: "alphanumeric"
339 value: AccountsService.passwordDisplayHint === AccountsService.Keyboard
340 }
342 Binding {
343 target: loader.item
344 property: "currentIndex"
345 value: d.currentIndex
346 }
348 Binding {
349 target: loader.item
350 property: "userModel"
351 value: lightDM.users
352 }
354 Binding {
355 target: loader.item
356 property: "infographicModel"
357 value: lightDM.infographic
358 }
359 }
361 Connections {
362 target: lightDM.greeter
364 onShowGreeter: root.forceShow()
366 onHideGreeter: {
367 d.login();
368 loader.item.hide();
369 }
371 onShowMessage: {
372 if (!lightDM.greeter.active) {
373 return; // could happen if hideGreeter() comes in before we prompt
374 }
376 // inefficient, but we only rarely deal with messages
377 var html = text.replace(/&/g, "&amp;")
378 .replace(/</g, "&lt;")
379 .replace(/>/g, "&gt;")
380 .replace(/\n/g, "<br>");
381 if (isError) {
382 html = "<font color=\"#df382c\">" + html + "</font>";
383 }
385 loader.item.showMessage(html);
386 }
388 onShowPrompt: {
389 d.waiting = false;
391 if (!lightDM.greeter.active) {
392 return; // could happen if hideGreeter() comes in before we prompt
393 }
395 loader.item.showPrompt(text, isSecret, isDefaultPrompt);
396 }
398 onAuthenticationComplete: {
399 d.waiting = false;
401 if (lightDM.greeter.authenticated) {
402 AccountsService.failedLogins = 0;
403 d.login();
404 if (!lightDM.greeter.promptless) {
405 loader.item.hide();
406 }
407 } else {
408 if (!lightDM.greeter.promptless) {
409 AccountsService.failedLogins++;
410 }
412 // Check if we should initiate a factory reset
413 if (maxFailedLogins >= 2) { // require at least a warning
414 if (AccountsService.failedLogins === maxFailedLogins - 1) {
415 loader.item.showLastChance();
416 } else if (AccountsService.failedLogins >= maxFailedLogins) {
417 SystemImage.factoryReset(); // Ouch!
418 }
419 }
421 // Check if we should initiate a forced login delay
422 if (failedLoginsDelayAttempts > 0
423 && AccountsService.failedLogins > 0
424 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
425 forcedDelayTimer.forceDelay();
426 }
428 loader.item.notifyAuthenticationFailed();
429 if (!lightDM.greeter.promptless) {
430 d.selectUser(d.currentIndex, false);
431 }
432 }
433 }
435 onRequestAuthenticationUser: {
436 // Find index for requested user, if it exists
437 for (var i = 0; i < lightDM.users.count; i++) {
438 if (user === lightDM.users.data(i, lightDM.userRoles.NameRole)) {
439 d.selectUser(i, true);
440 return;
441 }
442 }
443 }
444 }
446 Binding {
447 target: lightDM.greeter
448 property: "active"
449 value: root.active
450 }
452 Binding {
453 target: lightDM.infographic
454 property: "username"
455 value: AccountsService.statsWelcomeScreen ? lightDM.users.data(d.currentIndex, lightDM.userRoles.NameRole) : ""
456 }
458 Connections {
459 target: i18n
460 onLanguageChanged: lightDM.infographic.readyForDataChange()
461 }