yast/yast-registration

View on GitHub
src/lib/registration/ui/base_system_registration_dialog.rb

Summary

Maintainability
D
1 day
Test Coverage

require "yast"
require "yast/suse_connect"
require "ui/event_dispatcher"
require "uri"

require "registration/registration"
require "registration/registration_ui"
require "registration/storage"
require "registration/sw_mgmt"
require "registration/helpers"
require "registration/url_helpers"
require "registration/ui/abort_confirmation"
require "y2packager/medium_type"
require "yast2/popup"

module Registration
  module UI
    # this class displays and runs the dialog for registering the base system
    class BaseSystemRegistrationDialog
      include Yast::Logger
      include Yast::I18n
      include Yast::UIShortcuts
      include Yast
      include ::UI::EventDispatcher

      # @return [Symbol] Current action (:register_scc, :register_local or :skip_registration)
      attr_reader :action

      Yast.import "Mode"
      Yast.import "GetInstArgs"
      Yast.import "UI"
      Yast.import "Wizard"
      Yast.import "Popup"
      Yast.import "Report"
      Yast.import "ProductFeatures"
      Yast.import "Stage"
      Yast.import "OSRelease"

      WIDGETS = {
        register_scc:      [:email, :reg_code],
        register_local:    [:custom_url],
        skip_registration: []
      }.freeze

      private_constant :WIDGETS

      # create and run the dialog for registering the base system
      # @return [Symbol] the user input
      def self.run
        dialog = BaseSystemRegistrationDialog.new
        dialog.run
      end

      # the constructor
      def initialize
        textdomain "registration"
      end

      # display the extension selection dialog and wait for a button click
      # @return [Symbol] user input (:import, :cancel)
      def run
        log.info "Displaying registration dialog"

        Yast::Wizard.SetContents(
          # dialog title
          _("Registration"),
          content,
          help_text,
          Yast::GetInstArgs.enable_back || (Yast::Mode.normal && Registration.is_registered?),
          Yast::GetInstArgs.enable_next || Yast::Mode.normal
        )

        # Limit the reg_code InputField to 512 (bsc#1098576)
        Yast::UI.ChangeWidget(:reg_code, :InputMaxLength, 512)

        # Set default action
        self.action = initial_action
        set_focus

        event_loop
      ensure
        Yast::Wizard.ClearContents
      end

      # Set the initial action
      #
      # * If the system is registered:
      #   * using the default url -> :register_scc
      #   * using a custom URL -> :register_local
      # * If the system is not registered -> :register_scc
      #
      # @return [Symbol] Selected action
      def initial_action
        using_default_url? ? :register_scc : :register_local
      end

      # Handle pushing the 'Next' button depending on the action
      #
      # When action is:
      #
      # * :skip_registration: returns :skip to skip the registration process.
      # * :register_scc o :register_local: calls #handle_registration
      #
      # @return [Symbol,nil]
      #
      # @see #handle_registration
      def next_handler
        result =
          case action
          when :skip_registration
            handle_skipping_registration
          when :register_scc, :register_local
            handle_registration
          end
        finish_dialog(result) unless result.nil?
        result
      end

      # Handle pushing the 'Abort' button
      #
      # * In normal mode returns :abort
      # * In installation mode, ask for confirmation. If user
      #   confirms, returns :abort; nil otherwise.
      #
      # @return [Symbol,nil] :abort or nil
      # @see finish_di
      def abort_handler
        result = (Yast::Mode.normal || AbortConfirmation.run) ? :abort : nil
        finish_dialog(result) unless result.nil?
        result
      end

      # Handle selecting the 'Skip' option
      #
      # Set the dialog's action to :skip_registration
      def skip_registration_handler
        self.action = :skip_registration
        show_skipping_warning
      end

      # Handle selection the 'Register System via scc.suse.com' option
      #
      # Set the dialog's action to :register_scc
      def register_scc_handler
        self.action = :register_scc
      end

      # Handle selection the 'Register System via local RMT Server' option
      #
      # Set the dialog's action to :register_local
      def register_local_handler
        self.action = :register_local
      end

      # Handle pushing 'Network Configuration' button
      #
      # Runs the network configuration
      def network_handler
        Helpers.run_network_configuration
      end

      # Handle pushing the 'Back' button
      def back_handler
        finish_dialog(:back)
      end

      attr_accessor :registration

    private

      # width of reg code input field widget
      REG_CODE_WIDTH = 33

      # content for the main registration dialog
      # @return [Yast::Term]  UI term
      def content
        VBox(
          network_button,
          VStretch(),
          product_details_widgets,
          VSpacing(Yast::UI.TextMode ? 1 : 2),
          registration_widgets,
          VStretch()
        )
      end

      def registration_widgets
        HSquash(
          VBox(
            RadioButtonGroup(
              Id(:action),
              VBox(
                register_scc_option,
                register_local_option,
                skip_option
              )
            )
          )
        )
      end

      # Return registration options
      #
      # Read registration options from Storage::InstallationOptions
      # and, if needed, from Storage::RegCodes.
      #
      # @return [Hash] Hash containing values for :reg_code,
      #               :email and :custom_url.
      def reg_options
        return @reg_options unless @reg_options.nil?
        options = Storage::InstallationOptions.instance

        reg_code = options.reg_code
        if reg_code.empty?
          known_reg_codes = Storage::RegCodes.instance.reg_codes
          base_product_name = SwMgmt.find_base_product["name"]
          reg_code = known_reg_codes[base_product_name] || ""
        end

        @reg_options = {
          reg_code:   reg_code,
          email:      options.email,
          custom_url: options.custom_url || default_url
        }
      end

      # Default registration server
      #
      # The boot_url takes precedence over the SUSE::Connect default
      # one. It shows a message if the boot_url is not valid.
      #
      # @return [String] URL for the registration server
      def default_url
        return @default_url if @default_url

        if boot_url
          return (@default_url = boot_url) if valid_custom_url?(boot_url)

          Yast::Report.Error(
            # TRANSLATORS: Wrong url for registration provided, %s is an URL.
            _("The registration URL provided by the command line is not valid.\n\n" \
              "URL: %s\n\nThe default one will be used instead.") % boot_url
          )
        end

        @default_url = SUSE::Connect::Config.new.url
      end

      # Registration server URL given through Linuxrc
      #
      # @return [String,nil] URL for the registration server; nil if not given.
      def boot_url
        @boot_url ||= UrlHelpers.boot_reg_url
      end

      # Widgets for :register_scc action
      #
      # @return [Yast::Term] UI terms
      def register_scc_option
        VBox(
          Left(
            RadioButton(
              Id(:register_scc),
              Opt(:notify),
              # TRANSLATORS: radio button; %s is a host name.
              format(_("Register System via %s"), (URI(default_url).host || "").downcase),
              action == :register_scc
            )
          ),
          VSpacing(0.3),
          Left(
            HBox(
              HSpacing(5),
              VBox(
                MinWidth(REG_CODE_WIDTH, InputField(Id(:email), _("&E-mail Address"),
                  reg_options[:email])),
                VSpacing(Yast::UI.TextMode ? 0 : 0.5),
                MinWidth(REG_CODE_WIDTH, InputField(Id(:reg_code), _("Registration &Code"),
                  reg_options[:reg_code]))
              )
            )
          ),
          VSpacing(1)
        )
      end

      # Example URL to be used in the :register_local UI
      EXAMPLE_RMT_URL = "https://rmt.example.com".freeze

      # Widgets for :register_local action
      #
      # @return [Yast::Term] UI terms
      def register_local_option
        VBox(
          Left(
            RadioButton(
              Id(:register_local),
              Opt(:notify),
              # TRANSLATORS: radio button
              _("Register System via local RMT Server"),
              action == :register_local
            )
          ),
          VSpacing(0.3),
          Left(
            HBox(
              HSpacing(5),
              VBox(
                MinWidth(REG_CODE_WIDTH,
                  ComboBox(Id(:custom_url), Opt(:editable),
                    _("&Local Registration Server URL"), local_registration_urls))
              )
            )
          ),
          VSpacing(1)
        )
      end

      # widget for skipping the registration
      # @return [Yast::Term]  UI term
      def skip_option
        return Empty() if hide_skip_option?
        Left(
          RadioButton(
            Id(:skip_registration),
            Opt(:notify),
            _("&Skip Registration"),
            action == :skip_registration
          )
        )
      end

      # Whether skip option should be hidden
      #
      # Do not display it in an installed system or when already registered or when registration
      # is mandatory.
      #
      # @return [Boolean]
      def hide_skip_option?
        Stage.normal ||
          Registration.is_registered? ||
          Storage::InstallationOptions.instance.force_registration
      end

      # part of the main dialog definition - the base product details
      # @return [Yast::Term]  UI term
      def product_details_widgets
        label = if Registration.is_registered?
          Heading(_("The system is already registered."))
        else
          Label(_("Please select your preferred method of registration."))
        end

        HSquash(
          VBox(
            VSpacing(1),
            Left(Heading(SwMgmt.product_label(SwMgmt.find_base_product))),
            VSpacing(1),
            label
          )
        )
      end

      # help text for the main registration dialog
      def help_text
        # help text
        _("Enter SUSE Customer Center credentials here to register the system to " \
            "get updates and extensions.")
      end

      #
      # Read the full media name from the product control file
      # Substituting $os_release_version pattern with the release
      # of the current system.
      #
      # @return [String] the name or empty string if not set
      #
      def media_name
        name = ProductFeatures.GetStringFeature(
          "globals",
          "full_system_media_name"
        )
        name.gsub(/\$os_release_version\b/,
          Yast::OSRelease.ReleaseVersionHumanReadable)
      end

      #
      # Read the full media download URL from the product control file
      #
      # @return [String] the URL or empty string if not set
      #
      def download_url
        ProductFeatures.GetStringFeature(
          "globals",
          "full_system_download_url"
        )
      end

      # Show a warning about skipping the registration
      #
      # @return [Boolean] true when skipping has been confirmed
      def show_skipping_warning
        Yast2::Popup.show(medium_warning_text, richtext: true, headline: medium_warning_headline)
      end

      # Convenience method to obtain the medium warning text depending on the
      # medium type
      def medium_warning_text
        if Yast::Stage.initial && Y2Packager::MediumType.online?
          online_skipping_text
        else
          default_skipping_text
        end
      end

      def medium_warning_headline
        if Yast::Stage.initial && Y2Packager::MediumType.online?
          online_skipping_headline
        else
          default_skipping_headline
        end
      end

      def online_skipping_headline
        # TRANSLATORS: popup header
        _("Registration Cannot Be Skipped")
      end

      def default_skipping_headline
        # TRANSLATORS: popup header
        _("Skipping Registration")
      end

      def default_skipping_text
        # TRANSLATORS:
        # Popup question: confirm skipping the registration on the Full medium
        _("<p>System registration is required for updates and security fixes.\n"\
          "Please confirm to proceed without updates.\n"\
          "You may register at a later date for updates and security fixes.</p>")
      end

      # the warning for the Online media
      # @return [String]
      def online_skipping_text
        # TRANSLATORS:
        # Popup (1/3) : Installation cannot be continued without registration.
        warning = _("<p>This installation is online only which requires registration for "\
                    "package repositories.</p>")

        # these cannot be nil
        if !media_name.empty? && !download_url.empty?
          # TRANSLATORS: a popup message (2/3) the user wants to skip the registration
          # %{media_name} is the media name (e.g. SLE-15-SP2-Full),
          # %{download_url} is an URL link (e.g. https://download.suse.com)
          warning += _("<p>For installation without registering the system please "\
              "install using the %{media_name} installation media from %{download_url}.</p>") %
            { media_name: media_name, download_url: download_url } # these cannot be nil

        else
          # TRANSLATORS: a popup message (3/3) the user wants to skip the registration
          warning += _("<p>For installations without registration please "\
            "install using full installation media.</p>")
        end
        warning
      end

      # UI term for the network configuration button (or empty if not needed)
      # @return [Yast::Term] UI term
      def network_button
        return Empty() unless Helpers.network_configurable && Stage.initial

        Right(PushButton(Id(:network), _("Net&work Configuration...")))
      end

      # Will show the skipping registration in case of the online medium
      # returning nil or will return :skip otherwise
      #
      # @return [Symbol, nil] :skip if not the online medium
      def handle_skipping_registration
        if !Yast::Stage.initial || !Y2Packager::MediumType.online?
          log.info "Skipping registration on user request"
          return :skip
        end

        show_skipping_warning

        nil
      end

      # run the registration
      # @return [Symbol] symbol for the next workflow step (depending on the registration result)
      def handle_registration
        # do not re-register during installation
        if !Yast::Mode.normal && Registration.is_registered? &&
            Storage::InstallationOptions.instance.base_registered

          return :next
        end

        return nil unless valid_input?

        set_registration_options
        return nil if init_registration == :cancel

        if register_system_and_base_product
          store_registration_status
          return :next
        else
          reset_registration
          return nil
        end
      end

      # run the system and the base product registration
      # @return [Boolean] true on success
      def register_system_and_base_product
        registration_ui = RegistrationUI.new(registration)

        # ensure the GPG keys from inst-sys are imported to the package manager,
        # on the online installation medium the package manager is initialized later
        Yast::Packages.ImportGPGKeys if Yast::Stage.initial && Y2Packager::MediumType.online?

        success, product_service = registration_ui.register_system_and_base_product

        if product_service && !registration_ui.install_updates?
          registration_ui.disable_update_repos(product_service)
        end

        success
      end

      # Set registration options according to current action
      #
      # When current action is:
      #
      # * :register_scc -> set email and registration code
      # * :register_local -> set custom url
      def set_registration_options
        options = Storage::InstallationOptions.instance
        case action
        when :register_scc
          options.email      = Yast::UI.QueryWidget(:email, :Value)
          options.reg_code   = Yast::UI.QueryWidget(:reg_code, :Value)
          # Avoid that boot_reg_url has precedence (see UrlHelpers.reg_url_at_installation)
          options.custom_url = default_url
        when :register_local
          options.email      = "" # Use an empty string like InstallationOptions constructor
          options.reg_code   = ""
          options.custom_url = Yast::UI.QueryWidget(:custom_url, :Value)
        else
          raise "Unknown action: #{action}"
        end
      end

      # store the successful registration
      def store_registration_status
        Storage::InstallationOptions.instance.base_registered = true
        # save the config if running in installed system
        # (in installation/upgrade it's written in _finish client)
        Helpers.write_config if Yast::Mode.normal
      end

      # reset the registration status when registration fails
      def reset_registration
        log.info "registration failed, resetting the registration URL"
        # reset the registration object and the cache to allow changing the URL
        self.registration = nil
        UrlHelpers.reset_registration_url
        Helpers.reset_registration_status
      end

      # initialize the Registration object
      # @return [Symbol, nil] returns :cancel if the URL selection was canceled
      def init_registration
        return if registration

        url = UrlHelpers.registration_url
        return :cancel if url == :cancel
        log.info "Initializing registration with URL: #{url.inspect}"
        self.registration = Registration.new(url)
      end

      # Set dialog action
      #
      # @return [Symbol] Action (:register_scc, :register_local or :skip_registration)
      def action=(value)
        @action = value
        refresh
      end

      # Refresh widgets status depending on the current action
      #
      # @see #action
      def refresh
        Yast::UI.ChangeWidget(Id(:action), :Value, action)
        refresh_next

        # Disable the input fields if system cannot be (re)registered
        return disable_widgets unless Registration.allowed?

        WIDGETS.values.flatten.each do |wgt|
          Yast::UI.ChangeWidget(Id(wgt), :Enabled, WIDGETS[action].include?(wgt))
        end
      end

      # Disable all input widgets
      def disable_widgets
        Yast::UI.ChangeWidget(Id(:action), :Enabled, false)
      end

      # In an online medium it disables the next button when skipping the
      # registration and enable it in any other selected action
      def refresh_next
        return unless Stage.initial && Y2Packager::MediumType.online?

        disable_next(action == :skip_registration)
      end

      # Convenience method for disabling / enabling the wizard next button
      # @param status [Boolean] true for disabling, false for enabling
      def disable_next(status)
        status ? Yast::Wizard.DisableNextButton : Yast::Wizard.EnableNextButton
      end

      # Set focus
      #
      # The focus is set to the first widget of the current action.
      #
      # @see #action
      def set_focus
        widget = WIDGETS[action].first
        Yast::UI.SetFocus(Id(widget)) if widget
      end

      # Determine whether we're using the default URL for SCC
      #
      # @return [Boolean] True if the default URL is used; false otherwise.
      def using_default_url?
        reg_options[:custom_url] == default_url
      end

      #
      # Scan the network for SLP registration servers
      #
      # @return [Array<String>] The list of found URLs
      #
      def slp_urls
        # do not scan again if the system has been registered during installation
        # (the user is going back)
        return [] if Registration.is_registered? && !Yast::Mode.normal
        services = UrlHelpers.slp_discovery_feedback
        services.map { |svc| UrlHelpers.service_url(svc.slp_url) }
      end

      # This method check whether the input is valid
      #
      # It relies on methods "validate_#!{action}".
      # For example, {#validate_register_local}.
      # It's intended to be used when the user clicks "Next" button.
      #
      # @return [Boolean] True if input is valid; false otherwise.
      def valid_input?
        validation_method = "validate_#{action}"
        if respond_to?(validation_method, true)
          send(validation_method)
        else
          true
        end
      end

      # Validate input for :register_local action
      #
      # Currently it makes sure that URL is valid. It shows
      # a message if validation fails.
      #
      # @return [Boolean] true if it's valid; false otherwise.
      def validate_register_local
        if valid_custom_url?(Yast::UI.QueryWidget(:custom_url, :Value))
          true
        else
          # error message, the entered URL is not valid.
          Yast::Report.Error(_("Invalid URL."))
          false
        end
      end

      def validate_register_scc
        reg_code = Yast::UI.QueryWidget(:reg_code, :Value)

        # no CR or LF control characters, they cannot be used in HTTP header fields
        if reg_code.include?("\n") || reg_code.include?("\r")
          # TRANSLATORS: error message, the entered registration code is not valid.
          Yast::Report.Error(_("Invalid registration code.\nCRLF characters are not allowed."))
          false
        else
          true
        end
      end

      VALID_CUSTOM_URL_SCHEMES = ["http", "https"].freeze

      # Determine whether an URL is valid and suitable to be used as local RMT server
      #
      # @return [Boolean] true if it's valid; false otherwise.
      def valid_custom_url?(custom_url)
        uri = URI(custom_url)
        VALID_CUSTOM_URL_SCHEMES.include?(uri.scheme)
      rescue URI::InvalidURIError
        false
      end

      #
      # List of offered local registration servers
      #
      # @return [Array<String>] List of URLs, contains an example URL
      #  if no local registration server was found
      #
      def local_registration_urls
        # If no special URL is used, probe with SLP
        return [reg_options[:custom_url]] unless using_default_url?

        # use an example URL if no server was found via SLP
        urls = slp_urls
        urls.empty? ? [EXAMPLE_RMT_URL] : urls
      end
    end
  end
end