/
mschtml.swift
882 lines (826 loc) · 37 KB
/
mschtml.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
//
// mschtml.swift
// Managed Software Center
//
// Created by Greg Neagle on 6/15/18.
// Copyright © 2018-2021 The Munki Project. All rights reserved.
//
import Cocoa
extension Array {
// Randomizes the order of an array's elements
mutating func shuffle() {
for _ in 0..<count {
sort { (_,_) in arc4random() < arc4random() }
}
}
}
func interfaceTheme() -> String {
// Returns "dark" if using Dark Mode, otherwise "light"
if #available(OSX 10.15, *) {
let appearanceDescription = NSApplication.shared.effectiveAppearance.debugDescription.lowercased()
if appearanceDescription.contains("dark") {
return "dark"
}
} else if #available(OSX 10.10, *) {
let os_vers = OperatingSystemVersion(majorVersion: 10, minorVersion: 14, patchVersion: 0)
if ProcessInfo().isOperatingSystemAtLeast(os_vers) || UserDefaults.standard.bool(forKey: "AllowDarkModeOnUnsupportedOSes") {
if let appleInterfaceStyle = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") {
if appleInterfaceStyle.lowercased().contains("dark") {
return "dark"
}
}
}
}
return "light"
}
func getRawTemplate(_ template_name: String) -> String {
// return a raw html template.
let customTemplatesPath = NSString.path(withComponents: [html_dir(), "custom/templates"])
let resourcesPath = Bundle.main.resourcePath ?? ""
let defaultTemplatesPath = NSString.path(withComponents: [resourcesPath, "templates"])
for directory in [customTemplatesPath, defaultTemplatesPath] {
let templatePath = NSString.path(withComponents: [directory, template_name])
var fileContents = ""
if FileManager.default.fileExists(atPath: templatePath) {
do {
fileContents = try NSString(
contentsOfFile: templatePath, encoding: String.Encoding.utf8.rawValue) as String
} catch {
return ""
}
return fileContents
}
}
return ""
}
func getTemplate(_ template_name: String) -> Template {
// return a Template object containing our html template
return Template(getRawTemplate(template_name))
}
func buildPage(_ filename: String) throws {
// Dispatch request to build a page to the appropriate function
msc_debug_log("build_page for \(filename)")
let name = (filename as NSString).deletingPathExtension
let parts = name.split(separator: "-", maxSplits: 1, omittingEmptySubsequences: false)
let key = parts[0]
let value = parts.count > 1 ? String(parts[1]) : ""
//msc_debug_log("buildPage key: \(key), value: \(value)")
switch key {
case "detail":
try buildDetailPage(item_name: value)
case "category":
try buildListPage(category: value)
case "categories":
try buildCategoriesPage()
case "filter":
try buildListPage(filter: value)
case "developer":
try buildListPage(developer: value)
case "myitems":
try buildMyItemsPage()
case "updates":
try buildUpdatesPage()
case "updatedetail":
try buildUpdateDetailPage(value)
default:
try buildItemNotFoundPage(filename)
}
}
func writePage(name: String, html: String) throws {
// write html to page_name in our local html directory
let html_file = NSString.path(withComponents: [html_dir(), name])
try html.write(toFile: html_file, atomically: true, encoding: .utf8)
}
func assemblePage(fromTemplate template_name: String,
usingItem pageItem: GenericItem,
additionalTemplates additional_subs: BaseItem = BaseItem() ) -> String {
// Returns HTML for our page from one or more templates
// and a dictionary of keys and values
// add current appearance style/theme
pageItem["data_theme"] = interfaceTheme()
// make sure our general labels are present
pageItem.addGeneralLabels()
// get our main template
let main_page = getTemplate(template_name)
// incorporate any sub-templates
let html_template = Template(main_page.substitute(additional_subs))
// now substitute page variables
return html_template.substitute(pageItem)
}
func generatePage(named page_name: String,
fromTemplate template_name: String,
usingItem pageItem: GenericItem,
additionalTemplates additional_subs: BaseItem = BaseItem()) throws {
// Assembles HTML and writes the page to page_name in our local html directory
msc_debug_log("generate_page for \(page_name)")
let html = assemblePage(fromTemplate: template_name,
usingItem: pageItem,
additionalTemplates: additional_subs)
try writePage(name: page_name, html: html)
}
func escapeQuotes(_ text: String) -> String {
//Escape single and double-quotes for JavaScript
return text.replacingOccurrences(of: "'", with: "\\'").replacingOccurrences(of: "\"", with: "\\\"")
}
func escapeHTML(_ text: String) -> String {
// Convert some problematic characters to entities
let html_escape_table = [
["&", "&"],
["\"", """],
["'", "'"],
[">", ">"],
["<", "<"],
]
var mutable_text = text
for subs in html_escape_table {
mutable_text = mutable_text.replacingOccurrences(of: subs[0], with: subs[1])
}
return mutable_text
}
extension GenericItem {
func escapeAndQuoteCommonFields() {
// Adds _escaped and _quoted versions of several commonly-used fields
my["name_escaped"] = escapeHTML(self["name"] as? String ?? "")
my["name_quoted"] = escapeQuotes(self["name"] as? String ?? "")
my["display_name_escaped"] = escapeHTML(self["display_name"] as? String ?? "")
my["developer_escaped"] = escapeHTML(self["developer"] as? String ?? "")
my["display_version_escaped"] = escapeHTML(self["display_version"] as? String ?? "")
if my["status"] as? String ?? "" == "will-be-removed" {
my["display_version_escaped_and_size"] = ""
} else {
my["display_version_escaped_and_size"] = (my["display_version_escaped"] as? String ?? "") + " • " + (my["size"] as? String ?? "")
}
}
func addGeneralLabels() {
// adds localized labels for Software, Categories, My Items and Updates to html pages
my["SoftwareLabel"] = NSLocalizedString("Software", comment: "Software label")
my["CategoriesLabel"] = NSLocalizedString("Categories", comment: "Categories label")
my["MyItemsLabel"] = NSLocalizedString("My Items", comment: "My Items label")
my["UpdatesLabel"] = NSLocalizedString("Updates", comment: "Updates label")
}
func addDetailSidebarLabels() {
// adds localized labels for the detail view sidebars
my["informationLabel"] = NSLocalizedString(
"Information", comment: "Sidebar Information label")
my["categoryLabel"] = NSLocalizedString(
"Category:", comment: "Sidebar Category label").trimmingCharacters(
in: CharacterSet(charactersIn: ":"))
my["versionLabel"] = NSLocalizedString(
"Version:", comment: "Sidebar Version label").trimmingCharacters(
in: CharacterSet(charactersIn: ":"))
my["sizeLabel"] = NSLocalizedString(
"Size:", comment: "Sidebar Size label").trimmingCharacters(
in: CharacterSet(charactersIn: ":"))
my["developerLabel"] = NSLocalizedString(
"Developer:", comment: "Sidebar Developer label").trimmingCharacters(
in: CharacterSet(charactersIn: ":"))
my["statusLabel"] = NSLocalizedString(
"Status:", comment: "Sidebar Status label").trimmingCharacters(
in: CharacterSet(charactersIn: ":"))
my["moreByDeveloperLabel"] = NSLocalizedString(
"More by %@", comment: "Sidebar More By Developer label")
my["moreInCategoryLabel"] = NSLocalizedString(
"More in %@", comment: "Sidebar More In Category label")
my["typeLabel"] = NSLocalizedString(
"Type", comment: "Sidebar Type label")
my["dueLabel"] = NSLocalizedString(
"Due:", comment: "Sidebar Due label").trimmingCharacters(
in: CharacterSet(charactersIn: ":"))
my["seeAllLocalizedString"] = NSLocalizedString("See All", comment: "See All link text")
}
func addMoreInCategory(of item: GenericItem, fromItems items: [GenericItem]) {
// make "More in CategoryFoo" list
let item_name = item["name"] as? String ?? ""
my["hide_more_in_category"] = "hidden"
var more_in_category_html = ""
//var excludeFromMoreByDeveloperNames = [String]()
if let category = item["category"] as? String {
let developer = item["developer"] as? String ?? "non-existent-developer-name"
my["category_link"] = "munki://category-\(quote(category)).html"
var more_in_category = items.filter(
{
( $0["category"] as? String == category &&
$0["name"] as? String != item_name &&
$0["status"] as? String != "installed" &&
$0["developer"] as? String != developer
)
}
)
if more_in_category.count > 0 {
my["hide_more_in_category"] = ""
let formatStr = my["moreInCategoryLabel"] as? NSString ?? ""
my["moreInCategoryLabel"] = NSString(format: formatStr, category)
more_in_category.shuffle()
more_in_category_html = buildItemListHTML(
Array(more_in_category[..<min(4, more_in_category.count)]))
}
}
//my["_excludeFromMoreByDeveloperNames"] = excludeFromMoreByDeveloperNames
my["more_in_category"] = more_in_category_html
}
func addMoreFromDeveloper(of item: GenericItem, fromItems items: [GenericItem]) {
// make "More by DeveloperFoo" list
let item_name = item["name"] as? String ?? ""
my["hide_more_by_developer"] = "hidden"
var more_by_developer_html = ""
var more_by_developer = [GenericItem]()
let excludeFromMoreByDeveloperNames = my["_excludeFromMoreByDeveloperNames"] as? [String] ?? []
my["_excludeFromMoreByDeveloperNames"] = nil
if let developer = item["developer"] as? String {
my["developer_link"] = "munki://developer-\(quote(developer)).html"
more_by_developer = items.filter(
{
( !excludeFromMoreByDeveloperNames.contains($0["name"] as? String ?? "") &&
$0["developer"] as? String == developer &&
$0["name"] as? String != item_name &&
$0["status"] as? String != "installed"
)
}
)
if more_by_developer.count > 0 {
my["hide_more_by_developer"] = ""
let formatStr = my["moreByDeveloperLabel"] as? NSString ?? ""
my["moreByDeveloperLabel"] = NSString(format: formatStr, developer)
more_by_developer.shuffle()
more_by_developer_html = buildItemListHTML(
Array(more_by_developer[..<min(4, more_by_developer.count)]))
}
}
my["more_by_developer"] = more_by_developer_html
}
}
func buildItemNotFoundPage(_ page_name: String) throws {
// Build item not found page
let page = GenericItem([String:String]())
page["item_not_found_title"] = NSLocalizedString(
"Not Found", comment: "Item Not Found title")
page["item_not_found_message"] = NSLocalizedString(
"Cannot display the requested item.", comment: "Item Not Found message")
let footer = getRawTemplate("footer_template.html")
try generatePage(named: page_name,
fromTemplate: "page_not_found_template.html",
usingItem: page,
additionalTemplates: BaseItem(["footer": footer]))
}
func buildDetailPage(item_name: String) throws {
// Build page showing detail for a single optional item
msc_debug_log("buildDetailPage for \(item_name)")
let items = getOptionalInstallItems()
let page_name = "detail-\(item_name).html"
for item in items {
if item["name"] as? String == item_name {
// make a copy of the item to use to build our page
let page = OptionalItem(item)
page.escapeAndQuoteCommonFields()
page.addDetailSidebarLabels()
page.addMoreInCategory(of: item, fromItems: items)
page.addMoreFromDeveloper(of: item, fromItems: items)
// might need better logic here eventually
page["dueLabel"] = ""
page["short_due_date"] = ""
item["hide_cancel_button"] = ""
let footer = getRawTemplate("footer_template.html")
try generatePage(named: page_name,
fromTemplate: "detail_template.html",
usingItem: page,
additionalTemplates: BaseItem(["footer": footer]))
return
}
}
msc_debug_log("No detail found for \(item_name)")
try buildItemNotFoundPage(page_name)
}
func buildListPage(category: String = "",
developer: String = "",
filter: String = "") throws {
// Build page listing available optional items
var category = category
let items = getOptionalInstallItems()
var header = NSLocalizedString("All items", comment: "AllItemsHeaderText")
var page_name = "category-all.html"
if category == "all" {
category = ""
}
if !category.isEmpty {
header = category
page_name = "category-\(category).html"
} else if !developer.isEmpty {
header = developer
page_name = "developer-\(developer).html"
} else if !filter.isEmpty {
header = "Search results for \(filter)" //TO-DO: localize?
page_name = "filter-\(filter).html"
}
msc_debug_log("page name: \(page_name)")
let featured_items = items.filter({ $0["featured"] as? Bool == true })
if !featured_items.isEmpty && category.isEmpty && developer.isEmpty && filter.isEmpty {
header = NSLocalizedString("Featured items",
comment: "FeaturedItemsHeaderText")
}
// make HTML for Categories pop-up menu
var all_categories_label = NSLocalizedString("All Categories",
comment: "AllCategoriesLabel")
if !featured_items.isEmpty {
all_categories_label = NSLocalizedString("Featured", comment: "FeaturedLabel")
}
var categories_html = "<option>\(all_categories_label)</option>\n"
if category.isEmpty {
categories_html = "<option selected>\(all_categories_label)</option>\n"
}
var category_set = Set<String>()
for item in items {
if let category_name = item["category"] as? String {
category_set.insert(category_name)
}
}
for item in Array(category_set).sorted() {
if item == category {
categories_html += "<option selected>\(item)</option>\n"
} else {
categories_html += "<option>\(item)</option>\n"
}
}
// make HTML for list of categories
var categories_html_list = ""
for item in Array(category_set).sorted() {
categories_html_list += (
"<li class=\"link\"><a href=\"munki://category-\(quote(item)).html\">\(item)</a></li>\n")
}
let item_hmtl = buildListPageItemsHTML(
category: category, developer: developer, filter: filter)
// assemble!
let page = GenericItem()
page["list_items"] = item_hmtl
page["category_items"] = categories_html
page["category_list"] = categories_html_list
page["header_text"] = header
let more_templates = BaseItem()
more_templates["showcase"] = getRawTemplate("showcase_template.html")
/*if category.isEmpty && filter.isEmpty && developer.isEmpty {
more_templates["showcase"] = getRawTemplate("showcase_template.html")
} else {
more_templates["showcase"] = ""
}*/
more_templates["sidebar"] = getRawTemplate("sidebar_template.html")
more_templates["footer"] = getRawTemplate("footer_template.html")
try generatePage(named: page_name,
fromTemplate: "list_template.html",
usingItem: page,
additionalTemplates: more_templates)
}
func buildItemListHTML(_ items: [GenericItem],
template: String = "list_item_template.html",
sort: Bool = true) -> String {
var item_html = ""
var sorted_items: [GenericItem] = []
let item_template = getTemplate(template)
if sort {
// sort items by display_name_lowercase
sorted_items = items.sorted(by:
{ $0["display_name_lower"] as? String ?? "" < $1["display_name_lower"] as? String ?? "" })
} else {
sorted_items = items
}
for item in sorted_items {
item.escapeAndQuoteCommonFields()
let category_and_developer = item["category_and_developer"] as? String ?? ""
item["category_and_developer_escaped"] = escapeHTML(category_and_developer)
item_html += item_template.substitute(item)
}
return item_html
}
func buildListPageItemsHTML(category: String = "",
developer: String = "",
filter: String = "") -> String {
// Returns HTML for the items on the list page
var items = getOptionalInstallItems()
var item_html = ""
if !filter.isEmpty {
msc_debug_log("Filtering on \(filter)")
let lc_filter = filter.lowercased()
items = items.filter(
{
($0["display_name"] as? String ?? "").lowercased().contains(lc_filter) ||
($0["description"] as? String ?? "").lowercased().contains(lc_filter) ||
($0["developer"] as? String ?? "").lowercased().contains(lc_filter) ||
($0["category"] as? String ?? "").lowercased().contains(lc_filter)
}
)
}
if !category.isEmpty {
items = items.filter(
{ ($0["category"] as? String ?? "").lowercased() == category.lowercased() }
)
}
if !developer.isEmpty {
items = items.filter(
{ ($0["developer"] as? String ?? "").lowercased() == developer.lowercased() }
)
}
if category.isEmpty && developer.isEmpty && filter.isEmpty {
// this is the default (formerly) "all items" view
// look for featured items and display those if we have them
let featured_items = items.filter(
{ pythonishBool($0["featured"]) == true }
)
if !featured_items.isEmpty {
items = featured_items
}
}
if !items.isEmpty {
item_html = buildItemListHTML(items)
} else {
// no items; build appropriate alert messages
let status_results_template = getTemplate("status_results_template.html")
let alert = BaseItem()
if !filter.isEmpty {
alert["primary_status_text"] = NSLocalizedString(
"Your search had no results.",
comment: "No Search Results primary text")
alert["secondary_status_text"] = NSLocalizedString(
"Try searching again.", comment: "No Search Results secondary text")
} else if !category.isEmpty {
alert["primary_status_text"] = NSLocalizedString(
"There are no items in this category.",
comment: "No Category Results primary text")
alert["secondary_status_text"] = NSLocalizedString(
"Try selecting another category.",
comment: "No Category Results secondary text")
} else if !developer.isEmpty {
alert["primary_status_text"] = NSLocalizedString(
"There are no items from this developer.",
comment: "No Developer Results primary text")
alert["secondary_status_text"] = NSLocalizedString(
"Try selecting another developer.",
comment: "No Developer Results secondary text")
} else {
alert["primary_status_text"] = NSLocalizedString(
"There are no available software items.",
comment: "No Items primary text")
alert["secondary_status_text"] = NSLocalizedString(
"Try again later.",
comment: "No Items secondary text")
}
alert["hide_progress_bar"] = "hidden"
alert["progress_bar_value"] = ""
item_html = status_results_template.substitute(alert)
}
return item_html
}
func buildCategoriesPage() throws {
// Build page showing available categories and some items in each one
let all_items = getOptionalInstallItems()
var category_list = Set<String>()
for item in all_items {
if let category = item["category"] as? String {
category_list.insert(category)
}
}
let all_categories_label = NSLocalizedString("All Categories", comment: "AllCategoriesLabel")
var categories_html = "<option selected>\(all_categories_label)</option>\n"
for item in category_list.sorted() {
categories_html += "<option>\(item)</option>\n"
}
let page = GenericItem()
page["list_items"] = buildCategoryItemsHTML()
page["category_items"] = categories_html
page["header_text"] = NSLocalizedString("Categories", comment: "Categories label")
let footer = getRawTemplate("footer_template.html")
let additional_templates = BaseItem(
["showcase": "<div class=\"showcase-empty-placeholder\"></div>",
"sidebar": "",
"footer": footer]
)
try generatePage(named: "categories.html",
fromTemplate: "list_template.html",
usingItem: page,
additionalTemplates: additional_templates)
}
func buildCategoryItemsHTML() -> String {
// Returns HTML for the items on the Categories page
var item_html = ""
let all_items = getOptionalInstallItems()
var category_list = Set<String>()
for item in all_items {
if let category = item["category"] as? String {
category_list.insert(category)
}
}
if all_items.isEmpty {
// no items
let status_results_template = getTemplate("status_results_template.html")
let alert = BaseItem()
alert["primary_status_text"] = NSLocalizedString(
"There are no available software items.",
comment: "No Items primary text")
alert["secondary_status_text"] = NSLocalizedString(
"Try again later.",
comment: "No Items secondary text")
alert["hide_progress_bar"] = "hidden"
alert["progress_bar_value"] = ""
item_html = status_results_template.substitute(alert)
} else {
let item_template = getTemplate("category_item_template.html")
for category in category_list.sorted() {
let category_data = BaseItem()
category_data["category_name_escaped"] = escapeHTML(category)
category_data["category_link"] = "munki://category-\(quote(category)).html"
var category_items = all_items.filter(
{ ($0["category"] as? String ?? "") == category }
)
category_items.shuffle()
category_data["item1_icon"] = category_items[0]["icon"] as? String ?? ""
category_data["item1_display_name_escaped"] = escapeHTML(
category_items[0]["display_name"] as? String ?? "")
category_data["item1_detail_link"] = category_items[0]["detail_link"] as? String ?? ""
if category_items.count > 1 {
category_data["item2_display_name_escaped"] = escapeHTML(
category_items[1]["display_name"] as? String ?? "")
category_data["item2_detail_link"] = category_items[1]["detail_link"] as? String ?? ""
} else {
category_data["item2_display_name_escaped"] = ""
category_data["item2_detail_link"] = "#"
}
if category_items.count > 2 {
category_data["item3_display_name_escaped"] = escapeHTML(
category_items[2]["display_name"] as? String ?? "")
category_data["item3_detail_link"] = category_items[2]["detail_link"] as? String ?? ""
} else {
category_data["item3_display_name_escaped"] = ""
category_data["item3_detail_link"] = "#"
}
item_html += item_template.substitute(category_data)
}
}
return item_html
}
func buildMyItemsPage() throws {
// Builds "My Items" page, which shows all current optional items the user has chosen
let page = GenericItem()
page["my_items_header_label"] = NSLocalizedString(
"My Items", comment: "My Items label")
page["myitems_rows"] = buildMyItemsRows()
let additional_templates = BaseItem(["footer": getRawTemplate("footer_template.html")])
try generatePage(named: "myitems.html",
fromTemplate: "myitems_template.html",
usingItem: page,
additionalTemplates: additional_templates)
}
func buildMyItemsRows() -> String {
// Returns HTML for the items on the 'My Items' page
var myitems_rows = ""
let item_list = getMyItemsList()
if !item_list.isEmpty {
myitems_rows = buildItemListHTML(item_list,
template: "myitems_item_template.html")
} else {
let status_results_template = getTemplate("status_results_template.html")
let alert = BaseItem()
alert["primary_status_text"] = NSLocalizedString(
"You have no selected software.",
comment: "No Installed Software primary text")
let select_software_msg = NSLocalizedString(
"Select software to install.",
comment: "No Installed Software secondary text")
alert["secondary_status_text"] = (
"<a href=\"munki://category-all.html\">\(select_software_msg)</a>" )
alert["hide_progress_bar"] = "hidden"
myitems_rows = status_results_template.substitute(alert)
}
return myitems_rows
}
func buildUpdatesPage() throws {
// available/pending updates
if (NSApp.delegate! as! AppDelegate).mainWindowController._update_in_progress {
try buildUpdateStatusPage()
return
}
let filterAppleUpdates = (NSApp.delegate! as! AppDelegate).mainWindowController.should_filter_apple_updates
let item_list = getEffectiveUpdateList(filterAppleUpdates)
for item in item_list {
item["added_class"] = ""
if item["note"] == nil {
item["note"] = ""
}
}
let problem_updates = getProblemItems()
for item in problem_updates {
item["added_class"] = ""
item["hide_cancel_button"] = "hidden"
}
// find any optional installs with update available
var other_updates = getOptionalInstallItems().filter(
{ ($0["status"] as? String ?? "") == "update-available" }
)
// find any listed optional install updates that require a higher OS
// or have insufficient disk space or other blockers (because they have a
// note)
let blocked_optional_updates = getOptionalInstallItems().filter(
{
(($0["status"] as? String ?? "") == "installed" &&
!(($0["note"] as? String ?? "").isEmpty))
}
)
for item in blocked_optional_updates {
item["hide_cancel_button"] = "hidden"
}
other_updates += blocked_optional_updates
for item in other_updates {
item["added_class"] = ""
}
let page = GenericItem()
page["update_rows"] = ""
page["hide_progress_spinner"] = "hidden"
page["hide_problem_updates"] = "hidden"
page["hide_other_updates"] = "hidden"
page["install_all_button_classes"] = ""
if item_list.isEmpty && other_updates.isEmpty && problem_updates.isEmpty {
let status_results_template = getTemplate("status_results_template.html")
let alert = BaseItem()
alert["primary_status_text"] = NSLocalizedString(
"Your software is up to date.", comment: "No Pending Updates primary text")
alert["secondary_status_text"] = NSLocalizedString(
"There is no new software for your computer at this time.",
comment: "No Pending Updates secondary text")
alert["hide_progress_bar"] = "hidden"
alert["progress_bar_value"] = ""
page["update_rows"] = status_results_template.substitute(alert)
} else {
if !item_list.isEmpty {
page["update_rows"] = buildItemListHTML(
item_list, template: "update_item_template.html", sort: false)
}
}
let count = item_list.count
// in Python was count = len([item for item in item_list if item['status'] != 'problem-item'])
page["update_count"] = updateCountMessage(count)
page["install_btn_label"] = getInstallAllButtonTextForCount(count)
page["warning_text"] = getWarningText(filterAppleUpdates)
// build problem updates table
page["problem_updates_header_message"] = NSLocalizedString(
"Problem updates", comment: "Problem Updates label")
page["problem_update_rows"] = ""
if !problem_updates.isEmpty {
page["hide_problem_updates"] = ""
page["problem_update_rows"] = buildItemListHTML(
problem_updates, template: "update_item_template.html")
}
// build other available updates table
page["other_updates_header_message"] = NSLocalizedString(
"Other available updates",
comment: "Other Available Updates label")
page["other_update_rows"] = ""
if !other_updates.isEmpty {
page["hide_other_updates"] = ""
page["other_update_rows"] = buildItemListHTML(
other_updates, template: "update_item_template.html")
}
let additional_templates = BaseItem(["footer": getRawTemplate("footer_template.html")])
try generatePage(named: "updates.html",
fromTemplate: "updates_template.html",
usingItem: page,
additionalTemplates: additional_templates)
}
func buildUpdateStatusPage() throws {
// generates our update status page
let status_title_default = NSLocalizedString("Checking for updates...",
comment: "Checking For Updates message")
let page = GenericItem()
page["update_rows"] = ""
page["hide_progress_spinner"] = ""
page["hide_problem_updates"] = "hidden"
page["hide_other_updates"] = "hidden"
page["other_updates_header_message"] = ""
page["other_update_rows"] = ""
// don't like this bit as it ties us to a different object
guard let status_controller = (NSApp.delegate as? AppDelegate)?.statusController else {
msc_debug_log("Could not get statusController object in buildUpdateStatusPage")
return
}
guard let main_window_controller = (NSApp.delegate as? AppDelegate)?.mainWindowController else {
msc_debug_log("Could not get mainWindowController object in buildUpdateStatusPage")
return
}
let status_results_template = getTemplate("status_results_template.html")
let alert = BaseItem()
if status_controller._status_message.isEmpty {
alert["primary_status_text"] = NSLocalizedString("Update in progress.",
comment: "Update In Progress primary text")
} else {
alert["primary_status_text"] = status_controller._status_message
}
if status_controller._status_detail.isEmpty {
alert["secondary_status_text"] = " "
} else {
alert["secondary_status_text"] = status_controller._status_detail
}
alert["hide_progress_bar"] = ""
if status_controller._status_percent < 0 {
alert["progress_bar_attributes"] = "class=\"indeterminate\""
} else {
alert["progress_bar_attributes"] = "style=\"width: \(status_controller._status_percent)%\""
}
page["update_rows"] = status_results_template.substitute(alert)
var install_all_button_classes = [String]()
if status_controller._status_stopBtnHidden {
install_all_button_classes.append("hidden")
}
if status_controller._status_stopBtnDisabled {
install_all_button_classes.append("disabled")
}
page["install_all_button_classes"] = install_all_button_classes.joined(separator: " ")
// don't like this bit as it ties us to yet another object
page["update_count"] = !main_window_controller._status_title.isEmpty ? main_window_controller._status_title : status_title_default
page["install_btn_label"] = NSLocalizedString("Cancel", comment: "Cancel button title/short action text")
page["warning_text"] = ""
let additional_templates = BaseItem(["footer": getRawTemplate("footer_template.html")])
try generatePage(named: "updates.html",
fromTemplate: "updates_template.html",
usingItem: page,
additionalTemplates: additional_templates)
}
func getRestartActionForUpdateList(_ update_list: [GenericItem]) -> String {
// Returns a localized overall restart action message for the list of updates
if update_list.isEmpty {
return ""
}
let restart_items = update_list.filter(
{ ($0["RestartAction"] as? String ?? "").contains("Restart") }
)
if !restart_items.isEmpty {
// found at least one item containing 'Restart' in its RestartAction
return NSLocalizedString("Restart Required", comment: "Restart Required title")
}
let logout_items = update_list.filter(
{ ($0["RestartAction"] as? String ?? "").contains("Logout") }
)
if !logout_items.isEmpty {
// found at least one item containing 'Logout' in its RestartAction
return NSLocalizedString("Logout Required", comment: "Logout Required title")
}
return ""
}
func getWarningText(_ filterAppleUpdates: Bool) -> String {
// Return localized text warning about forced installs and/or
// logouts and/or restarts
let item_list = getEffectiveUpdateList()
var warning_text = ""
//if let forced_install_date = earliestForceInstallDate(item_list) {
if let forced_install_date = earliestForceInstallDate() {
let date_str = stringFromDate(forced_install_date)
let forced_date_text = NSLocalizedString(
"One or more items must be installed by %@",
comment: "Forced Install Date summary")
warning_text = NSString(format: forced_date_text as NSString, date_str) as String
} else if !filterAppleUpdates && shouldAggressivelyNotifyAboutAppleUpdates() {
warning_text = NSLocalizedString(
"One or more important Apple updates must be installed",
comment: "Pending Apple Updates warning"
)
}
let restart_text = getRestartActionForUpdateList(item_list)
if !restart_text.isEmpty {
if warning_text.isEmpty {
warning_text = restart_text
} else {
warning_text += " • " + restart_text
}
}
return warning_text
}
func buildUpdateDetailPage(_ identifier: String) throws {
// Build detail page for a non-optional update
let items = getUpdateList() + getProblemItems()
let components = (identifier as NSString).components(separatedBy: "--version-")
let name = components[0]
let version = (components.count > 1 ? components[1] : "")
let page_name = "updatedetail-\(identifier).html"
for item in items {
if ((item["name"] as? String ?? "") == name &&
(item["version_to_install"] as? String ?? "") == version) {
let page = UpdateItem(item)
page.escapeAndQuoteCommonFields()
page.addDetailSidebarLabels()
page.addMoreInCategory(of: item, fromItems: getOptionalInstallItems())
page.addMoreFromDeveloper(of: item, fromItems: getOptionalInstallItems())
if let force_install_after_date = item["force_install_after_date"] as? Date {
let local_date = discardTimeZoneFromDate(force_install_after_date)
let date_str = shortRelativeStringFromDate(local_date)
page["dueLabel"] = (page["dueLabel"] as? String ?? "") + " "
page["short_due_date"] = date_str
} else {
page["dueLabel"] = ""
page["short_due_date"] = ""
}
let additional_templates = BaseItem(["footer": getRawTemplate("footer_template.html")])
try generatePage(named: page_name,
fromTemplate: "detail_template.html",
usingItem: page,
additionalTemplates: additional_templates)
return
}
}
// if we get here we didn't find any item matching identifier
msc_debug_log("No update detail found for \(identifier)")
try buildItemNotFoundPage(page_name)
}