Use dcp::File in DCP-o-matic (#2231).
[dcpomatic.git] / src / wx / config_dialog.cc
1 /*
2     Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21
22 #include "audio_mapping_view.h"
23 #include "check_box.h"
24 #include "config_dialog.h"
25 #include "dcpomatic_button.h"
26 #include "nag_dialog.h"
27 #include "static_text.h"
28 #include <dcp/file.h>
29 #include <dcp/raw_convert.h>
30
31
32 using std::function;
33 using std::make_pair;
34 using std::make_shared;
35 using std::map;
36 using std::pair;
37 using std::shared_ptr;
38 using std::string;
39 using std::vector;
40 using boost::bind;
41 using boost::optional;
42 #if BOOST_VERSION >= 106100
43 using namespace boost::placeholders;
44 #endif
45
46
47 static
48 bool
49 do_nothing ()
50 {
51         return false;
52 }
53
54 Page::Page (wxSize panel_size, int border)
55         : _border (border)
56         , _panel (0)
57         , _panel_size (panel_size)
58         , _window_exists (false)
59 {
60         _config_connection = Config::instance()->Changed.connect (bind (&Page::config_changed_wrapper, this));
61 }
62
63
64 wxWindow*
65 Page::CreateWindow (wxWindow* parent)
66 {
67         return create_window (parent);
68 }
69
70
71 wxWindow*
72 Page::create_window (wxWindow* parent)
73 {
74         _panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
75         auto s = new wxBoxSizer (wxVERTICAL);
76         _panel->SetSizer (s);
77
78         setup ();
79         _window_exists = true;
80         config_changed ();
81
82         _panel->Bind (wxEVT_DESTROY, bind (&Page::window_destroyed, this));
83
84         return _panel;
85 }
86
87 void
88 Page::config_changed_wrapper ()
89 {
90         if (_window_exists) {
91                 config_changed ();
92         }
93 }
94
95 void
96 Page::window_destroyed ()
97 {
98         _window_exists = false;
99 }
100
101
102 GeneralPage::GeneralPage (wxSize panel_size, int border)
103         : Page (panel_size, border)
104 {
105
106 }
107
108
109 wxString
110 GeneralPage::GetName () const
111 {
112         return _("General");
113 }
114
115
116 void
117 GeneralPage::add_language_controls (wxGridBagSizer* table, int& r)
118 {
119         _set_language = new CheckBox (_panel, _("Set language"));
120         table->Add (_set_language, wxGBPosition (r, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
121         _language = new wxChoice (_panel, wxID_ANY);
122         vector<pair<string, string>> languages;
123         languages.push_back (make_pair("Čeština", "cs_CZ"));
124         languages.push_back (make_pair("汉语/漢語", "zh_CN"));
125         languages.push_back (make_pair("Dansk", "da_DK"));
126         languages.push_back (make_pair("Deutsch", "de_DE"));
127         languages.push_back (make_pair("English", "en_GB"));
128         languages.push_back (make_pair("Español", "es_ES"));
129         languages.push_back (make_pair("Français", "fr_FR"));
130         languages.push_back (make_pair("Italiano", "it_IT"));
131         languages.push_back (make_pair("Nederlands", "nl_NL"));
132         languages.push_back (make_pair("Русский", "ru_RU"));
133         languages.push_back (make_pair("Polski", "pl_PL"));
134         languages.push_back (make_pair("Português europeu", "pt_PT"));
135         languages.push_back (make_pair("Português do Brasil", "pt_BR"));
136         languages.push_back (make_pair("Svenska", "sv_SE"));
137         languages.push_back (make_pair("Slovenský jazyk", "sk_SK"));
138         // languages.push_back (make_pair("Türkçe", "tr_TR"));
139         languages.push_back (make_pair("українська мова", "uk_UA"));
140         checked_set (_language, languages);
141         table->Add (_language, wxGBPosition (r, 1));
142         ++r;
143
144         auto restart = add_label_to_sizer (
145                 table, _panel, _("(restart DCP-o-matic to see language changes)"), false, wxGBPosition (r, 0), wxGBSpan (1, 2)
146                 );
147         wxFont font = restart->GetFont();
148         font.SetStyle (wxFONTSTYLE_ITALIC);
149         font.SetPointSize (font.GetPointSize() - 1);
150         restart->SetFont (font);
151         ++r;
152
153         _set_language->Bind (wxEVT_CHECKBOX, bind (&GeneralPage::set_language_changed, this));
154         _language->Bind     (wxEVT_CHOICE,   bind (&GeneralPage::language_changed,     this));
155 }
156
157 void
158 GeneralPage::add_update_controls (wxGridBagSizer* table, int& r)
159 {
160         _check_for_updates = new CheckBox (_panel, _("Check for updates on startup"));
161         table->Add (_check_for_updates, wxGBPosition (r, 0), wxGBSpan (1, 2));
162         ++r;
163
164         _check_for_test_updates = new CheckBox (_panel, _("Check for testing updates on startup"));
165         table->Add (_check_for_test_updates, wxGBPosition (r, 0), wxGBSpan (1, 2));
166         ++r;
167
168         _check_for_updates->Bind (wxEVT_CHECKBOX, bind (&GeneralPage::check_for_updates_changed, this));
169         _check_for_test_updates->Bind (wxEVT_CHECKBOX, bind (&GeneralPage::check_for_test_updates_changed, this));
170 }
171
172 void
173 GeneralPage::config_changed ()
174 {
175         auto config = Config::instance ();
176
177         checked_set (_set_language, static_cast<bool>(config->language()));
178
179         /* Backwards compatibility of config file */
180
181         map<string, string> compat_map;
182         compat_map["fr"] = "fr_FR";
183         compat_map["it"] = "it_IT";
184         compat_map["es"] = "es_ES";
185         compat_map["sv"] = "sv_SE";
186         compat_map["de"] = "de_DE";
187         compat_map["nl"] = "nl_NL";
188         compat_map["ru"] = "ru_RU";
189         compat_map["pl"] = "pl_PL";
190         compat_map["da"] = "da_DK";
191         compat_map["pt"] = "pt_PT";
192         compat_map["sk"] = "sk_SK";
193         compat_map["cs"] = "cs_CZ";
194         compat_map["uk"] = "uk_UA";
195
196         auto lang = config->language().get_value_or("en_GB");
197         if (compat_map.find(lang) != compat_map.end ()) {
198                 lang = compat_map[lang];
199         }
200
201         checked_set (_language, lang);
202
203         checked_set (_check_for_updates, config->check_for_updates ());
204         checked_set (_check_for_test_updates, config->check_for_test_updates ());
205
206         setup_sensitivity ();
207 }
208
209 void
210 GeneralPage::setup_sensitivity ()
211 {
212         _language->Enable (_set_language->GetValue ());
213         _check_for_test_updates->Enable (_check_for_updates->GetValue ());
214 }
215
216 void
217 GeneralPage::set_language_changed ()
218 {
219         setup_sensitivity ();
220         if (_set_language->GetValue ()) {
221                 language_changed ();
222         } else {
223                 Config::instance()->unset_language ();
224         }
225 }
226
227 void
228 GeneralPage::language_changed ()
229 {
230         int const sel = _language->GetSelection ();
231         if (sel != -1) {
232                 Config::instance()->set_language (string_client_data (_language->GetClientObject (sel)));
233         } else {
234                 Config::instance()->unset_language ();
235         }
236 }
237
238 void
239 GeneralPage::check_for_updates_changed ()
240 {
241         Config::instance()->set_check_for_updates (_check_for_updates->GetValue ());
242 }
243
244 void
245 GeneralPage::check_for_test_updates_changed ()
246 {
247         Config::instance()->set_check_for_test_updates (_check_for_test_updates->GetValue ());
248 }
249
250 CertificateChainEditor::CertificateChainEditor (
251         wxWindow* parent,
252         wxString title,
253         int border,
254         function<void (shared_ptr<dcp::CertificateChain>)> set,
255         function<shared_ptr<const dcp::CertificateChain> (void)> get,
256         function<bool (void)> nag_alter
257         )
258         : wxDialog (parent, wxID_ANY, title)
259         , _set (set)
260         , _get (get)
261         , _nag_alter (nag_alter)
262 {
263         _sizer = new wxBoxSizer (wxVERTICAL);
264
265         auto certificates_sizer = new wxBoxSizer (wxHORIZONTAL);
266         _sizer->Add (certificates_sizer, 0, wxALL, border);
267
268         _certificates = new wxListCtrl (this, wxID_ANY, wxDefaultPosition, wxSize (440, 150), wxLC_REPORT | wxLC_SINGLE_SEL);
269
270         {
271                 wxListItem ip;
272                 ip.SetId (0);
273                 ip.SetText (_("Type"));
274                 ip.SetWidth (100);
275                 _certificates->InsertColumn (0, ip);
276         }
277
278         {
279                 wxListItem ip;
280                 ip.SetId (1);
281                 ip.SetText (_("Thumbprint"));
282                 ip.SetWidth (340);
283
284                 wxFont font = ip.GetFont ();
285                 font.SetFamily (wxFONTFAMILY_TELETYPE);
286                 ip.SetFont (font);
287
288                 _certificates->InsertColumn (1, ip);
289         }
290
291         certificates_sizer->Add (_certificates, 1, wxEXPAND);
292
293         {
294                 auto s = new wxBoxSizer (wxVERTICAL);
295                 _add_certificate = new Button (this, _("Add..."));
296                 s->Add (_add_certificate, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
297                 _remove_certificate = new Button (this, _("Remove"));
298                 s->Add (_remove_certificate, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
299                 _export_certificate = new Button (this, _("Export certificate..."));
300                 s->Add (_export_certificate, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
301                 _export_chain = new Button (this, _("Export chain..."));
302                 s->Add (_export_chain, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
303                 certificates_sizer->Add (s, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP);
304         }
305
306         auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
307         _sizer->Add (table, 1, wxALL | wxEXPAND, border);
308         int r = 0;
309
310         add_label_to_sizer (table, this, _("Leaf private key"), true, wxGBPosition (r, 0));
311         _private_key = new StaticText (this, wxT(""));
312         wxFont font = _private_key->GetFont ();
313         font.SetFamily (wxFONTFAMILY_TELETYPE);
314         _private_key->SetFont (font);
315         table->Add (_private_key, wxGBPosition (r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
316         _import_private_key = new Button (this, _("Import..."));
317         table->Add (_import_private_key, wxGBPosition (r, 2));
318         _export_private_key = new Button (this, _("Export..."));
319         table->Add (_export_private_key, wxGBPosition (r, 3));
320         ++r;
321
322         _button_sizer = new wxBoxSizer (wxHORIZONTAL);
323         _remake_certificates = new Button (this, _("Re-make certificates and key..."));
324         _button_sizer->Add (_remake_certificates, 1, wxRIGHT, border);
325         table->Add (_button_sizer, wxGBPosition (r, 0), wxGBSpan (1, 4));
326         ++r;
327
328         _private_key_bad = new StaticText (this, _("Leaf private key does not match leaf certificate!"));
329         font = *wxSMALL_FONT;
330         font.SetWeight (wxFONTWEIGHT_BOLD);
331         _private_key_bad->SetFont (font);
332         table->Add (_private_key_bad, wxGBPosition (r, 0), wxGBSpan (1, 3));
333         ++r;
334
335         _add_certificate->Bind     (wxEVT_BUTTON,       bind (&CertificateChainEditor::add_certificate, this));
336         _remove_certificate->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::remove_certificate, this));
337         _export_certificate->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::export_certificate, this));
338         _certificates->Bind        (wxEVT_LIST_ITEM_SELECTED,   bind (&CertificateChainEditor::update_sensitivity, this));
339         _certificates->Bind        (wxEVT_LIST_ITEM_DESELECTED, bind (&CertificateChainEditor::update_sensitivity, this));
340         _remake_certificates->Bind (wxEVT_BUTTON,       bind (&CertificateChainEditor::remake_certificates, this));
341         _export_chain->Bind        (wxEVT_BUTTON,       bind (&CertificateChainEditor::export_chain, this));
342         _import_private_key->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::import_private_key, this));
343         _export_private_key->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::export_private_key, this));
344
345         auto buttons = CreateSeparatedButtonSizer (wxCLOSE);
346         if (buttons) {
347                 _sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
348         }
349
350         SetSizerAndFit (_sizer);
351
352         update_certificate_list ();
353         update_private_key ();
354         update_sensitivity ();
355 }
356
357 void
358 CertificateChainEditor::add_button (wxWindow* button)
359 {
360         _button_sizer->Add (button, 0, wxLEFT | wxRIGHT, DCPOMATIC_SIZER_X_GAP);
361         _sizer->Layout ();
362 }
363
364 void
365 CertificateChainEditor::add_certificate ()
366 {
367         auto d = new wxFileDialog (this, _("Select Certificate File"));
368
369         if (d->ShowModal() == wxID_OK) {
370                 try {
371                         dcp::Certificate c;
372                         string extra;
373                         try {
374                                 extra = c.read_string (dcp::file_to_string (wx_to_std (d->GetPath ())));
375                         } catch (boost::filesystem::filesystem_error& e) {
376                                 error_dialog (this, _("Could not import certificate (%s)"), d->GetPath());
377                                 d->Destroy ();
378                                 return;
379                         }
380
381                         if (!extra.empty ()) {
382                                 message_dialog (
383                                         this,
384                                         _("This file contains other certificates (or other data) after its first certificate. "
385                                           "Only the first certificate will be used.")
386                                         );
387                         }
388                         auto chain = make_shared<dcp::CertificateChain>(*_get().get());
389                         chain->add (c);
390                         if (!chain->chain_valid ()) {
391                                 error_dialog (
392                                         this,
393                                         _("Adding this certificate would make the chain inconsistent, so it will not be added. "
394                                           "Add certificates in order from root to intermediate to leaf.")
395                                         );
396                                 chain->remove (c);
397                         } else {
398                                 _set (chain);
399                                 update_certificate_list ();
400                         }
401                 } catch (dcp::MiscError& e) {
402                         error_dialog (this, _("Could not read certificate file."), std_to_wx(e.what()));
403                 }
404         }
405
406         d->Destroy ();
407
408         update_sensitivity ();
409 }
410
411 void
412 CertificateChainEditor::remove_certificate ()
413 {
414         if (_nag_alter()) {
415                 /* Cancel was clicked */
416                 return;
417         }
418
419         int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
420         if (i == -1) {
421                 return;
422         }
423
424         _certificates->DeleteItem (i);
425         auto chain = make_shared<dcp::CertificateChain>(*_get().get());
426         chain->remove (i);
427         _set (chain);
428
429         update_sensitivity ();
430         update_certificate_list ();
431 }
432
433 void
434 CertificateChainEditor::export_certificate ()
435 {
436         int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
437         if (i == -1) {
438                 return;
439         }
440
441         auto all = _get()->root_to_leaf();
442
443         wxString default_name;
444         if (i == 0) {
445                 default_name = "root.pem";
446         } else if (i == static_cast<int>(all.size() - 1)) {
447                 default_name = "leaf.pem";
448         } else {
449                 default_name = "intermediate.pem";
450         }
451
452         auto d = new wxFileDialog(
453                 this, _("Select Certificate File"), wxEmptyString, default_name, wxT ("PEM files (*.pem)|*.pem"),
454                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
455                 );
456
457         auto j = all.begin ();
458         for (int k = 0; k < i; ++k) {
459                 ++j;
460         }
461
462         if (d->ShowModal () == wxID_OK) {
463                 boost::filesystem::path path (wx_to_std(d->GetPath()));
464                 if (path.extension() != ".pem") {
465                         path += ".pem";
466                 }
467                 dcp::File f(path, "w");
468                 if (!f) {
469                         throw OpenFileError (path, errno, OpenFileError::WRITE);
470                 }
471
472                 string const s = j->certificate (true);
473                 f.checked_write(s.c_str(), s.length());
474         }
475         d->Destroy ();
476 }
477
478 void
479 CertificateChainEditor::export_chain ()
480 {
481         auto d = new wxFileDialog (
482                 this, _("Select Chain File"), wxEmptyString, wxT("certificate_chain.pem"), wxT("PEM files (*.pem)|*.pem"),
483                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
484                 );
485
486         if (d->ShowModal () == wxID_OK) {
487                 boost::filesystem::path path (wx_to_std(d->GetPath()));
488                 if (path.extension() != ".pem") {
489                         path += ".pem";
490                 }
491                 dcp::File f(path, "w");
492                 if (!f) {
493                         throw OpenFileError (path, errno, OpenFileError::WRITE);
494                 }
495
496                 auto const s = _get()->chain();
497                 f.checked_write (s.c_str(), s.length());
498         }
499
500         d->Destroy ();
501 }
502
503 void
504 CertificateChainEditor::update_certificate_list ()
505 {
506         _certificates->DeleteAllItems ();
507         size_t n = 0;
508         auto certs = _get()->root_to_leaf();
509         for (auto const& i: certs) {
510                 wxListItem item;
511                 item.SetId (n);
512                 _certificates->InsertItem (item);
513                 _certificates->SetItem (n, 1, std_to_wx (i.thumbprint ()));
514
515                 if (n == 0) {
516                         _certificates->SetItem (n, 0, _("Root"));
517                 } else if (n == (certs.size() - 1)) {
518                         _certificates->SetItem (n, 0, _("Leaf"));
519                 } else {
520                         _certificates->SetItem (n, 0, _("Intermediate"));
521                 }
522
523                 ++n;
524         }
525
526         static wxColour normal = _private_key_bad->GetForegroundColour ();
527
528         if (_get()->private_key_valid()) {
529                 _private_key_bad->Hide ();
530                 _private_key_bad->SetForegroundColour (normal);
531         } else {
532                 _private_key_bad->Show ();
533                 _private_key_bad->SetForegroundColour (wxColour (255, 0, 0));
534         }
535 }
536
537 void
538 CertificateChainEditor::remake_certificates ()
539 {
540         if (_nag_alter()) {
541                 /* Cancel was clicked */
542                 return;
543         }
544
545         auto d = new MakeChainDialog (this, _get());
546
547         if (d->ShowModal () == wxID_OK) {
548                 _set (d->get());
549                 update_certificate_list ();
550                 update_private_key ();
551         }
552
553         d->Destroy ();
554 }
555
556 void
557 CertificateChainEditor::update_sensitivity ()
558 {
559         /* We can only remove the leaf certificate */
560         _remove_certificate->Enable (_certificates->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) == (_certificates->GetItemCount() - 1));
561         _export_certificate->Enable (_certificates->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) != -1);
562 }
563
564 void
565 CertificateChainEditor::update_private_key ()
566 {
567         checked_set (_private_key, dcp::private_key_fingerprint (_get()->key().get()));
568         _sizer->Layout ();
569 }
570
571 void
572 CertificateChainEditor::import_private_key ()
573 {
574         auto d = new wxFileDialog (this, _("Select Key File"));
575
576         if (d->ShowModal() == wxID_OK) {
577                 try {
578                         boost::filesystem::path p (wx_to_std (d->GetPath ()));
579                         if (boost::filesystem::file_size (p) > 8192) {
580                                 error_dialog (
581                                         this,
582                                         wxString::Format (_("Could not read key file; file is too long (%s)"), std_to_wx (p.string ()))
583                                         );
584                                 return;
585                         }
586
587                         auto chain = make_shared<dcp::CertificateChain>(*_get().get());
588                         chain->set_key (dcp::file_to_string (p));
589                         _set (chain);
590                         update_private_key ();
591                 } catch (std::exception& e) {
592                         error_dialog (this, _("Could not read certificate file."), std_to_wx(e.what()));
593                 }
594         }
595
596         d->Destroy ();
597
598         update_sensitivity ();
599 }
600
601 void
602 CertificateChainEditor::export_private_key ()
603 {
604         auto key = _get()->key();
605         if (!key) {
606                 return;
607         }
608
609         auto d = new wxFileDialog (
610                 this, _("Select Key File"), wxEmptyString, wxT("private_key.pem"), wxT("PEM files (*.pem)|*.pem"),
611                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
612                 );
613
614         if (d->ShowModal () == wxID_OK) {
615                 boost::filesystem::path path (wx_to_std(d->GetPath()));
616                 if (path.extension() != ".pem") {
617                         path += ".pem";
618                 }
619                 dcp::File f(path, "w");
620                 if (!f) {
621                         throw OpenFileError (path, errno, OpenFileError::WRITE);
622                 }
623
624                 auto const s = _get()->key().get ();
625                 f.checked_write(s.c_str(), s.length());
626         }
627         d->Destroy ();
628 }
629
630 wxString
631 KeysPage::GetName () const
632 {
633         return _("Keys");
634 }
635
636 void
637 KeysPage::setup ()
638 {
639         wxFont subheading_font (*wxNORMAL_FONT);
640         subheading_font.SetWeight (wxFONTWEIGHT_BOLD);
641
642         auto sizer = _panel->GetSizer();
643
644         {
645                 auto m = new StaticText (_panel, _("Decrypting KDMs"));
646                 m->SetFont (subheading_font);
647                 sizer->Add (m, 0, wxALL | wxEXPAND, _border);
648         }
649
650         auto kdm_buttons = new wxBoxSizer (wxVERTICAL);
651
652         auto export_decryption_certificate = new Button (_panel, _("Export KDM decryption leaf certificate..."));
653         kdm_buttons->Add (export_decryption_certificate, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
654         auto export_settings = new Button (_panel, _("Export all KDM decryption settings..."));
655         kdm_buttons->Add (export_settings, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
656         auto import_settings = new Button (_panel, _("Import all KDM decryption settings..."));
657         kdm_buttons->Add (import_settings, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
658         auto decryption_advanced = new Button (_panel, _("Advanced..."));
659         kdm_buttons->Add (decryption_advanced, 0);
660
661         sizer->Add (kdm_buttons, 0, wxLEFT, _border);
662
663         export_decryption_certificate->Bind (wxEVT_BUTTON, bind (&KeysPage::export_decryption_certificate, this));
664         export_settings->Bind (wxEVT_BUTTON, bind (&KeysPage::export_decryption_chain_and_key, this));
665         import_settings->Bind (wxEVT_BUTTON, bind (&KeysPage::import_decryption_chain_and_key, this));
666         decryption_advanced->Bind (wxEVT_BUTTON, bind (&KeysPage::decryption_advanced, this));
667
668         {
669                 auto m = new StaticText (_panel, _("Signing DCPs and KDMs"));
670                 m->SetFont (subheading_font);
671                 sizer->Add (m, 0, wxALL | wxEXPAND, _border);
672         }
673
674         auto signing_buttons = new wxBoxSizer (wxVERTICAL);
675
676         auto signing_advanced = new Button (_panel, _("Advanced..."));
677         signing_buttons->Add (signing_advanced, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
678         auto remake_signing = new Button (_panel, _("Re-make certificates and key..."));
679         signing_buttons->Add (remake_signing, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
680
681         sizer->Add (signing_buttons, 0, wxLEFT, _border);
682
683         signing_advanced->Bind (wxEVT_BUTTON, bind (&KeysPage::signing_advanced, this));
684         remake_signing->Bind (wxEVT_BUTTON, bind(&KeysPage::remake_signing, this));
685 }
686
687
688 void
689 KeysPage::remake_signing ()
690 {
691         auto d = new MakeChainDialog (_panel, Config::instance()->signer_chain());
692
693         if (d->ShowModal () == wxID_OK) {
694                 Config::instance()->set_signer_chain(d->get());
695         }
696 }
697
698
699 void
700 KeysPage::decryption_advanced ()
701 {
702         auto c = new CertificateChainEditor (
703                 _panel, _("Decrypting KDMs"), _border,
704                 bind(&Config::set_decryption_chain, Config::instance(), _1),
705                 bind(&Config::decryption_chain, Config::instance()),
706                 bind(&KeysPage::nag_alter_decryption_chain, this)
707                 );
708
709         c->ShowModal();
710 }
711
712 void
713 KeysPage::signing_advanced ()
714 {
715         auto c = new CertificateChainEditor (
716                 _panel, _("Signing DCPs and KDMs"), _border,
717                 bind(&Config::set_signer_chain, Config::instance(), _1),
718                 bind(&Config::signer_chain, Config::instance()),
719                 bind(&do_nothing)
720                 );
721
722         c->ShowModal();
723 }
724
725 void
726 KeysPage::export_decryption_chain_and_key ()
727 {
728         auto d = new wxFileDialog (
729                 _panel, _("Select Export File"), wxEmptyString, wxEmptyString, wxT ("DOM files (*.dom)|*.dom"),
730                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
731                 );
732
733         if (d->ShowModal () == wxID_OK) {
734                 boost::filesystem::path path (wx_to_std(d->GetPath()));
735                 dcp::File f(path, "w");
736                 if (!f) {
737                         throw OpenFileError (path, errno, OpenFileError::WRITE);
738                 }
739
740                 auto const chain = Config::instance()->decryption_chain()->chain();
741                 f.checked_write (chain.c_str(), chain.length());
742                 auto const key = Config::instance()->decryption_chain()->key();
743                 DCPOMATIC_ASSERT (key);
744                 f.checked_write(key->c_str(), key->length());
745         }
746         d->Destroy ();
747
748 }
749
750 void
751 KeysPage::import_decryption_chain_and_key ()
752 {
753         if (NagDialog::maybe_nag (
754                     _panel,
755                     Config::NAG_IMPORT_DECRYPTION_CHAIN,
756                     _("If you continue with this operation you will no longer be able to use any DKDMs that you have created with the current certificates and key.  Also, any KDMs that have been sent to you for those certificates will become useless.  Proceed with caution!"),
757                     true
758                     )) {
759                 return;
760         }
761
762         auto d = new wxFileDialog (
763                 _panel, _("Select File To Import"), wxEmptyString, wxEmptyString, wxT ("DOM files (*.dom)|*.dom")
764                 );
765
766         if (d->ShowModal () == wxID_OK) {
767                 auto new_chain = make_shared<dcp::CertificateChain>();
768
769                 dcp::File f(wx_to_std(d->GetPath()), "r");
770                 if (!f) {
771                         throw OpenFileError (f.path(), errno, OpenFileError::WRITE);
772                 }
773
774                 string current;
775                 while (!f.eof()) {
776                         char buffer[128];
777                         if (f.gets(buffer, 128) == 0) {
778                                 break;
779                         }
780                         current += buffer;
781                         if (strncmp (buffer, "-----END CERTIFICATE-----", 25) == 0) {
782                                 new_chain->add (dcp::Certificate (current));
783                                 current = "";
784                         } else if (strncmp (buffer, "-----END RSA PRIVATE KEY-----", 29) == 0) {
785                                 new_chain->set_key (current);
786                                 current = "";
787                         }
788                 }
789
790                 if (new_chain->chain_valid() && new_chain->private_key_valid()) {
791                         Config::instance()->set_decryption_chain (new_chain);
792                 } else {
793                         error_dialog (_panel, _("Invalid DCP-o-matic export file"));
794                 }
795         }
796         d->Destroy ();
797 }
798
799 bool
800 KeysPage::nag_alter_decryption_chain ()
801 {
802         return NagDialog::maybe_nag (
803                 _panel,
804                 Config::NAG_ALTER_DECRYPTION_CHAIN,
805                 _("If you continue with this operation you will no longer be able to use any DKDMs that you have created.  Also, any KDMs that have been sent to you will become useless.  Proceed with caution!"),
806                 true
807                 );
808 }
809
810 void
811 KeysPage::export_decryption_certificate ()
812 {
813         auto config = Config::instance();
814         wxString default_name = "dcpomatic";
815         if (!config->dcp_creator().empty()) {
816                 default_name += "_" + std_to_wx(careful_string_filter(config->dcp_creator()));
817         }
818         if (!config->dcp_issuer().empty()) {
819                 default_name += "_" + std_to_wx(careful_string_filter(config->dcp_issuer()));
820         }
821         default_name += wxT("_kdm_decryption_cert.pem");
822
823         auto d = new wxFileDialog (
824                 _panel, _("Select Certificate File"), wxEmptyString, default_name, wxT("PEM files (*.pem)|*.pem"),
825                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
826                 );
827
828         if (d->ShowModal () == wxID_OK) {
829                 boost::filesystem::path path (wx_to_std(d->GetPath()));
830                 if (path.extension() != ".pem") {
831                         path += ".pem";
832                 }
833                 dcp::File f(path, "w");
834                 if (!f) {
835                         throw OpenFileError (path, errno, OpenFileError::WRITE);
836                 }
837
838                 auto const s = Config::instance()->decryption_chain()->leaf().certificate (true);
839                 f.checked_write(s.c_str(), s.length());
840         }
841
842         d->Destroy ();
843 }
844
845 wxString
846 SoundPage::GetName () const
847 {
848         return _("Sound");
849 }
850
851 void
852 SoundPage::setup ()
853 {
854         auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
855         _panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
856
857         int r = 0;
858
859         _sound = new CheckBox (_panel, _("Play sound via"));
860         table->Add (_sound, wxGBPosition (r, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
861         wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
862         _sound_output = new wxChoice (_panel, wxID_ANY);
863         s->Add (_sound_output, 0);
864         _sound_output_details = new wxStaticText (_panel, wxID_ANY, wxT(""));
865         s->Add (_sound_output_details, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, DCPOMATIC_SIZER_X_GAP);
866         table->Add (s, wxGBPosition(r, 1));
867         ++r;
868
869         add_label_to_sizer (table, _panel, _("Mapping"), true, wxGBPosition(r, 0));
870         _map = new AudioMappingView (_panel, _("DCP"), _("DCP"), _("Output"), _("output"));
871         _map->SetSize (-1, 400);
872         table->Add (_map, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
873         ++r;
874
875         _reset_to_default = new Button (_panel, _("Reset to default"));
876         table->Add (_reset_to_default, wxGBPosition(r, 1));
877         ++r;
878
879         wxFont font = _sound_output_details->GetFont();
880         font.SetStyle (wxFONTSTYLE_ITALIC);
881         font.SetPointSize (font.GetPointSize() - 1);
882         _sound_output_details->SetFont (font);
883
884         RtAudio audio (DCPOMATIC_RTAUDIO_API);
885         for (unsigned int i = 0; i < audio.getDeviceCount(); ++i) {
886                 try {
887                         auto dev = audio.getDeviceInfo (i);
888                         if (dev.probed && dev.outputChannels > 0) {
889                                 _sound_output->Append (std_to_wx (dev.name));
890                         }
891                 } catch (RtAudioError&) {
892                         /* Something went wrong so let's just ignore that device */
893                 }
894         }
895
896         _sound->Bind        (wxEVT_CHECKBOX, bind(&SoundPage::sound_changed, this));
897         _sound_output->Bind (wxEVT_CHOICE,   bind(&SoundPage::sound_output_changed, this));
898         _map->Changed.connect (bind(&SoundPage::map_changed, this, _1));
899         _reset_to_default->Bind (wxEVT_BUTTON,   bind(&SoundPage::reset_to_default, this));
900 }
901
902 void
903 SoundPage::reset_to_default ()
904 {
905         Config::instance()->set_audio_mapping_to_default ();
906 }
907
908 void
909 SoundPage::map_changed (AudioMapping m)
910 {
911         Config::instance()->set_audio_mapping (m);
912 }
913
914 void
915 SoundPage::sound_changed ()
916 {
917         Config::instance()->set_sound (_sound->GetValue ());
918 }
919
920 void
921 SoundPage::sound_output_changed ()
922 {
923         RtAudio audio (DCPOMATIC_RTAUDIO_API);
924         auto const so = get_sound_output();
925         string default_device;
926         try {
927                 default_device = audio.getDeviceInfo(audio.getDefaultOutputDevice()).name;
928         } catch (RtAudioError&) {
929                 /* Never mind */
930         }
931         if (!so || *so == default_device) {
932                 Config::instance()->unset_sound_output ();
933         } else {
934                 Config::instance()->set_sound_output (*so);
935         }
936 }
937
938 void
939 SoundPage::config_changed ()
940 {
941         auto config = Config::instance ();
942
943         checked_set (_sound, config->sound ());
944
945         auto const current_so = get_sound_output ();
946         optional<string> configured_so;
947
948         if (config->sound_output()) {
949                 configured_so = config->sound_output().get();
950         } else {
951                 /* No configured output means we should use the default */
952                 RtAudio audio (DCPOMATIC_RTAUDIO_API);
953                 try {
954                         configured_so = audio.getDeviceInfo(audio.getDefaultOutputDevice()).name;
955                 } catch (RtAudioError&) {
956                         /* Probably no audio devices at all */
957                 }
958         }
959
960         if (configured_so && current_so != configured_so) {
961                 /* Update _sound_output with the configured value */
962                 unsigned int i = 0;
963                 while (i < _sound_output->GetCount()) {
964                         if (_sound_output->GetString(i) == std_to_wx(*configured_so)) {
965                                 _sound_output->SetSelection (i);
966                                 break;
967                         }
968                         ++i;
969                 }
970         }
971
972         RtAudio audio (DCPOMATIC_RTAUDIO_API);
973
974         map<int, wxString> apis;
975         apis[RtAudio::MACOSX_CORE]    = _("CoreAudio");
976         apis[RtAudio::WINDOWS_ASIO]   = _("ASIO");
977         apis[RtAudio::WINDOWS_DS]     = _("Direct Sound");
978         apis[RtAudio::WINDOWS_WASAPI] = _("WASAPI");
979         apis[RtAudio::UNIX_JACK]      = _("JACK");
980         apis[RtAudio::LINUX_ALSA]     = _("ALSA");
981         apis[RtAudio::LINUX_PULSE]    = _("PulseAudio");
982         apis[RtAudio::LINUX_OSS]      = _("OSS");
983         apis[RtAudio::RTAUDIO_DUMMY]  = _("Dummy");
984
985         int channels = 0;
986         if (configured_so) {
987                 for (unsigned int i = 0; i < audio.getDeviceCount(); ++i) {
988                         try {
989                                 auto info = audio.getDeviceInfo(i);
990                                 if (info.name == *configured_so && info.outputChannels > 0) {
991                                         channels = info.outputChannels;
992                                 }
993                         } catch (RtAudioError&) {
994                                 /* Never mind */
995                         }
996                 }
997         }
998
999         _sound_output_details->SetLabel (
1000                 wxString::Format(_("%d channels on %s"), channels, apis[audio.getCurrentApi()])
1001                 );
1002
1003         _map->set (Config::instance()->audio_mapping(channels));
1004
1005         vector<NamedChannel> input;
1006         for (int i = 0; i < MAX_DCP_AUDIO_CHANNELS; ++i) {
1007                 input.push_back (NamedChannel(short_audio_channel_name(i), i));
1008         }
1009         _map->set_input_channels (input);
1010
1011         vector<NamedChannel> output;
1012         for (int i = 0; i < channels; ++i) {
1013                 output.push_back (NamedChannel(dcp::raw_convert<string>(i), i));
1014         }
1015         _map->set_output_channels (output);
1016
1017         setup_sensitivity ();
1018 }
1019
1020 void
1021 SoundPage::setup_sensitivity ()
1022 {
1023         _sound_output->Enable (_sound->GetValue());
1024 }
1025
1026 /** @return Currently-selected preview sound output in the dialogue */
1027 optional<string>
1028 SoundPage::get_sound_output ()
1029 {
1030         int const sel = _sound_output->GetSelection ();
1031         if (sel == wxNOT_FOUND) {
1032                 return optional<string> ();
1033         }
1034
1035         return wx_to_std (_sound_output->GetString (sel));
1036 }
1037
1038
1039 LocationsPage::LocationsPage (wxSize panel_size, int border)
1040         : Page (panel_size, border)
1041 {
1042
1043 }
1044
1045 wxString
1046 LocationsPage::GetName () const
1047 {
1048         return _("Locations");
1049 }
1050
1051 #ifdef DCPOMATIC_OSX
1052 wxBitmap
1053 LocationsPage::GetLargeIcon () const
1054 {
1055         return wxBitmap(bitmap_path("locations"), wxBITMAP_TYPE_PNG);
1056 }
1057 #endif
1058
1059 void
1060 LocationsPage::setup ()
1061 {
1062         int r = 0;
1063
1064         auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
1065         _panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
1066
1067         add_label_to_sizer (table, _panel, _("Content directory"), true, wxGBPosition (r, 0));
1068         _content_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
1069         table->Add (_content_directory, wxGBPosition (r, 1));
1070         ++r;
1071
1072         add_label_to_sizer (table, _panel, _("Playlist directory"), true, wxGBPosition (r, 0));
1073         _playlist_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
1074         table->Add (_playlist_directory, wxGBPosition (r, 1));
1075         ++r;
1076
1077         add_label_to_sizer (table, _panel, _("KDM directory"), true, wxGBPosition (r, 0));
1078         _kdm_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
1079         table->Add (_kdm_directory, wxGBPosition (r, 1));
1080         ++r;
1081
1082         _content_directory->Bind (wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::content_directory_changed, this));
1083         _playlist_directory->Bind (wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::playlist_directory_changed, this));
1084         _kdm_directory->Bind (wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::kdm_directory_changed, this));
1085 }
1086
1087 void
1088 LocationsPage::config_changed ()
1089 {
1090         auto config = Config::instance ();
1091
1092         if (config->player_content_directory()) {
1093                 checked_set (_content_directory, *config->player_content_directory());
1094         }
1095         if (config->player_playlist_directory()) {
1096                 checked_set (_playlist_directory, *config->player_playlist_directory());
1097         }
1098         if (config->player_kdm_directory()) {
1099                 checked_set (_kdm_directory, *config->player_kdm_directory());
1100         }
1101 }
1102
1103 void
1104 LocationsPage::content_directory_changed ()
1105 {
1106         Config::instance()->set_player_content_directory(wx_to_std(_content_directory->GetPath()));
1107 }
1108
1109 void
1110 LocationsPage::playlist_directory_changed ()
1111 {
1112         Config::instance()->set_player_playlist_directory(wx_to_std(_playlist_directory->GetPath()));
1113 }
1114
1115 void
1116 LocationsPage::kdm_directory_changed ()
1117 {
1118         Config::instance()->set_player_kdm_directory(wx_to_std(_kdm_directory->GetPath()));
1119 }