ACC SHELL

Path : /usr/share/YaST2/modules/
File Upload :
Current File : //usr/share/YaST2/modules/AddOnProduct.ycp

/**
 * File:
 *	AddOnProduct.ycp
 *
 * Module:
 *	AddOnProduct
 *
 * Summary:
 *	This module provides integration of the add-on products
 *
 * Authors:
 *	Jiri Srain <jsrain@suse.de>
 *	Lukas Ocilka <locilka@suse.cz>
 */

{
module "AddOnProduct";

// IMPORTANT: maintainer of yast2-add-on is responsible for this module

textdomain "packager";

import "Label";
import "Mode";
import "ProductControl";
import "ProductFeatures";
import "Report";
import "XML";
import "Wizard";
import "FileUtils";
import "Language";
import "Popup";
import "InstShowInfo";
import "ProductLicense";
import "FileUtils";
import "Directory";
import "String";
import "WorkflowManager";
import "URL";
import "Mode";
import "Stage";
import "Icon";
import "PackageCallbacks";
import "PackagesProposal";

// variables for installation with product
/**
 * ID for cache in the inst-sys
 */
integer src_cache_id = -1;

/**
 * System proposals have already been prepared for merging?
 */
boolean system_proposals_prepared = false;

/**
 * System workflows have already been prepared for merging?
 */
boolean system_workflows_prepared = false;

/**
 * List of all selected repositories
 *
 * @struct add_on_products = [
 *   $[
 *     "media" : 4, // ID of the source
 *     "product_dir" : "/",
 *     "product" : "openSUSE version XX.Y",
 *     "autoyast_product" : "'PRODUCT' tag for AutoYaST Export",
 *   ],
 *   ...
 * ]
 */
global list<map<string,any> > add_on_products = [];

/**
 * ID of currently added repository for the add-on product
 */
global integer src_id  = nil;

// for the add-on product workflow - needed for dialog skipping
/**
 * return value of last step in the product adding workflow
 */
global symbol last_ret = nil;

global boolean modified = false;

global list <integer> mode_config_sources = [];

global map<string,any> current_addon = $[];

// Bugzilla #239630
// In installation: check for low-memory machines
global boolean low_memory_already_reported = false;

// Bugzilla #305554
// Both online-repositories and add-ons use the same function and variable
// if true, both are skipped at once without asking
global boolean skip_add_ons = false;

/**
 * @struct $["src_id|media|filename" : "/path/to/the/file"]
 */
map <string, string> source_file_cache = $[];

string filecachedir = sformat ("%1/AddOns_CacheDir/", Directory::tmpdir);

integer filecachecounter = -1;

/**
 * Downloads a requested file, caches it and returns path to that cached file.
 * If a file is alerady cached, just returns the path to a cached file.
 * Parameter 'sod' defines whether a file is 'signed' (file + file.asc) or 'digested'
 * (file digest mentioned in signed content file).
 *
 * @param integer src_id
 * @param integer media
 * @param string filename
 * @param string sod ("signed" or "digested")
 * @param boolean optional (false if mandatory)
 * @return string path to a cached file
 *
 * @example:
 *   // content file is usually signed with content.asc
 *   AddOnProduct::GetCachedFileFromSource (8, 1, "/content", "signed", false);
 *   // the other files are usually digested in content file
 *   AddOnProduct::GetCachedFileFromSource (8, 1, "/images/images.xml", "digested", true);
 */
global string GetCachedFileFromSource (integer src_id, integer media, string filename, string sod, boolean optional) {
    // BNC #486785: Jukebox when using more physical media-based Add-Ons at once
    string file_ID = sformat ("%1|%2|%3", src_id, media, filename);

    string provided_file = source_file_cache[file_ID]:"";

    if (provided_file != nil && provided_file != "") {
	// Checking whether the cached file exists
	if (FileUtils::Exists (provided_file)) {
	    y2milestone ("File %1 found in cache: %2", file_ID, provided_file);

	    return provided_file;
	} else {
	    y2warning ("Cached file %1 not accessible!", provided_file);
	    source_file_cache = remove (source_file_cache, file_ID);
	}
    }

    if (optional == nil) optional = true;

    if (sod == "signed") {
	provided_file = Pkg::SourceProvideSignedFile (src_id, media, filename, optional);
    } else if (sod == "digested") {
	provided_file = Pkg::SourceProvideDigestedFile (src_id, media, filename, optional);
    } else {
	y2error ("Unknown SoD: %1. It can be only 'signed' or 'digested'", sod);
	provided_file = nil;
    }

    // A file has been found, caching...
    if (provided_file != nil) {
	filecachecounter = filecachecounter + 1;

	// Where the file is finally cached
	string cached_file = sformat ("%1%2", filecachedir, filecachecounter);

	string cmd = sformat (
	    "/bin/mkdir -p '%1'; /bin/cp '%2' '%3'",
	    String::Quote (filecachedir), String::Quote (provided_file), String::Quote (cached_file)
	);
	map cmd_run = (map) SCR::Execute (.target.bash_output, cmd);

	// Unable to cache a file, the original file will be returned
	if (cmd_run["exit"]:-1 != 0) {
	    y2warning ("Error caching file: %1: %2", cmd, cmd_run);
	} else {
	    y2milestone ("File %1 cached as %2", file_ID, cached_file);
	    // Writes entry into cache database
	    source_file_cache[file_ID] = cached_file;
	    // Path to a cached file will be returned
	    provided_file = cached_file;
	}
    }

    return provided_file;
}

// Which part installation.xml will be used
string _inst_mode = "installation";

/**
 * Returns the current add-on installation mode.
 *
 * @return string current mode
 * @see SetMode()
 */
string GetMode () {
    return _inst_mode;
}

/**
 * Sets internal add-on installation mode to either "installation" or "update".
 * Mode is used later when deciding which part of the installation.xml to use.
 *
 * @param string new_mode ("installation" or "update")
 * @see GetMode();
 */
void SetMode (string new_mode) {
    if (new_mode == nil || ! contains (["installation", "update"], new_mode)) {
	y2error ("Wrong Add-On mode: %1", new_mode);
    }

    _inst_mode = new_mode;
}

/**
 * Returns whether add-on product got as parameter (source id)
 * replaces some already installed add-on or whether it is a new
 * installation. Repositories and target have to be initialized.
 *
 * @param integer source_id
 * @param string "installation" or "update" according the current state
 */
global string AddOnMode (integer source_id) {
    list <map <string, any> > all_products = Pkg::ResolvableProperties ("", `product, "");

    map <string, any> check_add_on = $[];

    // Search for an add-on using source ID
    foreach (map <string, any> one_product, all_products, {
	if (one_product["source"]:-1 == source_id) {
	    check_add_on = one_product;
	    break;
	}
    });

    string ret = "installation";

    list <symbol> supported_statuses = [`installed, `selected];
    boolean already_found = false;

    // Found the 
    if (check_add_on != $[] && haskey (check_add_on, "replaces")) {
	list <map <string, any> > product_replaces = (list <map <string, any> >) check_add_on["replaces"]:[];

	// Run through through all products that the add-on can replace
	foreach (map <string, any> one_replaces, product_replaces, {
	    if (already_found)
		break;

	    // Run through all installed (or selected) products
	    foreach (map <string, any> one_product, all_products, {
		// checking the status
		if (! contains (supported_statuses, one_product["status"]:`unknown))
		    continue;

		// ignore itself
		if (one_product["source"]:-42 == source_id)
		    continue;

		// check name to replace
		if (one_product["name"]:"-A-" != one_replaces["name"]:"-B-")
		    continue;

		// check version to replace
		if (one_product["version"]:"-A-" != one_replaces["version"]:"-B-")
		    continue;

		// check version to replace
		if (one_product["arch"]:"-A-" != one_replaces["arch"]:"-B-")
		    continue;

		y2milestone ("Found product matching update criteria: %1 -> %2", one_product, check_add_on);
		ret = "update";
		already_found = true;
		break;
	    });
	});
    }

    return ret;
}

