Skip to content
This repository was archived by the owner on Feb 8, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ This is the second set of text above the **Update Machine** button.
"button_sub_titletext": "Click on the button below."
```

### Dismissal Count Threshold
This is the amount of times a user can disregard nudge before more aggressive behaviors kick in.

```json
"dismissal_count_threshold": 100
```

### URL for self-servicing upgrade app
This is the full URL for a local self-servicing app such as Jamf Self
Service or Munki Managed Software Center linking directly to a Jamf
Expand Down Expand Up @@ -211,7 +218,7 @@ This is the path to the macOS installer application.
Note: This setting is ignored when `local_url_for_upgrade` is provided.

### Days Between Notifications
Instead of having the Nudge GUI appear every half hour, make sure there is at least this many days between notifications.
Instead of having the Nudge GUI appear every half hour, make sure there is at least this many days between notifications.
*Note*: if you set this to something other than 0, it may not be evaluated in full 24-hour increments. For example, if the Nudge GUI appeared on Monday in the afternoon, it may appear Tuesday morning.
```json
"days_between_notifications": 0
Expand Down
92 changes: 83 additions & 9 deletions payload/Library/nudge/Resources/nudge
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import urllib.request, urllib.parse, urllib.error
import webbrowser
from datetime import datetime, timedelta
from distutils.version import LooseVersion
from urllib.parse import urlparse, unquote
import Foundation
import objc
from AppKit import NSApplication, NSImage
from AppKit import *
from CoreFoundation import CFPreferencesCopyAppValue, CFPreferencesSetAppValue, CFPreferencesAppSynchronize
from SystemConfiguration import SCDynamicStoreCopyConsoleUser

Expand All @@ -29,34 +30,92 @@ import gurl
class timerController(Foundation.NSObject):
'''Thanks to frogor for help in figuring this part out'''
def activateWindow_(self, timer_obj):
nudgelog('Re-activating .nib to the foreground')
# Move the application to the front
NSApplication.sharedApplication().activateIgnoringOtherApps_(True)
# Move the main window to the front
# Nibbler objects have a .win property (...should probably be .window)
# that contains a reference to the first NSWindow it finds
nudge.win.makeKeyAndOrderFront_(None)
determine_state_and_nudge()


def determine_state_and_nudge():
'''Determine the state of nudge and re-fresh window'''
workspace = NSWorkspace.sharedWorkspace()
currently_active = NSApplication.sharedApplication().isActive()
frontmost_app = workspace.frontmostApplication().bundleIdentifier()
# Setup these globals as we will potentially override them
global NUDGE_DISMISSED_COUNT
global ACCEPTABLE_APPS
if not currently_active and frontmost_app not in ACCEPTABLE_APPS:
nudgelog('Nudge or acceptable applications not currently active')
# If this is the under max dismissed count, just bring nudge back to the forefront
# This is the old behavior
if NUDGE_DISMISSED_COUNT < DISMISSAL_COUNT_THRESHOLD:
nudgelog('Nudge dismissed count under threshold')
NUDGE_DISMISSED_COUNT += 1
bring_nudge_to_forefront()
else:
# Get more aggressive - new behavior
nudgelog('Nudge dismissed count over threshold')
NUDGE_DISMISSED_COUNT += 1
nudgelog('Enforcing acceptable applications')
# Loop through all the running applications
for app in NSWorkspace.sharedWorkspace().runningApplications():
app_name = str(app.bundleIdentifier())
app_bundle = str(app.bundleURL())
if app_bundle:
# The app bundle contains file://, quoted path and trailing slashes
app_bundle_path = unquote(urlparse(app_bundle).path).rstrip('\/')
# Add Software Update pane or macOS upgrade app to acceptable app list
if app_bundle_path == PATH_TO_APP:
ACCEPTABLE_APPS.append(app_name)
else:
# Some of the apps from NSWorkspace don't have bundles, so force empty string
app_bundle_path = ''
# Hide any apps that are not in acceptable list or are not the macOS upgrade app
if (app_name not in ACCEPTABLE_APPS) or (app_bundle_path != PATH_TO_APP):
app.hide()
# Race condition with NSWorkspace. Python is faster :)
time.sleep(0.001)
# Another small sleep to ensure we can bring Nudge on top
time.sleep(0.5)
bring_nudge_to_forefront()
# Pretend to open the button and open the update mechanism
button_update(True)


