Supporters update.
[dcpomatic.git] / src / lib / config.cc
index 18c792223b020ff1c195f2fb0eca7c1b06e1f84e..384db5cde137be48177afc3cb3ceca8df95f090f 100644 (file)
@@ -23,8 +23,8 @@
 #include "colour_conversion.h"
 #include "compose.hpp"
 #include "config.h"
+#include "constants.h"
 #include "cross.h"
-#include "crypto.h"
 #include "dcp_content_type.h"
 #include "dkdm_recipient.h"
 #include "dkdm_wrapper.h"
@@ -32,8 +32,7 @@
 #include "filter.h"
 #include "log.h"
 #include "ratio.h"
-#include "types.h"
-#include "util.h"
+#include "unzipper.h"
 #include "zipper.h"
 #include <dcp/certificate_chain.h>
 #include <dcp/name_format.h>
@@ -94,6 +93,7 @@ Config::set_defaults ()
        _servers.clear ();
        _only_servers_encode = false;
        _tms_protocol = FileTransferProtocol::SCP;
+       _tms_passive = true;
        _tms_ip = "";
        _tms_path = ".";
        _tms_user = "";
@@ -105,9 +105,8 @@ Config::set_defaults ()
        _show_experimental_audio_processors = false;
        _language = optional<string> ();
        _default_still_length = 10;
-       _default_container = Ratio::from_id ("185");
        _default_dcp_content_type = DCPContentType::from_isdcf_name ("FTR");
-       _default_dcp_audio_channels = 6;
+       _default_dcp_audio_channels = 8;
        _default_j2k_bandwidth = 150000000;
        _default_audio_delay = 0;
        _default_interop = false;
@@ -141,9 +140,9 @@ Config::set_defaults ()
        _dkdm_recipients_file = read_path ("dkdm_recipients.xml");
        _show_hints_before_make_dcp = true;
        _confirm_kdm_email = true;
-       _kdm_container_name_format = dcp::NameFormat ("KDM %f %c");
-       _kdm_filename_format = dcp::NameFormat ("KDM %f %c %s");
-       _dkdm_filename_format = dcp::NameFormat ("DKDM %f %c %s");
+       _kdm_container_name_format = dcp::NameFormat("KDM_%f_%c");
+       _kdm_filename_format = dcp::NameFormat("KDM_%f_%c_%s");
+       _dkdm_filename_format = dcp::NameFormat("DKDM_%f_%c_%s");
        _dcp_metadata_filename_format = dcp::NameFormat ("%t");
        _dcp_asset_filename_format = dcp::NameFormat ("%t");
        _jump_to_selected = true;
@@ -154,6 +153,7 @@ Config::set_defaults ()
        _sound_output = optional<string> ();
        _last_kdm_write_type = KDM_WRITE_FLAT;
        _last_dkdm_write_type = DKDM_WRITE_INTERNAL;