// --> FATE #302123: Allow relative paths in "add_on_products" file
string base_product_url = nil;

global void SetBaseProductURL (string url) {
    if (url == "" || url == nil)
	y2warning ("Empty base url");

    base_product_url = url;
    y2milestone ("New base URL: %1", URL::HidePassword(base_product_url));
}

global string GetBaseProductURL () {
    return base_product_url;
}

/**
 * Returns an absolute URL from base + relative url.
 * Relative URL needs to start with 'reulrl://' othewise
 * it is not considered being relative and it's returned
 * as it is (just the relative_url parameter).
 *
 * @param string base_url
 * @param string relative_url
 * @return string absolute_url
 *
 * @example
 *   AddOnProduct::GetAbsoluteURL (
 *     "http://www.example.org/some%20dir/another%20dir",
 *     "relurl://../AnotherProduct/"
 *   ) -> "http://www.example.org/some%20dir/AnotherProduct/"
 *   AddOnProduct::GetAbsoluteURL (
 *     "username:password@ftp://www.example.org/dir/",
 *     "relurl://./Product_CD1/"
 *   ) -> "username:password@ftp://www.example.org/dir/Product_CD1/"
 */
global string GetAbsoluteURL (string base_url, string url) {
    if (! regexpmatch (url, "^relurl://")) {
	y2debug ("Not a relative URL: %1", URL::HidePassword(url));
	return url;
    }

    if (base_url == nil || base_url == "") {
	y2error ("No base_url defined");
	return url;
    }

    // bugzilla #306670
    integer base_params_pos = search (base_url, "?");
    string base_params = "";

    if (base_params_pos != nil && base_params_pos >= 0) {
	base_params = substring (base_url, (base_params_pos + 1));
	base_url = substring (base_url, 0, base_params_pos);
    }

    integer added_params_pos = search (url, "?");
    string added_params = "";

    if (added_params_pos != nil && added_params_pos >= 0) {
	added_params = substring (url, (added_params_pos + 1));
	url = substring (url, 0, added_params_pos);
    }

    if (! regexpmatch (base_url, "/$")) {
	base_url = base_url + "/";
    }

    y2milestone ("Merging '%1' (params '%2') to '%3' (params '%4')", url, added_params, base_url, base_params);
    url = regexpsub (url, "^relurl://(.*)$", "\\1");

    url = sformat ("%1%2", base_url, url);

    // merge /something/../
    integer max_count = 100;

    while (max_count > 0 && regexpmatch (url, "(.*/)[^/]+/+\\.\\./")) {
	max_count = max_count - 1;
	list <integer> str_offset_l = (list <integer>) regexppos (url, "/\\.\\./");
	integer str_offset = str_offset_l[0]:nil;

	if (str_offset != nil && str_offset > 0) {
	    string stringfirst = substring (url, 0, str_offset);
	    string stringsecond = substring (url, str_offset);

	    y2debug ("Pos: %1 First: >%2< Second: >%3<", str_offset, stringfirst, stringsecond);

	    stringfirst = regexpsub (stringfirst, "^(.*/)[^/]+/*$", "\\1");
	    stringsecond = regexpsub (stringsecond, "^/\\.\\./(.*)$", "\\1");
	    
	    url = stringfirst + stringsecond;
	}
    }

    // remove /./
    max_count = 100;

    while (max_count > 0 && regexpmatch (url, "/\\./")) {
	max_count = max_count - 1;
	url = regexpsub (url, "^(.*)/\\./(.*)", "\\1/\\2");
    }

    map <string, string> base_params_map  = URL::MakeMapFromParams (base_params);
    map <string, string> added_params_map = URL::MakeMapFromParams (added_params);
    map <string, string> final_params_map = (map <string, string>) union (base_params_map, added_params_map);

    if (size (final_params_map) > 0) {
	y2milestone ("%1 merge %2 -> %3", base_params_map, added_params_map, final_params_map);

	url = url + "?" + URL::MakeParamsFromMap (final_params_map);
    }

    y2milestone ("Final URL: '%1'", URL::HidePassword(url));
    return url;
}
// <--

/**
 * Adapts the inst-sys from the tarball
 * @param filename string the filename with the tarball to use to the update
 * @return boolean true on success
 */
