• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

yast / yast-packager / 15441808734

04 Jun 2025 12:05PM UTC coverage: 37.221% (-0.2%) from 37.407%
15441808734

push

github

web-flow
Merge pull request #659 from yast/huha-no-baseurl-crash

Prevent a crash if a repo doesn't have a baseurl

1 of 1 new or added line in 1 file covered. (100.0%)

123 existing lines in 11 files now uncovered.

4366 of 11730 relevant lines covered (37.22%)

23.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

50.66
/src/modules/ProductLicense.rb
1
require "yast"
1✔
2
require "uri"
1✔
3
require "fileutils"
1✔
4
require "shellwords"
1✔
5

6
require "y2packager/product"
1✔
7
require "y2packager/product_license"
1✔
8
require "y2packager/resolvable"
1✔
9

10
# Yast namespace
11
module Yast
1✔
12
  # Provide access / dialog for product license
13
  class ProductLicenseClass < Module
1✔
14
    attr_accessor :license_patterns, :license_file_print
1✔
15

16
    include Yast::Logger
1✔
17

18
    DOWNLOAD_URL_SCHEMA = ["http", "https", "ftp"].freeze
1✔
19
    README_BETA = "/README.BETA".freeze
1✔
20

21
    def main
1✔
22
      Yast.import "Pkg"
1✔
23
      Yast.import "UI"
1✔
24

25
      Yast.import "Directory"
1✔
26
      Yast.import "InstShowInfo"
1✔
27
      Yast.import "Language"
1✔
28
      Yast.import "Popup"
1✔
29
      Yast.import "Report"
1✔
30
      Yast.import "Stage"
1✔
31
      Yast.import "Wizard"
1✔
32
      Yast.import "Mode"
1✔
33
      Yast.import "FileUtils"
1✔
34
      Yast.import "ProductFeatures"
1✔
35
      Yast.import "String"
1✔
36
      Yast.import "WorkflowManager"
1✔
37
      Yast.import "Progress"
1✔
38

39
      # IMPORTANT: maintainer of yast2-installation is responsible for this module
40

41
      textdomain "packager"
1✔
42

43
      @license_patterns = [
44
        "license\\.html",
1✔
45
        "license\\.%1\\.html",
46
        "license\\.htm",
47
        "license\\.%1\\.htm",
48
        "license\\.txt",
49
        "license\\.%1\\.txt"
50
      ]
51
      # no more wildcard patterns here, UI can display only html and txt anyway
52

53
      initialize_default_values
1✔
54
    end
55

56
    # (Re)Initializes all internal caches
57
    def initialize_default_values
1✔
58
      # All licenses have their own unique ID
59
      @license_ids = []
10✔
60

61
      # License files by their eula_ID
62
      #
63
      # **Structure:**
64
      #
65
      #     $["ID":$[licenses]]
66
      @all_licenses = {}
10✔
67

68
      # filename printed in the license dialog
69
      @license_file_print = nil
10✔
70
      # license file is on installed system
71
      @license_on_installed_system = false
10✔
72

73
      # BNC #448598
74
      # no-acceptance-needed file in license.tar.gz means the license
75
      # doesn't have to be accepted by user, just displayed
76
      @license_acceptance_needed = {}
10✔
77

78
      @tmpdir = nil
10✔
79
      @license_dir = nil
10✔
80
      @beta_file = nil
10✔
81

82
      @lic_lang = ""
10✔
83

84
      # FIXME: map <string, boolean> ...
85
      @beta_file_already_seen = {}
10✔
86
    end
87

88
    # Returns whether accepting the license manually is required.
89
    #
90
    # @see BNC #448598
91
    # @param [Any] id unique ID
92
    # @return [Boolean] if required
93
    def AcceptanceNeeded(id)
1✔
94
      # FIXME: lazy loading of the info about licenses, bigger refactoring needed
95
      # @see bsc#993285
96
      #
97
      # In the initial installation, for base product, acceptance_needed needs
98
      # to be known before showing the license dialog (inst_complex_welcome).
99
      # Loading the info is handled internally in other cases.
100
      #
101
      # id can be a string (currently is) when called from inst_complex_welcome
102
      if !@license_acceptance_needed.key?(id) &&
11✔
103
          Stage.initial &&
104
          id.to_s == base_product_id.to_s
105
        # Although we know the base product ID, the function below expects
106
        # id to be nil for base product in inital installation
107
        GetSourceLicenseDirectory(nil, "/")
3✔
108
        cache_license_acceptance_needed(id, @license_dir)
3✔
109
      end
110

111
      if @license_acceptance_needed.key?(id)
11✔
112
        @license_acceptance_needed[id]
6✔
113
      else
114
        log.warn "SetAcceptanceNeeded(#{id}) should be called first, using default 'true'"
5✔
115
        true
5✔
116
      end
117
    end
118

119
    # Sets whether explicit acceptance of a license is needed
120
    #
121
    # @param [Any] id unique ID (often a source ID)
122
    # @param [Boolean] new_value if needed
123
    def SetAcceptanceNeeded(id, new_value)
1✔
124
      if new_value.nil?
6✔
125
        Builtins.y2error(
×
126
          "Undefined behavior (License ID %1), AcceptanceNeeded: %2",
127
          id,
128
          new_value
129
        )
130
        return
×
131
      end
132

133
      @license_acceptance_needed[id] = new_value
6✔
134

135
      if new_value == true
6✔
136
        log.info "License agreement (ID #{id}) WILL be required"
2✔
137
      else
138
        log.info "License agreement (ID #{id}) will NOT be required"
4✔
139
      end
140

141
      nil
6✔
142
    end
143

144
    # Generic cleanup
145
    def CleanUp
1✔
146
      # BNC #581933: All license IDs are cached while the module is in memory.
147
      # Removing them when leaving the license dialog.
148
      @license_ids = []
×
149

UNCOV
150
      nil
×
151
    end
152

153
    # Ask user to confirm license agreement
154
    # @param [Fixnum,nil] src_id integer repository to get the license from.
155
    #   If set to 'nil', the license is considered to belong to a base product
156
    # @param [String] dir string directory to look for the license in if src_id is nil
157
    #   and not 1st stage installation
158
    # @param [Array<String>] _patterns a list of patterns for the files, regular expressions
159
    #   with %1 for the language
160
    # @param [Boolean] enable_back sets the back_button status
161
    # @param [Boolean] base_product defines whether it is a base or add-on product
162
    #   true means base product, false add-on product
163
    # @param [Boolean] require_agreement means that even if the license (or the very same license)
164
    #   has been already accepetd, ask user to accept it again (because of 'going back'
165
    #   in the installation proposal).
166
    # @param [String] id usually source id but it can be any unique id in UI
167
    def AskLicenseAgreement(src_id, dir, _patterns, action,
1✔
168
      enable_back, base_product, require_agreement, id)
169
      @lic_lang = ""
3✔
170
      licenses = {}
3✔
171
      available_langs = []
3✔
172
      license_ident = ""
3✔
173

174
      init_ret = (
175
        licenses_ref = arg_ref(licenses)
3✔
176
        available_langs_ref = arg_ref(available_langs)
3✔
177
        license_ident_ref = arg_ref(license_ident)
3✔
178
        result = InitLicenseData(
3✔
179
          src_id,
180
          dir,
181
          licenses_ref,
182
          available_langs_ref,
183
          require_agreement,
184
          license_ident_ref,
185
          id
186
        )
187
        licenses = licenses_ref.value
3✔
188
        available_langs = available_langs_ref.value
3✔
189
        license_ident = license_ident_ref.value
3✔
190
        result
3✔
191
      )
192

193
      if [:auto, :accepted].include?(init_ret)
3✔
194
        Builtins.y2milestone("Returning %1", init_ret)
3✔
195
        return init_ret
3✔
196
      end
197

198
      product = repository_product(src_id)
×
199
      return :accepted if product && license_accepted_for?(product, licenses)
×
200

201
      created_new_dialog = false
×
202

203
      # #459391
204
      # If a progress is running open another dialog
205
      if Progress.IsRunning
×
206
        Builtins.y2milestone(
×
207
          "Some progress is running, opening new dialog for license..."
208
        )
209
        Wizard.OpenLeftTitleNextBackDialog
×
210
        created_new_dialog = true
×
211
      end
212

213
      licenses_ref = arg_ref(licenses)
×
214

215
      title = _("License Agreement")
×
216

217
      if src_id
×
218
        repo_data = Pkg::SourceGeneralData(src_id)
×
219

