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