diff --git a/doc/CONFIGURATION.json b/doc/CONFIGURATION.json
index d9c75ec81..f3849fabf 100644
--- a/doc/CONFIGURATION.json
+++ b/doc/CONFIGURATION.json
@@ -1,4 +1,280 @@
[
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.CollaborateAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.collaborate.enabled",
+ "key" : "com.owncloud.action.collaborate.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.CopyAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.copy.enabled",
+ "key" : "com.owncloud.action.copy.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloudAppShared.CreateFolderAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.createFolder.enabled",
+ "key" : "com.owncloud.action.createFolder.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.DeleteAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.delete.enabled",
+ "key" : "com.owncloud.action.delete.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.DiscardSceneAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.discardscene.enabled",
+ "key" : "com.owncloud.action.discardscene.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.DuplicateAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.duplicate.enabled",
+ "key" : "com.owncloud.action.duplicate.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.FavoriteAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.favorite.enabled",
+ "key" : "com.owncloud.action.favorite.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.LinksAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.links.enabled",
+ "key" : "com.owncloud.action.links.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.MakeAvailableOfflineAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.makeAvailableOffline.enabled",
+ "key" : "com.owncloud.action.makeAvailableOffline.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.MakeUnavailableOfflineAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.makeUnavailableOffline.enabled",
+ "key" : "com.owncloud.action.makeUnavailableOffline.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.DocumentEditingAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.markup.enabled",
+ "key" : "com.owncloud.action.markup.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.MediaEditingAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.mediaediting.enabled",
+ "key" : "com.owncloud.action.mediaediting.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.MoveAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.move.enabled",
+ "key" : "com.owncloud.action.move.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.OpenInAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.openin.enabled",
+ "key" : "com.owncloud.action.openin.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.OpenSceneAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.openscene.enabled",
+ "key" : "com.owncloud.action.openscene.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.RenameAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.rename.enabled",
+ "key" : "com.owncloud.action.rename.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.ScanAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.scan.enabled",
+ "key" : "com.owncloud.action.scan.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.DisplayExifMetadataAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.show-exif.enabled",
+ "key" : "com.owncloud.action.show-exif.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.UnfavoriteAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.unfavorite.enabled",
+ "key" : "com.owncloud.action.unfavorite.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.UnshareAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.unshare.enabled",
+ "key" : "com.owncloud.action.unshare.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.UploadCameraMediaAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.upload.camera_media.enabled",
+ "key" : "com.owncloud.action.upload.camera_media.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.UploadFileAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.uploadfile.enabled",
+ "key" : "com.owncloud.action.uploadfile.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Actions",
+ "categoryTag" : "actions",
+ "classIdentifier" : "action",
+ "className" : "ownCloud.UploadMediaAction",
+ "description" : "Controls whether action can be accessed in the app UI.",
+ "flatIdentifier" : "action.com.owncloud.action.uploadphotos.enabled",
+ "key" : "com.owncloud.action.uploadphotos.enabled",
+ "status" : "advanced",
+ "type" : "bool"
+ },
{
"autoExpansion" : "none",
"category" : "App",
@@ -193,32 +469,6 @@
"status" : "supported",
"type" : "string"
},
- {
- "autoExpansion" : "none",
- "category" : "OIDC",
- "categoryTag" : "oidc",
- "classIdentifier" : "authentication-oauth2",
- "className" : "OCAuthenticationMethodOAuth2",
- "defaultValue" : true,
- "description" : "Use OpenID Connect Dynamic Client Registration if the `.well-known/openid-configuration` provides a `registration_endpoint`. If this option is enabled and a registration endpoint is available, `oa2-client-id` and `oa2-client-secret` will be ignored.",
- "flatIdentifier" : "authentication-oauth2.oidc-register-client",
- "key" : "oidc-register-client",
- "status" : "supported",
- "type" : "bool"
- },
- {
- "autoExpansion" : "none",
- "category" : "OIDC",
- "categoryTag" : "oidc",
- "classIdentifier" : "authentication-oauth2",
- "className" : "OCAuthenticationMethodOAuth2",
- "defaultValue" : "ownCloud/{{os.name}} {{app.version}}",
- "description" : "Client Name Template to use during OpenID Connect Dynamic Client Registration. In addition to the placeholders available for `http.user-agent`, `{{url.hostname}}` can also be used.",
- "flatIdentifier" : "authentication-oauth2.oidc-register-client-name-template",
- "key" : "oidc-register-client-name-template",
- "status" : "supported",
- "type" : "string"
- },
{
"autoExpansion" : "none",
"category" : "OIDC",
@@ -287,7 +537,7 @@
"classIdentifier" : "bookmark",
"className" : "ownCloud.BookmarkViewController",
"defaultValue" : true,
- "description" : "Controls whetehr the server URL in the text field during the creation of new bookmarks can be changed.",
+ "description" : "Controls whether the server URL in the text field during the creation of new bookmarks can be changed.",
"flatIdentifier" : "bookmark.url-editable",
"key" : "url-editable",
"status" : "supported",
@@ -321,6 +571,20 @@
"status" : "recommended",
"type" : "bool"
},
+ {
+ "autoExpansion" : "none",
+ "category" : "Connection",
+ "categoryTag" : "connection",
+ "classIdentifier" : "connection",
+ "className" : "OCConnection",
+ "defaultValue" : false,
+ "description" : "Controls whether private links are requested with regular PROPFINDs.",
+ "flags" : 4,
+ "flatIdentifier" : "connection.always-request-private-link",
+ "key" : "always-request-private-link",
+ "status" : "advanced",
+ "type" : "bool"
+ },
{
"autoExpansion" : "trailing",
"category" : "Security",
@@ -329,8 +593,8 @@
"className" : "OCConnection",
"description" : "Array of allowed authentication methods. Nil/Missing for no restrictions.",
"flags" : 4,
- "flatIdentifier" : "connection.allowed-authentication-methods",
- "key" : "allowed-authentication-methods",
+ "flatIdentifier" : "connection.connection-allowed-authentication-methods",
+ "key" : "connection-allowed-authentication-methods",
"possibleValues" : [
{
"description" : "Basic Auth",
@@ -348,19 +612,65 @@
"status" : "recommended",
"type" : "stringArray"
},
+ {
+ "autoExpansion" : "none",
+ "category" : "Security",
+ "categoryTag" : "security",
+ "classIdentifier" : "connection",
+ "className" : "OCConnection",
+ "defaultValue" : "bookmarkCertificate == serverCertificate",
+ "description" : "Rule that defines the criteria a certificate needs to meet for OCConnection to recognize it as valid for a bookmark.\n\nExamples of expressions:\n- `bookmarkCertificate == serverCertificate`: the whole certificate needs to be identical to the one stored in the bookmark during setup.\n- `bookmarkCertificate.publicKeyData == serverCertificate.publicKeyData`: the public key of the received certificate needs to be identical to the public key stored in the bookmark during setup.\n- `serverCertificate.passedValidationOrIsUserAccepted == true`: any certificate is accepted as long as it has passed validation by the OS or was accepted by the user.\n- `serverCertificate.commonName == \"demo.owncloud.org\"`: the common name of the certificate must be \"demo.owncloud.org\".\n- `serverCertificate.rootCertificate.commonName == \"DST Root CA X3\"`: the common name of the root certificate must be \"DST Root CA X3\".\n- `serverCertificate.parentCertificate.commonName == \"Let's Encrypt Authority X3\"`: the common name of the parent certificate must be \"Let's Encrypt Authority X3\".\n- `serverCertificate.publicKeyData.sha256Hash.asFingerPrintString == \"2A 00 98 90 BD … F7\"`: the SHA-256 fingerprint of the public key of the server certificate needs to match the provided value.\n",
+ "flags" : 4,
+ "flatIdentifier" : "connection.connection-certificate-extended-validation-rule",
+ "key" : "connection-certificate-extended-validation-rule",
+ "status" : "advanced",
+ "type" : "string"
+ },
{
"autoExpansion" : "none",
"category" : "Connection",
"categoryTag" : "connection",
"classIdentifier" : "connection",
"className" : "OCConnection",
- "defaultValue" : false,
- "description" : "Controls whether private links are requested with regular PROPFINDs.",
+ "defaultValue" : "10.0",
+ "description" : "The minimum server version required.",
"flags" : 4,
- "flatIdentifier" : "connection.always-request-private-link",
- "key" : "always-request-private-link",
- "status" : "advanced",
- "type" : "bool"
+ "flatIdentifier" : "connection.connection-minimum-server-version",
+ "key" : "connection-minimum-server-version",
+ "status" : "debugOnly",
+ "type" : "string"
+ },
+ {
+ "autoExpansion" : "trailing",
+ "category" : "Security",
+ "categoryTag" : "security",
+ "classIdentifier" : "connection",
+ "className" : "OCConnection",
+ "defaultValue" : [
+ "com.owncloud.openid-connect",
+ "com.owncloud.oauth2",
+ "com.owncloud.basicauth"
+ ],
+ "description" : "Array of authentication methods in order of preference (most preferred first).",
+ "flags" : 4,
+ "flatIdentifier" : "connection.connection-preferred-authentication-methods",
+ "key" : "connection-preferred-authentication-methods",
+ "possibleValues" : [
+ {
+ "description" : "Basic Auth",
+ "value" : "com.owncloud.basicauth"
+ },
+ {
+ "description" : "OAuth2",
+ "value" : "com.owncloud.oauth2"
+ },
+ {
+ "description" : "OpenID Connect",
+ "value" : "com.owncloud.openid-connect"
+ }
+ ],
+ "status" : "recommended",
+ "type" : "stringArray"
},
{
"autoExpansion" : "none",
@@ -368,11 +678,11 @@
"categoryTag" : "security",
"classIdentifier" : "connection",
"className" : "OCConnection",
- "defaultValue" : "bookmarkCertificate == serverCertificate",
- "description" : "Rule that defines the criteria a certificate needs to meet for OCConnection to recognize it as valid for a bookmark.",
+ "defaultValue" : "(bookmarkCertificate.publicKeyData == serverCertificate.publicKeyData) OR ((check.parentCertificatesHaveIdenticalPublicKeys == true) AND (serverCertificate.passedValidationOrIsUserAccepted == true))",
+ "description" : "Rule that defines the criteria that need to be met for OCConnection to accept a renewed certificate and update the bookmark's certificate automatically instead of prompting the user. Used when the extended validation rule fails. Set this to `never` if the user should always be prompted when a server's certificate changed.",
"flags" : 4,
- "flatIdentifier" : "connection.certificate-extended-validation-rule",
- "key" : "certificate-extended-validation-rule",
+ "flatIdentifier" : "connection.connection-renewed-certificate-acceptance-rule",
+ "key" : "connection-renewed-certificate-acceptance-rule",
"status" : "advanced",
"type" : "string"
},
@@ -516,20 +826,6 @@
"status" : "debugOnly",
"type" : "bool"
},
- {
- "autoExpansion" : "none",
- "category" : "Connection",
- "categoryTag" : "connection",
- "classIdentifier" : "connection",
- "className" : "OCConnection",
- "defaultValue" : "10.0",
- "description" : "The minimum server version required.",
- "flags" : 4,
- "flatIdentifier" : "connection.minimum-server-version",
- "key" : "minimum-server-version",
- "status" : "debugOnly",
- "type" : "string"
- },
{
"autoExpansion" : "none",
"category" : "Connection",
@@ -544,52 +840,6 @@
"status" : "advanced",
"type" : "string"
},
- {
- "autoExpansion" : "trailing",
- "category" : "Security",
- "categoryTag" : "security",
- "classIdentifier" : "connection",
- "className" : "OCConnection",
- "defaultValue" : [
- "com.owncloud.openid-connect",
- "com.owncloud.oauth2",
- "com.owncloud.basicauth"
- ],
- "description" : "Array of authentication methods in order of preference (most preferred first).",
- "flags" : 4,
- "flatIdentifier" : "connection.preferred-authentication-methods",
- "key" : "preferred-authentication-methods",
- "possibleValues" : [
- {
- "description" : "Basic Auth",
- "value" : "com.owncloud.basicauth"
- },
- {
- "description" : "OAuth2",
- "value" : "com.owncloud.oauth2"
- },
- {
- "description" : "OpenID Connect",
- "value" : "com.owncloud.openid-connect"
- }
- ],
- "status" : "recommended",
- "type" : "stringArray"
- },
- {
- "autoExpansion" : "none",
- "category" : "Security",
- "categoryTag" : "security",
- "classIdentifier" : "connection",
- "className" : "OCConnection",
- "defaultValue" : "(bookmarkCertificate.publicKeyData == serverCertificate.publicKeyData) OR ((check.parentCertificatesHaveIdenticalPublicKeys == true) AND (serverCertificate.passedValidationOrIsUserAccepted == true))",
- "description" : "Rule that defines the criteria that need to be met for OCConnection to accept a renewed certificate and update the bookmark's certificate automatically instead of prompting the user. Used when the extended validation rule fails. Set this to `never` if the user should always be prompted when a server's certificate changed.",
- "flags" : 4,
- "flatIdentifier" : "connection.renewed-certificate-acceptance-rule",
- "key" : "renewed-certificate-acceptance-rule",
- "status" : "advanced",
- "type" : "string"
- },
{
"autoExpansion" : "none",
"category" : "Security",
@@ -882,8 +1132,8 @@
"defaultValue" : false,
"description" : "Controls whether filtered out messages should still be logged, but with the message replaced with `-`.",
"flags" : 4,
- "flatIdentifier" : "log.blank-filtered-messages",
- "key" : "blank-filtered-messages",
+ "flatIdentifier" : "log.log-blank-filtered-messages",
+ "key" : "log-blank-filtered-messages",
"status" : "advanced",
"type" : "bool"
},
@@ -896,25 +1146,26 @@
"defaultValue" : false,
"description" : "Controls whether log levels should be replaced with colored emojis.",
"flags" : 4,
- "flatIdentifier" : "log.colored",
- "key" : "colored",
+ "flatIdentifier" : "log.log-colored",
+ "key" : "log-colored",
"status" : "advanced",
"type" : "bool"
},
{
- "autoExpansion" : "none",
+ "autoExpansion" : "trailing",
"category" : "Logging",
"categoryTag" : "logging",
"classIdentifier" : "log",
"className" : "OCLogger",
"defaultValue" : [
"writer.stderr",
- "writer.file"
+ "writer.file",
+ "option.log-requests-and-responses"
],
"description" : "List of enabled logging system components.",
"flags" : 4,
- "flatIdentifier" : "log.enabled-components",
- "key" : "enabled-components",
+ "flatIdentifier" : "log.log-enabled-components",
+ "key" : "log-enabled-components",
"possibleValues" : [
{
"description" : "Log HTTP requests and responses",
@@ -941,8 +1192,8 @@
"defaultValue" : "text",
"description" : "Determines the format that log messages are saved in",
"flags" : 4,
- "flatIdentifier" : "log.format",
- "key" : "format",
+ "flatIdentifier" : "log.log-format",
+ "key" : "log-format",
"possibleValues" : [
{
"description" : "Detailed JSON (one line per message).",
@@ -969,8 +1220,8 @@
"defaultValue" : 4,
"description" : "Log level",
"flags" : 2,
- "flatIdentifier" : "log.level",
- "key" : "level",
+ "flatIdentifier" : "log.log-level",
+ "key" : "log-level",
"possibleValues" : [
{
"description" : "verbose",
@@ -1009,8 +1260,8 @@
"defaultValue" : 0,
"description" : "Maximum length of a log message before the message is truncated. A value of 0 means no limit.",
"flags" : 4,
- "flatIdentifier" : "log.maximum-message-size",
- "key" : "maximum-message-size",
+ "flatIdentifier" : "log.log-maximum-message-size",
+ "key" : "log-maximum-message-size",
"status" : "advanced",
"type" : "int"
},
@@ -1022,8 +1273,8 @@
"className" : "OCLogger",
"description" : "If set, omits logs messages containing any of the exact terms in this array.",
"flags" : 4,
- "flatIdentifier" : "log.omit-matching",
- "key" : "omit-matching",
+ "flatIdentifier" : "log.log-omit-matching",
+ "key" : "log-omit-matching",
"status" : "advanced",
"type" : "stringArray"
},
@@ -1035,8 +1286,8 @@
"className" : "OCLogger",
"description" : "If set, omits all log messages tagged with tags in this array.",
"flags" : 4,
- "flatIdentifier" : "log.omit-tags",
- "key" : "omit-tags",
+ "flatIdentifier" : "log.log-omit-tags",
+ "key" : "log-omit-tags",
"status" : "advanced",
"type" : "stringArray"
},
@@ -1048,8 +1299,8 @@
"className" : "OCLogger",
"description" : "If set, only logs messages containing at least one of the exact terms in this array.",
"flags" : 4,
- "flatIdentifier" : "log.only-matching",
- "key" : "only-matching",
+ "flatIdentifier" : "log.log-only-matching",
+ "key" : "log-only-matching",
"status" : "advanced",
"type" : "stringArray"
},
@@ -1061,8 +1312,8 @@
"className" : "OCLogger",
"description" : "If set, omits all log messages not tagged with tags in this array.",
"flags" : 4,
- "flatIdentifier" : "log.only-tags",
- "key" : "only-tags",
+ "flatIdentifier" : "log.log-only-tags",
+ "key" : "log-only-tags",
"status" : "advanced",
"type" : "stringArray"
},
@@ -1075,8 +1326,8 @@
"defaultValue" : false,
"description" : "Controls whether certain objects in log statements should be masked for privacy.",
"flags" : 4,
- "flatIdentifier" : "log.privacy-mask",
- "key" : "privacy-mask",
+ "flatIdentifier" : "log.log-privacy-mask",
+ "key" : "log-privacy-mask",
"status" : "supported",
"type" : "bool"
},
@@ -1089,8 +1340,8 @@
"defaultValue" : true,
"description" : "Controls whether messages spanning more than one line should be broken into their individual lines and each be logged with the complete lead-in/lead-out sequence.",
"flags" : 4,
- "flatIdentifier" : "log.single-lined",
- "key" : "single-lined",
+ "flatIdentifier" : "log.log-single-lined",
+ "key" : "log-single-lined",
"status" : "advanced",
"type" : "bool"
},
@@ -1103,8 +1354,21 @@
"defaultValue" : false,
"description" : "Controls whether log messages should be written synchronously (which can impact performance) or asynchronously (which can loose messages in case of a crash).",
"flags" : 4,
- "flatIdentifier" : "log.synchronous",
- "key" : "synchronous",
+ "flatIdentifier" : "log.log-synchronous",
+ "key" : "log-synchronous",
+ "status" : "advanced",
+ "type" : "bool"
+ },
+ {
+ "autoExpansion" : "none",
+ "category" : "Passcode",
+ "categoryTag" : "passcode",
+ "classIdentifier" : "passcode",
+ "className" : "ownCloudAppShared.AppLockManager",
+ "defaultValue" : false,
+ "description" : "Controls wether the user MUST establish a passcode upon app installation",
+ "flatIdentifier" : "passcode.enforced",
+ "key" : "enforced",
"status" : "advanced",
"type" : "bool"
},
@@ -1156,4 +1420,4 @@
"status" : "advanced",
"type" : "stringArray"
}
-]
\ No newline at end of file
+]
diff --git a/docs/modules/ROOT/pages/ios_mdm.adoc b/docs/modules/ROOT/pages/ios_mdm.adoc
index 233908013..a93c89d46 100644
--- a/docs/modules/ROOT/pages/ios_mdm.adoc
+++ b/docs/modules/ROOT/pages/ios_mdm.adoc
@@ -9,15 +9,143 @@
Starting with iOS 7, Apple added support for {mdm-protocol-ref-url}[managed application configuration].
An MDM server can push a configuration to the iOS App.
-The app can access the configuration using the `NSUserDefaults` class.
+The app can access this configuration (read-only) using the `NSUserDefaults` class by reading a configuration dictionary under the key _com.apple.configuration.managed_. An app can also observe a system notification (_NSUserDefaultsDidChangeNotification_) to get notified about configuration changes. In addition feedback can be queried back by MDM server. To enable that, app has to write a dictionary with feedback information into user defaults under _com.apple.feedback.managed_ key.
The configuration is basically a key-value dictionary provided as a `.plist` file.
+=== Configurable Settings
+
+ownCloud App implements a mechanism internally called Class Settings which can be derived from different sources:
+
+- Environment variables which e.g. can be set in Xcode for testing. In this case setting keys have to be prepended with _oc:_ prefix.
+- User preferences accessed by the very same API but stored under _org.owncloud.user-settings_ key.
+- Settings dictionary pushed by an MDM Server and accessible using `NSUserDefaults` API under the key _com.apple.configuration.managed_.
+- Default settings defined directly in the app sourcecode.
+- Branding.plist which is the part of the Xcode project under ownCloud/Resources/Theming. It allows to override class settings by specifying them in the `Configuration` section
+
+This is also an order in which these settings take precedence (environment variables have highest priority). So, when settings are accessed, they are merged and higher priority value for the same key overwrites lower priority ones.
+
+Some settings are accessed only once at runtime and the read value is cached, so that new setting to take effect may a require an app to be terminated and restarted.
+
+==== App Basic Configurations
+There are few settings allowing to mark an app installation as BETA and e.g. to supress UIKit animation and review prompt.
+
+include::./ios_mdm_tables.adoc[tag=app]
+
+==== Extensions / Actions
+ownCloud app uses internally a plug-in like mechanism called extensions. Extensions are used to implement menu actions mostly found under "+" menu allowing to add new items (Upload media, take photo etc.) or in more menu (Copy, Move, Open in etc.). Using below settings actions / extensions can be disabled. Extensions are enabled by default, however this might depend on licensing requirements of a particular extension.
+
+include::./ios_mdm_tables.adoc[tag=actions]
+
+(*) These extensions might require additional license (in-app purchase, enterprise version).
+
+==== Display Settings
+To customize file list UI behevior, following settings are available:
+
+// include::./ios_mdm_tables.adoc[tag=displaysettings]
+
+==== Passcode Enforcement
+If your organization policies require users to use a passcode as an additional security barrier for managed apps, the below setting will allow to enforce this requirement.
+
+include::./ios_mdm_tables.adoc[tag=passcode]
+
+==== Bookmark
+
+Below settings allow to configure the app to use a certain server URL and even bind it to this URL only by setting the default non-editable.
+
+include::./ios_mdm_tables.adoc[tag=bookmarks]
+
+==== Item Policies
+
+include::./ios_mdm_tables.adoc[tag=policies]
+
+==== Connection
+
+Settings concerinng HTTP user agent, cookies, background support etc.
+
+include::./ios_mdm_tables.adoc[tag=connection]
+
+===== Server Endpoints
+
+Individually configurable endpoints of the ownCloud server instance.
+
+include::./ios_mdm_tables.adoc[tag=endpoints]
+
+===== Connection Authentication / Security
+
+Settings concerning certificate validation policies.
+
+include::./ios_mdm_tables.adoc[tag=security]
+
+===== OAuth2 Based Authentication
+
+Settings allowing to configure OAuth2 based authentication.
+
+include::./ios_mdm_tables.adoc[tag=oauth2]
+
+==== Shortcuts
+
+Shortcuts are a very powerful way to build automated workflows in iOS. Apps can provide shortcut intents for certain actions. ownCloud app provides certain actions as shortcuts as well (e.g. allowing to get account information, create folder and so on). However in some cases it might make sense to disable shortcuts to minimize security risks. It can be done using following option:
+
+include::./ios_mdm_tables.adoc[tag=shortcuts]
+
+==== Logging
+Logging settings control the ammount and type of app internal log messages stored as text files and accessible via settings menu.
+
+include::./ios_mdm_tables.adoc[tag=logging]
+
+== AppConfig XML Example
+
+Here is an example of an XML spec-file based on AppConfig standard with minimal logging settings allowing to change a log level and disable / enable private information masking:
+
+```
+
+ 1.0.0
+ com.owncloud.ios-app
+
+
+
+ 4
+
+
+
+
+
+
+
+
+
+
+ Logging
+
+
+
+
+ 0 - Debug, 1 - Info, 2 - Warning, 3 - Error, 4 - Off
+
+
+
+
+
+ Hide private user's data
+
+
+
+
+
+```
+
== AppConfig XML Schema
{appconfig-xml-format-url}[The XML format], developed by AppConfig community, makes it easy for developers to define and deploy an app configuration.
It not only supports configuration variables having default values, but also provides a configuration UI description, which can be interpreted by the tool and which generates a plist file.
Moreover, specfile XML is consistently supported by major EMM vendors.
+AppConfig conformant spec file tailored to administrator needs and containing one or more of the above settings can be easily created using https://www.appconfig.org/www/appconfigspeccreator/[Config Spec Creator] tool hosted at https://www.appconfig.org[AppConfig website].
+
== Example: Deployment with MobileIron
1. Open https://appconfig.jamfresearch.com[AppConfig Generator].
diff --git a/docs/modules/ROOT/pages/ios_mdm_tables.adoc b/docs/modules/ROOT/pages/ios_mdm_tables.adoc
index 5d6a29727..2fcf1838d 100644
--- a/docs/modules/ROOT/pages/ios_mdm_tables.adoc
+++ b/docs/modules/ROOT/pages/ios_mdm_tables.adoc
@@ -1,4 +1,156 @@
+tag::actions[]
+[cols="1,2,3,4a,5",options=header]
+|===
+|Key
+|Type
+|Default
+|Description
+|Status
+
+
+|action.com.owncloud.action.collaborate.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.copy.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.createFolder.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.delete.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.discardscene.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.duplicate.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.favorite.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.links.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.makeAvailableOffline.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.makeUnavailableOffline.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.markup.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.mediaediting.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.move.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.openin.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.openscene.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.rename.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.scan.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.show-exif.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.unfavorite.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.unshare.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.upload.camera_media.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.uploadfile.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|action.com.owncloud.action.uploadphotos.enabled
+|bool
+|
+|Controls whether action can be accessed in the app UI.
+|advanced `candidate`
+
+|===
+end::actions[]
+
+
tag::app[]
[cols="1,2,3,4a,5",options=header]
|===
@@ -187,18 +339,18 @@ The following placeholders can be used to make it dynamic:
|Allow the use of background URL sessions. Note: depending on iOS version, the app may still choose not to use them. This settings is overriden by `force-background-url-sessions`.
|debugOnly
+|connection.connection-minimum-server-version
+|string
+|`10.0`
+|The minimum server version required.
+|debugOnly
+
|connection.force-background-url-sessions
|bool
|`false`
|Forces the use of background URL sessions. Overrides `allow-background-url-sessions`.
|debugOnly
-|connection.minimum-server-version
-|string
-|`10.0`
-|The minimum server version required.
-|debugOnly
-
|core.override-availability-signal
|bool
|
@@ -410,7 +562,7 @@ tag::logging[]
|Status
-|log.level
+|log.log-level
|int
|`4`
|Log level
@@ -440,27 +592,27 @@ tag::logging[]
|supported `candidate`
-|log.privacy-mask
+|log.log-privacy-mask
|bool
|`false`
|Controls whether certain objects in log statements should be masked for privacy.
|supported `candidate`
-|log.blank-filtered-messages
+|log.log-blank-filtered-messages
|bool
|`false`
|Controls whether filtered out messages should still be logged, but with the message replaced with `-`.
|advanced `candidate`
-|log.colored
+|log.log-colored
|bool
|`false`
|Controls whether log levels should be replaced with colored emojis.
|advanced `candidate`
-|log.enabled-components
+|log.log-enabled-components
|stringArray
-|`[writer.stderr writer.file]`
+|`[writer.stderr writer.file option.log-requests-and-responses]`
|List of enabled logging system components.
[cols="1,2"]
!===
@@ -479,7 +631,7 @@ tag::logging[]
|advanced `candidate`
-|log.format
+|log.log-format
|string
|`text`
|Determines the format that log messages are saved in
@@ -500,43 +652,43 @@ tag::logging[]
|advanced `candidate`
-|log.maximum-message-size
+|log.log-maximum-message-size
|int
|`0`
|Maximum length of a log message before the message is truncated. A value of 0 means no limit.
|advanced `candidate`
-|log.omit-matching
+|log.log-omit-matching
|stringArray
|
|If set, omits logs messages containing any of the exact terms in this array.
|advanced `candidate`
-|log.omit-tags
+|log.log-omit-tags
|stringArray
|
|If set, omits all log messages tagged with tags in this array.
|advanced `candidate`
-|log.only-matching
+|log.log-only-matching
|stringArray
|
|If set, only logs messages containing at least one of the exact terms in this array.
|advanced `candidate`
-|log.only-tags
+|log.log-only-tags
|stringArray
|
|If set, omits all log messages not tagged with tags in this array.
|advanced `candidate`
-|log.single-lined
+|log.log-single-lined
|bool
|`true`
|Controls whether messages spanning more than one line should be broken into their individual lines and each be logged with the complete lead-in/lead-out sequence.
|advanced `candidate`
-|log.synchronous
+|log.log-synchronous
|bool
|`false`
|Controls whether log messages should be written synchronously (which can impact performance) or asynchronously (which can loose messages in case of a crash).
@@ -612,18 +764,6 @@ tag::oidc[]
|OpenID Connect Redirect URI
|supported `candidate`
-|authentication-oauth2.oidc-register-client
-|bool
-|`true`
-|Use OpenID Connect Dynamic Client Registration if the `.well-known/openid-configuration` provides a `registration_endpoint`. If this option is enabled and a registration endpoint is available, `oa2-client-id` and `oa2-client-secret` will be ignored.
-|supported `candidate`
-
-|authentication-oauth2.oidc-register-client-name-template
-|string
-|`ownCloud/{{os.name}} {{app.version}}`
-|Client Name Template to use during OpenID Connect Dynamic Client Registration. In addition to the placeholders available for `http.user-agent`, `{{url.hostname}}` can also be used.
-|supported `candidate`
-
|authentication-oauth2.oidc-scope
|string
|`openid offline_access email profile`
@@ -634,6 +774,26 @@ tag::oidc[]
end::oidc[]
+tag::passcode[]
+[cols="1,2,3,4a,5",options=header]
+|===
+|Key
+|Type
+|Default
+|Description
+|Status
+
+
+|passcode.enforced
+|bool
+|`false`
+|Controls wether the user MUST establish a passcode upon app installation
+|advanced `candidate`
+
+|===
+end::passcode[]
+
+
tag::policies[]
[cols="1,2,3,4a,5",options=header]
|===
@@ -722,7 +882,7 @@ tag::security[]
|Status
-|connection.allowed-authentication-methods
+|connection.connection-allowed-authentication-methods
|stringArray
|
|Array of allowed authentication methods. Nil/Missing for no restrictions.
@@ -743,7 +903,7 @@ tag::security[]
|recommended `candidate`
-|connection.preferred-authentication-methods
+|connection.connection-preferred-authentication-methods
|stringArray
|`[com.owncloud.openid-connect com.owncloud.oauth2 com.owncloud.basicauth]`
|Array of authentication methods in order of preference (most preferred first).
@@ -764,13 +924,23 @@ tag::security[]
|recommended `candidate`
-|connection.certificate-extended-validation-rule
+|connection.connection-certificate-extended-validation-rule
|string
|`bookmarkCertificate == serverCertificate`
|Rule that defines the criteria a certificate needs to meet for OCConnection to recognize it as valid for a bookmark.
+
+Examples of expressions:
+- `bookmarkCertificate == serverCertificate`: the whole certificate needs to be identical to the one stored in the bookmark during setup.
+- `bookmarkCertificate.publicKeyData == serverCertificate.publicKeyData`: the public key of the received certificate needs to be identical to the public key stored in the bookmark during setup.
+- `serverCertificate.passedValidationOrIsUserAccepted == true`: any certificate is accepted as long as it has passed validation by the OS or was accepted by the user.
+- `serverCertificate.commonName == "demo.owncloud.org"`: the common name of the certificate must be "demo.owncloud.org".
+- `serverCertificate.rootCertificate.commonName == "DST Root CA X3"`: the common name of the root certificate must be "DST Root CA X3".
+- `serverCertificate.parentCertificate.commonName == "Let's Encrypt Authority X3"`: the common name of the parent certificate must be "Let's Encrypt Authority X3".
+- `serverCertificate.publicKeyData.sha256Hash.asFingerPrintString == "2A 00 98 90 BD … F7"`: the SHA-256 fingerprint of the public key of the server certificate needs to match the provided value.
+
|advanced `candidate`
-|connection.renewed-certificate-acceptance-rule
+|connection.connection-renewed-certificate-acceptance-rule
|string
|`(bookmarkCertificate.publicKeyData == serverCertificate.publicKeyData) OR ((check.parentCertificatesHaveIdenticalPublicKeys == true) AND (serverCertificate.passedValidationOrIsUserAccepted == true))`
|Rule that defines the criteria that need to be met for OCConnection to accept a renewed certificate and update the bookmark's certificate automatically instead of prompting the user. Used when the extended validation rule fails. Set this to `never` if the user should always be prompted when a server's certificate changed.
diff --git a/enterprise/MDM/AppConfig/log-passcode-specfile.xml b/enterprise/MDM/AppConfig/log-passcode-specfile.xml
new file mode 100644
index 000000000..46afcada4
--- /dev/null
+++ b/enterprise/MDM/AppConfig/log-passcode-specfile.xml
@@ -0,0 +1,66 @@
+
+ 1
+ com.owncloud.ios-app
+
+
+
+ 4
+
+
+
+
+
+
+
+
+ text
+
+
+
+
+
+
+
+
+ Logging
+
+
+
+
+ 0 - Debug, 1 - Info, 2 - Warning, 3 - Error, 4 - Off
+
+
+
+
+
+ Hide private user's data
+
+
+
+
+
+ Options: text, json, json-composed
+
+
+
+
+
+ Passcode
+
+
+
+
+ User is forced to set up passcode
+
+
+
+
+
\ No newline at end of file
diff --git a/enterprise/MDM/AppConfig/specfile.xml b/enterprise/MDM/AppConfig/specfile.xml
index 247f8bf65..6f945e2dc 100644
--- a/enterprise/MDM/AppConfig/specfile.xml
+++ b/enterprise/MDM/AppConfig/specfile.xml
@@ -35,16 +35,6 @@
-
-
- true
-
-
-
-
- true
-
-
ocs/v1.php/cloud/capabilities
@@ -65,6 +55,22 @@
remote.php/dav/files
+
+
+ 4
+
+
+
+
+
+
+
+
+ text
+
+
+
+
@@ -131,46 +137,69 @@
- Connection
+ Endpoints
-
+
+
+
+
-
- Require the certificate stored in the connection's bookmark if the connection's state is active.
-
-
+
+
+
+
-
- Endpoints
+ Logging
-
+
+
+ 0 - Debug, 1 - Info, 2 - Warning, 3 - Error, 4 - Off
+
-
+
+
+ Hide private user's data
+
-
+
+
+ Options: text, json, json-composed
+
-
+
+
+
+ Passcode
+
+
+
+ User is forced to set up passcode
+
diff --git a/ownCloud Intents/OCBookmarkManager+Extension.swift b/ownCloud Intents/OCBookmarkManager+Extension.swift
index 6ee506ddc..ca35e8795 100644
--- a/ownCloud Intents/OCBookmarkManager+Extension.swift
+++ b/ownCloud Intents/OCBookmarkManager+Extension.swift
@@ -36,7 +36,7 @@ extension OCBookmarkManager {
return accountList
}
- public func bookmark(for uuidString: String) -> OCBookmark? {
+ func bookmark(for uuidString: String) -> OCBookmark? {
return OCBookmarkManager.shared.bookmarks.filter({ $0.uuid.uuidString == uuidString}).first
}
diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj
index 617b1084d..46ec19dcc 100644
--- a/ownCloud.xcodeproj/project.pbxproj
+++ b/ownCloud.xcodeproj/project.pbxproj
@@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
02072E6023E46022006548A7 /* UIWindow+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02072E5F23E46022006548A7 /* UIWindow+Extension.swift */; };
0233F45E246E9D960095A799 /* UploadCameraMediaAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0233F45D246E9D960095A799 /* UploadCameraMediaAction.swift */; };
+ 0234EF0E2515138B00AE921A /* PasscodeSetupCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0234EF072515138A00AE921A /* PasscodeSetupCoordinator.swift */; };
024F3A2124A3AB410083E11E /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 024F3A2024A3AB410083E11E /* CrashReporter */; };
025F063324AA163C009D8FC5 /* DisplayExifMetadataAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025F063224AA163C009D8FC5 /* DisplayExifMetadataAction.swift */; };
025F063A24AA18C7009D8FC5 /* ImageMetadataViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025F063924AA18C7009D8FC5 /* ImageMetadataViewController.swift */; };
@@ -181,7 +182,7 @@
6E91F37E21ECA6FD009436D2 /* CopyAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E91F37D21ECA6FD009436D2 /* CopyAction.swift */; };
6EA78B8F2179B55400A5216A /* ImageScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA78B8E2179B55400A5216A /* ImageScrollView.swift */; };
75AC0B4AD332C8CC785FE349 /* Pods_ownCloudTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A56EA84D8AD331FFA604138B /* Pods_ownCloudTests.framework */; };
- A45A8D98137C902524B84E6D /* EarlGrey.framework in EarlGrey Copy Files */ = {isa = PBXBuildFile; fileRef = D0D9C062DD1E85A838608B0F /* EarlGrey.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ A45A8D98137C902524B84E6D /* EarlGrey.framework in EarlGrey Copy Files */ = {isa = PBXBuildFile; fileRef = D0D9C062DD1E85A838608B0F /* EarlGrey.framework */; };
DC0030C12350B1CE00BB8570 /* NSData+Encoding.m in Sources */ = {isa = PBXBuildFile; fileRef = DC0030BF2350B1CE00BB8570 /* NSData+Encoding.m */; };
DC0030C22350B1CE00BB8570 /* NSData+Encoding.h in Headers */ = {isa = PBXBuildFile; fileRef = DC0030C02350B1CE00BB8570 /* NSData+Encoding.h */; settings = {ATTRIBUTES = (Public, ); }; };
DC0030CB2350B75000BB8570 /* ScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1AC7CF2319ADAE002B7892 /* ScanViewController.swift */; };
@@ -840,6 +841,7 @@
/* Begin PBXFileReference section */
02072E5F23E46022006548A7 /* UIWindow+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+Extension.swift"; sourceTree = ""; };
0233F45D246E9D960095A799 /* UploadCameraMediaAction.swift */ = {isa = PBXFileReference; indentWidth = 5; lastKnownFileType = sourcecode.swift; path = UploadCameraMediaAction.swift; sourceTree = ""; };
+ 0234EF072515138A00AE921A /* PasscodeSetupCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeSetupCoordinator.swift; sourceTree = ""; };
025F063224AA163C009D8FC5 /* DisplayExifMetadataAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayExifMetadataAction.swift; sourceTree = ""; };
025F063924AA18C7009D8FC5 /* ImageMetadataViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageMetadataViewController.swift; sourceTree = ""; };
025FC71F247810AB009307A7 /* MediaUploadSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadSettingsViewController.swift; sourceTree = ""; };
@@ -2080,6 +2082,7 @@
DC0A354F24C0E18800FB58FC /* AppLock */ = {
isa = PBXGroup;
children = (
+ 0234EF072515138A00AE921A /* PasscodeSetupCoordinator.swift */,
593BAB96209F8A0500023634 /* AppLockManager.swift */,
597A404820AD59EF00B028B2 /* AppLockWindow.swift */,
593BAB44209AE1BC00023634 /* PasscodeViewController.swift */,
@@ -3964,6 +3967,7 @@
DCE4E44124C1A07E0051722F /* UITableViewController+Extension.swift in Sources */,
DC0A358E24C0E44B00FB58FC /* ThemeableColoredView.swift in Sources */,
DC0A356D24C0E42200FB58FC /* PasscodeViewController.swift in Sources */,
+ 0234EF0E2515138B00AE921A /* PasscodeSetupCoordinator.swift in Sources */,
DCA35DA724D309B600DBE2B0 /* OCFileProviderServiceSession+UploadByFileProvider.swift in Sources */,
39BE385D23435AFE0062A2FE /* String+Extension.swift in Sources */,
DC0A357A24C0E43700FB58FC /* CardViewController.swift in Sources */,
diff --git a/ownCloud/AppDelegate.swift b/ownCloud/AppDelegate.swift
index 72a3b24a9..7492f08de 100644
--- a/ownCloud/AppDelegate.swift
+++ b/ownCloud/AppDelegate.swift
@@ -153,6 +153,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
return true
}
+ VendorServices.shared.onFirstLaunch {
+ OCAppIdentity.shared.keychain?.wipe()
+ }
+
setupAndHandleCrashReports()
return true
diff --git a/ownCloud/Release Notes/ReleaseNotesHostViewController.swift b/ownCloud/Release Notes/ReleaseNotesHostViewController.swift
index 51014fac9..ef74a896d 100644
--- a/ownCloud/Release Notes/ReleaseNotesHostViewController.swift
+++ b/ownCloud/Release Notes/ReleaseNotesHostViewController.swift
@@ -103,6 +103,7 @@ class ReleaseNotesHostViewController: UIViewController {
footerText = String(format:"Thank you for using %@.\nIf you like our App, please leave an AppStore review.\n❤️".localized, appName)
}
footerButton.setTitle(footerText, for: .normal)
+
footerButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.footnote)
footerButton.titleLabel?.adjustsFontForContentSizeCategory = true
footerButton.titleLabel?.numberOfLines = 0
diff --git a/ownCloud/Resources/Assets.xcassets/gear.imageset/gear.png b/ownCloud/Resources/Assets.xcassets/gear.imageset/gear.png
deleted file mode 100644
index f13515bb5..000000000
Binary files a/ownCloud/Resources/Assets.xcassets/gear.imageset/gear.png and /dev/null differ
diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings
index 5ec9fdb2c..13de325fe 100644
--- a/ownCloud/Resources/en.lproj/Localizable.strings
+++ b/ownCloud/Resources/en.lproj/Localizable.strings
@@ -305,6 +305,7 @@
"Please try again in %@" = "Please try again in %@";
"Unlock %@" = "Unlock %@";
"Biometric authentication failed" = "Biometric authentication failed";
+"You are required to set the passcode" = "You are required to set the passcode";
/* Certificate management */
diff --git a/ownCloud/Server List/ServerListTableViewController.swift b/ownCloud/Server List/ServerListTableViewController.swift
index 7e4e90f13..225e8472b 100644
--- a/ownCloud/Server List/ServerListTableViewController.swift
+++ b/ownCloud/Server List/ServerListTableViewController.swift
@@ -210,6 +210,10 @@ class ServerListTableViewController: UITableViewController, Themeable {
settingsBarButtonItem
]
+ if AppLockManager.shared.passcode == nil && AppLockManager.shared.isPasscodeEnforced {
+ PasscodeSetupCoordinator(parentViewController: self, action: .setup).start()
+ }
+
if showBetaWarning, shownFirstTime {
showBetaWarning = !considerAutoLogin()
}
diff --git a/ownCloud/Settings/SecuritySettingsSection.swift b/ownCloud/Settings/SecuritySettingsSection.swift
index dc50ec18b..9015efbbc 100644
--- a/ownCloud/Settings/SecuritySettingsSection.swift
+++ b/ownCloud/Settings/SecuritySettingsSection.swift
@@ -57,19 +57,6 @@ class SecuritySettingsSection: SettingsSection {
}
}
- var isPasscodeSecurityEnabled: Bool {
- get {
- if ProcessInfo.processInfo.arguments.contains("UI-Testing") {
- return true
- } else {
- return AppLockManager.shared.lockEnabled
- }
- }
- set(newValue) {
- AppLockManager.shared.lockEnabled = newValue
- updateUI()
- }
- }
var isBiometricalSecurityEnabled: Bool {
get {
if ProcessInfo.processInfo.arguments.contains("UI-Testing") {
@@ -140,106 +127,34 @@ class SecuritySettingsSection: SettingsSection {
// Creation of the passcode row.
passcodeRow = StaticTableViewRow(switchWithAction: { [weak self] (row, sender) in
- if let passcodeSwitch = sender as? UISwitch {
- if let viewController = row.viewController {
-
- var passcodeViewController: PasscodeViewController?
- var defaultMessage : String?
-
- // Handlers
- let cancelHandler:PasscodeViewControllerCancelHandler = { (passcodeViewController: PasscodeViewController) in
- passcodeViewController.dismiss(animated: true, completion: {
- self?.isPasscodeSecurityEnabled = !passcodeSwitch.isOn
- })
- self?.passcodeFromFirstStep = nil
- }
- if passcodeSwitch.isOn {
- defaultMessage = "Enter code".localized
- } else {
- defaultMessage = "Delete code".localized
- }
+ guard let passcodeSwitch = sender as? UISwitch, let viewController = row.viewController else { return }
- passcodeViewController = PasscodeViewController(cancelHandler: cancelHandler, completionHandler: { (passcodeViewController: PasscodeViewController, passcode: String) in
- if !passcodeSwitch.isOn {
- // Delete
- if passcode == AppLockManager.shared.passcode {
- // Success
- AppLockManager.shared.passcode = nil
- AppLockManager.shared.unlocked = false
- passcodeViewController.dismiss(animated: true, completion: {
- self?.isPasscodeSecurityEnabled = passcodeSwitch.isOn
- self?.updateUI()
- })
- } else {
- // Error
- passcodeViewController.message = defaultMessage
- passcodeViewController.errorMessage = "Incorrect code".localized
- passcodeViewController.passcode = nil
- }
- } else {
- // Add
- if self?.passcodeFromFirstStep == nil {
- // First step
- self?.passcodeFromFirstStep = passcode
- passcodeViewController.message = "Repeat code".localized
- passcodeViewController.passcode = nil
- } else {
- // Second step
- if self?.passcodeFromFirstStep == passcode {
- // Passcode right
- // Save to keychain
- AppLockManager.shared.passcode = passcode
- AppLockManager.shared.unlocked = true
- passcodeViewController.dismiss(animated: true, completion: {
- self?.isPasscodeSecurityEnabled = passcodeSwitch.isOn
- self?.updateUI()
- })
- } else {
- //Passcode is not the same
- passcodeViewController.message = defaultMessage
- passcodeViewController.errorMessage = "The entered codes are different".localized
- passcodeViewController.passcode = nil
- }
- self?.passcodeFromFirstStep = nil
- }
- }
- })
-
- passcodeViewController?.message = defaultMessage
- viewController.present(passcodeViewController!, animated: true, completion: nil)
+ let action: PasscodeAction = passcodeSwitch.isOn ? .setup : .delete
+ PasscodeSetupCoordinator(parentViewController: viewController, action: action, completion: { (cancelled) in
+ if cancelled {
+ passcodeSwitch.isOn = !passcodeSwitch.isOn
+ } else {
+ self?.updateUI()
}
- }
- }, title: "Passcode Lock".localized, value: isPasscodeSecurityEnabled, identifier: "passcodeSwitchIdentifier")
+
+ }).start()
+
+ }, title: "Passcode Lock".localized, value: PasscodeSetupCoordinator.isPasscodeSecurityEnabled, identifier: "passcodeSwitchIdentifier")
// Creation of the biometrical row.
if let biometricalSecurityName = LAContext().supportedBiometricsAuthenticationName() {
- biometricalRow = StaticTableViewRow(switchWithAction: { [weak self] (row, sender) in
- if let biometricalSwitch = sender as? UISwitch {
- if let viewController = row.viewController {
- var passcodeViewController: PasscodeViewController?
-
- passcodeViewController = PasscodeViewController(cancelHandler: { (passcodeViewController: PasscodeViewController) in
- passcodeViewController.dismiss(animated: true, completion: {
- biometricalSwitch.setOn(self!.isBiometricalSecurityEnabled, animated: true)
- })
- }, completionHandler: { (passcodeViewController: PasscodeViewController, passcode: String) in
- if passcode == AppLockManager.shared.passcode {
- // Success
- passcodeViewController.dismiss(animated: true, completion: {
- self?.isBiometricalSecurityEnabled = biometricalSwitch.isOn
- })
- } else {
- // Error
- passcodeViewController.errorMessage = "Incorrect code".localized
- passcodeViewController.passcode = nil
- }
- })
-
- passcodeViewController?.message = "Enter code".localized
- viewController.present(passcodeViewController!, animated: true, completion: nil)
+ biometricalRow = StaticTableViewRow(switchWithAction: { (row, sender) in
+ guard let biometricalSwitch = sender as? UISwitch, let viewController = row.viewController else { return }
+
+ PasscodeSetupCoordinator(parentViewController: viewController, completion: { (cancelled) in
+ if cancelled {
+ biometricalSwitch.isOn = !biometricalSwitch.isOn
+ } else {
+ biometricalSwitch.isOn = PasscodeSetupCoordinator.isBiometricalSecurityEnabled
}
- }
+ }).startBiometricalFlow(biometricalSwitch.isOn)
+
}, title: biometricalSecurityName, value: isBiometricalSecurityEnabled, identifier: "BiometricalSwitch")
}
@@ -262,11 +177,13 @@ class SecuritySettingsSection: SettingsSection {
var rowsToAdd: [StaticTableViewRow] = []
var rowsToRemove: [StaticTableViewRow] = []
- if !rows.contains(passcodeRow!) {
- rowsToAdd.append(passcodeRow!)
+ if !AppLockManager.shared.isPasscodeEnforced {
+ if !rows.contains(passcodeRow!) {
+ rowsToAdd.append(passcodeRow!)
+ }
}
- if isPasscodeSecurityEnabled {
+ if PasscodeSetupCoordinator.isPasscodeSecurityEnabled {
if !rows.contains(frequencyRow!) {
rowsToAdd.append(frequencyRow!)
}
@@ -312,6 +229,6 @@ class SecuritySettingsSection: SettingsSection {
}
}
- passcodeRow?.value = isPasscodeSecurityEnabled
+ passcodeRow?.value = PasscodeSetupCoordinator.isPasscodeSecurityEnabled
}
}
diff --git a/ownCloud/Settings/SettingsViewController.swift b/ownCloud/Settings/SettingsViewController.swift
index 21c029c45..0089eb349 100644
--- a/ownCloud/Settings/SettingsViewController.swift
+++ b/ownCloud/Settings/SettingsViewController.swift
@@ -43,8 +43,9 @@ class SettingsViewController: StaticTableViewController {
if #available(iOS 13, *), // Require iOS 13
!OCLicenseEMMProvider.isEMMVersion, // Do not show purchases in the EMM version
- !VendorServices.shared.isBranded,
- OCLicenseEnterpriseProvider.numberOfEnterpriseAccounts < OCBookmarkManager.shared.bookmarks.count { // Do only show purchases section if there's at least one non-Enterprise account
+ // Do only show purchases section if there's at least one non-Enterprise account
+ OCLicenseEnterpriseProvider.numberOfEnterpriseAccounts < OCBookmarkManager.shared.bookmarks.count, !VendorServices.shared.isBranded // Do not show purchases in branded app
+ {
self.addSection(PurchasesSettingsSection(userDefaults: userDefaults))
}
diff --git a/ownCloudAppShared/AppLock/AppLockManager.swift b/ownCloudAppShared/AppLock/AppLockManager.swift
index 971ea3c2f..fd9febe56 100644
--- a/ownCloudAppShared/AppLock/AppLockManager.swift
+++ b/ownCloudAppShared/AppLock/AppLockManager.swift
@@ -143,6 +143,10 @@ public class AppLockManager: NSObject {
}
}
+ public var isPasscodeEnforced : Bool {
+ return (self.classSetting(forOCClassSettingsKey: .passcodeEnforced) as? Bool) ?? false
+ }
+
// Set a view controller only, if you want to use it in an extension, when UIWindow is not working
public var passwordViewHostViewController: UIViewController?
@@ -504,3 +508,34 @@ public class AppLockManager: NSObject {
}
}
}
+
+// MARK: - OCClassSettings support
+
+extension OCClassSettingsIdentifier {
+ static let passcode = OCClassSettingsIdentifier("passcode")
+}
+
+extension OCClassSettingsKey {
+ static let passcodeEnforced = OCClassSettingsKey("enforced")
+}
+
+extension AppLockManager: OCClassSettingsSupport {
+ public static var classSettingsIdentifier: OCClassSettingsIdentifier = .passcode
+
+ public static func defaultSettings(forIdentifier identifier: OCClassSettingsIdentifier) -> [OCClassSettingsKey : Any]? {
+ return [
+ .passcodeEnforced : false
+ ]
+ }
+
+ public static func classSettingsMetadata() -> [OCClassSettingsKey : [OCClassSettingsMetadataKey : Any]]? {
+ return [
+ OCClassSettingsKey.passcodeEnforced: [
+ OCClassSettingsMetadataKey.type: OCClassSettingsMetadataType.boolean,
+ OCClassSettingsMetadataKey.description: "Controls wether the user MUST establish a passcode upon app installation",
+ OCClassSettingsMetadataKey.category: "Passcode",
+ OCClassSettingsMetadataKey.status: OCClassSettingsKeyStatus.advanced
+ ]
+ ]
+ }
+}
diff --git a/ownCloudAppShared/AppLock/PasscodeSetupCoordinator.swift b/ownCloudAppShared/AppLock/PasscodeSetupCoordinator.swift
new file mode 100644
index 000000000..778b922ce
--- /dev/null
+++ b/ownCloudAppShared/AppLock/PasscodeSetupCoordinator.swift
@@ -0,0 +1,173 @@
+//
+// PasscodeSetupCoordinator.swift
+// ownCloud
+//
+// Created by Michael Neuwert on 07.08.20.
+// Copyright © 2020 ownCloud GmbH. All rights reserved.
+//
+
+/*
+* Copyright (C) 2020, ownCloud GmbH.
+*
+* This code is covered by the GNU Public License Version 3.
+*
+* For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/
+* You should have received a copy of this license along with this program. If not, see .
+*
+*/
+
+import UIKit
+
+public enum PasscodeAction {
+ case setup
+ case delete
+
+ var localizedDescription : String {
+ switch self {
+ case .setup: return "Enter code".localized
+ case .delete: return "Delete code".localized
+ }
+ }
+}
+
+public class PasscodeSetupCoordinator {
+
+ public typealias PasscodeSetupCompletion = (_ cancelled:Bool) -> Void
+
+ private var parentViewController: UIViewController
+ private var action: PasscodeAction
+
+ private var passcodeViewController: PasscodeViewController?
+ private var passcodeFromFirstStep: String?
+ private var completionHandler: PasscodeSetupCompletion?
+
+ public class var isPasscodeSecurityEnabled: Bool {
+ get {
+ if ProcessInfo.processInfo.arguments.contains("UI-Testing") {
+ return true
+ } else {
+ return AppLockManager.shared.lockEnabled
+ }
+ }
+ set(newValue) {
+ AppLockManager.shared.lockEnabled = newValue
+ }
+ }
+ public class var isBiometricalSecurityEnabled: Bool {
+ get {
+ if ProcessInfo.processInfo.arguments.contains("UI-Testing") {
+ return true
+ } else {
+ return AppLockManager.shared.biometricalSecurityEnabled
+ }
+ }
+ set(newValue) {
+ AppLockManager.shared.biometricalSecurityEnabled = newValue
+ }
+ }
+
+ public init(parentViewController:UIViewController, action:PasscodeAction = .setup, completion:PasscodeSetupCompletion? = nil) {
+ self.parentViewController = parentViewController
+ self.action = action
+ }
+
+ public func start() {
+
+ passcodeViewController = PasscodeViewController(cancelHandler: { (passcodeViewController) in
+ passcodeViewController.dismiss(animated: true) {
+ self.completionHandler?(true)
+ }
+ }, completionHandler: { (passcodeViewController, passcode) in
+ if self.action == .delete {
+ if passcode == AppLockManager.shared.passcode {
+ // Success -> Remove stored passcode and unlock the app
+ self.resetPasscode()
+ self.passcodeViewController?.dismiss(animated: true, completion: {
+ self.completionHandler?(false)
+ })
+ } else {
+ // Entered passcode doesn't match saved ones
+ self.updateUI(with: self.action.localizedDescription, errorMessage: "Incorrect code".localized)
+ }
+ } else { // Setup
+ if self.passcodeFromFirstStep == nil {
+ // 1) Enter passcode
+ self.passcodeFromFirstStep = passcode
+ self.updateUI(with: "Repeat code".localized)
+ } else {
+ // 2) Confirm passcode
+ if self.passcodeFromFirstStep == passcode {
+ // Confirmed passcode matches the original ones -> save and lock the app
+ self.lock(with: passcode)
+ self.passcodeViewController?.dismiss(animated: true, completion: {
+ self.completionHandler?(false)
+ })
+ } else {
+ //Passcode is not the same
+ self.updateUI(with: self.action.localizedDescription, errorMessage: "The entered codes are different".localized)
+ }
+ self.passcodeFromFirstStep = nil
+ }
+ }
+ }, hasCancelButton: !AppLockManager.shared.isPasscodeEnforced)
+
+ passcodeViewController?.message = self.action.localizedDescription
+ if AppLockManager.shared.isPasscodeEnforced {
+ passcodeViewController?.errorMessage = "You are required to set the passcode".localized
+ }
+
+ if parentViewController.presentedViewController != nil {
+ parentViewController.dismiss(animated: false) { [weak self] in
+ guard let passcodeController = self?.passcodeViewController else { return }
+ self?.parentViewController.present(passcodeController, animated: false, completion: nil)
+ }
+ } else {
+ parentViewController.present(passcodeViewController!, animated: true, completion: nil)
+ }
+
+ }
+
+ public func startBiometricalFlow(_ enable:Bool) {
+
+ passcodeViewController = PasscodeViewController(cancelHandler: { (passcodeViewController: PasscodeViewController) in
+ passcodeViewController.dismiss(animated: true) {
+ self.completionHandler?(true)
+ }
+ }, completionHandler: { (passcodeViewController: PasscodeViewController, passcode: String) in
+ if passcode == AppLockManager.shared.passcode {
+ // Success
+ passcodeViewController.dismiss(animated: true, completion: {
+ self.completionHandler?(false)
+ PasscodeSetupCoordinator.isBiometricalSecurityEnabled = enable
+ })
+ } else {
+ // Error
+ passcodeViewController.errorMessage = "Incorrect code".localized
+ passcodeViewController.passcode = nil
+ }
+ })
+
+ passcodeViewController?.message = self.action.localizedDescription
+ parentViewController.present(passcodeViewController!, animated: true, completion: nil)
+ }
+
+ private func resetPasscode() {
+ AppLockManager.shared.passcode = nil
+ AppLockManager.shared.unlocked = false
+ PasscodeSetupCoordinator.isPasscodeSecurityEnabled = false
+ }
+
+ private func lock(with passcode:String) {
+ AppLockManager.shared.passcode = passcode
+ AppLockManager.shared.unlocked = true
+ PasscodeSetupCoordinator.isPasscodeSecurityEnabled = true
+ }
+
+ private func updateUI(with message:String, errorMessage:String? = nil) {
+ self.passcodeViewController?.message = message
+ if errorMessage != nil {
+ self.passcodeViewController?.errorMessage = errorMessage
+ }
+ self.passcodeViewController?.passcode = nil
+ }
+}
diff --git a/ownCloudAppShared/Client/Actions/Action.swift b/ownCloudAppShared/Client/Actions/Action.swift
index 1e679c5ac..391df0910 100644
--- a/ownCloudAppShared/Client/Actions/Action.swift
+++ b/ownCloudAppShared/Client/Actions/Action.swift
@@ -477,3 +477,41 @@ open class Action : NSObject {
}
}
+
+extension OCClassSettingsIdentifier {
+ static let action = OCClassSettingsIdentifier("action")
+}
+
+extension Action : OCClassSettingsSupport {
+ public static let classSettingsIdentifier : OCClassSettingsIdentifier = .action
+
+ public static func defaultSettings(forIdentifier identifier: OCClassSettingsIdentifier) -> [OCClassSettingsKey : Any]? {
+ return nil
+ }
+
+ static func enabledKey() -> OCClassSettingsKey? {
+ guard let identifier = Self.identifier?.rawValue else { return nil }
+ return OCClassSettingsKey(identifier + ".enabled")
+ }
+
+ static var enabled : Bool {
+ if let key = Self.enabledKey() {
+ if let value = Self.classSetting(forOCClassSettingsKey: key) as? Bool {
+ return value
+ }
+ }
+ return true
+ }
+
+ public static func classSettingsMetadata() -> [OCClassSettingsKey : [OCClassSettingsMetadataKey : Any]]? {
+ guard let enabledKey = Self.enabledKey() else { return nil }
+ return [
+ enabledKey : [
+ .type : OCClassSettingsMetadataType.boolean,
+ .description : "Controls whether action can be accessed in the app UI.",
+ .category : "Actions",
+ .status : OCClassSettingsKeyStatus.advanced
+ ]
+ ]
+ }
+}
diff --git a/ownCloudAppShared/Tools/VendorServices.swift b/ownCloudAppShared/Tools/VendorServices.swift
index 0e1b7fefd..de947aae5 100644
--- a/ownCloudAppShared/Tools/VendorServices.swift
+++ b/ownCloudAppShared/Tools/VendorServices.swift
@@ -22,6 +22,11 @@ import ownCloudSDK
import ownCloudApp
public class VendorServices : NSObject {
+
+ enum UserDefaultsKeys: String {
+ case notFirstAppLaunch
+ }
+
// MARK: - App version information
public var appVersion: String {
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String {
@@ -269,6 +274,16 @@ public class VendorServices : NSObject {
// Make sure at least 122 have elapsed since last prompting (Apple allows to show the dialog 3 times per 365 days)
AppStatistics.shared.requestAppStoreReview(onceInDays: 122)
}
+
+ public func onFirstLaunch(executeBlock:() -> Void) {
+ guard let userDefaults = OCAppIdentity.shared.userDefaults else { return }
+ guard userDefaults.bool(forKey: UserDefaultsKeys.notFirstAppLaunch.rawValue) == false else { return }
+
+ executeBlock()
+
+ userDefaults.setValue(true, forKey: UserDefaultsKeys.notFirstAppLaunch.rawValue)
+ userDefaults.synchronize()
+ }
}
extension VendorServices: MFMailComposeViewControllerDelegate {