Remove all use of add_child() from xmlpp.
[dcpomatic.git] / src / lib / config.cc
index ed00d274bd0e78664453ace902e927a9460e7e48..912a71c4d140da9ddd6b6a7f47191edcb4384132 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2022 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
 
 */
 
-#include "config.h"
-#include "filter.h"
-#include "ratio.h"
-#include "types.h"
-#include "log.h"
-#include "dcp_content_type.h"
-#include "colour_conversion.h"
+
 #include "cinema.h"
-#include "util.h"
-#include "cross.h"
-#include "film.h"
-#include "dkdm_wrapper.h"
+#include "colour_conversion.h"
 #include "compose.hpp"
-#include "crypto.h"
+#include "config.h"
+#include "constants.h"
+#include "cross.h"
+#include "dcp_content_type.h"
 #include "dkdm_recipient.h"
-#include <dcp/raw_convert.h>
-#include <dcp/name_format.h>
+#include "dkdm_wrapper.h"
+#include "film.h"
+#include "filter.h"
+#include "log.h"
+#include "ratio.h"
+#include "unzipper.h"
+#include "zipper.h"
 #include <dcp/certificate_chain.h>
+#include <dcp/name_format.h>
+#include <dcp/raw_convert.h>
 #include <libcxml/cxml.h>
 #include <glib.h>
 #include <libxml++/libxml++.h>
 
 #include "i18n.h"
 
-using std::vector;
+
 using std::cout;
+using std::dynamic_pointer_cast;
 using std::ifstream;
-using std::string;
 using std::list;
-using std::min;
+using std::make_shared;
 using std::max;
+using std::min;
 using std::remove;
-using std::exception;
-using std::cerr;
 using std::shared_ptr;
-using std::make_shared;
-using boost::optional;
-using std::dynamic_pointer_cast;
+using std::string;
+using std::vector;
 using boost::algorithm::trim;
+using boost::optional;
 using dcp::raw_convert;
 
+
 Config* Config::_instance = 0;
 int const Config::_current_version = 3;
-boost::signals2::signal<void ()> Config::FailedToLoad;
+boost::signals2::signal<void (Config::LoadFailure)> Config::FailedToLoad;
 boost::signals2::signal<void (string)> Config::Warning;
 boost::signals2::signal<bool (Config::BadReason)> Config::Bad;
 
+
 /** Construct default configuration */
 Config::Config ()
         /* DKDMs are not considered a thing to reset on set_defaults() */
        : _dkdms (new DKDMGroup ("root"))
+       , _default_kdm_duration (1, RoughDuration::Unit::WEEKS)
+       , _export(this)
 {
        set_defaults ();
 }
@@ -89,18 +93,20 @@ Config::set_defaults ()
        _servers.clear ();
        _only_servers_encode = false;
        _tms_protocol = FileTransferProtocol::SCP;
+       _tms_passive = true;
        _tms_ip = "";
        _tms_path = ".";
        _tms_user = "";
        _tms_password = "";
        _allow_any_dcp_frame_rate = false;
        _allow_any_container = false;
+       _allow_96khz_audio = false;
+       _use_all_audio_channels = false;
        _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;
@@ -127,13 +133,16 @@ Config::set_defaults ()
 #ifdef DCPOMATIC_WINDOWS
        _win32_console = false;
 #endif
-       _cinemas_file = path ("cinemas.xml");
-       _dkdm_recipients_file = path ("dkdm_recipients.xml");
+       /* At the moment we don't write these files anywhere new after a version change, so they will be read from
+        * ~/.config/dcpomatic2 (or equivalent) and written back there.
+        */
+       _cinemas_file = read_path ("cinemas.xml");
+       _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;
@@ -144,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
@@ -167,17 +177,38 @@ Config::set_defaults ()
        _gdc_username = optional<string>();
        _gdc_password = optional<string>();
        _player_mode = PLAYER_MODE_WINDOW;
+       _player_restricted_menus = false;
+       _playlist_editor_restricted_menus = false;
        _image_display = 0;
        _video_view_type = VIDEO_VIEW_SIMPLE;
        _respect_kdm_validity_periods = true;
-       _player_activity_log_file = boost::none;
        _player_debug_log_file = boost::none;
        _player_content_directory = boost::none;
        _player_playlist_directory = boost::none;
        _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;
+       _default_kdm_type = dcp::Formulation::MODIFIED_TRANSITIONAL_1;
+       _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);
@@ -190,6 +221,15 @@ Config::set_defaults ()
        set_kdm_email_to_default ();
        set_notification_email_to_default ();
        set_cover_sheet_to_default ();
+
+#ifdef DCPOMATIC_GROK
+       _grok = boost::none;
+#endif
+
+       _main_divider_sash_position = {};
+       _main_content_divider_sash_position = {};
+
+       _export.set_defaults();
 }
 
 void
