github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/usersession/userd/settings.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017-2020 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package userd 21 22 import ( 23 "fmt" 24 "os/exec" 25 "path/filepath" 26 "strings" 27 "time" 28 29 "github.com/godbus/dbus" 30 31 "github.com/snapcore/snapd/dirs" 32 "github.com/snapcore/snapd/i18n" 33 "github.com/snapcore/snapd/osutil" 34 "github.com/snapcore/snapd/snap" 35 "github.com/snapcore/snapd/usersession/userd/ui" 36 ) 37 38 // Timeout when the confirmation dialog for an xdg-setging 39 // automatically closes. Keep in sync with the core snaps 40 // xdg-settings wrapper which also sets this value to 300. 41 var defaultConfirmDialogTimeout = 300 * time.Second 42 43 const settingsIntrospectionXML = ` 44 <interface name="org.freedesktop.DBus.Peer"> 45 <method name='Ping'> 46 </method> 47 <method name='GetMachineId'> 48 <arg type='s' name='machine_uuid' direction='out'/> 49 </method> 50 </interface> 51 <interface name='io.snapcraft.Settings'> 52 <method name='Check'> 53 <arg type='s' name='setting' direction='in'/> 54 <arg type='s' name='check' direction='in'/> 55 <arg type='s' name='result' direction='out'/> 56 </method> 57 <method name='CheckSub'> 58 <arg type='s' name='setting' direction='in'/> 59 <arg type='s' name='subproperty' direction='in'/> 60 <arg type='s' name='check' direction='in'/> 61 <arg type='s' name='result' direction='out'/> 62 </method> 63 <method name='Get'> 64 <arg type='s' name='setting' direction='in'/> 65 <arg type='s' name='result' direction='out'/> 66 </method> 67 <method name='GetSub'> 68 <arg type='s' name='setting' direction='in'/> 69 <arg type='s' name='subproperty' direction='in'/> 70 <arg type='s' name='result' direction='out'/> 71 </method> 72 <method name='Set'> 73 <arg type='s' name='setting' direction='in'/> 74 <arg type='s' name='value' direction='in'/> 75 </method> 76 <method name='SetSub'> 77 <arg type='s' name='setting' direction='in'/> 78 <arg type='s' name='subproperty' direction='in'/> 79 <arg type='s' name='value' direction='in'/> 80 </method> 81 </interface>` 82 83 var validSettings = []string{ 84 "default-web-browser", 85 "default-url-scheme-handler", 86 } 87 88 func allowedSetting(setting string) bool { 89 if !strings.HasSuffix(setting, ".desktop") { 90 return false 91 } 92 base := strings.TrimSuffix(setting, ".desktop") 93 94 return snap.ValidAppName(base) 95 } 96 97 // settingSpec specifies a setting with an optional subproperty 98 type settingSpec struct { 99 setting string 100 subproperty string 101 } 102 103 func (s *settingSpec) String() string { 104 if s.subproperty != "" { 105 return fmt.Sprintf("%q subproperty %q", s.setting, s.subproperty) 106 } else { 107 return fmt.Sprintf("%q", s.setting) 108 } 109 } 110 111 func (s *settingSpec) validate() *dbus.Error { 112 for _, valid := range validSettings { 113 if s.setting == valid { 114 return nil 115 } 116 } 117 return dbus.MakeFailedError(fmt.Errorf("invalid setting %q", s.setting)) 118 } 119 120 // Settings implements the 'io.snapcraft.Settings' DBus interface. 121 type Settings struct { 122 conn *dbus.Conn 123 } 124 125 // Interface returns the name of the interface this object implements 126 func (s *Settings) Interface() string { 127 return "io.snapcraft.Settings" 128 } 129 130 // ObjectPath returns the path that the object is exported as 131 func (s *Settings) ObjectPath() dbus.ObjectPath { 132 return "/io/snapcraft/Settings" 133 } 134 135 // IntrospectionData gives the XML formatted introspection description 136 // of the DBus service. 137 func (s *Settings) IntrospectionData() string { 138 return settingsIntrospectionXML 139 } 140 141 // some notes: 142 // - we only set/get desktop files 143 // - all desktop files of snaps are prefixed with: ${snap}_ 144 // - on get/check/set we need to add/strip this prefix 145 146 func safeSnapFromSender(s *Settings, sender dbus.Sender) (string, *dbus.Error) { 147 // avoid information leak: see https://github.com/snapcore/snapd/pull/4073#discussion_r146682758 148 snap, err := snapFromSender(s.conn, sender) 149 if err != nil { 150 return "", dbus.MakeFailedError(err) 151 } 152 return snap, nil 153 } 154 155 func desktopFileFromValueForSetting(s *Settings, command string, setspec *settingSpec, dotDesktopValue string, sender dbus.Sender) (string, *dbus.Error) { 156 snap, err := safeSnapFromSender(s, sender) 157 if err != nil { 158 return "", err 159 } 160 if err := setspec.validate(); err != nil { 161 return "", err 162 } 163 164 if !allowedSetting(dotDesktopValue) { 165 return "", dbus.MakeFailedError(fmt.Errorf("cannot %s %s setting to invalid value %q", command, setspec, dotDesktopValue)) 166 } 167 168 // FIXME: this works only for desktop files 169 desktopFile := fmt.Sprintf("%s_%s", snap, dotDesktopValue) 170 return desktopFile, nil 171 } 172 173 func desktopFileFromOutput(s *Settings, output string, sender dbus.Sender) (string, *dbus.Error) { 174 snap, err := safeSnapFromSender(s, sender) 175 if err != nil { 176 return "", err 177 } 178 if !strings.HasPrefix(output, snap+"_") { 179 return "NOT-THIS-SNAP.desktop", nil 180 } 181 182 desktopFile := strings.SplitN(output, "_", 2)[1] 183 return strings.TrimSpace(desktopFile), nil 184 } 185 186 func setDialog(s *Settings, setspec *settingSpec, desktopFile string, sender dbus.Sender) *dbus.Error { 187 df := filepath.Join(dirs.SnapDesktopFilesDir, desktopFile) 188 if !osutil.FileExists(df) { 189 return dbus.MakeFailedError(fmt.Errorf("cannot find desktop file %q", df)) 190 } 191 192 // FIXME: we need to know the parent PID or our dialog may pop under 193 // the existing windows. We might get it with the help of 194 // the xdg-settings tool inside the core snap. It would have 195 // to get the PID of the process asking for the settings 196 // then xdg-settings can sent this to us and we can intospect 197 // the X windows for _NET_WM_PID and use the windowID to 198 // attach to zenity - not sure how this translate to the 199 // wayland world though :/ 200 dialog, uiErr := ui.New() 201 if uiErr != nil { 202 return dbus.MakeFailedError(fmt.Errorf("cannot ask for settings change: %v", uiErr)) 203 } 204 205 snap, err := safeSnapFromSender(s, sender) 206 if err != nil { 207 return err 208 } 209 210 answeredYes := dialog.YesNo( 211 i18n.G("Allow settings change?"), 212 fmt.Sprintf(i18n.G("Allow snap %q to change %s to %q ?"), snap, setspec, desktopFile), 213 &ui.DialogOptions{ 214 Timeout: defaultConfirmDialogTimeout, 215 Footer: i18n.G("This dialog will close automatically after 5 minutes of inactivity."), 216 }, 217 ) 218 if !answeredYes { 219 return dbus.MakeFailedError(fmt.Errorf("cannot change configuration: user declined change")) 220 } 221 return nil 222 } 223 224 func checkOutput(cmd *exec.Cmd, command string, setspec *settingSpec) (string, *dbus.Error) { 225 output, err := cmd.CombinedOutput() 226 if err != nil { 227 return "", dbus.MakeFailedError(fmt.Errorf("cannot %s %s setting: %s", command, setspec, osutil.OutputErr(output, err))) 228 } 229 return string(output), nil 230 } 231 232 // Check implements the 'Check' method of the 'io.snapcraft.Settings' 233 // DBus interface. 234 // 235 // Example usage: dbus-send --session --dest=io.snapcraft.Settings --type=method_call --print-reply /io/snapcraft/Settings io.snapcraft.Settings.Check string:'default-web-browser' string:'firefox.desktop' 236 func (s *Settings) Check(setting string, check string, sender dbus.Sender) (string, *dbus.Error) { 237 if err := checkOnClassic(); err != nil { 238 return "", err 239 } 240 241 settingMain := &settingSpec{setting: setting} 242 desktopFile, err := desktopFileFromValueForSetting(s, "check", settingMain, check, sender) 243 if err != nil { 244 return "", err 245 } 246 247 cmd := exec.Command("xdg-settings", "check", setting, desktopFile) 248 output, err := checkOutput(cmd, "check", settingMain) 249 if err != nil { 250 return "", err 251 } 252 253 return strings.TrimSpace(output), nil 254 } 255 256 // CheckSub implements the 'CheckSub' method of the 'io.snapcraft.Settings' 257 // DBus interface. 258 // 259 // Example usage: dbus-send --session --dest=io.snapcraft.Settings --type=method_call --print-reply /io/snapcraft/Settings io.snapcraft.Settings.CheckSub string:'default-url-scheme-handler' string:'irc' string:'ircclient.desktop' 260 func (s *Settings) CheckSub(setting string, subproperty string, check string, sender dbus.Sender) (string, *dbus.Error) { 261 if err := checkOnClassic(); err != nil { 262 return "", err 263 } 264 265 settingSub := &settingSpec{setting: setting, subproperty: subproperty} 266 desktopFile, err := desktopFileFromValueForSetting(s, "check", settingSub, check, sender) 267 if err != nil { 268 return "", err 269 } 270 271 cmd := exec.Command("xdg-settings", "check", setting, subproperty, desktopFile) 272 output, err := checkOutput(cmd, "check", settingSub) 273 if err != nil { 274 return "", err 275 } 276 277 return strings.TrimSpace(output), nil 278 } 279 280 // Get implements the 'Get' method of the 'io.snapcraft.Settings' 281 // DBus interface. 282 // 283 // Example usage: dbus-send --session --dest=io.snapcraft.Settings --type=method_call --print-reply /io/snapcraft/Settings io.snapcraft.Settings.Get string:'default-web-browser' 284 func (s *Settings) Get(setting string, sender dbus.Sender) (string, *dbus.Error) { 285 if err := checkOnClassic(); err != nil { 286 return "", err 287 } 288 289 settingMain := &settingSpec{setting: setting} 290 if err := settingMain.validate(); err != nil { 291 return "", err 292 } 293 294 cmd := exec.Command("xdg-settings", "get", setting) 295 output, err := checkOutput(cmd, "get", settingMain) 296 if err != nil { 297 return "", err 298 } 299 300 return desktopFileFromOutput(s, output, sender) 301 } 302 303 // GetSub implements the 'GetSub' method of the 'io.snapcraft.Settings' 304 // DBus interface. 305 // 306 // Example usage: dbus-send --session --dest=io.snapcraft.Settings --type=method_call --print-reply /io/snapcraft/Settings io.snapcraft.Settings.GetSub string:'default-url-scheme-handler' string:'irc' 307 func (s *Settings) GetSub(setting string, subproperty string, sender dbus.Sender) (string, *dbus.Error) { 308 if err := checkOnClassic(); err != nil { 309 return "", err 310 } 311 312 settingSub := &settingSpec{setting: setting, subproperty: subproperty} 313 if err := settingSub.validate(); err != nil { 314 return "", err 315 } 316 317 cmd := exec.Command("xdg-settings", "get", setting, subproperty) 318 output, err := checkOutput(cmd, "get", settingSub) 319 if err != nil { 320 return "", err 321 } 322 323 return desktopFileFromOutput(s, output, sender) 324 } 325 326 // Set implements the 'Set' method of the 'io.snapcraft.Settings' 327 // DBus interface. 328 // 329 // Example usage: dbus-send --session --dest=io.snapcraft.Settings --type=method_call --print-reply /io/snapcraft/Settings io.snapcraft.Settings.Set string:'default-web-browser' string:'chromium-browser.desktop' 330 func (s *Settings) Set(setting string, new string, sender dbus.Sender) *dbus.Error { 331 if err := checkOnClassic(); err != nil { 332 return err 333 } 334 335 settingMain := &settingSpec{setting: setting} 336 desktopFile, err := desktopFileFromValueForSetting(s, "set", settingMain, new, sender) 337 if err != nil { 338 return err 339 } 340 341 if err := setDialog(s, settingMain, desktopFile, sender); err != nil { 342 return err 343 } 344 345 cmd := exec.Command("xdg-settings", "set", setting, desktopFile) 346 if _, err := checkOutput(cmd, "set", settingMain); err != nil { 347 return err 348 } 349 350 return nil 351 } 352 353 // SetSub implements the 'SetSub' method of the 'io.snapcraft.Settings' 354 // DBus interface. 355 // 356 // Example usage: dbus-send --session --dest=io.snapcraft.Settings --type=method_call --print-reply /io/snapcraft/Settings io.snapcraft.Settings.SetSub string:'default-url-scheme-handler' string:'irc' string:'ircclient.desktop' 357 func (s *Settings) SetSub(setting string, subproperty string, new string, sender dbus.Sender) *dbus.Error { 358 if err := checkOnClassic(); err != nil { 359 return err 360 } 361 362 settingSub := &settingSpec{setting: setting, subproperty: subproperty} 363 desktopFile, err := desktopFileFromValueForSetting(s, "set", settingSub, new, sender) 364 if err != nil { 365 return err 366 } 367 368 if err := setDialog(s, settingSub, desktopFile, sender); err != nil { 369 return err 370 } 371 372 cmd := exec.Command("xdg-settings", "set", setting, subproperty, desktopFile) 373 if _, err := checkOutput(cmd, "set", settingSub); err != nil { 374 return err 375 } 376 377 return nil 378 }