global boolean UpdateInstSys (string filename) {
    src_cache_id = src_cache_id + 1;
    string tmpdir = (string)SCR::Read (.target.tmpdir);
    tmpdir = sformat ("%1/%2", tmpdir, src_cache_id);
    map out = (map)SCR::Execute (.target.bash_output, sformat ("
/bin/mkdir %1;
cd %1;
/bin/tar -xvf %2;
/sbin/adddir %1 /;
", tmpdir, filename));
    if (out["exit"]:0 != 0)
    {
	y2error ("Including installation image failed: %1", out);
	return false;
    }
    y2milestone ("Including installation image succeeded");
    return true;
}

/**
 * New add-on product might add also new agents.
 * Functions Rereads all available agents.
 *
 * @see bugzilla #239055, #245508
 */
global void RereadAllSCRAgents () {
    y2milestone ("Registering new agents...");
    boolean ret = (boolean) SCR::RegisterNewAgents();

    if (ret) {
	y2milestone ("Successful");
    } else {
	y2error ("Error occured during registering new agents!");
	Report::Error (_("An error occurred while preparing the installation system."));
    }
}

/**
 * Remove the /y2update directory from the system
 */
void CleanY2Update() {
    SCR::Execute (.target.bash, "/bin/rm -rf /y2update");
}

/**
 * Show /media.1/info.txt file in a pop-up message if such file exists.
 * Show license if such exists and return whether users accepts it.
 * Returns 'nil' when did not succed.
 *
 * @return boolean whether the license has been accepted
 */
global boolean AcceptedLicenseAndInfoFile (integer src_id) {
    symbol ret = ProductLicense::AskAddOnLicenseAgreement (src_id);
    if (ret == nil)
	return nil;
    else if (ret == `abort || ret == `back)
    {
	y2milestone ("License confirmation failed");
	return false;
    }
    return true;
}

boolean AnyPatternInRepo()
{
    list<map> patterns = Pkg::ResolvableProperties("", `pattern, "");

    y2milestone("Total number of patterns: %1", size(patterns));

    patterns = filter(map pat, patterns,
	{
	    return (pat["source"]:nil == src_id);
	}
    );

    y2milestone("Found %1 add-on patterns", size(patterns));
    y2debug("Found add-on patterns: %1", patterns);

    return size(patterns) > 0;
}

symbol DoInstall_NoControlFile () {
    y2milestone("File /installation.xml not found, running sw_single for this repository");

    // display pattern the dialog when there is a pattern provided by the addon
    // otherwise use search mode
    symbol mode = AnyPatternInRepo() ? `patternSelector : `searchMode;
    // enable repository management if not in installation mode
    boolean enable_repo_management = Mode::normal();

    map args = $[ "dialog_type" : mode, "repo_mgmt" : enable_repo_management ];
    y2milestone ("Arguments for sw_single: %1", args);

    any ret = WFM::CallFunction ("sw_single", [args]);
    y2milestone ("sw_single returned: %1", ret);

    if (ret == `abort || ret == `cancel || ret == `close)
	return `abort;

    return `register;
}

boolean IntegrateY2Update (integer src_id) {
    string binaries = GetCachedFileFromSource (src_id, 1, "/y2update.tgz", "digested", true /* optional */);
    // File /y2update.tgz exists
    if (binaries != nil)
    {
	// Try to extract files from the archive
	map out = (map)SCR::Execute (.target.bash_output, sformat ("
test -d /y2update && rm -rf /y2update;
/bin/mkdir -p /y2update/all;
cd /y2update/all;
/bin/tar -xvf %1;
cd /y2update;
ln -s all/usr/share/YaST2/* .;
ln -s all/usr/lib/YaST2/* .;
", binaries));

	// Failed
	if (out["exit"]:0 != 0)
	{
	    // error report
	    Report::Error (_("An error occurred while preparing the installation system."));
	    CleanY2Update();
	    return false;
	} else {
	    // bugzilla #239055
	    RereadAllSCRAgents();
	}
    }
    else
    {
	y2milestone("File /y2update.tgz not provided");
    }

    return true;
}

symbol DoInstall_WithControlFile (string control) {
    y2milestone ("File /installation.xml was found, running own workflow...");
    // copy the control file to local filesystem - in case of media release
    string tmp = (string)SCR::Read (.target.tmpdir);
    tmp = tmp + "/installation.xml";
    SCR::Execute (.target.bash, sformat ("/bin/cp %1 %2", control, tmp));
    control = tmp;

    if (! IntegrateY2Update (src_id)) return nil;

    // set control file
    ProductControl::custom_control_file = control;
    if (!ProductControl::Init())
    {
	// error report
	Report::Error (sformat (_("Control file %1 not found on media."),
	    control));
	CleanY2Update();
	return nil;
    }

    string current_stage = "normal";
    string current_mode  = "installation";

    // Special add-on mode (GetMode()) returns the same
    // add-on can be either installed (first time) or updated by another add-on
    ProductControl::SetAdditionalWorkflowParams ($["add_on_mode" : AddOnMode (src_id)]);

    list <map> steps = ProductControl::getModules (current_stage, current_mode, `enabled);
    if (steps == nil || size (steps) < 1) {
	y2warning ("Add-On product workflow for stage: %1, mode: %2 not defined", current_stage, current_mode);
	ProductControl::ResetAdditionalWorkflowParams();
	return nil;
    }

    // start workflow
    Wizard::OpenNextBackStepsDialog();
    // dialog caption
    Wizard::SetContents(_("Initializing..."), `Empty (), "", false, false);

    list <map> stage_mode = [$["stage": current_stage,  "mode": current_mode, ]];
    y2milestone ("Using Add-On control file parts: %1", stage_mode);
    ProductControl::AddWizardSteps (stage_mode);

    string old_mode = nil;
    // Running system, not installation, not update
    if (Stage::normal() && Mode::normal()) {
	old_mode = Mode::mode();
	Mode::SetMode (current_mode);
    }

    // Run the workflow
    symbol ret = ProductControl::Run();

    if (old_mode != nil) {
	Mode::SetMode (old_mode);
    }

    UI::CloseDialog();
    CleanY2Update();

    ProductControl::ResetAdditionalWorkflowParams();

    return ret;
}

/**
 * Contains list of repository IDs that request registration
 */
list <integer> addons_requesting_registration = [];

global void ClearRegistrationRequest (integer src_id) {
    y2milestone ("Clearing registration flag for repository ID %1", src_id);
    if (src_id != nil) {
	addons_requesting_registration = filter (integer one_source, addons_requesting_registration, {
	    return one_source != src_id;
	});
    }
}

/**
 * Returns whether registration is requested by at least one of
 * used Add-On products.
 *
 * @return boolean if requested
 */
global boolean ProcessRegistration () {
    boolean force_registration = false;

    // checking add-on products one by one
    foreach (map<string,any> prod, AddOnProduct::add_on_products, {
	integer srcid = (integer) prod["media"]:nil;

	if (srcid != nil && contains (addons_requesting_registration, srcid)) {
	    force_registration = true;
	    break;
	}
    });
    
    y2milestone ("Requesting registration: %1", force_registration);
    return force_registration;
}

/**
 * Add-On product might have been added into products requesting
 * registration. This pruduct has been removed (during configuring
 * list of add-on products).
 */
global void RemoveRegistrationFlag (integer src_id) {
    // filtering out src_id
    addons_requesting_registration = filter (integer one_id, addons_requesting_registration, {
	return one_id != src_id;
    });
    
    // removing cached file
    string tmpdir = (string) SCR::Read (.target.tmpdir) + "/add-on-content-files/";
    string cachedfile = sformat ("%1content-%2", tmpdir, src_id);
    if (FileUtils::Exists (cachedfile)) {
	y2milestone ("Removing cached file %1", cachedfile);
	SCR::Execute (.target.remove, cachedfile);
    }
}

/**
 * Checks whether the content file of the add-on has a flag REGISTERPRODUCT
 * set to "true" or "yes". If it has, product is added into list of pruducts
 * that need registration. Cached content file is used if possible.
 *
 * @param integer source id
 */
global void PrepareForRegistration (integer src_id) {
    string tmpdir = (string) SCR::Read (.target.tmpdir) + "/add-on-content-files/";

    // create directory if doesn't exist
    if (! FileUtils::Exists (tmpdir)) {
	integer run = (integer) SCR::Execute (.target.bash, sformat("/bin/mkdir -p '%1'", tmpdir));
	if (run != 0) {
	    y2error ("Cannot create directory %1", tmpdir);
	    return nil;
	}
    }

    // use cached file if possible
    string contentfile = sformat ("%1content-%2", tmpdir, src_id);
    if (FileUtils::Exists (contentfile)) {
	y2milestone ("Using cached contentfile %1", contentfile);
    } else {
	y2milestone ("Checking contentfile from repository");
	string sourcefile = GetCachedFileFromSource (src_id, 1, "/content", "signed", true);
	if (sourcefile == nil) {
	    y2warning ("Cannot obtain content file!");
	    return nil;
	}
	// copying content file
	integer run = (integer) SCR::Execute (.target.bash,
	    sformat ("/bin/cp '%1' '%2'", String::Quote (sourcefile), String::Quote (contentfile))
	);
	if (run != 0) {
	    y2error ("Cannot copy '%1' to '%2'", sourcefile, contentfile);
	    return nil;
	}
    }

    // registering agent for the current content file
    SCR::RegisterAgent (.addon.content, `ag_ini (
	`IniAgent( contentfile, $[
	    "options" : [ "read_only", "global_values", "flat" ],
	    "comments" : [ "^#.*", "^[ \t]*$", ],
	    "params" : [
		$[ "match" : [ "^[ \t]*([a-zA-Z0-9_\.]+)[ \t]*(.*)[ \t]*$", "%s %s" ] ]
	    ]
	]
    )));
    string register_product = (string) SCR::Read (.addon.content.REGISTERPRODUCT);
    SCR::UnregisterAgent (.addon.content);

    // evaluating REGISTERPRODUCT flag, default (nil == false)
    y2milestone ("RegisterProduct flag for repository %1 is %2", src_id, register_product);
    if (register_product == "yes" || register_product == "true") {
	addons_requesting_registration = add (addons_requesting_registration, src_id);
    }
}

/**
 * Calls registration client if needed.
 *
 * @param integer source id
 */
global void RegisterAddOnProduct (integer src_id) {
    if (contains (addons_requesting_registration, src_id)) {
	y2milestone ("Repository ID %1 requests registration", src_id);
	WFM::CallFunction ("inst_suse_register", []);
    } else {
	y2milestone ("Repository ID %1 doesn't need registration", src_id);
    }
}

// function content defined later
void HandleProductPATTERNS (integer srcid);

/**
 * Do installation of the add-on product within an installed system
 * srcid is got via AddOnProduct::src_id
 *
 * @param string src_id
 * @return symbol the result symbol from wizard sequencer
 */
global symbol DoInstall() {
    // Display /media.1/info.txt if such file exists
    // Display license and wait for agreement
    // Not needed here, license already shown in the workflow
    /*
    boolean license_ret = AcceptedLicenseAndInfoFile(src_id);
    if (license_ret != true) {
	y2milestone("Removing the current source ID %1", src_id);
	Pkg::SourceDelete(src_id);
	return nil;
    }
    */

    // FATE #301312
    PrepareForRegistration (src_id);

    // FATE #302398: PATTERNS keyword in content file
    HandleProductPATTERNS (src_id);

    // FATE #301997: Support update of add-on products properly
    string add_on_mode = AddOnMode (src_id);
    SetMode (add_on_mode);

    // BNC #468449
    // Always store the current set of repositories as they might get
    // changed by registration or the called add-on workflow
    Pkg::SourceSaveAll();

    symbol ret = nil;

    string control = GetCachedFileFromSource (src_id, 1, "/installation.xml", "digested", true /* optional */);
    if (control != nil) {
	y2milestone ("Add-On has own control file");
	ret = DoInstall_WithControlFile (control);
    }
    // Fallback -- Repository didn't provide needed control file
    // or control file doesn't contain needed stage/mode
    // Handling as it was a repository
    if (control == nil || ret == nil) {
	ret = DoInstall_NoControlFile();
    }

    y2milestone("Result of the add-on installation: %1", ret);

    if (ret != nil && ret != `abort) {
	// registers Add-On product if requested
	RegisterAddOnProduct (src_id);
    }

    y2milestone ("Returning: %1", ret);
    return ret;
}

/**
 * Every Add-On can preselect some patterns.
 * Only patterns that are not selected/installed yet will be used.
 *
 * @struct $[
 *   src_id : [
 *     "pattern_1", "pattern_2", "pattern_6"
 *   ]
 * ]
 */
map <integer, list <string> > patterns_preselected_by_addon = $[];

string PackagesProposalAddonID (integer src_id) {
    return sformat ("Add-On-Product-ID:%1", src_id);
}

// See also DeselectProductPatterns()
boolean SelectProductPatterns (string content_file, integer src_id) {
    if (! FileUtils::Exists (content_file)) {
	y2error ("No such file: %1", content_file);
	return false;
    }

    map contentmap = (map) SCR::Read (.content_file, content_file);

    // no PATTERNS defined
    if (! haskey (contentmap, "PATTERNS")) {
	y2milestone ("Add-On doesn't have any required patterns (PATTERNS in content)");
	return true;
    }

    // parsing PATTERNS
    list <string> patterns_to_select = splitstring (contentmap["PATTERNS"]:"", "\t ");
    patterns_to_select = filter (string one_pattern, patterns_to_select, {
	return (one_pattern != nil && one_pattern != "");
    });

    if (size (patterns_to_select) == 0) {
	y2error ("Erroneous PATTERNS: %1", contentmap["PATTERNS"]:"");
	return false;
    }

    y2milestone ("Add-On requires these PATTERNS: %1", patterns_to_select);
    // clear/set
    patterns_preselected_by_addon[src_id] = [];

    // bnc #458297
    // Using PackagesProposal to select the patterns itself
    PackagesProposal::SetResolvables (PackagesProposalAddonID (src_id), `pattern, patterns_to_select);

    if (Stage::initial()) {
	y2milestone ("Using PackagesProposal to select Add-On patterns");
	return true;
    }

    boolean ret = true;

    foreach (string one_pattern, patterns_to_select, {
	list <map <string,any> > pattern_properties = Pkg::ResolvableProperties (one_pattern, `pattern, "");

	boolean already_selected = false;

	foreach (map <string,any> one_pattern_found, pattern_properties, {
	    symbol patt_status = one_pattern_found["status"]:`unknown;
	    // patern is already selected
	    if (patt_status == `installed || patt_status == `selected) {
		already_selected = true;
		break;
	    }
	});

	if (already_selected) {
	    y2milestone ("Pattern %1 is already installed/selected", one_pattern);
	    return;
	}

	if (! Pkg::ResolvableInstall (one_pattern, `pattern)) {
	    y2error ("Cannot select pattern: %1, reason: %2", one_pattern, Pkg::LastError());
	    ret = false;
	} else {
	    patterns_preselected_by_addon[src_id] = add (patterns_preselected_by_addon[src_id]:[], one_pattern);
	}
    });

    return ret;
}


// See also SelectProductPatterns()
boolean DeselectProductPatterns (integer src_id) {
    // bnc #458297
    // Using PackagesProposal to deselect the patterns itself
    PackagesProposal::SetResolvables (PackagesProposalAddonID (src_id), `pattern, []);

    if (Stage::initial()) {
	y2milestone ("Initial stage, using PackagesProposal to deselect patterns");
	return true;
    }

    list <string> patterns_to_deselect = patterns_preselected_by_addon[src_id]:[];

    if (size (patterns_to_deselect) == 0) {
	y2milestone ("There's no pattern to be deselected");
	return true;
    }

    boolean ret = true;

    foreach (string one_pattern, patterns_to_deselect, {
	if (! Pkg::ResolvableNeutral (one_pattern, `pattern, true)) {
	    y2error ("Cannot deselect pattern: %1, reason: %2", one_pattern, Pkg::LastError());
	    ret = false;
	}
    });

    return ret;
}

/**
 * Function checks whether the product content file contains
 * PATTERNS tag and pre-selects patterns listed there.
 *
 * @param integer source ID
 */
void HandleProductPATTERNS (integer srcid) {
    // FATE #302398: PATTERNS keyword in content file
    string content_file = GetCachedFileFromSource (srcid, 1, "/content", "signed", true);

    if (content_file == nil) {
	y2warning ("Add-On %1 doesn't have a content file", srcid);
    } else {
	SelectProductPatterns (content_file, srcid);
    }
}

/**
 * Integrate the add-on product to the installation workflow, including
 * preparations for 2nd stage and inst-sys update
 * @param srcid integer the ID of the repository
 * @return boolean true on success
 */
global boolean Integrate (integer srcid) {
    y2milestone ("Integrating repository %1", srcid);

    // Updating inst-sys
    string y2update = GetCachedFileFromSource (srcid, 1, "/y2update.tgz", "digested", true /* optional */);

    if (y2update == nil) {
	y2milestone ("No YaST update found on the media");
    } else {
	UpdateInstSys (y2update);
    }

    // FATE #302398: PATTERNS keyword in content file
    HandleProductPATTERNS (srcid);

    // Adds workflow to the Workflow Store if any workflow exists
    WorkflowManager::AddWorkflow (`addon, srcid, "");

    return true;
}

/**
 * Opposite to Integrate()
 *
 * @param srcid integer the ID of the repository
 */
global void Disintegrate (integer srcid) {
    DeselectProductPatterns (srcid);

    WorkflowManager::RemoveWorkflow (`addon, srcid, "");
}

/**
 * Some product(s) were removed, reintegrating their control files from scratch.
 */
global boolean ReIntegrateFromScratch () {
    y2milestone ("Reintegration workflows from scratch...");

    // bugzilla #239055
    RereadAllSCRAgents();

// Should have been done before (by calling AddOnProduct::Integrate()
//    foreach (map<string,any> prod, AddOnProduct::add_on_products, {
//        integer srcid = (integer) prod["media"]:nil;
//
//        if (srcid == nil) {
//            y2error ("Wrong definition of Add-on product: %1, cannot reintegrate", srcid);
//            return;
//        } else {
//            y2milestone ("Reintegrating product %1", prod);
//            Integrate (srcid);
//        }
//    });
    boolean redraw = WorkflowManager::SomeWorkflowsWereChanged();

    // New implementation: Control files are cached, just merging them into the Base Workflow
    WorkflowManager::MergeWorkflows();

    // steps might have been changed, forcing redraw
    if (redraw) {
	y2milestone ("Forcing RedrawWizardSteps()");
	WorkflowManager::RedrawWizardSteps ();
    }

    return true;
}

global boolean CheckProductDependencies (list<string> products) {
// TODO check the dependencies of the product
    return true;
}

string preselected_add_ons = "plain";

/**
 * Sets an add_on_products file type ("plain" or "xml")
 * @see FATE #303675
 *
 * @param string type "plain" or "xml"
 */
global void SetPreselectedAddOnProductsType (string type) {
    if (type == "xml" || type == "plain") {
	preselected_add_ons = type;
	y2milestone ("add_on_products type set: %1", preselected_add_ons);
    } else {
	y2error ("Unknown type: %1", type);
    }
}

/**
 * Reads temporary add_on_products file, parses supported products,
 * merges base URL if products use relative URL and returns list of
 * maps defining additional products to add.
 *
 * @see FATE #303675
 * @param string parse_file
 * @param string base_url
 * @return list <map> of products to add
 *
 * @struct
 *  [
 *    // product defined with URL and additional path (typically "/")
 *    $["url":(string) url, "path":(string) path]
 *    // additional list of products to install
 *    // media URL can contain several products at once
 *    $["url":(string) url, "path":(string) path, "install_products":(list <string>) pti]
 *  ]
 */
list <map> ParsePlainAddOnProductsFile (string parse_file, string base_url) {
    if (! FileUtils::Exists (parse_file)) {
	y2error ("Cannot parse missing file: %1", parse_file);
	return [];
    }

    list <string> products = splitstring ((string) SCR::Read (.target.string, parse_file), "\r\n");

    if (products == nil) {
	// TRANSLATORS: error report
	Report::Error (_("Unable to use additional products."));
	y2error ("Erroneous file: %1", parse_file);
	return [];
    }

    list <map> ret = [];

    foreach (string p, products, {
	if (p == "")
	    return;

	list <string> elements = splitstring (p, " \t");
	elements = filter (string e, elements, { return e != ""; });
	string url = elements[0]:"";
	string pth = elements[1]:"/";

	if (elements[0]:nil != nil) elements = remove (elements, 0);
	if (elements[0]:nil != nil) elements = remove (elements, 0);

	// FATE #302123
	if (base_url != nil && base_url != "") {
	    url = GetAbsoluteURL (base_url, url);
	}

	ret = add (ret, $[
	    "url" : url,
	    "path" : pth,
	    "install_products" : elements,
	]);
    });

    return ret;
}

list <map> UserSelectsRequiredAddOns (list <map> products) {
    if (products == nil || products == []) {
	return [];
    }

    list <term> ask_user_products = [];
    map <integer, map> ask_user_products_map = $[];

    // key in ask_user_products_map
    integer id_counter = -1;
    string visible_string = "";

    // filter those that are selected by default (without 'ask_user')
    list <map> selected_products = filter (map one_product, products, {
	if (one_product["ask_user"]:false == false) {
	    return true;
	}

	// wrong definition, 'url' is mandatory
	if (! haskey (one_product, "url")) {
	    y2error ("No 'url' defined: %1", one_product);
	    return false;
	}

	// user is asked for the rest
	id_counter = id_counter + 1;

	// fill up internal map (used later when item selected)
	ask_user_products_map[id_counter] = one_product;

	if (haskey (one_product, "name")) {
	    visible_string = sformat (_("%1, URL: %2"), one_product["name"]:"", one_product["url"]:"");
	} else if (haskey (one_product, "install_products")) {
	    visible_string = sformat (_("%1, URL: %2"), mergestring (one_product["install_products"]:[], ", "), one_product["url"]:"");
	} else if (haskey (one_product, "path") && one_product["path"]:"/" != "/") {
	    visible_string = sformat (_("URL: %1, Path: %2"), one_product["url"]:"", one_product["path"]:"");
	} else {
	    visible_string = sformat (_("URL: %1"), one_product["url"]:"");
	}

	// create items
	ask_user_products = add (ask_user_products, `item (
	    `id (id_counter),
	    visible_string,
	    one_product["selected"]:false
	));

	return false;
    });

    ask_user_products = sort (term x, term y, ask_user_products, ``(x[1]:"" < y[1]:""));

    UI::OpenDialog (
	`VBox (
	    `HBox (
		`HSquash (`MarginBox (0.5, 0.2, Icon::Simple ("yast-addon"))),
		// TRANSLATORS: popup heading
		`Left (`Heading(`id(`search_heading), _("Additional Products")))
	    ),
	    `VSpacing (0.5),
	    // TRANSLATORS: additional dialog information
	    `Left (`Label (_("The installation repository contains also the listed additional repositories.
Select those you want to use."))),
	    `VSpacing (0.5),
	    `MinSize (
		70, 16,
		`MultiSelectionBox (
		    `id (`products),
		    _("Additional Products to Select"),
		    ask_user_products
		)
	    ),
	    `HBox (
		`HStretch(),
		// push button label
		`PushButton (`id (`ok), _("Add Selected &Products")),
		`HSpacing (1),
		`PushButton (`id (`cancel), Label::CancelButton())
	    )
	)
    );

    any ret = UI::UserInput();
    y2milestone ("User ret: %1", ret);

    // add also selected
    if (ret == `ok) {
	list <integer> selprods = (list <integer>) UI::QueryWidget (`products, `SelectedItems);
	foreach (integer one_product, selprods, {
	    selected_products = add (selected_products, ask_user_products_map[one_product]:$[]);
	});
    }

    UI::CloseDialog ();

    y2milestone ("Selected products: %1", selected_products);

    return selected_products;
}

list <map> ParseXMLBasedAddOnProductsFile (string parse_file, string base_url) {
    if (! FileUtils::Exists (parse_file)) {
	y2error ("Cannot parse missing file: %1", parse_file);
	return [];
    }

    map <string, any> xmlfile_products = XML::XMLToYCPFile (parse_file);

    if (xmlfile_products == nil) {
	// TRANSLATORS: error report
	Report::Error (_("Unable to use additional products."));
	y2error ("Erroneous file %1", parse_file);
	return [];
    } else if (xmlfile_products["product_items"]:[] == []) {
	y2warning ("Empty file %1", parse_file);
	return [];
    }

    list <map> products = [];


    boolean run_ask_user = false;

    foreach (map one_prod, xmlfile_products["product_items"]:[], {
	if (! haskey (one_prod, "url")) {
	    y2error ("No 'url' defined in %1", one_prod);
	    return;
	}

	// FATE #302123
	if (base_url != nil && base_url != "") {
	    one_prod["url"] = GetAbsoluteURL (base_url, one_prod["url"]:"");
	}

	if (one_prod["ask_user"]:false == true) {
	    run_ask_user = true;
	}

	products = add (products, one_prod);
    });

    if (run_ask_user) {
	products = UserSelectsRequiredAddOns (products);
    }

    return products;
}

/**
 * Auto-integrate add-on products in specified file (usually add_on_products file)
 *
 * @param filelist string a file containing a list of add-on products to integrate
 * @see FATE #303675: Support several add-ons on standard medium
 * @return boolean true on exit
 *
 * @struct
 * Format of /add_on_products.xml file on media root:
 * <?xml version="1.0"?>
 * <add_on_products xmlns="http://www.suse.com/1.0/yast2ns"
 *	xmlns:config="http://www.suse.com/1.0/configns">
 *	<product_items config:type="list">
 *		<product_item>
 *			<!-- Product name visible in UI when offered to user (optional item) -->
 *			<name>Add-on Name to Display</name>
 *			<!-- Product URL (mandatory item) -->
 *			<url>http://product.repository/url/</url>
 *			<!-- Product path, default is "/" (optional item) -->
 *			<path>/relative/product/path</path>
 *			<!--
 *				List of products to install from media, by default all products
 *				from media are installed (optional item)
 *			-->
 *			<install_products config:type="list">
 *				<!--
 *					Product to install - matching the metadata product 'name'
 *					(mandatory to fully define 'install_products')
 *				-->
 *				<product>Product-ID-From-Repository</product>
 *				<product>...</product>
 *			</install_products>
 *			<!--
 *				If set to 'true', user is asked whether to install this product,
 *				default is 'false' (optional)
 *			-->
 *			<ask_user config:type="boolean">true</ask_user>
 *			<!--
 *				Connected to 'ask_user', sets the default status of product,
 *				default is 'false' (optional)
 *			-->
 *			<selected config:type="boolean">true</selected>
 *		</product_item>
 *		<product_item>
 *			...
 *		</product_item>
 *	</product_items>
 * </add_on_products>
 */
global boolean AddPreselectedAddOnProducts (string filelist) {
    if (filelist == nil)
    {
	y2milestone ("No add-on products defined on the media");
	return true;
    }

    string base_url = GetBaseProductURL();
    y2milestone ("Base URL: %1", URL::HidePassword(base_url));

    list <map> add_products = [];

    // new xml format
    if (preselected_add_ons == "xml") {
	add_products = ParseXMLBasedAddOnProductsFile (filelist, base_url);
    // old fallback
    } else if (preselected_add_ons == "plain") {
	add_products = ParsePlainAddOnProductsFile (filelist, base_url);
    } else {
	y2error ("Unsupported type: %1", preselected_add_ons);
	return false;
    }

    y2milestone ("Adding products: %1", add_products);
    foreach (map one_product, add_products, {
	string url = one_product["url"]:"";
	string pth = one_product["path"]:"";

	y2milestone ("Adding Repository: %1 %2", url, pth);

	integer src = Pkg::SourceCreate (url, pth);

	if (src == nil || src < 0) {
	    y2error ("Unable to add product: %1", url);
	    // TRANSLATORS: error message, %1 is replaced with product URL
	    Report::Error (sformat (_("Unable to add product %1."), url));
	    return;
	}

	if (! AcceptedLicenseAndInfoFile (src)) {
	    Pkg::SourceDelete (src);
	    return;
	}
	Integrate (src);

	// adding the product to the list of products
	// bugzilla #269625
	map <string, string> prod = (map <string, string>) Pkg::SourceProductData (src);

	AddOnProduct::add_on_products = add (AddOnProduct::add_on_products, $[
	    "media" : src,
	    "product" : prod["label"]:prod["productname"]:prod["productversion"]:"",
	    "autoyast_product" : prod["productname"]:"",
	    "media_url" : url,
	    "product_dir" : pth,
	]);

	list <string> prods_to_install = one_product["install_products"]:[];

	// there are more products at the destination
	// install the listed ones only
	if (prods_to_install != nil && size (prods_to_install) > 0) {
	    foreach (string one_prod, prods_to_install, {
		y2milestone ("Selecting product '%1' for installation", one_prod);
		Pkg::ResolvableInstall (one_prod, `product);
	    });

	// install all products from the destination
	} else {
	    list<map<string,any> > products = Pkg::ResolvableProperties ("", `product, "");
	    // only those that come from the new source
	    products = filter (map<string,any> p, products, {
		return p["source"]:-1 == src;
	    });

	    foreach (map<string,any> p, products, {
		y2milestone ("Selecting product '%1' for installation", p["name"]:"");
		Pkg::ResolvableInstall (p["name"]:"", `product);
	    });
	}
    });

    // reread agents, redraw wizard steps, etc.
    ReIntegrateFromScratch();
}

/* Export/Import --> */

/**
 * Returns map describing all used add-ons.
 *
 * @return map
 *
 * @struct This is an XML file created from exported map:
 * <add-on>
 *   <add_on_products config:type="list">
 *     <listentry>
 *       <media_url>ftp://server.name/.../</media_url>
 *       <product>NEEDS_TO_MATCH_"PRODUCT"_TAG_FROM_content_FILE!</product>
 *       <product_dir>/</product_dir>
 *     </listentry>
 *     ...
 *   </add_on_products>
 * </add-on>
 */
global map Export () {
    y2milestone ("Add-Ons Input: %1", add_on_products);

    list<map<string,any> > exp = maplist (map<string,any> p, add_on_products, {
	if (haskey (p, "media"))
	    p = remove (p, "media");

	// bugzilla #279893
	if (haskey (p, "autoyast_product")) {
	    p["product"] = p["autoyast_product"]:"";
	    p = remove (p, "autoyast_product");
	}

	return p;
    });

    y2milestone ("Add-Ons Output: %1", exp);

    return $[
	"add_on_products" : exp,
    ];
}

global boolean Import (map settings) {
    add_on_products = settings["add_on_products"]:[];
    modified = false;
    if (Mode::config ())
    {
	foreach (map prod, add_on_products, {
	    string media = prod["media_url"]:"";
	    string pth = prod["product_dir"]:"/";
	    integer src = Pkg::SourceCreate (media, pth);
	    if (src != -1)
		mode_config_sources = add (mode_config_sources, src);
	});
    }
    return true;
}

global void CleanModeConfigSources () {
    foreach (integer src, mode_config_sources, {
	Pkg::SourceDelete (src);
    });
    mode_config_sources = [];
}

/**
 * Returns the path where Add-Ons configuration is stored during the fist stage installation.
 * This path reffers to the installed system.
 *
 * @see bugzilla #187558
 */
global string TmpExportFilename () {
    return Directory::vardir + "/exported_add_ons_configuration";
}

/**
 * Reads the Add-Ons configuration stored on disk during the first stage installation.
 *
 * @see bugzilla #187558
 */
global boolean ReadTmpExportFilename () {
    string tmp_filename = TmpExportFilename();
    modified = true;

    if (FileUtils::Exists(tmp_filename)) {
	y2milestone ("Reading %1 content", tmp_filename);

	// there might be something already set, store the current configuration
	list <map <string,any> > already_in_configuration = add_on_products;
	map configuration_from_disk  = (map) SCR::Read (.target.ycp, tmp_filename);
	y2milestone ("Configuration from disk: %1", configuration_from_disk);

	if (configuration_from_disk != nil) {
	    Import (configuration_from_disk);
	    if (already_in_configuration != [] && already_in_configuration != nil) {
		add_on_products = (list <map <string,any> >) union (add_on_products, already_in_configuration);
	    }
	    return true;
	} else {
	    y2error ("Reading %1 file returned nil result!", tmp_filename);
	    return false;
	}
    } else {
	y2warning ("File %1 doesn't exists, skipping...", tmp_filename);
	return true;
    }
}


boolean AcceptUnsignedFile(string file, integer repo)
{
    y2milestone("Accepting unsigned file %1 from repository %2", file, repo);
    return true;
}

boolean RejectUnsignedFile(string file, integer repo)
{
    y2milestone("Rejecting unsigned file %1 from repository %2", file, repo);
    return false;
}

boolean AcceptFileWithoutChecksum(string file)
{
    y2milestone("Accepting file without checksum: %1", file);
    return true;
}

boolean RejectFileWithoutChecksum(string file)
{
    y2milestone("Rejecting file without checksum: %1", file);
    return false;
}

boolean AcceptVerificationFailed(string file, map<string,any> key, integer repo)
{
    y2milestone("Accepting failed verification of file %1 with key %2 from repository %3", file, key, repo);
    return true;
}

boolean RejectVerificationFailed(string file, map<string,any> key, integer repo)
{
    y2milestone("Rejecting failed verification of file %1 with key %2 from repository %3", file, key, repo);
    return false;
}


global boolean AcceptUnknownGpgKeyCallback( string filename, string keyid, integer repo)
{
    y2milestone("AcceptUnknownGpgKeyCallback %1: %2 (from repository %3)", filename, keyid, repo);

    return ( current_addon["signature-handling","accept_unknown_gpg_key","all"]:false ||
             contains( current_addon["signature-handling","accept_unknown_gpg_key","keys"]:[], keyid ) );
}

global boolean ImportGpgKeyCallback(map<string,any> key, integer repo)
{
    y2milestone("ImportGpgKeyCallback: %1 from repository %2", key, repo);

    return ( current_addon["signature-handling","import_gpg_key","all"]:false ||
             contains( current_addon["signature-handling","import_gpg_key","keys"]:[], key["id"]:"" ) );
}

global boolean AcceptNonTrustedGpgKeyCallback(map<string,any> key)
{
    y2milestone("AcceptNonTrustedGpgKeyCallback %1", key);

    return ( current_addon["signature-handling","accept_non_trusted_gpg_key","all"]:false ||
             contains( current_addon["signature-handling","accept_non_trusted_gpg_key","keys"]:[], key["id"]:"" ) );
}


/* <-- Export/Import */

/*
  <add-on>
    <add_on_products config:type="list">
      <listentry>
        <media_url>http://software.opensuse.org/download/server:/dns/SLE_10/</media_url>
        <product>buildservice</product>
        <product_dir>/</product_dir>
        <signature-handling>
           <accept_unsigned_file config:type="boolean">true</accept_unsigned_file>
           <accept_file_without_checksum config:type="boolean">true</accept_file_without_checksum>
           <accept_verification_failed config:type="boolean">true</accept_verification_failed>
           <accept_unknown_gpg_key>
             <all config:type="boolean">true</all>
             <keys config:type="list">
                <keyid>...</keyid>
                <keyid>3B3011B76B9D6523</keyid>
             </keys>
           </accept_unknown_gpg_key>
           <accept_non_trusted_gpg_key>
             <all config:type="boolean">true</all>
             <keys config:type="list">
                <keyid>...</keyid>
             </keys>
           </accept_non_trusted_gpg_key>
           <import_gpg_key>
             <all config:type="boolean">true</all>
             <keys config:type="list">
                <keyid>...</keyid>
             </keys>
           </import_gpg_key>
        </signature-handling>
      </listentry>
    </add_on_products>
  </add-on>
*/
global void SetSignatureCallbacks( string product ) {
    current_addon = $[];
    foreach( map<string,any> addon, add_on_products, ``{
        if( addon["product"]:"" != product )
            continue;
        current_addon = addon; // remember the current addon for the Callbacks
        if( haskey( addon["signature-handling"]:$[], "accept_unsigned_file" ) )
            Pkg::CallbackAcceptUnsignedFile(
                addon["signature-handling","accept_unsigned_file"]:false ? AcceptUnsignedFile : RejectUnsignedFile
            );
        if( haskey( addon["signature-handling"]:$[], "accept_file_without_checksum" ) )
            Pkg::CallbackAcceptFileWithoutChecksum(
                addon["signature-handling","accept_file_without_checksum"]:false ? AcceptFileWithoutChecksum : RejectFileWithoutChecksum
            );
        if( haskey( addon["signature-handling"]:$[], "accept_verification_failed") )
            Pkg::CallbackAcceptVerificationFailed(
                addon["signature-handling","accept_verification_failed"]:false ? AcceptVerificationFailed : RejectVerificationFailed
            );
        if( haskey( addon["signature-handling"]:$[], "accept_unknown_gpg_key") )
            Pkg::CallbackAcceptUnknownGpgKey(AddOnProduct::AcceptUnknownGpgKeyCallback);
        if( haskey( addon["signature-handling"]:$[], "import_gpg_key") )
            Pkg::CallbackImportGpgKey(AddOnProduct::ImportGpgKeyCallback);
        if( haskey( addon["signature-handling"]:$[], "accept_non_trusted_gpg_key") )
            Pkg::CallbackAcceptNonTrustedGpgKey(AddOnProduct::AcceptNonTrustedGpgKeyCallback);

        break;
    });
    return;
}

} // module end

ACC SHELL 2018