diff --git a/lib/PlatformConfigParser.js b/lib/PlatformConfigParser.js new file mode 100644 index 000000000..675287c62 --- /dev/null +++ b/lib/PlatformConfigParser.js @@ -0,0 +1,32 @@ +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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. +*/ + +const ConfigParser = require('cordova-common').ConfigParser; + +class PlatformConfigParser extends ConfigParser { + /** + * Returns the privacy manifest node, if available. + * Otherwise `null` is returned. + */ + getPrivacyManifest () { + return this.doc.find('./platform[@name="ios"]/privacy-manifest'); + } +} + +module.exports = PlatformConfigParser; diff --git a/lib/prepare.js b/lib/prepare.js index 38239e4c8..cfaab02c2 100644 --- a/lib/prepare.js +++ b/lib/prepare.js @@ -17,12 +17,11 @@ under the License. */ -'use strict'; - const fs = require('fs-extra'); const path = require('path'); const unorm = require('unorm'); const plist = require('plist'); +const et = require('elementtree'); const URL = require('url'); const events = require('cordova-common').events; const xmlHelpers = require('cordova-common').xmlHelpers; @@ -35,6 +34,7 @@ const FileUpdater = require('cordova-common').FileUpdater; const projectFile = require('./projectFile'); const Podfile = require('./Podfile').Podfile; const check_reqs = require('./check_reqs'); +const PlatformConfigParser = require('./PlatformConfigParser'); // launch storyboard and related constants const IMAGESET_COMPACT_SIZE_CLASS = 'compact'; @@ -43,9 +43,16 @@ const CDV_ANY_SIZE_CLASS = 'any'; module.exports.prepare = function (cordovaProject, options) { const platformJson = PlatformJson.load(this.locations.root, 'ios'); const munger = new PlatformMunger('ios', this.locations.root, platformJson, new PluginInfoProvider()); - this._config = updateConfigFile(cordovaProject.projectConfig, munger, this.locations); + const parser = new PlatformConfigParser(cordovaProject.projectConfig.path); + try { + const manifest = parser.getPrivacyManifest(); + overwritePrivacyManifest(manifest, this.locations); + } catch (err) { + return Promise.reject(new CordovaError(`Could not parse PrivacyManifest in config.xml: ${err}`)); + } + // Update own www dir with project's www assets and plugins' assets and js-files return updateWww(cordovaProject, this.locations) // update project according to config.xml changes. @@ -87,6 +94,33 @@ module.exports.clean = function (options) { }); }; +/** + * Overwrites the privacy manifest file with the provided manifest or sets the default manifest. + * @param {ElementTree} manifest - The manifest to be written to the privacy manifest file. + * @param {Object} locations - The locations object containing the path to the Xcode Cordova project. + */ +function overwritePrivacyManifest (manifest, locations) { + const privacyManifestDest = path.join(locations.xcodeCordovaProj, 'PrivacyInfo.xcprivacy'); + if (manifest != null) { + const XML_DECLARATION = '\n'; + const DOCTYPE = '\n'; + const plistElement = et.Element('plist'); + plistElement.set('version', '1.0'); + const dictElement = et.SubElement(plistElement, 'dict'); + manifest.getchildren().forEach((child) => { + dictElement.append(child); + }); + const etree = new et.ElementTree(plistElement); + const xmlString = XML_DECLARATION + DOCTYPE + etree.write({ xml_declaration: false }); + fs.writeFileSync(privacyManifestDest, xmlString, 'utf-8'); + return; + } + // Set default privacy manifest + const defaultPrivacyManifest = path.join(__dirname, '..', 'templates', 'project', '__PROJECT_NAME__', 'PrivacyInfo.xcprivacy'); + const xmlString = fs.readFileSync(defaultPrivacyManifest, 'utf8'); + fs.writeFileSync(privacyManifestDest, xmlString, 'utf-8'); +} + /** * Updates config files in project based on app's config.xml and config munge, * generated by plugins. diff --git a/package-lock.json b/package-lock.json index 744d6aa17..862e21156 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "cordova-common": "^5.0.0", + "elementtree": "^0.1.7", "execa": "^5.1.1", "fs-extra": "^11.1.1", "ios-sim": "^8.0.2", diff --git a/package.json b/package.json index 6082340a1..2a305d22b 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,8 @@ "unorm": "^1.6.0", "which": "^3.0.1", "xcode": "^3.0.1", - "xml-escape": "^1.1.0" + "xml-escape": "^1.1.0", + "elementtree": "^0.1.7" }, "nyc": { "include": [ diff --git a/tests/spec/unit/fixtures/prepare/no-privacy-manifest.xml b/tests/spec/unit/fixtures/prepare/no-privacy-manifest.xml new file mode 100644 index 000000000..98afac213 --- /dev/null +++ b/tests/spec/unit/fixtures/prepare/no-privacy-manifest.xml @@ -0,0 +1,23 @@ + + + + + SampleApp + diff --git a/tests/spec/unit/fixtures/prepare/privacy-manifest.xml b/tests/spec/unit/fixtures/prepare/privacy-manifest.xml new file mode 100644 index 000000000..510464a50 --- /dev/null +++ b/tests/spec/unit/fixtures/prepare/privacy-manifest.xml @@ -0,0 +1,56 @@ + + + + + SampleApp + + + NSPrivacyTracking + + NSPrivacyAccessedAPITypes + + NSPrivacyTrackingDomains + + NSPrivacyCollectedDataTypes + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeDeviceID + + + NSPrivacyCollectedDataTypeLinked + + + + NSPrivacyCollectedDataTypeTracking + + + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + + + diff --git a/tests/spec/unit/prepare.spec.js b/tests/spec/unit/prepare.spec.js index b51a9f6da..2ec83531f 100644 --- a/tests/spec/unit/prepare.spec.js +++ b/tests/spec/unit/prepare.spec.js @@ -1504,6 +1504,44 @@ describe('prepare', () => { expect(plist.build.calls.mostRecent().args[0].CFBundleDisplayName).toEqual('MyApp'); }); }); + it('Test#021 : - should write out the privacy manifest ', () => { + plist.parse.and.callThrough(); + writeFileSyncSpy.and.callThrough(); + const projectRoot = iosProject; + const platformProjDir = path.join(projectRoot, 'platforms', 'ios', 'SampleApp'); + const PlatformConfigParser = require('../../../lib/PlatformConfigParser'); + const my_config = new PlatformConfigParser(path.join(FIXTURES, 'prepare', 'privacy-manifest.xml')); + const privacyManifest = my_config.getPrivacyManifest(); + const overwritePrivacyManifest = prepare.__get__('overwritePrivacyManifest'); + overwritePrivacyManifest(privacyManifest, p.locations); + const privacyManifestPathDest = path.join(platformProjDir, 'PrivacyInfo.xcprivacy'); + expect(writeFileSyncSpy).toHaveBeenCalledWith(privacyManifestPathDest, jasmine.any(String), 'utf-8'); + const xml = writeFileSyncSpy.calls.all()[0].args[1]; + const json = plist.parse(xml); + expect(json.NSPrivacyTracking).toBeTrue(); + expect(json.NSPrivacyAccessedAPITypes.length).toBe(0); + expect(json.NSPrivacyTrackingDomains.length).toBe(0); + expect(json.NSPrivacyCollectedDataTypes.length).toBe(1); + }); + it('Test#022 : no - should write out the privacy manifest ', () => { + plist.parse.and.callThrough(); + writeFileSyncSpy.and.callThrough(); + const projectRoot = iosProject; + const platformProjDir = path.join(projectRoot, 'platforms', 'ios', 'SampleApp'); + const PlatformConfigParser = require('../../../lib/PlatformConfigParser'); + const my_config = new PlatformConfigParser(path.join(FIXTURES, 'prepare', 'no-privacy-manifest.xml')); + const privacyManifest = my_config.getPrivacyManifest(); + const overwritePrivacyManifest = prepare.__get__('overwritePrivacyManifest'); + overwritePrivacyManifest(privacyManifest, p.locations); + const privacyManifestPathDest = path.join(platformProjDir, 'PrivacyInfo.xcprivacy'); + expect(writeFileSyncSpy).toHaveBeenCalledWith(privacyManifestPathDest, jasmine.any(String), 'utf-8'); + const xml = writeFileSyncSpy.calls.all()[0].args[1]; + const json = plist.parse(xml); + expect(json.NSPrivacyTracking).toBeFalse(); + expect(json.NSPrivacyAccessedAPITypes.length).toBe(0); + expect(json.NSPrivacyTrackingDomains.length).toBe(0); + expect(json.NSPrivacyCollectedDataTypes.length).toBe(0); + }); }); describe(' tests', () => {