+       _default_add_file_location = DefaultAddFileLocation::SAME_AS_LAST_TIME;
 
        /* I think the scaling factor here should be the ratio of the longest frame
           encode time to the shortest; if the thread count is T, longest time is L
@@ -186,7 +186,18 @@ Config::set_defaults ()
        _player_kdm_directory = boost::none;
        _audio_mapping = boost::none;
        _custom_languages.clear ();
-       _add_files_path = boost::none;
+       _initial_paths.clear();
+       _initial_paths["AddFilesPath"] = boost::none;
+       _initial_paths["AddKDMPath"] = boost::none;
+       _initial_paths["AddDKDMPath"] = boost::none;
+       _initial_paths["SelectCertificatePath"] = boost::none;
+       _initial_paths["AddCombinerInputPath"] = boost::none;
+       _initial_paths["ExportSubtitlesPath"] = boost::none;
+       _initial_paths["ExportVideoPath"] = boost::none;
+       _initial_paths["DebugLogPath"] = boost::none;
+       _initial_paths["CinemaDatabasePath"] = boost::none;
+       _initial_paths["ConfigFilePath"] = boost::none;
+       _initial_paths["Preferences"] = boost::none;
        _use_isdcf_name_by_default = true;
        _write_kdms_to_disk = true;
        _email_kdms = false;
@@ -194,6 +205,8 @@ Config::set_defaults ()
        _default_kdm_duration = RoughDuration(1, RoughDuration::Unit::WEEKS);
        _auto_crop_threshold = 0.1;
        _last_release_notes_version = boost::none;
+       _allow_smpte_bv20 = false;
+       _isdcf_name_part_length = 14;
 
        _allowed_dcp_frame_rates.clear ();
        _allowed_dcp_frame_rates.push_back (24);
@@ -207,6 +220,9 @@ Config::set_defaults ()
        set_notification_email_to_default ();
        set_cover_sheet_to_default ();
 
+       _main_divider_sash_position = {};
+       _main_content_divider_sash_position = {};
+
        _export.set_defaults();
 }
 
@@ -282,7 +298,7 @@ Config::read_config()
 try
 {
        cxml::Document f ("Config");
-       f.read_file (config_read_file());
+       f.read_file(dcp::filesystem::fix_long_path(config_read_file()));
 
        auto version = f.optional_number_child<int> ("Version");
        if (version && *version < _current_version) {
@@ -322,6 +338,7 @@ try
 
        _only_servers_encode = f.optional_bool_child ("OnlyServersEncode").get_value_or (false);
        _tms_protocol = static_cast<FileTransferProtocol>(f.optional_number_child<int>("TMSProtocol").get_value_or(static_cast<int>(FileTransferProtocol::SCP)));
+       _tms_passive = f.optional_bool_child("TMSPassive").get_value_or(true);
        _tms_ip = f.string_child ("TMSIP");
        _tms_path = f.string_child ("TMSPath");
        _tms_user = f.string_child ("TMSUser");
@@ -329,16 +346,6 @@ try
 
        _language = f.optional_string_child ("Language");
 
-       auto c = f.optional_string_child ("DefaultContainer");
-       if (c) {
-               _default_container = Ratio::from_id (c.get ());
-       }
-
-       if (_default_container && !_default_container->used_for_container()) {
-               Warning (_("Your default container is not valid and has been changed to Flat (1.85:1)"));
-               _default_container = Ratio::from_id ("185");
-       }
-
        _default_dcp_content_type = DCPContentType::from_isdcf_name(f.optional_string_child("DefaultDCPContentType").get_value_or("FTR"));
        _default_dcp_audio_channels = f.optional_number_child<int>("DefaultDCPAudioChannels").get_value_or (6);
 
@@ -364,6 +371,20 @@ try
        _default_audio_delay = f.optional_number_child<int>("DefaultAudioDelay").get_value_or (0);
        _default_interop = f.optional_bool_child("DefaultInterop").get_value_or (false);
 
+       try {
+               auto al = f.optional_string_child("DefaultAudioLanguage");
+               if (al) {
+                       _default_audio_language = dcp::LanguageTag(*al);
+               }
+       } catch (std::runtime_error&) {}
+
+       try {
+               auto te = f.optional_string_child("DefaultTerritory");
+               if (te) {
+                       _default_territory = dcp::LanguageTag::RegionSubtag(*te);
+               }
+       } catch (std::runtime_error&) {}
+
        for (auto const& i: f.node_children("DefaultMetadata")) {
                _default_metadata[i->string_attribute("key")] = i->content();
        }
@@ -487,6 +508,7 @@ try
                        case BAD_SIGNER_UTF8_STRINGS:
                        case BAD_SIGNER_INCONSISTENT:
                        case BAD_SIGNER_VALIDITY_TOO_LONG:
+                       case BAD_SIGNER_DN_QUALIFIER:
                                _signer_chain = create_certificate_chain ();
                                break;
                        case BAD_DECRYPTION_INCONSISTENT:
@@ -592,7 +614,9 @@ try
                } catch (std::runtime_error& e) {}
        }
 
-       _add_files_path = f.optional_string_child("AddFilesPath");
+       for (auto& initial: _initial_paths) {
+               initial.second = f.optional_string_child(initial.first);
+       }
        _use_isdcf_name_by_default = f.optional_bool_child("UseISDCFNameByDefault").get_value_or(true);
        _write_kdms_to_disk = f.optional_bool_child("WriteKDMsToDisk").get_value_or(true);
        _email_kdms = f.optional_bool_child("EmailKDMs").get_value_or(false);
@@ -604,6 +628,19 @@ try
        }
        _auto_crop_threshold = f.optional_number_child<double>("AutoCropThreshold").get_value_or(0.1);
        _last_release_notes_version = f.optional_string_child("LastReleaseNotesVersion");
+       _main_divider_sash_position = f.optional_number_child<int>("MainDividerSashPosition");
+       _main_content_divider_sash_position = f.optional_number_child<int>("MainContentDividerSashPosition");
+
+       if (auto loc = f.optional_string_child("DefaultAddFileLocation")) {
+               if (*loc == "last") {
+                       _default_add_file_location = DefaultAddFileLocation::SAME_AS_LAST_TIME;
+               } else if (*loc == "project") {
+                       _default_add_file_location = DefaultAddFileLocation::SAME_AS_PROJECT;
+               }
+       }
+
+       _allow_smpte_bv20 = f.optional_bool_child("AllowSMPTEBv20").get_value_or(false);
+       _isdcf_name_part_length = f.optional_number_child<int>("ISDCFNamePartLength").get_value_or(14);
 
        _export.read(f.optional_node_child("Export"));
 }
@@ -625,10 +662,10 @@ catch (...) {
 void
 Config::read_cinemas()
 {
-       if (boost::filesystem::exists (_cinemas_file)) {
+       if (dcp::filesystem::exists(_cinemas_file)) {
                try {
                        cxml::Document f("Cinemas");
-                       f.read_file(_cinemas_file);
+                       f.read_file(dcp::filesystem::fix_long_path(_cinemas_file));
                        read_cinemas(f);
                } catch (...) {
                        backup();
@@ -642,10 +679,10 @@ Config::read_cinemas()
 void
 Config::read_dkdm_recipients()
 {
-       if (boost::filesystem::exists (_dkdm_recipients_file)) {
+       if (dcp::filesystem::exists(_dkdm_recipients_file)) {
                try {
                        cxml::Document f("DKDMRecipients");
-                       f.read_file(_dkdm_recipients_file);
+                       f.read_file(dcp::filesystem::fix_long_path(_dkdm_recipients_file));
                        read_dkdm_recipients(f);
                } catch (...) {
                        backup();
@@ -714,6 +751,8 @@ Config::write_config () const
        root->add_child("OnlyServersEncode")->add_child_text (_only_servers_encode ? "1" : "0");
        /* [XML] TMSProtocol Protocol to use to copy files to a TMS; 0 to use SCP, 1 for FTP. */
        root->add_child("TMSProtocol")->add_child_text (raw_convert<string> (static_cast<int> (_tms_protocol)));