220
        if repo_data
×
221
          label = repo_data["name"]
×
222
          # TRANSLATORS: %s is an extension name
223
          # e.g. "SUSE Linux Enterprise Software Development Kit"
224
          title = _("%s License Agreement") % label unless label.empty?
×
225
        end
226
      end
227

228
      DisplayLicenseDialogWithTitle(
×
229
        available_langs, # license id
230
        enable_back,
231
        @lic_lang,
232
        licenses_ref,
233
        id,
234
        title
235
      )
236
      licenses = licenses_ref.value
×
237

238
      update_license_archive_location(src_id) if src_id
×
239

240
      # Display beta file as a popup if exists
241
      InstShowInfo.show_info_txt(@beta_file) if !@beta_file.nil?
×
242

243
      # initial loop
244
      licenses_ref = arg_ref(licenses)
×
245
      ret = HandleLicenseDialogRet(
×
246
        licenses_ref,
247
        base_product,
248
        action
249
      )
250

251
      if ret == :accepted
×
252
        # store already accepted license ID
253
        LicenseHasBeenAccepted(license_ident)
×
254
        product.license.accept! if product&.license
×
255
      end
256

257
      CleanUpLicense(@tmpdir)
×
258

259
      # bugzilla #303922
260
      Wizard.CloseDialog if created_new_dialog || (!Stage.initial && !src_id.nil?)
×
261

262
      CleanUp()
×
263

264
      ret
×
265
    end
266

267
    # Ask user to confirm license agreement
268
    # @param [Array<String>] dirs - directories to look for the licenses
269
    # @param [Array<String>] patterns a list of patterns for the files, regular expressions
270
    #   with %1 for the language
271
    # @param [String] action what to do if the license is declined,
272
    #   can be "continue", "abort" or "halt"
273
    # @param [Boolean] enable_back sets the back_button status
274
    # @param [Boolean] base_product defines whether it is a base or add-on product
275
    #   true means base product, false add-on product
276
    # @param [Boolean] require_agreement means that even if the license (or the very same license)
277
    #   has been already accepetd, ask user to accept it again (because of 'going back'
278
    #   in the installation proposal).
279
    def AskLicensesAgreement(dirs, patterns, action, enable_back, base_product, require_agreement)
1✔
280
      # dialog caption
281
      caption = _("License Agreement")
×
282
      heading = nil
×
283

284
      AskLicensesAgreementWithHeading(dirs, patterns, action, enable_back,
×
285
        base_product, require_agreement, caption, heading)
286
    end
287

288
    # @see {AskLicensesAgreement} for details
289
    # @param caption [String] custom dialog title
290
    # @param heading [String] optional heading displayed above the license text
291
    def AskLicensesAgreementWithHeading(dirs, _patterns, action, enable_back,
1✔
292
      base_product, require_agreement, caption, heading)
293
      dirs = deep_copy(dirs)
×
294
      if dirs.nil? || dirs == []
×
295
        Builtins.y2error("No directories: %1", dirs)
×
296
        # error message
297
        Report.Error("Internal Error: No license to show")
×
298
        return :auto
×
299
      end
300

301
      created_new_dialog = false
×
302

303
      # #459391
304
      # If a progress is running open another dialog
305
      if Progress.IsRunning
×
306
        Builtins.y2milestone(
×
307
          "Some progress is running, opening new dialog for license..."
308
        )
309
        Wizard.OpenNextBackDialog
×
310
        created_new_dialog = true
×
311
      end
312

313
      license_idents = []
×
314

315
      licenses = []
×
316
      counter = -1
×
317
      contents = VBox(
×
318
        if heading
×
319
          VBox(
×
320
            VSpacing(0.5),
321
            Left(Heading(heading)),
322
            VSpacing(0.5)
323
          )
324
        else
325
          Empty()
×
326
        end
327
      )
328

329
      Builtins.foreach(dirs) do |dir|
×
330
        counter = Ops.add(counter, 1)
×
331
        Ops.set(licenses, counter, {})
×
332
        @lic_lang = ""
×
333
        available_langs = []
×
334
        license_ident = ""
×
335
        tmp_licenses = {}
×
336
        tmp_licenses_ref = arg_ref(tmp_licenses)
×
337
        available_langs_ref = arg_ref(available_langs)
×
338
        license_ident_ref = arg_ref(license_ident)
×
339
        InitLicenseData(
×
340
          nil,
341
          dir,
342
          tmp_licenses_ref,
343
          available_langs_ref,
344
          require_agreement,
345
          license_ident_ref,
346
          dir
347
        )
348
        tmp_licenses = tmp_licenses_ref.value
×
349
        available_langs = available_langs_ref.value
×
350
        license_ident = license_ident_ref.value
×
351
        license_idents << license_ident if license_ident
×
352
        license_term = (
353
          tmp_licenses_ref = arg_ref(tmp_licenses)
×
354
          result = GetLicenseDialog(
×
355
            available_langs,
356
            @lic_lang,
357
            tmp_licenses_ref,
358
            dir,
359
            true
360
          )
361
          tmp_licenses = tmp_licenses_ref.value
×
362
          result
×
363
        )
364
        if license_term.nil?
×
365
          Builtins.y2error("Oops, license term is: %1", license_term)
×
366
        else
367
          contents = Builtins.add(contents, license_term)
×
368
        end
369
        # Display beta file as a popup if exists
370
        InstShowInfo.show_info_txt(@beta_file) if !@beta_file.nil?
×
371
        Ops.set(licenses, counter, tmp_licenses)
×
372
      end
373

374
      Wizard.SetContents(
×
375
        caption,
376
        contents,
377
        GetLicenseDialogHelp(),
378
        enable_back,
379
        true # always enable next, as popup is raised if not accepted (bnc#993530)
380
      )
381

382
      Wizard.SetTitleIcon("yast-license")
×
383
      Wizard.SetFocusToNextButton
×
384

385
      tmp_licenses = {}
×
386
      ret = (
387
        tmp_licenses_ref = arg_ref(tmp_licenses)
×
388
        result = HandleLicenseDialogRet(
×
389
          tmp_licenses_ref,
390
          base_product,
391
          action
392
        )
393
        result
×
394
      )
395
      Builtins.y2milestone("Dialog ret: %1", ret)
×
396

397
      # store already accepted license IDs
398
      if ret == :accepted
×
399
        Builtins.foreach(license_idents) do |license_ident|
×
400
          LicenseHasBeenAccepted(license_ident)
×
401
        end
402
      end
403

404
      CleanUpLicense(@tmpdir)
×
405

406
      # bugzilla #303922
407
      Wizard.CloseDialog if created_new_dialog
×
408

409
      CleanUp()
×
410

411
      ret
×
412
    end
413

414
    def AskAddOnLicenseAgreement(src_id)
1✔
415
      AskLicenseAgreement(
3✔
416
        src_id,
417
        "",
418
        @license_patterns,
419
        "refuse",
420
        # back button is disabled
421
        false,
422
        false,
423
        false,
424
        Builtins.tostring(src_id)
425
      )
426
    end
427

428
    def AskFirstStageLicenseAgreement(src_id, action)
1✔
429
      # bug #223258
430
      # disabling back button when the select-language dialog is skipped
431
      #
432
      enable_back = true
×
433
      enable_back = false if Language.selection_skipped
×
434

435
      AskLicenseAgreement(
×
436
        nil,
437
        "",
438
        @license_patterns,
439
        action,
440
        # back button is enabled
441
        enable_back,
442
        true,
443
        true,
444
        # unique id
445
        Builtins.tostring(src_id)
446
      )
447
    end
448

449
    # Called from the first stage Welcome dialog by clicking on a button
450
    def ShowFullScreenLicenseInInstallation(replace_point_id, src_id)
1✔
451
      replace_point_id = deep_copy(replace_point_id)
×
452
      @lic_lang = ""
×
453
      licenses = {}
×
454
      available_langs = []
×
455
      license_ident = ""
×
456

457
      licenses_ref = arg_ref(licenses)
×
458
      available_langs_ref = arg_ref(available_langs)
×
459
      license_ident_ref = arg_ref(license_ident)
×
460
      InitLicenseData(
×
461
        nil,
462
        "",
463
        licenses_ref,
464
        available_langs_ref,
465
        true,
466
        license_ident_ref,
467
        Builtins.tostring(src_id)
468
      )
469
      licenses = licenses_ref.value
×
470
      available_langs = available_langs_ref.value
×
471

472
      # Replaces the dialog content with Languages combo-box
