├── Stupid Groups ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── 16.png │ │ ├── 32.png │ │ ├── 64.png │ │ ├── 1024.png │ │ ├── 128.png │ │ ├── 256.png │ │ ├── 32-1.png │ │ ├── 512.png │ │ ├── 256-1.png │ │ ├── 512-1.png │ │ └── Contents.json │ ├── IconUser.imageset │ │ ├── IconUser.png │ │ └── Contents.json │ ├── IconLocked.imageset │ │ ├── IconLocked.png │ │ └── Contents.json │ ├── IconSafari.imageset │ │ ├── IconSafari.png │ │ └── Contents.json │ └── IconUnlocked.imageset │ │ ├── IconUnlocked.png │ │ └── Contents.json ├── Stupid Groups.entitlements ├── Stupid_Groups.entitlements ├── AppDelegate.swift ├── Info.plist ├── popPrompt.swift ├── apiFunctions.swift ├── prepareData.swift ├── loginWindow.swift ├── ViewController.swift └── Base.lproj │ └── Main.storyboard ├── Stupid Groups.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Stupid Groups.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ └── mlev.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── project.pbxproj ├── Stupid GroupsTests ├── Info.plist └── Stupid_GroupsTests.swift ├── Stupid GroupsUITests ├── Info.plist └── Stupid_GroupsUITests.swift └── README.md /Stupid Groups/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-levenick/stupid-groups/HEAD/Stupid Groups/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-levenick/stupid-groups/HEAD/Stupid Groups/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-levenick/stupid-groups/HEAD/Stupid Groups/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-levenick/stupid-groups/HEAD/Stupid Groups/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-levenick/stupid-groups/HEAD/Stupid Groups/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-levenick/stupid-groups/HEAD/Stupid Groups/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/AppIcon.appiconset/32-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-levenick/stupid-groups/HEAD/Stupid Groups/Assets.xcassets/AppIcon.appiconset/32-1.png -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-levenick/stupid-groups/HEAD/Stupid Groups/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/AppIcon.appiconset/256-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-levenick/stupid-groups/HEAD/Stupid Groups/Assets.xcassets/AppIcon.appiconset/256-1.png -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/AppIcon.appiconset/512-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-levenick/stupid-groups/HEAD/Stupid Groups/Assets.xcassets/AppIcon.appiconset/512-1.png -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/IconUser.imageset/IconUser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-levenick/stupid-groups/HEAD/Stupid Groups/Assets.xcassets/IconUser.imageset/IconUser.png -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/IconLocked.imageset/IconLocked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-levenick/stupid-groups/HEAD/Stupid Groups/Assets.xcassets/IconLocked.imageset/IconLocked.png -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/IconSafari.imageset/IconSafari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-levenick/stupid-groups/HEAD/Stupid Groups/Assets.xcassets/IconSafari.imageset/IconSafari.png -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/IconUnlocked.imageset/IconUnlocked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-levenick/stupid-groups/HEAD/Stupid Groups/Assets.xcassets/IconUnlocked.imageset/IconUnlocked.png -------------------------------------------------------------------------------- /Stupid Groups.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Stupid Groups.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Stupid Groups.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Stupid Groups.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/IconUser.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "IconUser.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Stupid Groups/Stupid Groups.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Stupid Groups/Stupid_Groups.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/IconLocked.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "IconLocked.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/IconSafari.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "IconSafari.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/IconUnlocked.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "IconUnlocked.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Stupid Groups.xcodeproj/xcuserdata/mlev.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Stupid Groups.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Stupid GroupsTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Stupid GroupsUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Stupid Groups/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Stupid Groups 4 | // 5 | // Created by Michael Levenick on 1/21/19. 6 | // Copyright © 2019 Michael Levenick. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | 15 | func applicationDidFinishLaunching(_ aNotification: Notification) { 16 | // Insert code here to initialize your application 17 | } 18 | 19 | func applicationWillTerminate(_ aNotification: Notification) { 20 | // Insert code here to tear down your application 21 | } 22 | 23 | // Causes app to be quit properly after last window closed 24 | // This is required by Apple in order for the app to be on the app store. 25 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 26 | return true 27 | } 28 | 29 | 30 | } 31 | 32 | -------------------------------------------------------------------------------- /Stupid GroupsTests/Stupid_GroupsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stupid_GroupsTests.swift 3 | // Stupid GroupsTests 4 | // 5 | // Created by Michael Levenick on 1/21/19. 6 | // Copyright © 2019 Michael Levenick. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Stupid_Groups 11 | 12 | class Stupid_GroupsTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Stupid GroupsUITests/Stupid_GroupsUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stupid_GroupsUITests.swift 3 | // Stupid GroupsUITests 4 | // 5 | // Created by Michael Levenick on 1/21/19. 6 | // Copyright © 2019 Michael Levenick. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class Stupid_GroupsUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | 16 | // In UI tests it is usually best to stop immediately when a failure occurs. 17 | continueAfterFailure = false 18 | 19 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 20 | XCUIApplication().launch() 21 | 22 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 23 | } 24 | 25 | override func tearDown() { 26 | // Put teardown code here. This method is called after the invocation of each test method in the class. 27 | } 28 | 29 | func testExample() { 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Stupid Groups/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | NSAllowsArbitraryLoadsInWebContent 10 | 11 | 12 | CFBundleDevelopmentRegion 13 | $(DEVELOPMENT_LANGUAGE) 14 | CFBundleExecutable 15 | $(EXECUTABLE_NAME) 16 | CFBundleIconFile 17 | 18 | CFBundleIdentifier 19 | $(PRODUCT_BUNDLE_IDENTIFIER) 20 | CFBundleInfoDictionaryVersion 21 | 6.0 22 | CFBundleName 23 | $(PRODUCT_NAME) 24 | CFBundlePackageType 25 | APPL 26 | CFBundleShortVersionString 27 | 1.1 28 | CFBundleVersion 29 | 1 30 | LSMinimumSystemVersion 31 | $(MACOSX_DEPLOYMENT_TARGET) 32 | NSHumanReadableCopyright 33 | Copyright © 2019 Michael Levenick. All rights reserved. 34 | NSMainStoryboardFile 35 | Main 36 | NSPrincipalClass 37 | NSApplication 38 | 39 | 40 | -------------------------------------------------------------------------------- /Stupid Groups/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "32-1.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "256-1.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "512-1.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stupid Groups 2 | ### What it is: 3 | A learning project by Mike Levenick that can convert Smart Groups in Jamf Pro to either Advanced Searches or Static Groups. 4 | ### Why it exists: 5 | Smart Groups are a powerful tool in Jamf Pro, which are designed to be used for active scoping to groups of users or devices whose membership are changing frequently. However, often times customers will have things like compliance reports set up as Smart Groups with nothing scoped to them. These do not need to recalculate constantly, and can be calculated upon view for compliance. Sometimes, smart groups were also set up for things such as iPad carts, whose membership rarely (if ever) change. 6 | 7 | This large number of unnecessary smart groups can have a significant performance impact on a Jamf Pro server, but cleanup and remediation is often frustrating. Stupid Groups is an app designed to "make your apps less smart", by converting them automatically to either a Static Group (if they are used for scoping) or an Advanced Search (if they are used for compliance reporting or similar). 8 | ### How to use it: 9 | Launch the app and enter either your Jamf Pro URL if you host your own server, or your Jamf Cloud instance name if you have a Jamf Cloud hosted instance. Then enter a username and password. Stupid Groups will authenticate to your Jamf Pro server and attempt to pull the activation code to verify your credentials. 10 | 11 | Once it has successfully authenticated, simply select the record type (Computer, Mobile Device, or User) and select whether you'd like to convert the Smart Group to an Advanced Search or Static Group. Then enter the ID of the Smart Group to convert, and run a Pre-Flight Check. 12 | 13 | The Pre-Flight Check will let you know what is about to happen, and open up the option to convert the group if everything looks good. 14 | -------------------------------------------------------------------------------- /Stupid Groups/popPrompt.swift: -------------------------------------------------------------------------------- 1 | // 2 | // popPrompt.swift 3 | // Stupid Groups 4 | // 5 | // Created by Michael Levenick on 1/21/19. 6 | // Copyright © 2019 Michael Levenick. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | 12 | public class popPrompt { 13 | 14 | var globalCSVString: String! 15 | 16 | 17 | // Generate a generic warning message for invalid credentials etc 18 | public func generalWarning(question: String, text: String) -> Bool { 19 | let myPopup: NSAlert = NSAlert() 20 | myPopup.messageText = question 21 | myPopup.informativeText = text 22 | myPopup.alertStyle = NSAlert.Style.warning 23 | myPopup.addButton(withTitle: "OK") 24 | return myPopup.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn 25 | } 26 | 27 | // Generate a specific prompt to ask for credentials 28 | public func selectDelim (question: String, text: String) -> Bool { 29 | let myPopup: NSAlert = NSAlert() 30 | myPopup.messageText = question 31 | myPopup.informativeText = text 32 | myPopup.alertStyle = NSAlert.Style.warning 33 | myPopup.addButton(withTitle: "Use Comma") 34 | myPopup.addButton(withTitle: "Use Semi-Colon") 35 | return myPopup.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn 36 | } 37 | 38 | // Generate a specific prompt to ask for concurrent runs 39 | public func selectConcurrent (question: String, text: String) -> Bool { 40 | let myPopup: NSAlert = NSAlert() 41 | myPopup.messageText = question 42 | myPopup.informativeText = text 43 | myPopup.alertStyle = NSAlert.Style.warning 44 | myPopup.addButton(withTitle: "2 at a time") 45 | myPopup.addButton(withTitle: "1 at a time") 46 | return myPopup.runModal() == NSApplication.ModalResponse.alertSecondButtonReturn 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Stupid Groups/apiFunctions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // apiFunctions.swift 3 | // Stupid Groups 4 | // 5 | // Created by Michael Levenick on 1/24/19. 6 | // Copyright © 2019 Michael Levenick. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class API { 12 | 13 | // This function can be used for any GET. Pass in a URL and base64 encoded credentials. 14 | // The credentials are inserted into the header. 15 | public func get(getCredentials: String, getURL: URL) -> String { 16 | 17 | // Declare a variable to be populated, and set up the HTTP Request with headers 18 | var stringToReturn = "nil" 19 | let semaphore = DispatchSemaphore(value: 0) 20 | let request = NSMutableURLRequest(url: getURL) 21 | request.httpMethod = "GET" 22 | let configuration = URLSessionConfiguration.default 23 | configuration.httpAdditionalHeaders = ["Authorization" : "Basic \(getCredentials)", "Content-Type" : "text/xml", "Accept" : "text/xml"] 24 | let session = Foundation.URLSession(configuration: configuration) 25 | 26 | // Completion handler. This is what ensures that the response is good/bad 27 | // and also what handles the semaphore 28 | let task = session.dataTask(with: request as URLRequest, completionHandler: { 29 | (data, response, error) -> Void in 30 | if let httpResponse = response as? HTTPURLResponse { 31 | if httpResponse.statusCode >= 199 && httpResponse.statusCode <= 299 { 32 | // Good response from API 33 | stringToReturn = String(decoding: data!, as: UTF8.self) 34 | NSLog("[INFO ]: Successful GET completed by StupidGroups.app") 35 | NSLog(response?.description ?? "nil") 36 | } else { 37 | // Bad Response from API 38 | stringToReturn = String(decoding: data!, as: UTF8.self) 39 | NSLog("[ERROR ]: Failed GET completed by StupidGroups.app") 40 | NSLog(response?.description ?? "nil") 41 | } 42 | semaphore.signal() // Signal completion to the semaphore 43 | } 44 | 45 | if error != nil { 46 | NSLog("[FATAL ]: " + error!.localizedDescription) 47 | stringToReturn = String("[FATAL ]: " + error!.localizedDescription) 48 | semaphore.signal() 49 | } 50 | }) 51 | task.resume() // Kick off the actual GET here 52 | semaphore.wait() // Wait for the semaphore before moving on to the return value 53 | return stringToReturn 54 | } 55 | 56 | // This function can be used for any POST. Pass in a URL and base64 encoded credentials. 57 | // The credentials are inserted into the header. 58 | public func post(postCredentials: String, postURL: URL, postBody: Data) -> String { 59 | 60 | // Declare a variable to be populated, and set up the HTTP Request with headers 61 | var stringToReturn = "nil" 62 | let semaphore = DispatchSemaphore(value: 0) 63 | let request = NSMutableURLRequest(url: postURL) 64 | request.httpMethod = "POST" 65 | request.httpBody = postBody 66 | let configuration = URLSessionConfiguration.default 67 | configuration.httpAdditionalHeaders = ["Authorization" : "Basic \(postCredentials)", "Content-Type" : "text/xml", "Accept" : "text/xml"] 68 | let session = Foundation.URLSession(configuration: configuration) 69 | 70 | // Completion handler. This is what ensures that the response is good/bad 71 | // and also what handles the semaphore 72 | let task = session.dataTask(with: request as URLRequest, completionHandler: { 73 | (data, response, error) -> Void in 74 | if let httpResponse = response as? HTTPURLResponse { 75 | if httpResponse.statusCode >= 199 && httpResponse.statusCode <= 299 { 76 | // Good Response from API 77 | stringToReturn = String(decoding: data!, as: UTF8.self) 78 | NSLog("[INFO ]: Successful POST completed by StupidGroups.app") 79 | NSLog(response?.description ?? "nil") 80 | } else { 81 | // Bad Response from API 82 | stringToReturn = String(decoding: data!, as: UTF8.self) 83 | NSLog("[ERROR ]: Failed POST completed by StupidGroups.app") 84 | NSLog(response?.description ?? "nil") 85 | } 86 | semaphore.signal() 87 | } 88 | 89 | if error != nil { 90 | NSLog("[FATAL ]: " + error!.localizedDescription) 91 | stringToReturn = String("[FATAL ]: " + error!.localizedDescription) 92 | semaphore.signal() 93 | } 94 | }) 95 | task.resume() // Kick off the actual GET here 96 | semaphore.wait() 97 | return stringToReturn 98 | } 99 | 100 | 101 | 102 | 103 | } 104 | -------------------------------------------------------------------------------- /Stupid Groups/prepareData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // xmlBuilder.swift 3 | // Stupid Groups 4 | // 5 | // Created by Michael Levenick on 1/21/19. 6 | // Copyright © 2019 Michael Levenick. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | 12 | public class prepareData { 13 | 14 | // This function returns the data neededfor the API call. It returns XML data as well as endpoint 15 | // and returns it in an array to be parsed later. Technically, I believe the "singular" one is not needed 16 | // but I am leaving it in, in case it is needed down the road. 17 | public func deviceData(deviceType: String, conversionType: String) -> Array { 18 | var xmlData = ["nil","nil","nil","nil"] 19 | 20 | // Logic block to determine what data to send back: 21 | // Mobile Devices 22 | if deviceType == "Mobile Device" { 23 | if conversionType == "Advanced Search" { 24 | xmlData = ["advanced_mobile_device_search","mobile_devices","mobile_device","advancedmobiledevicesearches"] 25 | } 26 | if conversionType == "Static Group" { 27 | xmlData = ["mobile_device_group","mobile_devices","mobile_device","mobiledevicegroups"] 28 | } 29 | 30 | // Computers 31 | } else if deviceType == "Computer" { 32 | if conversionType == "Advanced Search" { 33 | xmlData = ["advanced_computer_search","computers","computer","advancedcomputersearches"] 34 | } 35 | if conversionType == "Static Group" { 36 | xmlData = ["computer_group","computers","computer","computergroups"] 37 | } 38 | 39 | // Users 40 | } else { 41 | if conversionType == "Advanced Search" { 42 | xmlData = ["advanced_user_search","users","user","advancedusersearches"] 43 | } 44 | if conversionType == "Static Group" { 45 | xmlData = ["user_group","users","user","usergroups"] 46 | } 47 | } 48 | return xmlData 49 | } 50 | 51 | // Generate the XML which will be sent to the API to generate the new object 52 | // Pass in all data, and it will determine what is needed based on 53 | // the conversion type 54 | public func xmlToPost(newName: String, siteID: String, criteria: String, membership: String, conversionType: String, deviceRoot: String, devicePlural: String, deviceSingular: String) -> Data { 55 | 56 | var newXMLString = "nil" 57 | var displayFields = "nil" 58 | 59 | // By default, new advanced searches made in Jamf Pro will only display 60 | // the name of the object. These XML blocks are to add additional display 61 | // fields to make the advanced searches more helpful by default. 62 | if devicePlural == "users" { 63 | displayFields = """ 64 | 7 65 | 66 | Computers 67 | 68 | 69 | Email Address 70 | 71 | 72 | Full Name 73 | 74 | 75 | Mobile Devices 76 | 77 | 78 | Phone Number 79 | 80 | 81 | Position 82 | 83 | 84 | Username 85 | 86 | """ 87 | } 88 | 89 | if devicePlural == "mobile_devices" { 90 | displayFields = """ 91 | 5 92 | 93 | Asset Tag 94 | 95 | 96 | Device ID 97 | 98 | 99 | Display Name 100 | 101 | 102 | Serial Number 103 | 104 | 105 | Username 106 | 107 | """ 108 | } 109 | 110 | if devicePlural == "computers" { 111 | displayFields = """ 112 | 5 113 | 114 | Asset Tag 115 | 116 | 117 | Computer Name 118 | 119 | 120 | JSS Computer ID 121 | 122 | 123 | Serial Number 124 | 125 | 126 | Username 127 | 128 | """ 129 | } 130 | 131 | // Build XML for an Advanced Search conversion 132 | if conversionType == "Advanced Search" { 133 | newXMLString = """ 134 | <\(deviceRoot)> 135 | \(newName) 136 | \(siteID) 137 | \(criteria) 138 | \(displayFields) 139 | 140 | """ 141 | } 142 | 143 | // Build XML for a Static Group conversion 144 | if conversionType == "Static Group" { 145 | newXMLString = """ 146 | <\(deviceRoot)> 147 | \(newName) 148 | \(siteID) 149 | false 150 | <\(devicePlural)>\(membership) 151 | 152 | """ 153 | } 154 | 155 | // Convert the XML string to a data type and return it 156 | let myData: Data? = newXMLString.data(using: .utf8) 157 | return myData! 158 | } 159 | 160 | // Leslie Helou wrote this sweet little function. It parses XML to gather 161 | // a substring between two specified tags. 162 | func parseXML(fullXMLString:String, startTag:String, endTag:String) -> String { 163 | var rawValue = "" 164 | if let start = fullXMLString.range(of: startTag), 165 | let end = fullXMLString.range(of: endTag, range: start.upperBound.. URL { 177 | let stringURL = "\(url)activationcode" 178 | let encodedURL = NSURL(string: stringURL) 179 | NSLog("[INFO ]: AUTH URL CREATED: " + stringURL) 180 | return encodedURL! as URL 181 | } 182 | 183 | // Create the URL that is used to gather the information from an existing smart group 184 | public func createGETURL(url: String, deviceType: String, id: String) -> URL { 185 | var endpoint = "none" 186 | if deviceType == "Mobile Device" { 187 | endpoint = "mobiledevicegroups" 188 | } else if deviceType == "Computer" { 189 | endpoint = "computergroups" 190 | } else { 191 | endpoint = "usergroups" 192 | } 193 | let stringURL = "\(url)\(endpoint)/id/\(id)" 194 | let encodedURL = NSURL(string: stringURL) 195 | NSLog("[INFO ]: GET URL CREATED: " + stringURL) 196 | return encodedURL! as URL 197 | } 198 | 199 | // Create the URL that is used to create the new group or advanced search 200 | public func createPOSTURL(url: String, endpoint: String) -> URL { 201 | let stringURL = "\(url)\(endpoint)/id/0" 202 | let encodedURL = NSURL(string: stringURL) 203 | NSLog("[INFO ]: POST URL CREATED: " + stringURL) 204 | return encodedURL! as URL 205 | } 206 | } 207 | 208 | -------------------------------------------------------------------------------- /Stupid Groups/loginWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // loginWindow.swift 3 | // Stupid Groups 4 | // 5 | // Created by Michael Levenick on 1/21/19. 6 | // Copyright © 2019 Michael Levenick. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | // This delegate is required to pass the base64 credentials and URL to the main view 13 | protocol DataSentDelegate { 14 | func userDidAuthenticate(base64Credentials: String, url: String) 15 | } 16 | 17 | class loginWindow: NSViewController, URLSessionDelegate { 18 | 19 | // Set up defaults and a delegate used for credential/url passing 20 | let loginDefaults = UserDefaults.standard 21 | var delegateAuth: DataSentDelegate? = nil 22 | 23 | // Declare outlets used on the login screen 24 | @IBOutlet weak var txtURLOutlet: NSTextField! 25 | @IBOutlet weak var txtUserOutlet: NSTextField! 26 | @IBOutlet weak var txtPassOutlet: NSSecureTextField! 27 | @IBOutlet weak var spinProgress: NSProgressIndicator! 28 | @IBOutlet weak var btnSubmitOutlet: NSButton! 29 | @IBOutlet weak var chkRememberMe: NSButton! 30 | @IBOutlet weak var chkBypass: NSButton! 31 | 32 | var doNotRun: String! 33 | var serverURL: String! 34 | var base64Credentials: String! 35 | var verified = false 36 | 37 | // This punctuation variable is used for cleaning thngs up below 38 | let punctuation = CharacterSet(charactersIn: ".:/") 39 | 40 | override func viewDidLoad() { 41 | super.viewDidLoad() 42 | 43 | // Restore the Username to text box if we have a default stored 44 | if loginDefaults.value(forKey: "UserName") != nil { 45 | txtUserOutlet.stringValue = loginDefaults.value(forKey: "UserName") as! String 46 | } 47 | 48 | // Restore Prem URL to text box if we have a default stored 49 | if loginDefaults.value(forKey: "InstanceURL") != nil { 50 | txtURLOutlet.stringValue = loginDefaults.value(forKey: "InstanceURL") as! String 51 | } 52 | 53 | if ( loginDefaults.value(forKey: "InstanceURL") != nil || loginDefaults.value(forKey: "InstanceURL") != nil ) && loginDefaults.value(forKey: "UserName") != nil { 54 | if self.txtPassOutlet.acceptsFirstResponder == true { 55 | self.txtPassOutlet.becomeFirstResponder() 56 | } 57 | } 58 | } 59 | 60 | override func viewDidAppear() { 61 | super.viewDidAppear() 62 | preferredContentSize = NSSize(width: 383, height: 420) // Limits resizing of the window 63 | // If we have a URL and a User stored focus the password field 64 | if loginDefaults.value(forKey: "InstanceURL") != nil && loginDefaults.value(forKey: "UserName") != nil { 65 | self.txtPassOutlet.becomeFirstResponder() 66 | } 67 | } 68 | 69 | @IBAction func btnSubmit(_ sender: Any) { 70 | 71 | // Clean up extraneous whitespace characters 72 | txtURLOutlet.stringValue = txtURLOutlet.stringValue.trimmingCharacters(in: CharacterSet.whitespaces) 73 | txtUserOutlet.stringValue = txtUserOutlet.stringValue.trimmingCharacters(in: CharacterSet.whitespaces) 74 | txtPassOutlet.stringValue = txtPassOutlet.stringValue.trimmingCharacters(in: CharacterSet.whitespaces) 75 | 76 | // Warn the user if they have failed to enter an instancename or prem URL 77 | if txtURLOutlet.stringValue == "" { 78 | _ = popPrompt().generalWarning(question: "No Server Info", text: "It appears that you have not entered any information for your Jamf Pro URL. Please enter either a Jamf Cloud instance name, or your full URL if you host your own server.") 79 | NSLog("[ERROR ]: No server info was entered. Setting doNotRun to 1") 80 | doNotRun = "1" // Set Do Not Run flag 81 | } 82 | 83 | // Warn the user if they have failed to enter a username 84 | if txtUserOutlet.stringValue == "" { 85 | _ = popPrompt().generalWarning(question: "No Username Found", text: "It appears that you have not entered a username for Stupid Groups to use while accessing Jamf Pro. Please enter your username and password, and try again.") 86 | NSLog("[ERROR ]: No user info was entered. Setting doNotRun to 1") 87 | doNotRun = "1" // Set Do Not Run flag 88 | } 89 | 90 | // Warn the user if they have failed to enter a password 91 | if txtPassOutlet.stringValue == "" { 92 | _ = popPrompt().generalWarning(question: "No Password Found", text: "It appears that you have not entered a password for Stupid Groups to use while accessing Jamf Pro. Please enter your username and password, and try again.") 93 | NSLog("[ERROR ]: No password info was entered. Setting doNotRun to 1") 94 | doNotRun = "1" // Set Do Not Run flag 95 | } 96 | 97 | // Move forward with verification if we have not flagged the doNotRun flag 98 | if doNotRun != "1" { 99 | 100 | // Create the API-Friendly Jamf Pro URL with resource appended, cleaning up double slashes 101 | if txtURLOutlet.stringValue.rangeOfCharacter(from: punctuation) == nil { 102 | serverURL = "https://\(txtURLOutlet.stringValue).jamfcloud.com/JSSResource/" 103 | } else { 104 | serverURL = "\(txtURLOutlet.stringValue)/JSSResource/" 105 | serverURL = serverURL.replacingOccurrences(of: "//JSSResource", with: "/JSSResource") 106 | } 107 | 108 | btnSubmitOutlet.isHidden = true 109 | spinProgress.startAnimation(self) 110 | 111 | // Concatenate the credentials and base64 encode the resulting string 112 | let concatCredentials = "\(txtUserOutlet.stringValue):\(txtPassOutlet.stringValue)" 113 | let utf8Credentials = concatCredentials.data(using: String.Encoding.utf8) 114 | base64Credentials = utf8Credentials?.base64EncodedString() 115 | 116 | // MARK - Credential Verification API Call 117 | let testURL = prepareData().createAuthURL(url: self.serverURL!) 118 | let authResponse = API().get(getCredentials: self.base64Credentials!, getURL: testURL) 119 | print(authResponse) 120 | 121 | // Look for the activation_code tag in the response. This works far better 122 | // than looking for a 200 response, because Jamf Now instances do not have 123 | // an API, but if you run a GET to them, you will always get a 200 response. 124 | // This causes issues with MUT, and I plan to implement this same code there as well. 125 | if authResponse.contains("") { 126 | NSLog("[INFO ]: Successful authentication attempt.") 127 | self.verified = true 128 | // Store username if remember me is checked 129 | if self.chkRememberMe.state.rawValue == 1 { 130 | self.loginDefaults.set(self.txtUserOutlet.stringValue, forKey: "UserName") 131 | self.loginDefaults.set(self.txtURLOutlet.stringValue, forKey: "InstanceURL") 132 | self.loginDefaults.set(true, forKey: "Remember") 133 | self.loginDefaults.synchronize() 134 | 135 | // Dump the stored defaults if no remember me is checked 136 | } else { 137 | self.loginDefaults.removeObject(forKey: "UserName") 138 | self.loginDefaults.removeObject(forKey: "InstanceURL") 139 | self.loginDefaults.set(false, forKey: "Remember") 140 | self.loginDefaults.synchronize() 141 | } 142 | self.spinProgress.stopAnimation(self) 143 | self.btnSubmitOutlet.isHidden = false 144 | 145 | // Pass the information forward using the delgate and dismiss the login view 146 | if self.delegateAuth != nil { 147 | self.delegateAuth?.userDidAuthenticate(base64Credentials: self.base64Credentials!, url: self.serverURL!) 148 | self.dismiss(self) 149 | } 150 | } else if authResponse.contains("[FATAL ]:"){ 151 | // Display an error message if there was a fatal http error 152 | DispatchQueue.main.async { 153 | self.spinProgress.stopAnimation(self) 154 | self.btnSubmitOutlet.isHidden = false 155 | if authResponse.contains("SSL error") { 156 | _ = popPrompt().generalWarning(question: "Fatal Error", text: "There was a fatal error upon authentication attempt. The error is: " + authResponse + "\n\nIf you are using a self-signed or built-in SSL certificate, try adding the certificate to your keychain, trusting it, and trying again.") 157 | } else { 158 | _ = popPrompt().generalWarning(question: "Fatal Error", text: "There was a fatal error upon authentication attempt. The error is: " + authResponse) 159 | } 160 | 161 | NSLog("[INFO ]: Invalid authentication attempt.") 162 | 163 | // Pass forward credentials and dismiss view if the "bypass authentication" 164 | // checkbox is checked. This is used in security-conscious organizations 165 | // where some admins have minimal permissions, and cannot GET the activation code 166 | if self.chkBypass.state.rawValue == 1 { 167 | if self.delegateAuth != nil { 168 | self.delegateAuth?.userDidAuthenticate(base64Credentials: self.base64Credentials!, url: self.serverURL!) 169 | self.dismiss(self) 170 | } 171 | self.verified = true 172 | } 173 | } 174 | } else { 175 | 176 | // Display an error message if there is no activation_code tag found 177 | DispatchQueue.main.async { 178 | self.spinProgress.stopAnimation(self) 179 | self.btnSubmitOutlet.isHidden = false 180 | _ = popPrompt().generalWarning(question: "Invalid Credentials", text: "The credentials you entered do not seem to have sufficient permissions. This could be due to an incorrect user/password, or possibly from insufficient permissions. Stupid Groups tests this against the user's ability to view the Activation Code via the API.") 181 | NSLog("[INFO ]: Invalid authentication attempt.") 182 | 183 | // Pass forward credentials and dismiss view if the "bypass authentication" 184 | // checkbox is checked. This is used in security-conscious organizations 185 | // where some admins have minimal permissions, and cannot GET the activation code 186 | if self.chkBypass.state.rawValue == 1 { 187 | if self.delegateAuth != nil { 188 | self.delegateAuth?.userDidAuthenticate(base64Credentials: self.base64Credentials!, url: self.serverURL!) 189 | self.dismiss(self) 190 | } 191 | self.verified = true 192 | } 193 | } 194 | } 195 | } else { 196 | // Reset the Do Not Run flag so that on subsequent runs we try the checks again. 197 | doNotRun = "0" 198 | } 199 | } 200 | 201 | // This is required to allow un-trusted SSL certificates. Leave it alone. 202 | func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 203 | completionHandler(Foundation.URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) 204 | } 205 | 206 | // This is added because it is actually the only way to quit the app with the sheet view 207 | // down over the main view controller. 208 | @IBAction func btnQuit(_ sender: Any) { 209 | self.dismiss(self) 210 | NSApplication.shared.terminate(self) 211 | } 212 | // Clear stored values such as username and URL 213 | @IBAction func btnClearStored(_ sender: AnyObject) { 214 | //Clear all stored values 215 | txtURLOutlet.stringValue = "" 216 | txtUserOutlet.stringValue = "" 217 | txtPassOutlet.stringValue = "" 218 | if let bundle = Bundle.main.bundleIdentifier { 219 | UserDefaults.standard.removePersistentDomain(forName: bundle) 220 | } 221 | } 222 | } 223 | 224 | -------------------------------------------------------------------------------- /Stupid Groups/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Stupid Groups 4 | // 5 | // Created by Michael Levenick on 1/21/19. 6 | // Copyright © 2019 Michael Levenick. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ViewController: NSViewController, URLSessionDelegate, DataSentDelegate { 12 | 13 | // Declare Variables 14 | // Many of these are declared globally so they can be easily passed from function to function 15 | var globalServerURL: String! 16 | var globalServerCredentials: String! 17 | var base64Credentials: String! 18 | var serverURL: String! 19 | var verified = false 20 | var globalHTTPFunction: String! 21 | var myURL: URL! 22 | var globalDebug = "off" 23 | var smartGroupCriteria: String! 24 | var smartGroupName: String! 25 | var newName: String! 26 | var siteID: String! 27 | var smartGroupMembership: String! 28 | var globalSmartGroupXML: String! 29 | @IBOutlet weak var txtPrefix: NSTextField! 30 | 31 | 32 | // Declare outlets for use in the view 33 | @IBOutlet weak var txtGroupID: NSTextField! 34 | @IBOutlet weak var popConvertTo: NSPopUpButton! 35 | @IBOutlet weak var popDeviceType: NSPopUpButton! 36 | @IBOutlet weak var btnPostOutlet: NSButton! 37 | @IBOutlet weak var btnGetOutlet: NSButton! 38 | @IBOutlet weak var txtMainWrapper: NSScrollView! 39 | @IBOutlet var txtMain: NSTextView! 40 | 41 | // Prepare the segue for the sheet view of the login window to appear 42 | override func prepare(for segue: NSStoryboardSegue, sender: Any?) { 43 | if segue.identifier == "segueLogin" { 44 | let loginWindow: loginWindow = segue.destinationController as! loginWindow 45 | loginWindow.delegateAuth = self 46 | } 47 | } 48 | 49 | // Print some welcome messaging upon loading the view 50 | override func viewDidLoad() { 51 | super.viewDidLoad() 52 | preferredContentSize = NSSize(width: 383, height: 420) // Limits resizing of the window 53 | printString(header: true, error: false, green: false, fixedPoint: false, lineBreakAfter: true, message: "Welcome to Stupid Groups v1.1") 54 | printString(header: false, error: false, green: false, fixedPoint: false, lineBreakAfter: true, message: "\nSometimes your groups get too smart.\n\nStupid Groups is here to help.\n\nConvert groups that rarely change membership to Static Groups, and convert compliance reporting groups that aren't used for scoping to Advanced Searches.\n\nEnter your data above and run a Pre-Flight Check to begin.\n") 55 | } 56 | 57 | // Trigger the actual sheet segue upon the view fully appearing 58 | // It seems to work better here than in viewDidLoad(). 59 | override func viewWillAppear() { 60 | super.viewWillAppear() 61 | performSegue(withIdentifier: "segueLogin", sender: self) 62 | } 63 | 64 | // I'm relatively certain this is not needed, but I will leave it in and commented for now. 65 | /* 66 | override var representedObject: Any? { 67 | didSet { 68 | // Update the view, if already loaded. 69 | } 70 | } 71 | */ 72 | 73 | // This is the "Pre-Flight Check" button. 74 | @IBAction func btnGET(_ sender: Any) { 75 | // Clear the box on the main view controller, and then print some information. 76 | clearLog() 77 | printString(header: false, error: false, green: false, fixedPoint: true, lineBreakAfter: true, message: "Gathering data about \(popDeviceType.titleOfSelectedItem!) group number \(txtGroupID.stringValue)...\n") 78 | NSLog("[INFO ]: Starting GET function.") 79 | // Prepare a URL to use for the GET call, based on device type and ID 80 | let getURL = prepareData().createGETURL(url: globalServerURL, deviceType: self.popDeviceType.titleOfSelectedItem!, id: self.txtGroupID.stringValue) 81 | 82 | // Pass the URL and credentials into the function to get the response XML back 83 | let smartGroupXML = API().get(getCredentials: globalServerCredentials, getURL: getURL) 84 | 85 | // I opted to parse the returned data, and look for a tag instead of using the 86 | // response code, as I have noticed the response code is not always reliable when 87 | // working with MUT. 88 | if smartGroupXML.contains(""){ 89 | let deviceData = prepareData().deviceData(deviceType: self.popDeviceType.titleOfSelectedItem!, conversionType: self.popConvertTo.titleOfSelectedItem!) 90 | 91 | // Parse the response XML to gather data needed for concatenation 92 | self.smartGroupCriteria = prepareData().parseXML(fullXMLString: smartGroupXML, startTag: "criteria>", endTag: "", endTag: "", endTag: "", endTag: ""){ 119 | DispatchQueue.main.async { 120 | self.clearLog() 121 | let newID = prepareData().parseXML(fullXMLString: postResponse, startTag: "id>", endTag: " Void) { 153 | completionHandler(Foundation.URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) 154 | } 155 | 156 | // Set the view to "ready to run" state 157 | func readyToRun() { 158 | btnPostOutlet.isHidden = false 159 | btnGetOutlet.isHidden = true 160 | } 161 | 162 | // Set the view to require another Pre-Flight Check 163 | func notReadyToRun() { 164 | btnGetOutlet.isHidden = false 165 | btnPostOutlet.isHidden = true 166 | } 167 | 168 | // This function will append text to the primary log block on the 169 | // main view controller. You can call this function to append or print 170 | // text to the log box, and select various formats depending on your use. 171 | // The bool selectors all overrule each other from left to right 172 | // for example, if you select TRUE for header, it will ignore "error" "green" and "fixed point" 173 | // Additional line breaks can be added by including \n in the message string 174 | 175 | func printString(header: Bool, error: Bool, green: Bool, fixedPoint: Bool, lineBreakAfter: Bool, message: String) { 176 | var stringToPrint = "" 177 | if lineBreakAfter { 178 | stringToPrint = message + "\n" 179 | } else { 180 | stringToPrint = message 181 | } 182 | if header { 183 | self.txtMain.textStorage?.append(NSAttributedString(string: "\(stringToPrint)", attributes: self.myHeaderAttribute)) 184 | } else if error { 185 | self.txtMain.textStorage?.append(NSAttributedString(string: "\(stringToPrint)", attributes: self.myFailFontAttribute)) 186 | } else if green { 187 | self.txtMain.textStorage?.append(NSAttributedString(string: "\(stringToPrint)", attributes: self.myOKFontAttribute)) 188 | } else if fixedPoint { 189 | self.txtMain.textStorage?.append(NSAttributedString(string: "\(stringToPrint)", attributes: self.myCSVFontAttribute)) 190 | } else { 191 | self.txtMain.textStorage?.append(NSAttributedString(string: "\(stringToPrint)", attributes: self.myFontAttribute)) 192 | } 193 | self.txtMain.scrollToEndOfDocument(self) 194 | } 195 | // Clears the entire logging text field 196 | func clearLog() { 197 | self.txtMain.textStorage?.setAttributedString(NSAttributedString(string: "", attributes: self.myFontAttribute)) 198 | } 199 | 200 | // Declare format for various output fonts for the end user to see. 201 | // These are the font formats called by the printString function. 202 | let myFontAttribute = [ NSAttributedString.Key.font: NSFont(name: "Helvetica Neue Thin", size: 14.0)! ] 203 | let myHeaderAttribute = [ NSAttributedString.Key.font: NSFont(name: "Helvetica Neue Thin", size: 20.0)! ] 204 | let myOKFontAttribute = [ 205 | NSAttributedString.Key.font: NSFont(name: "Helvetica Neue Thin", size: 14.0)!, 206 | NSAttributedString.Key.foregroundColor: NSColor(deviceRed: 0.0, green: 0.8, blue: 0.0, alpha: 1.0) 207 | ] 208 | let myFailFontAttribute = [ 209 | NSAttributedString.Key.font: NSFont(name: "Helvetica Neue Thin", size: 14.0)!, 210 | NSAttributedString.Key.foregroundColor: NSColor(deviceRed: 0.8, green: 0.0, blue: 0.0, alpha: 1.0) 211 | ] 212 | let myCSVFontAttribute = [ NSAttributedString.Key.font: NSFont(name: "Helvetica Neue Thin", size: 14.0)! ] 213 | let myAlertFontAttribute = [ 214 | NSAttributedString.Key.font: NSFont(name: "Helvetica Neue Thin", size: 14.0)!, 215 | NSAttributedString.Key.foregroundColor: NSColor(deviceRed: 0.8, green: 0.0, blue: 0.0, alpha: 1.0) 216 | ] 217 | 218 | // These actions are to reset the pre-flight button with the notreadytorun() function 219 | // If something changes such as group ID or group type/target type 220 | @IBAction func popGroupType(_ sender: Any) { 221 | notReadyToRun() 222 | } 223 | @IBAction func popConvertTo(_ sender: Any) { 224 | notReadyToRun() 225 | } 226 | @IBAction func txtIDAction(_ sender: Any) { 227 | notReadyToRun() 228 | } 229 | @IBAction func txtPrefixAction(_ sender: Any) { 230 | notReadyToRun() 231 | } 232 | 233 | } 234 | -------------------------------------------------------------------------------- /Stupid Groups.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F77F8D2921F9FA1F005C33EE /* apiFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77F8D2821F9FA1F005C33EE /* apiFunctions.swift */; }; 11 | F7EAD10921F61129001F8492 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7EAD10821F61129001F8492 /* AppDelegate.swift */; }; 12 | F7EAD10B21F61129001F8492 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7EAD10A21F61129001F8492 /* ViewController.swift */; }; 13 | F7EAD10D21F6112A001F8492 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F7EAD10C21F6112A001F8492 /* Assets.xcassets */; }; 14 | F7EAD11021F6112B001F8492 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7EAD10E21F6112B001F8492 /* Main.storyboard */; }; 15 | F7EAD11C21F6112B001F8492 /* Stupid_GroupsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7EAD11B21F6112B001F8492 /* Stupid_GroupsTests.swift */; }; 16 | F7EAD12721F6112B001F8492 /* Stupid_GroupsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7EAD12621F6112B001F8492 /* Stupid_GroupsUITests.swift */; }; 17 | F7EAD13721F6351C001F8492 /* popPrompt.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7EAD13621F6351C001F8492 /* popPrompt.swift */; }; 18 | F7EAD13921F63703001F8492 /* loginWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7EAD13821F63703001F8492 /* loginWindow.swift */; }; 19 | F7EAD13B21F64BFD001F8492 /* prepareData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7EAD13A21F64BFD001F8492 /* prepareData.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXContainerItemProxy section */ 23 | F7EAD11821F6112B001F8492 /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = F7EAD0FD21F61129001F8492 /* Project object */; 26 | proxyType = 1; 27 | remoteGlobalIDString = F7EAD10421F61129001F8492; 28 | remoteInfo = "Stupid Groups"; 29 | }; 30 | F7EAD12321F6112B001F8492 /* PBXContainerItemProxy */ = { 31 | isa = PBXContainerItemProxy; 32 | containerPortal = F7EAD0FD21F61129001F8492 /* Project object */; 33 | proxyType = 1; 34 | remoteGlobalIDString = F7EAD10421F61129001F8492; 35 | remoteInfo = "Stupid Groups"; 36 | }; 37 | /* End PBXContainerItemProxy section */ 38 | 39 | /* Begin PBXFileReference section */ 40 | F77F8D2821F9FA1F005C33EE /* apiFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = apiFunctions.swift; sourceTree = ""; }; 41 | F7EAD10521F61129001F8492 /* Stupid Groups.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Stupid Groups.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | F7EAD10821F61129001F8492 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 43 | F7EAD10A21F61129001F8492 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 44 | F7EAD10C21F6112A001F8492 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 45 | F7EAD10F21F6112B001F8492 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 46 | F7EAD11121F6112B001F8492 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 47 | F7EAD11221F6112B001F8492 /* Stupid_Groups.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Stupid_Groups.entitlements; sourceTree = ""; }; 48 | F7EAD11721F6112B001F8492 /* Stupid GroupsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Stupid GroupsTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | F7EAD11B21F6112B001F8492 /* Stupid_GroupsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stupid_GroupsTests.swift; sourceTree = ""; }; 50 | F7EAD11D21F6112B001F8492 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | F7EAD12221F6112B001F8492 /* Stupid GroupsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Stupid GroupsUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | F7EAD12621F6112B001F8492 /* Stupid_GroupsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stupid_GroupsUITests.swift; sourceTree = ""; }; 53 | F7EAD12821F6112B001F8492 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 54 | F7EAD13621F6351C001F8492 /* popPrompt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = popPrompt.swift; sourceTree = ""; }; 55 | F7EAD13821F63703001F8492 /* loginWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = loginWindow.swift; sourceTree = ""; }; 56 | F7EAD13A21F64BFD001F8492 /* prepareData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = prepareData.swift; sourceTree = ""; }; 57 | F7EAD13C21F64D28001F8492 /* Stupid Groups.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Stupid Groups.entitlements"; sourceTree = ""; }; 58 | /* End PBXFileReference section */ 59 | 60 | /* Begin PBXFrameworksBuildPhase section */ 61 | F7EAD10221F61129001F8492 /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | ); 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | F7EAD11421F6112B001F8492 /* Frameworks */ = { 69 | isa = PBXFrameworksBuildPhase; 70 | buildActionMask = 2147483647; 71 | files = ( 72 | ); 73 | runOnlyForDeploymentPostprocessing = 0; 74 | }; 75 | F7EAD11F21F6112B001F8492 /* Frameworks */ = { 76 | isa = PBXFrameworksBuildPhase; 77 | buildActionMask = 2147483647; 78 | files = ( 79 | ); 80 | runOnlyForDeploymentPostprocessing = 0; 81 | }; 82 | /* End PBXFrameworksBuildPhase section */ 83 | 84 | /* Begin PBXGroup section */ 85 | 3CAE7773ED3827AE9430ECF8 /* Frameworks */ = { 86 | isa = PBXGroup; 87 | children = ( 88 | ); 89 | name = Frameworks; 90 | sourceTree = ""; 91 | }; 92 | F7EAD0FC21F61129001F8492 = { 93 | isa = PBXGroup; 94 | children = ( 95 | F7EAD10721F61129001F8492 /* Stupid Groups */, 96 | F7EAD11A21F6112B001F8492 /* Stupid GroupsTests */, 97 | F7EAD12521F6112B001F8492 /* Stupid GroupsUITests */, 98 | F7EAD10621F61129001F8492 /* Products */, 99 | 3CAE7773ED3827AE9430ECF8 /* Frameworks */, 100 | ); 101 | sourceTree = ""; 102 | }; 103 | F7EAD10621F61129001F8492 /* Products */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | F7EAD10521F61129001F8492 /* Stupid Groups.app */, 107 | F7EAD11721F6112B001F8492 /* Stupid GroupsTests.xctest */, 108 | F7EAD12221F6112B001F8492 /* Stupid GroupsUITests.xctest */, 109 | ); 110 | name = Products; 111 | sourceTree = ""; 112 | }; 113 | F7EAD10721F61129001F8492 /* Stupid Groups */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | F7EAD13C21F64D28001F8492 /* Stupid Groups.entitlements */, 117 | F7EAD10821F61129001F8492 /* AppDelegate.swift */, 118 | F7EAD10A21F61129001F8492 /* ViewController.swift */, 119 | F77F8D2821F9FA1F005C33EE /* apiFunctions.swift */, 120 | F7EAD10C21F6112A001F8492 /* Assets.xcassets */, 121 | F7EAD10E21F6112B001F8492 /* Main.storyboard */, 122 | F7EAD13A21F64BFD001F8492 /* prepareData.swift */, 123 | F7EAD11121F6112B001F8492 /* Info.plist */, 124 | F7EAD11221F6112B001F8492 /* Stupid_Groups.entitlements */, 125 | F7EAD13621F6351C001F8492 /* popPrompt.swift */, 126 | F7EAD13821F63703001F8492 /* loginWindow.swift */, 127 | ); 128 | path = "Stupid Groups"; 129 | sourceTree = ""; 130 | }; 131 | F7EAD11A21F6112B001F8492 /* Stupid GroupsTests */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | F7EAD11B21F6112B001F8492 /* Stupid_GroupsTests.swift */, 135 | F7EAD11D21F6112B001F8492 /* Info.plist */, 136 | ); 137 | path = "Stupid GroupsTests"; 138 | sourceTree = ""; 139 | }; 140 | F7EAD12521F6112B001F8492 /* Stupid GroupsUITests */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | F7EAD12621F6112B001F8492 /* Stupid_GroupsUITests.swift */, 144 | F7EAD12821F6112B001F8492 /* Info.plist */, 145 | ); 146 | path = "Stupid GroupsUITests"; 147 | sourceTree = ""; 148 | }; 149 | /* End PBXGroup section */ 150 | 151 | /* Begin PBXNativeTarget section */ 152 | F7EAD10421F61129001F8492 /* Stupid Groups */ = { 153 | isa = PBXNativeTarget; 154 | buildConfigurationList = F7EAD12B21F6112B001F8492 /* Build configuration list for PBXNativeTarget "Stupid Groups" */; 155 | buildPhases = ( 156 | F7EAD10121F61129001F8492 /* Sources */, 157 | F7EAD10221F61129001F8492 /* Frameworks */, 158 | F7EAD10321F61129001F8492 /* Resources */, 159 | ); 160 | buildRules = ( 161 | ); 162 | dependencies = ( 163 | ); 164 | name = "Stupid Groups"; 165 | productName = "Stupid Groups"; 166 | productReference = F7EAD10521F61129001F8492 /* Stupid Groups.app */; 167 | productType = "com.apple.product-type.application"; 168 | }; 169 | F7EAD11621F6112B001F8492 /* Stupid GroupsTests */ = { 170 | isa = PBXNativeTarget; 171 | buildConfigurationList = F7EAD12E21F6112B001F8492 /* Build configuration list for PBXNativeTarget "Stupid GroupsTests" */; 172 | buildPhases = ( 173 | F7EAD11321F6112B001F8492 /* Sources */, 174 | F7EAD11421F6112B001F8492 /* Frameworks */, 175 | F7EAD11521F6112B001F8492 /* Resources */, 176 | ); 177 | buildRules = ( 178 | ); 179 | dependencies = ( 180 | F7EAD11921F6112B001F8492 /* PBXTargetDependency */, 181 | ); 182 | name = "Stupid GroupsTests"; 183 | productName = "Stupid GroupsTests"; 184 | productReference = F7EAD11721F6112B001F8492 /* Stupid GroupsTests.xctest */; 185 | productType = "com.apple.product-type.bundle.unit-test"; 186 | }; 187 | F7EAD12121F6112B001F8492 /* Stupid GroupsUITests */ = { 188 | isa = PBXNativeTarget; 189 | buildConfigurationList = F7EAD13121F6112B001F8492 /* Build configuration list for PBXNativeTarget "Stupid GroupsUITests" */; 190 | buildPhases = ( 191 | F7EAD11E21F6112B001F8492 /* Sources */, 192 | F7EAD11F21F6112B001F8492 /* Frameworks */, 193 | F7EAD12021F6112B001F8492 /* Resources */, 194 | ); 195 | buildRules = ( 196 | ); 197 | dependencies = ( 198 | F7EAD12421F6112B001F8492 /* PBXTargetDependency */, 199 | ); 200 | name = "Stupid GroupsUITests"; 201 | productName = "Stupid GroupsUITests"; 202 | productReference = F7EAD12221F6112B001F8492 /* Stupid GroupsUITests.xctest */; 203 | productType = "com.apple.product-type.bundle.ui-testing"; 204 | }; 205 | /* End PBXNativeTarget section */ 206 | 207 | /* Begin PBXProject section */ 208 | F7EAD0FD21F61129001F8492 /* Project object */ = { 209 | isa = PBXProject; 210 | attributes = { 211 | LastSwiftUpdateCheck = 1010; 212 | LastUpgradeCheck = 1010; 213 | ORGANIZATIONNAME = "Michael Levenick"; 214 | TargetAttributes = { 215 | F7EAD10421F61129001F8492 = { 216 | CreatedOnToolsVersion = 10.1; 217 | SystemCapabilities = { 218 | com.apple.Sandbox = { 219 | enabled = 1; 220 | }; 221 | }; 222 | }; 223 | F7EAD11621F6112B001F8492 = { 224 | CreatedOnToolsVersion = 10.1; 225 | TestTargetID = F7EAD10421F61129001F8492; 226 | }; 227 | F7EAD12121F6112B001F8492 = { 228 | CreatedOnToolsVersion = 10.1; 229 | TestTargetID = F7EAD10421F61129001F8492; 230 | }; 231 | }; 232 | }; 233 | buildConfigurationList = F7EAD10021F61129001F8492 /* Build configuration list for PBXProject "Stupid Groups" */; 234 | compatibilityVersion = "Xcode 9.3"; 235 | developmentRegion = en; 236 | hasScannedForEncodings = 0; 237 | knownRegions = ( 238 | en, 239 | Base, 240 | ); 241 | mainGroup = F7EAD0FC21F61129001F8492; 242 | productRefGroup = F7EAD10621F61129001F8492 /* Products */; 243 | projectDirPath = ""; 244 | projectRoot = ""; 245 | targets = ( 246 | F7EAD10421F61129001F8492 /* Stupid Groups */, 247 | F7EAD11621F6112B001F8492 /* Stupid GroupsTests */, 248 | F7EAD12121F6112B001F8492 /* Stupid GroupsUITests */, 249 | ); 250 | }; 251 | /* End PBXProject section */ 252 | 253 | /* Begin PBXResourcesBuildPhase section */ 254 | F7EAD10321F61129001F8492 /* Resources */ = { 255 | isa = PBXResourcesBuildPhase; 256 | buildActionMask = 2147483647; 257 | files = ( 258 | F7EAD10D21F6112A001F8492 /* Assets.xcassets in Resources */, 259 | F7EAD11021F6112B001F8492 /* Main.storyboard in Resources */, 260 | ); 261 | runOnlyForDeploymentPostprocessing = 0; 262 | }; 263 | F7EAD11521F6112B001F8492 /* Resources */ = { 264 | isa = PBXResourcesBuildPhase; 265 | buildActionMask = 2147483647; 266 | files = ( 267 | ); 268 | runOnlyForDeploymentPostprocessing = 0; 269 | }; 270 | F7EAD12021F6112B001F8492 /* Resources */ = { 271 | isa = PBXResourcesBuildPhase; 272 | buildActionMask = 2147483647; 273 | files = ( 274 | ); 275 | runOnlyForDeploymentPostprocessing = 0; 276 | }; 277 | /* End PBXResourcesBuildPhase section */ 278 | 279 | /* Begin PBXSourcesBuildPhase section */ 280 | F7EAD10121F61129001F8492 /* Sources */ = { 281 | isa = PBXSourcesBuildPhase; 282 | buildActionMask = 2147483647; 283 | files = ( 284 | F7EAD10B21F61129001F8492 /* ViewController.swift in Sources */, 285 | F7EAD13921F63703001F8492 /* loginWindow.swift in Sources */, 286 | F7EAD13B21F64BFD001F8492 /* prepareData.swift in Sources */, 287 | F77F8D2921F9FA1F005C33EE /* apiFunctions.swift in Sources */, 288 | F7EAD10921F61129001F8492 /* AppDelegate.swift in Sources */, 289 | F7EAD13721F6351C001F8492 /* popPrompt.swift in Sources */, 290 | ); 291 | runOnlyForDeploymentPostprocessing = 0; 292 | }; 293 | F7EAD11321F6112B001F8492 /* Sources */ = { 294 | isa = PBXSourcesBuildPhase; 295 | buildActionMask = 2147483647; 296 | files = ( 297 | F7EAD11C21F6112B001F8492 /* Stupid_GroupsTests.swift in Sources */, 298 | ); 299 | runOnlyForDeploymentPostprocessing = 0; 300 | }; 301 | F7EAD11E21F6112B001F8492 /* Sources */ = { 302 | isa = PBXSourcesBuildPhase; 303 | buildActionMask = 2147483647; 304 | files = ( 305 | F7EAD12721F6112B001F8492 /* Stupid_GroupsUITests.swift in Sources */, 306 | ); 307 | runOnlyForDeploymentPostprocessing = 0; 308 | }; 309 | /* End PBXSourcesBuildPhase section */ 310 | 311 | /* Begin PBXTargetDependency section */ 312 | F7EAD11921F6112B001F8492 /* PBXTargetDependency */ = { 313 | isa = PBXTargetDependency; 314 | target = F7EAD10421F61129001F8492 /* Stupid Groups */; 315 | targetProxy = F7EAD11821F6112B001F8492 /* PBXContainerItemProxy */; 316 | }; 317 | F7EAD12421F6112B001F8492 /* PBXTargetDependency */ = { 318 | isa = PBXTargetDependency; 319 | target = F7EAD10421F61129001F8492 /* Stupid Groups */; 320 | targetProxy = F7EAD12321F6112B001F8492 /* PBXContainerItemProxy */; 321 | }; 322 | /* End PBXTargetDependency section */ 323 | 324 | /* Begin PBXVariantGroup section */ 325 | F7EAD10E21F6112B001F8492 /* Main.storyboard */ = { 326 | isa = PBXVariantGroup; 327 | children = ( 328 | F7EAD10F21F6112B001F8492 /* Base */, 329 | ); 330 | name = Main.storyboard; 331 | sourceTree = ""; 332 | }; 333 | /* End PBXVariantGroup section */ 334 | 335 | /* Begin XCBuildConfiguration section */ 336 | F7EAD12921F6112B001F8492 /* Debug */ = { 337 | isa = XCBuildConfiguration; 338 | buildSettings = { 339 | ALWAYS_SEARCH_USER_PATHS = NO; 340 | CLANG_ANALYZER_NONNULL = YES; 341 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 342 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 343 | CLANG_CXX_LIBRARY = "libc++"; 344 | CLANG_ENABLE_MODULES = YES; 345 | CLANG_ENABLE_OBJC_ARC = YES; 346 | CLANG_ENABLE_OBJC_WEAK = YES; 347 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 348 | CLANG_WARN_BOOL_CONVERSION = YES; 349 | CLANG_WARN_COMMA = YES; 350 | CLANG_WARN_CONSTANT_CONVERSION = YES; 351 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 352 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 353 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 354 | CLANG_WARN_EMPTY_BODY = YES; 355 | CLANG_WARN_ENUM_CONVERSION = YES; 356 | CLANG_WARN_INFINITE_RECURSION = YES; 357 | CLANG_WARN_INT_CONVERSION = YES; 358 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 359 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 360 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 361 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 362 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 363 | CLANG_WARN_STRICT_PROTOTYPES = YES; 364 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 365 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 366 | CLANG_WARN_UNREACHABLE_CODE = YES; 367 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 368 | CODE_SIGN_IDENTITY = "Mac Developer"; 369 | COPY_PHASE_STRIP = NO; 370 | DEBUG_INFORMATION_FORMAT = dwarf; 371 | ENABLE_STRICT_OBJC_MSGSEND = YES; 372 | ENABLE_TESTABILITY = YES; 373 | GCC_C_LANGUAGE_STANDARD = gnu11; 374 | GCC_DYNAMIC_NO_PIC = NO; 375 | GCC_NO_COMMON_BLOCKS = YES; 376 | GCC_OPTIMIZATION_LEVEL = 0; 377 | GCC_PREPROCESSOR_DEFINITIONS = ( 378 | "DEBUG=1", 379 | "$(inherited)", 380 | ); 381 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 382 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 383 | GCC_WARN_UNDECLARED_SELECTOR = YES; 384 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 385 | GCC_WARN_UNUSED_FUNCTION = YES; 386 | GCC_WARN_UNUSED_VARIABLE = YES; 387 | MACOSX_DEPLOYMENT_TARGET = 10.14; 388 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 389 | MTL_FAST_MATH = YES; 390 | ONLY_ACTIVE_ARCH = YES; 391 | SDKROOT = macosx; 392 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 393 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 394 | }; 395 | name = Debug; 396 | }; 397 | F7EAD12A21F6112B001F8492 /* Release */ = { 398 | isa = XCBuildConfiguration; 399 | buildSettings = { 400 | ALWAYS_SEARCH_USER_PATHS = NO; 401 | CLANG_ANALYZER_NONNULL = YES; 402 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 403 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 404 | CLANG_CXX_LIBRARY = "libc++"; 405 | CLANG_ENABLE_MODULES = YES; 406 | CLANG_ENABLE_OBJC_ARC = YES; 407 | CLANG_ENABLE_OBJC_WEAK = YES; 408 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 409 | CLANG_WARN_BOOL_CONVERSION = YES; 410 | CLANG_WARN_COMMA = YES; 411 | CLANG_WARN_CONSTANT_CONVERSION = YES; 412 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 413 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 414 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 415 | CLANG_WARN_EMPTY_BODY = YES; 416 | CLANG_WARN_ENUM_CONVERSION = YES; 417 | CLANG_WARN_INFINITE_RECURSION = YES; 418 | CLANG_WARN_INT_CONVERSION = YES; 419 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 420 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 421 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 422 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 423 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 424 | CLANG_WARN_STRICT_PROTOTYPES = YES; 425 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 426 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 427 | CLANG_WARN_UNREACHABLE_CODE = YES; 428 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 429 | CODE_SIGN_IDENTITY = "Mac Developer"; 430 | COPY_PHASE_STRIP = NO; 431 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 432 | ENABLE_NS_ASSERTIONS = NO; 433 | ENABLE_STRICT_OBJC_MSGSEND = YES; 434 | GCC_C_LANGUAGE_STANDARD = gnu11; 435 | GCC_NO_COMMON_BLOCKS = YES; 436 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 437 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 438 | GCC_WARN_UNDECLARED_SELECTOR = YES; 439 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 440 | GCC_WARN_UNUSED_FUNCTION = YES; 441 | GCC_WARN_UNUSED_VARIABLE = YES; 442 | MACOSX_DEPLOYMENT_TARGET = 10.14; 443 | MTL_ENABLE_DEBUG_INFO = NO; 444 | MTL_FAST_MATH = YES; 445 | SDKROOT = macosx; 446 | SWIFT_COMPILATION_MODE = wholemodule; 447 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 448 | }; 449 | name = Release; 450 | }; 451 | F7EAD12C21F6112B001F8492 /* Debug */ = { 452 | isa = XCBuildConfiguration; 453 | buildSettings = { 454 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 455 | CODE_SIGN_ENTITLEMENTS = "Stupid Groups/Stupid Groups.entitlements"; 456 | CODE_SIGN_IDENTITY = "Mac Developer"; 457 | CODE_SIGN_STYLE = Automatic; 458 | COMBINE_HIDPI_IMAGES = YES; 459 | DEVELOPMENT_TEAM = X6CF4FM9JH; 460 | INFOPLIST_FILE = "Stupid Groups/Info.plist"; 461 | LD_RUNPATH_SEARCH_PATHS = ( 462 | "$(inherited)", 463 | "@executable_path/../Frameworks", 464 | ); 465 | MACOSX_DEPLOYMENT_TARGET = 10.12; 466 | PRODUCT_BUNDLE_IDENTIFIER = "Levenick.Stupid-Groups"; 467 | PRODUCT_NAME = "$(TARGET_NAME)"; 468 | PROVISIONING_PROFILE_SPECIFIER = ""; 469 | SWIFT_VERSION = 4.2; 470 | }; 471 | name = Debug; 472 | }; 473 | F7EAD12D21F6112B001F8492 /* Release */ = { 474 | isa = XCBuildConfiguration; 475 | buildSettings = { 476 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 477 | CODE_SIGN_ENTITLEMENTS = "Stupid Groups/Stupid Groups.entitlements"; 478 | CODE_SIGN_IDENTITY = "Mac Developer"; 479 | CODE_SIGN_STYLE = Automatic; 480 | COMBINE_HIDPI_IMAGES = YES; 481 | DEVELOPMENT_TEAM = X6CF4FM9JH; 482 | INFOPLIST_FILE = "Stupid Groups/Info.plist"; 483 | LD_RUNPATH_SEARCH_PATHS = ( 484 | "$(inherited)", 485 | "@executable_path/../Frameworks", 486 | ); 487 | MACOSX_DEPLOYMENT_TARGET = 10.12; 488 | PRODUCT_BUNDLE_IDENTIFIER = "Levenick.Stupid-Groups"; 489 | PRODUCT_NAME = "$(TARGET_NAME)"; 490 | PROVISIONING_PROFILE_SPECIFIER = ""; 491 | SWIFT_VERSION = 4.2; 492 | }; 493 | name = Release; 494 | }; 495 | F7EAD12F21F6112B001F8492 /* Debug */ = { 496 | isa = XCBuildConfiguration; 497 | buildSettings = { 498 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 499 | BUNDLE_LOADER = "$(TEST_HOST)"; 500 | CODE_SIGN_STYLE = Automatic; 501 | COMBINE_HIDPI_IMAGES = YES; 502 | DEVELOPMENT_TEAM = X6CF4FM9JH; 503 | INFOPLIST_FILE = "Stupid GroupsTests/Info.plist"; 504 | LD_RUNPATH_SEARCH_PATHS = ( 505 | "$(inherited)", 506 | "@executable_path/../Frameworks", 507 | "@loader_path/../Frameworks", 508 | ); 509 | PRODUCT_BUNDLE_IDENTIFIER = "Levenick.Stupid-GroupsTests"; 510 | PRODUCT_NAME = "$(TARGET_NAME)"; 511 | SWIFT_VERSION = 4.2; 512 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Stupid Groups.app/Contents/MacOS/Stupid Groups"; 513 | }; 514 | name = Debug; 515 | }; 516 | F7EAD13021F6112B001F8492 /* Release */ = { 517 | isa = XCBuildConfiguration; 518 | buildSettings = { 519 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 520 | BUNDLE_LOADER = "$(TEST_HOST)"; 521 | CODE_SIGN_STYLE = Automatic; 522 | COMBINE_HIDPI_IMAGES = YES; 523 | DEVELOPMENT_TEAM = X6CF4FM9JH; 524 | INFOPLIST_FILE = "Stupid GroupsTests/Info.plist"; 525 | LD_RUNPATH_SEARCH_PATHS = ( 526 | "$(inherited)", 527 | "@executable_path/../Frameworks", 528 | "@loader_path/../Frameworks", 529 | ); 530 | PRODUCT_BUNDLE_IDENTIFIER = "Levenick.Stupid-GroupsTests"; 531 | PRODUCT_NAME = "$(TARGET_NAME)"; 532 | SWIFT_VERSION = 4.2; 533 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Stupid Groups.app/Contents/MacOS/Stupid Groups"; 534 | }; 535 | name = Release; 536 | }; 537 | F7EAD13221F6112B001F8492 /* Debug */ = { 538 | isa = XCBuildConfiguration; 539 | buildSettings = { 540 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 541 | CODE_SIGN_STYLE = Automatic; 542 | COMBINE_HIDPI_IMAGES = YES; 543 | DEVELOPMENT_TEAM = X6CF4FM9JH; 544 | INFOPLIST_FILE = "Stupid GroupsUITests/Info.plist"; 545 | LD_RUNPATH_SEARCH_PATHS = ( 546 | "$(inherited)", 547 | "@executable_path/../Frameworks", 548 | "@loader_path/../Frameworks", 549 | ); 550 | PRODUCT_BUNDLE_IDENTIFIER = "Levenick.Stupid-GroupsUITests"; 551 | PRODUCT_NAME = "$(TARGET_NAME)"; 552 | SWIFT_VERSION = 4.2; 553 | TEST_TARGET_NAME = "Stupid Groups"; 554 | }; 555 | name = Debug; 556 | }; 557 | F7EAD13321F6112B001F8492 /* Release */ = { 558 | isa = XCBuildConfiguration; 559 | buildSettings = { 560 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 561 | CODE_SIGN_STYLE = Automatic; 562 | COMBINE_HIDPI_IMAGES = YES; 563 | DEVELOPMENT_TEAM = X6CF4FM9JH; 564 | INFOPLIST_FILE = "Stupid GroupsUITests/Info.plist"; 565 | LD_RUNPATH_SEARCH_PATHS = ( 566 | "$(inherited)", 567 | "@executable_path/../Frameworks", 568 | "@loader_path/../Frameworks", 569 | ); 570 | PRODUCT_BUNDLE_IDENTIFIER = "Levenick.Stupid-GroupsUITests"; 571 | PRODUCT_NAME = "$(TARGET_NAME)"; 572 | SWIFT_VERSION = 4.2; 573 | TEST_TARGET_NAME = "Stupid Groups"; 574 | }; 575 | name = Release; 576 | }; 577 | /* End XCBuildConfiguration section */ 578 | 579 | /* Begin XCConfigurationList section */ 580 | F7EAD10021F61129001F8492 /* Build configuration list for PBXProject "Stupid Groups" */ = { 581 | isa = XCConfigurationList; 582 | buildConfigurations = ( 583 | F7EAD12921F6112B001F8492 /* Debug */, 584 | F7EAD12A21F6112B001F8492 /* Release */, 585 | ); 586 | defaultConfigurationIsVisible = 0; 587 | defaultConfigurationName = Release; 588 | }; 589 | F7EAD12B21F6112B001F8492 /* Build configuration list for PBXNativeTarget "Stupid Groups" */ = { 590 | isa = XCConfigurationList; 591 | buildConfigurations = ( 592 | F7EAD12C21F6112B001F8492 /* Debug */, 593 | F7EAD12D21F6112B001F8492 /* Release */, 594 | ); 595 | defaultConfigurationIsVisible = 0; 596 | defaultConfigurationName = Release; 597 | }; 598 | F7EAD12E21F6112B001F8492 /* Build configuration list for PBXNativeTarget "Stupid GroupsTests" */ = { 599 | isa = XCConfigurationList; 600 | buildConfigurations = ( 601 | F7EAD12F21F6112B001F8492 /* Debug */, 602 | F7EAD13021F6112B001F8492 /* Release */, 603 | ); 604 | defaultConfigurationIsVisible = 0; 605 | defaultConfigurationName = Release; 606 | }; 607 | F7EAD13121F6112B001F8492 /* Build configuration list for PBXNativeTarget "Stupid GroupsUITests" */ = { 608 | isa = XCConfigurationList; 609 | buildConfigurations = ( 610 | F7EAD13221F6112B001F8492 /* Debug */, 611 | F7EAD13321F6112B001F8492 /* Release */, 612 | ); 613 | defaultConfigurationIsVisible = 0; 614 | defaultConfigurationName = Release; 615 | }; 616 | /* End XCConfigurationList section */ 617 | }; 618 | rootObject = F7EAD0FD21F61129001F8492 /* Project object */; 619 | } 620 | -------------------------------------------------------------------------------- /Stupid Groups/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 321 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | NSAllRomanInputSourcesLocaleIdentifier 437 | 438 | 439 | 440 | 448 | 456 | 470 | 484 | 485 | 486 | 487 | 488 | 496 | 497 | 498 | 499 | 500 | 501 | 509 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | --------------------------------------------------------------------------------