@@ -204,6 +244,7 @@ Config::create_certificate_chain ()
 {
        return make_shared<dcp::CertificateChain> (
                openssl_path(),
+               CERTIFICATE_VALIDITY_PERIOD,
                "dcpomatic.com",
                "dcpomatic.com",
                ".dcpomatic.smpte-430-2.ROOT",
@@ -215,25 +256,55 @@ Config::create_certificate_chain ()
 void
 Config::backup ()
 {
-       /* Make a copy of the configuration */
-       try {
+       using namespace boost::filesystem;
+
+       auto copy_adding_number = [](path const& path_to_copy) {
+
+               auto add_number = [](path const& path, int number) {
+                       return String::compose("%1.%2", path, number);
+               };
+
                int n = 1;
-               while (n < 100 && boost::filesystem::exists(path(String::compose("config.xml.%1", n)))) {
+               while (n < 100 && exists(add_number(path_to_copy, n))) {
                        ++n;
                }
+               boost::system::error_code ec;
+               copy_file(path_to_copy, add_number(path_to_copy, n), ec);
+       };
+
+       /* Make a backup copy of any config.xml, cinemas.xml, dkdm_recipients.xml that we might be about
+        * to write over.  This is more intended for the situation where we have a corrupted config.xml,
+        * and decide to overwrite it with a new one (possibly losing important details in the corrupted
+        * file).  But we might as well back up the other files while we're about it.
+        */
 
-               boost::filesystem::copy_file(path("config.xml", false), path(String::compose("config.xml.%1", n), false));
-               boost::filesystem::copy_file(path("cinemas.xml", false), path(String::compose("cinemas.xml.%1", n), false));
-               boost::filesystem::copy_file(path("dkdm_recipients.xml", false), path(String::compose("dkdm_recipients.xml.%1", n), false));
-       } catch (...) {}
+       /* This uses the State::write_path stuff so, e.g. for a current version 2.16 we might copy
+        * ~/.config/dcpomatic2/2.16/config.xml to ~/.config/dcpomatic2/2.16/config.xml.1
+        */
+       copy_adding_number (config_write_file());
+
+       /* These do not use State::write_path, so whatever path is in the Config we will copy
+        * adding a number.
+        */
+       copy_adding_number (_cinemas_file);
+       copy_adding_number (_dkdm_recipients_file);
 }
 
 void
 Config::read ()
+{
+       read_config();
+       read_cinemas();
+       read_dkdm_recipients();
+}
+
+
+void
+Config::read_config()
 try
 {
        cxml::Document f ("Config");
-       f.read_file (config_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) {
@@ -273,6 +344,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");
@@ -280,16 +352,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);
 
@@ -315,6 +377,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();
        }
@@ -375,6 +451,8 @@ try
        _maximum_j2k_bandwidth = f.optional_number_child<int> ("MaximumJ2KBandwidth").get_value_or (250000000);
        _allow_any_dcp_frame_rate = f.optional_bool_child ("AllowAnyDCPFrameRate").get_value_or (false);
        _allow_any_container = f.optional_bool_child ("AllowAnyContainer").get_value_or (false);
+       _allow_96khz_audio = f.optional_bool_child("Allow96kHzAudio").get_value_or(false);
+       _use_all_audio_channels = f.optional_bool_child("UseAllAudioChannels").get_value_or(false);
        _show_experimental_audio_processors = f.optional_bool_child ("ShowExperimentalAudioProcessors").get_value_or (false);
 
        _log_types = f.optional_number_child<int> ("LogTypes").get_value_or (LogEntry::TYPE_GENERAL | LogEntry::TYPE_WARNING | LogEntry::TYPE_ERROR);
@@ -422,34 +500,21 @@ try
           of the nags.
        */
        for (auto i: f.node_children("Nagged")) {
-               auto const id = i->number_attribute<int>("Id");
+               auto const id = number_attribute<int>(i, "Id", "id");
                if (id >= 0 && id < NAG_COUNT) {
                        _nagged[id] = raw_convert<int>(i->content());
                }
        }
 
-       optional<BadReason> bad;
-
-       for (auto const& i: _signer_chain->unordered()) {
-               if (i.has_utf8_strings()) {
-                       bad = BAD_SIGNER_UTF8_STRINGS;
-               }
-       }
-
-       if (!_signer_chain->chain_valid() || !_signer_chain->private_key_valid()) {
-               bad = BAD_SIGNER_INCONSISTENT;
-       }
-
-       if (!_decryption_chain->chain_valid() || !_decryption_chain->private_key_valid()) {
-               bad = BAD_DECRYPTION_INCONSISTENT;
-       }
-
+       auto bad = check_certificates ();
        if (bad) {
                auto const remake = Bad(*bad);
                if (remake && *remake) {
                        switch (*bad) {
                        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:
@@ -464,13 +529,13 @@ try
                _dkdms = dynamic_pointer_cast<DKDMGroup> (DKDMBase::read (f.node_child("DKDMGroup")));
        } else {
                /* Old-style: one or more DKDM nodes */
-               _dkdms.reset (new DKDMGroup ("root"));
+               _dkdms = make_shared<DKDMGroup>("root");
                for (auto i: f.node_children("DKDM")) {
                        _dkdms->add (DKDMBase::read (i));
                }
        }
-       _cinemas_file = f.optional_string_child("CinemasFile").get_value_or (path ("cinemas.xml").string ());
-       _dkdm_recipients_file = f.optional_string_child("DKDMRecipientsFile").get_value_or (path("dkdm_recipients.xml").string());
+       _cinemas_file = f.optional_string_child("CinemasFile").get_value_or(read_path("cinemas.xml").string());
+       _dkdm_recipients_file = f.optional_string_child("DKDMRecipientsFile").get_value_or(read_path("dkdm_recipients.xml").string());
        _show_hints_before_make_dcp = f.optional_bool_child("ShowHintsBeforeMakeDCP").get_value_or (true);
        _confirm_kdm_email = f.optional_bool_child("ConfirmKDMEmail").get_value_or (true);
        _kdm_container_name_format = dcp::NameFormat (f.optional_string_child("KDMContainerNameFormat").get_value_or ("KDM %f %c"));
@@ -507,7 +572,7 @@ try
        _default_notify = f.optional_bool_child("DefaultNotify").get_value_or(false);
 
        for (auto i: f.node_children("Notification")) {
-               int const id = i->number_attribute<int>("Id");
+               int const id = number_attribute<int>(i, "Id", "id");
                if (id >= 0 && id < NOTIFICATION_COUNT) {
                        _notification[id] = raw_convert<int>(i->content());
                }
@@ -529,6 +594,9 @@ try
                _player_mode = PLAYER_MODE_DUAL;
        }
 
+       _player_restricted_menus = f.optional_bool_child("PlayerRestrictedMenus").get_value_or(false);
+       _playlist_editor_restricted_menus = f.optional_bool_child("PlaylistEditorRestrictedMenus").get_value_or(false);
+
        _image_display = f.optional_number_child<int>("ImageDisplay").get_value_or(0);
        auto vc = f.optional_string_child("VideoViewType");
        if (vc && *vc == "opengl") {
@@ -537,11 +605,6 @@ try
                _video_view_type = VIDEO_VIEW_SIMPLE;
        }
        _respect_kdm_validity_periods = f.optional_bool_child("RespectKDMValidityPeriods").get_value_or(true);
-       /* PlayerLogFile is old name */
-       _player_activity_log_file = f.optional_string_child("PlayerLogFile");
-       if (!_player_activity_log_file) {
-               _player_activity_log_file = f.optional_string_child("PlayerActivityLogFile");
-       }
        _player_debug_log_file = f.optional_string_child("PlayerDebugLogFile");
        _player_content_directory = f.optional_string_child("PlayerContentDirectory");
        _player_playlist_directory = f.optional_string_child("PlayerPlaylistDirectory");
@@ -560,39 +623,96 @@ try
                } catch (std::runtime_error& e) {}
        }
 
-       _add_files_path = f.optional_string_child("AddFilesPath");
-
-       if (boost::filesystem::exists (_cinemas_file)) {
-               cxml::Document f ("Cinemas");
-               f.read_file (_cinemas_file);
-               read_cinemas (f);
+       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);
+       _default_kdm_type = dcp::string_to_formulation(f.optional_string_child("DefaultKDMType").get_value_or("modified-transitional-1"));
+       if (auto duration = f.optional_node_child("DefaultKDMDuration")) {
+               _default_kdm_duration = RoughDuration(duration);
+       } else {
+               _default_kdm_duration = RoughDuration(1, RoughDuration::Unit::WEEKS);
+       }
+       _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);
 
-       if (boost::filesystem::exists (_dkdm_recipients_file)) {
-               cxml::Document f ("DKDMRecipients");
-               f.read_file (_dkdm_recipients_file);
-               read_dkdm_recipients (f);
+#ifdef DCPOMATIC_GROK
+       if (auto grok = f.optional_node_child("Grok")) {
+               _grok = Grok(grok);
        }
+#endif
+
+       _export.read(f.optional_node_child("Export"));
 }
 catch (...) {
-       if (have_existing ("config.xml")) {
+       if (have_existing("config.xml")) {
                backup ();
                /* We have a config file but it didn't load */
-               FailedToLoad ();
+               FailedToLoad(LoadFailure::CONFIG);
        }
        set_defaults ();
        /* Make a new set of signing certificates and key */
        _signer_chain = create_certificate_chain ();
        /* And similar for decryption of KDMs */
        _decryption_chain = create_certificate_chain ();
-       write ();
+       write_config();
 }
 
+
+void
+Config::read_cinemas()
+{
+       if (dcp::filesystem::exists(_cinemas_file)) {
+               try {
+                       cxml::Document f("Cinemas");
+                       f.read_file(dcp::filesystem::fix_long_path(_cinemas_file));
+                       read_cinemas(f);
+               } catch (...) {
+                       backup();
+                       FailedToLoad(LoadFailure::CINEMAS);
+                       write_cinemas();
+               }
+       }
+}
+
+
+void
+Config::read_dkdm_recipients()
+{
+       if (dcp::filesystem::exists(_dkdm_recipients_file)) {
+               try {
+                       cxml::Document f("DKDMRecipients");
+                       f.read_file(dcp::filesystem::fix_long_path(_dkdm_recipients_file));
+                       read_dkdm_recipients(f);
+               } catch (...) {
+                       backup();
+                       FailedToLoad(LoadFailure::DKDM_RECIPIENTS);
+                       write_dkdm_recipients();
+               }
+       }
+}
+
+
 /** @return Singleton instance */
 Config *
 Config::instance ()
 {
-       if (_instance == 0) {
+       if (_instance == nullptr) {
                _instance = new Config;
                _instance->read ();
        }
@@ -616,210 +736,218 @@ Config::write_config () const
        auto root = doc.create_root_node ("Config");
 
        /* [XML] Version The version number of the configuration file format. */
-       root->add_child("Version")->add_child_text (raw_convert<string>(_current_version));
+       cxml::add_text_child(root, "Version", raw_convert<string>(_current_version));
        /* [XML] MasterEncodingThreads Number of encoding threads to use when running as master. */
-       root->add_child("MasterEncodingThreads")->add_child_text (raw_convert<string> (_master_encoding_threads));
+       cxml::add_text_child(root, "MasterEncodingThreads", raw_convert<string>(_master_encoding_threads));
        /* [XML] ServerEncodingThreads Number of encoding threads to use when running as server. */
-       root->add_child("ServerEncodingThreads")->add_child_text (raw_convert<string> (_server_encoding_threads));
+       cxml::add_text_child(root, "ServerEncodingThreads", raw_convert<string>(_server_encoding_threads));
        if (_default_directory) {
                /* [XML:opt] DefaultDirectory Default directory when creating a new film in the GUI. */
-               root->add_child("DefaultDirectory")->add_child_text (_default_directory->string ());
+               cxml::add_text_child(root, "DefaultDirectory", _default_directory->string());
        }
        /* [XML] ServerPortBase Port number to use for frame encoding requests.  <code>ServerPortBase</code> + 1 and
           <code>ServerPortBase</code> + 2 are used for querying servers.  <code>ServerPortBase</code> + 3 is used
           by the batch converter to listen for job requests.
        */
-       root->add_child("ServerPortBase")->add_child_text (raw_convert<string> (_server_port_base));
+       cxml::add_text_child(root, "ServerPortBase", raw_convert<string>(_server_port_base));
        /* [XML] UseAnyServers 1 to broadcast to look for encoding servers to use, 0 to use only those configured. */
-       root->add_child("UseAnyServers")->add_child_text (_use_any_servers ? "1" : "0");
+       cxml::add_text_child(root, "UseAnyServers", _use_any_servers ? "1" : "0");
 
        for (auto i: _servers) {
                /* [XML:opt] Server IP address or hostname of an encoding server to use; you can use as many of these tags
                   as you like.
                */
-               root->add_child("Server")->add_child_text (i);
+               cxml::add_text_child(root, "Server", i);
        }
 
        /* [XML] OnlyServersEncode 1 to set the master to do decoding of source content no JPEG2000 encoding; all encoding
           is done by the encoding servers.  0 to set the master to do some encoding as well as coordinating the job.
        */
-       root->add_child("OnlyServersEncode")->add_child_text (_only_servers_encode ? "1" : "0");
+       cxml::add_text_child(root, "OnlyServersEncode", _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)));
+       cxml::add_text_child(root, "TMSProtocol", raw_convert<string>(static_cast<int>(_tms_protocol)));
+       /* [XML] TMSPassive True to use PASV mode with TMS FTP connections. */
+       cxml::add_text_child(root, "TMSPassive", _tms_passive ? "1" : "0");
        /* [XML] TMSIP IP address of TMS. */
-       root->add_child("TMSIP")->add_child_text (_tms_ip);
+       cxml::add_text_child(root, "TMSIP", _tms_ip);
        /* [XML] TMSPath Path on the TMS to copy files to. */
-       root->add_child("TMSPath")->add_child_text (_tms_path);
+       cxml::add_text_child(root, "TMSPath", _tms_path);
        /* [XML] TMSUser Username to log into the TMS with. */
-       root->add_child("TMSUser")->add_child_text (_tms_user);
+       cxml::add_text_child(root, "TMSUser", _tms_user);
        /* [XML] TMSPassword Password to log into the TMS with. */
-       root->add_child("TMSPassword")->add_child_text (_tms_password);
+       cxml::add_text_child(root, "TMSPassword", _tms_password);
        if (_language) {
                /* [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 ());
+               cxml::add_text_child(root, "Language", _language.get());
        }
        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 ());
+               cxml::add_text_child(root, "DefaultDCPContentType", _default_dcp_content_type->isdcf_name());
        }
        /* [XML] DefaultDCPAudioChannels Default number of audio channels to use when creating new films. */
-       root->add_child("DefaultDCPAudioChannels")->add_child_text (raw_convert<string> (_default_dcp_audio_channels));
+       cxml::add_text_child(root, "DefaultDCPAudioChannels", raw_convert<string>(_default_dcp_audio_channels));
        /* [XML] DCPIssuer Issuer text to write into CPL files. */
-       root->add_child("DCPIssuer")->add_child_text (_dcp_issuer);
+       cxml::add_text_child(root, "DCPIssuer", _dcp_issuer);
        /* [XML] DCPCreator Creator text to write into CPL files. */
-       root->add_child("DCPCreator")->add_child_text (_dcp_creator);
+       cxml::add_text_child(root, "DCPCreator", _dcp_creator);
        /* [XML] Company name to write into MXF files. */
-       root->add_child("DCPCompanyName")->add_child_text (_dcp_company_name);
+       cxml::add_text_child(root, "DCPCompanyName", _dcp_company_name);
        /* [XML] Product name to write into MXF files. */
-       root->add_child("DCPProductName")->add_child_text (_dcp_product_name);
+       cxml::add_text_child(root, "DCPProductName", _dcp_product_name);
        /* [XML] Product version to write into MXF files. */
-       root->add_child("DCPProductVersion")->add_child_text (_dcp_product_version);
+       cxml::add_text_child(root, "DCPProductVersion", _dcp_product_version);
        /* [XML] Comment to write into JPEG2000 data. */
-       root->add_child("DCPJ2KComment")->add_child_text (_dcp_j2k_comment);
+       cxml::add_text_child(root, "DCPJ2KComment", _dcp_j2k_comment);
        /* [XML] UploadAfterMakeDCP 1 to upload to a TMS after making a DCP, 0 for no upload. */
-       root->add_child("UploadAfterMakeDCP")->add_child_text (_upload_after_make_dcp ? "1" : "0");
+       cxml::add_text_child(root, "UploadAfterMakeDCP", _upload_after_make_dcp ? "1" : "0");
 
        /* [XML] DefaultStillLength Default length (in seconds) for still images in new films. */
-       root->add_child("DefaultStillLength")->add_child_text (raw_convert<string> (_default_still_length));
+       cxml::add_text_child(root, "DefaultStillLength", raw_convert<string>(_default_still_length));
        /* [XML] DefaultJ2KBandwidth Default bitrate (in bits per second) for JPEG2000 data in new films. */
-       root->add_child("DefaultJ2KBandwidth")->add_child_text (raw_convert<string> (_default_j2k_bandwidth));
+       cxml::add_text_child(root, "DefaultJ2KBandwidth", raw_convert<string>(_default_j2k_bandwidth));
        /* [XML] DefaultAudioDelay Default delay to apply to audio (positive moves audio later) in milliseconds. */
-       root->add_child("DefaultAudioDelay")->add_child_text (raw_convert<string> (_default_audio_delay));
+       cxml::add_text_child(root, "DefaultAudioDelay", 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");
+       cxml::add_text_child(root, "DefaultInterop", _default_interop ? "1" : "0");
+       if (_default_audio_language) {
+               /* [XML] DefaultAudioLanguage Default audio language to use for new films */
+               cxml::add_text_child(root, "DefaultAudioLanguage", _default_audio_language->to_string());
+       }
+       if (_default_territory) {
+               /* [XML] DefaultTerritory Default territory to use for new films */
+               cxml::add_text_child(root, "DefaultTerritory", _default_territory->subtag());
+       }
        for (auto const& i: _default_metadata) {
-               auto c = root->add_child("DefaultMetadata");
+               auto c = cxml::add_child(root, "DefaultMetadata");
                c->set_attribute("key", i.first);
                c->add_child_text(i.second);
        }
        if (_default_kdm_directory) {
                /* [XML:opt] DefaultKDMDirectory Default directory to write KDMs to. */
-               root->add_child("DefaultKDMDirectory")->add_child_text (_default_kdm_directory->string ());
+               cxml::add_text_child(root, "DefaultKDMDirectory", _default_kdm_directory->string ());
        }
+       _default_kdm_duration.as_xml(cxml::add_child(root, "DefaultKDMDuration"));
        /* [XML] MailServer Hostname of SMTP server to use. */
-       root->add_child("MailServer")->add_child_text (_mail_server);
+       cxml::add_text_child(root, "MailServer", _mail_server);
        /* [XML] MailPort Port number to use on SMTP server. */
-       root->add_child("MailPort")->add_child_text (raw_convert<string> (_mail_port));
+       cxml::add_text_child(root, "MailPort", raw_convert<string>(_mail_port));
        /* [XML] MailProtocol Protocol to use on SMTP server (Auto, Plain, STARTTLS or SSL) */
        switch (_mail_protocol) {
        case EmailProtocol::AUTO:
-               root->add_child("MailProtocol")->add_child_text("Auto");
+               cxml::add_text_child(root, "MailProtocol", "Auto");
                break;
        case EmailProtocol::PLAIN:
-               root->add_child("MailProtocol")->add_child_text("Plain");
+               cxml::add_text_child(root, "MailProtocol", "Plain");
                break;
        case EmailProtocol::STARTTLS:
-               root->add_child("MailProtocol")->add_child_text("STARTTLS");
+               cxml::add_text_child(root, "MailProtocol", "STARTTLS");
                break;
        case EmailProtocol::SSL:
-               root->add_child("MailProtocol")->add_child_text("SSL");
+               cxml::add_text_child(root, "MailProtocol", "SSL");
                break;
        }
        /* [XML] MailUser Username to use on SMTP server. */
-       root->add_child("MailUser")->add_child_text (_mail_user);
+       cxml::add_text_child(root, "MailUser", _mail_user);
        /* [XML] MailPassword Password to use on SMTP server. */
-       root->add_child("MailPassword")->add_child_text (_mail_password);
+       cxml::add_text_child(root, "MailPassword", _mail_password);
 
        /* [XML] KDMSubject Subject to use for KDM emails. */
-       root->add_child("KDMSubject")->add_child_text (_kdm_subject);
+       cxml::add_text_child(root, "KDMSubject", _kdm_subject);
        /* [XML] KDMFrom From address to use for KDM emails. */
-       root->add_child("KDMFrom")->add_child_text (_kdm_from);
+       cxml::add_text_child(root, "KDMFrom", _kdm_from);
        for (auto i: _kdm_cc) {
                /* [XML] KDMCC CC address to use for KDM emails; you can use as many of these tags as you like. */
-               root->add_child("KDMCC")->add_child_text (i);
+               cxml::add_text_child(root, "KDMCC", i);
        }
        /* [XML] KDMBCC BCC address to use for KDM emails. */
-       root->add_child("KDMBCC")->add_child_text (_kdm_bcc);
+       cxml::add_text_child(root, "KDMBCC", _kdm_bcc);
        /* [XML] KDMEmail Text of KDM email. */
-       root->add_child("KDMEmail")->add_child_text (_kdm_email);
+       cxml::add_text_child(root, "KDMEmail", _kdm_email);
 
        /* [XML] NotificationSubject Subject to use for notification emails. */
-       root->add_child("NotificationSubject")->add_child_text (_notification_subject);
+       cxml::add_text_child(root, "NotificationSubject", _notification_subject);
        /* [XML] NotificationFrom From address to use for notification emails. */
-       root->add_child("NotificationFrom")->add_child_text (_notification_from);
+       cxml::add_text_child(root, "NotificationFrom", _notification_from);
        /* [XML] NotificationFrom To address to use for notification emails. */
-       root->add_child("NotificationTo")->add_child_text (_notification_to);
+       cxml::add_text_child(root, "NotificationTo", _notification_to);
        for (auto i: _notification_cc) {
                /* [XML] NotificationCC CC address to use for notification emails; you can use as many of these tags as you like. */
-               root->add_child("NotificationCC")->add_child_text (i);
+               cxml::add_text_child(root, "NotificationCC", i);
        }
        /* [XML] NotificationBCC BCC address to use for notification emails. */
-       root->add_child("NotificationBCC")->add_child_text (_notification_bcc);
+       cxml::add_text_child(root, "NotificationBCC", _notification_bcc);
        /* [XML] NotificationEmail Text of notification email. */
-       root->add_child("NotificationEmail")->add_child_text (_notification_email);
+       cxml::add_text_child(root, "NotificationEmail", _notification_email);
 
        /* [XML] CheckForUpdates 1 to check dcpomatic.com for new versions, 0 to check only on request. */
-       root->add_child("CheckForUpdates")->add_child_text (_check_for_updates ? "1" : "0");
+       cxml::add_text_child(root, "CheckForUpdates", _check_for_updates ? "1" : "0");
        /* [XML] CheckForUpdates 1 to check dcpomatic.com for new text versions, 0 to check only on request. */
-       root->add_child("CheckForTestUpdates")->add_child_text (_check_for_test_updates ? "1" : "0");
+       cxml::add_text_child(root, "CheckForTestUpdates", _check_for_test_updates ? "1" : "0");
 
        /* [XML] MaximumJ2KBandwidth Maximum J2K bandwidth (in bits per second) that can be specified in the GUI. */
-       root->add_child("MaximumJ2KBandwidth")->add_child_text (raw_convert<string> (_maximum_j2k_bandwidth));
+       cxml::add_text_child(root, "MaximumJ2KBandwidth", raw_convert<string>(_maximum_j2k_bandwidth));
        /* [XML] AllowAnyDCPFrameRate 1 to allow users to specify any frame rate when creating DCPs, 0 to limit the GUI to standard rates. */
-       root->add_child("AllowAnyDCPFrameRate")->add_child_text (_allow_any_dcp_frame_rate ? "1" : "0");
+       cxml::add_text_child(root, "AllowAnyDCPFrameRate", _allow_any_dcp_frame_rate ? "1" : "0");
        /* [XML] AllowAnyContainer 1 to allow users to user any container ratio for their DCP, 0 to limit the GUI to DCI Flat/Scope */
-       root->add_child("AllowAnyContainer")->add_child_text (_allow_any_container ? "1" : "0");
+       cxml::add_text_child(root, "AllowAnyContainer", _allow_any_container ? "1" : "0");
+       /* [XML] Allow96kHzAudio 1 to allow users to make DCPs with 96kHz audio, 0 to always make 48kHz DCPs */
+       cxml::add_text_child(root, "Allow96kHzAudio", _allow_96khz_audio ? "1" : "0");
+       /* [XML] UseAllAudioChannels 1 to allow users to map audio to all 16 DCP channels, 0 to limit to the channels used in standard DCPs */
+       cxml::add_text_child(root, "UseAllAudioChannels", _use_all_audio_channels ? "1" : "0");
        /* [XML] ShowExperimentalAudioProcessors 1 to offer users the (experimental) audio upmixer processors, 0 to hide them */
-       root->add_child("ShowExperimentalAudioProcessors")->add_child_text (_show_experimental_audio_processors ? "1" : "0");
+       cxml::add_text_child(root, "ShowExperimentalAudioProcessors", _show_experimental_audio_processors ? "1" : "0");
        /* [XML] LogTypes Types of logging to write; a bitfield where 1 is general notes, 2 warnings, 4 errors, 8 debug information related
           to 3D, 16 debug information related to encoding, 32 debug information for timing purposes, 64 debug information related
           to sending email, 128 debug information related to the video view, 256 information about disk writing, 512 debug information
           related to the player, 1024 debug information related to audio analyses.
        */
-       root->add_child("LogTypes")->add_child_text (raw_convert<string> (_log_types));
+       cxml::add_text_child(root, "LogTypes", raw_convert<string> (_log_types));
        /* [XML] AnalyseEBUR128 1 to do EBUR128 analyses when analysing audio, otherwise 0. */
-       root->add_child("AnalyseEBUR128")->add_child_text (_analyse_ebur128 ? "1" : "0");
+       cxml::add_text_child(root, "AnalyseEBUR128", _analyse_ebur128 ? "1" : "0");
        /* [XML] AutomaticAudioAnalysis 1 to run audio analysis automatically when audio content is added to the film, otherwise 0. */
-       root->add_child("AutomaticAudioAnalysis")->add_child_text (_automatic_audio_analysis ? "1" : "0");
+       cxml::add_text_child(root, "AutomaticAudioAnalysis", _automatic_audio_analysis ? "1" : "0");
 #ifdef DCPOMATIC_WINDOWS
        if (_win32_console) {
                /* [XML] Win32Console 1 to open a console when running on Windows, otherwise 0.
                 * We only write this if it's true, which is a bit of a hack to allow unit tests to work
                 * more easily on Windows (without a platform-specific reference in config_write_utf8_test)
                 */
-               root->add_child("Win32Console")->add_child_text ("1");
+               cxml::add_text_child(root, "Win32Console", "1");
        }
 #endif
 
        /* [XML] Signer Certificate chain and private key to use when signing DCPs and KDMs.  Should contain <code>&lt;Certificate&gt;</code>
           tags in order and a <code>&lt;PrivateKey&gt;</code> tag all containing PEM-encoded certificates or private keys as appropriate.
        */
-       auto signer = root->add_child ("Signer");
+       auto signer = cxml::add_child(root, "Signer");
        DCPOMATIC_ASSERT (_signer_chain);
        for (auto const& i: _signer_chain->unordered()) {
-               signer->add_child("Certificate")->add_child_text (i.certificate (true));
+               cxml::add_text_child(signer, "Certificate", i.certificate (true));
        }
-       signer->add_child("PrivateKey")->add_child_text (_signer_chain->key().get ());
+       cxml::add_text_child(signer, "PrivateKey", _signer_chain->key().get ());
 
        /* [XML] Decryption Certificate chain and private key to use when decrypting KDMs */
-       auto decryption = root->add_child ("Decryption");
+       auto decryption = cxml::add_child(root, "Decryption");
        DCPOMATIC_ASSERT (_decryption_chain);
        for (auto const& i: _decryption_chain->unordered()) {
-               decryption->add_child("Certificate")->add_child_text (i.certificate (true));
+               cxml::add_text_child(decryption, "Certificate", i.certificate (true));
        }
-       decryption->add_child("PrivateKey")->add_child_text (_decryption_chain->key().get ());
+       cxml::add_text_child(decryption, "PrivateKey", _decryption_chain->key().get());
 
        /* [XML] History Filename of DCP to present in the <guilabel>File</guilabel> menu of the GUI; there can be more than one
           of these tags.
        */
        for (auto i: _history) {
-               root->add_child("History")->add_child_text (i.string ());
+               cxml::add_text_child(root, "History", i.string());
        }
 
        /* [XML] History Filename of DCP to present in the <guilabel>File</guilabel> menu of the player; there can be more than one
           of these tags.
        */
        for (auto i: _player_history) {
-               root->add_child("PlayerHistory")->add_child_text (i.string ());
+               cxml::add_text_child(root, "PlayerHistory", i.string());
        }
 
        /* [XML] DKDMGroup A group of DKDMs, each with a <code>Name</code> attribute, containing other <code>&lt;DKDMGroup&gt;</code>
@@ -829,53 +957,53 @@ Config::write_config () const
        _dkdms->as_xml (root);
 
        /* [XML] CinemasFile Filename of cinemas list file. */
-       root->add_child("CinemasFile")->add_child_text (_cinemas_file.string());
+       cxml::add_text_child(root, "CinemasFile", _cinemas_file.string());
        /* [XML] DKDMRecipientsFile Filename of DKDM recipients list file. */
-       root->add_child("DKDMRecipientsFile")->add_child_text (_dkdm_recipients_file.string());
+       cxml::add_text_child(root, "DKDMRecipientsFile", _dkdm_recipients_file.string());
        /* [XML] ShowHintsBeforeMakeDCP 1 to show hints in the GUI before making a DCP, otherwise 0. */
-       root->add_child("ShowHintsBeforeMakeDCP")->add_child_text (_show_hints_before_make_dcp ? "1" : "0");
+       cxml::add_text_child(root, "ShowHintsBeforeMakeDCP", _show_hints_before_make_dcp ? "1" : "0");
        /* [XML] ConfirmKDMEmail 1 to confirm before sending KDM emails in the GUI, otherwise 0. */
-       root->add_child("ConfirmKDMEmail")->add_child_text (_confirm_kdm_email ? "1" : "0");
+       cxml::add_text_child(root, "ConfirmKDMEmail", _confirm_kdm_email ? "1" : "0");
        /* [XML] KDMFilenameFormat Format for KDM filenames. */
-       root->add_child("KDMFilenameFormat")->add_child_text (_kdm_filename_format.specification ());
+       cxml::add_text_child(root, "KDMFilenameFormat", _kdm_filename_format.specification());
        /* [XML] KDMFilenameFormat Format for DKDM filenames. */
-       root->add_child("DKDMFilenameFormat")->add_child_text(_dkdm_filename_format.specification());
+       cxml::add_text_child(root, "DKDMFilenameFormat", _dkdm_filename_format.specification());
        /* [XML] KDMContainerNameFormat Format for KDM containers (directories or ZIP files). */
-       root->add_child("KDMContainerNameFormat")->add_child_text (_kdm_container_name_format.specification ());
+       cxml::add_text_child(root, "KDMContainerNameFormat", _kdm_container_name_format.specification());
        /* [XML] DCPMetadataFilenameFormat Format for DCP metadata filenames. */
-       root->add_child("DCPMetadataFilenameFormat")->add_child_text (_dcp_metadata_filename_format.specification ());
+       cxml::add_text_child(root, "DCPMetadataFilenameFormat", _dcp_metadata_filename_format.specification());
        /* [XML] DCPAssetFilenameFormat Format for DCP asset filenames. */
-       root->add_child("DCPAssetFilenameFormat")->add_child_text (_dcp_asset_filename_format.specification ());
+       cxml::add_text_child(root, "DCPAssetFilenameFormat", _dcp_asset_filename_format.specification());
        /* [XML] JumpToSelected 1 to make the GUI jump to the start of content when it is selected, otherwise 0. */
-       root->add_child("JumpToSelected")->add_child_text (_jump_to_selected ? "1" : "0");
+       cxml::add_text_child(root, "JumpToSelected", _jump_to_selected ? "1" : "0");
        /* [XML] Nagged 1 if a particular nag screen has been shown and should not be shown again, otherwise 0. */
        for (int i = 0; i < NAG_COUNT; ++i) {
-               xmlpp::Element* e = root->add_child ("Nagged");
-               e->set_attribute ("Id", raw_convert<string>(i));
+               auto e = cxml::add_child(root, "Nagged");
+               e->set_attribute("id", raw_convert<string>(i));
                e->add_child_text (_nagged[i] ? "1" : "0");
        }
        /* [XML] PreviewSound 1 to use sound in the GUI preview and player, otherwise 0. */
-       root->add_child("PreviewSound")->add_child_text (_sound ? "1" : "0");
+       cxml::add_text_child(root, "PreviewSound", _sound ? "1" : "0");
        if (_sound_output) {
                /* [XML:opt] PreviewSoundOutput Name of the audio output to use. */
-               root->add_child("PreviewSoundOutput")->add_child_text (_sound_output.get());
+               cxml::add_text_child(root, "PreviewSoundOutput", _sound_output.get());
        }
        /* [XML] CoverSheet Text of the cover sheet to write when making DCPs. */
-       root->add_child("CoverSheet")->add_child_text (_cover_sheet);
+       cxml::add_text_child(root, "CoverSheet", _cover_sheet);
        if (_last_player_load_directory) {
-               root->add_child("LastPlayerLoadDirectory")->add_child_text(_last_player_load_directory->string());
+               cxml::add_text_child(root, "LastPlayerLoadDirectory", _last_player_load_directory->string());
        }
        /* [XML] LastKDMWriteType Last type of KDM-write: <code>flat</code> for a flat file, <code>folder</code> for a folder or <code>zip</code> for a ZIP file. */
        if (_last_kdm_write_type) {
                switch (_last_kdm_write_type.get()) {
                case KDM_WRITE_FLAT:
-                       root->add_child("LastKDMWriteType")->add_child_text("flat");
+                       cxml::add_text_child(root, "LastKDMWriteType", "flat");
                        break;
                case KDM_WRITE_FOLDER:
-                       root->add_child("LastKDMWriteType")->add_child_text("folder");
+                       cxml::add_text_child(root, "LastKDMWriteType", "folder");
                        break;
                case KDM_WRITE_ZIP:
-                       root->add_child("LastKDMWriteType")->add_child_text("zip");
+                       cxml::add_text_child(root, "LastKDMWriteType", "zip");
                        break;
                }
        }
@@ -883,133 +1011,171 @@ Config::write_config () const
        if (_last_dkdm_write_type) {
                switch (_last_dkdm_write_type.get()) {
                case DKDM_WRITE_INTERNAL:
-                       root->add_child("LastDKDMWriteType")->add_child_text("internal");
+                       cxml::add_text_child(root, "LastDKDMWriteType", "internal");
                        break;
                case DKDM_WRITE_FILE:
-                       root->add_child("LastDKDMWriteType")->add_child_text("file");
+                       cxml::add_text_child(root, "LastDKDMWriteType", "file");
                        break;
                }
        }
        /* [XML] FramesInMemoryMultiplier value to multiply the encoding threads count by to get the maximum number of
           frames to be held in memory at once.
        */
-       root->add_child("FramesInMemoryMultiplier")->add_child_text(raw_convert<string>(_frames_in_memory_multiplier));
+       cxml::add_text_child(root, "FramesInMemoryMultiplier", raw_convert<string>(_frames_in_memory_multiplier));
 
        /* [XML] DecodeReduction power of 2 to reduce DCP images by before decoding in the player. */
        if (_decode_reduction) {
-               root->add_child("DecodeReduction")->add_child_text(raw_convert<string>(_decode_reduction.get()));
+               cxml::add_text_child(root, "DecodeReduction", raw_convert<string>(_decode_reduction.get()));
        }
 
        /* [XML] DefaultNotify 1 to default jobs to notify when complete, otherwise 0. */
-       root->add_child("DefaultNotify")->add_child_text(_default_notify ? "1" : "0");
+       cxml::add_text_child(root, "DefaultNotify", _default_notify ? "1" : "0");
 
        /* [XML] Notification 1 if a notification type is enabled, otherwise 0. */
        for (int i = 0; i < NOTIFICATION_COUNT; ++i) {
-               xmlpp::Element* e = root->add_child ("Notification");
-               e->set_attribute ("Id", raw_convert<string>(i));
+               auto e = cxml::add_child(root, "Notification");
+               e->set_attribute ("id", raw_convert<string>(i));
                e->add_child_text (_notification[i] ? "1" : "0");
        }
 
        if (_barco_username) {
                /* [XML] BarcoUsername Username for logging into Barco's servers when downloading server certificates. */
-               root->add_child("BarcoUsername")->add_child_text(*_barco_username);
+               cxml::add_text_child(root, "BarcoUsername", *_barco_username);
        }
        if (_barco_password) {
                /* [XML] BarcoPassword Password for logging into Barco's servers when downloading server certificates. */
-               root->add_child("BarcoPassword")->add_child_text(*_barco_password);
+               cxml::add_text_child(root, "BarcoPassword", *_barco_password);
        }
 
        if (_christie_username) {
                /* [XML] ChristieUsername Username for logging into Christie's servers when downloading server certificates. */
-               root->add_child("ChristieUsername")->add_child_text(*_christie_username);
+               cxml::add_text_child(root, "ChristieUsername", *_christie_username);
        }
        if (_christie_password) {
                /* [XML] ChristiePassword Password for logging into Christie's servers when downloading server certificates. */
-               root->add_child("ChristiePassword")->add_child_text(*_christie_password);
+               cxml::add_text_child(root, "ChristiePassword", *_christie_password);
        }
 
        if (_gdc_username) {
                /* [XML] GDCUsername Username for logging into GDC's servers when downloading server certificates. */
-               root->add_child("GDCUsername")->add_child_text(*_gdc_username);
+               cxml::add_text_child(root, "GDCUsername", *_gdc_username);
        }
        if (_gdc_password) {
                /* [XML] GDCPassword Password for logging into GDC's servers when downloading server certificates. */
-               root->add_child("GDCPassword")->add_child_text(*_gdc_password);
+               cxml::add_text_child(root, "GDCPassword", *_gdc_password);
        }
 
        /* [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:
-               root->add_child("PlayerMode")->add_child_text("window");
+               cxml::add_text_child(root, "PlayerMode", "window");
                break;
        case PLAYER_MODE_FULL:
-               root->add_child("PlayerMode")->add_child_text("full");
+               cxml::add_text_child(root, "PlayerMode", "full");
                break;
        case PLAYER_MODE_DUAL:
-               root->add_child("PlayerMode")->add_child_text("dual");
+               cxml::add_text_child(root, "PlayerMode", "dual");
                break;
        }
 
+       if (_player_restricted_menus) {
+               cxml::add_text_child(root, "PlayerRestrictedMenus", "1");
+       }
+
+       if (_playlist_editor_restricted_menus) {
+               root->add_child("PlaylistEditorRestrictedMenus")->add_child_text("1");
+       }
+
        /* [XML] ImageDisplay Screen number to put image on in dual-screen player mode. */
-       root->add_child("ImageDisplay")->add_child_text(raw_convert<string>(_image_display));
+       cxml::add_text_child(root, "ImageDisplay", raw_convert<string>(_image_display));
        switch (_video_view_type) {
        case VIDEO_VIEW_SIMPLE:
-               root->add_child("VideoViewType")->add_child_text("simple");
+               cxml::add_text_child(root, "VideoViewType", "simple");
                break;
        case VIDEO_VIEW_OPENGL:
-               root->add_child("VideoViewType")->add_child_text("opengl");
+               cxml::add_text_child(root, "VideoViewType", "opengl");
                break;
        }
        /* [XML] RespectKDMValidityPeriods 1 to refuse to use KDMs that are out of date, 0 to ignore KDM dates. */
-       root->add_child("RespectKDMValidityPeriods")->add_child_text(_respect_kdm_validity_periods ? "1" : "0");
-       if (_player_activity_log_file) {
-               /* [XML] PlayerLogFile Filename to use for player activity logs (e.g starting, stopping, playlist loads) */
-               root->add_child("PlayerActivityLogFile")->add_child_text(_player_activity_log_file->string());
-       }
+       cxml::add_text_child(root, "RespectKDMValidityPeriods", _respect_kdm_validity_periods ? "1" : "0");
        if (_player_debug_log_file) {
-               /* [XML] PlayerLogFile Filename to use for player debug logs */
-               root->add_child("PlayerDebugLogFile")->add_child_text(_player_debug_log_file->string());
+               /* [XML] PlayerLogFile Filename to use for player debug logs. */
+               cxml::add_text_child(root, "PlayerDebugLogFile", _player_debug_log_file->string());
        }
        if (_player_content_directory) {
                /* [XML] PlayerContentDirectory Directory to use for player content in the dual-screen mode. */
-               root->add_child("PlayerContentDirectory")->add_child_text(_player_content_directory->string());
+               cxml::add_text_child(root, "PlayerContentDirectory", _player_content_directory->string());
        }
        if (_player_playlist_directory) {
                /* [XML] PlayerPlaylistDirectory Directory to use for player playlists in the dual-screen mode. */
-               root->add_child("PlayerPlaylistDirectory")->add_child_text(_player_playlist_directory->string());
+               cxml::add_text_child(root, "PlayerPlaylistDirectory", _player_playlist_directory->string());
        }
        if (_player_kdm_directory) {
                /* [XML] PlayerKDMDirectory Directory to use for player KDMs in the dual-screen mode. */
-               root->add_child("PlayerKDMDirectory")->add_child_text(_player_kdm_directory->string());
+               cxml::add_text_child(root, "PlayerKDMDirectory", _player_kdm_directory->string());
        }
        if (_audio_mapping) {
-               _audio_mapping->as_xml (root->add_child("AudioMapping"));
+               _audio_mapping->as_xml(cxml::add_child(root, "AudioMapping"));
        }
        for (auto const& i: _custom_languages) {
-               root->add_child("CustomLanguage")->add_child_text(i.to_string());
+               cxml::add_text_child(root, "CustomLanguage", i.to_string());
+       }
+       for (auto const& initial: _initial_paths) {
+               if (initial.second) {
+                       cxml::add_text_child(root, initial.first, initial.second->string());
+               }
+       }
+       cxml::add_text_child(root, "UseISDCFNameByDefault", _use_isdcf_name_by_default ? "1" : "0");
+       cxml::add_text_child(root, "WriteKDMsToDisk", _write_kdms_to_disk ? "1" : "0");
+       cxml::add_text_child(root, "EmailKDMs", _email_kdms ? "1" : "0");
+       cxml::add_text_child(root, "DefaultKDMType", dcp::formulation_to_string(_default_kdm_type));
+       cxml::add_text_child(root, "AutoCropThreshold", raw_convert<string>(_auto_crop_threshold));
+       if (_last_release_notes_version) {
+               cxml::add_text_child(root, "LastReleaseNotesVersion", *_last_release_notes_version);
+       }
+       if (_main_divider_sash_position) {
+               cxml::add_text_child(root, "MainDividerSashPosition", raw_convert<string>(*_main_divider_sash_position));
        }
-       if (_add_files_path) {
-               /* [XML] 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());
+       if (_main_content_divider_sash_position) {
+               cxml::add_text_child(root, "MainContentDividerSashPosition", raw_convert<string>(*_main_content_divider_sash_position));
        }
 
+       cxml::add_text_child(root, "DefaultAddFileLocation",
+               _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 */
+       cxml::add_text_child(root, "AllowSMPTEBv20", _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 */
+       cxml::add_text_child(root, "ISDCFNamePartLength", raw_convert<string>(_isdcf_name_part_length));
+
+#ifdef DCPOMATIC_GROK
+       if (_grok) {
+               _grok->as_xml(cxml::add_child(root, "Grok"));
+       }
+#endif
+
+       _export.write(cxml::add_child(root, "Export"));
+
+       auto target = config_write_file();
+
        try {
                auto const s = doc.write_to_string_formatted ();
-               boost::filesystem::path tmp (string(config_file().string()).append(".tmp"));
-               auto f = fopen_boost (tmp, "w");
+               boost::filesystem::path tmp (string(target.string()).append(".tmp"));
+               dcp::File f(tmp, "w");
                if (!f) {
                        throw FileError (_("Could not open file for writing"), tmp);
                }
-               checked_fwrite (s.c_str(), s.bytes(), f, tmp);
-               fclose (f);
-               boost::filesystem::remove (config_file());
-               boost::filesystem::rename (tmp, config_file());
+               f.checked_write(s.c_str(), s.bytes());
+               f.close();
+               dcp::filesystem::remove(target);
+               dcp::filesystem::rename(tmp, target);
        } catch (xmlpp::exception& e) {
                string s = e.what ();
                trim (s);
-               throw FileError (s, config_file());
+               throw FileError (s, target);
        }
 }
 
@@ -1020,16 +1186,16 @@ write_file (string root_node, string node, string version, list<shared_ptr<T>> t
 {
        xmlpp::Document doc;
        auto root = doc.create_root_node (root_node);
-       root->add_child("Version")->add_child_text(version);
+       cxml::add_text_child(root, "Version", version);
 
        for (auto i: things) {
-               i->as_xml (root->add_child(node));
+               i->as_xml(cxml::add_child(root, node));
        }
 
        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);
@@ -1072,7 +1238,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;
        }
@@ -1084,7 +1250,7 @@ void
 Config::drop ()
 {
        delete _instance;
-       _instance = 0;
+       _instance = nullptr;
 }
 
 void
@@ -1137,6 +1303,7 @@ Config::set_cover_sheet_to_default ()
 {
        _cover_sheet = _(
                "$CPL_NAME\n\n"
+               "CPL Filename: $CPL_FILENAME\n"
                "Type: $TYPE\n"
                "Format: $CONTAINER\n"
                "Audio: $AUDIO\n"
@@ -1153,7 +1320,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 ()
 {
@@ -1166,7 +1333,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 ()
 {
@@ -1194,7 +1361,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 (...) {
@@ -1203,12 +1370,14 @@ Config::clean_history_internal (vector<boost::filesystem::path>& h)
        }
 }
 
+
 bool
 Config::have_existing (string file)
 {
-       return boost::filesystem::exists (path (file, false));
+       return dcp::filesystem::exists(read_path(file));
 }
 
+
 void
 Config::read_cinemas (cxml::Document const & f)
 {
@@ -1232,13 +1401,14 @@ 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);
        }
 
+       changed (CINEMAS);
        changed (OTHER);
 }
 
@@ -1248,45 +1418,27 @@ Config::read_dkdm_recipients (cxml::Document const & f)
 {
        _dkdm_recipients.clear ();
        for (auto i: f.node_children("DKDMRecipient")) {
-               _dkdm_recipients.push_back (shared_ptr<DKDMRecipient>(new DKDMRecipient(i)));
+               _dkdm_recipients.push_back (make_shared<DKDMRecipient>(i));
        }
 }
 
-void
-Config::set_dkdm_recipients_file (boost::filesystem::path file)
-{
-       if (file == _dkdm_recipients_file) {
-               return;
-       }
-
-       _dkdm_recipients_file = file;
-
-       if (boost::filesystem::exists (_dkdm_recipients_file)) {
-               /* Existing file; read it in */
-               cxml::Document f ("DKDMRecipients");
-               f.read_file (_dkdm_recipients_file);
-               read_dkdm_recipients (f);
-       }
-
-       changed (OTHER);
-}
-
 
 void
 Config::save_template (shared_ptr<const Film> film, string name) const
 {
-       film->write_template (template_path (name));
+       film->write_template (template_write_path(name));
 }
 
+
 list<string>
 Config::templates () const
 {
-       if (!boost::filesystem::exists (path ("templates"))) {
+       if (!dcp::filesystem::exists(read_path("templates"))) {
                return {};
        }
 
        list<string> n;
-       for (auto const& i: boost::filesystem::directory_iterator(path("templates"))) {
+       for (auto const& i: dcp::filesystem::directory_iterator(read_path("templates"))) {
                n.push_back (i.path().filename().string());
        }
        return n;
@@ -1295,41 +1447,49 @@ Config::templates () const
 bool
 Config::existing_template (string name) const
 {
-       return boost::filesystem::exists (template_path (name));
+       return dcp::filesystem::exists(template_read_path(name));
+}
+
+
+boost::filesystem::path
+Config::template_read_path (string name) const
+{
+       return read_path("templates") / tidy_for_filename (name);
 }
 
+
 boost::filesystem::path
-Config::template_path (string name) const
+Config::template_write_path (string name) const
 {
-       return path("templates") / tidy_for_filename (name);
+       return write_path("templates") / tidy_for_filename (name);
 }
 
+
 void
 Config::rename_template (string old_name, string new_name) const
 {
-       boost::filesystem::rename (template_path (old_name), template_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_path (name));
+       dcp::filesystem::remove(template_write_path(name));
 }
 
 /** @return Path to the config.xml containing the actual settings, following a link if required */
 boost::filesystem::path
-Config::config_file ()
+config_file (boost::filesystem::path main)
 {
        cxml::Document f ("Config");
-       auto main = path("config.xml", false);
-       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;
@@ -1343,6 +1503,21 @@ Config::config_file ()
        return main;
 }
 
+
+boost::filesystem::path
+Config::config_read_file ()
+{
+       return config_file (read_path("config.xml"));
+}
+
+
+boost::filesystem::path
+Config::config_write_file ()
+{
+       return config_file (write_path("config.xml"));
+}
+
+
 void
 Config::reset_cover_sheet ()
 {
@@ -1354,13 +1529,13 @@ void
 Config::link (boost::filesystem::path new_file) const
 {
        xmlpp::Document doc;
-       doc.create_root_node("Config")->add_child("Link")->add_child_text(new_file.string());
+       cxml::add_text_child(doc.create_root_node("Config"), "Link", new_file.string());
        try {
-               doc.write_to_file_formatted(path("config.xml", true).string());
+               doc.write_to_file_formatted(write_path("config.xml").string());
        } catch (xmlpp::exception& e) {
                string s = e.what ();
                trim (s);
-               throw FileError (s, path("config.xml"));
+               throw FileError (s, write_path("config.xml"));
        }
 }
 
@@ -1368,20 +1543,15 @@ void
 Config::copy_and_link (boost::filesystem::path new_file) const
 {
        write ();
-       boost::filesystem::copy_file (config_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);
 }
 
 bool
 Config::have_write_permission () const
 {
-       auto f = fopen_boost (config_file(), "r+");
-       if (!f) {
-               return false;
-       }
-
-       fclose (f);
-       return true;
+       dcp::File f(config_write_file(), "r+");
+       return static_cast<bool>(f);
 }
 
 /** @param  output_channels Number of output channels in use.
@@ -1444,3 +1614,138 @@ Config::add_custom_language (dcp::LanguageTag tag)
        }
 }
 
+
+optional<Config::BadReason>
+Config::check_certificates () const
+{
+       optional<BadReason> bad;
+
+       for (auto const& i: _signer_chain->unordered()) {
+               if (i.has_utf8_strings()) {
+                       bad = BAD_SIGNER_UTF8_STRINGS;
+               }
+               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()) {
+               bad = BAD_SIGNER_INCONSISTENT;
+       }
+
+       if (!_decryption_chain->chain_valid() || !_decryption_chain->private_key_valid()) {
+               bad = BAD_DECRYPTION_INCONSISTENT;
+       }
+
+       return bad;
+}
+
+
+void
+save_all_config_as_zip (boost::filesystem::path zip_file)
+{
+       Zipper zipper (zip_file);
+
+       auto config = Config::instance();
+       zipper.add ("config.xml", dcp::file_to_string(config->config_read_file()));
+       if (dcp::filesystem::exists(config->cinemas_file())) {
+               zipper.add ("cinemas.xml", dcp::file_to_string(config->cinemas_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;
+}
+
+
+#ifdef DCPOMATIC_GROK
+
+Config::Grok::Grok(cxml::ConstNodePtr node)
+       : enable(node->bool_child("Enable"))
+       , binary_location(node->string_child("BinaryLocation"))
+       , selected(node->number_child<int>("Selected"))
+       , licence_server(node->string_child("LicenceServer"))
+       , licence_port(node->number_child<int>("LicencePort"))
+       , licence(node->string_child("Licence"))
+{
+
+}
+
+
+void
+Config::Grok::as_xml(xmlpp::Element* node) const
+{
+       node->add_child("BinaryLocation")->add_child_text(binary_location.string());
+       node->add_child("Enable")->add_child_text((enable ? "1" : "0"));
+       node->add_child("Selected")->add_child_text(raw_convert<string>(selected));
+       node->add_child("LicenceServer")->add_child_text(licence_server);
+       node->add_child("LicencePort")->add_child_text(raw_convert<string>(licence_port));
+       node->add_child("Licence")->add_child_text(licence);
+}
+
+
+void
+Config::set_grok(Grok const& grok)
+{
+       _grok = grok;
+       changed(GROK);
+}
+
+#endif