473
      # and the current license text (richtext)
474
      UI.ReplaceWidget(
×
475
        Id(replace_point_id),
476
        (
477
          licenses_ref = arg_ref(licenses)
×
478
          result = GetLicenseDialogTerm(
×
479
            available_langs,
480
            @lic_lang,
481
            licenses_ref,
482
            Builtins.tostring(src_id)
483
          )
484
          licenses = licenses_ref.value
×
485
          result
×
486
        )
487
      )
488

489
      ret = nil
×
490

491
      loop do
×
492
        ret = UI.UserInput
×
493

494
        if Ops.is_string?(ret) &&
×
495
            Builtins.regexpmatch(
496
              Builtins.tostring(ret),
497
              "^license_language_[[:digit:]]+"
498
            )
499
          licenses_ref = arg_ref(licenses)
×
500
          UpdateLicenseContent(licenses_ref, GetId(Builtins.tostring(ret)))
×
501
          licenses = licenses_ref.value
×
502
        else
503
          break
×
504
        end
505
      end
506

507
      CleanUp()
×
508

509
      true
×
510
    end
511

512
    # Used in the first-stage Welcome dialog
513
    def ShowLicenseInInstallation(replace_point_id, src_id)
1✔
514
      replace_point_id = deep_copy(replace_point_id)
×
515
      @lic_lang = ""
×
516
      licenses = {}
×
517
      available_langs = []
×
518
      license_ident = ""
×
519

520
      licenses_ref = arg_ref(licenses)
×
521
      available_langs_ref = arg_ref(available_langs)
×
522
      license_ident_ref = arg_ref(license_ident)
×
523
      InitLicenseData(
×
524
        nil,
525
        "",
526
        licenses_ref,
527
        available_langs_ref,
528
        true,
529
        license_ident_ref,
530
        Builtins.tostring(src_id)
531
      )
532
      licenses = licenses_ref.value
×
533

534
      licenses_ref = arg_ref(licenses)
×
535
      rt = GetLicenseContent(
×
536
        @lic_lang,
537
        licenses_ref,
538
        Builtins.tostring(src_id)
539
      )
540
      UI.ReplaceWidget(Id(replace_point_id), rt)
×
541

542
      display_beta(src_id) if @beta_file && !beta_seen?(src_id)
×
543

544
      CleanUp()
×
545

546
      true
×
547
    end
548

549
    def AskInstalledLicenseAgreement(directory, action)
1✔
550
      # patterns are hard-coded
551
      AskLicenseAgreement(
×
552
        nil,
553
        directory,
554
        [],
555
        action,
556
        false,
557
        true,
558
        false,
559
        directory
560
      )
561
    end
562

563
    # FATE #306295: More licenses in one dialog
564
    def AskInstalledLicensesAgreement(directories, action)
1✔
565
      directories = deep_copy(directories)
×
566
      # patterns are hard-coded
567
      AskLicensesAgreement(directories, [], action, false, true, false)
×
568
    end
569

570
    # FIXME: this is needed only by yast2-registration, fix it later
571
    # and make this method private
572
    # @param licenses [ArgRef<Hash{String, String}>] a map $[ lang_code : filename ]
573
    def HandleLicenseDialogRet(licenses, base_product, action)
1✔
574
      ret = nil
13✔
575

576
      loop do
13✔
577
        ret = UI.UserInput
17✔
578
        log.info "User ret: #{ret}"
17✔
579

580
        if ret.is_a?(::String) && ret.start_with?("license_language_")
17✔
581
          licenses_ref = arg_ref(licenses.value)
1✔
582
          UpdateLicenseContent(licenses_ref, GetId(ret))
1✔
583
          licenses.value = licenses_ref.value
1✔
584
          ret = :language
1✔
585
        # bugzilla #303828
586
        # disabled next button unless yes/no is selected
587
        elsif ret.is_a?(::String) && ret.start_with?("eula_")
16✔
588
          Wizard.EnableNextButton if AllLicensesAcceptedOrDeclined()
1✔
589
        # Aborting the license dialog
590
        elsif ret == :abort
15✔
591
          # bnc#886662
592
          if Stage.initial
4✔
593
            next unless Popup.ConfirmAbort(:painless)
2✔
594
          else
595
            # popup question
596
            next unless Popup.YesNo(_("Really abort the add-on product installation?"))
2✔
597
          end
598

599
          log.warn "Aborting..."
2✔
600
          break
2✔
601
        elsif ret == :next
11✔
602
          if AllLicensesAccepted()
10✔
603
            log.info "All licenses have been accepted."
5✔
604
            ret = :accepted
5✔
605
            break
5✔
606
          end
607

608
          # License declined
609

610
          # message is void in case not accepting license doesn't stop the installation
611
          if action == "continue"
5✔
612
            log.info "action in case of license refusal is continue, not asking user"
1✔
613
            ret = :accepted
1✔
614
            break
1✔
615
          end
616

617
          if base_product
4✔
618
            # TODO: refactor to use same widget as in inst_complex_welcome
619
            # NOTE: keep in sync with inst_compex_welcome client, for grabing its translation
620
            # mimic inst_complex_welcome behavior see bnc#993530
621
            refuse_popup_text = Builtins.dgettext(
×
622
              "installation",
623
              "You must accept the license to install this product"
624
            )
625
            Popup.Message(refuse_popup_text)
×
626
            next
×
627
          else
628
            # text changed due to bug #162499
629
            # TRANSLATORS: text asking whether to refuse a license (Yes-No popup)
630
            refuse_popup_text = _("Refusing the license agreement cancels the add-on\n" \
4✔
631
                                  "product installation. Really refuse the agreement?")
632
            next unless Popup.YesNo(refuse_popup_text)
4✔
633
          end
634

635
          log.info "License has been declined."
4✔
636

637
          case action
4✔
638
          when "refuse"
639
            ret = :refused
1✔
640
          when "abort"
641
            ret = :abort
1✔
642
          when "halt"
643
            # timed ok/cancel popup
644
            next unless Popup.TimedOKCancel(_("The system is shutting down..."), 10)
1✔
645

646
            ret = :halt
1✔
647
          else
648
            log.error "Unknown action #{action}"
1✔
649
            ret = :abort
1✔
650
          end
651

652
          break
4✔
653
        elsif ret == :back
1✔
654
          ret = :back
1✔
655
          break
1✔
656
        else
657
          log.error "Unhandled input: #{ret}"
×
658
        end
659
      end
660

661
      log.info "Returning #{ret}"
13✔
662
      ret
13✔
663
    end
664

665
    # @return [Array<String>] Fallback languages
666
    DEFAULT_FALLBACK_LANGUAGES = ["en_US", "en"].freeze
1✔
667

668
    def displayable_language?(lang)
1✔
669
      return true if lang.empty? # zypp means English here
2✔
670

671
      Yast::Language.supported_language?(lang)
2✔
672
    end
673
    private :displayable_language?
1✔
674

675
    # FIXME: this is needed only by yast2-registration, fix it later
676
    # and make this method private
677
    #
678
    # Displays License with Help and ( ) Yes / ( ) No radio buttons
679
    # @param [Array<String>] languages list of license translations
680
    # @param [Boolean] back enable "Back" button
681
    # @param [String] license_language default license language
682
    # @param licenses [ArgRef<Hash{String, String}>] a map $[ lang_code : filename ]
683
    # @param [String] id unique license ID
684
    # @param [String] caption dialog title
685
    def DisplayLicenseDialogWithTitle(languages, back, license_language, licenses, id, caption)
1✔
686
      languages = languages.find_all { |lang| displayable_language?(lang) }
3✔
687
      log.info "Displayable languages: #{languages}, wanted: #{license_language}"
1✔
688

689
      contents = GetLicenseDialog(
1✔
690
        languages,
691
        license_language,
692
        licenses,
693
        id,
694
        false
695
      )
696

697
      Wizard.SetContents(
1✔
698
        caption,
699
        contents,
700
        GetLicenseDialogHelp(),
701
        back,
702
        # always allow next button, as if not accepted, it will raise popup (bnc#993530)
703
        true
704
      )
705

706
      # set the initial license download URL
707
      update_license_location(license_language, licenses)
1✔
708

709
      Wizard.SetTitleIcon("yast-license")
1✔
710
      Wizard.SetFocusToNextButton
1✔
711

712
      nil
1✔
713
    end
714

715
    # Help text for asking license for product
716
    def GetLicenseDialogHelp
1✔
717
      # help text
