Unity 8
Greeter.qml
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  */
16 
17 import QtQuick 2.4
18 import AccountsService 0.1
19 import GSettings 1.0
20 import Ubuntu.Components 1.3
21 import Ubuntu.SystemImage 0.1
22 import Unity.Launcher 0.1
23 import Unity.Session 0.1
24 import "../Components"
25 
26 Showable {
27  id: root
28  created: loader.status == Loader.Ready
29 
30  property real dragHandleLeftMargin: 0
31 
32  property url background
33 
34  // How far to offset the top greeter layer during a launcher left-drag
35  property real launcherOffset
36 
37  readonly property bool active: required || hasLockedApp
38  readonly property bool fullyShown: loader.item ? loader.item.fullyShown : false
39 
40  // True when the greeter is waiting for PAM or other setup process
41  readonly property alias waiting: d.waiting
42 
43  property string lockedApp: ""
44  readonly property bool hasLockedApp: lockedApp !== ""
45 
46  property bool forcedUnlock
47  readonly property bool locked: lightDM.greeter.active && !lightDM.greeter.authenticated && !forcedUnlock
48 
49  property bool tabletMode
50  property url viewSource // only used for testing
51 
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
55 
56  readonly property bool animating: loader.item ? loader.item.animating : false
57 
58  signal tease()
59  signal sessionStarted()
60  signal emergencyCall()
61 
62  function forceShow() {
63  showNow();
64  d.selectUser(d.currentIndex, true);
65  }
66 
67  function notifyAppFocused(appId) {
68  if (!active) {
69  return;
70  }
71 
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  }
83 
84  function notifyAboutToFocusApp(appId) {
85  if (!active) {
86  return;
87  }
88 
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  }
95 
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  }
106 
107  return d.startUnlock(true /* toTheRight */);
108  }
109 
110  LightDM{id:lightDM} // Provide backend access
111  QtObject {
112  id: d
113 
114  readonly property bool multiUser: lightDM.users.count > 1
115  property int currentIndex
116  property bool waiting
117 
118  // We want 'launcherOffset' to animate down to zero. But not to animate
119  // while being dragged. So ideally we change this only when the user
120  // lets go and launcherOffset drops to zero. But we need to wait for
121  // the behavior to be enabled first. So we cache the last known good
122  // launcherOffset value to cover us during that brief gap between
123  // release and the behavior turning on.
124  property real lastKnownPositiveOffset // set in a launcherOffsetChanged below
125  property real launcherOffsetProxy: (shown && !launcherOffsetProxyBehavior.enabled) ? lastKnownPositiveOffset : 0
126  Behavior on launcherOffsetProxy {
127  id: launcherOffsetProxyBehavior
128  enabled: launcherOffset === 0
129  UbuntuNumberAnimation {}
130  }
131 
132  function selectUser(uid, reset) {
133  d.waiting = true;
134  if (reset) {
135  loader.item.reset();
136  }
137  currentIndex = uid;
138  var user = lightDM.users.data(uid, lightDM.userRoles.NameRole);
139  AccountsService.user = user;
140  LauncherModel.setUser(user);
141  lightDM.greeter.authenticate(user); // always resets auth state
142  }
143 
144  function login() {
145  enabled = false;
146  if (lightDM.greeter.startSessionSync()) {
147  sessionStarted();
148  if (loader.item) {
149  loader.item.notifyAuthenticationSucceeded();
150  }
151  } else if (loader.item) {
152  loader.item.notifyAuthenticationFailed();
153  }
154  enabled = true;
155  }
156 
157  function startUnlock(toTheRight) {
158  if (loader.item) {
159  return loader.item.tryToUnlock(toTheRight);
160  } else {
161  return false;
162  }
163  }
164 
165  function checkForcedUnlock() {
166  if (forcedUnlock && shown && loader.item) {
167  // pretend we were just authenticated
168  loader.item.notifyAuthenticationSucceeded();
169  loader.item.hide();
170  }
171  }
172  }
173 
174  onLauncherOffsetChanged: {
175  if (launcherOffset > 0) {
176  d.lastKnownPositiveOffset = launcherOffset;
177  }
178  }
179 
180  onForcedUnlockChanged: d.checkForcedUnlock()
181  Component.onCompleted: d.checkForcedUnlock()
182 
183  onRequiredChanged: {
184  if (required) {
185  d.waiting = true;
186  lockedApp = "";
187  }
188  }
189 
190  GSettings {
191  id: greeterSettings
192  schema.id: "com.canonical.Unity8.Greeter"
193  }
194 
195  Timer {
196  id: forcedDelayTimer
197 
198  // We use a short interval and check against the system wall clock
199  // because we have to consider the case that the system is suspended
200  // for a few minutes. When we wake up, we want to quickly be correct.
201  interval: 500
202 
203  property var delayTarget
204  property int delayMinutes
205 
206  function forceDelay() {
207  // Store the beginning time for a lockout in GSettings, so that
208  // we still lock the user out if they reboot. And we store
209  // starting time rather than end-time or how-long because:
210  // - If storing end-time and on boot we have a problem with NTP,
211  // we might get locked out for a lot longer than we thought.
212  // - If storing how-long, and user turns their phone off for an
213  // hour rather than wait, they wouldn't expect to still be locked
214  // out.
215  // - A malicious actor could manipulate either of the above
216  // settings to keep the user out longer. But by storing
217  // start-time, we never make the user wait longer than the full
218  // lock out time.
219  greeterSettings.lockedOutTime = new Date().getTime();
220  checkForForcedDelay();
221  }
222 
223  onTriggered: {
224  var diff = delayTarget - new Date();
225  if (diff > 0) {
226  delayMinutes = Math.ceil(diff / 60000);
227  start(); // go again
228  } else {
229  delayMinutes = 0;
230  }
231  }
232 
233  function checkForForcedDelay() {
234  if (greeterSettings.lockedOutTime === 0) {
235  return;
236  }
237 
238  var now = new Date();
239  delayTarget = new Date(greeterSettings.lockedOutTime + failedLoginsDelayMinutes * 60000);
240 
241  // If tooEarly is true, something went very wrong. Bug or NTP
242  // misconfiguration maybe?
243  var tooEarly = now.getTime() < greeterSettings.lockedOutTime;
244  var tooLate = now >= delayTarget;
245 
246  // Compare stored time to system time. If a malicious actor is
247  // able to manipulate time to avoid our lockout, they already have
248  // enough access to cause damage. So we choose to trust this check.
249  if (tooEarly || tooLate) {
250  stop();
251  delayMinutes = 0;
252  } else {
253  triggered();
254  }
255  }
256 
257  Component.onCompleted: checkForForcedDelay()
258  }
259 
260  // event eater
261  // Nothing should leak to items behind the greeter
262  MouseArea { anchors.fill: parent; hoverEnabled: true }
263 
264  Loader {
265  id: loader
266  objectName: "loader"
267 
268  anchors.fill: parent
269 
270  active: root.required
271  source: root.viewSource.toString() ? root.viewSource :
272  (d.multiUser || root.tabletMode) ? "WideView.qml" : "NarrowView.qml"
273 
274  onLoaded: {
275  root.lockedApp = "";
276  root.forceActiveFocus();
277  d.selectUser(d.currentIndex, true);
278  lightDM.infographic.readyForDataChange();
279  }
280 
281  Connections {
282  target: loader.item
283  onSelected: {
284  d.selectUser(index, true);
285  }
286  onResponded: {
287  if (root.locked) {
288  lightDM.greeter.respond(response);
289  } else {
290  if (lightDM.greeter.active && !lightDM.greeter.authenticated) { // could happen if forcedUnlock
291  d.login();
292  }
293  loader.item.hide();
294  }
295  }
296  onTease: root.tease()
297  onEmergencyCall: root.emergencyCall()
298  onRequiredChanged: {
299  if (!loader.item.required) {
300  root.hide();
301  }
302  }
303  }
304 
305  Binding {
306  target: loader.item
307  property: "backgroundTopMargin"
308  value: -root.y
309  }
310 
311  Binding {
312  target: loader.item
313  property: "launcherOffset"
314  value: d.launcherOffsetProxy
315  }
316 
317  Binding {
318  target: loader.item
319  property: "dragHandleLeftMargin"
320  value: root.dragHandleLeftMargin
321  }
322 
323  Binding {
324  target: loader.item
325  property: "delayMinutes"
326  value: forcedDelayTimer.delayMinutes
327  }
328 
329  Binding {
330  target: loader.item
331  property: "background"
332  value: root.background
333  }
334 
335  Binding {
336  target: loader.item
337  property: "locked"
338  value: root.locked
339  }
340 
341  Binding {
342  target: loader.item
343  property: "alphanumeric"
344  value: AccountsService.passwordDisplayHint === AccountsService.Keyboard
345  }
346 
347  Binding {
348  target: loader.item
349  property: "currentIndex"
350  value: d.currentIndex
351  }
352 
353  Binding {
354  target: loader.item
355  property: "userModel"
356  value: lightDM.users
357  }
358 
359  Binding {
360  target: loader.item
361  property: "infographicModel"
362  value: lightDM.infographic
363  }
364  }
365 
366  Connections {
367  target: lightDM.greeter
368 
369  onShowGreeter: root.forceShow()
370 
371  onHideGreeter: {
372  d.login();
373  loader.item.hide();
374  }
375 
376  onShowMessage: {
377  if (!lightDM.greeter.active) {
378  return; // could happen if hideGreeter() comes in before we prompt
379  }
380 
381  // inefficient, but we only rarely deal with messages
382  var html = text.replace(/&/g, "&amp;")
383  .replace(/</g, "&lt;")
384  .replace(/>/g, "&gt;")
385  .replace(/\n/g, "<br>");
386  if (isError) {
387  html = "<font color=\"#df382c\">" + html + "</font>";
388  }
389 
390  loader.item.showMessage(html);
391  }
392 
393  onShowPrompt: {
394  d.waiting = false;
395 
396  if (!lightDM.greeter.active) {
397  return; // could happen if hideGreeter() comes in before we prompt
398  }
399 
400  loader.item.showPrompt(text, isSecret, isDefaultPrompt);
401  }
402 
403  onAuthenticationComplete: {
404  d.waiting = false;
405 
406  if (lightDM.greeter.authenticated) {
407  AccountsService.failedLogins = 0;
408  d.login();
409  if (!lightDM.greeter.promptless) {
410  loader.item.hide();
411  }
412  } else {
413  if (!lightDM.greeter.promptless) {
414  AccountsService.failedLogins++;
415  }
416 
417  // Check if we should initiate a factory reset
418  if (maxFailedLogins >= 2) { // require at least a warning
419  if (AccountsService.failedLogins === maxFailedLogins - 1) {
420  loader.item.showLastChance();
421  } else if (AccountsService.failedLogins >= maxFailedLogins) {
422  SystemImage.factoryReset(); // Ouch!
423  }
424  }
425 
426  // Check if we should initiate a forced login delay
427  if (failedLoginsDelayAttempts > 0
428  && AccountsService.failedLogins > 0
429  && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
430  forcedDelayTimer.forceDelay();
431  }
432 
433  loader.item.notifyAuthenticationFailed();
434  if (!lightDM.greeter.promptless) {
435  d.selectUser(d.currentIndex, false);
436  }
437  }
438  }
439 
440  onRequestAuthenticationUser: {
441  // Find index for requested user, if it exists
442  for (var i = 0; i < lightDM.users.count; i++) {
443  if (user === lightDM.users.data(i, lightDM.userRoles.NameRole)) {
444  d.selectUser(i, true);
445  return;
446  }
447  }
448  }
449  }
450 
451  Connections {
452  target: DBusUnitySessionService
453  onLockRequested: root.forceShow()
454  }
455 
456  Binding {
457  target: lightDM.greeter
458  property: "active"
459  value: root.active
460  }
461 
462  Binding {
463  target: lightDM.infographic
464  property: "username"
465  value: AccountsService.statsWelcomeScreen ? lightDM.users.data(d.currentIndex, lightDM.userRoles.NameRole) : ""
466  }
467 
468  Connections {
469  target: i18n
470  onLanguageChanged: lightDM.infographic.readyForDataChange()
471  }
472 }