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