718
      _(
2✔
719
        "<p>Read the license agreement carefully and select\n" \
720
        "one of the available options. If you do not agree to the license agreement,\n" \
721
        "the configuration will be aborted.</p>\n"
722
      )
723
    end
724

725
    publish function: :AcceptanceNeeded, type: "boolean (string)"
1✔
726
    publish function: :AskLicenseAgreement,
1✔
727
      type:     "symbol (integer, string, list <string>, string, " \
728
                "boolean, boolean, boolean, string)"
729
    publish function: :AskAddOnLicenseAgreement, type: "symbol (integer)"
1✔
730
    publish function: :AskFirstStageLicenseAgreement, type: "symbol (integer, string)"
1✔
731
    publish function: :ShowFullScreenLicenseInInstallation, type: "boolean (any, integer)"
1✔
732
    publish function: :ShowLicenseInInstallation, type: "boolean (any, integer)"
1✔
733
    publish function: :AskInstalledLicenseAgreement, type: "symbol (string, string)"
1✔
734
    publish function: :AskInstalledLicensesAgreement, type: "symbol (list <string>, string)"
1✔
735

736
  private
1✔
737

738
    # check if the license location is an URL for download
739
    # @param [String] location
740
    # @return [Boolean] true if it is a HTTP, HTTPS or an FTP URL
741
    def location_is_url?(location)
1✔
742
      return false unless location.is_a?(::String)
11✔
743

744
      DOWNLOAD_URL_SCHEMA.include?(URI(location).scheme)
8✔
745
    rescue URI::InvalidURIError => e
746
      log.error "Error while parsing URL #{location.inspect}: #{e.message}"
×
747
      false
×
748
    end
749

750
    # split a long URL to multiple lines
751
    # @param [String] url URL
752
    # @return [String] URL split to multiple lines if too long
753
    def format_url(url)
1✔
754
      url.scan(/.{1,57}/).join("\n")
×
755
    end
756

757
    # crate a label describing the license URL location
758
    # @param [String] display_url URL to display
759
    # return [String] translated label
760
    def license_download_label(display_url)
1✔
761
      # TRANSLATORS: %{license_url} is an URL where the displayed license can be found
762
      format(
×
763
        _("If you want to print this EULA, you can download it from\n%{license_url}"),
764
        license_url: display_url
765
      )
766
    end
767

768
    # update license location displayed in the dialog (e.g. after license translation
769
    # is changed)
770
    # @param [String] lang language of the currently displayed license
771
    # @param licenses [ArgRef<Hash{String, String}>] a map $[ lang_code : filename ]
772
    def update_license_location(lang, licenses)
1✔
773
      return if !location_is_url?(license_file_print) || !UI.WidgetExists(:printing_hint)
1✔
774

775
      # name of the license file
776
      file = File.basename(WhichLicenceFile(lang, licenses))
×
777

778
      url = URI(license_file_print)
×
779
      url.path = File.join(url.path, file)
×
780
      log.info "Updating license URL: #{url}"
×
781

782
      display_url = format_url(url.to_s)
×
783

784
      UI.ReplaceWidget(:printing_hint, Label(license_download_label(display_url)))
×
785
    end
786

787
    # update license location displayed in the dialog
788
    # @param [Fixnum] src_id integer repository to get the license from.
789
    def update_license_archive_location(src_id)
1✔
790
      repo_data = Pkg::SourceGeneralData(src_id)
×
791
      return unless repo_data
×
792

793
      src_url = repo_data["url"]
×
794
      if location_is_url?(src_url) && UI.WidgetExists(:printing_hint)
×
795
        lic_url = File.join(src_url, @license_file_print)
×
796
        UI.ReplaceWidget(:printing_hint, Label(license_download_label(lic_url)))
×
797
      end
798

UNCOV
799
      nil
×
800
    end
801

802
    # Display beta file as a popup if exists
803
    def display_beta(id)
1✔
804
      if Mode.autoinst
2✔
805
        Builtins.y2milestone("Autoinstallation: Skipping beta file...")
×
806
      else
807
        InstShowInfo.show_info_txt(@beta_file)
2✔
808
        beta_seen!(id)
2✔
809
      end
810
    end
811

812
    # Determines whether the license was accepted for a given product
813
    #
814
    # This method reads the license content and uses the new
815
    # Y2Packager::ProductLicense API to determine whether the license was
816
    # accepted or not. This is just a transitory method because, in the future,
817
    # all licenses should be read through the new API.
818
    #
819
    # @param product  [Y2Packager::Product] Product instance
820
    # @param licenses [Hash<String,String>] License files indexed by language
821
    # @return [boolean] true if the license was already accepted; false if it was
822
    #   not acceptedd or it is not found.
823
    def license_accepted_for?(product, licenses)
1✔
824
      license_file = licenses["en_US"] || licenses["en"] || licenses[""]
3✔
825
      content = SCR.Read(path(".target.string"), license_file).to_s
3✔
826
      if content.empty?
3✔
827
        log.error "No license found for #{product.name} in the repository"
×
828
        return false
×
829
      end
830

831
      license = Y2Packager::ProductLicense.find(product.name, content: content)
3✔
832
      license&.accepted?
3✔
833
    end
834

835
    # Find the product in the given repository
836
    #
837
    # @param src_id [Integer] Repository ID
838
    # @return [Y2Packager::Product,nil] Product or nil if it was not found
839
    def repository_product(src_id)
1✔
840
      products = Y2Packager::Resolvable.find(kind: :product, source: src_id)
2✔
841
      if products.empty?
2✔
842
        log.error "No product found in the repository (#{src_id})"
1✔
843
        return
1✔
844
      end
845
      product = products.first
1✔
846
      Y2Packager::Product.new(name: product.name, short_name: product.short_name,
1✔
847
        display_name: product.display_name, version: product.version,
848
        arch: product.arch, category: product.category, vendor: product.vendor)
849
    end
850

851
    # @param licenses [ArgRef<Hash{String, String}>] a map $[ lang_code : filename ]
852
    def GetLicenseContent(lic_lang, licenses, id)
1✔
853
      license_file = (
854
        licenses_ref = arg_ref(licenses.value)
1✔
855
        result = WhichLicenceFile(lic_lang, licenses_ref)
1✔
856
        licenses.value = licenses_ref.value
1✔
857
        result
1✔
858
      )
859

860
      license_text = Convert.to_string(
1✔
861
        SCR.Read(path(".target.string"), license_file)
862
      )
863
      if license_text.nil?
1✔
864
        if Mode.live_installation
1✔
865
          license_text = Builtins.sformat(
×
866
            "<b>%1</b><br>%2",
867
            Builtins.sformat(_("Cannot read license file %1"), license_file),
868
            _(
869
              "To show the product license properly, put the license.tar.gz file to " \
870
              "the root of the live media when building the image."
871
            )
872
          )
873
        else
874
          Report.Error(
1✔
875
            Builtins.sformat(_("Cannot read license file %1"), license_file)
876
          )
877
          license_text = ""
1✔
878
        end
879
      end
880
      # License is HTML (or RichText)
881
      rt = if Builtins.regexpmatch(license_text, "</.*>")
1✔
882
        MinWidth(
×
883
          80,
884
          RichText(Id(Builtins.sformat("welcome_text_%1", id)), license_text)
885
        )
886
      else
887
        # License is plain text
888
        # details in BNC #449188
889
        MinWidth(
1✔
890
          80,
891
          RichText(
892
            Id(Builtins.sformat("welcome_text_%1", id)),
893
            Ops.add(Ops.add("<pre>", String.EscapeTags(license_text)), "</pre>")
894
          )
895
        )
896
      end
897

898
      deep_copy(rt)
1✔
899
    end
900

901
    # Checks the string that might contain ID of a license and
902
    # eventually returns that id.
903
    # See also GetIdPlease for a better ratio of successful stories.
904
    def GetId(id_text)
1✔
905
      id = nil
1✔
906

907
      if Builtins.regexpmatch(id_text, "^license_language_.+")
1✔
908
        id = Builtins.regexpsub(id_text, "^license_language_(.+)", "\\1")
1✔
909
      else
910
        Builtins.y2error("Cannot get ID from %1", id_text)
×
911
      end
912

913
      id
1✔
914
    end
915

916
    # Helper func. Cuts encoding suffix off the LANG
917
    # env. variable i.e. foo_BAR.UTF-8 => foo_BAR
918
    def EnvLangToLangCode(env_lang)
1✔
919
      tmp = []
×
920
      tmp = Builtins.splitstring(env_lang, ".@") if !env_lang.nil?
