Files
munki/code/apps/Managed Software Center/Managed Software Center/MSCMainWindowController.py
T

1254 lines
55 KiB
Python

# encoding: utf-8
#
# MSCMainWindowController.py
# Managed Software Center
#
# Copyright 2013-2016 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
#
# https://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 munki
import mschtml
import msclib
import msclog
#import FoundationPlist
import MSCBadgedTemplateImage
import MunkiItems
from urlparse import urlparse
from AlertController import AlertController
from objc import YES, NO, IBAction, IBOutlet, nil
from PyObjCTools import AppHelper
# Disable PyLint complaining about wildcard imports and unused symbols
# pylint: disable=W0401,W0614
from Foundation import *
from AppKit import *
from WebKit import *
# Disable PyLint complaining about 'invalid' camelCase names
# pylint: disable=C0103
class MSCMainWindowController(NSWindowController):
_alertedUserToOutstandingUpdates = False
_update_in_progress = False
managedsoftwareupdate_task = None
_update_queue = set()
# status vars
_status_title = u''
stop_requested = False
user_warned_about_extra_updates = False
html_dir = None
alert_context_info = None
cached_self_service = None
alert_controller = None
# Cocoa UI binding properties
softwareToolbarButton = IBOutlet()
categoriesToolbarButton = IBOutlet()
myItemsToolbarButton = IBOutlet()
updatesToolbarButton = IBOutlet()
webView = IBOutlet()
navigationBtn = IBOutlet()
progressSpinner = IBOutlet()
searchField = IBOutlet()
updateButtonCell = IBOutlet()
windowMenuSeperatorItem = IBOutlet()
fullScreenMenuItem = IBOutlet()
findMenuItem = IBOutlet()
softwareMenuItem = IBOutlet()
categoriesMenuItem = IBOutlet()
myItemsMenuItem = IBOutlet()
def appShouldTerminate(self):
'''called by app delegate
when it receives applicationShouldTerminate:'''
if self.getUpdateCount() == 0:
# no pending updates
return YES
if (self.currentPageIsUpdatesPage()
and not munki.thereAreUpdatesToBeForcedSoon()):
# We're already at the updates view, so user is aware of the
# pending updates, so OK to just terminate
# (unless there are some updates to be forced soon)
return YES
if (self.currentPageIsUpdatesPage()
and self._alertedUserToOutstandingUpdates):
return YES
# we have pending updates and we have not yet warned the user
# about them
self.alertToPendingUpdates()
return NO
def alertToPendingUpdates(self):
'''Alert user to pending updates before quitting the application'''
self._alertedUserToOutstandingUpdates = True
# show the updates
self.loadUpdatesPage_(self)
if munki.thereAreUpdatesToBeForcedSoon():
alertTitle = NSLocalizedString(u"Mandatory Updates Pending",
u"Mandatory Updates Pending text")
deadline = munki.earliestForceInstallDate()
time_til_logout = deadline.timeIntervalSinceNow()
if time_til_logout > 0:
deadline_str = munki.stringFromDate(deadline)
formatString = NSLocalizedString(
(u"One or more updates must be installed by %s. A logout "
"may be forced if you wait too long to update."),
u"Mandatory Updates Pending detail")
alertDetail = formatString % deadline_str
else:
alertDetail = NSLocalizedString(
(u"One or more mandatory updates are overdue for "
"installation. A logout will be forced soon."),
u"Mandatory Updates Imminent detail")
else:
alertTitle = NSLocalizedString(
u"Pending updates", u"Pending Updates alert title")
alertDetail = NSLocalizedString(
u"There are pending updates for this computer.",
u"Pending Updates alert detail text")
alert = NSAlert.alertWithMessageText_defaultButton_alternateButton_otherButton_informativeTextWithFormat_(
alertTitle,
NSLocalizedString(u"Quit", u"Quit button title"),
nil,
NSLocalizedString(u"Update now", u"Update Now button title"),
u"%@", alertDetail)
alert.beginSheetModalForWindow_modalDelegate_didEndSelector_contextInfo_(
self.window(), self,
self.updateAlertDidEnd_returnCode_contextInfo_, objc.nil)
@AppHelper.endSheetMethod
def updateAlertDidEnd_returnCode_contextInfo_(
self, alert, returncode, contextinfo):
'''Called when alert invoked by alertToPendingUpdates ends'''
if returncode == NSAlertDefaultReturn:
msclog.log("user", "quit")
NSApp.terminate_(self)
elif returncode == NSAlertOtherReturn:
msclog.log("user", "install_now_clicked")
# make sure this alert panel is gone before we proceed
# which might involve opening another alert sheet
alert.window().orderOut_(self)
# initiate the updates
self.updateNow()
self.loadUpdatesPage_(self)
### no longer needed now that we are using a "real" NSToolbar ###
#def window_willPositionSheet_usingRect_(self, window, sheet, rect):
# '''NSWindowDelegate method that allows us to modify the
# position sheets appear attached to a window'''
# # move the anchor point of our sheets to below our toolbar
# # (or really, to the top of the web view)
# webViewRect = self.webView.frame()
# return NSMakeRect(webViewRect.origin.x,
# webViewRect.origin.y + webViewRect.size.height,
# webViewRect.size.width, 0)
def loadInitialView(self):
'''Called by app delegate from applicationDidFinishLaunching:'''
self.enableOrDisableSoftwareViewControls()
optional_items = MunkiItems.getOptionalInstallItems()
if not optional_items or self.getUpdateCount():
self.loadUpdatesPage_(self)
else:
self.loadAllSoftwarePage_(self)
self.displayUpdateCount()
self.cached_self_service = MunkiItems.SelfService()
def highlightToolbarButtons_(self, nameToHighlight):
'''Highlight/dim buttons in our toolbar'''
self.softwareToolbarButton.setState_(nameToHighlight == "Software")
self.categoriesToolbarButton.setState_(nameToHighlight == "Categories")
self.myItemsToolbarButton.setState_(nameToHighlight == "My Items")
self.updatesToolbarButton.setState_(nameToHighlight == "Updates")
def enableOrDisableToolbarButtons_(self, enabled_state):
'''Enable or disable buttons in our toolbar'''
if self.window().isMainWindow() == NO:
enabled_state = NO
updates_button_state = NO
else:
updates_button_state = YES
self.softwareToolbarButton.setEnabled_(enabled_state)
self.categoriesToolbarButton.setEnabled_(enabled_state)
self.myItemsToolbarButton.setEnabled_(enabled_state)
self.updatesToolbarButton.setEnabled_(updates_button_state)
def enableOrDisableSoftwareViewControls(self):
'''Disable or enable the controls that let us view optional items'''
optional_items = MunkiItems.getOptionalInstallItems()
enabled_state = (len(optional_items) > 0)
self.enableOrDisableToolbarButtons_(enabled_state)
self.searchField.setEnabled_(enabled_state)
self.findMenuItem.setEnabled_(enabled_state)
self.softwareMenuItem.setEnabled_(enabled_state)
self.softwareMenuItem.setEnabled_(enabled_state)
self.categoriesMenuItem.setEnabled_(enabled_state)
self.myItemsMenuItem.setEnabled_(enabled_state)
def munkiStatusSessionEnded_(self, sessionResult):
'''Called by StatusController when a Munki session ends'''
msclog.debug_log(u"MunkiStatus session ended: %s" % sessionResult)
msclog.debug_log(
u"MunkiStatus session type: %s" % self.managedsoftwareupdate_task)
tasktype = self.managedsoftwareupdate_task
self.managedsoftwareupdate_task = None
self._update_in_progress = False
# The managedsoftwareupdate run will have changed state preferences
# in ManagedInstalls.plist. Load the new values.
munki.reload_prefs()
lastCheckResult = munki.pref("LastCheckResult")
if sessionResult != 0 or lastCheckResult < 0:
OKButtonTitle = NSLocalizedString(u"OK", u"OK button title")
alertMessageText = NSLocalizedString(
u"Update check failed", u"Update Check Failed title")
if tasktype == "installwithnologout":
msclog.log("MSC", "cant_update", "Install session failed")
alertMessageText = NSLocalizedString(
u"Install session failed", u"Install Session Failed title")
if sessionResult == -1:
# connection was dropped unexpectedly
msclog.log("MSC", "cant_update", "unexpected process end")
detailText = NSLocalizedString(
(u"There is a configuration problem with the managed "
"software installer. The process ended unexpectedly. "
"Contact your systems administrator."),
u"Unexpected Session End message")
elif sessionResult == -2:
# session never started
msclog.log("MSC", "cant_update", "process did not start")
detailText = NSLocalizedString(
(u"There is a configuration problem with the managed "
"software installer. Could not start the process. "
"Contact your systems administrator."),
u"Could Not Start Session message")
elif lastCheckResult == -1:
# server not reachable
msclog.log("MSC", "cant_update", "cannot contact server")
detailText = NSLocalizedString(
(u"Managed Software Center cannot contact the update "
"server at this time.\n"
"Try again later. If this situation continues, "
"contact your systems administrator."),
u"Cannot Contact Server detail")
elif lastCheckResult == -2:
# preflight failed
msclog.log("MSU", "cant_update", "failed preflight")
detailText = NSLocalizedString(
(u"Managed Software Center cannot check for updates now.\n"
"Try again later. If this situation continues, "
"contact your systems administrator."),
u"Failed Preflight Check detail")
# show the alert sheet
self.window().makeKeyAndOrderFront_(self)
alert = NSAlert.alertWithMessageText_defaultButton_alternateButton_otherButton_informativeTextWithFormat_(
alertMessageText, OKButtonTitle, nil, nil, u"%@", detailText)
alert.beginSheetModalForWindow_modalDelegate_didEndSelector_contextInfo_(
self.window(), self,
self.munkiSessionErrorAlertDidEnd_returnCode_contextInfo_, nil)
return
if tasktype == 'checktheninstall':
MunkiItems.reset()
# possibly check again if choices have changed
self.updateNow()
return
# all done checking and/or installing: display results
self.resetAndReload()
if MunkiItems.updateCheckNeeded():
# more stuff pending? Let's do it...
self.updateNow()
@AppHelper.endSheetMethod
def munkiSessionErrorAlertDidEnd_returnCode_contextInfo_(
self, alert, returncode, contextinfo):
'''Called when alert raised by munkiStatusSessionEnded ends'''
self.resetAndReload()
def resetAndReload(self):
'''Clear cached values, reload from disk. Display any changes.
Typically called soon after a Munki session completes'''
msclog.debug_log('resetAndReload method called')
# need to clear out cached data
MunkiItems.reset()
# recache SelfService choices
self.cached_self_service = MunkiItems.SelfService()
# copy any new custom client resources
msclib.get_custom_resources()
# pending updates may have changed
self._alertedUserToOutstandingUpdates = False
# enable/disable controls as needed
self.enableOrDisableSoftwareViewControls()
# what page are we currently viewing?
page_url = self.webView.mainFrameURL()
filename = NSURL.URLWithString_(page_url).lastPathComponent()
name = os.path.splitext(filename)[0]
key = name.partition('-')[0]
if key == 'detail':
# optional item detail page
self.webView.reload_(self)
if key in ['category', 'filter', 'developer']:
# optional item list page
self.updateListPage()
if key == 'categories':
# categories page
self.updateCategoriesPage()
if key == 'myitems':
# my items page
self.updateMyItemsPage()
if key == 'updates':
# updates page
self.webView.reload_(self)
self._alertedUserToOutstandingUpdates = True
if key == 'updatedetail':
# update detail page
self.webView.reload_(self)
# update count might have changed
self.displayUpdateCount()
def windowShouldClose_(self, sender):
'''NSWindowDelegate method called when user closes a window'''
# closing the main window should be the same as quitting
NSApp.terminate_(self)
return NO
def windowDidBecomeMain_(self, notification):
'''Our window was activated, make sure controls enabled as needed'''
optional_items = MunkiItems.getOptionalInstallItems()
enabled_state = (len(optional_items) > 0)
self.enableOrDisableToolbarButtons_(enabled_state)
def windowDidResignMain_(self, notification):
'''Our window was deactivated, make sure controls enabled as needed'''
self.enableOrDisableToolbarButtons_(NO)
def configureFullScreenMenuItem(self):
'''check to see if NSWindow's toggleFullScreen: selector is implemented.
if so, unhide the menu items for going full screen'''
if self.window().respondsToSelector_('toggleFullScreen:'):
self.windowMenuSeperatorItem.setHidden_(False)
self.fullScreenMenuItem.setHidden_(False)
self.fullScreenMenuItem.setEnabled_(True)
def awakeFromNib(self):
'''Stuff we need to intialize when we start'''
self.configureFullScreenMenuItem()
self.webView.setDrawsBackground_(NO)
self.webView.setUIDelegate_(self)
self.webView.setFrameLoadDelegate_(self)
self.webView.setResourceLoadDelegate_(self)
self.webView.setPolicyDelegate_(self)
self.setNoPageCache()
self.alert_controller = AlertController.alloc().init()
self.alert_controller.setWindow_(self.window())
self.html_dir = msclib.html_dir()
self.registerForNotifications()
def registerForNotifications(self):
'''register for notification messages'''
# register for notification if available updates change
notification_center = NSDistributedNotificationCenter.defaultCenter()
notification_center.addObserver_selector_name_object_suspensionBehavior_(
self,
self.updateAvailableUpdates,
'com.googlecode.munki.managedsoftwareupdate.updateschanged',
None,
NSNotificationSuspensionBehaviorDeliverImmediately)
# register for notification to display a logout warning
# from the logouthelper
notification_center = NSDistributedNotificationCenter.defaultCenter()
notification_center.addObserver_selector_name_object_suspensionBehavior_(
self,
self.forcedLogoutWarning,
'com.googlecode.munki.ManagedSoftwareUpdate.logoutwarn',
None,
NSNotificationSuspensionBehaviorDeliverImmediately)
def updateAvailableUpdates(self):
'''If a Munki session is not in progress (that we know of) and
we get a updateschanged notification, resetAndReload'''
msclog.debug_log(u"Managed Software Center got update notification")
if not self._update_in_progress:
self.resetAndReload()
def forcedLogoutWarning(self, notification_obj):
'''Received a logout warning from the logouthelper for an
upcoming forced install'''
msclog.debug_log(u"Managed Software Center got forced logout warning")
# got a notification of an upcoming forced install
# switch to updates view, then display alert
self.loadUpdatesPage_(self)
self.alert_controller.forcedLogoutWarning(notification_obj)
def checkForUpdates(self, suppress_apple_update_check=False):
'''start an update check session'''
# attempt to start the update check
if self._update_in_progress:
return
result = munki.startUpdateCheck(suppress_apple_update_check)
if result == 0:
self._update_in_progress = True
self.displayUpdateCount()
self.managedsoftwareupdate_task = "manualcheck"
NSApp.delegate().statusController.startMunkiStatusSession()
self.markRequestedItemsAsProcessing()
else:
self.munkiStatusSessionEnded_(2)
def kickOffInstallSession(self):
'''start an update install/removal session'''
# check for need to logout, restart, firmware warnings
# warn about blocking applications, etc...
# then start an update session
if (MunkiItems.updatesRequireRestart()
or MunkiItems.updatesRequireLogout()):
# switch to updates view
self.loadUpdatesPage_(self)
# warn about need to logout or restart
self.alert_controller.confirmUpdatesAndInstall()
else:
if self.alert_controller.alertedToBlockingAppsRunning():
self.loadUpdatesPage_(self)
return
if self.alert_controller.alertedToRunningOnBatteryAndCancelled():
self.loadUpdatesPage_(self)
return
self.managedsoftwareupdate_task = None
msclog.log("user", "install_without_logout")
self._update_in_progress = True
self.displayUpdateCount()
self.setStatusViewTitle_(
NSLocalizedString(u"Updating...", u"Updating message"))
result = munki.justUpdate()
if result:
msclog.debug_log("Error starting install session: %s" % result)
self.munkiStatusSessionEnded_(2)
else:
self.managedsoftwareupdate_task = "installwithnologout"
NSApp.delegate().statusController.startMunkiStatusSession()
self.markPendingItemsAsInstalling()
def markPendingItemsAsInstalling(self):
'''While an install/removal session is happening, mark optional items
that are being installed/removed with the appropriate status'''
msclog.debug_log('marking pendingItems as installing')
install_info = munki.getInstallInfo()
items_to_be_installed_names = [
item['name'] for item in install_info.get('managed_installs', [])]
items_to_be_removed_names = [
item['name'] for item in install_info.get('removals', [])]
for name in items_to_be_installed_names:
# remove names for user selections since we are installing
MunkiItems.user_install_selections.discard(name)
for name in items_to_be_removed_names:
# remove names for user selections since we are removing
MunkiItems.user_removal_selections.discard(name)
for item in MunkiItems.getOptionalInstallItems():
new_status = None
if item['name'] in items_to_be_installed_names:
msclog.debug_log(
'Setting status for %s to "installing"' % item['name'])
new_status = u'installing'
elif item['name'] in items_to_be_removed_names:
msclog.debug_log(
'Setting status for %s to "removing"' % item['name'])
new_status = u'removing'
if new_status:
item['status'] = new_status
self.updateDOMforOptionalItem(item)
def markRequestedItemsAsProcessing(self):
'''When an update check session is happening, mark optional items
that have been requested as processing'''
msclog.debug_log('marking requested items as processing')
for item in MunkiItems.getOptionalInstallItems():
new_status = None
if item['status'] == 'install-requested':
msclog.debug_log(
'Setting status for %s to "downloading"' % item['name'])
new_status = u'downloading'
elif item['status'] == 'removal-requested':
msclog.debug_log(
'Setting status for %s to "preparing-removal"'
% item['name'])
new_status = u'preparing-removal'
if new_status:
item['status'] = new_status
self.updateDOMforOptionalItem(item)
def updateNow(self):
'''If user has added to/removed from the list of things to be updated,
run a check session. If there are no more changes, proceed to an update
installation session if items to be installed/removed are exclusively
those selected by the user in this session'''
if self.stop_requested:
# reset the flag
self.stop_requested = False
self.resetAndReload()
return
if MunkiItems.updateCheckNeeded():
# any item status changes that require an update check?
msclog.debug_log('updateCheck needed')
msclog.log("user", "check_then_install_without_logout")
# since we are just checking for changed self-service items
# we can suppress the Apple update check
suppress_apple_update_check = True
self._update_in_progress = True
self.displayUpdateCount()
result = munki.startUpdateCheck(suppress_apple_update_check)
if result:
msclog.debug_log(
"Error starting check-then-install session: %s" % result)
self.munkiStatusSessionEnded_(2)
else:
self.managedsoftwareupdate_task = "checktheninstall"
NSApp.delegate().statusController.startMunkiStatusSession()
self.markRequestedItemsAsProcessing()
elif (not self._alertedUserToOutstandingUpdates
and MunkiItems.updatesContainNonUserSelectedItems()):
# current list of updates contains some not explicitly chosen by
# the user
msclog.debug_log(
'updateCheck not needed, items require user approval')
self._update_in_progress = False
self.displayUpdateCount()
self.loadUpdatesPage_(self)
self.alert_controller.alertToExtraUpdates()
else:
msclog.debug_log('updateCheck not needed')
self._alertedUserToOutstandingUpdates = False
self.kickOffInstallSession()
def getUpdateCount(self):
'''Get the count of effective updates'''
if self._update_in_progress:
return 0
return len(MunkiItems.getEffectiveUpdateList())
def displayUpdateCount(self):
'''Display the update count as a badge in the window toolbar
and as an icon badge in the Dock'''
updateCount = self.getUpdateCount()
btn_image = MSCBadgedTemplateImage.imageNamed_withCount_(
'updatesTemplate.pdf', updateCount)
self.updateButtonCell.setImage_(btn_image)
if updateCount not in [u'', 0]:
NSApp.dockTile().setBadgeLabel_(str(updateCount))
else:
NSApp.dockTile().setBadgeLabel_(None)
def updateMyItemsPage(self):
'''Update the "My Items" page with current data.
Modifies the DOM to avoid ugly browser refresh'''
myitems_rows = mschtml.build_myitems_rows()
document = self.webView.mainFrameDocument()
table_body_element = document.getElementById_('my_items_rows')
table_body_element.setInnerHTML_(myitems_rows)
def updateCategoriesPage(self):
'''Update the Catagories page with current data.
Modifies the DOM to avoid ugly browser refresh'''
items_html = mschtml.build_category_items_html()
document = self.webView.mainFrameDocument()
items_div_element = document.getElementById_('optional_installs_items')
items_div_element.setInnerHTML_(items_html)
def updateListPage(self):
'''Update the optional items list page with current data.
Modifies the DOM to avoid ugly browser refresh'''
page_url = self.webView.mainFrameURL()
filename = NSURL.URLWithString_(page_url).lastPathComponent()
name = os.path.splitext(filename)[0]
key, _, value = name.partition('-')
category = None
our_filter = None
developer = None
if key == 'category':
if value != 'all':
category = value
elif key == 'filter':
our_filter = value
elif key == 'developer':
developer = value
else:
msclog.debug_log(
'updateListPage unexpected error: _current_page_filename is %s'
% filename)
return
msclog.debug_log(
'updating software list page with category: '
'%s, developer; %s, filter: %s' % (category, developer, our_filter))
items_html = mschtml.build_list_page_items_html(
category=category, developer=developer, filter=our_filter)
document = self.webView.mainFrameDocument()
items_div_element = document.getElementById_('optional_installs_items')
items_div_element.setInnerHTML_(items_html)
def load_page(self, url_fragment):
'''Tells the WebView to load the appropriate page'''
msclog.debug_log('load_page request for %s' % url_fragment)
html_file = os.path.join(self.html_dir, url_fragment)
request = NSURLRequest.requestWithURL_cachePolicy_timeoutInterval_(
NSURL.fileURLWithPath_(html_file),
NSURLRequestReloadIgnoringLocalCacheData, 10)
self.webView.mainFrame().loadRequest_(request)
def setNoPageCache(self):
'''We disable the back/forward page cache because
we generate each page dynamically; we want things
that are changed in one page view to be reflected
immediately in all page views'''
identifier = u'com.googlecode.munki.ManagedSoftwareCenter'
prefs = WebPreferences.alloc().initWithIdentifier_(identifier)
prefs.setUsesPageCache_(False)
self.webView.setPreferencesIdentifier_(identifier)
##### WebView delegate methods #####
def webView_decidePolicyForNewWindowAction_request_newFrameName_decisionListener_(
self, sender, actionInformation, request, frameName, listener):
'''open link in default browser instead of in our app's WebView'''
listener.ignore()
NSWorkspace.sharedWorkspace().openURL_(request.URL())
def webView_decidePolicyForMIMEType_request_frame_decisionListener_(
self, sender, mimetype, request, frame, listener):
'''Decide whether to show or download content'''
if WebView.canShowMIMEType_(mimetype):
listener.use()
else:
# send the request to the user's default browser instead, where it
# can display or download it
listener.ignore()
NSWorkspace.sharedWorkspace().openURL_(request.URL())
def webView_resource_willSendRequest_redirectResponse_fromDataSource_(
self, sender, identifier, request, redirectResponse, dataSource):
'''By reacting to this delegate notification, we can build the page
the WebView wants to load'''
msclog.debug_log(
'webView_resource_willSendRequest_redirectResponse_fromDataSource_')
url = request.URL()
msclog.debug_log('Got URL scheme: %s' % url.scheme())
if url.scheme() == NSURLFileScheme:
msclog.debug_log(u'Request path is %s' % url.path())
if self.html_dir in url.path():
msclog.debug_log(u'request for %s' % url.path())
filename = unicode(url.lastPathComponent())
if (filename.endswith(u'.html')
and (filename.startswith(u'detail-')
or filename.startswith(u'category-')
or filename.startswith(u'filter-')
or filename.startswith(u'developer-')
or filename.startswith(u'updatedetail-')
or filename == u'myitems.html'
or filename == u'updates.html'
or filename == u'categories.html')):
try:
mschtml.build_page(filename)
except BaseException, err:
msclog.debug_log(u'Could not build page for %s: %s'
% (filename, err))
return request
def webView_didClearWindowObject_forFrame_(
self, sender, windowScriptObject, frame):
'''Configure webView to let JavaScript talk to this object.'''
windowScriptObject.setValue_forKey_(self, 'AppController')
def webView_didStartProvisionalLoadForFrame_(self, view, frame):
'''Animate progress spinner while we load a page and highlight the
proper toolbar button'''
self.progressSpinner.startAnimation_(self)
main_url = self.webView.mainFrameURL()
parts = urlparse(main_url)
pagename = os.path.basename(parts.path)
msclog.debug_log('Requested pagename is %s' % pagename)
if (pagename == 'category-all.html'
or pagename.startswith('detail-')
or pagename.startswith('filter-')
or pagename.startswith('developer-')):
self.highlightToolbarButtons_("Software")
elif pagename == 'categories.html' or pagename.startswith('category-'):
self.highlightToolbarButtons_("Categories")
elif pagename == 'myitems.html':
self.highlightToolbarButtons_("My Items")
elif pagename == 'updates.html' or pagename.startswith('updatedetail-'):
self.highlightToolbarButtons_("Updates")
else:
# no idea what type of item it is
self.highlightToolbarButtons_(None)
def webView_didFinishLoadForFrame_(self, view, frame):
'''Stop progress spinner and update state of back/forward buttons'''
self.progressSpinner.stopAnimation_(self)
self.navigationBtn.setEnabled_forSegment_(self.webView.canGoBack(), 0)
self.navigationBtn.setEnabled_forSegment_(
self.webView.canGoForward(), 1)
def webView_didFailProvisionalLoadWithError_forFrame_(
self, view, error, frame):
'''Stop progress spinner and log'''
self.progressSpinner.stopAnimation_(self)
msclog.debug_log(u'Provisional load error: %s' % error)
files = os.listdir(self.html_dir)
msclog.debug_log('Files in html_dir: %s' % files)
def webView_didFailLoadWithError_forFrame_(self, view, error, frame):
'''Stop progress spinner and log error'''
#TO-DO: display an error page?
self.progressSpinner.stopAnimation_(self)
msclog.debug_log('Committed load error: %s' % error)
def isSelectorExcludedFromWebScript_(self, aSelector):
'''Declare which methods can be called from JavaScript'''
# For security, you must explicitly allow a selector to be called
# from JavaScript.
if aSelector in ['openExternalLink:',
'actionButtonClicked:',
'myItemsActionButtonClicked:',
'changeSelectedCategory:',
'installButtonClicked',
'updateOptionalInstallButtonClicked:',
'updateOptionalInstallButtonFinishAction:']:
return NO # this selector is NOT _excluded_ from scripting
return YES # disallow everything else
#### handling DOM UI elements ####
def openExternalLink_(self, url):
'''open a link in the default browser'''
NSWorkspace.sharedWorkspace().openURL_(NSURL.URLWithString_(url))
def installButtonClicked(self):
'''this method is called from JavaScript when the user
clicks the Install button in the Updates view'''
if self._update_in_progress:
# this is now a stop/cancel button
msclog.log('user', 'cancel_updates')
NSApp.delegate().statusController.disableStopButton()
NSApp.delegate().statusController._status_stopBtnState = 1
self.stop_requested = True
# send a notification that stop button was clicked
STOP_REQUEST_FLAG = (
u'/private/tmp/'
'com.googlecode.munki.managedsoftwareupdate.stop_requested')
if not os.path.exists(STOP_REQUEST_FLAG):
open(STOP_REQUEST_FLAG, 'w').close()
elif self.getUpdateCount() == 0:
# no updates, this button must say "Check Again"
msclog.log('user', 'refresh_clicked')
self.checkForUpdates()
else:
# must say "Update"
self.updateNow()
def showUpdateProgressSpinner(self):
'''This method is currently unused'''
# update the status header on the updates page
document = self.webView.mainFrameDocument()
spinner = document.getElementById_('updates-progress-spinner')
if spinner:
spinner_classes = spinner.className().split(' ')
if 'hidden' in spinner_classes:
spinner_classes.remove('hidden')
spinner.setClassName_(' '.join(spinner_classes))
update_count_element = document.getElementById_('update-count-string')
if update_count_element:
update_count_element.setInnerText_(
NSLocalizedString(u"Checking for updates...",
u"Checking For Updates message"))
warning_text_element = document.getElementById_('update-warning-text')
if warning_text_element:
warning_text_element.setInnerHTML_('')
install_all_button = document.getElementById_('install-all-button-text')
if install_all_button:
install_all_button.setInnerText_(
NSLocalizedString(u"Cancel",
u"Cancel button title/short action text"))
container_div = document.getElementById_('os-and-app-updates')
if container_div:
container_div_classes = container_div.className().split(' ')
if not 'updating' in container_div_classes:
container_div_classes.append('updating')
container_div.setClassName_(' '.join(container_div_classes))
def updateOptionalInstallButtonClicked_(self, item_name):
'''this method is called from JavaScript when a user clicks
the cancel or add button in the updates list'''
item = MunkiItems.optionalItemForName_(item_name)
if not item:
msclog.debug_log(
'Unexpected error: Can\'t find item for %s' % item_name)
return
if (item['status'] == 'update-available'
and item.get('preupgrade_alert')):
self.displayPreInstallUninstallAlert_Action_Item_(
item['preupgrade_alert'],
self.updateOptionalInstallButtonBeginAction_, item_name)
else:
self.updateOptionalInstallButtonBeginAction_(item_name)
def updateOptionalInstallButtonBeginAction_(self, item_name):
scriptObject = self.webView.windowScriptObject()
args = [item_name]
scriptObject.callWebScriptMethod_withArguments_(
'fadeOutAndRemove', args)
def update_status_for_item(self, item):
'''Attempts to update an item's status; displays an error dialog
if SelfServeManifest is not writable.
Returns a boolean to indicate success'''
try:
item.update_status()
return True
except MunkiItems.SelfServiceError, err:
msclog.debug_log(str(err))
alertTitle = NSLocalizedString(
u"System configuration problem",
u"System configuration problem alert title")
alertDetail = NSLocalizedString(
u"A systems configuration issue is preventing Managed Software "
"Center from operating correctly. The reported issue is: ",
u"System configuration problem alert detail")
alertDetail = alertDetail + "\n" + str(err)
alert = NSAlert.alertWithMessageText_defaultButton_alternateButton_otherButton_informativeTextWithFormat_(
alertTitle,
NSLocalizedString(u"OK", u"OK button title"),
nil,
nil,
u"%@", alertDetail)
result = alert.runModal()
return False
def updateOptionalInstallButtonFinishAction_(self, item_name):
'''Perform the required action when a user clicks
the cancel or add button in the updates list'''
# TO-DO: better handling of all the possible "unexpected error"s
document = self.webView.mainFrameDocument()
item = MunkiItems.optionalItemForName_(item_name)
if not item:
msclog.debug_log(
'Unexpected error: Can\'t find item for %s' % item_name)
return
update_table_row = document.getElementById_('%s_update_table_row'
% item_name)
if not update_table_row:
msclog.debug_log(
'Unexpected error: Can\'t find table row for %s' % item_name)
return
# remove this row from its current table
update_table_row.parentNode().removeChild_(update_table_row)
previous_status = item['status']
# update item status
if not self.update_status_for_item(item):
# there was a problem, can't continue
return
msclog.log('user', 'optional_install_' + item['status'], item_name)
# do we need to add a new node to the other list?
if item.get('needs_update'):
# make some new HTML for the updated item
managed_update_names = MunkiItems.getInstallInfo().get(
'managed_updates', [])
item_template = mschtml.get_template('update_row_template.html')
item_html = item_template.safe_substitute(item)
if item['status'] in ['install-requested',
'update-will-be-installed', 'installed']:
# add the node to the updates-to-install table
table = document.getElementById_('updates-to-install-table')
if item['status'] == 'update-available':
# add the node to the other-updates table
table = document.getElementById_('other-updates-table')
if not table:
msclog.debug_log(
'Unexpected error: could not find other-updates-table')
return
# this isn't the greatest way to add something to the DOM
# but it works...
table.setInnerHTML_(table.innerHTML() + item_html)
# might need to toggle visibility of other updates div
other_updates_div = document.getElementById_('other-updates')
other_updates_div_classes = other_updates_div.className().split(' ')
other_updates_table = document.getElementById_('other-updates-table')
if other_updates_table.innerHTML().strip():
if 'hidden' in other_updates_div_classes:
other_updates_div_classes.remove('hidden')
other_updates_div.setClassName_(
' '.join(other_updates_div_classes))
else:
if not 'hidden' in other_updates_div_classes:
other_updates_div_classes.append('hidden')
other_updates_div.setClassName_(
' '.join(other_updates_div_classes))
# update the updates-to-install header to reflect the new list of
# updates to install
updateCount = self.getUpdateCount()
update_count_message = msclib.updateCountMessage(updateCount)
update_count_element = document.getElementById_('update-count-string')
if update_count_element:
update_count_element.setInnerText_(update_count_message)
warning_text = mschtml.get_warning_text()
warning_text_element = document.getElementById_('update-warning-text')
if warning_text_element:
warning_text_element.setInnerHTML_(warning_text)
# update text of Install All button
install_all_button_element = document.getElementById_(
'install-all-button-text')
if install_all_button_element:
install_all_button_element.setInnerText_(
msclib.getInstallAllButtonTextForCount(updateCount))
# update count badges
self.displayUpdateCount()
if MunkiItems.updateCheckNeeded():
# check for updates after a short delay so UI changes visually
# complete first
self.performSelector_withObject_afterDelay_(
self.checkForUpdates, True, 1.0)
def myItemsActionButtonClicked_(self, item_name):
'''this method is called from JavaScript when the user clicks
the Install/Remove/Cancel button in the My Items view'''
item = MunkiItems.optionalItemForName_(item_name)
if not item:
msclog.debug_log(
'Unexpected error: Can\'t find item for %s' % item_name)
return
if item['status'] == 'installed' and item.get('preuninstall_alert'):
self.displayPreInstallUninstallAlert_Action_Item_(
item['preuninstall_alert'],
self.myItemsActionButtonPerformAction_, item_name)
else:
self.myItemsActionButtonPerformAction_(item_name)
def myItemsActionButtonPerformAction_(self, item_name):
'''perfrom action needed when user clicks
the Install/Remove/Cancel button in the My Items view'''
document = self.webView.mainFrameDocument()
item = MunkiItems.optionalItemForName_(item_name)
status_line = document.getElementById_('%s_status_text' % item_name)
btn = document.getElementById_('%s_action_button_text' % item_name)
if not item or not btn or not status_line:
msclog.debug_log(
'User clicked MyItems action button for %s' % item_name)
msclog.debug_log('Unexpected error finding HTML elements')
return
prior_status = item['status']
if not self.update_status_for_item(item):
# there was a problem, can't continue
return
self.displayUpdateCount()
if item['status'] == 'not-installed':
# we removed item from list of things to install
# now remove from display
table_row = document.getElementById_(
'%s_myitems_table_row' % item_name)
if table_row:
table_row.parentNode().removeChild_(table_row)
else:
btn.setInnerText_(item['myitem_action_text'])
status_line.setInnerText_(item['status_text'])
status_line.setClassName_('status %s' % item['status'])
if item['status'] in ['install-requested', 'removal-requested']:
self._alertedUserToOutstandingUpdates = False
if not self._update_in_progress:
self.updateNow()
elif prior_status in ['will-be-installed', 'update-will-be-installed',
'will-be-removed']:
# cancelled a pending install or removal; should run an updatecheck
self.checkForUpdates(suppress_apple_update_check=True)
def updateDOMforOptionalItem(self, item):
'''Update displayed status of an item'''
document = self.webView.mainFrameDocument()
if not document:
return
status_line = document.getElementById_('%s_status_text' % item['name'])
status_text_span = document.getElementById_(
'%s_status_text_span' % item['name'])
btn = document.getElementById_('%s_action_button_text' % item['name'])
if not btn or not status_line:
msclog.debug_log('ERROR in updateDOMforOptionalItem: '
'could not find items in DOM')
return
btn_classes = btn.className().split(' ')
# filter out status class
btn_classes = [class_name for class_name in btn_classes
if class_name in ['msc-button-inner', 'large', 'small',
'install-updates']]
btn_classes.append(item['status'])
btn.setClassName_(' '.join(btn_classes))
if 'install-updates' in btn_classes:
btn.setInnerText_(item['myitem_action_text'])
elif 'large' in btn_classes:
btn.setInnerText_(item['long_action_text'])
else:
btn.setInnerText_(item['short_action_text'])
if status_text_span:
status_text_span.setInnerText_(item['status_text'])
status_line.setClassName_(item['status'])
def actionButtonClicked_(self, item_name):
'''this method is called from JavaScript when the user clicks
the Install/Removel/Cancel button in the list or detail view'''
item = MunkiItems.optionalItemForName_(item_name)
if not item:
msclog.debug_log(
'User clicked Install/Remove/Upgrade/Cancel button in the list '
'or detail view')
msclog.debug_log('Can\'t find item: %s' % item_name)
return
showAlert = True
if item['status'] == 'not-installed' and item.get('preinstall_alert'):
self.displayPreInstallUninstallAlert_Action_Item_(
item['preinstall_alert'],
self.actionButtonPerformAction_, item_name)
elif item['status'] == 'installed' and item.get('preuninstall_alert'):
self.displayPreInstallUninstallAlert_Action_Item_(
item['preuninstall_alert'],
self.actionButtonPerformAction_, item_name)
elif (item['status'] == 'update-available'
and item.get('preupgrade_alert')):
self.displayPreInstallUninstallAlert_Action_Item_(
item['preupgrade_alert'],
self.actionButtonPerformAction_, item_name)
else:
self.actionButtonPerformAction_(item_name)
showAlert = False
if showAlert:
msclog.log("user", "show_alert")
def displayPreInstallUninstallAlert_Action_Item_(
self, alert_dict, action_selector, item_name):
''' Display an alert sheet before processing item install/upgrade
or uninstall '''
defaultAlertTitle = NSLocalizedString(
u'Attention', u'Pre Install Uninstall Upgrade Alert Title')
defaultAlertDetail = NSLocalizedString(
u'Some conditions apply to this software. '
'Please contact your administrator for more details',
u'Pre Install Uninstall Upgrade Alert Detail')
defaultOKLabel = NSLocalizedString(
u'OK', u'Pre Install Uninstall Upgrade OK Label')
defaultCancelLabel = NSLocalizedString(
u'Cancel', u'Pre Install Uninstall Upgrade Cancel Label')
alertTitle = alert_dict.get('alert_title') or defaultAlertTitle
alertDetail = alert_dict.get('alert_detail', defaultAlertDetail)
OKLabel = alert_dict.get('ok_label') or defaultOKLabel
cancelLabel = alert_dict.get('cancel_label') or defaultCancelLabel
self.alert_context_info = {'selector': action_selector,
'item_name': item_name}
# show the alert sheet
self.window().makeKeyAndOrderFront_(self)
alert = NSAlert.alertWithMessageText_defaultButton_alternateButton_otherButton_informativeTextWithFormat_(
alertTitle,
cancelLabel,
OKLabel,
nil,
u"%@", alertDetail)
alert.beginSheetModalForWindow_modalDelegate_didEndSelector_contextInfo_(
self.window(),
self,
self.actionAlertDidEnd_returnCode_contextInfo_,
nil)
def actionAlertDidEnd_returnCode_contextInfo_(
self, alert, returncode, contextinfo):
'''Called when alert invoked by actionButtonClicked_ ends'''
alert.window().orderOut_(self)
if returncode == NSAlertDefaultReturn:
msclog.log("user", "alert_canceled")
else:
msclog.log("user", "alert_accepted")
selector = self.alert_context_info.get('selector')
item_name = self.alert_context_info.get('item_name')
if selector and item_name:
selector(item_name)
def actionButtonPerformAction_(self, item_name):
'''Perform the action requested when clicking the action button
in the list or detail view'''
item = MunkiItems.optionalItemForName_(item_name)
if not item:
msclog.debug_log(
'User clicked Install/Upgrade/Removal/Cancel button '
'in the list or detail view')
msclog.debug_log('Can\'t find item: %s' % item_name)
return
prior_status = item['status']
if not self.update_status_for_item(item):
# there was a problem, can't continue
return
msclog.log('user', 'action_button_' + item['status'], item_name)
self.displayUpdateCount()
self.updateDOMforOptionalItem(item)
if item['status'] in ['install-requested', 'removal-requested']:
self._alertedUserToOutstandingUpdates = False
if not self._update_in_progress:
self.updateNow()
elif prior_status in ['will-be-installed', 'update-will-be-installed',
'will-be-removed']:
# cancelled a pending install or removal; should run an updatecheck
self.checkForUpdates(suppress_apple_update_check=True)
def changeSelectedCategory_(self, category):
'''this method is called from JavaScript when the user
changes the category selected in the sidebar popup'''
all_categories_label = NSLocalizedString(
u"All Categories", u"AllCategoriesLabel")
if category == all_categories_label:
category = u'all'
self.load_page('category-%s.html' % category)
def setStatusViewTitle_(self, title_text):
'''When displaying status during a managedsoftwareupdate run, this
method is used to display info where the update count message
usually is'''
document = self.webView.mainFrameDocument()
self._status_title = title_text
# we re-purpose the update count message for this
update_count_element = document.getElementById_('update-count-string')
if update_count_element:
update_count_element.setInnerText_(title_text)
#### some Cocoa UI bindings #####
@IBAction
def showHelp_(self, sender):
helpURL = munki.pref('HelpURL')
if helpURL:
NSWorkspace.sharedWorkspace().openURL_(
NSURL.URLWithString_(helpURL))
else:
alertTitle = NSLocalizedString(u"Help", u"No help alert title")
alertDetail = NSLocalizedString(
u"Help isn't available for Managed Software Center.",
u"No help alert detail")
alert = NSAlert.alertWithMessageText_defaultButton_alternateButton_otherButton_informativeTextWithFormat_(
alertTitle,
NSLocalizedString(u"OK", u"OK button title"),
nil,
nil,
u"%@", alertDetail)
result = alert.runModal()
@IBAction
def navigationBtnClicked_(self, sender):
'''Handle WebView forward/back buttons'''
segment = sender.selectedSegment()
if segment == 0:
self.webView.goBack_(self)
if segment == 1:
self.webView.goForward_(self)
@IBAction
def loadAllSoftwarePage_(self, sender):
'''Called by Navigate menu item'''
self.load_page('category-all.html')
@IBAction
def loadCategoriesPage_(self, sender):
'''Called by Navigate menu item'''
self.load_page('categories.html')
@IBAction
def loadMyItemsPage_(self, sender):
'''Called by Navigate menu item'''
self.load_page('myitems.html')
@IBAction
def loadUpdatesPage_(self, sender):
'''Called by Navigate menu item'''
self.load_page('updates.html')
self._alertedUserToOutstandingUpdates = True
@IBAction
def softwareToolbarButtonClicked_(self, sender):
'''User clicked Software toolbar button'''
self.loadAllSoftwarePage_(sender)
@IBAction
def categoriesToolbarButtonClicked_(self, sender):
'''User clicked Categories toolbar button'''
self.loadCategoriesPage_(sender)
@IBAction
def myItemsToolbarButtonClicked_(self, sender):
'''User clicked My Items toolbar button'''
self.loadMyItemsPage_(sender)
@IBAction
def updatesToolbarButtonClicked_(self, sender):
'''User clicked Updates toolbar button'''
self.loadUpdatesPage_(sender)
@IBAction
def searchFilterChanged_(self, sender):
'''User changed the search field'''
filterString = self.searchField.stringValue().lower()
if filterString:
msclog.debug_log('Search filter is: %s'
% repr(filterString.encode('utf-8')))
self.load_page(u'filter-%s.html' % filterString)
def currentPageIsUpdatesPage(self):
'''return True if current tab selected is Updates'''
return self.updatesToolbarButton.state() == NSOnState
#def currentPageIsMyItemsPage(self):
# '''return True if current tab selected is My Items'''
# return (self.myItemsToolbarButton.state() == NSOnState)
#def currentPageIsCategoriesPage(self):
# '''return True if current tab selected is Categories'''
# return (self.categoriesToolbarButton.state() == NSOnState)