+       /* [XML] TMSPassive True to use PASV mode with TMS FTP connections. */
+       root->add_child("TMSPassive")->add_child_text(_tms_passive ? "1" : "0");
        /* [XML] TMSIP IP address of TMS. */
        root->add_child("TMSIP")->add_child_text (_tms_ip);
        /* [XML] TMSPath Path on the TMS to copy files to. */
@@ -726,15 +765,8 @@ Config::write_config () const
                /* [XML:opt] Language Language to use in the GUI e.g. <code>fr_FR</code>. */
                root->add_child("Language")->add_child_text (_language.get());
        }
-       if (_default_container) {
-               /* [XML:opt] DefaultContainer ID of default container
-                  to use when creating new films (<code>185</code>,<code>239</code> or
-                  <code>190</code>).
-               */
-               root->add_child("DefaultContainer")->add_child_text (_default_container->id ());
-       }
        if (_default_dcp_content_type) {
-               /* [XML:opt] DefaultDCPContentType Default content type ot use when creating new films (<code>FTR</code>, <code>SHR</code>,
+               /* [XML:opt] DefaultDCPContentType Default content type to use when creating new films (<code>FTR</code>, <code>SHR</code>,
                   <code>TLR</code>, <code>TST</code>, <code>XSN</code>, <code>RTG</code>, <code>TSR</code>, <code>POL</code>,
                   <code>PSA</code> or <code>ADV</code>). */
                root->add_child("DefaultDCPContentType")->add_child_text (_default_dcp_content_type->isdcf_name ());
@@ -764,6 +796,14 @@ Config::write_config () const
        root->add_child("DefaultAudioDelay")->add_child_text (raw_convert<string> (_default_audio_delay));
        /* [XML] DefaultInterop 1 to default new films to Interop, 0 for SMPTE. */
        root->add_child("DefaultInterop")->add_child_text (_default_interop ? "1" : "0");
+       if (_default_audio_language) {
+               /* [XML] DefaultAudioLanguage Default audio language to use for new films */
+               root->add_child("DefaultAudioLanguage")->add_child_text(_default_audio_language->to_string());
+       }
+       if (_default_territory) {
+               /* [XML] DefaultTerritory Default territory to use for new films */
+               root->add_child("DefaultTerritory")->add_child_text(_default_territory->subtag());
+       }
        for (auto const& i: _default_metadata) {
                auto c = root->add_child("DefaultMetadata");
                c->set_attribute("key", i.first);
@@ -1011,7 +1051,7 @@ Config::write_config () const
        }
 
        /* [XML] PlayerMode <code>window</code> for a single window, <code>full</code> for full-screen and <code>dual</code> for full screen playback
-          with controls on another monitor.
+          with separate (advanced) controls.
        */
        switch (_player_mode) {
        case PLAYER_MODE_WINDOW:
@@ -1059,9 +1099,10 @@ Config::write_config () const
        for (auto const& i: _custom_languages) {
                root->add_child("CustomLanguage")->add_child_text(i.to_string());
        }
-       if (_add_files_path) {
-               /* [XML] AddFilesPath The default path that will be offered in the picker when adding files to a film. */
-               root->add_child("AddFilesPath")->add_child_text(_add_files_path->string());
+       for (auto const& initial: _initial_paths) {
+               if (initial.second) {
+                       root->add_child(initial.first)->add_child_text(initial.second->string());
+               }
        }
        root->add_child("UseISDCFNameByDefault")->add_child_text(_use_isdcf_name_by_default ? "1" : "0");
        root->add_child("WriteKDMsToDisk")->add_child_text(_write_kdms_to_disk ? "1" : "0");
@@ -1071,6 +1112,21 @@ Config::write_config () const
        if (_last_release_notes_version) {
                root->add_child("LastReleaseNotesVersion")->add_child_text(*_last_release_notes_version);
        }
+       if (_main_divider_sash_position) {
+               root->add_child("MainDividerSashPosition")->add_child_text(raw_convert<string>(*_main_divider_sash_position));
+       }
+       if (_main_content_divider_sash_position) {
+               root->add_child("MainContentDividerSashPosition")->add_child_text(raw_convert<string>(*_main_content_divider_sash_position));
+       }
+
+       root->add_child("DefaultAddFileLocation")->add_child_text(
+               _default_add_file_location == DefaultAddFileLocation::SAME_AS_LAST_TIME ? "last" : "project"
+               );
+
+       /* [XML] AllowSMPTEBv20 1 to allow the user to choose SMPTE (Bv2.0 only) as a standard, otherwise 0 */
+       root->add_child("AllowSMPTEBv20")->add_child_text(_allow_smpte_bv20 ? "1" : "0");
+       /* [XML] ISDCFNamePartLength Maximum length of the "name" part of an ISDCF name, which should be 14 according to the standard */
+       root->add_child("ISDCFNamePartLength")->add_child_text(raw_convert<string>(_isdcf_name_part_length));
 
        _export.write(root->add_child("Export"));
 
@@ -1085,8 +1141,8 @@ Config::write_config () const
                }
                f.checked_write(s.c_str(), s.bytes());
                f.close();
-               boost::filesystem::remove (target);
-               boost::filesystem::rename (tmp, target);
+               dcp::filesystem::remove(target);
+               dcp::filesystem::rename(tmp, target);
        } catch (xmlpp::exception& e) {
                string s = e.what ();
                trim (s);
@@ -1109,8 +1165,8 @@ write_file (string root_node, string node, string version, list<shared_ptr<T>> t
 
        try {
                doc.write_to_file_formatted (file.string() + ".tmp");
-               boost::filesystem::remove (file);
-               boost::filesystem::rename (file.string() + ".tmp", file);
+               dcp::filesystem::remove(file);
+               dcp::filesystem::rename(file.string() + ".tmp", file);
        } catch (xmlpp::exception& e) {
                string s = e.what ();
                trim (s);
@@ -1153,7 +1209,7 @@ Config::directory_or (optional<boost::filesystem::path> dir, boost::filesystem::
        }
 
        boost::system::error_code ec;
-       auto const e = boost::filesystem::exists (*dir, ec);
+       auto const e = dcp::filesystem::exists(*dir, ec);
        if (ec || !e) {
                return a;
        }
@@ -1165,7 +1221,7 @@ void
 Config::drop ()
 {
        delete _instance;
-       _instance = 0;
+       _instance = nullptr;
 }
 
 void
@@ -1235,7 +1291,7 @@ Config::add_to_history (boost::filesystem::path p)
        add_to_history_internal (_history, p);
 }
 
-/** Remove non-existant items from the history */
+/** Remove non-existent items from the history */
 void
 Config::clean_history ()
 {
@@ -1248,7 +1304,7 @@ Config::add_to_player_history (boost::filesystem::path p)
        add_to_history_internal (_player_history, p);
 }
 
-/** Remove non-existant items from the player history */
+/** Remove non-existent items from the player history */
 void
 Config::clean_player_history ()
 {
@@ -1276,7 +1332,7 @@ Config::clean_history_internal (vector<boost::filesystem::path>& h)
        h.clear ();
        for (auto i: old) {
                try {
-                       if (boost::filesystem::is_directory(i)) {
+                       if (dcp::filesystem::is_directory(i)) {
                                h.push_back (i);
                        }
                } catch (...) {
@@ -1289,7 +1345,7 @@ Config::clean_history_internal (vector<boost::filesystem::path>& h)
 bool
 Config::have_existing (string file)
 {
-       return boost::filesystem::exists (read_path(file));
+       return dcp::filesystem::exists(read_path(file));
 }
 
 
@@ -1316,10 +1372,10 @@ Config::set_cinemas_file (boost::filesystem::path file)
 
        _cinemas_file = file;
 
-       if (boost::filesystem::exists (_cinemas_file)) {
+       if (dcp::filesystem::exists(_cinemas_file)) {
                /* Existing file; read it in */
                cxml::Document f ("Cinemas");
-               f.read_file (_cinemas_file);
+               f.read_file(dcp::filesystem::fix_long_path(_cinemas_file));
                read_cinemas (f);
        }
 
@@ -1348,12 +1404,12 @@ Config::save_template (shared_ptr<const Film> film, string name) const
 list<string>
 Config::templates () const
 {
-       if (!boost::filesystem::exists(read_path("templates"))) {
+       if (!dcp::filesystem::exists(read_path("templates"))) {
                return {};
        }
 
        list<string> n;
-       for (auto const& i: boost::filesystem::directory_iterator(read_path("templates"))) {
+       for (auto const& i: dcp::filesystem::directory_iterator(read_path("templates"))) {
                n.push_back (i.path().filename().string());
        }
        return n;
@@ -1362,7 +1418,7 @@ Config::templates () const
 bool
 Config::existing_template (string name) const
 {
-       return boost::filesystem::exists (template_read_path(name));
+       return dcp::filesystem::exists(template_read_path(name));
 }
 
 
@@ -1383,13 +1439,13 @@ Config::template_write_path (string name) const
 void
 Config::rename_template (string old_name, string new_name) const
 {
-       boost::filesystem::rename (template_read_path(old_name), template_write_path(new_name));
+       dcp::filesystem::rename(template_read_path(old_name), template_write_path(new_name));
 }
 
 void
 Config::delete_template (string name) const
 {
-       boost::filesystem::remove (template_write_path(name));
+       dcp::filesystem::remove(template_write_path(name));
 }
 
 /** @return Path to the config.xml containing the actual settings, following a link if required */
@@ -1397,14 +1453,14 @@ boost::filesystem::path
 config_file (boost::filesystem::path main)
 {
        cxml::Document f ("Config");
-       if (!boost::filesystem::exists (main)) {
+       if (!dcp::filesystem::exists(main)) {
                /* It doesn't exist, so there can't be any links; just return it */
                return main;
        }
 
        /* See if there's a link */
        try {
-               f.read_file (main);
+               f.read_file(dcp::filesystem::fix_long_path(main));
                auto link = f.optional_string_child("Link");
                if (link) {
                        return *link;
@@ -1458,7 +1514,7 @@ void
 Config::copy_and_link (boost::filesystem::path new_file) const
 {
        write ();
-       boost::filesystem::copy_file (config_read_file(), new_file, boost::filesystem::copy_option::overwrite_if_exists);
+       dcp::filesystem::copy_file(config_read_file(), new_file, boost::filesystem::copy_option::overwrite_if_exists);
        link (new_file);
 }
 
@@ -1542,6 +1598,9 @@ Config::check_certificates () const
                if ((i.not_after().year() - i.not_before().year()) > 15) {
                        bad = BAD_SIGNER_VALIDITY_TOO_LONG;
                }
+               if (dcp::escape_digest(i.subject_dn_qualifier()) != dcp::public_key_digest(i.public_key())) {
+                       bad = BAD_SIGNER_DN_QUALIFIER;
+               }
        }
 
        if (!_signer_chain->chain_valid() || !_signer_chain->private_key_valid()) {
@@ -1563,13 +1622,66 @@ save_all_config_as_zip (boost::filesystem::path zip_file)
 
        auto config = Config::instance();
        zipper.add ("config.xml", dcp::file_to_string(config->config_read_file()));
-       if (boost::filesystem::exists(config->cinemas_file())) {
+       if (dcp::filesystem::exists(config->cinemas_file())) {
                zipper.add ("cinemas.xml", dcp::file_to_string(config->cinemas_file()));
        }
-       if (boost::filesystem::exists(config->dkdm_recipients_file())) {
+       if (dcp::filesystem::exists(config->dkdm_recipients_file())) {
                zipper.add ("dkdm_recipients.xml", dcp::file_to_string(config->dkdm_recipients_file()));
        }
 
        zipper.close ();
 }
 
+
+void
+Config::load_from_zip(boost::filesystem::path zip_file)
+{
+       Unzipper unzipper(zip_file);
+       dcp::write_string_to_file(unzipper.get("config.xml"), config_write_file());
+
+       try {
+               dcp::write_string_to_file(unzipper.get("cinemas.xml"), cinemas_file());
+               dcp::write_string_to_file(unzipper.get("dkdm_recipient.xml"), dkdm_recipients_file());
+       } catch (std::runtime_error&) {}
+
+       read();
+
+       changed(Property::USE_ANY_SERVERS);
+       changed(Property::SERVERS);
+       changed(Property::CINEMAS);
+       changed(Property::DKDM_RECIPIENTS);
+       changed(Property::SOUND);
+       changed(Property::SOUND_OUTPUT);
+       changed(Property::PLAYER_CONTENT_DIRECTORY);
+       changed(Property::PLAYER_PLAYLIST_DIRECTORY);
+       changed(Property::PLAYER_DEBUG_LOG);
+       changed(Property::HISTORY);
+       changed(Property::SHOW_EXPERIMENTAL_AUDIO_PROCESSORS);
+       changed(Property::AUDIO_MAPPING);
+       changed(Property::AUTO_CROP_THRESHOLD);
+       changed(Property::ALLOW_SMPTE_BV20);
+       changed(Property::ISDCF_NAME_PART_LENGTH);
+       changed(Property::OTHER);
+}
+
+
+void
+Config::set_initial_path(string id, boost::filesystem::path path)
+{
+       auto iter = _initial_paths.find(id);
+       DCPOMATIC_ASSERT(iter != _initial_paths.end());
+       iter->second = path;
+       changed();
+}
+
+
+optional<boost::filesystem::path>
+Config::initial_path(string id) const
+{
+       auto iter = _initial_paths.find(id);
+       if (iter == _initial_paths.end()) {
+               return {};
+       }
+       return iter->second;
+}
+