×
921

922
      Ops.get(tmp, 0, "")
×
923
    end
924

925
    # Sets that the license (file) has been already accepted
926
    #
927
    # @param [String] license_ident file name
928
    def LicenseHasBeenAccepted(license_ident)
1✔
929
      if license_ident.nil? || license_ident == ""
×
930
        Builtins.y2error("Wrong license ID '%1'", license_ident)
×
931
        return
×
932
      end
933

UNCOV
934
      nil
×
935
    end
936

937
    # @param licenses [ArgRef<Hash{String, String}>] a map $[ lang_code : filename ]
938
    def WhichLicenceFile(license_language, licenses)
1✔
939
      license_file = Ops.get(licenses.value, license_language, "")
1✔
940

941
      if license_file.nil? || license_file == ""
1✔
942
        Builtins.y2error(
×
943
          "No license file defined for language '%1' in %2",
944
          license_language,
945
          licenses.value
946
        )
947
      else
948
        Builtins.y2milestone("Using license file: %1", license_file)
1✔
949
      end
950

951
      license_file
1✔
952
    end
953

954
    # @param licenses [ArgRef<Hash{String, String}>] a map $[ lang_code : filename ]
955
    def GetLicenseDialogTerm(languages, license_language, licenses, id)
1✔
956
      languages = deep_copy(languages)
1✔
957
      rt = (
958
        licenses_ref = arg_ref(licenses.value)
1✔
959
        result = GetLicenseContent(
1✔
960
          license_language,
961
          licenses_ref,
962
          id
963
        )
964
        licenses.value = licenses_ref.value
1✔
965
        result
1✔
966
      )
967

968
      # bug #204791, no more "languages.ycp" client
969
      lang_names_orig = Language.GetLanguagesMap(false)
1✔
970
      if lang_names_orig.nil?
1✔
971
        Builtins.y2error("Wrong definition of languages")
×
972
        lang_names_orig = {}
×
973
      end
974

975
      lang_names = {}
1✔
976

977
      # $[ "en" : "English (US)", "de" : "Deutsch" ]
978
      lang_names = Builtins.mapmap(lang_names_orig) do |code, descr|
1✔
979
        { code => Ops.get_string(descr, 4, "") }
×
980
      end
981

982
      # for the default fallback
983
      if Ops.get(lang_names, "").nil?
1✔
984
        # language name
985
        Ops.set(
1✔
986
          lang_names,
987
          "",
988
          Ops.get_string(lang_names_orig, ["en_US", 4], "")
989
        )
990
      end
991

992
      if Ops.get(lang_names, "en").nil?
1✔
993
        # language name
994
        Ops.set(
1✔
995
          lang_names,
996
          "en",
997
          Ops.get_string(lang_names_orig, ["en_US", 4], "")
998
        )
999
      end
1000

1001
      lang_pairs = Builtins.maplist(languages) do |l|
1✔
1002
        name_print = Ops.get(lang_names, l, "")
2✔
1003
        if name_print == ""
2✔
1004
          # TODO: FIXME: the language code might be longer than 2 characters,
1005
          # e.g. "ast_ES"
1006
          l_short = Builtins.substring(l, 0, 2)
2✔
1007

1008
          Builtins.foreach(lang_names) do |k, v|
2✔
1009
            if Builtins.substring(k, 0, 2) == l_short
4✔
1010
              name_print = v
1✔
1011
              next true
1✔
1012
            end
1013
            false
3✔
1014
          end
1015
        end
1016
        [l, name_print]
2✔
1017
      end
1018

1019
      # filter-out languages that don't have any name
1020
      lang_pairs = Builtins.filter(lang_pairs) do |lang_pair|
1✔
1021
        if Ops.get(lang_pair, 1, "") == ""
2✔
1022
          Builtins.y2warning(
2✔
1023
            "Unknown license language '%1', filtering out...",
1024
            lang_pair
1025
          )
1026
          false
2✔
1027
        else
1028
          true
×
1029
        end
1030
      end
1031

1032
      lang_pairs = Builtins.sort(lang_pairs) do |a, b|
1✔
1033
        # bnc#385172: must use < instead of <=, the following means:
1034
        # strcoll(x) <= strcoll(y) && strcoll(x) != strcoll(y)
1035
        lsorted = Builtins.lsort([Ops.get(a, 1, ""), Ops.get(b, 1, "")])
×
1036
        lsorted_r = Builtins.lsort([Ops.get(b, 1, ""), Ops.get(a, 1, "")])
×
1037
        Ops.get_string(lsorted, 0, "") == Ops.get(a, 1, "") &&
×
1038
          lsorted == lsorted_r
1039
      end
1040
      langs = Builtins.maplist(lang_pairs) do |descr|
1✔
1041
        Item(
×
1042
          Id(Ops.get(descr, 0, "")),
1043
          Ops.get(descr, 1, ""),
1044
          Ops.get(descr, 0, "") == license_language
1045
        )
1046
      end
1047

1048
      lang_selector_options = Opt(:notify)
1✔
1049
      # Disable in case there is no language to select
1050
      # bugzilla #203543
1051
      if Ops.less_or_equal(Builtins.size(langs), 1)
1✔
1052
        lang_selector_options = Builtins.add(lang_selector_options, :disabled)
1✔
1053
      end
1054

1055
      @license_ids = Builtins.toset(Builtins.add(@license_ids, id))
1✔
1056

1057
      VBox(
1✔
1058
        # combo box
1059
        Left(
1060
          ComboBox(
1061
            Id(Builtins.sformat("license_language_%1", id)),
1062
            lang_selector_options,
1063
            _("&Language"),
1064
            langs
1065
          )
1066
        ),
1067
        ReplacePoint(Id(Builtins.sformat("license_contents_rp_%1", id)), rt)
1068
      )
1069
    end
1070

1071
    # Returns source ID of the base product - initial installation only!
1072
    # If no sources are found, returns 0.
1073
    # FIXME: Connected to bsc#993285, refactoring needed
1074
    #
1075
    # return [Integer] base_product_id or 0
1076
    def base_product_id
1✔
1077
      raise "Base product can be only found in installation" unless Stage.initial
×
1078

1079
      # The first product in the list of known products
1080
      # 0 is the backward-compatible default value, first installation repo always
1081
      # gets this ID later
1082
      current_sources = Pkg.SourceGetCurrent(true)
×
1083
      current_sources.any? ? current_sources.first : 0
×
1084
    end
1085

1086
    # @param [Array<String>] languages list of license translations
1087
    # @param [String] license_language default license language
1088
    # @param licenses [ArgRef<Hash{String, String}>] a map $[ lang_code : filename ]
1089
    # @param [String] id unique license ID
1090
    # @param [Boolean] spare_space
1091
    def GetLicenseDialog(languages, license_language, licenses, id, spare_space)
1✔
1092
      space = UI.TextMode ? 1 : 3
1✔
1093

1094
      license_buttons = VBox(
1✔
1095
        VSpacing(spare_space ? 0 : 0.5),
1✔
1096
        Left(
1097
          CheckBox(
1098
            Id("eula_#{id}"),
1099
            Opt(:notify),
1100
            # check box label
1101
            _("I &Agree to the License Terms.")
1102
          )
1103
        )
1104
      )
1105

1106
      VBox(
1✔
1107
        VSpacing(spare_space ? 0 : 1),
1✔
1108
        HBox(
1109
          HSpacing(2 * space),
1110
          VBox(
1111
            GetLicenseDialogTerm(
1112
              languages,
1113
              license_language,
1114
              arg_ref(licenses.value),
1115
              id
1116
            ),
1117
            if @license_file_print.nil?
1✔
1118
              Empty()
1✔
1119
            else
1120
              Left(
×
1121
                # FATE #302018
1122
                ReplacePoint(
1123
                  Id(:printing_hint),
1124
                  Label(
1125
                    if @license_on_installed_system
×
1126
                      # TRANSLATORS: addition license information
1127
                      # %s is replaced with the directory name
1128
                      _("This EULA can be found in the directory\n%s") % @license_file_print
×
1129
                    else
1130
                      # TRANSLATORS: addition license information
1131
                      # %s is replaced with the filename
1132
                      _("If you want to print this EULA, you can find it\n" \
×
1133
                        "on the first media in the file %s") %
1134
                      @license_file_print
1135
                    end
1136
                  )
1137
                )
1138
              )
1139
            end,
1140
            # BNC #448598
1141
            # yes/no buttons exist only if needed
1142
            # if they don't exist, user is not asked to accept the license later
1143
            AcceptanceNeeded(id) ? license_buttons : Empty()
1✔
1144
          ),
1145
          HSpacing(2 * space)
1146
        )
1147
      )