def bring_nudge_to_forefront():
'''Brings nudge to the forefront - old behavior'''
nudgelog('Nudge not active - Activating to the foreground')
# We have to bring back python to the forefront since nibbler is a giant cheat
NSApplication.sharedApplication().activateIgnoringOtherApps_(True)
# Now bring the nudge window itself to the forefront
# Nibbler objects have a .win property (...should probably be .window)
# that contains a reference to the first NSWindow it finds
nudge.win.makeKeyAndOrderFront_(None)


def button_moreinfo():
'''Open browser more info button'''
nudgelog('User clicked on more info button - opening URL in default browser')
webbrowser.open_new_tab(MORE_INFO_URL)


def button_update():
def button_update(simulated_click=False):
'''Start the update process'''
if simulated_click:
nudgelog('Simulated click on update button - opening update application')
else:
nudgelog('User clicked on update button - opening update application')
cmd = ['/usr/bin/open', PATH_TO_APP]
subprocess.Popen(cmd)


def button_ok():
'''Quit out of nudge if user hits the ok button'''
nudgelog('User clicked on ok button - exiting application')
nudge.quit()


def button_understand():
'''Add an extra button to force the user to read the dialog, prior to being
able to exit the UI.'''
nudgelog('User clicked on understand button - enabling ok button')
nudge.views['button.understand'].setHidden_(True)
nudge.views['button.ok'].setHidden_(False)
nudge.views['button.ok'].setEnabled_(True)
Expand Down Expand Up @@ -128,6 +187,7 @@ def get_os_version():
'''Return OS version.'''
return LooseVersion(platform.mac_ver()[0])


def get_os_version_major():
'''Return major OS version.'''
full_os = platform.mac_ver()[0]
Expand All @@ -141,6 +201,7 @@ def get_os_version_major():
nudgelog('Cannot reliably determine OS major version. Exiting...')
exit(1)


def get_parsed_options():
'''Return the parsed options and args for this application.'''
# Options
Expand Down Expand Up @@ -312,13 +373,24 @@ def main():
exit(0)

# Setup our globals to use across nibbler and nibbler functions
global DISMISSAL_COUNT_THRESHOLD
global NUDGE_PATH
global MORE_INFO_URL
global PATH_TO_APP
global NUDGE_DISMISSED_COUNT
global ACCEPTABLE_APPS

# Figure out the local path of nudge
NUDGE_PATH = os.path.dirname(os.path.realpath(__file__))

# Part for enhanced enforcement of Nudge
NUDGE_DISMISSED_COUNT = 0
ACCEPTABLE_APPS = [
'com.apple.loginwindow',
'com.apple.systempreferences',
'org.python.python'
]

# local json path - if it exists already, let's assume someone is bundling
# it with their package. Otherwise check for it and use gurl.
json_path = os.path.join(NUDGE_PATH, 'nudge.json')
Expand Down Expand Up @@ -383,6 +455,7 @@ def main():
cut_off_date_warning = nudge_prefs.get('cut_off_date_warning', 3)
days_between_notifications = nudge_prefs.get('days_between_notifications',
0)
DISMISSAL_COUNT_THRESHOLD = nudge_prefs.get('dismissal_count_threshold', 9999999)
logo_path = nudge_prefs.get('logo_path', 'company_logo.png')
main_subtitle_text = nudge_prefs.get('main_subtitle_text',
'A friendly reminder from your local IT team')
Expand Down Expand Up @@ -421,6 +494,7 @@ def main():
update_minor = False
else:
nudgelog('Target OS subversion: %s' % minimum_os_sub_build_version)
nudgelog('Dismissal count threshold: %s ' % DISMISSAL_COUNT_THRESHOLD)


# cleanup the tmp stuff now
Expand Down