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