├── 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 | \(deviceRoot)>
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)\(devicePlural)>
151 | \(deviceRoot)>
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: "\(deviceData[1])")
97 | printString(header: false, error: false, green: true, fixedPoint: false, lineBreakAfter: false, message: "Group Found. ")
98 | printString(header: false, error: false, green: false, fixedPoint: true, lineBreakAfter: true, message: "Group name appears to be:\n\"\(self.smartGroupName!)\"\n\nand will be converted to\n\"\(self.newName!)\".\n\nPress the Convert button to continue.")
99 | readyToRun()
100 | } else {
101 | printString(header: false, error: true, green: false, fixedPoint: false, lineBreakAfter: false, message: "It seems an error has occured. ")
102 | printString(header: false, error: false, green: false, fixedPoint: true, lineBreakAfter: true, message: "The data gathered by Stupid Groups does not appear to match any existing group. Please try again.")
103 | }
104 | NSLog("[INFO ]: GET function returned: " + smartGroupXML)
105 | }
106 |
107 | @IBAction func btnPOST(_ sender: Any) {
108 | clearLog()
109 | printString(header: false, error: false, green: false, fixedPoint: true, lineBreakAfter: true, message: "Submitting data to create new \(popConvertTo.titleOfSelectedItem!) named \(newName ?? "nil")...\n")
110 | notReadyToRun()
111 | NSLog("[INFO ]: Starting POST function.")
112 | let deviceData = prepareData().deviceData(deviceType: self.popDeviceType.titleOfSelectedItem!, conversionType: self.popConvertTo.titleOfSelectedItem!)
113 |
114 | let xmlToPost = prepareData().xmlToPost(newName: newName, siteID: siteID, criteria: smartGroupCriteria, membership: smartGroupMembership, conversionType: popConvertTo.titleOfSelectedItem!, deviceRoot: deviceData[0], devicePlural: deviceData[1], deviceSingular: deviceData[2])
115 | let postURL = prepareData().createPOSTURL(url: globalServerURL, endpoint: deviceData[3] )
116 | let postResponse = API().post(postCredentials: globalServerCredentials, postURL: postURL, postBody: xmlToPost)
117 |
118 | if postResponse.contains(""){
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 |
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 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
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 |
--------------------------------------------------------------------------------