| /* |
| * Copyright 2012 Research In Motion Limited. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| /*jshint sub:true*/ |
| var fs = require("fs"), |
| util = require('util'), |
| xml2js = require('xml2js'), |
| packagerUtils = require('./packager-utils'), |
| check = require('validator').check, |
| sanitize = require('validator').sanitize, |
| localize = require("./localize"), |
| logger = require("./logger"), |
| fileManager = require("./file-manager"), |
| utils = require("./packager-utils"), |
| i18nMgr = require("./i18n-manager"), |
| et = require("elementtree"), |
| _config_doc, |
| _self, |
| _predefinedFeatures; |
| |
| //This function will convert a wc3 paramObj with a list of |
| //<param name="" value=""> elements into a single object |
| function processParamObj(paramObj) { |
| var processedObj = {}, |
| attribs, |
| paramName, |
| paramValue; |
| |
| if (paramObj) { |
| //Convert to array for single param entries where only an object is created |
| if (!Array.isArray(paramObj)) { |
| paramObj = [paramObj]; |
| } |
| |
| paramObj.forEach(function (param) { |
| attribs = param["@"]; |
| |
| if (attribs) { |
| paramName = attribs["name"]; |
| paramValue = attribs["value"]; |
| |
| if (paramName && paramValue) { |
| //Add the key/value pair to the processedObj |
| processedObj[paramName] = paramValue; |
| } |
| } |
| }); |
| } |
| |
| return processedObj; |
| } |
| |
| function processFeatures(featuresArray, widgetConfig, processPredefinedFeatures) { |
| var features = [], |
| attribs; |
| if (featuresArray) { |
| featuresArray.forEach(function (feature) { |
| attribs = feature["@"]; |
| if (attribs) { |
| attribs.required = packagerUtils.toBoolean(attribs.required, true); |
| |
| // We do NOT want to auto defer networking and JavaScript if the |
| // blackberry.push feature is being used |
| if (attribs.id === "blackberry.push") { |
| widgetConfig.autoDeferNetworkingAndJavaScript = false; |
| } |
| |
| if (_predefinedFeatures[attribs.id]) { |
| //Handle features that do NOT contain an API namespace |
| if (processPredefinedFeatures) { |
| _predefinedFeatures[attribs.id](feature, widgetConfig); |
| } |
| } else { |
| features.push(attribs); |
| } |
| } else { |
| features.push(attribs); |
| } |
| }); |
| } |
| |
| return features; |
| } |
| |
| function createAccessListObj(uriOrOrigin, allowSubDomain) { |
| return { |
| uri: uriOrOrigin, |
| allowSubDomain: allowSubDomain |
| }; |
| } |
| |
| function processVersion(widgetConfig) { |
| if (widgetConfig.version) { |
| var versionArray = widgetConfig.version.split("."); |
| |
| //if 4rth number in version exists, extract for build id |
| if (versionArray.length > 3) { |
| widgetConfig.buildId = versionArray[3]; |
| widgetConfig.version = widgetConfig.version.substring(0, widgetConfig.version.lastIndexOf('.')); |
| } |
| } |
| } |
| |
| function processBuildID(widgetConfig, session) { |
| if (session.buildId) { |
| //user specified a build id (--buildId), override any previously set build id |
| widgetConfig.buildId = session.buildId; |
| } |
| } |
| |
| function processWidgetData(data, widgetConfig, session) { |
| var attribs, |
| featureArray, |
| uriExist, |
| originExist, |
| header; |
| |
| if (data["@"]) { |
| widgetConfig.version = data["@"].version; |
| widgetConfig.id = data["@"].id; |
| |
| if (data["@"]["rim:header"]) { |
| widgetConfig.customHeaders = {}; |
| header = data["@"]["rim:header"].split(":"); |
| // Just set it for now, in the future we can append them |
| widgetConfig.customHeaders[header[0]] = header[1]; |
| } |
| |
| if (data["@"]["rim:userAgent"]) { |
| widgetConfig.userAgent = data["@"]["rim:userAgent"]; |
| } |
| } |
| |
| //Default values |
| widgetConfig.hasMultiAccess = false; |
| widgetConfig.accessList = []; |
| widgetConfig.enableFlash = false; |
| widgetConfig.autoOrientation = true; |
| widgetConfig.autoDeferNetworkingAndJavaScript = true; |
| widgetConfig.theme = "default"; |
| widgetConfig.autoHideSplashScreen = "true"; |
| |
| //set locally available features to access list |
| if (data.feature) { |
| featureArray = packagerUtils.isArray(data.feature) ? data.feature : [data.feature]; |
| } |
| |
| //Handle features that do not have source code |
| featureArray = processFeatures(featureArray, widgetConfig, true); |
| |
| //Push empty WIDGET_LOCAL access obj until whitelisting is cleaned up |
| widgetConfig.accessList.push(createAccessListObj("WIDGET_LOCAL", true)); |
| |
| //add whitelisted features to access list |
| if (data.access) { |
| //If there is only one access list element, it will be parsed as an object and not an array |
| if (!packagerUtils.isArray(data.access)) { |
| data.access = [data.access]; |
| } |
| |
| //check if uri and origin attributes are used |
| data.access.forEach(function (accessElement, accessIndex) { |
| attribs = accessElement["@"]; |
| |
| if (attribs) { |
| if (attribs.uri) { |
| uriExist = true; |
| } |
| if (attribs.origin) { |
| originExist = true; |
| } |
| } |
| }); |
| if (uriExist === true && originExist === true) { |
| logger.warn(localize.translate("WARNING_URI_AND_ORIGIN_FOUND_IN_CONFIG")); |
| } |
| |
| data.access.forEach(function (accessElement) { |
| attribs = accessElement["@"]; |
| |
| if (attribs) { |
| if (attribs.uri === "*" || attribs.origin === "*") { |
| if (accessElement.feature) { |
| throw localize.translate("EXCEPTION_FEATURE_DEFINED_WITH_WILDCARD_ACCESS_URI_OR_ORIGIN"); |
| } |
| widgetConfig.hasMultiAccess = true; |
| } else { |
| attribs.subdomains = packagerUtils.toBoolean(attribs.subdomains); |
| if (attribs.uri || attribs.origin) { |
| if (uriExist === true && originExist === true) { |
| widgetConfig.accessList.push(createAccessListObj(attribs.uri, attribs.subdomains)); //using uri attribute by default |
| } else { |
| widgetConfig.accessList.push(createAccessListObj(attribs.uri || attribs.origin, attribs.subdomains)); |
| } |
| } |
| } |
| } |
| }); |
| } |
| } |
| |
| function trim(obj) { |
| return (typeof obj === "string" ? obj.trim() : obj); |
| } |
| |
| function processSplashScreenIconSrc(data, widgetConfig, key) { |
| if (data[key]) { |
| widgetConfig[key] = []; |
| |
| if (!(data[key] instanceof Array)) { |
| data[key] = [data[key]]; |
| } |
| |
| data[key].forEach(function (obj) { |
| if (obj["@"]) { |
| widgetConfig[key].push(obj["@"].src); |
| } else { |
| widgetConfig[key].push(obj); |
| } |
| }); |
| } |
| } |
| |
| function processSplashScreenData(data, widgetConfig) { |
| // |
| // This takes config.xml markup in the form of: |
| // |
| // <rim:splash src="splash-1280x768.jpg" /> |
| // <rim:splash src="splash-768x1280.jpg" /> |
| // <rim:splash src="splash-1024x600.jpg" /> |
| // <rim:splash src="splash-600x1024.jpg" /> |
| // |
| // and turns it into: |
| // |
| // icon: ["splash-1280x768.jpg", "splash-768x1280.jpg", "splash-1024x600.jpg", "splash-600x1024.jpg"] |
| // |
| // Folder-based localization now done in i18n-manager |
| // |
| processSplashScreenIconSrc(data, widgetConfig, "rim:splash"); |
| } |
| |
| function processIconData(data, widgetConfig, session) { |
| // |
| // This takes config.xml markup in the form of: |
| // |
| // <icon src="icon-86.png" /> |
| // <icon src="icon-150.png" /> |
| // |
| // and turns it into: |
| // |
| // icon: ["icon-86.png", "icon-150.png"] |
| // |
| // Folder-based localization now done in i18n-manager |
| // |
| var default_icon_filename = "default-icon.png", |
| default_icon_src = session.conf.DEFAULT_ICON, |
| default_icon_dst = session.sourceDir; |
| |
| processSplashScreenIconSrc(data, widgetConfig, "icon"); |
| |
| if (!widgetConfig.icon) { |
| packagerUtils.copyFile(default_icon_src, default_icon_dst); |
| |
| widgetConfig["icon"] = []; |
| widgetConfig["icon"].push(default_icon_filename); |
| } |
| } |
| |
| function validateSplashScreensIcon(widgetConfig, key) { |
| if (widgetConfig[key]) { |
| var msg = localize.translate(key === "icon" ? "EXCEPTION_INVALID_ICON_SRC" : "EXCEPTION_INVALID_SPLASH_SRC"); |
| |
| if (widgetConfig[key].length === 0) { |
| // element without src attribute |
| throw msg; |
| } else { |
| widgetConfig[key].forEach(function (src) { |
| var msg2 = localize.translate(key === "icon" ? "EXCEPTION_INVALID_ICON_SRC_LOCALES" : "EXCEPTION_INVALID_SPLASH_SRC_LOCALES"); |
| |
| // check that src attribute is specified and is not empty |
| check(src, msg).notNull(); |
| |
| // check that src attribute does not start with reserved locales folder |
| src = src.replace(/\\/g, "/"); |
| check(src, msg2).notRegex("^" + i18nMgr.LOCALES_DIR + "\/"); |
| }); |
| } |
| |
| } |
| } |
| |
| function processAuthorData(data, widgetConfig) { |
| if (data.author) { |
| var attribs = data.author["@"]; |
| |
| if (!attribs && typeof data.author === "string") { |
| //do not sanitize empty objects {} (must be string) |
| widgetConfig.author = sanitize(data.author).trim(); |
| } else if (data.author["#"]) { |
| widgetConfig.author = sanitize(data.author["#"]).trim(); |
| } |
| |
| if (attribs) { |
| widgetConfig.authorURL = attribs.href; |
| widgetConfig.copyright = attribs["rim:copyright"]; |
| widgetConfig.authorEmail = attribs.email; |
| } |
| } |
| } |
| |
| function processLicenseData(data, widgetConfig) { |
| if (data.license && data.license["#"]) { |
| widgetConfig.license = data.license["#"]; |
| } else { |
| widgetConfig.license = ""; |
| } |
| |
| if (data.license && data.license["@"]) { |
| widgetConfig.licenseURL = data.license["@"].href; |
| } else { |
| widgetConfig.licenseURL = ""; |
| } |
| } |
| |
| function processContentData(data, widgetConfig) { |
| if (data.content) { |
| var attribs = data.content["@"], |
| startPage; |
| if (attribs) { |
| widgetConfig.content = attribs.src; |
| |
| startPage = packagerUtils.parseUri(attribs.src); |
| |
| // if start page is local but does not start with local:///, will prepend it |
| // replace any backslash with forward slash |
| if (!packagerUtils.isAbsoluteURI(startPage) && !packagerUtils.isLocalURI(startPage)) { |
| if (!startPage.relative.match(/^\//)) { |
| widgetConfig.content = "local:///" + startPage.relative.replace(/\\/g, "/"); |
| } else { |
| widgetConfig.content = "local://" + startPage.relative.replace(/\\/g, "/"); |
| } |
| } |
| |
| widgetConfig.foregroundSource = attribs.src; |
| widgetConfig.contentType = attribs.type; |
| widgetConfig.contentCharSet = attribs.charset; |
| widgetConfig.allowInvokeParams = attribs["rim:allowInvokeParams"]; |
| //TODO content rim:background |
| } |
| } |
| } |
| |
| function processPermissionsData(data, widgetConfig) { |
| if (data["rim:permissions"] && data["rim:permissions"]["rim:permit"]) { |
| var permissions = data["rim:permissions"]["rim:permit"]; |
| |
| if (permissions instanceof Array) { |
| widgetConfig.permissions = permissions; |
| } else { |
| //user entered one permission and it comes in as an object |
| widgetConfig.permissions = [permissions]; |
| } |
| } else { |
| widgetConfig.permissions = []; |
| } |
| |
| // We do NOT want to auto defer networking and JavaScript if the |
| // run_when_backgrounded permission is set |
| if (widgetConfig.permissions.indexOf("run_when_backgrounded") >= 0) { |
| widgetConfig.autoDeferNetworkingAndJavaScript = false; |
| } |
| } |
| |
| function processInvokeTargetsData(data, widgetConfig) { |
| |
| if (data["rim:invoke-target"]) { |
| widgetConfig["invoke-target"] = data["rim:invoke-target"]; |
| |
| //If invoke-target is not an array, wrap the invoke-target in an array |
| utils.wrapPropertyInArray(widgetConfig, "invoke-target"); |
| |
| widgetConfig["invoke-target"].forEach(function (invokeTarget) { |
| if (invokeTarget.type && !packagerUtils.isEmpty(invokeTarget.type)) { |
| invokeTarget.type = invokeTarget.type.toUpperCase(); |
| } |
| |
| if (invokeTarget.filter) { |
| utils.wrapPropertyInArray(invokeTarget, "filter"); |
| |
| invokeTarget.filter.forEach(function (filter) { |
| |
| if (filter["action"]) { |
| utils.wrapPropertyInArray(filter, "action"); |
| } |
| |
| if (filter["mime-type"]) { |
| utils.wrapPropertyInArray(filter, "mime-type"); |
| } |
| |
| if (filter["property"]) { |
| utils.wrapPropertyInArray(filter, "property"); |
| } |
| }); |
| } |
| }); |
| } |
| } |
| |
| function validateConfig(widgetConfig) { |
| check(widgetConfig.version, localize.translate("EXCEPTION_INVALID_VERSION")) |
| .notNull() |
| .regex("^[0-9]{1,3}([.][0-9]{1,3}){2,3}$"); |
| |
| for (var prop in widgetConfig.name) { |
| if (widgetConfig.name.hasOwnProperty(prop)) { |
| check(widgetConfig.name[prop], localize.translate("EXCEPTION_INVALID_NAME")).notEmpty(); |
| } |
| } |
| |
| check(widgetConfig.author, localize.translate("EXCEPTION_INVALID_AUTHOR")).notNull(); |
| check(widgetConfig.id, localize.translate("EXCEPTION_INVALID_ID")).notNull().notEmpty(); |
| |
| validateSplashScreensIcon(widgetConfig, "rim:splash"); |
| |
| validateSplashScreensIcon(widgetConfig, "icon"); |
| |
| if (widgetConfig.accessList) { |
| widgetConfig.accessList.forEach(function (access) { |
| if (access.uri) { |
| if (access.uri !== "WIDGET_LOCAL") { |
| check(access.uri, localize.translate("EXCEPTION_INVALID_ACCESS_URI_NO_PROTOCOL", access.uri)) |
| .regex("^[a-zA-Z]+:\/\/"); |
| check(access.uri, localize.translate("EXCEPTION_INVALID_ACCESS_URI_NO_URN", access.uri)) |
| .notRegex("^[a-zA-Z]+:\/\/$"); |
| } |
| } |
| |
| if (access.features) { |
| // Assert each feature has a proper ID and is not empty |
| access.features.forEach(function (feature) { |
| if (!feature) { |
| throw localize.translate("EXCEPTION_INVALID_FEATURE_ID"); |
| } |
| check(feature.id, localize.translate("EXCEPTION_INVALID_FEATURE_ID")).notNull().notEmpty(); |
| }); |
| } |
| |
| }); |
| } |
| |
| if (widgetConfig["invoke-target"]) { |
| |
| widgetConfig["invoke-target"].forEach(function (invokeTarget) { |
| |
| check(typeof invokeTarget["@"] === "undefined", |
| localize.translate("EXCEPTION_INVOKE_TARGET_INVALID_ID")) |
| .equals(false); |
| check(invokeTarget["@"].id, localize.translate("EXCEPTION_INVOKE_TARGET_INVALID_ID")) |
| .notNull() |
| .notEmpty(); |
| check(invokeTarget.type, localize.translate("EXCEPTION_INVOKE_TARGET_INVALID_TYPE")) |
| .notNull() |
| .notEmpty(); |
| |
| if (invokeTarget.filter) { |
| |
| invokeTarget.filter.forEach(function (filter) { |
| |
| check(filter["action"] && filter["action"] instanceof Array && filter["action"].length > 0, |
| localize.translate("EXCEPTION_INVOKE_TARGET_ACTION_INVALID")) |
| .equals(true); |
| |
| check(filter["mime-type"] && filter["mime-type"] instanceof Array && filter["mime-type"].length > 0, |
| localize.translate("EXCEPTION_INVOKE_TARGET_MIME_TYPE_INVALID")) |
| .equals(true); |
| |
| if (filter.property) { |
| filter.property.forEach(function (property) { |
| check(property["@"] && property["@"]["var"] && typeof property["@"]["var"] === "string", |
| localize.translate("EXCEPTION_INVOKE_TARGET_FILTER_PROPERTY_INVALID")) |
| .equals(true); |
| }); |
| } |
| }); |
| } |
| }); |
| } |
| } |
| |
| function processLocalizedText(tag, data, widgetConfig) { |
| var tagData = data[tag], |
| DEFAULT = 'default'; |
| |
| function processLanguage(tagElement) { |
| var attribs = tagElement['@'], |
| language; |
| |
| if (attribs) { |
| language = attribs['xml:lang'] || DEFAULT; |
| widgetConfig[tag][language.toLowerCase()] = tagElement['#']; |
| } else { |
| widgetConfig[tag][DEFAULT] = tagElement; |
| } |
| } |
| |
| if (Array.isArray(tagData)) { |
| //i.e. <element xml:lang="en">english value</element> |
| // <element xml:lang="fr">french value</element> |
| tagData.forEach(processLanguage); |
| } else if (tagData instanceof Object) { |
| //i.e. <element xml:lang="en">english value</element> |
| processLanguage(tagData); |
| } else { |
| //i.e <element>value</element> |
| widgetConfig[tag][DEFAULT] = tagData; |
| } |
| } |
| |
| function processNameAndDescription(data, widgetConfig) { |
| widgetConfig.name = {}; |
| widgetConfig.description = {}; |
| |
| processLocalizedText('name', data, widgetConfig); |
| processLocalizedText('description', data, widgetConfig); |
| } |
| |
| function processCordovaPreferences(data, widgetConfig) { |
| if (data.preference) { |
| var preference = JSON.parse(JSON.stringify(processParamObj(data.preference)).toLowerCase()), |
| hideFormControl = preference.hidekeyboardformaccessorybar; |
| |
| widgetConfig.packageCordovaJs = preference.packagecordovajs === "enable"; |
| widgetConfig.autoHideSplashScreen = preference.autohidesplashscreen !== "false"; |
| |
| // <preference name="backgroundColor" value="hex" /> |
| if (preference.backgroundcolor) { |
| widgetConfig.backgroundColor = processBgColor(preference.backgroundcolor); |
| } |
| |
| // <preference name="childBrowser" value="enable or disable" /> |
| if (preference.childbrowser) { |
| widgetConfig.enableChildWebView = (preference.childbrowser === 'disable') === false; |
| } |
| |
| // <preference name="HideKeyboardFormAccessoryBar" value="enable/true or disable/false" /> |
| if (preference.hidekeyboardformaccessorybar) { |
| widgetConfig.enableFormControl = (hideFormControl !== 'enable') && (hideFormControl !== 'true'); |
| } |
| |
| // <preference name="popupBlocker" value="enable or disable" /> |
| if (preference.popupblocker) { |
| widgetConfig.enablePopupBlocker = (preference.popupblocker === 'enable') === true; |
| } |
| |
| // <preference name="orientation" value="portrait, landscape, north, auto or default" /> |
| if (preference.orientation) { |
| if (preference.orientation === "landscape" || preference.orientation === "portrait" || preference.orientation === "north") { |
| widgetConfig.autoOrientation = false; |
| widgetConfig.orientation = preference.orientation; |
| } else if (preference.orientation !== "auto" && preference.orientation !== "default") { |
| throw localize.translate("EXCEPTION_INVALID_ORIENTATION_MODE", preference.orientation); |
| } |
| } |
| |
| // <preference name="theme" value="bright, dark, inherit or default" /> |
| if (preference.theme && (typeof preference.theme === "string")) { |
| if (preference.theme === "bright" || preference.theme === "dark" || preference.theme === "inherit" || preference.theme === "default") { |
| widgetConfig.theme = preference.theme; |
| } |
| } |
| |
| // <preference name="webSecurity" value="enable or disable" /> |
| if (preference.websecurity && (typeof preference.websecurity === "string") && (preference.websecurity === "disable")) { |
| widgetConfig.enableWebSecurity = false; |
| logger.warn(localize.translate("WARNING_WEBSECURITY_DISABLED")); |
| } |
| |
| // <preference name="diskCache" value="enable or disable" /> |
| if (preference.diskcache) { |
| widgetConfig.enableDiskCache = (preference.diskcache === 'enable'); |
| } |
| } |
| } |
| |
| function processBgColor(bgColor) { |
| //convert bgColor to a number |
| var bgColorNum = parseInt(bgColor, 16); |
| |
| if (isNaN(bgColorNum)) { |
| //bgColor is not a number, throw error |
| throw localize.translate("EXCEPTION_BGCOLOR_INVALID", bgColor); |
| } else { |
| return bgColorNum; |
| } |
| } |
| |
| function processResult(data, session) { |
| var widgetConfig = {}; |
| |
| processWidgetData(data, widgetConfig, session); |
| processIconData(data, widgetConfig, session); |
| processAuthorData(data, widgetConfig); |
| processLicenseData(data, widgetConfig); |
| processContentData(data, widgetConfig); |
| processPermissionsData(data, widgetConfig); |
| processInvokeTargetsData(data, widgetConfig); |
| processSplashScreenData(data, widgetConfig); |
| processNameAndDescription(data, widgetConfig); |
| processCordovaPreferences(data, widgetConfig); |
| |
| widgetConfig.configXML = "config.xml"; |
| |
| //validate the widgetConfig |
| validateConfig(widgetConfig); |
| |
| //special handling for version and grabbing the buildId if specified (4rth number) |
| processVersion(widgetConfig); |
| |
| //if --buildId was specified, it takes precedence |
| processBuildID(widgetConfig, session); |
| |
| //store any config-file element injections |
| widgetConfig.configFileInjections = _config_doc.findall("config-file"); |
| |
| return widgetConfig; |
| } |
| |
| function init() { |
| //Predefined features are features that do NOT contain an API namespace |
| _predefinedFeatures = { |
| "enable-flash" : function (feature, widgetConfig) { |
| widgetConfig.enableFlash = true; |
| }, |
| "blackberry.app.orientation": function (feature, widgetConfig) { |
| if (feature) { |
| var params = processParamObj(feature.param), |
| mode = params.mode; |
| |
| if (!mode) { |
| //No mode provided, throw error |
| throw localize.translate("EXCEPTION_EMPTY_ORIENTATION_MODE", mode); |
| } else if (mode === "landscape" || mode === "portrait" || mode === "north") { |
| widgetConfig.autoOrientation = false;//Overwrites default value |
| widgetConfig.orientation = mode; |
| } else if (mode !== "auto") { |
| //Mode invalid, throw error |
| throw localize.translate("EXCEPTION_INVALID_ORIENTATION_MODE", mode); |
| } |
| |
| // Throw a warning since this feature is deprecated |
| logger.warn(localize.translate("WARNING_ORIENTATION_DEPRECATED")); |
| } |
| } |
| }; |
| } |
| |
| _self = { |
| parse: function (xmlPath, session, callback) { |
| if (!fs.existsSync(xmlPath)) { |
| throw localize.translate("EXCEPTION_CONFIG_NOT_FOUND"); |
| } |
| |
| var fileData = fs.readFileSync(xmlPath), |
| xml = utils.bufferToString(fileData), |
| parser = new xml2js.Parser({trim: true, normalize: true, explicitRoot: false}); |
| |
| //Used for config-file injections |
| _config_doc = new et.ElementTree(et.XML(xml)); |
| |
| init(); |
| |
| //parse xml file data |
| parser.parseString(xml, function (err, result) { |
| if (err) { |
| logger.error(localize.translate("EXCEPTION_PARSING_XML")); |
| fileManager.cleanSource(session); |
| } else { |
| callback(processResult(result, session)); |
| } |
| }); |
| } |
| }; |
| |
| module.exports = _self; |