github.com/rigado/snapd@v2.42.5-go-mod+incompatible/usersession/userd/settings.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017 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  	"github.com/snapcore/snapd/dirs"
    31  	"github.com/snapcore/snapd/i18n"
    32  	"github.com/snapcore/snapd/osutil"
    33  	"github.com/snapcore/snapd/snap"
    34  	"github.com/snapcore/snapd/usersession/userd/ui"
    35  )
    36  
    37  // Timeout when the confirmation dialog for an xdg-setging
    38  // automatically closes. Keep in sync with the core snaps
    39  // xdg-settings wrapper which also sets this value to 300.
    40  var defaultConfirmDialogTimeout = 300 * time.Second
    41  
    42  const settingsIntrospectionXML = `
    43  <interface name="org.freedesktop.DBus.Peer">
    44  	<method name='Ping'>
    45  	</method>
    46  	<method name='GetMachineId'>
    47                 <arg type='s' name='machine_uuid' direction='out'/>
    48  	</method>
    49  </interface>
    50  <interface name='io.snapcraft.Settings'>
    51  	<method name='Check'>
    52  		<arg type='s' name='setting' direction='in'/>
    53  		<arg type='s' name='check' direction='in'/>
    54  		<arg type='s' name='result' direction='out'/>
    55  	</method>
    56  	<method name='Get'>
    57  		<arg type='s' name='setting' direction='in'/>
    58  		<arg type='s' name='result' direction='out'/>
    59  	</method>
    60  	<method name='Set'>
    61  		<arg type='s' name='setting' direction='in'/>
    62  		<arg type='s' name='value' direction='in'/>
    63  	</method>
    64  </interface>`
    65  
    66  // TODO: allow setting default-url-scheme-handler ?
    67  var settingsWhitelist = []string{
    68  	"default-web-browser",
    69  }
    70  
    71  func allowedSetting(setting string) bool {
    72  	if !strings.HasSuffix(setting, ".desktop") {
    73  		return false
    74  	}
    75  	base := strings.TrimSuffix(setting, ".desktop")
    76  
    77  	return snap.ValidAppName(base)
    78  }
    79  
    80  func settingWhitelisted(setting string) *dbus.Error {
    81  	for _, whitelisted := range settingsWhitelist {
    82  		if setting == whitelisted {
    83  			return nil
    84  		}
    85  	}
    86  	return dbus.MakeFailedError(fmt.Errorf("cannot use setting %q: not allowed", setting))
    87  }
    88  
    89  // Settings implements the 'io.snapcraft.Settings' DBus interface.
    90  type Settings struct {
    91  	conn *dbus.Conn
    92  }
    93  
    94  // Name returns the name of the interface this object implements
    95  func (s *Settings) Name() string {
    96  	return "io.snapcraft.Settings"
    97  }
    98  
    99  // BasePath returns the base path of the object
   100  func (s *Settings) BasePath() dbus.ObjectPath {
   101  	return "/io/snapcraft/Settings"
   102  }
   103  
   104  // IntrospectionData gives the XML formatted introspection description
   105  // of the DBus service.
   106  func (s *Settings) IntrospectionData() string {
   107  	return settingsIntrospectionXML
   108  }
   109  
   110  // some notes:
   111  // - we only set/get desktop files
   112  // - all desktop files of snaps are prefixed with: ${snap}_
   113  // - on get/check/set we need to add/strip this prefix
   114  
   115  // Check implements the 'Check' method of the 'io.snapcraft.Settings'
   116  // DBus interface.
   117  //
   118  // 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'
   119  func (s *Settings) Check(setting, check string, sender dbus.Sender) (string, *dbus.Error) {
   120  	// avoid information leak: see https://github.com/snapcore/snapd/pull/4073#discussion_r146682758
   121  	snap, err := snapFromSender(s.conn, sender)
   122  	if err != nil {
   123  		return "", dbus.MakeFailedError(err)
   124  	}
   125  	if err := settingWhitelisted(setting); err != nil {
   126  		return "", err
   127  	}
   128  	if !allowedSetting(check) {
   129  		return "", dbus.MakeFailedError(fmt.Errorf("cannot check setting %q to value %q: value not allowed", setting, check))
   130  	}
   131  
   132  	// FIXME: this works only for desktop files
   133  	desktopFile := fmt.Sprintf("%s_%s", snap, check)
   134  
   135  	cmd := exec.Command("xdg-settings", "check", setting, desktopFile)
   136  	output, err := cmd.CombinedOutput()
   137  	if err != nil {
   138  		return "", dbus.MakeFailedError(fmt.Errorf("cannot check setting %s: %s", setting, osutil.OutputErr(output, err)))
   139  	}
   140  
   141  	return strings.TrimSpace(string(output)), nil
   142  }
   143  
   144  // Get implements the 'Get' method of the 'io.snapcraft.Settings'
   145  // DBus interface.
   146  //
   147  // 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'
   148  func (s *Settings) Get(setting string, sender dbus.Sender) (string, *dbus.Error) {
   149  	if err := settingWhitelisted(setting); err != nil {
   150  		return "", err
   151  	}
   152  
   153  	cmd := exec.Command("xdg-settings", "get", setting)
   154  	output, err := cmd.CombinedOutput()
   155  	if err != nil {
   156  		return "", dbus.MakeFailedError(fmt.Errorf("cannot get setting %s: %s", setting, osutil.OutputErr(output, err)))
   157  	}
   158  
   159  	// avoid information leak: see https://github.com/snapcore/snapd/pull/4073#discussion_r146682758
   160  	snap, err := snapFromSender(s.conn, sender)
   161  	if err != nil {
   162  		return "", dbus.MakeFailedError(err)
   163  	}
   164  	if !strings.HasPrefix(string(output), snap+"_") {
   165  		return "NOT-THIS-SNAP.desktop", nil
   166  	}
   167  
   168  	desktopFile := strings.SplitN(string(output), "_", 2)[1]
   169  	return strings.TrimSpace(desktopFile), nil
   170  }
   171  
   172  // Set implements the 'Set' method of the 'io.snapcraft.Settings'
   173  // DBus interface.
   174  //
   175  // 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'
   176  func (s *Settings) Set(setting, new string, sender dbus.Sender) *dbus.Error {
   177  	if err := settingWhitelisted(setting); err != nil {
   178  		return err
   179  	}
   180  	// see https://github.com/snapcore/snapd/pull/4073#discussion_r146682758
   181  	snap, err := snapFromSender(s.conn, sender)
   182  	if err != nil {
   183  		return dbus.MakeFailedError(err)
   184  	}
   185  
   186  	if !allowedSetting(new) {
   187  		return dbus.MakeFailedError(fmt.Errorf("cannot set setting %q to value %q: value not allowed", setting, new))
   188  	}
   189  	new = fmt.Sprintf("%s_%s", snap, new)
   190  	df := filepath.Join(dirs.SnapDesktopFilesDir, new)
   191  	if !osutil.FileExists(df) {
   192  		return dbus.MakeFailedError(fmt.Errorf("cannot find desktop file %q", df))
   193  	}
   194  
   195  	// FIXME: we need to know the parent PID or our dialog may pop under
   196  	//        the existing windows. We might get it with the help of
   197  	//        the xdg-settings tool inside the core snap. It would have
   198  	//        to get the PID of the process asking for the settings
   199  	//        then xdg-settings can sent this to us and we can intospect
   200  	//        the X windows for _NET_WM_PID and use the windowID to
   201  	//        attach to zenity - not sure how this translate to the
   202  	//        wayland world though :/
   203  	dialog, err := ui.New()
   204  	if err != nil {
   205  		return dbus.MakeFailedError(fmt.Errorf("cannot ask for settings change: %v", err))
   206  	}
   207  	answeredYes := dialog.YesNo(
   208  		i18n.G("Allow settings change?"),
   209  		fmt.Sprintf(i18n.G("Allow snap %q to change %q to %q ?"), snap, setting, new),
   210  		&ui.DialogOptions{
   211  			Timeout: defaultConfirmDialogTimeout,
   212  			Footer:  i18n.G("This dialog will close automatically after 5 minutes of inactivity."),
   213  		},
   214  	)
   215  	if !answeredYes {
   216  		return dbus.MakeFailedError(fmt.Errorf("cannot change configuration: user declined change"))
   217  	}
   218  
   219  	cmd := exec.Command("xdg-settings", "set", setting, new)
   220  	output, err := cmd.CombinedOutput()
   221  	if err != nil {
   222  		return dbus.MakeFailedError(fmt.Errorf("cannot set setting %s: %s", setting, osutil.OutputErr(output, err)))
   223  	}
   224  
   225  	return nil
   226  }