/* * Copyright (C) 2005-2018 Team Kodi * This file is part of Kodi - https://kodi.tv * * SPDX-License-Identifier: GPL-2.0-or-later * See LICENSES/README.md for more information. */ #include "GUIIncludes.h" #include "GUIInfoManager.h" #include "addons/Skin.h" #include "guilib/GUIComponent.h" #include "guilib/guiinfo/GUIInfoLabel.h" #include "interfaces/info/SkinVariable.h" #include "utils/StringUtils.h" #include "utils/XBMCTinyXML.h" #include "utils/XMLUtils.h" #include "utils/log.h" using namespace KODI::GUILIB; CGUIIncludes::CGUIIncludes() { m_constantAttributes.insert("x"); m_constantAttributes.insert("y"); m_constantAttributes.insert("width"); m_constantAttributes.insert("height"); m_constantAttributes.insert("center"); m_constantAttributes.insert("max"); m_constantAttributes.insert("min"); m_constantAttributes.insert("w"); m_constantAttributes.insert("h"); m_constantAttributes.insert("time"); m_constantAttributes.insert("acceleration"); m_constantAttributes.insert("delay"); m_constantAttributes.insert("start"); m_constantAttributes.insert("end"); m_constantAttributes.insert("center"); m_constantAttributes.insert("border"); m_constantAttributes.insert("repeat"); m_constantNodes.insert("posx"); m_constantNodes.insert("posy"); m_constantNodes.insert("left"); m_constantNodes.insert("centerleft"); m_constantNodes.insert("right"); m_constantNodes.insert("centerright"); m_constantNodes.insert("top"); m_constantNodes.insert("centertop"); m_constantNodes.insert("bottom"); m_constantNodes.insert("centerbottom"); m_constantNodes.insert("width"); m_constantNodes.insert("height"); m_constantNodes.insert("offsetx"); m_constantNodes.insert("offsety"); m_constantNodes.insert("textoffsetx"); m_constantNodes.insert("textoffsety"); m_constantNodes.insert("textwidth"); m_constantNodes.insert("spinposx"); m_constantNodes.insert("spinposy"); m_constantNodes.insert("spinwidth"); m_constantNodes.insert("spinheight"); m_constantNodes.insert("radioposx"); m_constantNodes.insert("radioposy"); m_constantNodes.insert("radiowidth"); m_constantNodes.insert("radioheight"); m_constantNodes.insert("sliderwidth"); m_constantNodes.insert("sliderheight"); m_constantNodes.insert("itemgap"); m_constantNodes.insert("bordersize"); m_constantNodes.insert("timeperimage"); m_constantNodes.insert("fadetime"); m_constantNodes.insert("pauseatend"); m_constantNodes.insert("depth"); m_constantNodes.insert("movement"); m_constantNodes.insert("focusposition"); m_expressionAttributes.insert("condition"); m_expressionNodes.insert("visible"); m_expressionNodes.insert("enable"); m_expressionNodes.insert("usealttexture"); m_expressionNodes.insert("selected"); } CGUIIncludes::~CGUIIncludes() = default; void CGUIIncludes::Clear() { m_includes.clear(); m_defaults.clear(); m_constants.clear(); m_skinvariables.clear(); m_files.clear(); m_expressions.clear(); } void CGUIIncludes::Load(const std::string &file) { if (!Load_Internal(file)) return; FlattenExpressions(); FlattenSkinVariableConditions(); } bool CGUIIncludes::Load_Internal(const std::string &file) { // check to see if we already have this loaded if (HasLoaded(file)) return true; CXBMCTinyXML doc; if (!doc.LoadFile(file)) { CLog::Log(LOGINFO, "Error loading include file {}: {} (row: {}, col: {})", file, doc.ErrorDesc(), doc.ErrorRow(), doc.ErrorCol()); return false; } TiXmlElement *root = doc.RootElement(); if (!root || !StringUtils::EqualsNoCase(root->Value(), "includes")) { CLog::Log(LOGERROR, "Error loading include file {}: Root element required.", file); return false; } // load components LoadDefaults(root); LoadConstants(root); LoadExpressions(root); LoadVariables(root); LoadIncludes(root); m_files.push_back(file); return true; } void CGUIIncludes::LoadDefaults(const TiXmlElement *node) { if (!node) return; const TiXmlElement* child = node->FirstChildElement("default"); while (child) { const char *type = child->Attribute("type"); if (type && child->FirstChild()) m_defaults.insert(std::make_pair(type, *child)); child = child->NextSiblingElement("default"); } } void CGUIIncludes::LoadExpressions(const TiXmlElement *node) { if (!node) return; const TiXmlElement* child = node->FirstChildElement("expression"); while (child) { const char *tagName = child->Attribute("name"); if (tagName && child->FirstChild()) m_expressions.insert(std::make_pair(tagName, "[" + child->FirstChild()->ValueStr() + "]")); child = child->NextSiblingElement("expression"); } } void CGUIIncludes::LoadConstants(const TiXmlElement *node) { if (!node) return; const TiXmlElement* child = node->FirstChildElement("constant"); while (child) { const char *tagName = child->Attribute("name"); if (tagName && child->FirstChild()) m_constants.insert(std::make_pair(tagName, child->FirstChild()->ValueStr())); child = child->NextSiblingElement("constant"); } } void CGUIIncludes::LoadVariables(const TiXmlElement *node) { if (!node) return; const TiXmlElement* child = node->FirstChildElement("variable"); while (child) { const char *tagName = child->Attribute("name"); if (tagName && child->FirstChild()) m_skinvariables.insert(std::make_pair(tagName, *child)); child = child->NextSiblingElement("variable"); } } void CGUIIncludes::LoadIncludes(const TiXmlElement *node) { if (!node) return; const TiXmlElement* child = node->FirstChildElement("include"); while (child) { const char *tagName = child->Attribute("name"); if (tagName && child->FirstChild()) { // we'll parse and store parameter list with defaults when include definition is first encountered // if there's a tag only use its body as the actually included part const TiXmlElement *definitionTag = child->FirstChildElement("definition"); const TiXmlElement *includeBody = definitionTag ? definitionTag : child; // if there's a tag there also must be a tag Params defaultParams; bool haveParamTags = GetParameters(child, "default", defaultParams); if (haveParamTags && !definitionTag) CLog::Log(LOGWARNING, "Skin has invalid include definition: {}", tagName); else m_includes.insert({ tagName, { *includeBody, std::move(defaultParams) } }); } else if (child->Attribute("file")) { std::string file = g_SkinInfo->GetSkinPath(child->Attribute("file")); const char *condition = child->Attribute("condition"); if (condition) { // load include file if condition evals to true if (CServiceBroker::GetGUI()->GetInfoManager().Register(condition)->Get( INFO::DEFAULT_CONTEXT)) Load_Internal(file); } else Load_Internal(file); } child = child->NextSiblingElement("include"); } } void CGUIIncludes::FlattenExpressions() { for (auto& expression : m_expressions) { std::vector resolved = std::vector(); resolved.push_back(expression.first); FlattenExpression(expression.second, resolved); } } void CGUIIncludes::FlattenExpression(std::string &expression, const std::vector &resolved) { std::string original(expression); GUIINFO::CGUIInfoLabel::ReplaceSpecialKeywordReferences(expression, "EXP", [&](const std::string &expressionName) -> std::string { if (std::find(resolved.begin(), resolved.end(), expressionName) != resolved.end()) { CLog::Log(LOGERROR, "Skin has a circular expression \"{}\": {}", resolved.back(), original); return std::string(); } auto it = m_expressions.find(expressionName); if (it == m_expressions.end()) return std::string(); std::vector rescopy = resolved; rescopy.push_back(expressionName); FlattenExpression(it->second, rescopy); return it->second; }); } void CGUIIncludes::FlattenSkinVariableConditions() { for (auto& variable : m_skinvariables) { TiXmlElement* valueNode = variable.second.FirstChildElement("value"); while (valueNode) { const char *condition = valueNode->Attribute("condition"); if (condition) valueNode->SetAttribute("condition", ResolveExpressions(condition)); valueNode = valueNode->NextSiblingElement("value"); } } } bool CGUIIncludes::HasLoaded(const std::string &file) const { for (const auto& loadedFile : m_files) { if (loadedFile == file) return true; } return false; } void CGUIIncludes::Resolve(TiXmlElement *node, std::map* xmlIncludeConditions /* = NULL */) { if (!node) return; SetDefaults(node); ResolveConstants(node); ResolveExpressions(node); ResolveIncludes(node, xmlIncludeConditions); TiXmlElement *child = node->FirstChildElement(); while (child) { // recursive call Resolve(child, xmlIncludeConditions); child = child->NextSiblingElement(); } } void CGUIIncludes::SetDefaults(TiXmlElement *node) { if (node->ValueStr() != "control") return; std::string type = XMLUtils::GetAttribute(node, "type"); const auto it = m_defaults.find(type); if (it != m_defaults.end()) { // we don't insert et. al. if or is specified bool hasPosX(node->FirstChild("posx") != nullptr); bool hasPosY(node->FirstChild("posy") != nullptr); const TiXmlElement &element = (*it).second; const TiXmlElement *tag = element.FirstChildElement(); while (tag) { std::string value = tag->ValueStr(); bool skip(false); if (hasPosX && (value == "left" || value == "right" || value == "centerleft" || value == "centerright")) skip = true; if (hasPosY && (value == "top" || value == "bottom" || value == "centertop" || value == "centerbottom")) skip = true; // we insert at the end of block if (!skip) node->InsertEndChild(*tag); tag = tag->NextSiblingElement(); } } } void CGUIIncludes::ResolveConstants(TiXmlElement *node) { if (!node) return; TiXmlNode *child = node->FirstChild(); if (child && child->Type() == TiXmlNode::TINYXML_TEXT && m_constantNodes.count(node->ValueStr())) { child->SetValue(ResolveConstant(child->ValueStr())); } else { TiXmlAttribute *attribute = node->FirstAttribute(); while (attribute) { if (m_constantAttributes.count(attribute->Name())) attribute->SetValue(ResolveConstant(attribute->ValueStr())); attribute = attribute->Next(); } } } void CGUIIncludes::ResolveExpressions(TiXmlElement *node) { if (!node) return; TiXmlNode *child = node->FirstChild(); if (child && child->Type() == TiXmlNode::TINYXML_TEXT && m_expressionNodes.count(node->ValueStr())) { child->SetValue(ResolveExpressions(child->ValueStr())); } else { TiXmlAttribute *attribute = node->FirstAttribute(); while (attribute) { if (m_expressionAttributes.count(attribute->Name())) attribute->SetValue(ResolveExpressions(attribute->ValueStr())); attribute = attribute->Next(); } } } void CGUIIncludes::ResolveIncludes(TiXmlElement *node, std::map* xmlIncludeConditions /* = NULL */) { if (!node) return; TiXmlElement *include = node->FirstChildElement("include"); while (include) { // file: load includes from specified XML file const char *file = include->Attribute("file"); if (file) Load(g_SkinInfo->GetSkinPath(file)); // condition: process include if condition evals to true const char *condition = include->Attribute("condition"); if (condition) { INFO::InfoPtr conditionID = CServiceBroker::GetGUI()->GetInfoManager().Register(ResolveExpressions(condition)); bool value = false; if (conditionID) { value = conditionID->Get(INFO::DEFAULT_CONTEXT); if (xmlIncludeConditions) xmlIncludeConditions->insert(std::make_pair(conditionID, value)); } if (!value) { include = include->NextSiblingElement("include"); continue; } } Params params; std::string tagName; // determine which form of include call we have const char *name = include->Attribute("content"); if (name) { // // or // // // 150 // ... // tagName = name; GetParameters(include, "value", params); } else { const TiXmlNode *child = include->FirstChild(); if (child && child->Type() == TiXmlNode::TINYXML_TEXT) { // MyControl // old-style includes for backward compatibility tagName = child->ValueStr(); } } // check, whether the include exists and therefore should be replaced by its definition auto it = m_includes.find(tagName); if (it != m_includes.end()) { const TiXmlElement *includeDefinition = &it->second.first; // combine passed include parameters with their default values into a single list (no overwrites) const Params& defaultParams = it->second.second; params.insert(defaultParams.begin(), defaultParams.end()); // process include definition const TiXmlElement *includeDefinitionChild = includeDefinition->FirstChildElement(); while (includeDefinitionChild) { // insert before element to keep order of occurrence in xml file TiXmlElement *insertedNode = static_cast(node->InsertBeforeChild(include, *includeDefinitionChild)); // process nested InsertNested(node, include, insertedNode); // resolve parameters even if parameter list is empty (to remove param references) ResolveParametersForNode(insertedNode, params); includeDefinitionChild = includeDefinitionChild->NextSiblingElement(); } // remove the include element itself node->RemoveChild(include); include = node->FirstChildElement("include"); } else { // invalid include CLog::Log(LOGWARNING, "Skin has invalid include: {}", tagName); include = include->NextSiblingElement("include"); } } } void CGUIIncludes::InsertNested(TiXmlElement *controls, TiXmlElement *include, TiXmlElement *node) { TiXmlElement *target; TiXmlElement *nested; if (node->ValueStr() == "nested") { nested = node; target = controls; } else { nested = node->FirstChildElement("nested"); target = node; } if (nested) { // copy all child elements except param elements const TiXmlElement *child = include->FirstChildElement(); while (child) { if (child->ValueStr() != "param") { // insert before element to keep order of occurrence in xml file target->InsertBeforeChild(nested, *child); } child = child->NextSiblingElement(); } target->RemoveChild(nested); } } bool CGUIIncludes::GetParameters(const TiXmlElement *include, const char *valueAttribute, Params& params) { bool foundAny = false; // collect parameters from include tag // // // 150 // ... // if (include) { const TiXmlElement *param = include->FirstChildElement("param"); foundAny = param != NULL; // doesn't matter if param isn't entirely valid while (param) { std::string paramName = XMLUtils::GetAttribute(param, "name"); if (!paramName.empty()) { std::string paramValue; // const char *value = param->Attribute(valueAttribute); // try attribute first if (value) paramValue = value; else { // 120 const TiXmlNode *child = param->FirstChild(); if (child && child->Type() == TiXmlNode::TINYXML_TEXT) paramValue = child->ValueStr(); // and then tag value } params.insert({ paramName, paramValue }); // no overwrites } param = param->NextSiblingElement("param"); } } return foundAny; } void CGUIIncludes::ResolveParametersForNode(TiXmlElement *node, const Params& params) { if (!node) return; std::string newValue; // run through this element's attributes, resolving any parameters TiXmlAttribute *attribute = node->FirstAttribute(); while (attribute) { ResolveParamsResult result = ResolveParameters(attribute->ValueStr(), newValue, params); if (result == SINGLE_UNDEFINED_PARAM_RESOLVED && strcmp(node->Value(), "param") == 0 && strcmp(attribute->Name(), "value") == 0 && node->Parent() && strcmp(node->Parent()->Value(), "include") == 0) { // special case: passing to the nested include // this usually happens when trying to forward a missing parameter from the enclosing include to the nested include // problem: since 'undefinedParam' is not defined, it expands to and overrides any default value set with in the nested include // to prevent this, we'll completely remove this parameter from the nested include call so that the default value can be correctly picked up later node->Parent()->RemoveChild(node); return; } else if (result != NO_PARAMS_FOUND) attribute->SetValue(newValue); attribute = attribute->Next(); } // run through this element's value and children, resolving any parameters TiXmlNode *child = node->FirstChild(); if (child) { if (child->Type() == TiXmlNode::TINYXML_TEXT) { ResolveParamsResult result = ResolveParameters(child->ValueStr(), newValue, params); if (result == SINGLE_UNDEFINED_PARAM_RESOLVED && strcmp(node->Value(), "param") == 0 && node->Parent() && strcmp(node->Parent()->Value(), "include") == 0) { // special case: passing $PARAM[undefinedParam] to the nested include // we'll remove the offending param tag same as above node->Parent()->RemoveChild(node); } else if (result != NO_PARAMS_FOUND) child->SetValue(newValue); } else if (child->Type() == TiXmlNode::TINYXML_ELEMENT || child->Type() == TiXmlNode::TINYXML_COMMENT) { do { // save next as current child might be removed from the tree TiXmlElement* next = child->NextSiblingElement(); if (child->Type() == TiXmlNode::TINYXML_ELEMENT) ResolveParametersForNode(static_cast(child), params); child = next; } while (child); } } } class ParamReplacer { const std::map& m_params; // keep some stats so that we know exactly what's been resolved int m_numTotalParams; int m_numUndefinedParams; public: explicit ParamReplacer(const std::map& params) : m_params(params), m_numTotalParams(0), m_numUndefinedParams(0) {} int GetNumTotalParams() const { return m_numTotalParams; } int GetNumDefinedParams() const { return m_numTotalParams - m_numUndefinedParams; } int GetNumUndefinedParams() const { return m_numUndefinedParams; } std::string operator()(const std::string ¶mName) { m_numTotalParams++; std::map::const_iterator it = m_params.find(paramName); if (it != m_params.end()) return it->second; m_numUndefinedParams++; return ""; } }; CGUIIncludes::ResolveParamsResult CGUIIncludes::ResolveParameters(const std::string& strInput, std::string& strOutput, const Params& params) { ParamReplacer paramReplacer(params); if (GUIINFO::CGUIInfoLabel::ReplaceSpecialKeywordReferences(strInput, "PARAM", std::ref(paramReplacer), strOutput)) // detect special input values of the form "$PARAM[undefinedParam]" (with no extra characters around) return paramReplacer.GetNumUndefinedParams() == 1 && paramReplacer.GetNumTotalParams() == 1 && strOutput.empty() ? SINGLE_UNDEFINED_PARAM_RESOLVED : PARAMS_RESOLVED; return NO_PARAMS_FOUND; } std::string CGUIIncludes::ResolveConstant(const std::string &constant) const { std::vector values = StringUtils::Split(constant, ","); for (auto& i : values) { std::map::const_iterator it = m_constants.find(i); if (it != m_constants.end()) i = it->second; } return StringUtils::Join(values, ","); } std::string CGUIIncludes::ResolveExpressions(const std::string &expression) const { std::string work(expression); GUIINFO::CGUIInfoLabel::ReplaceSpecialKeywordReferences(work, "EXP", [&](const std::string &str) -> std::string { std::map::const_iterator it = m_expressions.find(str); if (it != m_expressions.end()) return it->second; return ""; }); return work; } const INFO::CSkinVariableString* CGUIIncludes::CreateSkinVariable(const std::string& name, int context) { std::map::const_iterator it = m_skinvariables.find(name); if (it != m_skinvariables.end()) return INFO::CSkinVariable::CreateFromXML(it->second, context); return NULL; }