Permalink
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up
Fetching contributors…
Cannot retrieve contributors at this time.
Cannot retrieve contributors at this time
| # encoding: utf-8 | |
| # | |
| # MunkiItems.py | |
| # Managed Software Center | |
| # | |
| # Created by Greg Neagle on 2/21/14. | |
| # | |
| # Copyright 2014 Greg Neagle. | |
| # | |
| # 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. | |
| import os | |
| import sys | |
| import msclib | |
| import munki | |
| from operator import itemgetter | |
| from HTMLParser import HTMLParser, HTMLParseError | |
| from Foundation import * | |
| from AppKit import * | |
| import FoundationPlist | |
| user_install_selections = set() | |
| user_removal_selections = set() | |
| # place to cache our expensive-to-calculate data | |
| _cache = {} | |
| def quote(a_string): | |
| '''Replacement for urllib.quote that handles Unicode strings''' | |
| return str(NSString.stringWithString_( | |
| a_string).stringByAddingPercentEscapesUsingEncoding_( | |
| NSUTF8StringEncoding)) | |
| def reset(): | |
| '''clear all our cached values''' | |
| global _cache | |
| _cache = {} | |
| def getAppleUpdates(): | |
| if not 'apple_updates' in _cache: | |
| _cache['apple_updates'] = munki.getAppleUpdates() | |
| return _cache['apple_updates'] | |
| def getInstallInfo(): | |
| if not 'install_info' in _cache: | |
| _cache['install_info'] = munki.getInstallInfo() | |
| return _cache['install_info'] | |
| def getOptionalInstallItems(): | |
| if munki.pref('AppleSoftwareUpdatesOnly'): | |
| return [] | |
| if not 'optional_install_items' in _cache: | |
| _cache['optional_install_items'] = [OptionalItem(item) | |
| for item in getInstallInfo().get('optional_installs', [])] | |
| return _cache['optional_install_items'] | |
| def updateCheckNeeded(): | |
| '''Returns True if any item in optional installs list has 'updatecheck_needed' == True''' | |
| return len([item for item in getOptionalInstallItems() | |
| if item.get('updatecheck_needed')]) != 0 | |
| def optionalItemForName_(item_name): | |
| for item in getOptionalInstallItems(): | |
| if item['name'] == item_name: | |
| return item | |
| return None | |
| def getOptionalWillBeInstalledItems(): | |
| return [item for item in getOptionalInstallItems() | |
| if item['status'] in ['install-requested', 'will-be-installed', | |
| 'update-will-be-installed', 'install-error']] | |
| def getOptionalWillBeRemovedItems(): | |
| return [item for item in getOptionalInstallItems() | |
| if item['status'] in ['removal-requested', 'will-be-removed', 'removal-error']] | |
| def getUpdateList(): | |
| if not 'update_list'in _cache: | |
| _cache['update_list'] = _build_update_list() | |
| return _cache['update_list'] | |
| def display_name(item_name): | |
| '''Returns a display_name for item_name, or item_name if not found''' | |
| for item in getOptionalInstallItems(): | |
| if item['name'] == item_name: | |
| return item['display_name'] | |
| return item_name | |
| def _build_update_list(): | |
| update_items = [] | |
| if not munki.munkiUpdatesContainAppleItems(): | |
| apple_updates = getAppleUpdates() | |
| apple_update_items = apple_updates.get('AppleUpdates', []) | |
| for item in apple_update_items: | |
| item['developer'] = u'Apple' | |
| item['status'] = u'will-be-installed' | |
| update_items.extend(apple_update_items) | |
| install_info = getInstallInfo() | |
| managed_installs = install_info.get('managed_installs', []) | |
| for item in managed_installs: | |
| item['status'] = u'will-be-installed' | |
| update_items.extend(managed_installs) | |
| removal_items = install_info.get('removals', []) | |
| for item in removal_items: | |
| item['status'] = u'will-be-removed' | |
| # TO-DO: handle the case where removal detail is suppressed | |
| update_items.extend(removal_items) | |
| # use our list to make UpdateItems | |
| update_list = [UpdateItem(item) for item in update_items] | |
| # sort it and return it | |
| return sorted(update_list, key=itemgetter( | |
| 'due_date_sort', 'restart_sort', 'developer_sort', 'size_sort')) | |
| def updatesRequireLogout(): | |
| '''Return True if any item in the update list requires a logout or if | |
| Munki's InstallRequiresLogout preference is true.''' | |
| if munki.installRequiresLogout(): | |
| return True | |
| return len([item for item in getUpdateList() | |
| if 'Logout' in item.get('RestartAction', '')]) > 0 | |
| def updatesRequireRestart(): | |
| '''Return True if any item in the update list requires a restart''' | |
| return len([item for item in getUpdateList() | |
| if 'Restart' in item.get('RestartAction', '')]) > 0 | |
| def updatesContainNonUserSelectedItems(): | |
| '''Does the list of updates contain items not selected by the user?''' | |
| if not munki.munkiUpdatesContainAppleItems() and getAppleUpdates(): | |
| # available Apple updates are not user selected | |
| return True | |
| install_info = getInstallInfo() | |
| install_items = install_info.get('managed_installs', []) | |
| removal_items = install_info.get('removals', []) | |
| filtered_installs = [item for item in install_items | |
| if item['name'] not in user_install_selections] | |
| if filtered_installs: | |
| return True | |
| filtered_uninstalls = [item for item in removal_items | |
| if item['name'] not in user_removal_selections] | |
| if filtered_uninstalls: | |
| return True | |
| return False | |
| def getEffectiveUpdateList(): | |
| '''Combine the updates Munki has found with any optional choices to | |
| make the effective list of updates''' | |
| managed_update_names = getInstallInfo().get('managed_updates', []) | |
| self_service_installs = SelfService().installs() | |
| self_service_uninstalls = SelfService().uninstalls() | |
| # items in the update_list that are part of optional_items | |
| # could have their installation state changed; so filter those out | |
| optional_installs = getOptionalWillBeInstalledItems() | |
| optional_removals = getOptionalWillBeRemovedItems() | |
| optional_item_names = [item['name'] for item in optional_installs + optional_removals] | |
| mandatory_updates = [item for item in getUpdateList() | |
| if item['name'] not in optional_item_names] | |
| return mandatory_updates + optional_installs + optional_removals | |
| def getMyItemsList(): | |
| '''Returns a list of optional_installs items the user has chosen | |
| to install or to remove''' | |
| self_service_installs = SelfService().installs() | |
| self_service_uninstalls = SelfService().uninstalls() | |
| item_list = [item for item in getOptionalInstallItems() | |
| if item['name'] in self_service_installs] | |
| items_to_remove = [item for item in getOptionalInstallItems() | |
| if item['name'] in self_service_uninstalls | |
| and item.get('installed')] | |
| item_list.extend(items_to_remove) | |
| return item_list | |
| def dependentItems(this_name): | |
| '''Returns the names of any selected optional items that require this optional item''' | |
| if not 'optional_installs_with_dependencies' in _cache: | |
| self_service_installs = SelfService().installs() | |
| optional_installs = getInstallInfo().get('optional_installs', []) | |
| _cache['optional_installs_with_dependencies'] = [item for item in optional_installs | |
| if item['name'] in self_service_installs | |
| and 'requires' in item] | |
| dependent_items = [] | |
| for item in _cache['optional_installs_with_dependencies']: | |
| if this_name in item['requires']: | |
| dependent_items.append(item['name']) | |
| return dependent_items | |
| def convertIconToPNG(app_name, destination_path, desired_size): | |
| '''Converts an application icns file to a png file, choosing the representation | |
| closest to (but >= than if possible) the desired_size. Returns True if | |
| successful, False otherwise''' | |
| app_path = os.path.join('/Applications', app_name + '.app') | |
| if not os.path.exists(app_path): | |
| return False | |
| try: | |
| info = FoundationPlist.readPlist(os.path.join(app_path, 'Contents/Info.plist')) | |
| except (FoundationPlist.FoundationPlistException): | |
| info = {} | |
| icon_filename = info.get('CFBundleIconFile', app_name) | |
| icon_path = os.path.join(app_path, 'Contents/Resources', icon_filename) | |
| if not os.path.splitext(icon_path)[1]: | |
| # no file extension, so add '.icns' | |
| icon_path += u'.icns' | |
| if os.path.exists(icon_path): | |
| image_data = NSData.dataWithContentsOfFile_(icon_path) | |
| bitmap_reps = NSBitmapImageRep.imageRepsWithData_(image_data) | |
| chosen_rep = None | |
| for bitmap_rep in bitmap_reps: | |
| if not chosen_rep: | |
| chosen_rep = bitmap_rep | |
| elif (bitmap_rep.pixelsHigh() >= desired_size | |
| and bitmap_rep.pixelsHigh() < chosen_rep.pixelsHigh()): | |
| chosen_rep = bitmap_rep | |
| if chosen_rep: | |
| png_data = chosen_rep.representationUsingType_properties_(NSPNGFileType, None) | |
| png_data.writeToFile_atomically_(destination_path, False) | |
| return True | |
| return False | |
| class MSCHTMLFilter(HTMLParser): | |
| '''Filters HTML and HTML fragments for use inside description paragraphs''' | |
| def __init__(self): | |
| HTMLParser.__init__(self) | |
| # ignore everything inside one of these tags | |
| self.ignore_elements = ['script', 'style', 'head', 'table', 'form'] | |
| # preserve these tags | |
| self.preserve_tags = ['a', 'b', 'i', 'strong', 'em', 'small', 'sub', 'sup', 'ins', | |
| 'del', 'mark', 'span', 'br', 'img'] | |
| # transform these tags | |
| self.transform_starttags = { 'ul': '<br>', | |
| 'ol': '<br>', | |
| 'li': ' • ', | |
| 'h1': '<strong>', | |
| 'h2': '<strong>', | |
| 'h3': '<strong>', | |
| 'h4': '<strong>', | |
| 'h5': '<strong>', | |
| 'h6': '<strong>', | |
| 'p': ''} | |
| self.transform_endtags = { 'ul': '<br>', | |
| 'ol': '<br>', | |
| 'li': '<br>', | |
| 'h1': '</strong><br>', | |
| 'h2': '</strong><br>', | |
| 'h3': '</strong><br>', | |
| 'h4': '</strong><br>', | |
| 'h5': '</strong><br>', | |
| 'h6': '</strong><br>', | |
| 'p': '<br>'} | |
| # track the currently-ignored element if any | |
| self.current_ignore_element = None | |
| # track the number of tags we found | |
| self.tag_count = 0 | |
| # track the number of HTML entities we found | |
| self.entity_count = 0 | |
| # store our filtered/transformed html fragment | |
| self.filtered_html = u'' | |
| def handle_starttag(self, tag, attrs): | |
| self.tag_count += 1 | |
| if not self.current_ignore_element: | |
| if tag in self.ignore_elements: | |
| self.current_ignore_element = tag | |
| elif tag in self.transform_starttags: | |
| self.filtered_html += self.transform_starttags[tag] | |
| elif tag in self.preserve_tags: | |
| self.filtered_html += self.get_starttag_text() | |
| def handle_endtag(self, tag): | |
| if tag == self.current_ignore_element: | |
| self.current_ignore_element = None | |
| elif not self.current_ignore_element: | |
| if tag in self.transform_endtags: | |
| self.filtered_html += self.transform_endtags[tag] | |
| elif tag in self.preserve_tags: | |
| self.filtered_html += u'</%s>' % tag | |
| def handle_data(self, data): | |
| if not self.current_ignore_element: | |
| self.filtered_html += data | |
| def handle_entityref(self, name): | |
| self.entity_count += 1 | |
| if not self.current_ignore_element: | |
| # add the entity reference as-is | |
| self.filtered_html += u'&%s;' % name | |
| def handle_charref(self, name): | |
| if not self.current_ignore_element: | |
| # just pass on unmodified | |
| self.filtered_html += name | |
| def filtered_html(text, filter_images=False): | |
| '''Returns filtered HTML for use in description paragraphs | |
| or converts plain text into basic HTML for the same use''' | |
| parser = MSCHTMLFilter() | |
| if filter_images: | |
| parser.preserve_tags.remove('img') | |
| parser.feed(text) | |
| if parser.tag_count or parser.entity_count: | |
| # found at least one HTML tag or HTML entity, so this is probably HTML | |
| return parser.filtered_html | |
| else: | |
| # might be plain text, so we should escape a few entities and | |
| # add <br> for line breaks | |
| text = text.replace('&', '&') | |
| text = text.replace('<', '<') | |
| text = text.replace('>', '>') | |
| return text.replace('\n', '<br>\n') | |
| class SelfService(object): | |
| '''An object to wrap interactions with the SelfServiceManifest''' | |
| def __init__(self): | |
| self._installs = set( | |
| munki.readSelfServiceManifest().get('managed_installs', [])) | |
| self._uninstalls = set( | |
| munki.readSelfServiceManifest().get('managed_uninstalls', [])) | |
| def __eq__(self, other): | |
| return (sorted(self._installs) == sorted(other._installs) | |
| and sorted(self._uninstalls) == sorted(other._uninstalls)) | |
| def __ne__(self, other): | |
| return (sorted(self._installs) != sorted(other._installs) | |
| or sorted(self._uninstalls) != sorted(other._uninstalls)) | |
| def installs(self): | |
| return list(self._installs) | |
| def uninstalls(self): | |
| return list(self._uninstalls) | |
| def subscribe(self, item): | |
| self._installs.add(item['name']) | |
| self._uninstalls.discard(item['name']) | |
| self._save_self_service_choices() | |
| def unsubscribe(self, item): | |
| self._installs.discard(item['name']) | |
| self._uninstalls.add(item['name']) | |
| self._save_self_service_choices() | |
| def unmanage(self, item): | |
| self._installs.discard(item['name']) | |
| self._uninstalls.discard(item['name']) | |
| self._save_self_service_choices() | |
| def _save_self_service_choices(self): | |
| current_choices = {} | |
| current_choices['managed_installs'] = list(self._installs) | |
| current_choices['managed_uninstalls'] = list(self._uninstalls) | |
| munki.writeSelfServiceManifest(current_choices) | |
| def subscribe(item): | |
| '''Add item to SelfServeManifest's managed_installs. | |
| Also track user selections.''' | |
| SelfService().subscribe(item) | |
| user_install_selections.add(item['name']) | |
| def unsubscribe(item): | |
| '''Add item to SelfServeManifest's managed_uninstalls. | |
| Also track user selections.''' | |
| SelfService().unsubscribe(item) | |
| user_removal_selections.add(item['name']) | |
| def unmanage(item): | |
| '''Remove item from SelfServeManifest. | |
| Also track user selections.''' | |
| SelfService().unmanage(item) | |
| user_install_selections.discard(item['name']) | |
| user_removal_selections.discard(item['name']) | |
| class GenericItem(dict): | |
| '''Base class for our types of Munki items''' | |
| def __init__(self, *arg, **kw): | |
| super(GenericItem, self).__init__(*arg, **kw) | |
| # now normalize values | |
| if not self.get('display_name'): | |
| self['display_name'] = self['name'] | |
| self['display_name_lower'] = self['display_name'].lower() | |
| if not self.get('developer'): | |
| self['developer'] = self.guess_developer() | |
| if self.get('description'): | |
| try: | |
| self['raw_description'] = filtered_html(self['description']) | |
| except HTMLParseError, err: | |
| self['raw_description'] = ( | |
| 'Invalid HTML in description for %s' % self['display_name']) | |
| del(self['description']) | |
| if not 'raw_description' in self: | |
| self['raw_description'] = u'' | |
| self['icon'] = self.getIcon() | |
| self['due_date_sort'] = NSDate.distantFuture() | |
| # sort items that need restart highest, then logout, then other | |
| if self.get('RestartAction') in [None, 'None']: | |
| self['restart_action_text'] = u'' | |
| self['restart_sort'] = 2 | |
| elif self['RestartAction'] in ['RequireRestart', 'RecommendRestart']: | |
| self['restart_sort'] = 0 | |
| self['restart_action_text'] = NSLocalizedString( | |
| u"Restart Required", u"Restart Required title") | |
| self['restart_action_text'] += u'<div class="restart-needed-icon"></div>' | |
| elif self['RestartAction'] in ['RequireLogout', 'RecommendLogout']: | |
| self['restart_sort'] = 1 | |
| self['restart_action_text'] = NSLocalizedString( | |
| u"Logout Required", u"Logout Required title") | |
| self['restart_action_text'] += u'<div class="logout-needed-icon"></div>' | |
| # sort bigger installs to the top | |
| if self.get('installed_size'): | |
| self['size_sort'] = -int(self['installed_size']) | |
| self['size'] = munki.humanReadable(self['installed_size']) | |
| elif self.get('installer_item_size'): | |
| self['size_sort'] = -int(self['installer_item_size']) | |
| self['size'] = munki.humanReadable(self['installer_item_size']) | |
| else: | |
| self['size_sort'] = 0 | |
| self['size'] = u'' | |
| def __getitem__(self, name): | |
| '''Allow access to instance variables and methods via dictionary syntax. | |
| This allows us to use class instances as a data source | |
| for our HTML templates (which want a dictionary-like object)''' | |
| try: | |
| return super(GenericItem, self).__getitem__(name) | |
| except KeyError, err: | |
| try: | |
| attr = getattr(self, name) | |
| except AttributeError: | |
| raise KeyError(err) | |
| if callable(attr): | |
| return attr() | |
| else: | |
| return attr | |
| def description(self): | |
| return self['raw_description'] | |
| def description_without_images(self): | |
| return filtered_html(self.description(), filter_images=True) | |
| def dependency_description(self): | |
| '''Return an html description of items this item depends on''' | |
| description = u'' | |
| prologue = NSLocalizedString( | |
| u"This item is required by:", u"Dependency List prologue text") | |
| if self.get('dependent_items'): | |
| description = u'<strong>' + prologue | |
| for item in self['dependent_items']: | |
| description += u'<br/> • ' + display_name(item) | |
| description += u'</strong><br/><br/>' | |
| return description | |
| def guess_developer(self): | |
| '''Figure out something to use for the developer name''' | |
| if self.get('apple_item'): | |
| return 'Apple' | |
| if self.get('installer_type', '').startswith('Adobe'): | |
| return 'Adobe' | |
| # now we must dig | |
| if self.get('installs'): | |
| for install_item in self['installs']: | |
| if install_item.get('CFBundleIdentifier'): | |
| parts = install_item['CFBundleIdentifier'].split('.') | |
| if (len(parts) > 1 | |
| and parts[0] in ['com', 'org', 'net', 'edu']): | |
| return parts[1].title() | |
| return '' | |
| def getIcon(self): | |
| '''Return name/relative path of image file to use for the icon''' | |
| # first look for downloaded icons | |
| icon_known_exts = ['.bmp', '.gif', '.icns', '.jpg', '.jpeg', '.png', '.psd', | |
| '.tga', '.tif', '.tiff', '.yuv'] | |
| icon_name = self.get('icon_name') or self['name'] | |
| if not os.path.splitext(icon_name)[1] in icon_known_exts: | |
| icon_name += '.png' | |
| icon_path = os.path.join(msclib.html_dir(), 'icons', icon_name) | |
| if os.path.exists(icon_path): | |
| return 'icons/' + quote(icon_name) | |
| # didn't find one in the downloaded icons | |
| # so create one if needed from a locally installed app | |
| for key in ['icon_name', 'display_name', 'name']: | |
| if key in self: | |
| name = self[key] | |
| icon_name = name | |
| if not os.path.splitext(icon_name)[1] in icon_known_exts: | |
| icon_name += '.png' | |
| icon_path = os.path.join(msclib.html_dir(), icon_name) | |
| if os.path.exists(icon_path) or convertIconToPNG(name, icon_path, 350): | |
| return quote(icon_name) | |
| else: | |
| # use the Generic package icon | |
| return 'static/Generic.png' | |
| def unavailable_reason_text(self): | |
| '''There are several reasons an item might be unavailable for install. | |
| Return the relevent reason''' | |
| if ('licensed_seats_available' in self | |
| and not self['licensed_seats_available']): | |
| return NSLocalizedString(u"No licenses available", | |
| u"No Licenses Available display text") | |
| if self.get('note') == 'Insufficient disk space to download and install.': | |
| return NSLocalizedString(u"Not enough disk space", | |
| u"Not Enough Disk Space display text") | |
| # return generic reason | |
| return NSLocalizedString(u"Not currently available", | |
| u"Not Currently Available display text") | |
| def status_text(self): | |
| '''Return localized status display text''' | |
| if self['status'] == 'unavailable': | |
| return self.unavailable_reason_text() | |
| map = { | |
| 'install-error': | |
| NSLocalizedString(u"Installation Error", | |
| u"Install Error status text"), | |
| 'removal-error': | |
| NSLocalizedString(u"Removal Error", | |
| u"Removal Error status text"), | |
| 'installed': | |
| NSLocalizedString(u"Installed", | |
| u"Installed status text"), | |
| 'installing': | |
| NSLocalizedString(u"Installing", | |
| u"Installing status text"), | |
| 'installed-not-removable': | |
| NSLocalizedString(u"Installed", | |
| u"Installed status text"), | |
| 'not-installed': | |
| NSLocalizedString(u"Not installed", | |
| u"Not Installed status text"), | |
| 'install-requested': | |
| NSLocalizedString(u"Install requested", | |
| u"Install Requested status text"), | |
| 'downloading': | |
| NSLocalizedString(u"Downloading", | |
| u"Downloading status text"), | |
| 'will-be-installed': | |
| NSLocalizedString(u"Will be installed", | |
| u"Will Be Installed status text"), | |
| 'must-be-installed': | |
| NSLocalizedString(u"Will be installed", | |
| u"Will Be Installed status text"), | |
| 'removal-requested': | |
| NSLocalizedString(u"Removal requested", | |
| u"Removal Requested status text"), | |
| 'preparing-removal': | |
| NSLocalizedString(u"Preparing removal", | |
| u"Preparing Removal status text"), | |
| 'will-be-removed': | |
| NSLocalizedString(u"Will be removed", | |
| u"Will Be Removed status text"), | |
| 'removing': | |
| NSLocalizedString(u"Removing", | |
| u"Removing status text"), | |
| 'update-will-be-installed': | |
| NSLocalizedString(u"Update will be installed", | |
| u"Update Will Be Installed status text"), | |
| 'update-must-be-installed': | |
| NSLocalizedString(u"Update will be installed", | |
| u"Update Will Be Installed status text"), | |
| 'update-available': | |
| NSLocalizedString(u"Update available", | |
| u"Update Available status text"), | |
| 'unavailable': | |
| NSLocalizedString(u"Unavailable", | |
| u"Unavailable status text"), | |
| } | |
| return map.get(self['status'], self['status']) | |
| def short_action_text(self): | |
| '''Return localized 'short' action text for button''' | |
| map = { | |
| 'install-error': | |
| NSLocalizedString(u"Cancel", | |
| u"Cancel button title/short action text"), | |
| 'removal-error': | |
| NSLocalizedString(u"Cancel", | |
| u"Cancel button title/short action text"), | |
| 'installed': | |
| NSLocalizedString(u"Remove", | |
| u"Remove action text"), | |
| 'installing': | |
| NSLocalizedString(u"Installing", | |
| u"Installing status text"), | |
| 'installed-not-removable': | |
| NSLocalizedString(u"Installed", | |
| u"Installed status text"), | |
| 'not-installed': | |
| NSLocalizedString(u"Install", | |
| u"Install action text"), | |
| 'install-requested': | |
| NSLocalizedString(u"Cancel", | |
| u"Cancel button title/short action text"), | |
| 'downloading': | |
| NSLocalizedString(u"Cancel", | |
| u"Cancel button title/short action text"), | |
| 'will-be-installed': | |
| NSLocalizedString(u"Cancel", | |
| u"Cancel button title/short action text"), | |
| 'must-be-installed': | |
| NSLocalizedString(u"Required", | |
| u"Install Required action text"), | |
| 'removal-requested': | |
| NSLocalizedString(u"Cancel", | |
| u"Cancel button title/short action text"), | |
| 'preparing-removal': | |
| NSLocalizedString(u"Cancel", | |
| u"Cancel button title/short action text"), | |
| 'will-be-removed': | |
| NSLocalizedString(u"Cancel", | |
| u"Cancel button title/short action text"), | |
| 'removing': | |
| NSLocalizedString(u"Removing", | |
| u"Removing status text"), | |
| 'update-will-be-installed': | |
| NSLocalizedString(u"Cancel", | |
| u"Cancel button title/short action text"), | |
| 'update-must-be-installed': | |
| NSLocalizedString(u"Required", | |
| u"Install Required action text"), | |
| 'update-available': | |
| NSLocalizedString(u"Update", | |
| u"Update button title/action text"), | |
| 'unavailable': | |
| NSLocalizedString(u"Unavailable", | |
| u"Unavailable status text"), | |
| } | |
| return map.get(self['status'], self['status']) | |
| def long_action_text(self): | |
| '''Return localized 'long' action text for button''' | |
| map = { | |
| 'install-error': | |
| NSLocalizedString(u"Cancel install", | |
| u"Cancel Install long action text"), | |
| 'removal-error': | |
| NSLocalizedString(u"Cancel removal", | |
| u"Cancel Removal long action text"), | |
| 'installed': | |
| NSLocalizedString(u"Remove", | |
| u"Remove action text"), | |
| 'installing': | |
| NSLocalizedString(u"Installing", | |
| u"Installing status text"), | |
| 'installed-not-removable': | |
| NSLocalizedString(u"Installed", | |
| u"Installed status text"), | |
| 'not-installed': | |
| NSLocalizedString(u"Install", | |
| u"Install action text"), | |
| 'install-requested': | |
| NSLocalizedString(u"Cancel install", | |
| u"Cancel Install long action text"), | |
| 'downloading': | |
| NSLocalizedString(u"Cancel install", | |
| u"Cancel Install long action text"), | |
| 'will-be-installed': | |
| NSLocalizedString(u"Cancel install", | |
| u"Cancel Install long action text"), | |
| 'must-be-installed': | |
| NSLocalizedString(u"Install Required", | |
| u"Install Required action text"), | |
| 'removal-requested': | |
| NSLocalizedString(u"Cancel removal", | |
| u"Cancel Removal long action text"), | |
| 'preparing-removal': | |
| NSLocalizedString(u"Cancel removal", | |
| u"Cancel Removal long action text"), | |
| 'will-be-removed': | |
| NSLocalizedString(u"Cancel removal", | |
| u"Cancel Removal long action text"), | |
| 'removing': | |
| NSLocalizedString(u"Removing", | |
| u"Removing status text"), | |
| 'update-will-be-installed': | |
| NSLocalizedString(u"Cancel update", | |
| u"Cancel Update long action text"), | |
| 'update-must-be-installed': | |
| NSLocalizedString(u"Update Required", | |
| u"Update Required long action text"), | |
| 'update-available': | |
| NSLocalizedString(u"Update", | |
| u"Update button title/action text"), | |
| 'unavailable': | |
| NSLocalizedString(u"Currently Unavailable", | |
| u"Unavailable long action text"), | |
| } | |
| return map.get(self['status'], self['status']) | |
| def myitem_action_text(self): | |
| '''Return localized 'My Items' action text for button''' | |
| map = { | |
| 'install-error': | |
| NSLocalizedString(u"Cancel install", | |
| u"Cancel Install long action text"), | |
| 'removal-error': | |
| NSLocalizedString(u"Cancel removal", | |
| u"Cancel Removal long action text"), | |
| 'installed': | |
| NSLocalizedString(u"Remove", | |
| u"Remove action text"), | |
| 'installing': | |
| NSLocalizedString(u"Installing", | |
| u"Installing status text"), | |
| 'installed-not-removable': | |
| NSLocalizedString(u"Installed", | |
| u"Installed status text"), | |
| 'removal-requested': | |
| NSLocalizedString(u"Cancel removal", | |
| u"Cancel Removal long action text"), | |
| 'preparing-removal': | |
| NSLocalizedString(u"Cancel removal", | |
| u"Cancel Removal long action text"), | |
| 'will-be-removed': | |
| NSLocalizedString(u"Cancel removal", | |
| u"Cancel Removal long action text"), | |
| 'removing': | |
| NSLocalizedString(u"Removing", | |
| u"Removing status text"), | |
| 'update-available': | |
| NSLocalizedString(u"Update", | |
| u"Update button title/action text"), | |
| 'update-will-be-installed': | |
| NSLocalizedString(u"Remove", | |
| u"Remove action text"), | |
| 'update-must-be-installed': | |
| NSLocalizedString(u"Update Required", | |
| u"Update Required long action text"), | |
| 'install-requested': | |
| NSLocalizedString(u"Cancel install", | |
| u"Cancel Install long action text"), | |
| 'downloading': | |
| NSLocalizedString(u"Cancel install", | |
| u"Cancel Install long action text"), | |
| 'will-be-installed': | |
| NSLocalizedString(u"Cancel install", | |
| u"Cancel Install long action text"), | |
| 'must-be-installed': | |
| NSLocalizedString(u"Required", | |
| u"Install Required action text"), | |
| } | |
| return map.get(self['status'], self['status']) | |
| def version_label(self): | |
| '''Text for the version label''' | |
| if self['status'] == 'will-be-removed': | |
| removal_text = NSLocalizedString( | |
| u"Will be removed", u"Will Be Removed status text") | |
| return '<span class="warning">%s</span>' % removal_text | |
| if self['status'] == 'removal-requested': | |
| removal_text = NSLocalizedString( | |
| u"Removal requested", u"Removal Requested status text") | |
| return '<span class="warning">%s</span>' % removal_text | |
| else: | |
| return NSLocalizedString(u"Version", u"Sidebar Version label") | |
| def display_version(self): | |
| '''Version number for display''' | |
| if self['status'] == 'will-be-removed': | |
| return '' | |
| else: | |
| return self.get('version_to_install', '') | |
| def developer_sort(self): | |
| '''returns sort priority based on developer and install/removal status''' | |
| if self['status'] != 'will-be-removed' and self['developer'] == 'Apple': | |
| return 0 | |
| return 1 | |
| def more_link_text(self): | |
| return NSLocalizedString(u"More", u"More link text") | |
| class OptionalItem(GenericItem): | |
| '''Dictionary subclass that models a given optional install item''' | |
| def __init__(self, *arg, **kw): | |
| '''Initialize an OptionalItem from a item dict from the | |
| InstallInfo.plist optional_installs array''' | |
| super(OptionalItem, self).__init__(*arg, **kw) | |
| if 'category' not in self: | |
| self['category'] = NSLocalizedString( | |
| u"Uncategorized", | |
| u"No Category name") | |
| if self['developer']: | |
| self['category_and_developer'] = u'%s - %s' % ( | |
| self['category'], self['developer']) | |
| else: | |
| self['category_and_developer'] = self['category'] | |
| self['dependent_items'] = dependentItems(self['name']) | |
| if self.get('installer_item_size'): | |
| self['size'] = munki.humanReadable(self['installer_item_size']) | |
| elif self.get('installed_size'): | |
| self['size'] = munki.humanReadable(self['installed_size']) | |
| else: | |
| self['size'] = u'' | |
| self['detail_link'] = u'detail-%s.html' % quote(self['name']) | |
| self['hide_cancel_button'] = u'' | |
| if not self.get('note'): | |
| self['note'] = self._get_note_from_problem_items() | |
| if not self.get('status'): | |
| self['status'] = self._get_status() | |
| def _get_status(self): | |
| '''Calculates initial status for an item and also sets a boolean | |
| if a updatecheck is needed''' | |
| managed_update_names = getInstallInfo().get('managed_updates', []) | |
| self_service_installs = SelfService().installs() | |
| self_service_uninstalls = SelfService().uninstalls() | |
| self['updatecheck_needed'] = False | |
| self['user_directed_action'] = False | |
| if self.get('installed'): | |
| if self.get('removal_error'): | |
| status = u'removal-error' | |
| elif self.get('will_be_removed'): | |
| status = u'will-be-removed' | |
| elif self['dependent_items']: | |
| status = u'installed-not-removable' | |
| elif self['name'] in self_service_uninstalls: | |
| status = u'removal-requested' | |
| self['updatecheck_needed'] = True | |
| else: # not in managed_uninstalls | |
| if not self.get('needs_update'): | |
| if self.get('uninstallable'): | |
| status = u'installed' | |
| else: # not uninstallable | |
| status = u'installed-not-removable' | |
| else: # there is an update available | |
| if self['name'] in managed_update_names: | |
| status = u'update-must-be-installed' | |
| elif self['dependent_items']: | |
| status = u'update-must-be-installed' | |
| elif self['name'] in self_service_installs: | |
| status = u'update-will-be-installed' | |
| else: # not in managed_installs | |
| status = u'update-available' | |
| else: # not installed | |
| if self.get('install_error'): | |
| status = u'install-error' | |
| elif self.get('note'): | |
| # TO-DO: handle this case better | |
| # some reason we can't install | |
| # usually not enough disk space | |
| # but can also be: | |
| # 'Integrity check failed' | |
| # 'Download failed (%s)' % errmsg | |
| # 'Can\'t install %s because: %s', manifestitemname, errmsg | |
| # 'Insufficient disk space to download and install.' | |
| # and others in the future | |
| # | |
| # for now we prevent install this way | |
| status = u'unavailable' | |
| elif ('licensed_seats_available' in self | |
| and not self['licensed_seats_available']): | |
| status = u'unavailable' | |
| elif self['dependent_items']: | |
| status = u'must-be-installed' | |
| elif self.get('will_be_installed'): | |
| status = u'will-be-installed' | |
| elif self['name'] in self_service_installs: | |
| status = u'install-requested' | |
| self['updatecheck_needed'] = True | |
| else: # not in managed_installs | |
| status = u'not-installed' | |
| return status | |
| def _get_note_from_problem_items(self): | |
| '''Checks InstallInfo's problem_items for any notes for self that might | |
| give feedback why this item can't be downloaded or installed''' | |
| problem_items = getInstallInfo().get('problem_items', []) | |
| # check problem items for any whose name matches the name of the current item | |
| matches = [item for item in problem_items if item['name'] == self['name']] | |
| if len(matches): | |
| return matches[0].get('note', '') | |
| def description(self): | |
| '''return a full description for the item, inserting dynamic data | |
| if needed''' | |
| start_text = '' | |
| if self.get('install_error'): | |
| warning_text = NSLocalizedString( | |
| u"An installation attempt failed. " | |
| "Installation will be attempted again.\n" | |
| "If this situation continues, contact your systems administrator.", | |
| u"Install Error message") | |
| start_text += '<span class="warning">%s</span><br/><br/>' % filtered_html(warning_text) | |
| if self.get('removal_error'): | |
| warning_text = NSLocalizedString( | |
| u"A removal attempt failed. " | |
| "Removal will be attempted again.\n" | |
| "If this situation continues, contact your systems administrator.", | |
| u"Removal Error message") | |
| start_text += '<span class="warning">%s</span><br/><br/>' % filtered_html(warning_text) | |
| if self.get('note'): | |
| # some other note. Probably won't be localized, but we can try | |
| warning_text = NSBundle.mainBundle().localizedStringForKey_value_table_( | |
| self['note'], self['note'], None) | |
| start_text += '<span class="warning">%s</span><br/><br/>' % filtered_html(warning_text) | |
| if self.get('dependent_items'): | |
| start_text += self.dependency_description() | |
| return start_text + self['raw_description'] | |
| def update_status(self): | |
| # user clicked an item action button - update the item's state | |
| # also sets a boolean indicating if we should run an updatecheck | |
| self['updatecheck_needed'] = True | |
| original_status = self['status'] | |
| managed_update_names = getInstallInfo().get('managed_updates', []) | |
| if self['status'] == 'update-available': | |
| # mark the update for install | |
| self['status'] = u'install-requested' | |
| subscribe(self) | |
| elif self['status'] == 'update-will-be-installed': | |
| # cancel the update | |
| self['status'] = u'update-available' | |
| unmanage(self) | |
| elif self['status'] in ['will-be-removed', 'removal-requested', | |
| 'preparing-removal', 'removal-error']: | |
| if self['name'] in managed_update_names: | |
| # update is managed, so user can't opt out | |
| self['status'] = u'installed' | |
| elif self.get('needs_update'): | |
| # update being installed; can opt-out | |
| self['status'] = u'update-will-be-installed' | |
| else: | |
| # item is simply installed | |
| self['status'] = u'installed' | |
| unmanage(self) | |
| if original_status == 'removal-requested': | |
| self['updatecheck_needed'] = False | |
| elif self['status'] in ['will-be-installed', 'install-requested', | |
| 'downloading', 'install-error']: | |
| # cancel install | |
| if self.get('needs_update'): | |
| self['status'] = u'update-available' | |
| else: | |
| self['status'] = u'not-installed' | |
| unmanage(self) | |
| if original_status == 'install-requested': | |
| self['updatecheck_needed'] = False | |
| elif self['status'] == 'not-installed': | |
| # mark for install | |
| self['status'] = u'install-requested' | |
| subscribe(self) | |
| elif self['status'] == 'installed': | |
| # mark for removal | |
| self['status'] = u'removal-requested' | |
| unsubscribe(self) | |
| class UpdateItem(GenericItem): | |
| '''GenericItem subclass that models an update install item''' | |
| def __init__(self, *arg, **kw): | |
| super(UpdateItem, self).__init__(*arg, **kw) | |
| identifier = self.get('name', '') + '--version-' + self.get('version_to_install', '') | |
| self['detail_link'] = 'updatedetail-%s.html' % quote(identifier) | |
| if not self['status'] == 'will-be-removed': | |
| force_install_after_date = self.get('force_install_after_date') | |
| if force_install_after_date: | |
| self['type'] = NSLocalizedString( | |
| u"Critical Update", u"Critical Update type") | |
| self['due_date_sort'] = force_install_after_date | |
| if not 'type' in self: | |
| self['type'] = NSLocalizedString(u"Managed Update", | |
| u"Managed Update type") | |
| self['hide_cancel_button'] = u'hidden' | |
| self['dependent_items'] = dependentItems(self['name']) | |
| def description(self): | |
| warning = '' | |
| dependent_items = '' | |
| if not self['status'] == 'will-be-removed': | |
| force_install_after_date = self.get('force_install_after_date') | |
| if force_install_after_date: | |
| # insert installation deadline into description | |
| try: | |
| local_date = munki.discardTimeZoneFromDate( | |
| force_install_after_date) | |
| except munki.BadDateError: | |
| # some issue with the stored date | |
| pass | |
| else: | |
| date_str = munki.stringFromDate(local_date) | |
| forced_date_text = NSLocalizedString( | |
| u"This item must be installed by %s", | |
| u"Forced Date warning") | |
| warning = ('<span class="warning">' | |
| + forced_date_text % date_str | |
| + '</span><br><br>') | |
| if self.get('dependent_items'): | |
| dependent_items = self.dependency_description() | |
| return warning + dependent_items + self['raw_description'] |