1148
    end
1149

1150
    # Displays License dialog
1151
    # @param licenses [ArgRef<Hash{String, String}>] a map $[ lang_code : filename ]
1152
    def DisplayLicenseDialog(languages, back, license_language, licenses, id)
1✔
1153
      # dialog title
1154
      DisplayLicenseDialogWithTitle(languages, back, license_language, licenses, id,
×
1155
        _("License Agreement"))
1156
    end
1157

1158
    # Removes the temporary directory for licenses
1159
    # @param [String] tmpdir temporary directory path
1160
    def CleanUpLicense(tmpdir)
1✔
1161
      if !tmpdir.nil? && tmpdir != "/"
×
1162
        SCR.Execute(
×
1163
          path(".target.bash_output"),
1164
          "/usr/bin/rm -rf #{tmpdir.shellescape}"
1165
        )
1166
      end
1167

UNCOV
1168
      nil
×
1169
    end
1170

1171
    # Get all files with license existing in specified directory
1172
    # @param [String] dir string directory to look into
1173
    # @param [Array<String>] patterns a list of patterns for the files, regular expressions
1174
    #   with %1 for the language
1175
    # @return [Hash{String, String}] a map $[ lang_code : filename ]
1176
    def LicenseFiles(dir, patterns)
1✔
1177
      patterns = deep_copy(patterns)
3✔
1178
      ret = {}
3✔
1179

1180
      return deep_copy(ret) if dir.nil?
3✔
1181

1182
      files = Convert.convert(
×
1183
        SCR.Read(path(".target.dir"), dir),
1184
        from: "any",
1185
        to:   "list <string>"
1186
      )
1187
      Builtins.y2milestone("All files in license directory: %1", files)
×
1188

1189
      # no license
1190
      return {} if files.nil?
×
1191

1192
      Builtins.foreach(patterns) do |p|
×
1193
        if Builtins.issubstring(p, "%")
×
1194
          regpat = Builtins.sformat(p, "(.+)")
×
1195
          Builtins.foreach(files) do |file|
×
1196
            if Builtins.regexpmatch(file, regpat)
×
1197
              key = Builtins.regexpsub(file, regpat, "\\1")
×
1198
              Ops.set(ret, key, Ops.add(Ops.add(dir, "/"), file))
×
1199
            end
1200
          end
1201
        else
1202
          Builtins.foreach(files) do |file|
×
1203
            # Possible license file names are regexp patterns
1204
            # (see list <string> license_patterns)
1205
            # so we should treat them as such (bnc#533026)
1206
            Ops.set(ret, "", Ops.add(Ops.add(dir, "/"), file)) if Builtins.regexpmatch(file, p)
×
1207
          end
1208
        end
1209
      end
1210
      Builtins.y2milestone("Files containing license: %1", ret)
×
1211
      deep_copy(ret)
×
1212
    end
1213

1214
    def UnpackLicenseTgzFileToDirectory(unpack_file, to_directory)
1✔
1215
      # License file exists
1216
      if FileUtils.Exists(unpack_file)
×
1217
        out = SCR.Execute(
×
1218
          path(".target.bash_output"),
1219
          Builtins.sformat(
1220
            "\n/usr/bin/rm -rf %1 && /usr/bin/mkdir -p %1 && cd %1 && " \
1221
            "/usr/bin/tar -xzf #{unpack_file.shellescape}\n",
1222
            to_directory.shellescape
1223
          )
1224
        )
1225

1226
        # Extracting license failed, cannot accept the license
1227
        if Ops.get_integer(out, "exit", 0).nonzero?
×
1228
          Builtins.y2error("Cannot untar license -> %1", out)
×
1229
          # popup error
1230
          Report.Error(
×
1231
            _("An error occurred while preparing the installation system.")
1232
          )
1233
          CleanUpLicense(to_directory)
×
1234
          return false
×
1235
        end
1236

1237
        # Success
1238
        true
×
1239

1240
        # Nothing to unpack
1241
      else
1242
        Builtins.y2error("No such file: %1", unpack_file)
×
1243
        false
×
1244
      end
1245
    end
1246

1247
    def SearchForLicense_FirstStageBaseProduct(_src_id, _fallback_dir)
1✔
1248
      Builtins.y2milestone("Getting license from installation product")
3✔
1249

1250
      license_file = "/license.tar.gz"
3✔
1251

1252
      if FileUtils.Exists(license_file)
3✔
1253
        Builtins.y2milestone("Installation Product has a license")
2✔
1254

1255
        @tmpdir = Builtins.sformat(
2✔
1256
          "%1/product-license/base-product/",
1257
          Convert.to_string(SCR.Read(path(".target.tmpdir")))
1258
        )
1259

1260
        if UnpackLicenseTgzFileToDirectory(license_file, @tmpdir)
2✔
1261
          @license_dir = @tmpdir
2✔
1262
          @license_file_print = "license.tar.gz"
2✔
1263
        end
1264
      else
1265
        Builtins.y2milestone("Installation Product doesn't have a license")
1✔
1266
      end
1267

1268
      @beta_file = README_BETA if FileUtils.Exists(README_BETA)
3✔
1269

1270
      nil
3✔
1271
    end
1272

1273
    def SearchForLicense_LiveCDInstallation(_src_id, _fallback_dir)
1✔
1274
      Builtins.y2milestone("LiveCD License")
×
1275

1276
      # BNC #594042: Multiple license locations
1277
      license_locations = ["/usr/share/doc/licenses/", "/"]
×
1278

1279
      @license_dir = nil
×
1280
      @beta_file = nil
×
1281

1282
      Builtins.foreach(license_locations) do |license_location|
×
1283
        license_location = Builtins.sformat(
×
1284
          "%1/license.tar.gz",
1285
          license_location
1286
        )
1287
        if FileUtils.Exists(license_location)
×
1288
          Builtins.y2milestone("Using license: %1", license_location)
×
1289
          @tmpdir = Builtins.sformat(
×
1290
            "%1/product-license/LiveCD/",
1291
            Convert.to_string(SCR.Read(path(".target.tmpdir")))
1292
          )
1293

1294
          if UnpackLicenseTgzFileToDirectory(license_location, @tmpdir)
×
1295
            @license_dir = @tmpdir
×
1296
            @license_file_print = "license.tar.gz"
×
1297
          else
1298
            CleanUpLicense(@tmpdir)
×
1299
          end
1300
          raise Break
×
1301
        end
1302
      end
1303

1304
      Builtins.y2milestone("No license found in: %1", license_locations) if @license_dir.nil?
×
1305

1306
      Builtins.foreach(license_locations) do |beta_location|
×
1307
        beta_location += README_BETA
×
1308
        if FileUtils.Exists(beta_location)
×
1309
          Builtins.y2milestone("Using beta file: %1", beta_location)
×
1310
          @beta_file = beta_location
×
1311
          raise Break
×
1312
        end
1313
      end
1314

1315
      Builtins.y2milestone("No beta file found in: %1", license_locations) if @beta_file.nil?
×
1316

UNCOV
1317
      nil
×
1318
    end
1319

1320
    def SearchForLicense_NormalRunBaseProduct(_src_id, fallback_dir)
1✔
1321
      Builtins.y2milestone("Using default license directory %1", fallback_dir)
×
1322

1323
      if FileUtils.Exists(fallback_dir)
×
1324
        @license_dir = fallback_dir
×
1325
        @license_file_print = fallback_dir
×
1326
        @license_on_installed_system = true
×
1327
      else
1328
        Builtins.y2warning("Fallback dir doesn't exist %1", fallback_dir)
×
1329
        @license_dir = nil
×
1330
      end
1331

1332
      @beta_file = README_BETA if FileUtils.Exists(README_BETA)
×
1333

UNCOV
1334
      nil
×
1335
    end
1336

1337
    def SearchForLicense_AddOnProduct(src_id, _fallback_dir)
1✔
1338
      Builtins.y2milestone("Getting license beta from repository %1", src_id)
3✔
1339

1340
      # use a separate license directory for each product
1341
      @tmpdir = File.join(SCR.Read(path(".target.tmpdir")), "product-license", src_id.to_s)
3✔
1342

1343
      # try reading the product license from libzypp first
1344
      return if product_license(src_id, @tmpdir)
3✔
1345

