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