def test_sanitize_html(self): with open(os.path.join(os.path.dirname(__file__), 'contenttypes-notes-tosanitize.plist'), 'rb') as f: strings = load_plist(f) with open(os.path.join(os.path.dirname(__file__), 'contenttypes-notes-sanitized.txt')) as f: sanitized = f.readlines() for s in zip(strings, sanitized): _check_sanitized(s[0], s[1])
def check_app_updates(app_info_list, raw_result=False): # This function expects a list of dicts with, at a minimum, the 'adam-id' key set # This ID is the unique product identifier for an app on the App Store and can be found in the store URL # when viewing the app's page in a web browser, like so: # https://itunes.apple.com/us/app/evernote/id406056744 # ^^^^^^^^^ # The other 3 keys that will be passed in the search are: CFBundleIdentifier, CFBundleShortVersionString, and # installed-version-identifier (explained earlier). Lack of any of these keys will result in a filler value # being provided which, as long as the adam-id is present, the App Store update mechanism doesn't seem to # care about. update_url = 'https://su.itunes.apple.com/WebObjects/MZSoftwareUpdate.woa/wa/availableSoftwareUpdatesExtended' request = urllib.request.Request(update_url) # Headers # # This sets us to the US store. See: # http://blogs.oreilly.com/iphone/2008/08/scraping-appstore-reviews.html # http://secrets.blacktree.com/edit?id=129761 request.add_header('X-Apple-Store-Front', '143441-1,13') # This indicates we're sending an XML plist request.add_header('Content-Type', 'application/x-apple-plist') # This seems to be the minimum string to be recognized as a valid app store update checker # Normally, it's of the form: User-Agent: MacAppStore/1.3 (Macintosh; OS X 10.9) AppleWebKit/537.71 request.add_header('User-Agent', 'MacAppStore') # Build up the plist local_software = [] for an_app in app_info_list: app_entry = { 'CFBundleIdentifier': an_app.get('CFBundleIdentifier', '.'), 'CFBundleShortVersionString': an_app.get('CFBundleShortVersionString', '0'), 'adam-id': an_app['adam-id'], 'installed-version-identifier': an_app.get('installed-version-identifier', 0) } local_software.append(app_entry) plist_dict = {'local-software': local_software} plist_str = dump_plist(plist_dict) request.add_data(plist_str) # Build the connection response_handle = urllib.request.urlopen(request) try: response = response_handle.read() except HTTPError as e: raise ProcessorError("Invalid adam-id %s" % e) response_handle.close() # Currently returning the raw response # Initial analysis: # - It appears that applications that need updating will be under the 'incompatible-items' key # - 'version-external-identifiers' is a list of historical versions, in order # - 'version-external-identifier' is the current version # - 'current-version' is the CFBundleShortVersionString # - 'bundle-id' is the CFBundleIdentifier # - 'preflight' is a *very* interesting 'pfpkg'. See details at bottom. return load_plist(response)
def plist_yaml(in_path, out_path): """Convert plist to yaml.""" with open(in_path, "rb") as in_file: input_data = load_plist(in_file) normalized = normalize_types(input_data) if sys.version_info.major == 3 and in_path.endswith( (".recipe", ".recipe.plist")): normalized = sort_autopkg_processes(normalized) output = convert(normalized) out_file = open(out_path, "w") out_file.writelines(output)
def plist_yaml(in_path, out_path): """Convert plist to yaml.""" with open(in_path, "rb") as in_file: input_data = load_plist(in_file) normalized = normalize_types(input_data) # handle conversion of AutoPkg recipes if sys.version_info.major == 3 and in_path.endswith( (".recipe", ".recipe.plist")): normalized = handle_autopkg_recipes.optimise_autopkg_recipes( normalized) output = convert(normalized) output = handle_autopkg_recipes.format_autopkg_recipes(output) else: output = convert(normalized) out_file = open(out_path, "w") out_file.writelines(output) print("Wrote to : {}\n".format(out_path))
def get_url(self, os_ver): plist_text = self.download(CHECK_URL) plist_filename = os.path.join(self.env['RECIPE_CACHE_DIR'], PLIST_FN) try: plist = load_plist(plist_text) except: raise ProcessorError('Could not read NVIDIA plist file %s' % plist_filename) # the Version is blank here due to the plist NSPredicate # testing it to not be the current version. pred_obj = {'Ticket': {'Version': ''}, 'SystemVersion': {'ProductVersion': os_ver}} url = None version = None for rule in plist['Rules']: if self.evaluate_predicate(pred_obj, rule['Predicate']): self.output('Satisfied predicate for OS version %s' % os_ver) url = rule['Codebase'] version = rule['kServerVersion'] # with a satisfied predicate, evaluate lower OS versions # so as to determine a minimum OS constraint, decrementing # one 10.x version at a time while self.evaluate_predicate(pred_obj, rule['Predicate']): # record the currently-evaluated OS version as the minimum required minimum_os_ver = os_ver osx, major = os_ver.split('.') os_ver = osx + '.' + str(int(major) - 1) pred_obj['SystemVersion']['ProductVersion'] = os_ver self.output('Evaluating predicate for lower OS version %s' % os_ver) self.output('OS version %s too low!' % os_ver) # highest required versions seem to be always first, so we break # after we've satisfied one Predicate break if not url: raise ProcessorError('No valid Predicate rules found!') return (url, version, minimum_os_ver)
def process_hd_installer(self): ''' Process HD installer - app_json: Path to the Application JSON from within the PKG ''' #pylint: disable=too-many-locals, too-many-statements self.output('Processing HD installer') with open(self.env['app_json']) as json_file: load_json = json.load(json_file) # AppLaunch is not always in the same format, but is splittable if 'AppLaunch' in load_json: # Bridge CC is HD but does not have AppLaunch app_launch = load_json['AppLaunch'] self.output('app_launch: %s' % app_launch) app_details = list(re.split('/', app_launch)) if app_details[2].endswith('.app'): app_bundle = app_details[2] app_path = app_details[1] else: app_bundle = app_details[1] app_path = list( re.split('/', (load_json['InstallDir']['value'])))[1] self.output('app_bundle: %s' % app_bundle) self.output('app_path: %s' % app_path) installed_path = os.path.join('/Applications', app_path, app_bundle) self.output('installed_path: %s' % installed_path) if not app_path.endswith('CC') and not app_path.endswith( '2021'): self.env['display_name'] = app_path + ' 2021' elif app_path.endswith('CC') and not app_path.endswith('2021'): self.env['display_name'] = app_path + ' 2021' else: self.env['display_name'] = app_path self.output('display_name: %s' % self.env['display_name']) zip_file = load_json['Packages']['Package'][0]['PackageName'] pimx_dir = '1' if zip_file.endswith('-LearnPanel'): zip_file = load_json['Packages']['Package'][1][ 'PackageName'] pimx_dir = '2' self.output('zip_file: %s' % zip_file) zip_path = os.path.join(self.env['PKG'], 'Contents/Resources/HD', \ self.env['target_folder'], zip_file + '.zip') self.output('zip_path: %s' % zip_path) with zipfile.ZipFile(zip_path, mode='r') as myzip: with myzip.open(zip_file + '.pimx') as mytxt: txt = mytxt.read() tree = ElementTree.fromstring(txt) # Loop through .pmx's Assets, look for target=[INSTALLDIR], # then grab Assets Source. # Break when found .app/Contents/Info.plist for elem in tree.findall('Assets'): for i in elem.getchildren(): # Below special tweak for the non-Classic Lightroom bundle if i.attrib['target'].upper().startswith('[INSTALLDIR]') and \ not i.attrib['target'].endswith('Icons'): bundle_location = i.attrib['source'] self.output('bundle_location: %s' % bundle_location) else: continue if not bundle_location.startswith( '[StagingFolder]'): continue elif bundle_location.endswith('Icons') or \ bundle_location.endswith('AMT'): continue else: bundle_location = bundle_location[16:] if bundle_location.endswith('.app'): zip_bundle = os.path.join(pimx_dir, bundle_location, \ 'Contents/Info.plist') else: zip_bundle = os.path.join(pimx_dir, bundle_location, \ app_bundle, 'Contents/Info.plist') try: with myzip.open(zip_bundle) as myplist: plist = myplist.read() data = load_plist(plist) # If the App is Lightroom (Classic or non-Classic) # we need to compare a different value in Info.plist if self.env['sap_code'] == 'LTRM' or \ self.env['sap_code'] == 'LRCC': self.env[ 'vers_compare_key'] = 'CFBundleVersion' else: self.env['vers_compare_key'] = \ 'CFBundleShortVersionString' self.output('vers_compare_key: %s' % \ self.env['vers_compare_key']) app_version = data[ self.env['vers_compare_key']] self.output('staging_folder: %s' % bundle_location) self.output( 'staging_folder_path: %s' % zip_bundle) self.output('app_version: %s' % app_version) self.output('app_bundle: %s' % app_bundle) break except zipfile.BadZipfile: continue # Now we have the deets, let's use them self.create_pkginfo(app_bundle, app_version, installed_path)
def read_info_plist(self, my_zip, pimx_dir, xml_tree, zip_path): ''' Try to read info.plist from within zip_bundle ''' # Loop through .pmx's Assets, look for target=[INSTALLDIR], # then grab Assets Source. # Break when found .app/Contents/Info.plist for xml_elem in xml_tree.findall('Assets'): for xml_item in xml_elem.getchildren(): # Below special tweak for the non-Classic Lightroom bundle if (xml_item.attrib['target'].upper().startswith( '[INSTALLDIR]') and not xml_item.attrib['target'].endswith('Icons')): # Get bundle_location bundle_location = xml_item.attrib['source'] self.output("bundle_location: {}".format(bundle_location)) else: continue # Amend bundle_location as needed if not bundle_location.startswith('[StagingFolder]'): continue if bundle_location.endswith('Icons') or \ bundle_location.endswith('AMT'): continue bundle_location = bundle_location[16:] # Create zip_bundle if bundle_location.endswith('.app'): zip_bundle = (os.path.join(pimx_dir, bundle_location, 'Contents/Info.plist')) else: zip_bundle = (os.path.join(pimx_dir, bundle_location, self.env['app_bundle'], 'Contents/Info.plist')) # Try to read info.plist from within zip_bundle try: with my_zip.open(zip_bundle) as my_plist: info_plist = my_plist.read() data = load_plist(info_plist) # If the App is Lightroom (Classic or non-Classic) # we need to compare a different value in Info.plist if self.env['sap_code'] == 'LTRM' or \ self.env['sap_code'] == 'LRCC': self.env['vers_compare_key'] = 'CFBundleVersion' else: self.env['vers_compare_key'] = ( 'CFBundleShortVersionString') # Get version from info.plist app_version = data[self.env['vers_compare_key']] # Get bundleid from info.plist self.env['app_bundle_id'] = data['CFBundleIdentifier'] # Progress notifications self.output("vers_compare_key: {}".format( self.env['vers_compare_key'])) self.output("app_bundle_id: {}".format( self.env['app_bundle_id'])) self.output( "staging_folder: {}".format(bundle_location)) self.output( "staging_folder_path: {}".format(zip_bundle)) self.env['app_version'] = app_version self.output("app_version: {}".format( self.env['app_version'])) break # If we cannot read the zip file except zipfile.BadZipfile as err_msg: raise ProcessorError("Failed to open {}: {}".format( zip_path, err_msg))
default='1.0') parser.add_argument( '--icon', help= 'Specifies an existing icon in the Munki repo to display for the printer in Managed Software Center. Optional.' ) parser.add_argument( '--csv', help= 'Path to CSV file containing printer info. If CSV is provided, all other options are ignored.' ) args = parser.parse_args() pwd = os.path.dirname(os.path.realpath(__file__)) f = open(os.path.join(pwd, 'Printer-Template.plist'), 'rb') templatePlist = load_plist(f) f.close() if args.csv: # A CSV was found, use that for all data. with open(args.csv, mode='r') as infile: reader = csv.reader(infile) next(reader, None) # skip the header row for row in reader: newPlist = dict(templatePlist) # each row contains 10 elements: # Printer name, location, display name, address, driver, description, options, version, requires, icon # options in the form of "Option=Value Option2=Value Option3=Value" # requires in the form of "package1 package2" Note: the space seperator theOptionString = '' if row[6] != "":
def get_installer_info(self): """Gets info about an installer from MS metadata.""" # Get the channel UUID, matching against a custom UUID if one is given channel_input = self.env.get("channel", DEFAULT_CHANNEL) rex = r"^([0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12})$" match_uuid = re.match(rex, channel_input) if not match_uuid and channel_input not in list(CHANNELS.keys()): raise ProcessorError( "'channel' input variable must be one of: %s or a custom " "uuid" % (", ".join(list(CHANNELS.keys())))) if match_uuid: channel = match_uuid.groups()[0] else: channel = CHANNELS[channel_input] base_url = BASE_URL % (channel, CULTURE_CODE + PROD_DICT[self.env["product"]]['id']) # Get metadata URL self.output("Requesting xml: %s" % base_url) req = urllib.request.Request(base_url) # Add the MAU User-Agent, since MAU feed server seems to explicitly # block a User-Agent of 'Python-urllib/2.7' - even a blank User-Agent # string passes. req.add_header( "User-Agent", "Microsoft%20AutoUpdate/3.6.16080300 CFNetwork/760.6.3 Darwin/15.6.0 (x86_64)" ) ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE try: fdesc = urllib.request.urlopen(req, context=ctx) data = fdesc.read() fdesc.close() except BaseException as err: raise ProcessorError("Can't download %s: %s" % (base_url, err)) metadata = load_plist(data) item = {} # Update feeds for a given 'channel' will have either combo or delta # pkg urls, with delta's additionally having a 'FullUpdaterLocation' key. # We populate the item dict with the appropriate section of the metadata # output, and do string replacement on the pattern of the URL in the # case of a Standalone app request. if self.env["version"] == "latest" or self.env[ "version"] == "latest-standalone": item = [u for u in metadata if not u.get("FullUpdaterLocation")] elif self.env["version"] == "latest-delta": item = [u for u in metadata if u.get("FullUpdaterLocation")] if not item: raise ProcessorError("Could not find an applicable update in " "update metadata.") item = item[0] if self.env["version"] == "latest-standalone": p = re.compile(r'(^[a-zA-Z0-9:/.-]*_[a-zA-Z]*_)(.*)Updater.pkg') url = item["Location"] (firstGroup, secondGroup) = re.search(p, url).group(1, 2) item[ "Location"] = firstGroup + "2019_" + secondGroup + "Installer.pkg" self.env["url"] = item["Location"] self.output("Found URL %s" % self.env["url"]) self.output("Got update: '%s'" % item["Title"]) # now extract useful info from the rest of the metadata that could # be used in a pkginfo pkginfo = {} # Get a copy of the description in our locale_id all_localizations = item.get("Localized") lcid = self.env["locale_id"] if lcid not in all_localizations: raise ProcessorError( "Locale ID %s not found in manifest metadata. Available IDs: " "%s. See %s for more details." % (lcid, ", ".join( list(all_localizations.keys())), LOCALE_ID_INFO_URL)) manifest_description = all_localizations[lcid]['Short Description'] # Store the description in a separate output variable and in our pkginfo # directly. pkginfo["description"] = "<html>%s</html>" % manifest_description self.env["description"] = manifest_description # Minimum OS version key should exist always, but default to the current # minimum as of 16/11/03 pkginfo["minimum_os_version"] = item.get('Minimum OS', '10.10.5') installs_items = self.get_installs_items(item) if installs_items: pkginfo["installs"] = installs_items # Extra work to do if this is a delta updater if self.env["version"] == "latest-delta": try: rel_versions = item["Triggers"]["Registered File"][ "VersionsRelative"] except KeyError: raise ProcessorError("Can't find expected VersionsRelative" "keys for determining minimum update " "required for delta update.") for expression in rel_versions: operator, ver_eval = expression.split() if operator == ">=": self.min_delta_version = ver_eval break if not self.min_delta_version: raise ProcessorError("Not able to determine minimum required " "version for delta update.") # Put minimum_update_version into installs item self.output("Adding minimum required version: %s" % self.min_delta_version) pkginfo["installs"][0]["minimum_update_version"] = \ self.min_delta_version required_update_name = self.env["NAME"] if self.env["munki_required_update_name"]: required_update_name = self.env["munki_required_update_name"] # Add 'requires' array pkginfo["requires"] = [ "%s-%s" % (required_update_name, self.min_delta_version) ] self.env["version"] = self.get_version(item) self.env["minimum_os_version"] = pkginfo["minimum_os_version"] self.env["minimum_version_for_delta"] = self.min_delta_version self.env["additional_pkginfo"] = pkginfo self.env["url"] = item["Location"] self.output("Additional pkginfo: %s" % self.env["additional_pkginfo"])