├── .gitignore
├── ChangeLog.md
├── LICENSE
├── README.md
├── Replicator.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcuserdata
│ │ ├── leslie.xcuserdatad
│ │ └── IDEFindNavigatorScopes.plist
│ │ └── lesliehelou.xcuserdatad
│ │ └── IDEFindNavigatorScopes.plist
├── xcshareddata
│ └── xcschemes
│ │ └── jamf-migrator.xcscheme
└── xcuserdata
│ ├── ladmin.xcuserdatad
│ └── xcschemes
│ │ ├── jamf-migrator.xcscheme
│ │ └── xcschememanagement.plist
│ ├── leslie.xcuserdatad
│ └── xcschemes
│ │ ├── jamf-migrator - backup mode.xcscheme
│ │ ├── jamf-migrator debug.xcscheme
│ │ ├── jamf-migrator headless migrate.xcscheme
│ │ ├── jamf-migrator help.xcscheme
│ │ ├── jamf-migrator ldap.xcscheme
│ │ └── xcschememanagement.plist
│ └── lesliehelou.xcuserdatad
│ └── xcschemes
│ ├── Jamf Migrator.xcscheme
│ ├── jamf-migrator.xcscheme
│ └── xcschememanagement.plist
└── Replicator
├── Alert.swift
├── AppDelegate.swift
├── Assets.xcassets
├── AppIcon.appiconset
│ ├── Contents.json
│ ├── icon_128x128.png
│ ├── icon_128x128@2x.png
│ ├── icon_16x16.png
│ ├── icon_16x16@2x.png
│ ├── icon_256x256.png
│ ├── icon_256x256@2x.png
│ ├── icon_32x32.png
│ ├── icon_32x32@2x.png
│ ├── icon_512x512.png
│ └── icon_512x512@2x.png
├── Contents.json
├── computer64.imageset
│ ├── Contents.json
│ └── computer64.png
├── password64.imageset
│ ├── Contents.json
│ └── password64.png
└── siteIcon.imageset
│ ├── Contents.json
│ └── siteIcon.png
├── Awake.swift
├── Base.lproj
├── Main.storyboard
└── Preferences.storyboard
├── Cleanup.swift
├── CreateEndpoints.swift
├── Credentials.swift
├── CustomSeparator.swift
├── EndpointXml.swift
├── ExistingObjects.swift
├── ExportItem.swift
├── Globals.swift
├── Headless.swift
├── HelpViewController.swift
├── IconDelegate.swift
├── Info.plist
├── JamfPro.swift
├── Jpapi.swift
├── Json.swift
├── JsonObjects.swift
├── LastUser.swift
├── LoggerExtension.swift
├── ObjectDelegate.swift
├── Package.swift
├── PackagesDelegate.swift
├── PatchDelegate.swift
├── PatchManagementApi.swift
├── Preferences
└── PreferencesViewController.swift
├── PrefsWindowController.swift
├── RemoveData.swift
├── RemoveObjects.swift
├── Replicator.entitlements
├── SaveDelegate.swift
├── SecurityScopedBookmarks.swift
├── Sites.swift
├── SourceDestVC.swift
├── SummaryViewController.swift
├── TimeDelegate.swift
├── Utilities.swift
├── VersionCheck.swift
├── ViewController.swift
├── WriteToLog.swift
├── XmlDelegate.swift
├── app.png
├── copy.png
├── export.png
├── images
├── Screenshot 2024-11-18 at 12.35.39 PM.png
├── Screenshot 2024-11-18 at 12.36.14 PM.png
├── allowAccess.png
├── appPrefs.png
├── computerPrefs.png
├── copyPrefs.png
├── exportDeleted.png
├── exportPrefs.png
├── exportTo.png
├── migrator2.1-old.png
├── migrator2.1.png
├── migrator2.5a.png
├── migrator2.5b.png
├── migrator2.png
├── migrator3.png
├── migrator3Policies.png
├── open.png
├── passwordPrefs.png
├── removeServer.png
├── selectiveFilter.png
├── sitePrefs.png
├── summary1.png
└── summary2.png
├── index.html
├── settings.plist
└── siteIcon.png
/.gitignore:
--------------------------------------------------------------------------------
1 | **/Notes/
2 | Notes
3 | Bookmarks.*
4 | **/.DS_Store
5 | *.xcuserstate
6 | *.xcbkptlist
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Jamf Professional Services
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Replicator (formerly known as Jamf Migrator)
3 |
4 |   
5 |    
6 |
7 |
8 | Download [Replicator](https://github.com/jamf/Replicator/releases/latest/download/Replicator.zip)
9 |
10 | A tool to synchronize configurations between Jamf Pro servers. Export configurations and save locally, then upload them to another Jamf Pro server. You can also use the tool to delete configurations from a server.
11 |
12 | 
13 |
14 | Migrate items from one Jamf server, or XML file(s), to another. If an item (based on name) within a category exists on both source and destination, the destination item will be updated with values from the source server.
15 |
16 |
17 | Username and password fields can be hidden/shown using the disclosure tringle on the left.
18 |
19 | 
20 |
21 | When replicating files be sure to open the 'raw' folder.
22 | 
23 |
24 | Devices (computers and iOS), Groups, Policies, and Configuration Profiles can be targeted to a particular site.
25 | 
26 |
27 |
28 | Servers can be removed from the (source/destination) list by holding down the option key while selecting the server. A warning will be presented to verify the removal.
29 | 
30 |
31 |
32 | **Limitations/requirements to be aware of:**
33 |
34 | * Passwords can not be extracted through the API which impacts replicating distribution points, computer management account, account used for LDAP. A password can be supplied for each service account, but credentials may need to be reset on the destination server for more complex configurations.
35 | * Certificate used for LDAPS does not replicate.
36 | * Icons associated with Mac App Store apps are not replicated as the API does not support it.
37 | * Only AFP and SMB shares can be replicated.
38 | * Patch management is not available through the API impacting smart groups dependent on patch management extension attributes.
39 | * If endpoints (computers, policies, configuration profiles...) have duplicate names on the source server issues will arise if the app is used to update those items from the source to destination server. As each item is migrited it will overwrite the previous item with the same name.
40 | * Replicating smart/static groups with criteria containing groups will fail if the parent group tries to replicate before the group in the criteria. Replicating groups several times should allow all the nested groups to replicate before the parent group.
41 | * Institutional disk encryptions that contain the private key cannot be replicated.
42 | * Approved System/Kernel Extension payloads do not replicate properly. Display names are dropped and additional keys/values are added by the Jamf API that results in a corrupt profile and failure in profile deployment.
43 | * Policies - The Software Update payload does not replicate. Also, within the packages payload, setting for the distribution point will not replicate.
44 | * Objects with trailing spaces in the name will replicate once but the process of uploading through the API removes those spaces. This causes issues re-replicating those objects as the names no longer match.
45 | * Users and usergroups used in policy limitations/exclusions do not replicate as the API does not provide that information.
46 | * Packages
47 | Only package metadata (display name, file name, size, ...) is replicated. To replicate the actual package either use your browser, [Jamf Sync](https://github.com/jamf/JamfSync), or [jamfcpr](https://github.com/BIG-RAT/jamfcpr)
48 | The API allows for the creation of multiple packages, with different display names, to reference the same package file name. The Jamf Pro console prevents this as there should be a one to one mapping.
49 |
50 | * Saving of objects whos name contains a : (colon) will be saved using a ; (semi-colon).
51 | * Enabled state of mobile device applications is not handled in the API, as a result all replicated mobile device applications will be enabled on the destination server whether it is enabled or disabled on the source.
52 | * Configuration Profiles -> Applications & Custom Settings -> External Applications settings will not replicate/export properly. The form generated by the custom schema is not replicated/exported, rather the current settings from the form are replicated/exported. The replicated profile will show the current setting for the external applicaton under the 'Upload' section within Application & Custom Settings.
53 | * Apps from the Jamf App Catalog are not listed under the macapplications (classic API) endpoint. This may cause issues with other objects, like smart computer groups.
54 | * __Patch Management:__ When replicating packages and patch policies may still be replicating after patch policy software titles are shown to have been completed. Better monitoring will appear in later updates.
55 |
56 |
57 |
58 | The Selective tab provides the ability to select a subset of (or all) items within a collection of objects. For example you might only want to transfer 4 scripts from a larger pool of existing scripts.
59 | 
60 |
61 |
62 | Also, policies may have their dependencies checked/replicated using the Include Dependencies button. Only 'top-level' dependencies are checked. i.e. if the scope is being replicated and contains nested computer groups or groups assigned to a site that doesn't exist on the destination server the policy replication will likely fail.
63 | 
64 | Note: The ID of any object can be seen my hovering the mouse over the object.
65 |
66 |
67 | Files exported using Replicator can be imported into another Jamf Pro server. Be sure to open the 'raw' folder when importing.
68 |
69 | 
70 |
71 | **Important:** Trimmed XML files cannot be used as they are missing data required for the replication.
72 |
73 |
74 | **Preferences:**
75 |
76 | * macOS Configuration Profiles
77 | * macOS Applications
78 | * Restrictions
79 | * Policies
80 | * Mobile Device Configuration Profiles
81 | * Mobile Device Applications
82 | * Static Computer Groups
83 | * Static Mobile Device Groups
84 | * Static User Groups
85 |
86 | In addition to scoping options the following are available:
87 | * Policies can be copied in a disabled state
88 | * Able to copy only items missing from the destination server - create only
89 | * Able to copy only items currently on the destination server - update only
90 |
91 | ** object name is used to determine whether or not it is on the destination server **
92 |
93 | 
94 |
95 | Options to export XML from the source server are also available.
96 |
97 | 
98 |
99 | * Raw Source XML gives you the XML from the source server before any modifications, like removing the id tag(s) and value(s).
100 | * Trimmed Source XML give you the XML that is sent to the destination server.
101 | * Save only saves the XML files and does not send them to the destination server.
102 | * Save the object XML either with or without its scope. Unchecked removes the scope.
103 | * Note Save only and Raw Source XML options should not be selected when File Import is being used.
104 |
105 | Options for replicating object(s) (groups, policies, and configuration profiles) to a particular site can be set.
106 |
107 | 
108 |
109 | * When copying an object to a site, the site name is appended to the object name.
110 | * Groups with groups as a criteria will not copy properly, moving them should be fine.
111 |
112 | The number of concurrent API operations (from 1 to 5), sticky sessions (when available), forcing basic authentication, color scheme, number of log files to retain, and number of servers can be remembered.
113 |
114 | 
115 |
116 | Migrated computers can show as managed by setting the management account.
117 |
118 | 
119 |
120 | Set a password for following replicated service accounts; bind, ldap, file share Read/Write, and file share Read-only.
121 |
122 | 
123 |
124 | Note, the same password will be applied if you have multiple binds, or ldap servers, or file shares coonfigured.
125 |
126 | **Migration Summary:**
127 |
128 | * To get details on how many items were created/updated or failed to replicate type ⌘S, or select Show Summary under the File menu.
129 |
130 | 
131 |
132 | * Additional information about each count can be obtained by clicking on the number. For example, if we want to see a list of the 28 failed scripts, click on the 28.
133 |
134 | 
135 |
136 |
137 | Information about successes/failures can be found in the log, located in
138 |
139 | ```
140 | ~/Library/Containers/com.jamf.Replicator/Data/Library/Logs/Replicator/__migration.log
141 | ```
142 |
143 | If you have used Replicator and saved passwords you will see the following after launching a new version.
144 |
145 | 
146 |
147 | If you'd like the new version to access existing credentials select the desired option.
148 |
149 |
150 |
151 | **Important:**
152 |
153 | * There are many dependencies between items, if they are not met transfers fail. For example, if a policy is site specific the site must be replicated before the policy; if a distribution point has a building and/or department defined those need to replicate first... If everything is replicated the order of sections is already taken care of, if you choose not to move some items that's where you can have issues.
154 | * Summary window doesn't seem to be the most responsive. May need to click the window or give the cursor some extra motion before the detailed summary appears.
155 |
156 |
157 | **Note:** the app can also be used to clear out a Jamf server. Typing the following after launching the app will set it into removal mode. Items from the destination server are deleted once Go is clicked.
158 |
159 | ```
160 | touch ~/Library/Containers/com.jamf.Replicator/Data/Library/Application\ Support/Replicator/delete
161 | ```
162 |
163 | * You can also toggle the mode using ⌘D or select Toggle Mode from View in the menu bar.
164 |
165 | ## Running from the command line
166 |
167 | Help is available by running:
168 | ```
169 | /path/to/Replicator.app/Contents/MacOS/Replicator -help
170 | ```
171 | Running the following in Terminal will export all objects (full XML) that can be replicated:
172 | ```
173 | /path/to/Replicator.app/Contents/MacOS/Replicator -source your.jamfPro.fqdn -export -objects allobjects
174 | ```
175 |
176 | In the event you have multiple entries in the keychain for a server you'll need to specify which username to use. For example:
177 | ```
178 | /path/to/Replicator.app/Contents/MacOS/Replicator -source dev.jamfcloud.com -destination prod.jamfcloud.com -objects "categories,buildings" -migrate -sourceUser devadmin -destUser prodadmin
179 | ```
180 |
181 | Before running an export via command line at least one export from the app must be manually run saving the source username and password or client ID and secret.
182 |
183 | To replicate object(s) using the command line, something like the following can be used:
184 | ```
185 | /path/to/Replicator.app/Contents/MacOS/Replicator -source your.jamfPro.fqdn -destination dest.jamfPro.fqdn -objects categories,buildings -migrate
186 | ```
187 | If importing files, the import folder must be selected in the UI before the command line can be successfully run.
188 |
189 | To set an ldap id of 3 on jamf user accounts and force that id (also converts local accounts to ldap) use the following:
190 | ```
191 | /path/to/Replicator.app/Contents/MacOS/Replicator -ldapid 3 -source /Users/admin/Desktop/export/raw -migrate -objects jamfusers
192 | ```
193 | This can also be accomplished using the UI by launching Replicator from Terminal:
194 | ```
195 | /path/to/Replicator.app/Contents/MacOS/Replicator -ldapid 3
196 | ```
197 |
198 |
--------------------------------------------------------------------------------
/Replicator.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Replicator.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Replicator.xcodeproj/project.xcworkspace/xcuserdata/leslie.xcuserdatad/IDEFindNavigatorScopes.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Replicator.xcodeproj/project.xcworkspace/xcuserdata/lesliehelou.xcuserdatad/IDEFindNavigatorScopes.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Replicator.xcodeproj/xcshareddata/xcschemes/jamf-migrator.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
44 |
46 |
52 |
53 |
54 |
55 |
61 |
63 |
69 |
70 |
71 |
72 |
74 |
75 |
78 |
79 |
81 |
84 |
85 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
99 |
102 |
103 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/Replicator.xcodeproj/xcuserdata/ladmin.xcuserdatad/xcschemes/jamf-migrator.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/Replicator.xcodeproj/xcuserdata/ladmin.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | jamf-migrator.xcscheme
8 |
9 | orderHint
10 | 0
11 |
12 |
13 | SuppressBuildableAutocreation
14 |
15 | 9BC01C5B1DFB0C07007E634E
16 |
17 | primary
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Replicator.xcodeproj/xcuserdata/leslie.xcuserdatad/xcschemes/jamf-migrator - backup mode.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
57 |
58 |
61 |
62 |
65 |
66 |
69 |
70 |
73 |
74 |
75 |
76 |
82 |
84 |
90 |
91 |
92 |
93 |
95 |
96 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/Replicator.xcodeproj/xcuserdata/leslie.xcuserdatad/xcschemes/jamf-migrator debug.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
57 |
58 |
59 |
60 |
66 |
68 |
74 |
75 |
76 |
77 |
79 |
80 |
83 |
84 |
86 |
89 |
90 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
104 |
107 |
108 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
--------------------------------------------------------------------------------
/Replicator.xcodeproj/xcuserdata/leslie.xcuserdatad/xcschemes/jamf-migrator headless migrate.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
57 |
58 |
61 |
62 |
65 |
66 |
69 |
70 |
73 |
74 |
77 |
78 |
81 |
82 |
85 |
86 |
89 |
90 |
93 |
94 |
97 |
98 |
101 |
102 |
105 |
106 |
107 |
108 |
114 |
116 |
122 |
123 |
124 |
125 |
127 |
128 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/Replicator.xcodeproj/xcuserdata/leslie.xcuserdatad/xcschemes/jamf-migrator help.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
57 |
58 |
59 |
60 |
66 |
68 |
74 |
75 |
76 |
77 |
79 |
80 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/Replicator.xcodeproj/xcuserdata/leslie.xcuserdatad/xcschemes/jamf-migrator ldap.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
57 |
58 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/Replicator.xcodeproj/xcuserdata/leslie.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | jamf-migrator - backup mode.xcscheme
8 |
9 | orderHint
10 | 0
11 |
12 | jamf-migrator debug.xcscheme
13 |
14 | orderHint
15 | 2
16 |
17 | jamf-migrator headless migrate.xcscheme
18 |
19 | orderHint
20 | 4
21 |
22 | jamf-migrator help.xcscheme
23 |
24 | orderHint
25 | 5
26 |
27 | jamf-migrator ldap.xcscheme
28 |
29 | orderHint
30 | 3
31 |
32 | jamf-migrator.xcscheme_^#shared#^_
33 |
34 | orderHint
35 | 1
36 |
37 |
38 | SuppressBuildableAutocreation
39 |
40 | 9BC01C5B1DFB0C07007E634E
41 |
42 | primary
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/Replicator.xcodeproj/xcuserdata/lesliehelou.xcuserdatad/xcschemes/Jamf Migrator.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
52 |
54 |
60 |
61 |
62 |
63 |
67 |
68 |
69 |
70 |
76 |
78 |
84 |
85 |
86 |
87 |
89 |
90 |
93 |
94 |
96 |
99 |
100 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/Replicator.xcodeproj/xcuserdata/lesliehelou.xcuserdatad/xcschemes/jamf-migrator.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
52 |
54 |
60 |
61 |
62 |
63 |
66 |
67 |
70 |
71 |
74 |
75 |
78 |
79 |
80 |
81 |
85 |
86 |
87 |
88 |
94 |
96 |
102 |
103 |
104 |
105 |
107 |
108 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/Replicator.xcodeproj/xcuserdata/lesliehelou.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Jamf Migrator.xcscheme
8 |
9 | orderHint
10 | 1
11 |
12 | jamf-migrator.xcscheme
13 |
14 | orderHint
15 | 0
16 |
17 |
18 | SuppressBuildableAutocreation
19 |
20 | 9BC01C5B1DFB0C07007E634E
21 |
22 | primary
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Replicator/Alert.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Alert.swift
3 | // Replicator
4 | //
5 | // Created by lnh on 12/22/21.
6 | // Copyright 2024 Jamf. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class Alert: NSObject {
12 |
13 | static let shared = Alert()
14 |
15 | func display(header: String, message: String, secondButton: String) -> String {
16 | NSApplication.shared.activate(ignoringOtherApps: true)
17 | var selected = ""
18 | let dialog: NSAlert = NSAlert()
19 | dialog.messageText = header
20 | dialog.informativeText = message
21 | dialog.alertStyle = NSAlert.Style.warning
22 | if secondButton != "" {
23 | let otherButton = dialog.addButton(withTitle: secondButton)
24 | otherButton.keyEquivalent = "\r"
25 | }
26 | let okButton = dialog.addButton(withTitle: "OK")
27 | okButton.keyEquivalent = "o"
28 |
29 | let theButton = dialog.runModal()
30 | switch theButton {
31 | case .alertFirstButtonReturn:
32 | selected = secondButton
33 | default:
34 | selected = "OK"
35 | }
36 | return selected
37 | }
38 |
39 | func versionDialog(header: String, message: String, updateAvail: Bool, manualCheck: Bool = false) {
40 | NSApp.activate(ignoringOtherApps: true)
41 | if userDefaults.bool(forKey: "hideVersionAlert") == false || manualCheck {
42 | let dialog: NSAlert = NSAlert()
43 | dialog.messageText = header
44 | dialog.informativeText = message
45 | dialog.alertStyle = NSAlert.Style.informational
46 | dialog.showsSuppressionButton = !manualCheck
47 | if updateAvail {
48 | dialog.addButton(withTitle: "View")
49 | dialog.addButton(withTitle: "Later")
50 | } else {
51 | dialog.addButton(withTitle: "OK")
52 | }
53 |
54 | let clicked:NSApplication.ModalResponse = dialog.runModal()
55 |
56 | if let supress = dialog.suppressionButton {
57 | let state = supress.state
58 | switch state {
59 | case .on:
60 | userDefaults.set(true, forKey: "hideVersionAlert")
61 | default: break
62 | }
63 | }
64 |
65 | if clicked.rawValue == 1000 && updateAvail {
66 | if let url = URL(string: "https://github.com/jamf/Replicator/releases") {
67 | NSWorkspace.shared.open(url)
68 | }
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Replicator/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Replicator
4 | //
5 | // Created by Leslie N. Helou on 12/9/16.
6 | // Copyright 2024 Jamf. All rights reserved.
7 | //
8 |
9 | import AppKit
10 | import ApplicationServices
11 | import Cocoa
12 |
13 | @NSApplicationMain
14 | class AppDelegate: NSObject, NSApplicationDelegate {
15 |
16 | static let shared = AppDelegate()
17 | private override init() { }
18 |
19 | var prefWindowController: NSWindowController?
20 |
21 | @IBOutlet weak var resetVersionAlert_MenuItem: NSMenuItem!
22 | @IBAction func resetVersionAlert_Action(_ sender: Any) {
23 | resetVersionAlert_MenuItem.isEnabled = false
24 | resetVersionAlert_MenuItem.isHidden = true
25 | userDefaults.set(false, forKey: "hideVersionAlert")
26 | }
27 |
28 |
29 | @IBAction func showSummaryWindow(_ sender: AnyObject) {
30 | NotificationCenter.default.post(name: .showSummaryWindow, object: self)
31 | }
32 | @IBAction func showLogFolder(_ sender: AnyObject) {
33 | NotificationCenter.default.post(name: .showLogFolder, object: self)
34 | }
35 | @IBAction func deleteMode(_ sender: AnyObject) {
36 | NotificationCenter.default.post(name: .deleteMode, object: self)
37 | }
38 | @IBAction func quit_menu(sender: AnyObject) {
39 | quitNow(sender: self)
40 | }
41 |
42 | public func quitNow(sender: AnyObject) {
43 | DispatchQueue.main.async {
44 | NSApp.hide(nil)
45 | let sourceMethod = (JamfProServer.validToken["source"] ?? false) ? "POST":"SKIP"
46 | let destMethod = (JamfProServer.validToken["dest"] ?? false) ? "POST":"SKIP"
47 |
48 | // check for file that sets mode to delete data from destination server, delete if found - start
49 | ViewController().rmDELETE()
50 |
51 | Jpapi.shared.action(whichServer: "source", endpoint: "auth/invalidate-token", apiData: [:], id: "", token: JamfProServer.authCreds["source"] ?? "", method: sourceMethod) {
52 | (returnedJSON: [String:Any]) in
53 | WriteToLog.shared.message("source server token task: \(returnedJSON["JPAPI_result"] ?? "unknown response")")
54 |
55 | Jpapi.shared.action(whichServer: "dest", endpoint: "auth/invalidate-token", apiData: [:], id: "", token: JamfProServer.authCreds["dest"] ?? "", method: destMethod) {
56 | (returnedJSON: [String:Any]) in
57 | WriteToLog.shared.message("destination server token task: \(returnedJSON["JPAPI_result"] ?? "unknown response")")
58 |
59 | NSApplication.shared.terminate(self)
60 | }
61 | }
62 | }
63 | }
64 |
65 | func applicationDidFinishLaunching(_ aNotification: Notification) {
66 | print("[\(#function.description)] loaded")
67 |
68 | if Setting.fullGUI {
69 | let hideVersionAlert = userDefaults.bool(forKey: "hideVersionAlert")
70 | resetVersionAlert_MenuItem.isEnabled = hideVersionAlert
71 | resetVersionAlert_MenuItem.isHidden = !hideVersionAlert
72 |
73 | // create log directory if missing - start
74 | if !fm.fileExists(atPath: History.logPath) {
75 | do {
76 | try fm.createDirectory(atPath: History.logPath, withIntermediateDirectories: true, attributes: nil )
77 | } catch {
78 | _ = Alert.shared.display(header: "Error:", message: "Unable to create log directory:\n\(String(describing: History.logPath))\nTry creating it manually.", secondButton: "")
79 | exit(0)
80 | }
81 | }
82 | // create log directory if missing - endlogFile = TimeDelegate().getCurrent().replacingOccurrences(of: ":", with: "") + "_replicator.log"
83 | History.logFile = TimeDelegate().getCurrent().replacingOccurrences(of: ":", with: "") + "_replicator.log"
84 |
85 | // isDir = false
86 | if !(fm.fileExists(atPath: History.logPath + History.logFile/*, isDirectory: &isDir*/)) {
87 | fm.createFile(atPath: History.logPath + History.logFile, contents: nil, attributes: nil)
88 | }
89 | sleep(1)
90 | if !(fm.fileExists(atPath: History.logPath + History.logFile/*, isDirectory: &isDir*/)) {
91 | print("Unable to create log file:\n\(History.logPath + History.logFile)")
92 | }
93 | }
94 |
95 |
96 | if !fm.fileExists(atPath: AppInfo.plistPath) {
97 | _ = readSettings(thePath: AppInfo.plistPathOld)
98 | // isDir = true
99 | if !fm.fileExists(atPath: AppInfo.appSupportPath/*, isDirectory: &isDir*/) {
100 | try? fm.createDirectory(atPath: AppInfo.appSupportPath, withIntermediateDirectories: true)
101 | }
102 | saveSettings(settings: AppInfo.settings)
103 | }
104 |
105 | // read command line arguments - start
106 | var numberOfArgs = 0
107 | // var startPos = 1
108 | // read commandline args
109 | numberOfArgs = CommandLine.arguments.count
110 | print("all arguments: \(CommandLine.arguments)")
111 | // if CommandLine.arguments.contains("-debug") {
112 | // numberOfArgs -= 1
113 | // startPos+=1
114 | // LogLevel.debug = true
115 | // }
116 | var index = 1
117 | while index < numberOfArgs {
118 | print("[\(#line)-applicationDidFinishLaunching] index: \(index)\t argument: \(CommandLine.arguments[index])")
119 | let cmdLineSwitch = CommandLine.arguments[index].lowercased()
120 | switch cmdLineSwitch {
121 | case "-debug":
122 | LogLevel.debug = true
123 | case "-dryrun":
124 | LogLevel.debug = true
125 | case "-backup","-export":
126 | export.backupMode = true
127 | export.saveOnly = true
128 | export.saveRawXml = true
129 | Setting.fullGUI = false
130 | case "-saverawxml":
131 | export.saveRawXml = true
132 | case "-savetrimmedxml":
133 | export.saveTrimmedXml = true
134 | case "-export.saveonly":
135 | export.saveOnly = true
136 | // case "-forceldapid":
137 | // index += 1
138 | // forceLdapId = Bool(CommandLine.arguments[index]) ?? false
139 | case "-help":
140 | print("\(helpText)")
141 | NSApplication.shared.terminate(self)
142 | case "-ldapid":
143 | index += 1
144 | Setting.ldapId = Int(CommandLine.arguments[index]) ?? -1
145 | if Setting.ldapId > 0 {
146 | Setting.hardSetLdapId = true
147 | }
148 | case "-migrate":
149 | Setting.migrate = true
150 | Setting.fullGUI = false
151 | case "-objects":
152 | index += 1
153 | let objectsString = "\(CommandLine.arguments[index])".lowercased()
154 | Setting.objects = objectsString.components(separatedBy: ",")
155 | case "-scope":
156 | index += 1
157 | Setting.copyScope = Bool(CommandLine.arguments[index].lowercased()) ?? true
158 | case "-site":
159 | index += 1
160 | JamfProServer.toSite = true
161 | JamfProServer.destSite = "\(CommandLine.arguments[index])"
162 | case "-source":
163 | index += 1
164 | JamfProServer.source = "\(CommandLine.arguments[index])"
165 | if JamfProServer.source.prefix(4) != "http" && JamfProServer.source.prefix(1) != "/" {
166 | JamfProServer.source = "https://\(JamfProServer.source)"
167 | } else if JamfProServer.source.prefix(1) == "/" {
168 | JamfProServer.importFiles = 1 // importing files
169 | }
170 | // JamfProServer.url["source"] = JamfProServer.source
171 | case "-dest","-destination":
172 | index += 1
173 | JamfProServer.destination = "\(CommandLine.arguments[index])"
174 | if JamfProServer.destination.prefix(4) != "http" && JamfProServer.destination.prefix(1) != "/" {
175 | JamfProServer.destination = "https://\(JamfProServer.destination)"
176 | }
177 | // JamfProServer.url["destination"] = JamfProServer.source
178 | case "-sourceuseclientid", "-destuseclientid":
179 | index += 1
180 | let useApiClient = ( "\(CommandLine.arguments[index])".lowercased() == "yes" || "\(CommandLine.arguments[index])".lowercased() == "true" ) ? 1:0
181 | if cmdLineSwitch == "-sourceuseclientid" {
182 | JamfProServer.sourceUseApiClient = useApiClient
183 | } else {
184 | JamfProServer.destUseApiClient = useApiClient
185 | }
186 | case "-sourceclientid":
187 | index += 1
188 | JamfProServer.sourceApiClient["id"] = CommandLine.arguments[index]
189 | JamfProServer.sourceUser = JamfProServer.sourceApiClient["id"] ?? ""
190 | JamfProServer.sourceUseApiClient = 1
191 | case "-destclientid":
192 | index += 1
193 | JamfProServer.destApiClient["id"] = CommandLine.arguments[index]
194 | JamfProServer.destUser = JamfProServer.destApiClient["id"] ?? ""
195 | JamfProServer.destUseApiClient = 1
196 | case "-sourceclientsecret":
197 | index += 1
198 | JamfProServer.sourceApiClient["secret"] = CommandLine.arguments[index]
199 | JamfProServer.sourcePwd = JamfProServer.sourceApiClient["secret"] ?? ""
200 | case "-sourceuser":
201 | index += 1
202 | JamfProServer.sourceUser = CommandLine.arguments[index]
203 | // JamfProServer.sourcePwd = JamfProServer.sourceApiClient["secret"] ?? ""
204 | case "-destuser":
205 | index += 1
206 | JamfProServer.destUser = CommandLine.arguments[index]
207 | case "-destclientsecret":
208 | index += 1
209 | JamfProServer.destApiClient["secret"] = CommandLine.arguments[index]
210 | JamfProServer.destPwd = JamfProServer.destApiClient["secret"] ?? ""
211 | case "-onlycopymissing":
212 | index += 1
213 | if "\(CommandLine.arguments[index].lowercased())" == "true" || "\(CommandLine.arguments[index].lowercased())" == "1" {
214 | Setting.onlyCopyMissing = true
215 | Setting.onlyCopyExisting = false
216 | } else {
217 | Setting.onlyCopyMissing = false
218 | }
219 | case "-onlycopyexisting":
220 | index += 1
221 | if CommandLine.arguments[index].lowercased() == "true" || CommandLine.arguments[index].lowercased() == "1" {
222 | Setting.onlyCopyMissing = false
223 | Setting.onlyCopyExisting = true
224 | } else {
225 | Setting.onlyCopyExisting = false
226 | }
227 | case "-silent":
228 | Setting.fullGUI = false
229 | case "-sticky":
230 | JamfProServer.stickySession = true
231 | case "-NSDocumentRevisionsDebugMode":
232 | index += 1
233 | break
234 | default:
235 | if !CommandLine.arguments[index].contains(AppInfo.name) {
236 | print("unknown switch passed: \(CommandLine.arguments[index])")
237 | }
238 | }
239 | index += 1
240 | }
241 | // read command line arguments - end
242 | // print("done reading command line args - index: \(index)")
243 |
244 | export.saveLocation = userDefaults.string(forKey: "saveLocation") ?? ""
245 | if export.saveLocation == "" || !(FileManager().fileExists(atPath: export.saveLocation)) {
246 | export.saveLocation = (NSHomeDirectory() + "/Downloads/Replicator/")
247 | userDefaults.set("\(export.saveLocation)", forKey: "saveLocation")
248 | } else {
249 | export.saveLocation = export.saveLocation.pathToString
250 | // self.userDefaults.synchronize()
251 | }
252 |
253 | if Setting.fullGUI {
254 | DispatchQueue.main.async { [self] in
255 | NSApp.setActivationPolicy(.regular)
256 | let storyboard = NSStoryboard(name: "Main", bundle: nil)
257 | let mainWindowController = storyboard.instantiateController(withIdentifier: "Main") as! NSWindowController
258 | mainWindowController.window?.hidesOnDeactivate = false
259 | mainWindowController.showWindow(self)
260 | checkForUpdates(self)
261 | }
262 | }
263 | else {
264 | WriteToLog.shared.message("[AppDelegate] Replicator is running silently")
265 |
266 | SourceDestVC().initVars()
267 | }
268 | }
269 |
270 | @IBAction func checkForUpdates(_ sender: AnyObject) {
271 | let verCheck = VersionCheck()
272 |
273 | var manualCheck = false
274 | if let _ = sender as? NSMenuItem {
275 | manualCheck = true
276 | }
277 |
278 | verCheck.versionCheck() {
279 | (result: Bool, latest: String) in
280 | if result {
281 | if Setting.fullGUI {
282 | Alert.shared.versionDialog(header: "A new version (\(latest)) is available.", message: "Running Replicator: \(AppInfo.version)", updateAvail: result, manualCheck: manualCheck)
283 | }
284 | WriteToLog.shared.message("A new version (\(latest)) is available")
285 | } else {
286 | if manualCheck && Setting.fullGUI {
287 | Alert.shared.versionDialog(header: "Running Replicator: \(AppInfo.version)", message: "No updates are currently available.", updateAvail: result, manualCheck: manualCheck)
288 | }
289 | }
290 | }
291 | }
292 |
293 | func versionAlert(header: String, message: String, updateAvail: Bool) {
294 |
295 | let dialog: NSAlert = NSAlert()
296 | dialog.messageText = header
297 | dialog.informativeText = message
298 | dialog.alertStyle = NSAlert.Style.informational
299 | if updateAvail {
300 | dialog.addButton(withTitle: "View")
301 | dialog.addButton(withTitle: "Ignore")
302 | } else {
303 | dialog.addButton(withTitle: "OK")
304 | }
305 |
306 | let clicked:NSApplication.ModalResponse = dialog.runModal()
307 |
308 | if clicked.rawValue == 1000 && updateAvail {
309 | if let url = URL(string: "https://github.com/jamf/Replicator/releases") {
310 | NSWorkspace.shared.open(url)
311 | }
312 | }
313 | } // func alert_dialog - end
314 |
315 | // Help Window
316 | @IBAction func showHelpWindow(_ sender: AnyObject) {
317 | let storyboard = NSStoryboard(name: "Main", bundle: nil)
318 | let helpWindowController = storyboard.instantiateController(withIdentifier: "Help View Controller") as! NSWindowController
319 | if !ViewController().windowIsVisible(windowName: "Help") {
320 | helpWindowController.window?.hidesOnDeactivate = false
321 | helpWindowController.showWindow(self)
322 | } else {
323 | let windowsCount = NSApp.windows.count
324 | for i in (0.. Bool {
339 | quitNow(sender: self)
340 | return false
341 | }
342 |
343 | }
344 |
345 |
--------------------------------------------------------------------------------
/Replicator/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon_16x16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "icon_16x16@2x.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "icon_32x32.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "icon_32x32@2x.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "icon_128x128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "icon_128x128@2x.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "icon_256x256.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "icon_256x256@2x.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "icon_512x512.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "icon_512x512@2x.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Replicator/Assets.xcassets/AppIcon.appiconset/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
--------------------------------------------------------------------------------
/Replicator/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png
--------------------------------------------------------------------------------
/Replicator/Assets.xcassets/AppIcon.appiconset/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
--------------------------------------------------------------------------------
/Replicator/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png
--------------------------------------------------------------------------------
/Replicator/Assets.xcassets/AppIcon.appiconset/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
--------------------------------------------------------------------------------
/Replicator/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png
--------------------------------------------------------------------------------
/Replicator/Assets.xcassets/AppIcon.appiconset/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
--------------------------------------------------------------------------------
/Replicator/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png
--------------------------------------------------------------------------------
/Replicator/Assets.xcassets/AppIcon.appiconset/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
--------------------------------------------------------------------------------
/Replicator/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png
--------------------------------------------------------------------------------
/Replicator/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Replicator/Assets.xcassets/computer64.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "computer64.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Replicator/Assets.xcassets/computer64.imageset/computer64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/Assets.xcassets/computer64.imageset/computer64.png
--------------------------------------------------------------------------------
/Replicator/Assets.xcassets/password64.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "password64.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Replicator/Assets.xcassets/password64.imageset/password64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/Assets.xcassets/password64.imageset/password64.png
--------------------------------------------------------------------------------
/Replicator/Assets.xcassets/siteIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "siteIcon.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 | }
--------------------------------------------------------------------------------
/Replicator/Assets.xcassets/siteIcon.imageset/siteIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/Assets.xcassets/siteIcon.imageset/siteIcon.png
--------------------------------------------------------------------------------
/Replicator/Awake.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Awake.swift
3 | // Replicator
4 | //
5 | // Created by Leslie Helou on 10/22/22.
6 | // Copyright 2024 Jamf. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import IOKit.pwr_mgt
11 |
12 | var noSleepAssertionID: IOPMAssertionID = 0
13 | var noSleepReturn: IOReturn?
14 |
15 | public func disableSleep(reason: String) -> Bool? {
16 | guard noSleepReturn == nil else { return nil }
17 | noSleepReturn = IOPMAssertionCreateWithName(kIOPMAssertPreventUserIdleSystemSleep as CFString,IOPMAssertionLevel(kIOPMAssertionLevelOn), reason as CFString, &noSleepAssertionID)
18 | return noSleepReturn == kIOReturnSuccess
19 | }
20 |
21 | public func enableSleep() -> Bool {
22 | if noSleepReturn != nil {
23 | _ = IOPMAssertionRelease(noSleepAssertionID) == kIOReturnSuccess
24 | noSleepReturn = nil
25 | return true
26 | }
27 | return false
28 | }
29 |
--------------------------------------------------------------------------------
/Replicator/CustomSeparator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomSeparator.swift
3 | // Replicator
4 | //
5 | // Created by Leslie Helou on 10/15/22.
6 | // Copyright 2024 Jamf. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AppKit
11 |
12 | class CustomSeparator: NSSplitView {
13 | // override
14 | override var dividerThickness:CGFloat {
15 | get {
16 | return 0.0
17 | }
18 | }
19 | // override var dividerColor: NSColor {
20 | // get {
21 | // return appColor.highlight["classic"]!
22 | // }
23 | // }
24 | }
25 |
--------------------------------------------------------------------------------
/Replicator/ExportItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExportItems.swift
3 | // Replicator
4 | //
5 | // Created by leslie on 11/6/24.
6 | // Copyright © 2024 Jamf. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import os.log
11 |
12 | class ExportItem: NSObject {
13 |
14 | static let shared = ExportItem()
15 |
16 | fileprivate func saveLocation(_ format: String) -> String {
17 | // Create folder to store objectString files if needed - start
18 | var baseFolder = userDefaults.string(forKey: "saveLocation") ?? ""
19 | if baseFolder == "" {
20 | baseFolder = (NSHomeDirectory() + "/Downloads/Replicator/")
21 | } else {
22 | baseFolder = baseFolder.pathToString
23 | }
24 |
25 | let saveFolder = baseFolder+format+"/"
26 |
27 | if !(fm.fileExists(atPath: saveFolder)) {
28 | do {
29 | try fm.createDirectory(atPath: saveFolder, withIntermediateDirectories: true, attributes: nil)
30 | } catch {
31 | if LogLevel.debug { WriteToLog.shared.message("[ExportItem.export] Problem creating \(saveFolder) folder: Error \(error)") }
32 | return ""
33 | }
34 | }
35 | return saveFolder
36 | }
37 |
38 | func export(node: String, object: Any/*, endpointPath: String*/, theName: String = "", id: String = "", format: String = "raw") {
39 |
40 | Logger.exportItem_export.debug("enter exportItem_export - \(node, privacy: .public)")
41 |
42 | var objectAsString = ""
43 | var exportFilename = ""
44 | var endpointPath = ""
45 |
46 | let saveFolder = saveLocation(format)
47 |
48 | // Create endpoint type to store objectString files if needed - start
49 | switch node {
50 | case "selfservicepolicyicon", "macapplicationsicon", "mobiledeviceapplicationsicon":
51 | endpointPath = saveFolder+node+"/\(id)"
52 | case "accounts/groupid":
53 | endpointPath = saveFolder+"jamfgroups"
54 | case "accounts/userid":
55 | endpointPath = saveFolder+"jamfusers"
56 | case "computergroups":
57 | if let objectString = object as? String {
58 | let isSmart = tagValue2(xmlString: objectString, startTag: "", endTag: " ")
59 | if isSmart == "true" {
60 | endpointPath = saveFolder+"smartcomputergroups"
61 | } else {
62 | endpointPath = saveFolder+"staticcomputergroups"
63 | }
64 | }
65 | default:
66 | endpointPath = saveFolder+node
67 | }
68 |
69 | do {
70 | let encoder = JSONEncoder()
71 | encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
72 |
73 | switch node {
74 | case "buildings":
75 | exportFilename = "\(theName)-\(id).json"
76 | if let theString = object as? String, theString.isEmpty == false {
77 | objectAsString = "\(theString.dropFirst().dropLast())"
78 | objectAsString = "{\(objectAsString)}"
79 | }
80 | // do {
81 | // try jsonString.write(toFile: endpointPath+"/"+jsonFile, atomically: true, encoding: .utf8)
82 | // if LogLevel.debug { WriteToLog.shared.message("[ExportItem.export] saved to: \(endpointPath)") }
83 | // } catch {
84 | // if LogLevel.debug { WriteToLog.shared.message("[ExportItem.export] Problem writing \(endpointPath) folder: Error \(error)") }
85 | // return
86 | // }
87 | case "patchPolicyDetails":
88 | exportFilename = "patch-policies-policy-details.json"
89 | let t = object as? [PatchPolicyDetail]
90 | let prettyPrintedData = try encoder.encode(t)
91 | objectAsString = String(data: prettyPrintedData, encoding: .utf8)!
92 | case "patch-software-title-configurations":
93 | if let displayName = (object as? PatchSoftwareTitleConfiguration)?.displayName, let id = (object as? PatchSoftwareTitleConfiguration)?.id {
94 | exportFilename = "\(displayName)-\(id).json"
95 | }
96 | let t = object as? PatchSoftwareTitleConfiguration
97 | let prettyPrintedData = try encoder.encode(t)
98 | objectAsString = String(data: prettyPrintedData, encoding: .utf8)!
99 | default:
100 | if let objectString = object as? String, objectString.isEmpty == false {
101 | // var name = theName.replacingOccurrences(of: ":", with: ";")
102 | // name = name.replacingOccurrences(of: "/", with: "_")
103 | if let xmlDoc = try? XMLDocument(xmlString: objectString, options: .nodePrettyPrint) {
104 | if let _ = try? XMLElement.init(xmlString:"\(objectString)") {
105 | exportFilename = "\(theName)-\(id).xml"
106 | let data = xmlDoc.xmlData(options:.nodePrettyPrint)
107 | objectAsString = String(data: data, encoding: .utf8)!
108 | // print("policy xml:\n\(formattedXml)")
109 |
110 | // do {
111 | // try formattedXml.write(toFile: endpointPath+"/"+exportFilename, atomically: true, encoding: .utf8)
112 | // if LogLevel.debug { WriteToLog.shared.message("[ExportItem.export] saved to: \(endpointPath)") }
113 | // } catch {
114 | // if LogLevel.debug { WriteToLog.shared.message("[ExportItem.export] Problem writing \(endpointPath) folder: Error \(error)") }
115 | // return
116 | // }
117 | } // if let prettyXml - end
118 | }
119 | }
120 | }
121 |
122 | // if node == "patchPolicyDetails" {
123 | // let t = object as? [PatchPolicyDetail]
124 | // let prettyPrintedData = try encoder.encode(t)
125 | // rawExport = String(data: prettyPrintedData, encoding: .utf8)!
126 | // } else {
127 | // let t = object as? PatchSoftwareTitleConfiguration
128 | // let prettyPrintedData = try encoder.encode(t)
129 | // rawExport = String(data: prettyPrintedData, encoding: .utf8)!
130 | // }
131 |
132 | print("[ExportItem.export] rawExport for node \(node): \(objectAsString)")
133 |
134 | exportFilename = exportFilename.replacingOccurrences(of: ":", with: ";")
135 | exportFilename = exportFilename.replacingOccurrences(of: "/", with: "_")
136 |
137 | if !fm.fileExists(atPath: endpointPath) {
138 | try? fm.createDirectory(atPath: endpointPath, withIntermediateDirectories: true, attributes: nil)
139 | }
140 |
141 | try objectAsString.write(toFile: endpointPath+"/"+exportFilename, atomically: true, encoding: .utf8)
142 | if LogLevel.debug { WriteToLog.shared.message("[ExportItem.export] saved to: \(endpointPath)") }
143 | } catch {
144 | if LogLevel.debug { WriteToLog.shared.message("[ExportItem.export] Problem writing \(endpointPath) folder: Error \(error)") }
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/Replicator/Headless.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Headless.swift
3 | // Replicator
4 | //
5 | // Created by leslie on 10/11/24.
6 | // Copyright © 2024 jamf. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import os.log
11 |
12 | class Headless: NSObject {
13 |
14 | static let shared = Headless()
15 | private override init() { }
16 |
17 | func runComplete(backupDate: DateFormatter, nodesMigrated: Int, objectsToMigrate: [String], counters: [String:[String:Int]]) {
18 | if export.backupMode {
19 |
20 | Logger.headless_runComplete.debug("enter headless_runComplete - \(objectsToMigrate.description, privacy: .public)")
21 |
22 | // if theOpQ.operationCount == 0 && nodesMigrated > 0 {
23 | Utilities.shared.zipIt(args: "cd \"\(export.saveLocation)\" ; /usr/bin/zip -rm -o \(JamfProServer.source.fqdnFromUrl)_export_\(backupDate.string(from: History.startTime)).zip \(JamfProServer.source.fqdnFromUrl)_export_\(backupDate.string(from: History.startTime))/") {
24 | (result: String) in
25 | // print("zipIt result: \(result)")
26 | do {
27 | if fm.fileExists(atPath: "\"\(export.saveLocation)\(JamfProServer.source.fqdnFromUrl)_export_\(backupDate.string(from: History.startTime))\"") {
28 | try fm.removeItem(at: URL(string: "\"\(export.saveLocation)\(JamfProServer.source.fqdnFromUrl)_export_\(backupDate.string(from: History.startTime))\"")!)
29 | }
30 | WriteToLog.shared.message("[Backup Complete] Backup created: \(export.saveLocation)\(JamfProServer.source.fqdnFromUrl)_export_\(backupDate.string(from: History.startTime)).zip")
31 |
32 | let (h,m,s, _) = timeDiff(forWhat: "runTime")
33 | WriteToLog.shared.message("[Backup Complete] runtime: \(Utilities.shared.dd(value: h)):\(Utilities.shared.dd(value: m)):\(Utilities.shared.dd(value: s)) (h:m:s)")
34 | } catch let error as NSError {
35 | if LogLevel.debug { WriteToLog.shared.message("Unable to delete backup folder! Something went wrong: \(error)") }
36 | }
37 | }
38 |
39 | WriteToLog.shared.logCleanup()
40 | NSApplication.shared.terminate(self)
41 | // } //zipIt(args: "cd - end
42 | } else {
43 | if nodesMigrated > 0 {
44 | // print("summaryDict: \(summaryDict)")
45 | // print("counters: \(counters)")
46 | var summary = ""
47 | var otherLine: Bool = true
48 | var paddingChar = " "
49 | let sortedObjects = ToMigrate.objects.sorted()
50 | // find longest length of objects migrated
51 | var column1Padding = ""
52 | for theObject in ToMigrate.objects {
53 | if theObject.count+1 > column1Padding.count {
54 | column1Padding = "".padding(toLength: theObject.count+1, withPad: " ", startingAt: 0)
55 | }
56 | }
57 | let leading = LogLevel.debug ? " ":" "
58 |
59 | summary = " ".padding(toLength: column1Padding.count-7, withPad: " ", startingAt: 0) + "Object".padding(toLength: 7, withPad: " ", startingAt: 0) +
60 | "created".padding(toLength: 10, withPad: " ", startingAt: 0) +
61 | "updated".padding(toLength: 10, withPad: " ", startingAt: 0) +
62 | "failed".padding(toLength: 10, withPad: " ", startingAt: 0) +
63 | "total".padding(toLength: 10, withPad: " ", startingAt: 0) + "\n"
64 | for theObject in sortedObjects {
65 | if Counter.shared.crud[theObject] != nil {
66 | let counts = Counter.shared.crud[theObject]!
67 | let rightJustify = leading.padding(toLength: leading.count+(column1Padding.count-theObject.count-2), withPad: " ", startingAt: 0)
68 | otherLine.toggle()
69 | paddingChar = otherLine ? " ":"."
70 | summary = summary.appending(rightJustify + "\(theObject)".padding(toLength: column1Padding.count+(7-"\(counts["create"]!)".count-(column1Padding.count-theObject.count-1)), withPad: paddingChar, startingAt: 0) +
71 | "\(String(describing: counts["create"]!))".padding(toLength: (10-"\(counts["update"]!)".count+"\(counts["create"]!)".count), withPad: paddingChar, startingAt: 0) +
72 | "\(String(describing: counts["update"]!))".padding(toLength: (9-"\(counts["fail"]!)".count+"\(counts["update"]!)".count), withPad: paddingChar, startingAt: 0) +
73 | "\(String(describing: counts["fail"]!))".padding(toLength: (9-"\(counts["total"]!)".count+"\(counts["fail"]!)".count), withPad: paddingChar, startingAt: 0) +
74 | "\(String(describing: counts["total"]!))".padding(toLength: 10, withPad: " ", startingAt: 0) + "")
75 | }
76 | }
77 | WriteToLog.shared.message(summary)
78 | let (h,m,s, _) = timeDiff(forWhat: "runTime")
79 | WriteToLog.shared.message("[Migration Complete] runtime: \(Utilities.shared.dd(value: h)):\(Utilities.shared.dd(value: m)):\(Utilities.shared.dd(value: s)) (h:m:s)")
80 |
81 | WriteToLog.shared.logCleanup()
82 | NSApplication.shared.terminate(self)
83 | }
84 | }
85 |
86 | }
87 |
88 | }
89 |
90 |
--------------------------------------------------------------------------------
/Replicator/HelpViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HelpViewController.swift
3 | // Replicator
4 | //
5 | // Created by Leslie Helou on 7/19/17.
6 | // Copyright 2024 Jamf. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import WebKit
11 |
12 | @available(OSX 10.11, *)
13 | class HelpViewController: NSViewController {
14 |
15 | @IBOutlet weak var help_WebView: WKWebView!
16 |
17 |
18 | // @IBAction func dismissHelpWindow(_ sender: NSButton) {
19 | // let application = NSApplication.shared()
20 | // application.stopModal()
21 | // }
22 |
23 | override func viewDidLoad() {
24 | super.viewDidLoad()
25 | // Do view setup here.
26 |
27 | let filePath = Bundle.main.path(forResource: "index", ofType: "html")
28 | let folderPath = Bundle.main.resourcePath
29 |
30 | let fileUrl = URL(fileURLWithPath: filePath!)
31 | let baseUrl = URL(fileURLWithPath: folderPath!, isDirectory: true)
32 |
33 | help_WebView.loadFileURL(fileUrl as URL, allowingReadAccessTo: baseUrl as URL)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Replicator/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleVersion
22 | 20250321-53E
23 | LSApplicationCategoryType
24 | public.app-category.utilities
25 | LSMinimumSystemVersion
26 | $(MACOSX_DEPLOYMENT_TARGET)
27 | LSUIElement
28 |
29 | NSAppTransportSecurity
30 |
31 | NSAllowsArbitraryLoads
32 |
33 |
34 | NSHumanReadableCopyright
35 | Copyright 2024 Jamf. All rights reserved.
36 | NSMainStoryboardFile
37 | Main
38 | NSPrincipalClass
39 | NSApplication
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Replicator/Json.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Json.swift
3 | // Replicator
4 | //
5 | // Created by Leslie Helou on 12/1/19.
6 | // Copyright 2024 Jamf. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class Json: NSObject, URLSessionDelegate {
12 |
13 | static let shared = Json()
14 |
15 | func getRecord(whichServer: String, base64Creds: String, theEndpoint: String, endpointBase: String = "0", endpointId: String = "0", completion: @escaping (_ objectRecord: Any) -> Void) {
16 |
17 | if theEndpoint == "skip" {
18 | completion([:])
19 | return
20 | }
21 |
22 | var existingDestUrl = (whichServer == "source") ? JamfProServer.source : JamfProServer.destination
23 | let objectEndpoint = theEndpoint.replacingOccurrences(of: "//", with: "/")
24 | WriteToLog.shared.message("[Json.getRecord] get endpoint: \(objectEndpoint) from server: \(existingDestUrl)")
25 |
26 | URLCache.shared.removeAllCachedResponses()
27 |
28 | switch endpointBase {
29 | case "patch-software-title-configurations":
30 | let theRecord = (whichServer == "source") ? PatchTitleConfigurations.source.filter({ $0.id == endpointId }):PatchTitleConfigurations.destination.filter({ $0.id == endpointId })
31 | if theRecord.count == 1 {
32 | print("[getRecord] [Json.getRecord] theRecord displayName \(theRecord[0].displayName)")
33 | completion(theRecord[0])
34 | } else {
35 | completion([])
36 | }
37 | return
38 |
39 | default:
40 | if ["jamfusers", "jamfgroups"].contains(objectEndpoint) {
41 | existingDestUrl = existingDestUrl.appending("/JSSResource/accounts").urlFix
42 | } else {
43 | existingDestUrl = existingDestUrl.appending("/JSSResource/\(objectEndpoint)").urlFix
44 | }
45 | }
46 |
47 | existingDestUrl = existingDestUrl.urlFix
48 |
49 | if LogLevel.debug { WriteToLog.shared.message("[Json.getRecord] Looking up: \(existingDestUrl)") }
50 | print("[Json.getRecord] existing endpoints URL: \(existingDestUrl)")
51 |
52 | let destEncodedURL = URL(string: existingDestUrl)
53 | let jsonRequest = NSMutableURLRequest(url: destEncodedURL! as URL)
54 |
55 | jsonRequest.httpMethod = "GET"
56 | let destConf = URLSessionConfiguration.ephemeral
57 |
58 | destConf.httpAdditionalHeaders = ["Authorization" : "\(JamfProServer.authType[whichServer] ?? "Bearer") \(JamfProServer.authCreds[whichServer] ?? "")", "Content-Type" : "application/json", "Accept" : "application/json", "User-Agent" : AppInfo.userAgentHeader]
59 |
60 | var headers = [String: String]()
61 | for (header, value) in destConf.httpAdditionalHeaders ?? [:] {
62 | headers[header as! String] = (header as! String == "Authorization") ? "Bearer ************" : value as? String
63 | }
64 | print("[apiCall] \(#function.description) method: \(jsonRequest.httpMethod)")
65 | print("[apiCall] \(#function.description) headers: \(headers)")
66 | print("[apiCall] \(#function.description) endpoint: \(destEncodedURL?.absoluteString ?? "")")
67 | print("[apiCall]")
68 |
69 |
70 | q.getRecord.maxConcurrentOperationCount = userDefaults.integer(forKey: "concurrentThreads")
71 |
72 | let semaphore = DispatchSemaphore(value: 0)
73 | q.getRecord.addOperation {
74 | let destSession = Foundation.URLSession(configuration: destConf, delegate: self, delegateQueue: OperationQueue.main)
75 | let task = destSession.dataTask(with: jsonRequest as URLRequest, completionHandler: { [self]
76 | (data, response, error) -> Void in
77 | destSession.finishTasksAndInvalidate()
78 | if let httpResponse = response as? HTTPURLResponse {
79 | // print("[Json.getRecord] httpResponse: \(String(describing: httpResponse))")
80 | if httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299 {
81 | do {
82 | let json = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments)
83 | if let endpointJSON = json as? [String: AnyObject] {
84 | WriteToLog.shared.message("[Json.getRecord] retrieved \(theEndpoint)")
85 | // print("[getRecord] [Json.getRecord] \(endpointJSON)")
86 | var finalJson = [String: AnyObject]()
87 | if theEndpoint == "policies" {
88 | finalJson = ["policies": policyCleanup(policies: endpointJSON["policies"] as! [[String : AnyObject]])]
89 | } else {
90 | finalJson = endpointJSON
91 | }
92 | if LogLevel.debug { WriteToLog.shared.message("[Json.getRecord] \(finalJson)") }
93 | completion(finalJson)
94 | } else {
95 | WriteToLog.shared.message("[Json.getRecord] error parsing JSON for \(existingDestUrl)")
96 | completion([:])
97 | }
98 | }
99 | } else {
100 | WriteToLog.shared.message("[Json.getRecord] error getting \(theEndpoint), HTTP Status Code: \(httpResponse.statusCode)")
101 | completion([:])
102 | }
103 | } else {
104 | WriteToLog.shared.message("[Json.getRecord] unknown response from \(existingDestUrl)")
105 | completion([:])
106 | } // if let httpResponse - end
107 | semaphore.signal()
108 | if error != nil {
109 | }
110 | }) // let task = destSession - end
111 | //print("GET")
112 | task.resume()
113 | semaphore.wait()
114 | } // getRecordQ - end
115 | }
116 |
117 | private func policyCleanup(policies: [[String: AnyObject]]) -> AnyObject {
118 | var cleanPolicies = [[String: AnyObject]]()
119 | for thePolicy in policies {
120 | if let policyId = thePolicy["id"], let policyName = thePolicy["name"] as? String {
121 | if policyName.range(of:"[0-9]{4}-[0-9]{2}-[0-9]{2} at [0-9]", options: .regularExpression) == nil && policyName != "Update Inventory" {
122 | // print("[ExistingObjects.capi] [\(existingEndpointNode)] adding \(destXmlName) (id: \(String(describing: destXmlID!))) to currentEP array.")
123 | if LogLevel.debug { WriteToLog.shared.message("[ExistingObjects.capi] adding \(policyName) (id: \(String(describing: policyId))) to policies array.") }
124 | cleanPolicies.append(thePolicy)
125 | }
126 | }
127 | }
128 | return cleanPolicies as AnyObject
129 | }
130 | }
131 |
132 |
--------------------------------------------------------------------------------
/Replicator/JsonObjects.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright 2024, Jamf
3 | //
4 |
5 | // This file was generated from JSON Schema using quicktype, do not modify it directly.
6 | // To parse the JSON, add this file to your project and do:
7 | //
8 | // let patchSoftwareTitleConfigurations = try? JSONDecoder().decode(PatchSoftwareTitleConfigurations.self, from: jsonData)
9 |
10 | import Foundation
11 |
12 | // MARK: - NamdId
13 | struct NameId: Codable {
14 | let id: Int
15 | let name: String
16 |
17 | init(id: Int, name: String) {
18 | self.id = id
19 | self.name = name
20 | }
21 | }
22 |
23 | //class ExternalPatchSource {
24 | // static var source = [NameId]()
25 | // static var destination = [NameId]()
26 | //}
27 |
28 | class PatchSource {
29 | // static var source = [NameId]()
30 | static var destination = [NameId]()
31 | }
32 |
33 | // MARK: - Category
34 | struct Category: Codable {
35 | let id: String
36 | let name: String
37 | let priority: Int
38 |
39 | init(id: String, name: String, priority: Int) {
40 | self.id = id
41 | self.name = name
42 | self.priority = priority
43 | }
44 | }
45 |
46 | class Categories {
47 | static var source = [Category]()
48 | static var destination = [Category]()
49 | }
50 |
51 | // MARK: - Site
52 | struct Site: Codable {
53 | let id: String
54 | let name: String
55 |
56 | init(id: String, name: String) {
57 | self.id = id
58 | self.name = name
59 | }
60 | }
61 |
62 | class JamfProSites {
63 | static var source = [Site]()
64 | static var destination = [Site]()
65 | }
66 |
67 | // MARK: - PatchSoftwareTitleConfiguration
68 | struct PatchSoftwareTitleConfiguration: Codable {
69 | let id: String?
70 | let jamfOfficial: Bool
71 | let displayName, categoryId, siteId: String
72 | var categoryName, siteName: String?
73 | let uiNotifications, emailNotifications: Bool
74 | let softwareTitleId: String
75 | let extensionAttributes: [ExtensionAttribute]
76 | let softwareTitleName, softwareTitleNameId, softwareTitlePublisher, patchSourceName: String
77 | let patchSourceEnabled: Bool
78 | var packages: [PatchPackage]
79 |
80 | enum CodingKeys: String, CodingKey {
81 | case id, jamfOfficial, displayName, categoryId, categoryName, siteId, siteName
82 | case uiNotifications, emailNotifications, softwareTitleId
83 | case extensionAttributes, softwareTitleName, softwareTitleNameId
84 | case softwareTitlePublisher, patchSourceName, patchSourceEnabled, packages
85 | }
86 |
87 | init(id: String?, jamfOfficial: Bool, displayName: String, categoryId: String, categoryName: String?, siteId: String, siteName: String?, uiNotifications: Bool, emailNotifications: Bool, softwareTitleId: String, extensionAttributes: [ExtensionAttribute], softwareTitleName: String, softwareTitleNameId: String, softwareTitlePublisher: String, patchSourceName: String, patchSourceEnabled: Bool, packages: [PatchPackage]) {
88 | self.id = id
89 | self.jamfOfficial = jamfOfficial
90 | self.displayName = displayName
91 | self.categoryId = categoryId
92 | self.categoryName = categoryName
93 | self.siteId = siteId
94 | self.siteName = siteName
95 | self.uiNotifications = uiNotifications
96 | self.emailNotifications = emailNotifications
97 | self.softwareTitleId = softwareTitleId
98 | self.extensionAttributes = extensionAttributes
99 | self.softwareTitleName = softwareTitleName
100 | self.softwareTitleNameId = softwareTitleNameId
101 | self.softwareTitlePublisher = softwareTitlePublisher
102 | self.patchSourceName = patchSourceName
103 | self.patchSourceEnabled = patchSourceEnabled
104 | self.packages = packages
105 | }
106 | }
107 |
108 | // MARK: - ExtensionAttribute
109 | struct ExtensionAttribute: Codable {
110 | let accepted: Bool
111 | let eaId: String
112 |
113 | enum CodingKeys: String, CodingKey {
114 | case accepted
115 | case eaId
116 | }
117 | }
118 |
119 | struct ObjectAndDependency: Codable {
120 | let objectType: String
121 | let objectName: String
122 | let objectId: String
123 |
124 | enum CodingKeys: String, CodingKey {
125 | case objectType
126 | case objectName
127 | case objectId
128 | }
129 | }
130 | struct ObjectAndDependencies {
131 | static var records: [ObjectAndDependency] = []
132 | }
133 |
134 | // MARK: - PatchPackage
135 | struct PatchPackage: Codable {
136 | let packageId, version: String
137 | var packageName, displayName: String?
138 |
139 | enum CodingKeys: String, CodingKey {
140 | case packageId
141 | case version, displayName, packageName
142 | }
143 |
144 | init(packageId: String, version: String, displayName: String?, packageName: String?) {
145 | self.packageId = packageId
146 | self.version = version
147 | self.displayName = displayName
148 | self.packageName = packageName
149 |
150 | }
151 | }
152 | class PatchPackages {
153 | static var source = [PatchPackage]()
154 | static var destination = [PatchPackage]()
155 | }
156 |
157 | // MARK: - PatchPolicyDetails
158 | struct PatchPolicyDetail: Codable {
159 | let id, name: String
160 | let enabled: Bool
161 | let targetPatchVersion, deploymentMethod, softwareTitleId, softwareTitleConfigurationId: String
162 | let killAppsDelayMinutes: Int
163 | let killAppsMessage: String
164 | let downgrade, patchUnknownVersion: Bool
165 | let notificationHeader: String
166 | let selfServiceEnforceDeadline: Bool
167 | let selfServiceDeadline: Int
168 | let installButtonText, selfServiceDescription, iconId: String
169 | let reminderFrequency: Int
170 | let reminderEnabled: Bool
171 |
172 | enum CodingKeys: String, CodingKey {
173 | case id, name, enabled, targetPatchVersion, deploymentMethod
174 | case softwareTitleId
175 | case softwareTitleConfigurationId
176 | case killAppsDelayMinutes, killAppsMessage, downgrade, patchUnknownVersion, notificationHeader, selfServiceEnforceDeadline, selfServiceDeadline, installButtonText, selfServiceDescription
177 | case iconId
178 | case reminderFrequency, reminderEnabled
179 | }
180 | }
181 |
182 | class PatchPoliciesDetails {
183 | static var source = [PatchPolicyDetail]()
184 | static var destination = [PatchPolicyDetail]()
185 | }
186 |
187 | typealias PatchSoftwareTitleConfigurations = [PatchSoftwareTitleConfiguration]
188 | class PatchTitleConfigurations {
189 | static var source = PatchSoftwareTitleConfigurations()
190 | static var destination = PatchSoftwareTitleConfigurations()
191 | }
192 |
--------------------------------------------------------------------------------
/Replicator/LastUser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LastUser.swift
3 | // Replicator
4 | //
5 | // Created by leslie on 3/14/25.
6 | // Copyright © 2025 Jamf. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // Define a Codable struct for each entry
12 | struct ServerEntry: Codable {
13 | var server: String
14 | var lastUser: String?
15 | var apiClient: Bool?
16 |
17 | // Custom decoding to handle missing values
18 | init(from decoder: Decoder) throws {
19 | let container = try decoder.container(keyedBy: CodingKeys.self)
20 | server = try container.decode(String.self, forKey: .server)
21 | lastUser = try container.decodeIfPresent(String.self, forKey: .lastUser) ?? ""
22 | apiClient = try container.decodeIfPresent(Bool.self, forKey: .apiClient) ?? false
23 | }
24 |
25 | // Default initializer
26 | init(server: String, lastUser: String? = nil, apiClient: Bool? = nil) {
27 | self.server = server
28 | self.lastUser = lastUser ?? ""
29 | self.apiClient = apiClient ?? false
30 | }
31 | }
32 |
33 | // Class to manage the array of dictionaries
34 | class LastUserManager {
35 | private var servers: [ServerEntry] = []
36 | // private let filePath: String
37 |
38 | init() {
39 | // let homeDir = NSHomeDirectory()
40 | // let fileURL = homeDir + "/Library/Application Support/Replicator/lastUser.json"
41 | // self.filePath = fileURL
42 | loadFromFile()
43 | }
44 |
45 | // Load JSON from file
46 | func loadFromFile() {
47 | guard FileManager.default.fileExists(atPath: AppInfo.lastUserPath) else {
48 | print("File does not exist, starting with an empty list.")
49 | return
50 | }
51 | let fileURL = URL(fileURLWithPath: AppInfo.lastUserPath)
52 | do {
53 | let data = try Data(contentsOf: fileURL)
54 | servers = try JSONDecoder().decode([ServerEntry].self, from: data)
55 | } catch {
56 | print("Failed to read JSON from file: \(error)")
57 | }
58 | }
59 |
60 | // Save JSON to file
61 | func saveToFile() {
62 | let fileURL = URL(fileURLWithPath: AppInfo.lastUserPath)
63 | do {
64 | let data = try JSONEncoder().encode(servers)
65 | try FileManager.default.createDirectory(at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true)
66 | try data.write(to: fileURL, options: .atomic)
67 | } catch {
68 | print("Failed to save JSON to file: \(error)")
69 | }
70 | }
71 |
72 | // Convert to JSON string
73 | func jsonString() -> String {
74 | do {
75 | let data = try JSONEncoder().encode(servers)
76 | return String(data: data, encoding: .utf8) ?? "[]"
77 | } catch {
78 | print("Failed to encode JSON: \(error)")
79 | return "[]"
80 | }
81 | }
82 |
83 | // Add a new entry
84 | func add(server: String, lastUser: String? = nil, apiClient: Bool? = nil) {
85 | servers.append(ServerEntry(server: server, lastUser: lastUser, apiClient: apiClient))
86 | saveToFile()
87 | }
88 |
89 | // Remove an entry by server name
90 | func remove(server: String) {
91 | servers.removeAll { $0.server == server }
92 | saveToFile()
93 | }
94 |
95 | // Update an existing entry
96 | func update(server: String, lastUser: String?, apiClient: Bool?) {
97 | if let index = servers.firstIndex(where: { $0.server == server }) {
98 | if let lastUser = lastUser {
99 | servers[index].lastUser = lastUser
100 | }
101 | if let apiClient = apiClient {
102 | servers[index].apiClient = apiClient
103 | }
104 | saveToFile()
105 | }
106 | }
107 |
108 | // Query by server name
109 | func query(server: String) -> (lastUser: String, apiClient: Bool)? {
110 | print("lastUser query \(server)")
111 | if let entry = servers.first(where: { $0.server == server }) {
112 | print("lastUser query lastUser: \(entry.lastUser ?? ""), apiClient: \(entry.apiClient ?? false)")
113 | return (entry.lastUser ?? "", entry.apiClient ?? false)
114 | }
115 | return nil
116 | }
117 | }
118 |
119 | /*
120 | // Example Usage
121 | let manager = ServerManager()
122 | manager.add(server: "server1.example.com", lastUser: "admin", apiClient: true)
123 | manager.add(server: "server2.example.com", lastUser: "user", apiClient: false)
124 |
125 | if let result = manager.query(server: "server1.example.com") {
126 | print("Last User: \(result.lastUser), API Client: \(result.apiClient)")
127 | update(server: server, lastUser: , apiClient: )
128 | } else {
129 | print("Server not found.")
130 | add(server: server, lastUser: , apiClient: )
131 | }
132 | */
133 |
--------------------------------------------------------------------------------
/Replicator/LoggerExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoggerExtension
3 | //
4 |
5 | import Foundation
6 | import os.log
7 |
8 | extension Logger {
9 | private static let subsystem = Bundle.main.bundleIdentifier!
10 |
11 | static let writeToLog = Logger(subsystem: subsystem, category: "writeToLog")
12 | static let token = Logger(subsystem: subsystem, category: "token")
13 | static let cleanup_json = Logger(subsystem: subsystem, category: "cleanup_json")
14 | static let cleanup_xml = Logger(subsystem: subsystem, category: "cleanup_xml")
15 | static let createEndpoints_queue = Logger(subsystem: subsystem, category: "createEndpoints_queue")
16 | static let createEndpoints_capi = Logger(subsystem: subsystem, category: "createEndpoints_capi")
17 | static let createEndpoints_jpapi = Logger(subsystem: subsystem, category: "createEndpoints_jpapi")
18 | static let endpointXml_endPointByIdQueue = Logger(subsystem: subsystem, category: "endpointXml_endPointByIdQueue")
19 | static let endpointXml_getById = Logger(subsystem: subsystem, category: "endpointXml_getById")
20 | static let existingObjects_capi = Logger(subsystem: subsystem, category: "existingObjects_capi")
21 | static let exportItem_export = Logger(subsystem: subsystem, category: "exportItem_export")
22 | static let iconDelegate_icons = Logger(subsystem: subsystem, category: "iconDelegate_icons")
23 | static let iconDelegate_iconMigrate = Logger(subsystem: subsystem, category: "iconDelegate_iconMigrate")
24 | static let ObjectDelegate_getAll = Logger(subsystem: subsystem, category: "ObjectDelegate_getAll")
25 | static let headless_runComplete = Logger(subsystem: subsystem, category: "headless_runComplete")
26 | }
27 |
--------------------------------------------------------------------------------
/Replicator/ObjectDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Untitled.swift
3 | // Replicator
4 | //
5 | // Created by leslie on 12/6/24.
6 | // Copyright © 2024 Jamf. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os.log
11 |
12 | var duplicatePackages = false
13 | var duplicatePackagesDict = [String:[String]]()
14 | //var failedPkgNameLookup = [String]()
15 |
16 | class ObjectDelegate: NSObject, URLSessionDelegate {
17 |
18 | static let shared = ObjectDelegate()
19 |
20 | func getAll(whichServer: String, endpoint: String, completion: @escaping (_ result: [Any]) -> Void) {
21 | print("[ObjectDelegate] getAll \(whichServer) server, endpoint: \(endpoint)")
22 |
23 | Logger.ObjectDelegate_getAll.debug("enter ObjectDelegate_getAll - \(endpoint, privacy: .public)")
24 |
25 | if Counter.shared.crud[endpoint] == nil {
26 | Counter.shared.crud[endpoint] = ["create":0, "update":0, "fail":0, "skipped":0, "total":0]
27 | Counter.shared.summary[endpoint] = ["create":[], "update":[], "fail":[]]
28 | }
29 |
30 | switch endpoint {
31 | case "packages":
32 | duplicatePackages = false
33 | duplicatePackagesDict.removeAll()
34 | Jpapi.shared.getAllDelegate(whichServer: (WipeData.state.on ? "dest":whichServer), theEndpoint: endpoint, whichPage: 0) {
35 | result in
36 | completion(result)
37 | }
38 | case "patch-software-title-configurations":
39 | Jpapi.shared.get(whichServer: (WipeData.state.on ? "dest":whichServer), theEndpoint: endpoint) {
40 | result in
41 | completion(result as [Any])
42 | }
43 | default:
44 | Json.shared.getRecord(whichServer: (WipeData.state.on ? "dest":whichServer), base64Creds: "", theEndpoint: endpoint) {
45 | (result: Any) in
46 | // print("[ObjectDelegate.getAll] default - \(endpoint): \(result)")
47 | completion([result])
48 | }
49 | }
50 | }
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/Replicator/Package.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright 2024, Jamf
3 | //
4 |
5 | import Foundation
6 |
7 | struct Package: Codable, Identifiable {
8 | var id = UUID()
9 | var jamfProId: Int?
10 | var displayName: String
11 | var fileName: String
12 | var size: Int64?
13 | var category: String?
14 | var categoryId: String?
15 | var info: String?
16 | var notes: String?
17 | var priority: Int?
18 | var osRequirements: String?
19 | var fillUserTemplate: Bool?
20 | var indexed: Bool? // Not to be updated
21 | var uninstall: Bool? // Not to be updated
22 | var fillExistingUsers: Bool?
23 | var swu: Bool?
24 | var rebootRequired: Bool?
25 | var selfHealNotify: Bool?
26 | var selfHealingAction: String?
27 | var osInstall: Bool?
28 | var serialNumber: String?
29 | var parentPackageId: String?
30 | var basePath: String?
31 | var suppressUpdates: Bool?
32 | var cloudTransferStatus: String? // Not to be updated
33 | var ignoreConflicts: Bool?
34 | var suppressFromDock: Bool?
35 | var suppressEula: Bool?
36 | var suppressRegistration: Bool?
37 | var installLanguage: String?
38 | var osInstallerVersion: String?
39 | var manifest: String?
40 | var manifestFileName: String?
41 | var format: String?
42 | var install_if_reported_available: String?
43 | var reinstall_option: String?
44 | var send_notification: Bool?
45 | var switch_with_package: String?
46 | var triggering_files: [String: String]?
47 |
48 | init(jamfProId: Int?, displayName: String, fileName: String, category: String, size: Int64?) {
49 | self.jamfProId = jamfProId
50 | self.displayName = displayName
51 | self.fileName = fileName
52 | self.category = category
53 | self.size = size
54 | }
55 |
56 | /*
57 | init(capiPackageDetail: JsonCapiPackageDetail) {
58 | jamfProId = capiPackageDetail.id
59 | displayName = capiPackageDetail.name ?? ""
60 | fileName = capiPackageDetail.filename ?? ""
61 | category = capiPackageDetail.category ?? "None"
62 | let hashType = capiPackageDetail.hash_type ?? "MD5"
63 | let hashValue = capiPackageDetail.hash_value
64 | // if let hashValue, !hashValue.isEmpty {
65 | // checksums.updateChecksum(Checksum(type: ChecksumType.fromRawValue(hashType), value: hashValue))
66 | // }
67 | info = capiPackageDetail.info
68 | notes = capiPackageDetail.notes
69 | priority = capiPackageDetail.priority
70 | osRequirements = capiPackageDetail.os_requirements
71 | fillUserTemplate = capiPackageDetail.fill_user_template
72 | fillExistingUsers = capiPackageDetail.fill_existing_users
73 | rebootRequired = capiPackageDetail.reboot_required
74 | osInstallerVersion = capiPackageDetail.os_requirements
75 | install_if_reported_available = capiPackageDetail.install_if_reported_available
76 | reinstall_option = capiPackageDetail.reinstall_option
77 | send_notification = capiPackageDetail.send_notification
78 | switch_with_package = capiPackageDetail.switch_with_package
79 | triggering_files = capiPackageDetail.triggering_files
80 | }
81 | */
82 |
83 |
84 | init(uapiPackageDetail: JsonUapiPackageDetail) {
85 | if let jamfProIdString = uapiPackageDetail.id, let jamfProId = Int(jamfProIdString) {
86 | self.jamfProId = jamfProId
87 | }
88 | self.displayName = uapiPackageDetail.packageName ?? ""
89 | self.fileName = uapiPackageDetail.fileName ?? ""
90 | self.categoryId = uapiPackageDetail.categoryId ?? "-1"
91 | // if let md5Value = uapiPackageDetail.md5, !md5Value.isEmpty {
92 | // self.checksums.updateChecksum(Checksum(type: .MD5, value: md5Value))
93 | // }
94 | // if let sha256Value = uapiPackageDetail.sha256, !sha256Value.isEmpty {
95 | // self.checksums.updateChecksum(Checksum(type: .SHA_256, value: sha256Value))
96 | // }
97 | // if let hashType = uapiPackageDetail.hashType, !hashType.isEmpty, let hashValue = uapiPackageDetail.hashValue, !hashValue.isEmpty {
98 | // self.checksums.updateChecksum(Checksum(type: ChecksumType.fromRawValue(hashType), value: hashValue))
99 | // }
100 | if let sizeString = uapiPackageDetail.size {
101 | self.size = Int64(sizeString)
102 | }
103 | self.info = uapiPackageDetail.info
104 | self.notes = uapiPackageDetail.notes
105 | self.priority = uapiPackageDetail.priority
106 | self.osRequirements = uapiPackageDetail.osRequirements
107 | self.fillUserTemplate = uapiPackageDetail.fillUserTemplate
108 | self.indexed = uapiPackageDetail.indexed
109 | self.uninstall = uapiPackageDetail.uninstall
110 | self.fillExistingUsers = uapiPackageDetail.fillExistingUsers
111 | self.swu = uapiPackageDetail.swu
112 | self.rebootRequired = uapiPackageDetail.rebootRequired
113 | self.selfHealNotify = uapiPackageDetail.selfHealNotify
114 | self.selfHealingAction = uapiPackageDetail.selfHealingAction
115 | self.osInstall = uapiPackageDetail.osInstall
116 | self.serialNumber = uapiPackageDetail.serialNumber
117 | self.parentPackageId = uapiPackageDetail.parentPackageId
118 | self.basePath = uapiPackageDetail.basePath
119 | self.suppressUpdates = uapiPackageDetail.suppressUpdates
120 | self.cloudTransferStatus = uapiPackageDetail.cloudTransferStatus
121 | self.ignoreConflicts = uapiPackageDetail.ignoreConflicts
122 | self.suppressFromDock = uapiPackageDetail.suppressFromDock
123 | self.suppressEula = uapiPackageDetail.suppressEula
124 | self.suppressRegistration = uapiPackageDetail.suppressRegistration
125 | self.installLanguage = uapiPackageDetail.installLanguage
126 | self.osInstallerVersion = uapiPackageDetail.osInstallerVersion
127 | self.manifest = uapiPackageDetail.manifest
128 | self.manifestFileName = uapiPackageDetail.manifestFileName
129 | self.format = uapiPackageDetail.format
130 | }
131 | }
132 |
133 |
--------------------------------------------------------------------------------
/Replicator/PatchDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PatchDelegate.swift
3 | // Replicator
4 | //
5 | // Created by leslie on 11/9/24.
6 | // Copyright © 2024 Jamf. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class PatchDelegate: NSObject {
12 |
13 | static let shared = PatchDelegate()
14 | var messageDelegate: SendMessageDelegate?
15 |
16 | func updateViewController(_ text: String) {
17 | messageDelegate?.sendMessage(text)
18 | }
19 |
20 | func getDependencies(whichServer: String, completion: @escaping (_ result: String) -> Void) {
21 | if WipeData.state.on {
22 | completion("skipped")
23 | return
24 | }
25 | print("[getEndpoints] fetching category records from \(whichServer) server")
26 | self.updateViewController("fetching category records from \(whichServer) server")
27 | Jpapi.shared.getAllDelegate(whichServer: whichServer, theEndpoint: "categories", whichPage: 0) { result in
28 | print("[getEndpoints] fetching site records from \(whichServer) server")
29 | self.updateViewController("fetching site records from \(whichServer) server")
30 | Jpapi.shared.action(whichServer: whichServer, endpoint: "sites", apiData: [:], id: "", token: "", method: "GET") { result in
31 |
32 | // print("[getEndpoints] sites from \(whichServer) server: \(result["sites"])")
33 |
34 | do {
35 | let jsonData = try JSONSerialization.data(withJSONObject: result["sites"] as Any)
36 | if whichServer == "source" {
37 | JamfProSites.source = try JSONDecoder().decode([Site].self, from: jsonData)
38 | for i in 0.. Bool {
19 | DistributedNotificationCenter.default.removeObserver(self, name: .saveOnlyButtonToggle, object: nil)
20 | self.window?.orderOut(sender)
21 | return false
22 | }
23 |
24 | func show() {
25 |
26 | var prefsVisible = false
27 | let tabs = ["Copy", "Export", "Site", "App", "Computer", "Password"]
28 | // let vc = ViewController()
29 | var pwc: NSWindowController?
30 |
31 | if !(pwc != nil) {
32 | let storyboard = NSStoryboard(name: "Preferences", bundle: nil)
33 | pwc = storyboard.instantiateInitialController() as? NSWindowController
34 | }
35 |
36 | if (pwc != nil) {
37 | // if !(vc.windowIsVisible(windowName: "Copy") || vc.windowIsVisible(windowName: "Export") || vc.windowIsVisible(windowName: "Site") || vc.windowIsVisible(windowName: "App") || vc.windowIsVisible(windowName: "Computer") || vc.windowIsVisible(windowName: "Password")) {
38 | // pwc?.window?.setIsVisible(true)
39 | //
40 | // } else {
41 | DispatchQueue.main.async {
42 | // print("[PrefsWindowController] show existing preference window")
43 | // NSApp.windows[1].makeKeyAndOrderFront(self)
44 | let windowsCount = NSApp.windows.count
45 | for i in (0.. String {
15 | var newJSON = rawJSON
16 | // remove keys with as the value
17 | for (key, value) in newJSON {
18 | if "\(value)" == "" || "\(value)" == "" {
19 | newJSON[key] = nil
20 | } else {
21 | newJSON[key] = "\(value)"
22 | }
23 | }
24 | if theTag != "" {
25 | if let _ = newJSON[theTag] {
26 | newJSON[theTag] = nil
27 | }
28 | }
29 |
30 | return "\(newJSON)"
31 | }
32 |
33 | func Xml(theXML: String, theTag: String, keepTags: Bool) -> String {
34 | var newXML = ""
35 | var newXML_trimmed = ""
36 | let f_regexComp = try! NSRegularExpression(pattern: "<\(theTag)>(.|\n|\r)*?\(theTag)>", options:.caseInsensitive)
37 | if keepTags {
38 | newXML = f_regexComp.stringByReplacingMatches(in: theXML, options: [], range: NSRange(0.. ")
39 | } else {
40 | newXML = f_regexComp.stringByReplacingMatches(in: theXML, options: [], range: NSRange(0..
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.bookmarks.app-scope
8 |
9 | com.apple.security.files.downloads.read-write
10 |
11 | com.apple.security.files.user-selected.read-write
12 |
13 | com.apple.security.network.client
14 |
15 | keychain-access-groups
16 |
17 | $(AppIdentifierPrefix)jamfie.SharedJPMA
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Replicator/SaveDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SaveDelegate.swift
3 | // Replicator
4 | //
5 | // Created by Leslie Helou on 12/28/21
6 | // Copyright 2024 Jamf. All rights reserved
7 | //
8 |
9 | /*
10 | import Cocoa
11 | import Foundation
12 |
13 | class SaveDelegate: NSObject, URLSessionDelegate {
14 |
15 | // let userDefaults = UserDefaults.standard
16 | var baseFolder = ""
17 | var saveFolder = ""
18 | var endpointPath = ""
19 |
20 | func exportObject(node: String, objectString: String, rawName: String, id: String, format: String) {
21 |
22 | var name = rawName.replacingOccurrences(of: ":", with: ";")
23 | name = name.replacingOccurrences(of: "/", with: ":")
24 | if LogLevel.debug { WriteToLog.shared.message("[SaveDelegate.exportObject] saving \(name), format: \(format), to folder \(node)") }
25 | // Create folder to store objectString files if needed - start
26 | // let saveURL = userDefaults.url(forKey: "saveLocation") ?? nil
27 | baseFolder = userDefaults.string(forKey: "saveLocation") ?? ""
28 | if baseFolder == "" {
29 | baseFolder = (NSHomeDirectory() + "/Downloads/Replicator/")
30 | } else {
31 | baseFolder = baseFolder.replacingOccurrences(of: "file://", with: "")
32 | baseFolder = baseFolder.replacingOccurrences(of: "%20", with: " ")
33 | }
34 |
35 | saveFolder = baseFolder+format+"/"
36 |
37 | if !(fm.fileExists(atPath: saveFolder)) {
38 | do {
39 | try fm.createDirectory(atPath: saveFolder, withIntermediateDirectories: true, attributes: nil)
40 | } catch {
41 | if LogLevel.debug { WriteToLog.shared.message("[SaveDelegate.exportObject] Problem creating \(saveFolder) folder: Error \(error)") }
42 | return
43 | }
44 | }
45 | // Create folder to store objectString files if needed - end
46 |
47 | print("[SaveDelegate] node: \(node)")
48 |
49 | // Create endpoint type to store objectString files if needed - start
50 | switch node {
51 | case "selfservicepolicyicon", "macapplicationsicon", "mobiledeviceapplicationsicon":
52 | endpointPath = saveFolder+node+"/\(id)"
53 | case "accounts/groupid":
54 | endpointPath = saveFolder+"jamfgroups"
55 | case "accounts/userid":
56 | endpointPath = saveFolder+"jamfusers"
57 | case "computergroups":
58 | let isSmart = tagValue2(xmlString: objectString, startTag: "", endTag: " ")
59 | if isSmart == "true" {
60 | endpointPath = saveFolder+"smartcomputergroups"
61 | } else {
62 | endpointPath = saveFolder+"staticcomputergroups"
63 | }
64 | default:
65 | endpointPath = saveFolder+node
66 | }
67 | if !(fm.fileExists(atPath: endpointPath)) {
68 | do {
69 | try fm.createDirectory(atPath: endpointPath, withIntermediateDirectories: true, attributes: nil)
70 | } catch {
71 | if LogLevel.debug { WriteToLog.shared.message("[SaveDelegate.exportObject] Problem creating \(endpointPath) folder: Error \(error)") }
72 | return
73 | }
74 | }
75 | // Create endpoint type to store objectString files if needed - end
76 |
77 | switch node {
78 | case "buildings":
79 | let jsonFile = "\(name)-\(id).json"
80 | var jsonString = objectString.dropFirst().dropLast()
81 | jsonString = "{\(jsonString)}"
82 | do {
83 | try jsonString.write(toFile: endpointPath+"/"+jsonFile, atomically: true, encoding: .utf8)
84 | if LogLevel.debug { WriteToLog.shared.message("[SaveDelegate.exportObject] saved to: \(endpointPath)") }
85 | } catch {
86 | if LogLevel.debug { WriteToLog.shared.message("[SaveDelegate.exportObject] Problem writing \(endpointPath) folder: Error \(error)") }
87 | return
88 | }
89 |
90 | case "selfservicepolicyicon", "macapplicationsicon", "mobiledeviceapplicationsicon":
91 |
92 | var copyIcon = true
93 | let iconSource = "\(objectString)"
94 | let iconDest = "\(endpointPath)/\(name)"
95 |
96 | // print("copy from \(iconSource) to: \(iconDest)")
97 | if fm.fileExists(atPath: iconDest) {
98 | do {
99 | if LogLevel.debug { WriteToLog.shared.message("[SaveDelegate.exportObject] removing currently saved icon: \(iconDest)") }
100 | try FileManager.default.removeItem(at: URL(fileURLWithPath: iconDest))
101 | }
102 | catch let error as NSError {
103 | if LogLevel.debug { WriteToLog.shared.message("[SaveDelegate.exportObject] unable to delete cached icon: \(iconDest). Error \(error).") }
104 | copyIcon = false
105 | }
106 | }
107 | if copyIcon {
108 | if LogLevel.debug { WriteToLog.shared.message("[SaveDelegate.exportObject] saving icon to: \(iconDest)") }
109 | do {
110 | try fm.copyItem(atPath: iconSource, toPath: iconDest)
111 | if export.saveOnly {
112 | do {
113 | if LogLevel.debug { WriteToLog.shared.message("[SaveDelegate.exportObject] removing cached icon: \(iconSource)/") }
114 | try FileManager.default.removeItem(at: URL(fileURLWithPath: "\(iconSource)/"))
115 | }
116 | catch let error as NSError {
117 | if LogLevel.debug { WriteToLog.shared.message("[SaveDelegate.exportObject] unable to delete \(iconSource)/. Error \(error)") }
118 | }
119 | }
120 |
121 | }
122 | catch let error as NSError {
123 | if LogLevel.debug { WriteToLog.shared.message("[SaveDelegate.exportObject] unable to save icon: \(iconDest). Error \(error).") }
124 | copyIcon = false
125 | }
126 | }
127 | // print("Copied \(iconSource) to: \(iconDest)")
128 |
129 | default:
130 | let xmlFile = "\(name)-\(id).xml"
131 | if let xmlDoc = try? XMLDocument(xmlString: objectString, options: .nodePrettyPrint) {
132 | if let _ = try? XMLElement.init(xmlString:"\(objectString)") {
133 | let data = xmlDoc.xmlData(options:.nodePrettyPrint)
134 | let formattedXml = String(data: data, encoding: .utf8)!
135 | // print("policy xml:\n\(formattedXml)")
136 |
137 | do {
138 | try formattedXml.write(toFile: endpointPath+"/"+xmlFile, atomically: true, encoding: .utf8)
139 | if LogLevel.debug { WriteToLog.shared.message("[SaveDelegate.exportObject] saved to: \(endpointPath)") }
140 | } catch {
141 | if LogLevel.debug { WriteToLog.shared.message("[SaveDelegate.exportObject] Problem writing \(endpointPath) folder: Error \(error)") }
142 | return
143 | }
144 | } // if let prettyXml - end
145 | }
146 | }
147 |
148 | } // func save
149 |
150 | func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping(URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
151 | completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
152 | }
153 | }
154 | */
155 |
--------------------------------------------------------------------------------
/Replicator/SecurityScopedBookmarks.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SecurityScopedBookmarks.swift
3 | // Replicator
4 | //
5 | // Created by leslie on 10/19/24.
6 | // Copyright © 2024 jamf. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class SecurityScopedBookmarks: NSObject {
12 |
13 | static let shared = SecurityScopedBookmarks()
14 |
15 | func allowAccess(for urlString: String) -> Bool {
16 |
17 | let allBookmarks = fetchBookmarks()
18 |
19 | guard let bookmarkData = allBookmarks[urlString] else {
20 | print("Bookmark data for \(urlString) does not exist.")
21 | return false
22 | }
23 |
24 | do {
25 | var isStale = false
26 | let fileURL = try URL(
27 | resolvingBookmarkData: bookmarkData,
28 | options: [.withSecurityScope],
29 | relativeTo: nil,
30 | bookmarkDataIsStale: &isStale
31 | )
32 |
33 | if isStale {
34 | print("Bookmark for \(urlString) is stale, needs to be recreated.")
35 | }
36 |
37 | if fileURL.startAccessingSecurityScopedResource() {
38 | print("Accessed security-scoped resource for \(urlString).")
39 | } else {
40 | print("Could not access security-scoped resource for \(urlString).")
41 | return false
42 | }
43 | } catch {
44 | print("Error resolving bookmark for \(urlString): \(error)")
45 | return false
46 | }
47 | return true
48 | }
49 |
50 | func create(for fileUrl: URL) {
51 | var bookmarks = fetchBookmarks()
52 | // let fileUrl = URL(fileURLWithPath: filePath)
53 |
54 | do {
55 | let bookmarkData = try fileUrl.bookmarkData(
56 | options: [.withSecurityScope],
57 | includingResourceValuesForKeys: nil,
58 | relativeTo: nil
59 | )
60 | bookmarks[fileUrl.path()] = bookmarkData
61 | } catch {
62 | print("Error creating bookmark for \(fileUrl.path()): \(error)")
63 | }
64 | userDefaults.set(bookmarks, forKey: "bookmarks")
65 | }
66 |
67 | func fetchBookmarks() -> [String: Data] {
68 | if fm.fileExists(atPath: AppInfo.bookmarksPath) {
69 | AppInfo.bookmarks = NSKeyedUnarchiver.unarchiveObject(withFile: AppInfo.bookmarksPath) as? [URL: Data] ?? [:]
70 | var currentBookmarks = [String: Data]()
71 | for (url, bookmarkData) in AppInfo.bookmarks {
72 | do {
73 | var isStale = false
74 | let _ = try URL(
75 | resolvingBookmarkData: bookmarkData,
76 | options: [.withSecurityScope],
77 | relativeTo: nil,
78 | bookmarkDataIsStale: &isStale
79 | )
80 |
81 | if isStale {
82 | print("Bookmark for \(url.path()) is stale, needs to be recreated.")
83 | } else {
84 | print("Register bookmark for \(url.path()).")
85 | currentBookmarks[url.path()] = bookmarkData
86 | }
87 |
88 | } catch {
89 | print("Error resolving bookmark for \(url.path()): \(error)")
90 | }
91 | }
92 | userDefaults.set(currentBookmarks, forKey: "bookmarks")
93 | try? FileManager.default.moveItem(at: URL(fileURLWithPath: AppInfo.bookmarksPath, isDirectory: true), to: URL(fileURLWithPath: AppInfo.bookmarksPath + ".migrated", isDirectory: true))
94 | }
95 |
96 | guard let savedBookmarks = userDefaults.dictionary(forKey: "bookmarks") as? [String: Data] else {
97 | return [:]
98 | }
99 | for (urlString, _) in savedBookmarks {
100 | print("found bookmark for: \(urlString)")
101 | }
102 |
103 | return savedBookmarks
104 | }
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/Replicator/Sites.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Sites.swift
3 | // Replicator
4 | //
5 | // Created by Leslie Helou on 8/21/19.
6 | // Copyright 2024 Jamf. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class Sites: NSObject, URLSessionDelegate {
12 |
13 | let vc = ViewController()
14 | var resourcePath = ""
15 | var base64Creds = ""
16 |
17 | var jamfpro: JamfPro?
18 |
19 | func fetch(server: String, creds: String, completion: @escaping ((Int,[String])) -> Void) {
20 |
21 | // jamfpro = JamfPro(controller: ViewController())
22 | var siteArray = [String]()
23 | // var siteDict = Dictionary()
24 | base64Creds = Data("\(creds)".utf8).base64EncodedString()
25 |
26 | if "\(server)" == "" {
27 | vc.alert_dialog(header: "Attention:", message: "Destination Jamf server is required.")
28 | completion((0,siteArray))
29 | }
30 | if "\(creds)" == ":" {
31 | vc.alert_dialog(header: "Attention:", message: "Destination credentials are required.")
32 | completion((401,siteArray))
33 | }
34 |
35 | resourcePath = "\(server)/JSSResource/sites"
36 | resourcePath = resourcePath.urlFix
37 |
38 | // get all the sites - start
39 | WriteToLog.shared.message("[Sites] Fetching sites from \(server)")
40 |
41 | getSites() {
42 | (result: [String]) in
43 | siteArray = result
44 | completion((200,siteArray))
45 | return siteArray
46 | }
47 | }
48 |
49 | func getSites(completion: @escaping ([String]) -> [String]) {
50 |
51 | var destSiteArray = [String]()
52 |
53 | let serverEncodedURL = URL(string: resourcePath)
54 | let serverRequest = NSMutableURLRequest(url: serverEncodedURL! as URL)
55 | // print("serverRequest: \(serverRequest)")
56 | serverRequest.httpMethod = "GET"
57 | let serverConf = URLSessionConfiguration.ephemeral
58 |
59 | serverConf.httpAdditionalHeaders = ["Authorization" : "\(JamfProServer.authType["dest"] ?? "Bearer") \(JamfProServer.authCreds["dest"] ?? "")", "Content-Type" : "application/json", "Accept" : "application/json", "User-Agent" : AppInfo.userAgentHeader]
60 |
61 | var headers = [String: String]()
62 | for (header, value) in serverConf.httpAdditionalHeaders ?? [:] {
63 | headers[header as! String] = (header as! String == "Authorization") ? "Bearer ************" : value as? String
64 | }
65 | print("[apiCall] \(#function.description) method: \(serverRequest.httpMethod)")
66 | print("[apiCall] \(#function.description) headers: \(headers)")
67 | print("[apiCall] \(#function.description) endpoint: \(serverEncodedURL?.absoluteString ?? "")")
68 | print("[apiCall]")
69 |
70 | let serverSession = Foundation.URLSession(configuration: serverConf, delegate: self, delegateQueue: OperationQueue.main)
71 | let task = serverSession.dataTask(with: serverRequest as URLRequest, completionHandler: {
72 | (data, response, error) -> Void in
73 | serverSession.finishTasksAndInvalidate()
74 | if let httpResponse = response as? HTTPURLResponse {
75 | // print("httpResponse: \(String(describing: response))")
76 |
77 | if httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299 {
78 | do {
79 | let json = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments)
80 | // print("\(json)")
81 | if let endpointJSON = json as? [String: Any] {
82 | if let siteEndpoints = endpointJSON["sites"] as? [Any] {
83 | let siteCount = siteEndpoints.count
84 | if siteCount > 0 {
85 | for i in (0.. Void) {
121 | completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/Replicator/SummaryViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SummaryViewController.swift
3 | // Replicator
4 | //
5 | // Created by Leslie Helou on 12/24/17.
6 | // Copyright 2024 Jamf. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import WebKit
11 |
12 | class SummaryViewController: NSViewController {
13 |
14 | @IBOutlet weak var summary_WebView: WKWebView!
15 | // var summaryDict = [String: [String:[String]]]() // summary counters of created, updated, and failed objects
16 |
17 | @IBOutlet weak var summary_TextField: NSTextField!
18 | override func viewDidLoad() {
19 | super.viewDidLoad()
20 | // Do view setup here.
21 | }
22 |
23 | @IBAction func dismissSummaryWindow(_ sender: NSButton) {
24 | let application = NSApplication.shared
25 | application.stopModal()
26 | }
27 |
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/Replicator/TimeDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimeDelegate.swift
3 | // Replicator
4 | //
5 | // Created by Leslie Helou on 6/9/20.
6 | // Copyright 2020 jamf. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class TimeDelegate {
12 | // get current time
13 | func getCurrent() -> String {
14 | let current = Date()
15 | let localCalendar = Calendar.current
16 | let dateObjects: Set = [.year, .month, .day, .hour, .minute, .second]
17 | let dateTime = localCalendar.dateComponents(dateObjects, from: current)
18 | let currentMonth = leadingZero(value: dateTime.month!)
19 | let currentDay = leadingZero(value: dateTime.day!)
20 | let currentHour = leadingZero(value: dateTime.hour!)
21 | let currentMinute = leadingZero(value: dateTime.minute!)
22 | let currentSecond = leadingZero(value: dateTime.second!)
23 | let stringDate = "\(dateTime.year!)\(currentMonth)\(currentDay)_\(currentHour)\(currentMinute)\(currentSecond)"
24 | return stringDate
25 | }
26 |
27 | // add leading zero to single digit integers
28 | func leadingZero(value: Int) -> String {
29 | var formattedValue = ""
30 | if value < 10 {
31 | formattedValue = "0\(value)"
32 | } else {
33 | formattedValue = "\(value)"
34 | }
35 | return formattedValue
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Replicator/Utilities.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Utilities.swift
3 | // Replicator
4 | //
5 | // Created by leslie on 10/11/24.
6 | // Copyright © 2024 jamf. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class Utilities: NSObject {
12 |
13 | static let shared = Utilities()
14 | private override init() { }
15 |
16 | func dd(value: Int) -> String {
17 | let formattedValue = (value < 10) ? "0\(value)":"\(value)"
18 | return formattedValue
19 | }
20 |
21 | func zipIt(args: String..., completion: @escaping (_ result: String) -> Void) {
22 |
23 | var cmdArgs = ["-c"]
24 | for theArg in args {
25 | cmdArgs.append(theArg)
26 | }
27 | var status = ""
28 | var statusArray = [String]()
29 | let pipe = Pipe()
30 | let task = Process()
31 |
32 | task.launchPath = "/bin/sh"
33 | task.arguments = cmdArgs
34 | task.standardOutput = pipe
35 |
36 | task.launch()
37 |
38 | let outdata = pipe.fileHandleForReading.readDataToEndOfFile()
39 | if var string = String(data: outdata, encoding: .utf8) {
40 | string = string.trimmingCharacters(in: .newlines)
41 | statusArray = string.components(separatedBy: "")
42 | status = statusArray[0]
43 | }
44 |
45 | task.waitUntilExit()
46 | completion(status)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Replicator/VersionCheck.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CheckForUpdate.swift
3 | // Replicator
4 | //
5 | // Created by Leslie Helou on 6/9/18.
6 | // Copyright 2024 Jamf. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class VersionCheck: NSObject, URLSessionDelegate {
12 |
13 | func versionCheck(completion: @escaping (_ result: Bool, _ latest: String) -> Void) {
14 |
15 | URLCache.shared.removeAllCachedResponses()
16 |
17 | // let (currMajor, currMinor, currPatch, runningBeta, currBeta) = versionDetails(theVersion: AppInfo.version)
18 |
19 | var updateAvailable = false
20 | // var versionTest = true
21 |
22 | let versionUrl = URL(string: "https://api.github.com/repos/jamf/Replicator/releases/latest")
23 |
24 | let configuration = URLSessionConfiguration.ephemeral
25 | var request = URLRequest(url: versionUrl!)
26 | request.httpMethod = "GET"
27 |
28 | configuration.httpAdditionalHeaders = ["Accept" : "application/vnd.github.jean-grey-preview+json"]
29 |
30 | var headers = [String: String]()
31 | for (header, value) in configuration.httpAdditionalHeaders ?? [:] {
32 | headers[header as! String] = (header as! String == "Authorization") ? "Bearer ************" : value as? String
33 | }
34 | print("[apiCall] \(#function.description) method: \(request.httpMethod ?? "")")
35 | print("[apiCall] \(#function.description) headers: \(headers)")
36 | print("[apiCall] \(#function.description) endpoint: \(versionUrl?.absoluteString ?? "")")
37 | print("[apiCall]")
38 |
39 | let session = Foundation.URLSession(configuration: configuration, delegate: self as URLSessionDelegate, delegateQueue: OperationQueue.main)
40 | let task = session.dataTask(with: request as URLRequest, completionHandler: {
41 | (data, response, error) -> Void in
42 | session.finishTasksAndInvalidate()
43 | if let httpResponse = response as? HTTPURLResponse {
44 | if httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299 {
45 | let json = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments)
46 | if let endpointJSON = json as? [String: Any] {
47 | let fullVersion = (endpointJSON["tag_name"] as! String).replacingOccurrences(of: "v", with: "")
48 | updateAvailable = self.update(current: AppInfo.version, available: fullVersion)
49 | completion(updateAvailable, "v\(fullVersion)")
50 | return
51 | } else {
52 | completion(false, "")
53 | return
54 | }
55 | } else {
56 | WriteToLog.shared.message("[versionCheck] response error: \(httpResponse.statusCode)")
57 | completion(false, "")
58 | return
59 | }
60 |
61 | } else {
62 | WriteToLog.shared.message("[versionCheck] unknown response for version check")
63 | completion(false, "")
64 | return
65 | }
66 | })
67 | task.resume()
68 | }
69 |
70 | func update(current: String, available: String) -> Bool {
71 | if current == available {
72 | return false
73 | }
74 | let sortedVersions = [current, available].sorted { current, available in
75 | let options: String.CompareOptions = [.numeric]
76 | return current.compare(available, options: options) == .orderedDescending
77 | }
78 | return (sortedVersions[0] == available)
79 | }
80 |
81 | func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping(URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
82 | completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Replicator/WriteToLog.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WriteToLog.swift
3 | // Replicator
4 | //
5 | // Created by Leslie Helou on 2/21/19.
6 | // Copyright 2024 Jamf. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os.log
11 |
12 | class WriteToLog {
13 |
14 | static let shared = WriteToLog()
15 |
16 | func message(_ message: String) {
17 | let timeStamp = Setting.fullGUI ? "\(TimeDelegate().getCurrent()) " : ""
18 | var logString = (LogLevel.debug) ? "\(timeStamp)[- debug -] \(message)\n":"\(timeStamp)\(message)\n"
19 | // print("[WriteToLog] \(logString)")
20 |
21 | if AppInfo.maskServerNames {
22 | logString = logString.replacingOccurrences(of: JamfProServer.url["source"]?.fqdnFromUrl ?? "SourceServer", with: "SourceServer")
23 | logString = logString.replacingOccurrences(of: JamfProServer.url["dest"]?.fqdnFromUrl ?? "DestinationServer", with: "DestinationServer")
24 | }
25 | // if !Setting.fullGUI {
26 | // Logger.writeToLog.info("\(logString, privacy: .public)")
27 | // return
28 | // }
29 | guard let logData = logString.data(using: .utf8) else { return }
30 | let logURL = URL(fileURLWithPath: History.logPath + History.logFile)
31 |
32 | do {
33 | let fileHandle = try FileHandle(forWritingTo: logURL)
34 | defer { fileHandle.closeFile() } // Ensure file is closed
35 |
36 | fileHandle.seekToEndOfFile()
37 | fileHandle.write(logData)
38 | } catch {
39 | print("[Log Error] Failed to write to log file: \(error.localizedDescription)")
40 | }
41 | }
42 |
43 | func logCleanup() {
44 | let maxLogFileCount = (userDefaults.integer(forKey: "logFilesCountPref") < 1) ? 20:userDefaults.integer(forKey: "logFilesCountPref")
45 | var logArray: [String] = []
46 | var logCount: Int = 0
47 | do {
48 | let logFiles = try fm.contentsOfDirectory(atPath: History.logPath)
49 |
50 | for logFile in logFiles {
51 | let filePath: String = History.logPath + logFile
52 | logArray.append(filePath)
53 | }
54 | logArray.sort()
55 | logCount = logArray.count
56 | if didRun {
57 | // remove old history files
58 | if logCount > maxLogFileCount {
59 | for i in (0.. 0 {
73 |
74 | }
75 | do {
76 | try fm.removeItem(atPath: logArray[0])
77 | }
78 | catch let error as NSError {
79 | if LogLevel.debug { WriteToLog.shared.message("Error deleting log file: \n" + History.logPath + logArray[0] + "\n \(error)") }
80 | }
81 | }
82 | } catch {
83 | WriteToLog.shared.message("no log files found")
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Replicator/XmlDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // XmlDelegate.swift
3 | // Replicator
4 | //
5 | // Created by Leslie Helou on 6/28/18.
6 | // Copyright 2024 Jamf. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import Foundation
11 |
12 | class XmlDelegate: NSObject, URLSessionDelegate {
13 |
14 | var baseXmlFolder = ""
15 | var saveXmlFolder = ""
16 | var endpointPath = ""
17 | let getRecordQ = OperationQueue()
18 |
19 | func apiAction(method: String, theServer: String, base64Creds: String, theEndpoint: String, completion: @escaping (_ result: (Int,String)) -> Void) {
20 |
21 | if theEndpoint.prefix(4) != "skip" {
22 |
23 | URLCache.shared.removeAllCachedResponses()
24 | var existingDestUrl = ""
25 |
26 | existingDestUrl = "\(theServer)/JSSResource/\(theEndpoint)"
27 | existingDestUrl = existingDestUrl.urlFix
28 | // existingDestUrl = existingDestUrl.replacingOccurrences(of: "//JSSResource", with: "/JSSResource")
29 |
30 | if LogLevel.debug { WriteToLog.shared.message("[Xml.apiAction] Looking up: \(existingDestUrl)") }
31 | // if "\(existingDestUrl)" == "" { existingDestUrl = "https://localhost" }
32 | let destEncodedURL = URL(string: existingDestUrl)
33 | let xmlRequest = NSMutableURLRequest(url: destEncodedURL! as URL)
34 |
35 | let semaphore = DispatchSemaphore(value: 1)
36 | getRecordQ.maxConcurrentOperationCount = 3
37 | getRecordQ.addOperation {
38 |
39 | xmlRequest.httpMethod = "\(method.uppercased())"
40 | let destConf = URLSessionConfiguration.default
41 |
42 | destConf.httpAdditionalHeaders = ["Authorization" : "\(JamfProServer.authType["dest"] ?? "Bearer") \(JamfProServer.authCreds["dest"] ?? "")", "Content-Type" : "text/xml", "Accept" : "text/xml", "User-Agent" : AppInfo.userAgentHeader]
43 |
44 | var headers = [String: String]()
45 | for (header, value) in destConf.httpAdditionalHeaders ?? [:] {
46 | headers[header as! String] = (header as! String == "Authorization") ? "Bearer ************" : value as? String
47 | }
48 | print("[apiCall] \(#function.description) method: \(xmlRequest.httpMethod)")
49 | print("[apiCall] \(#function.description) headers: \(headers)")
50 | print("[apiCall] \(#function.description) endpoint: \(destEncodedURL?.absoluteString ?? "")")
51 | print("[apiCall]")
52 |
53 | // sticky session
54 | if JamfProServer.sessionCookie.count > 0 && JamfProServer.stickySession {
55 | // print("xml sticky session for \(theServer)")
56 | URLSession.shared.configuration.httpCookieStorage!.setCookies(JamfProServer.sessionCookie, for: URL(string: theServer), mainDocumentURL: URL(string: theServer))
57 | }
58 |
59 | let destSession = Foundation.URLSession(configuration: destConf, delegate: self, delegateQueue: OperationQueue.main)
60 | let task = destSession.dataTask(with: xmlRequest as URLRequest, completionHandler: {
61 | (data, response, error) -> Void in
62 | destSession.finishTasksAndInvalidate()
63 | if let httpResponse = response as? HTTPURLResponse {
64 | if httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299 {
65 | do {
66 | let returnedXML = String(data: data!, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue))!
67 |
68 | completion((httpResponse.statusCode,returnedXML))
69 | }
70 | } else {
71 | WriteToLog.shared.message("[Xml.apiAction] error HTTP Status Code: \(httpResponse.statusCode)")
72 | completion((httpResponse.statusCode,""))
73 | }
74 | } else {
75 | WriteToLog.shared.message("[Xml.apiAction] error getting XML for \(existingDestUrl)")
76 | completion((0,""))
77 | } // if let httpResponse - end
78 | semaphore.signal()
79 | if error != nil {
80 | }
81 | }) // let task = destSession - end
82 | //print("GET")
83 | task.resume()
84 | } // getRecordQ - end
85 | } else {
86 | completion((200,""))
87 | }
88 | }
89 |
90 |
91 | func save(node: String, xml: String, rawName: String, id: String, format: String) {
92 |
93 | var name = rawName.replacingOccurrences(of: ":", with: ";")
94 | name = name.replacingOccurrences(of: "/", with: ":")
95 | if LogLevel.debug { WriteToLog.shared.message("[XmlDelegate.save] saving \(name), format: \(format), to folder \(node)") }
96 | // Create folder to store xml files if needed - start
97 | baseXmlFolder = userDefaults.string(forKey: "saveLocation") ?? ""
98 | if baseXmlFolder == "" {
99 | baseXmlFolder = (NSHomeDirectory() + "/Downloads/Replicator/")
100 | } else {
101 | baseXmlFolder = baseXmlFolder.pathToString
102 | }
103 |
104 | saveXmlFolder = baseXmlFolder+format+"/"
105 |
106 | if !(fm.fileExists(atPath: saveXmlFolder)) {
107 | do {
108 | try fm.createDirectory(atPath: saveXmlFolder, withIntermediateDirectories: true, attributes: nil)
109 | } catch {
110 | WriteToLog.shared.message("[XmlDelegate.save] Problem creating \(saveXmlFolder) folder: Error \(error)")
111 | return
112 | }
113 | }
114 | // Create folder to store xml files if needed - end
115 |
116 | print("[XmlDelegate] node: \(node)")
117 |
118 |
119 | // Create endpoint type to store xml files if needed - start
120 | switch node {
121 | case "selfservicepolicyicon", "macapplicationsicon", "mobiledeviceapplicationsicon":
122 | // print("[icons] saveFolder: \(saveXmlFolder)")
123 | endpointPath = saveXmlFolder+node+"/\(id)"
124 | case "accounts/groupid":
125 | endpointPath = saveXmlFolder+"jamfgroups"
126 | case "accounts/userid":
127 | endpointPath = saveXmlFolder+"jamfusers"
128 | case "computergroups":
129 | let isSmart = tagValue2(xmlString: xml, startTag: "", endTag: " ")
130 | if isSmart == "true" {
131 | endpointPath = saveXmlFolder+"smartcomputergroups"
132 | } else {
133 | endpointPath = saveXmlFolder+"staticcomputergroups"
134 | }
135 | default:
136 | endpointPath = saveXmlFolder+node
137 | }
138 | if !(fm.fileExists(atPath: endpointPath)) {
139 | do {
140 | try fm.createDirectory(atPath: endpointPath, withIntermediateDirectories: true, attributes: nil)
141 | } catch {
142 | if LogLevel.debug { WriteToLog.shared.message("[XmlDelegate.save] Problem creating \(endpointPath) folder: Error \(error)") }
143 | return
144 | }
145 | }
146 | // Create endpoint type to store xml files if needed - end
147 |
148 | switch node {
149 | case "selfservicepolicyicon", "macapplicationsicon", "mobiledeviceapplicationsicon":
150 |
151 | var copyIcon = true
152 | let iconSource = "\(xml)"
153 |
154 | let iconDest = "\(endpointPath)/\(name)"
155 |
156 | // print("copy from \(iconSource) to: \(iconDest)")
157 | if fm.fileExists(atPath: iconDest) {
158 | do {
159 | if LogLevel.debug { WriteToLog.shared.message("[XmlDelegate.save] removing currently saved icon: \(iconDest)") }
160 | try FileManager.default.removeItem(at: URL(fileURLWithPath: iconDest))
161 | } catch let error as NSError {
162 | if LogLevel.debug { WriteToLog.shared.message("[XmlDelegate.save] unable to delete cached icon: \(iconDest). Error \(error).") }
163 | copyIcon = false
164 | }
165 | }
166 | if copyIcon {
167 | if LogLevel.debug { WriteToLog.shared.message("[XmlDelegate.save] saving icon to: \(iconDest)") }
168 | do {
169 | // print("[icons] copy to: \(iconDest)")
170 | try fm.copyItem(atPath: iconSource, toPath: iconDest)
171 | if export.saveOnly {
172 | do {
173 | if LogLevel.debug { WriteToLog.shared.message("[XmlDelegate.save] removing cached icon: \(iconSource)/") }
174 | try FileManager.default.removeItem(at: URL(fileURLWithPath: "\(iconSource)/"))
175 | } catch let error as NSError {
176 | if LogLevel.debug { WriteToLog.shared.message("[XmlDelegate.save] unable to delete \(iconSource)/. Error \(error)") }
177 | }
178 | }
179 |
180 | } catch let error as NSError {
181 | if LogLevel.debug { WriteToLog.shared.message("[XmlDelegate.save] unable to save icon: \(iconDest). Error \(error).") }
182 | copyIcon = false
183 | }
184 | }
185 | // print("Copied \(iconSource) to: \(iconDest)")
186 |
187 | default:
188 | let xmlFile = "\(name)-\(id).xml"
189 | if let xmlDoc = try? XMLDocument(xmlString: xml, options: .nodePrettyPrint) {
190 | if let _ = try? XMLElement.init(xmlString:"\(xml)") {
191 | let data = xmlDoc.xmlData(options:.nodePrettyPrint)
192 | var formattedXml = String(data: data, encoding: .utf8)!
193 | if node == "scripts" {
194 | formattedXml = formattedXml.xmlDecode
195 | }
196 | // print("policy xml:\n\(formattedXml)")
197 | do {
198 | try formattedXml.write(toFile: endpointPath+"/"+xmlFile, atomically: true, encoding: .utf8)
199 | if LogLevel.debug { WriteToLog.shared.message("[XmlDelegate.save] saved to: \(endpointPath)") }
200 | } catch {
201 | if LogLevel.debug { WriteToLog.shared.message("[XmlDelegate.save] Problem writing \(endpointPath) folder: Error \(error)") }
202 | return
203 | }
204 | } // if let prettyXml - end
205 | }
206 | }
207 |
208 | } // func save
209 |
210 | func encodeSpecialChars(textString: String) -> String {
211 |
212 | let newString = textString.replacingOccurrences(of: "&", with: "&")
213 | .replacingOccurrences(of: "\"", with: """)
214 | .replacingOccurrences(of: "'", with: "'")
215 | .replacingOccurrences(of: "<", with: "<")
216 | .replacingOccurrences(of: ">", with: ">")
217 |
218 | return newString
219 | }
220 |
221 | func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping(URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
222 | completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/Replicator/app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/app.png
--------------------------------------------------------------------------------
/Replicator/copy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/copy.png
--------------------------------------------------------------------------------
/Replicator/export.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/export.png
--------------------------------------------------------------------------------
/Replicator/images/Screenshot 2024-11-18 at 12.35.39 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/Screenshot 2024-11-18 at 12.35.39 PM.png
--------------------------------------------------------------------------------
/Replicator/images/Screenshot 2024-11-18 at 12.36.14 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/Screenshot 2024-11-18 at 12.36.14 PM.png
--------------------------------------------------------------------------------
/Replicator/images/allowAccess.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/allowAccess.png
--------------------------------------------------------------------------------
/Replicator/images/appPrefs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/appPrefs.png
--------------------------------------------------------------------------------
/Replicator/images/computerPrefs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/computerPrefs.png
--------------------------------------------------------------------------------
/Replicator/images/copyPrefs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/copyPrefs.png
--------------------------------------------------------------------------------
/Replicator/images/exportDeleted.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/exportDeleted.png
--------------------------------------------------------------------------------
/Replicator/images/exportPrefs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/exportPrefs.png
--------------------------------------------------------------------------------
/Replicator/images/exportTo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/exportTo.png
--------------------------------------------------------------------------------
/Replicator/images/migrator2.1-old.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/migrator2.1-old.png
--------------------------------------------------------------------------------
/Replicator/images/migrator2.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/migrator2.1.png
--------------------------------------------------------------------------------
/Replicator/images/migrator2.5a.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/migrator2.5a.png
--------------------------------------------------------------------------------
/Replicator/images/migrator2.5b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/migrator2.5b.png
--------------------------------------------------------------------------------
/Replicator/images/migrator2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/migrator2.png
--------------------------------------------------------------------------------
/Replicator/images/migrator3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/migrator3.png
--------------------------------------------------------------------------------
/Replicator/images/migrator3Policies.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/migrator3Policies.png
--------------------------------------------------------------------------------
/Replicator/images/open.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/open.png
--------------------------------------------------------------------------------
/Replicator/images/passwordPrefs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/passwordPrefs.png
--------------------------------------------------------------------------------
/Replicator/images/removeServer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/removeServer.png
--------------------------------------------------------------------------------
/Replicator/images/selectiveFilter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/selectiveFilter.png
--------------------------------------------------------------------------------
/Replicator/images/sitePrefs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/sitePrefs.png
--------------------------------------------------------------------------------
/Replicator/images/summary1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/summary1.png
--------------------------------------------------------------------------------
/Replicator/images/summary2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/images/summary2.png
--------------------------------------------------------------------------------
/Replicator/settings.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | checkForUpdate
6 |
7 | dest_server_array
8 |
9 | source_server_array
10 |
11 | maxHistory
12 | 20
13 | wipe_data
14 |
15 | source_jp_server
16 |
17 | source_user
18 |
19 | dest_jp_server
20 |
21 | dest_user
22 |
23 | storeSourceCreds
24 | 0
25 | storeDestCreds
26 | 0
27 | sourceApiClient
28 | 0
29 | destApiClient
30 | 0
31 | scope
32 |
33 | iosapps
34 |
35 | copy
36 |
37 |
38 | macapps
39 |
40 | copy
41 |
42 |
43 | sig
44 |
45 | copy
46 |
47 |
48 | users
49 |
50 | copy
51 |
52 |
53 | scg
54 |
55 | copy
56 |
57 |
58 | mobiledeviceconfigurationprofiles
59 |
60 | copy
61 |
62 |
63 | osxconfigurationprofiles
64 |
65 | copy
66 |
67 |
68 | policies
69 |
70 | copy
71 |
72 | disable
73 |
74 |
75 | restrictedsoftware
76 |
77 | copy
78 |
79 |
80 |
81 | xml
82 |
83 | saveOnly
84 |
85 | saveRawXml
86 |
87 | saveTrimmedXml
88 |
89 | saveRawXmlScope
90 |
91 | saveTrimmedXmlScope
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/Replicator/siteIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Replicator/64cac6fa8dd94703d14003a0e4796efe7b1da5fb/Replicator/siteIcon.png
--------------------------------------------------------------------------------