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 "../Components"
24 
25 Showable {
26  id: root
27  created: loader.status == Loader.Ready
28 
29  property real dragHandleLeftMargin: 0
30 
31  property url background
32 
33  // How far to offset the top greeter layer during a launcher left-drag
34  property real launcherOffset
35 
36  readonly property bool active: required || hasLockedApp
37  readonly property bool fullyShown: loader.item ? loader.item.fullyShown : false
38 
39  // True when the greeter is waiting for PAM or other setup process
40  readonly property alias waiting: d.waiting
41 
42  property string lockedApp: ""
43  readonly property bool hasLockedApp: lockedApp !== ""
44 
45  property bool forcedUnlock
46  readonly property bool locked: lightDM.greeter.active && !lightDM.greeter.authenticated && !forcedUnlock
47 
48  property bool tabletMode
49  property url viewSource // only used for testing
50 
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
54 
55  readonly property bool animating: loader.item ? loader.item.animating : false
56 
57  signal tease()
58  signal sessionStarted()
59  signal emergencyCall()
60 
61  function forceShow() {
62  showNow();
63  loader.item.reset();
64  }
65 
66  function notifyAppFocused(appId) {
67  if (!active) {
68  return;
69  }
70 
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  }
82 
83  function notifyAboutToFocusApp(appId) {
84  if (!active) {
85  return;
86  }
87 
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  }
94 
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  }
105 
106  return d.startUnlock(true /* toTheRight */);
107  }
108 
109  LightDM{id:lightDM} // Provide backend access
110  QtObject {
111  id: d
112 
113  readonly property bool multiUser: lightDM.users.count > 1
114  property int currentIndex
115  property bool waiting
116 
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  }
130 
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  }
142 
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  }
155 
156  function startUnlock(toTheRight) {
157  if (loader.item) {
158  return loader.item.tryToUnlock(toTheRight);
159  } else {
160  return false;
161  }
162  }
163  }
164 
165  onLauncherOffsetChanged: {
166  if (launcherOffset > 0) {
167  d.lastKnownPositiveOffset = launcherOffset;
168  }
169  }
170 
171  onForcedUnlockChanged: {
172  if (forcedUnlock && shown) {
173  // pretend we were just authenticated
174  loader.item.notifyAuthenticationSucceeded();
175  }
176  }
177 
178  onRequiredChanged: {
179  if (required) {
180  d.waiting = true;
181  lockedApp = "";
182  }
183  }
184 
185  GSettings {
186  id: greeterSettings
187  schema.id: "com.canonical.Unity8.Greeter"
188  }
189 
190  Timer {
191  id: forcedDelayTimer
192 
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
197 
198  property var delayTarget
199  property int delayMinutes
200 
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  }
217 
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  }
227 
228  function checkForForcedDelay() {
229  if (greeterSettings.lockedOutTime === 0) {
230  return;
231  }
232 
233  var now = new Date();
234  delayTarget = new Date(greeterSettings.lockedOutTime + failedLoginsDelayMinutes * 60000);
235 
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;
240 
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  }
251 
252  Component.onCompleted: checkForForcedDelay()
253  }
254 
255  // event eater
256  // Nothing should leak to items behind the greeter
257  MouseArea { anchors.fill: parent; hoverEnabled: true }
258 
259  Loader {
260  id: loader
261  objectName: "loader"
262 
263  anchors.fill: parent
264 
265  active: root.required
266  source: root.viewSource.toString() ? root.viewSource :
267  (d.multiUser || root.tabletMode) ? "WideView.qml" : "NarrowView.qml"
268 
269  onLoaded: {
270  root.lockedApp = "";
271  root.forceActiveFocus();
272  d.selectUser(d.currentIndex, true);
273  lightDM.infographic.readyForDataChange();
274  }
275 
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  }
299 
300  Binding {
301  target: loader.item
302  property: "backgroundTopMargin"
303  value: -root.y
304  }
305 
306  Binding {
307  target: loader.item
308  property: "launcherOffset"
309  value: d.launcherOffsetProxy
310  }
311 
312  Binding {
313  target: loader.item
314  property: "dragHandleLeftMargin"
315  value: root.dragHandleLeftMargin
316  }
317 
318  Binding {
319  target: loader.item
320  property: "delayMinutes"
321  value: forcedDelayTimer.delayMinutes
322  }
323 
324  Binding {
325  target: loader.item
326  property: "background"
327  value: root.background
328  }
329 
330  Binding {
331  target: loader.item
332  property: "locked"
333  value: root.locked
334  }
335 
336  Binding {
337  target: loader.item
338  property: "alphanumeric"
339  value: AccountsService.passwordDisplayHint === AccountsService.Keyboard
340  }
341 
342  Binding {
343  target: loader.item
344  property: "currentIndex"
345  value: d.currentIndex
346  }
347 
348  Binding {
349  target: loader.item
350  property: "userModel"
351  value: lightDM.users
352  }
353 
354  Binding {
355  target: loader.item
356  property: "infographicModel"
357  value: lightDM.infographic
358  }
359  }
360 
361  Connections {
362  target: lightDM.greeter
363 
364  onShowGreeter: root.forceShow()
365 
366  onHideGreeter: {
367  d.login();
368  loader.item.hide();
369  }
370 
371  onShowMessage: {
372  if (!lightDM.greeter.active) {
373  return; // could happen if hideGreeter() comes in before we prompt
374  }
375 
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  }
384 
385  loader.item.showMessage(html);
386  }
387 
388  onShowPrompt: {
389  d.waiting = false;
390 
391  if (!lightDM.greeter.active) {
392  return; // could happen if hideGreeter() comes in before we prompt
393  }
394 
395  loader.item.showPrompt(text, isSecret, isDefaultPrompt);
396  }
397 
398  onAuthenticationComplete: {
399  d.waiting = false;
400 
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  }
411 
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  }
420 
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  }
427 
428  loader.item.notifyAuthenticationFailed();
429  if (!lightDM.greeter.promptless) {
430  d.selectUser(d.currentIndex, false);
431  }
432  }
433  }
434 
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  }
445 
446  Binding {
447  target: lightDM.greeter
448  property: "active"
449  value: root.active
450  }
451 
452  Binding {
453  target: lightDM.infographic
454  property: "username"
455  value: AccountsService.statsWelcomeScreen ? lightDM.users.data(d.currentIndex, lightDM.userRoles.NameRole) : ""
456  }
457 
458  Connections {
459  target: i18n
460  onLanguageChanged: lightDM.infographic.readyForDataChange()
461  }
462 }