1346
      @beta_file = Pkg.SourceProvideOptionalFile(
3✔
1347
        src_id, # optional
1348
        1,
1349
        README_BETA
1350
      )
1351

1352
      # FATE #302018 comment #54
1353
      license_file_location = "/license.tar.gz"
3✔
1354
      license_file = Pkg.SourceProvideDigestedFile(
3✔
1355
        src_id, # optional
1356
        1,
1357
        license_file_location,
1358
        true
1359
      )
1360

1361
      if !license_file.nil?
3✔
1362
        Builtins.y2milestone("Using file %1 with licenses", license_file)
×
1363

1364
        if UnpackLicenseTgzFileToDirectory(license_file, @tmpdir)
×
1365
          @license_dir = @tmpdir
×
1366
          @license_file_print = "license.tar.gz"
×
1367
        else
1368
          @license_dir = nil
×
1369
        end
1370

1371
        return
×
1372
      end
1373

1374
      Builtins.y2milestone(
3✔
1375
        "Licenses in %1... not supported",
1376
        license_file_location
1377
      )
1378

1379
      # New format didn't work, try the old one 1stMedia:/media.1/license.zip
1380
      @license_dir = @tmpdir
3✔
1381
      license_file = Pkg.SourceProvideDigestedFile(
3✔
1382
        src_id, # optional
1383
        1,
1384
        "/media.1/license.zip",
1385
        true
1386
      )
1387

1388
      # no license present
1389
      if license_file.nil?
3✔
1390
        Builtins.y2milestone("No license present")
3✔
1391
        @license_dir = nil
3✔
1392
        @tmpdir = nil
3✔
1393
        # return from the function
1394
        return
3✔
1395
      end
1396

1397
      Builtins.y2milestone("Product has a license")
×
1398
      out = Convert.to_map(
×
1399
        SCR.Execute(
1400
          path(".target.bash_output"),
1401
          Builtins.sformat(
1402
            "\n/usr/bin/rm -rf %1 && /usr/bin/mkdir -p %1 && cd %1 && " \
1403
            "/usr/bin/unzip -qqo #{license_file.shellescape}\n",
1404
            @tmpdir.shellescape
1405
          )
1406
        )
1407
      )
1408

1409
      # Extracting license failed, cannot accept the license
1410
      if Ops.get_integer(out, "exit", 0).nonzero?
×
1411
        Builtins.y2error("Cannot unzip license -> %1", out)
×
1412
        # popup error
1413
        Report.Error(
×
1414
          _("An error occurred while preparing the installation system.")
1415
        )
1416
        CleanUpLicense(@tmpdir)
×
1417
        @license_dir = nil
×
1418
      else
1419
        @license_dir = @tmpdir
×
1420
        @license_file_print = "/media.1/license.zip"
×
1421
      end
1422

UNCOV
1423
      nil
×
1424
    end
1425

1426
    def GetSourceLicenseDirectory(src_id, fallback_dir)
1✔
1427
      Builtins.y2milestone(
6✔
1428
        "Searching for licenses... (src_id: %1, fallback_dir: %2, mode: %3, stage: %4)",
1429
        src_id,
1430
        fallback_dir,
1431
        Mode.mode,
1432
        Stage.stage
1433
      )
1434

1435
      @license_file_print = nil
6✔
1436

1437
      # Bugzilla #299732
1438
      # Base Product - LiveCD installation
1439
      if Mode.live_installation
6✔
1440
        log.info "LiveCD Installation"
×
1441
        SearchForLicense_LiveCDInstallation(src_id, fallback_dir)
×
1442

1443
        # Base-product - license not in installation
1444
        #   * Stage is not initial
1445
        #   * source ID is not defined
1446
      elsif !Stage.initial && src_id.nil?
6✔
1447
        log.info "Base product, not in initial stage"
×
1448
        SearchForLicense_NormalRunBaseProduct(src_id, fallback_dir)
×
1449

1450
        # Base-product - first-stage installation
1451
        #   * Stage is initial
1452
        #   * Source ID is not set
1453
        # bugzilla #298342
1454
      elsif Stage.initial && src_id.nil?
6✔
1455
        log.info "Base product, initial stage"
3✔
1456
        SearchForLicense_FirstStageBaseProduct(base_product_id, fallback_dir)
3✔
1457

1458
        # Add-on-product license
1459
        #   * Source ID is set
1460
      elsif !src_id.nil? && Ops.greater_than(src_id, -1)
3✔
1461
        log.info "Add-On product"
3✔
1462
        SearchForLicense_AddOnProduct(src_id, fallback_dir)
3✔
1463

1464
        # Fallback
1465
      else
1466
        Builtins.y2warning(
×
1467
          "Source ID not defined, using fallback dir '%1'",
1468
          fallback_dir
1469
        )
1470
        @license_dir = fallback_dir
×
1471
      end
1472

1473
      Builtins.y2milestone(
6✔
1474
        "ProductLicense settings: license_dir: %1, tmpdir: %2, beta_file: %3",
1475
        @license_dir,
1476
        @tmpdir,
1477
        @beta_file
1478
      )
1479

1480
      display_beta(src_id) if @beta_file && !beta_seen?(src_id)
6✔
1481

1482
      nil
6✔
1483
    end
1484

1485
    # Finds out whether user needs to 'Agree to the license coming from a given source_id'
1486
    #
1487
    # @param [Any] id unique ID
1488
    # @param [String,nil] license_dir path to directory with unpacked licenses
1489
    def cache_license_acceptance_needed(id, license_dir)
1✔
1490
      # license_dir can be nil if there is no license present (e.g. DUDs)
1491
      return if license_dir.nil?
6✔
1492

1493
      license_acceptance_needed = !FileUtils.Exists("#{license_dir}/no-acceptance-needed")
2✔
1494
      SetAcceptanceNeeded(id, license_acceptance_needed)
2✔
1495
    end
1496

1497
    # @param licenses [ArgRef<Hash{String, String}>] a map $[ lang_code : filename ]
1498
    # @return [:cont,:auto]
1499
    def InitLicenseData(src_id, dir, licenses, available_langs,
1✔
1500
      _require_agreement, _license_ident, id)
1501
      # Downloads and unpacks all licenses for a given source ID
1502
      GetSourceLicenseDirectory(src_id, dir)
3✔
1503
      cache_license_acceptance_needed(id, @license_dir)
3✔
1504

1505
      licenses.value = LicenseFiles(@license_dir, @license_patterns)
3✔
1506

1507
      # all other 'licenses' could be replaced by this one
1508
      Ops.set(@all_licenses, id, licenses.value)
3✔
1509

1510
      return :auto if @beta_file.nil? && Builtins.size(licenses.value).zero?
3✔
1511

1512
      # Let's do getenv here. Language::language may not be initialized
1513
      # by now (see bnc#504803, c#28). Language::Language does only
1514
      # sysconfig reading, which is not too useful in cases like
1515
      # 'LANG=foo_BAR yast repositories'
1516
      language = EnvLangToLangCode(Builtins.getenv("LANG"))
×
1517

1518
      # Preferences how the client selects from available languages
1519
      langs = [
1520
        language,
×
1521
        Builtins.substring(language, 0, 2), # "it_IT" -> "it"
1522
        "en_US",
1523
        "en_GB",
1524
        "en",
1525
        ""
1526
      ] # license.txt fallback
1527
      available_langs.value = Builtins.maplist(licenses.value) do |lang, _fn|
×
1528
        lang
×
1529
      end
1530

1531
      # "en" is the same as "", we don't need to have them both
1532
      if Builtins.contains(available_langs.value, "en") &&
×
1533
          Builtins.contains(available_langs.value, "")
1534
        Builtins.y2milestone(
×
1535
          "Removing license fallback '' as we already have 'en'..."
1536
        )
1537
        available_langs.value = Builtins.filter(available_langs.value) do |one_lang|
×
1538
          one_lang != "en"
×
1539
        end
1540
      end
1541

1542
      Builtins.y2milestone("Preferred lang: %1", language)
×
1543
      return :auto if Builtins.size(available_langs.value).zero? # no license available
×
1544

1545
      @lic_lang = Builtins.find(langs) { |l| Builtins.haskey(licenses.value, l) }
×
1546
      @lic_lang = Ops.get(available_langs.value, 0, "") if @lic_lang.nil?
×
1547

1548
      Builtins.y2milestone("Preselected language: '%1'", @lic_lang)
×
1549

1550
      if @lic_lang.nil?
×
1551
        CleanUpLicense(@tmpdir) if !@tmpdir.nil?
×
1552
        return :auto
