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