×
1553
      end
1554

1555
      # Check whether such license hasn't been already accepted
1556
      # Bugzilla #305503
1557
      license_ident_lang = nil
×
1558

1559
      # We need to store the original -- not localized license ID (if possible)
1560
      Builtins.foreach(["", "en", @lic_lang]) do |check_this|
×
1561
        if Builtins.contains(available_langs.value, check_this)
×
1562
          license_ident_lang = check_this
×
1563
          Builtins.y2milestone(
×
1564
            "Using localization '%1' (for license ID)",
1565
            license_ident_lang
1566
          )
1567
          raise Break
×
1568
        end
1569
      end
1570

1571
      # fallback
1572
      license_ident_lang = @lic_lang if license_ident_lang.nil?
×
1573

1574
      licenses_ref = arg_ref(licenses.value)
×
1575
      WhichLicenceFile(
×
1576
        license_ident_lang,
1577
        licenses_ref
1578
      )
1579
      licenses.value = licenses_ref.value
×
1580
      log.info "License needs to be shown"
×
1581

1582
      # bugzilla #303922
1583
      # src_id == nil (the initial product license)
1584
      if !src_id.nil?
×
1585
        # use wizard with steps
1586
        if Stage.initial
×
1587
          # Wizard::OpenNextBackStepsDialog();
1588
          # WorkflowManager::RedrawWizardSteps();
1589
          Builtins.y2milestone("Initial stage, not opening any window...")
×
1590
          # use normal wizard
1591
        else
1592
          Wizard.OpenNextBackDialog
×
1593
        end
1594
      end
1595

1596
      :cont
×
1597
    end
1598

1599
    # Should have been named 'UpdateLicenseContentBasedOnSelectedLanguage' :->
1600
    # @param licenses [ArgRef<Hash{String, String}>] a map $[ lang_code : filename ]
1601
    def UpdateLicenseContent(licenses, id)
1✔
1602
      # read the selected language
1603
      @lic_lang = Convert.to_string(
×
1604
        UI.QueryWidget(Id(Builtins.sformat("license_language_%1", id)), :Value)
1605
      )
1606
      rp_id = Id(Builtins.sformat("license_contents_rp_%1", id))
×
1607

1608
      licenses.value = Ops.get(@all_licenses, id, {}) if licenses.value == {}
×
1609

1610
      if UI.WidgetExists(rp_id)
×
1611
        UI.ReplaceWidget(
×
1612
          rp_id,
1613
          (
1614
            licenses_ref = arg_ref(licenses.value)
×
1615
            result = GetLicenseContent(
×
1616
              @lic_lang,
1617
              licenses_ref,
1618
              id
1619
            )
1620
            licenses.value = licenses_ref.value
×
1621
            result
×
1622
          )
1623
        )
1624
      else
1625
        Builtins.y2error("No such widget: %1", rp_id)
×
1626
      end
1627

1628
      # update displayed license URL after changing the license translation
1629
      update_license_location(@lic_lang, licenses)
×
1630

UNCOV
1631
      nil
×
1632
    end
1633

1634
    def AllLicensesAccepted
1✔
1635
      # BNC #448598
1636
      # If buttons don't exist, eula is automatically accepted
1637
      accepted = true
×
1638
      eula_id = nil
×
1639

1640
      Builtins.foreach(@license_ids) do |one_license_id|
×
1641
        if AcceptanceNeeded(one_license_id) != true
×
1642
          Builtins.y2milestone(
×
1643
            "License %1 does not need to be accepted",
1644
            one_license_id
1645
          )
1646
          next
×
1647
        end
1648
        eula_id = Builtins.sformat("eula_%1", one_license_id)
×
1649
        if UI.WidgetExists(Id(eula_id)) != true
×
1650
          Builtins.y2error("Widget %1 does not exist", eula_id)
×
1651
          next
×
1652
        end
1653

1654
        # All licenses have to be accepted
1655
        license_accepted = UI.QueryWidget(Id(eula_id), :Value)
×
1656

1657
        Builtins.y2milestone(
×
1658
          "License %1 accepted: %2",
1659
          eula_id,
1660
          license_accepted
1661
        )
1662

1663
        if !license_accepted
×
1664
          accepted = false
×
1665
          raise Break
×
1666
        end
1667
      end
1668

1669
      accepted
×
1670
    end
1671

1672
    def AllLicensesAcceptedOrDeclined
1✔
1673
      ret = true
×
1674

1675
      eula_id = nil
×
1676
      Builtins.foreach(@license_ids) do |one_license_id|
×
1677
        next if AcceptanceNeeded(one_license_id) != true
×
1678

1679
        eula_id = Builtins.sformat("eula_%1", one_license_id)
×
1680
        if UI.WidgetExists(Id(eula_id)) != true
×
1681
          Builtins.y2error("Widget %1 does not exist", eula_id)
×
1682
        end
1683
      end
1684

1685
      ret
×
1686
    end
1687

1688
    # Check if installation beta file had been seen to given ID
1689
    def beta_seen?(id)
1✔
1690
      @beta_file_already_seen.fetch(id, false)
2✔
1691
    end
1692

1693
    # Mark given id as seen
1694
    def beta_seen!(id)
1✔
1695
      @beta_file_already_seen[id] = true
2✔
1696
    end
1697

1698
    # Search for the product licenses in the addon repository.
1699
    # @param id [Integer] repository ID
1700
    # @param tmpdir [String] where to save the licenses
1701
    # @return [Boolean] true if a license has been found
1702
    def product_license(id, tmpdir)
1✔
1703
      # make sure the resolvables are loaded into the libzypp pool
1704
      Pkg.SourceLoad
6✔
1705
      ::FileUtils.mkdir_p(tmpdir)
6✔
1706

1707
      products = Y2Packager::Resolvable.find(kind: :product, source: id)
6✔
1708
      product_names = products.map(&:name).uniq
6✔
1709
      log.info("Found products from source #{id}: #{product_names.inspect}")
6✔
1710
      found_license = false
6✔
1711

1712
      product_names.each do |product_name|
6✔
1713
        # in some corner cases the repository might contain a broken product
1714
        # for which we get 'nil' locales (bsc#1155454)
1715
        locales = Pkg.PrdLicenseLocales(product_name) || []
3✔
1716
        log.info("License locales for product #{product_name.inspect}: #{locales.inspect}")
3✔
1717

1718
        locales.each do |locale|
3✔
1719
          # Pkg.PrdGetLicenseToConfirm returns the license in the installation
1720
          # language, not the default English license if the requested locale
1721
          # is an empty string (bsc#1160806)
1722
          license_locale = locale.empty? ? "en" : locale
2✔
1723
          license = Pkg.PrdGetLicenseToConfirm(product_name, license_locale)
2✔
1724
          next if license.nil? || license.empty?
2✔
1725

1726
          found_license = true
1✔
1727
          log.info("Found license for language #{locale.inspect}, size: #{license.bytesize} bytes")
1✔
1728

1729
          path = File.join(tmpdir, locale.empty? ? "license.txt" : "license.#{locale}.txt")
1✔
1730
          File.write(path, license)
1✔
1731
          log.info("License saved to #{path}")
1✔
1732
        end
1733

1734
        # we can display only one license at one time, there should be only one
1735
        # product per addon repository anyway
1736
        break if found_license
3✔
1737
      end
1738

1739
      if found_license
6✔
1740
        @license_dir = tmpdir
1✔
1741
        @license_file_print = repo_license_file(id)
1✔
1742
      end
1743

1744
      found_license
6✔
1745
    end
1746

1747
    # build the path to the repository license tarball
1748
    # @param id [Integer] repository ID
1749
    # @return [String] path
1750
    def repo_license_file(id)
1✔
1751
      repo = Pkg.SourceGeneralData(id)
1✔
1752
      # add the product dir and the license tarball to the path,
1753
      # the file name contains unique hash which we cannot get easily,
1754
      # use the "*" wildcard to make it simple
1755
      path = File.join(repo["product_dir"], "repodata", "*-license*.tar.gz")
1✔
1756
      log.info("License path: #{path}")
1✔
1757
      path
1✔
1758
    end
1759

1760
    # Return the default language to be used in licenses
1761
    #
1762
    # @return [String] Default language
1763
    def default_language
1✔
1764
      Yast::Stage.initial ? Yast::Language.preselected : Yast::Language.language
×
1765
    end
1766
  end
1767

1768
  ProductLicense = ProductLicenseClass.new
1✔
1769
  ProductLicense.main
1✔
1770
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc