From 7b180478003dd97b96a7bc6d3d79a89cbeee0502 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 19 Jun 2018 19:35:42 -0400 Subject: [PATCH] CBA support for iOS (#1767) * CBA Support * cba support for iOS Share extension * Autologin with credentials * Set initial config for ExperimentalClientSideCertEnable as false * Fix mattermost-redux to cba branch * feedback review * Fix eslint --- .gitignore | 1 + app/components/custom_list/index.test.js | 2 +- .../file_attachment_document.js | 5 + .../offline_indicator/offline_indicator.js | 16 +- app/fetch_preconfig.js | 21 +- app/screens/channel/channel.js | 16 +- app/screens/image_preview/downloader.ios.js | 5 + app/screens/select_server/index.js | 5 + app/screens/select_server/select_server.js | 111 +++++- app/store/middleware.js | 1 + app/utils/image_cache_manager.js | 5 + assets/base/config.json | 1 + ios/0155-keys.png | Bin 0 -> 2532 bytes ios/KeyShareConsumer.storyboard | 74 ++++ ios/Mattermost.xcodeproj/project.pbxproj | 51 ++- .../xcschemes/Mattermost.xcscheme | 2 - ios/Mattermost/Mattermost.entitlements | 4 + .../MattermostShare.entitlements | 18 + ios/MattermostShare/SessionManager.h | 6 + ios/MattermostShare/SessionManager.m | 354 +++++++++++------- ios/MattermostShare/ShareViewController.m | 6 +- packager/modulePaths.js | 1 + share_extension/ios/extension_post.js | 2 + share_extension/ios/index.js | 1 + 24 files changed, 521 insertions(+), 187 deletions(-) create mode 100644 ios/0155-keys.png create mode 100644 ios/KeyShareConsumer.storyboard diff --git a/.gitignore b/.gitignore index b6686cbc7..8764da11b 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ build/ *.perspectivev3 !default.perspectivev3 xcuserdata +xcshareddata *.xccheckout *.moved-aside DerivedData diff --git a/app/components/custom_list/index.test.js b/app/components/custom_list/index.test.js index 0df1be395..372a1e448 100644 --- a/app/components/custom_list/index.test.js +++ b/app/components/custom_list/index.test.js @@ -1,5 +1,5 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. +// See LICENSE.txt for license information. import React from 'react'; import {configure, shallow} from 'enzyme'; diff --git a/app/components/file_attachment_list/file_attachment_document.js b/app/components/file_attachment_list/file_attachment_document.js index ffe33b668..ceb412d61 100644 --- a/app/components/file_attachment_list/file_attachment_document.js +++ b/app/components/file_attachment_list/file_attachment_document.js @@ -22,8 +22,11 @@ import tinyColor from 'tinycolor2'; import {getFileUrl} from 'mattermost-redux/utils/file_utils.js'; import {DeviceTypes} from 'app/constants/'; +import mattermostBucket from 'app/mattermost_bucket'; import {changeOpacity} from 'app/utils/theme'; +import LocalConfig from 'assets/config'; + import FileAttachmentIcon from './file_attachment_icon'; const {DOCUMENTS_PATH} = DeviceTypes; @@ -106,6 +109,7 @@ export default class FileAttachmentDocument extends PureComponent { this.setState({didCancel: false}); try { + const certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId); const isDir = await RNFetchBlob.fs.isDir(DOCUMENTS_PATH); if (!isDir) { try { @@ -122,6 +126,7 @@ export default class FileAttachmentDocument extends PureComponent { indicator: true, overwrite: true, path, + certificate, }; const mime = data.mime_type.split(';')[0]; diff --git a/app/components/offline_indicator/offline_indicator.js b/app/components/offline_indicator/offline_indicator.js index 80d2d38f9..ef5a9779c 100644 --- a/app/components/offline_indicator/offline_indicator.js +++ b/app/components/offline_indicator/offline_indicator.js @@ -16,7 +16,9 @@ import IonIcon from 'react-native-vector-icons/Ionicons'; import FormattedText from 'app/components/formatted_text'; import {ViewTypes} from 'app/constants'; +import mattermostBucket from 'app/mattermost_bucket'; import checkNetwork from 'app/utils/network'; +import LocalConfig from 'assets/config'; import {RequestStatus} from 'mattermost-redux/constants'; @@ -95,7 +97,7 @@ export default class OfflineIndicator extends Component { this.setState({network: CONNECTING}, () => { if (result) { this.props.actions.connection(true); - this.props.actions.initWebSocket(Platform.OS); + this.initializeWebSocket(); } else { this.setState({network: OFFLINE}); } @@ -154,6 +156,18 @@ export default class OfflineIndicator extends Component { return IOS_TOP_PORTRAIT; }; + initializeWebSocket = async () => { + const {actions} = this.props; + const {initWebSocket} = actions; + const platform = Platform.OS; + let certificate = null; + if (platform === 'ios') { + certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId); + } + + initWebSocket(platform, null, null, null, {certificate}); + }; + offline = () => { this.setState({network: OFFLINE}, () => { this.show(); diff --git a/app/fetch_preconfig.js b/app/fetch_preconfig.js index c3c8af3e7..3cac7a6d6 100644 --- a/app/fetch_preconfig.js +++ b/app/fetch_preconfig.js @@ -1,12 +1,16 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. +// See LICENSE.txt for license information. +import {Platform} from 'react-native'; import RNFetchBlob from 'react-native-fetch-blob'; import {Client4} from 'mattermost-redux/client'; import {General} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import mattermostBucket from 'app/mattermost_bucket'; +import LocalConfig from 'assets/config'; + const HEADER_X_VERSION_ID = 'X-Version-Id'; const HEADER_X_CLUSTER_ID = 'X-Cluster-Id'; const HEADER_TOKEN = 'Token'; @@ -91,6 +95,15 @@ Client4.doFetchWithResponse = async (url, options) => { }; }; -window.fetch = new RNFetchBlob.polyfill.Fetch({ - auto: true, -}).build(); +if (Platform.OS === 'ios') { + mattermostBucket.getPreference('cert', LocalConfig.AppGroupId).then((certificate) => { + window.fetch = new RNFetchBlob.polyfill.Fetch({ + auto: true, + certificate, + }).build(); + }); +} else { + window.fetch = new RNFetchBlob.polyfill.Fetch({ + auto: true, + }).build(); +} diff --git a/app/screens/channel/channel.js b/app/screens/channel/channel.js index e917bfc90..f5aea1526 100644 --- a/app/screens/channel/channel.js +++ b/app/screens/channel/channel.js @@ -19,6 +19,7 @@ import KeyboardLayout from 'app/components/layout/keyboard_layout'; import OfflineIndicator from 'app/components/offline_indicator'; import SafeAreaView from 'app/components/safe_area_view'; import StatusBar from 'app/components/status_bar'; +import mattermostBucket from 'app/mattermost_bucket'; import {preventDoubleTap} from 'app/utils/tap'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; import PostTextbox from 'app/components/post_textbox'; @@ -175,13 +176,12 @@ export default class Channel extends PureComponent { const {actions} = this.props; const { closeWebSocket, - initWebSocket, startPeriodicStatusUpdates, stopPeriodicStatusUpdates, } = actions; if (open) { - initWebSocket(Platform.OS); + this.initializeWebSocket(); startPeriodicStatusUpdates(); } else { closeWebSocket(true); @@ -204,6 +204,18 @@ export default class Channel extends PureComponent { this.props.actions.selectDefaultTeam(); }; + initializeWebSocket = async () => { + const {actions} = this.props; + const {initWebSocket} = actions; + const platform = Platform.OS; + let certificate = null; + if (platform === 'ios') { + certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId); + } + + initWebSocket(platform, null, null, null, {certificate}); + }; + loadChannels = (teamId) => { const { loadChannelsIfNecessary, diff --git a/app/screens/image_preview/downloader.ios.js b/app/screens/image_preview/downloader.ios.js index 5641a9689..fc7ef6dca 100644 --- a/app/screens/image_preview/downloader.ios.js +++ b/app/screens/image_preview/downloader.ios.js @@ -12,8 +12,11 @@ import {intlShape} from 'react-intl'; import {Client4} from 'mattermost-redux/client'; import FormattedText from 'app/components/formatted_text'; +import mattermostBucket from 'app/mattermost_bucket'; import {emptyFunction} from 'app/utils/general'; +import LocalConfig from 'assets/config'; + const {View: AnimatedView} = Animated; export default class Downloader extends PureComponent { @@ -294,12 +297,14 @@ export default class Downloader extends PureComponent { } if (downloadFile) { + const certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId); const imageUrl = Client4.getFileUrl(data.id); const options = { session: data.id, timeout: 10000, indicator: true, overwrite: true, + certificate, }; if (downloadPath && prompt) { diff --git a/app/screens/select_server/index.js b/app/screens/select_server/index.js index eef58a407..c453c58cd 100644 --- a/app/screens/select_server/index.js +++ b/app/screens/select_server/index.js @@ -5,8 +5,10 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getPing, resetPing, setServerVersion} from 'mattermost-redux/actions/general'; +import {login} from 'mattermost-redux/actions/users'; import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade'; +import {getSession, handleSuccessfulLogin} from 'app/actions/views/login'; import {loadConfigAndLicense} from 'app/actions/views/root'; import {handleServerUrlChanged} from 'app/actions/views/select_server'; import getClientUpgrade from 'app/selectors/client_upgrade'; @@ -33,9 +35,12 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ + handleSuccessfulLogin, getPing, + getSession, handleServerUrlChanged, loadConfigAndLicense, + login, resetPing, setLastUpgradeCheck, setServerVersion, diff --git a/app/screens/select_server/select_server.js b/app/screens/select_server/select_server.js index ed29c4180..1be34586c 100644 --- a/app/screens/select_server/select_server.js +++ b/app/screens/select_server/select_server.js @@ -18,16 +18,20 @@ import { View, } from 'react-native'; import Button from 'react-native-button'; +import RNFetchBlob from 'react-native-fetch-blob'; import {Client4} from 'mattermost-redux/client'; import ErrorText from 'app/components/error_text'; import FormattedText from 'app/components/formatted_text'; import {UpgradeTypes} from 'app/constants/view'; +import mattermostBucket from 'app/mattermost_bucket'; +import PushNotifications from 'app/push_notifications'; import {GlobalStyles} from 'app/styles'; import checkUpgradeType from 'app/utils/client_upgrade'; import {isValidUrl, stripTrailingSlashes} from 'app/utils/url'; import {preventDoubleTap} from 'app/utils/tap'; +import tracker from 'app/utils/time_tracker'; import LocalConfig from 'assets/config'; @@ -36,7 +40,10 @@ export default class SelectServer extends PureComponent { actions: PropTypes.shape({ getPing: PropTypes.func.isRequired, handleServerUrlChanged: PropTypes.func.isRequired, + handleSuccessfulLogin: PropTypes.func.isRequired, + getSession: PropTypes.func.isRequired, loadConfigAndLicense: PropTypes.func.isRequired, + login: PropTypes.func.isRequired, resetPing: PropTypes.func.isRequired, setLastUpgradeCheck: PropTypes.func.isRequired, setServerVersion: PropTypes.func.isRequired, @@ -108,6 +115,23 @@ export default class SelectServer extends PureComponent { } } + goToNextScreen = (screen, title) => { + const {navigator, theme} = this.props; + navigator.push({ + screen, + title, + animated: true, + backButtonTitle: '', + navigatorStyle: { + navBarHidden: LocalConfig.AutoSelectServerUrl, + disabledBackGesture: LocalConfig.AutoSelectServerUrl, + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + }, + }); + }; + handleNavigatorEvent = (event) => { if (event.id === 'didDisappear') { this.setState({ @@ -125,7 +149,7 @@ export default class SelectServer extends PureComponent { title: intl.formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Client Upgrade'}), backButtonTitle: '', navigatorStyle: { - navBarHidden: false, + navBarHidden: LocalConfig.AutoSelectServerUrl, disabledBackGesture: LocalConfig.AutoSelectServerUrl, statusBarHidden: true, statusBarHideWithNavBar: true, @@ -138,11 +162,11 @@ export default class SelectServer extends PureComponent { upgradeType, }, }); - } + }; handleLoginOptions = (props = this.props) => { const {intl} = this.context; - const {config, license, theme} = props; + const {config, license} = props; const samlEnabled = config.EnableSaml === 'true' && license.IsLicensed === 'true' && license.SAML === 'true'; const gitlabEnabled = config.EnableSignUpWithGitLab === 'true'; @@ -161,21 +185,21 @@ export default class SelectServer extends PureComponent { title = intl.formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'}); } - this.props.navigator.push({ - screen, - title, - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarHidden: LocalConfig.AutoSelectServerUrl, - disabledBackGesture: LocalConfig.AutoSelectServerUrl, - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - }, - }); - this.props.actions.resetPing(); + + if (Platform.OS === 'ios' && LocalConfig.ExperimentalClientSideCertEnable) { + if (config.ExperimentalClientSideCertEnable === 'true' && config.ExperimentalClientSideCertCheck === 'primary') { + // log in automatically and send directly to the channel screen + this.loginWithCertificate(); + return; + } + + setTimeout(() => { + this.goToNextScreen(screen, title); + }, 350); + } else { + this.goToNextScreen(screen, title); + } }; handleAndroidKeyboard = () => { @@ -186,6 +210,44 @@ export default class SelectServer extends PureComponent { this.setState({url}); }; + loginWithCertificate = async () => { + const {intl, navigator} = this.props; + + tracker.initialLoad = Date.now(); + + await this.props.actions.login('credential', 'password'); + await this.props.actions.handleSuccessfulLogin(); + const expiresAt = await this.props.actions.getSession(); + + if (expiresAt) { + PushNotifications.localNotificationSchedule({ + date: new Date(expiresAt), + message: intl.formatMessage({ + id: 'mobile.session_expired', + defaultMessage: 'Session Expired: Please log in to continue receiving notifications.', + }), + userInfo: { + localNotification: true, + }, + }); + } + + navigator.resetTo({ + screen: 'Channel', + title: '', + animated: false, + backButtonTitle: '', + navigatorStyle: { + animated: true, + animationType: 'fade', + navBarHidden: true, + statusBarHidden: false, + statusBarHideWithNavBar: false, + screenBackgroundColor: 'transparent', + }, + }); + }; + onClick = preventDoubleTap(async () => { const urlParse = require('url-parse'); const preUrl = urlParse(this.state.url, true); @@ -212,7 +274,20 @@ export default class SelectServer extends PureComponent { return; } - this.pingServer(url); + if (LocalConfig.ExperimentalClientSideCertEnable && Platform.OS === 'ios') { + RNFetchBlob.cba.selectCertificate((certificate) => { + if (certificate) { + mattermostBucket.setPreference('cert', certificate, LocalConfig.AppGroupId); + window.fetch = new RNFetchBlob.polyfill.Fetch({ + auto: true, + certificate, + }).build(); + this.pingServer(url); + } + }); + } else { + this.pingServer(url); + } }); pingServer = (url) => { diff --git a/app/store/middleware.js b/app/store/middleware.js index 0d2f1d14e..2ada8e05f 100644 --- a/app/store/middleware.js +++ b/app/store/middleware.js @@ -361,6 +361,7 @@ export function shareExtensionData() { switch (action.type) { case UserTypes.LOGOUT_SUCCESS: + mattermostBucket.removePreference('cert', Config.AppGroupId); mattermostBucket.removePreference('emm', Config.AppGroupId); mattermostBucket.removeFile('entities', Config.AppGroupId); break; diff --git a/app/utils/image_cache_manager.js b/app/utils/image_cache_manager.js index f7ddab228..28627a8ef 100644 --- a/app/utils/image_cache_manager.js +++ b/app/utils/image_cache_manager.js @@ -6,6 +6,9 @@ import RNFetchBlob from 'react-native-fetch-blob'; import {DeviceTypes} from 'app/constants'; +import mattermostBucket from 'app/mattermost_bucket'; + +import LocalConfig from 'assets/config'; const {IMAGES_PATH} = DeviceTypes; @@ -31,12 +34,14 @@ export default class ImageCacheManager { } try { + const certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId); const options = { session: uri, timeout: 10000, indicator: true, overwrite: true, path, + certificate, }; this.downloadTask = await RNFetchBlob.config(options).fetch('GET', uri); diff --git a/assets/base/config.json b/assets/base/config.json index dc5a9c46d..0939daa2f 100644 --- a/assets/base/config.json +++ b/assets/base/config.json @@ -12,6 +12,7 @@ "MobileNoticeURL": "https://about.mattermost.com/mobile-notice-txt/", "SegmentApiKey": "oPPT2MA24VYZTBGDyXtUDa7u50lkPIE4", "ExperimentalUsernamePressIsMention": false, + "ExperimentalClientSideCertEnable": false, "HideEmailLoginExperimental": false, "HideGitLabLoginExperimental": false, diff --git a/ios/0155-keys.png b/ios/0155-keys.png new file mode 100644 index 0000000000000000000000000000000000000000..94d62e31d220f3f22242b92072e9baa2428d3d6d GIT binary patch literal 2532 zcmV004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv00000008+zyMF)x010qNS#tmY3labT3lag+-G2N400~G*L_t(o!@Zb$a8%VD z$G_*C``W$9?&i%SBmoj2Neu`>KtYS5GY;C(g0|LHK$&T0Pz3Ems`TO1QS8`O3w5eE zT3Ycjc4})Ysc0cpI}QX4%9})zkSE#AV_)}q&gma6ZHh5OP5Rq8^P79l@B6!-{r&Fj zo_h#Ju;a&$=0qZq`Kqcdw{3flZQD7vZKrJ8Ii60X{@l~s`_9UhE20=jQ$|`OgyeJ1 z%LwtO3n9vd5IJ1%ln~tKoRDICN;0;QTpX_&i_)kEp6TU|E(A0+vi9 zgZ=&evj+P6vXjZ=W-jpPx;xjo|2M$8U3K^QzW1=AD0PK}MZcIkZ|?5%WA^NQ_mVUH z>e{ZBBi~lM?iUzUzPaJM-xFgE_=y(pe17NR_V)INbMtauzV7<#zxHXurC+|R0YJlV zAAf9rm!GwrCOdWjs2giQdj3R5NBeTycHlbRL!(KsbkZAdg(pty2Y1#E?!9rn?0In2 zxB@b!By?Sc84N~75+2(=Q$4k_{K<-i)Am@gvpJO&=5n*F_;Hc6p0*BcoHuTO<>lpn z2nNk=-Ozt@^2CW@pV5DOA4@0AEZJ9n?b_Ox>Z_BfjPr} zI4r=MyLVsXy6!6l1qC0@m{GAZ9Lec=`H#CQ)5+AW73pmgYA(*d!75rEe(Qkv=f3x& zW^LZ zKme$!u0Gk>)$>5KuRo&b#yusaCHI(U6KcRnq}q;ZM#n>k=grh!a*w=F7Clz~^Zb(H z63SG#whh;@!F@ju1V8ECbE`fsfB?|aaweFUo6mC!CnDAKDk~XyRGBFq6)%F*a}Zh}h|Z6iF%XMFWfa`8K(kClW4%c9MWLG}Tq_MHoqTZm zSN^hhTmVVtBPF2lI0~l}Lz}Z1U8mb&$3BAXQ4nxQ$>CZSK!8wLDf)W5;aF*a5O|JL z`+4CmTtNa5@^KjF9*ErsZOT=M7v6@mCl4Z^3#gg~E1ibt`xrRgf^0Jjc{69A`*brv z87N56xBHTmroy zAF+-$FipSy;P%@2;{qrpPJobn#7@Bv%mR}FAOdzg zg~6s~6kj|SJ^>Pau?b939{JbfRrzDzVvr9^)4bTWteeiXwf7srAOHg9cEi<*Au4|e ztL$F1Mea^@4;oJ}H?~#Pnbq6eEbV9?is#RVWu@Rb&Xr8j?i_Q#4I9>pMGF^n7Ehk^ z%GsW?TVv6f7>uVtgo7+434Qw4k&Y~LJ)`txF3J5&x*Nlh(A|uZv)zr22!_HasIG>c zPC?g=`wl!c|I#t8;D2p!aPZ-M`x;(}$K#Naz%;W_5LOUisYaEgH(h&AeX?eKr_Vj_ zc_SFyFC76Ke6-gI&ySyAhu01ithalP!$!0vuDHeTo{4iy~lQ5xp2$@SFgFv@_oO~ z^FDrg6OsTDAcTU_;kK1@Pj_qU+SbD#98NbkV{*-Gs73h@Lg-A@o;vp8Rgp0TeCC;F z1Ax3#f`AYT5CK96R8@tlesZ(2;#cXIlS)2I0zos;-hp&WtDHD@9y~^&YWhWrsy;BL zfRs={DSMky+D8bWQU=BtR84`VtHWvtCBdwm$lbj?-A}gctw(M+7vUM@aJdH~V6JQW z)AGe*2Ds+-RgUMnJ@JA5fT}PkstU>|G?hV9*|3_MxB1B>8;`x#-`n$^9t@Ji(c{Q3 zEd|Sq0F)X=Fu3(h-HN<10o=a*Swq+LZ%vyvJrD>4;ByXzQ7BA-!baRy!cHdt-gdg_ zr!5B$q{%=6`Bjy08G&X5W)rI1KPG^bP}g>x-?VphGy#CF>7XD`6a|W^j;Ot9tJf3j z=&aX5*=f7IOX^?1028`;j}j7>ISOZbjk%YXYBVob}E3ZrHld;poD@D3JD1* zM|WmaQH-eSo_+CjS|%gaSFm4XlgQc6%p zk)542svMVZK`U@*)jJzeF!knTVPfoPx{zknEw(JnEGa1~ipBcCJs*@3c%F|)C?s=p zaz>j50I>L%=p~EZydd0X0^GIhb+xcCe{Ic0)eusEbAd=WghV_Z+_GiMxiABOmKi!e z?hwm(iV+S>M`I+D%mk1NU;=KF0x)&q!bPi_nwsNMO4-uVE~S*RzP`Qzz*GPw z0OkOw22h#NO#@JxS(^l)2ta-&e*{1_fZ&iX#xfjaDAdquG610rqzNF95oc%C+bS!o zYL+ZnyufihB^1iC-+lL;hJy#+Kb5(W1i&-HAL3=k`$N`;i}L|^86J5);JIr$#8Uuj zW&{8T0KJqkSyfdmTQBIAe7otqaKAURAdhIkAIcy4a!P%`7(Ay5Lq u&;#=zfbcLkJ|7T60Ea>i{f(06!~7duS05>5sFckB0000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 962280f1c..56768b5f5 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -46,7 +46,6 @@ 7F292AA71E8ABB1100A450A3 /* splash.png in Resources */ = {isa = PBXBuildFile; fileRef = 7F292AA51E8ABB1100A450A3 /* splash.png */; }; 7F380D0B1FDB3CFC0061AAD2 /* libRCTVideo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F93F9D91FBB726B0088E416 /* libRCTVideo.a */; }; 7F3F66021FE426EE0085CA0E /* libRNSVG.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDF28E11E1F4B1F00DBBE56 /* libRNSVG.a */; }; - 7F3F660F1FE4280D0085CA0E /* libReactNativeExceptionHandler.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FA7950B1F61A1A500C02924 /* libReactNativeExceptionHandler.a */; }; 7F3F66101FE4281A0085CA0E /* libRNSentry.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FA795061F61A1A500C02924 /* libRNSentry.a */; }; 7F43D5D61F6BF8C2001FC614 /* libRNLocalAuth.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F8C49871F3DFC30003A22BA /* libRNLocalAuth.a */; }; 7F43D5D71F6BF8D0001FC614 /* libRNPasscodeStatus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F8C49F81F3E0710003A22BA /* libRNPasscodeStatus.a */; }; @@ -69,6 +68,7 @@ 7F6877B31E7836070094B63F /* libToolTipMenu.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F6877B01E7835E50094B63F /* libToolTipMenu.a */; }; 7F6C47A51FE87E8C00F5A912 /* PerformRequests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F6C47A41FE87E8C00F5A912 /* PerformRequests.m */; }; 7F7D7F98201645E100D31155 /* libReactNativePermissions.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F7D7F87201645D300D31155 /* libReactNativePermissions.a */; }; + 7F95132F208646CE00616E3E /* 0155-keys.png in Resources */ = {isa = PBXBuildFile; fileRef = 7F9512EA208646CD00616E3E /* 0155-keys.png */; }; 7FB6006B1FE3116800DB284F /* libRNFetchBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 375218411F4B9E320035444B /* libRNFetchBlob.a */; }; 7FBB5E9B1E1F5A4B000DE18A /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDF290C1E1F4B4E00DBBE56 /* libRNVectorIcons.a */; }; 7FC200E81EBB65370099331B /* libReactNativeNavigation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FC200DF1EBB65100099331B /* libReactNativeNavigation.a */; }; @@ -79,11 +79,13 @@ 7FEB10981F6101710039A015 /* BlurAppScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB10971F6101710039A015 /* BlurAppScreen.m */; }; 7FEB109D1F61019C0039A015 /* MattermostManaged.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109A1F61019C0039A015 /* MattermostManaged.m */; }; 7FEB109E1F61019C0039A015 /* UIImage+ImageEffects.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109C1F61019C0039A015 /* UIImage+ImageEffects.m */; }; + 7FF2AF8B2086483E00FFBDF4 /* KeyShareConsumer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7FF2AF8A2086483E00FFBDF4 /* KeyShareConsumer.storyboard */; }; 7FF7BE2C1FDEE4AE005E55FE /* MattermostManaged.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109A1F61019C0039A015 /* MattermostManaged.m */; }; 7FF7BE6D1FDEE5E8005E55FE /* MattermostBucket.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FF7BE6C1FDEE5E8005E55FE /* MattermostBucket.m */; }; 7FF7BE6E1FDEE5E8005E55FE /* MattermostBucket.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FF7BE6C1FDEE5E8005E55FE /* MattermostBucket.m */; }; 7FF7BE6F1FDF3CE4005E55FE /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDF290C1E1F4B4E00DBBE56 /* libRNVectorIcons.a */; }; 7FF7BE701FDF3EE7005E55FE /* Ionicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 2DCFD31D3F4A4154822AB532 /* Ionicons.ttf */; }; + 7FF7BE711FE004A3005E55FE /* libRNDeviceInfo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 37DD11281E79EBE1004111BA /* libRNDeviceInfo.a */; }; 7FFDB3191FE3566C009E3BCF /* FontAwesome.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 005346E5C0E542BFABAE1411 /* FontAwesome.ttf */; }; 7FFE329E1FD9CB650038C7A0 /* ShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FFE329D1FD9CB650038C7A0 /* ShareViewController.m */; }; 7FFE32A11FD9CB650038C7A0 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7FFE329F1FD9CB650038C7A0 /* MainInterface.storyboard */; }; @@ -720,6 +722,7 @@ 7F6C47A31FE87E8C00F5A912 /* PerformRequests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PerformRequests.h; sourceTree = ""; }; 7F6C47A41FE87E8C00F5A912 /* PerformRequests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PerformRequests.m; sourceTree = ""; }; 7F7D7F52201645D300D31155 /* ReactNativePermissions.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactNativePermissions.xcodeproj; path = "../node_modules/react-native-permissions/ios/ReactNativePermissions.xcodeproj"; sourceTree = ""; }; + 7F9512EA208646CD00616E3E /* 0155-keys.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "0155-keys.png"; sourceTree = ""; }; 7FC200BC1EBB65100099331B /* ReactNativeNavigation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactNativeNavigation.xcodeproj; path = "../node_modules/react-native-navigation/ios/ReactNativeNavigation.xcodeproj"; sourceTree = ""; }; 7FD8DE972029ECDE001AAC5E /* RNTableView.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNTableView.xcodeproj; path = "../node_modules/react-native-tableview/RNTableView.xcodeproj"; sourceTree = ""; }; 7FDB92751F706F45006CDFD1 /* RNImagePicker.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNImagePicker.xcodeproj; path = "../node_modules/react-native-image-picker/ios/RNImagePicker.xcodeproj"; sourceTree = ""; }; @@ -729,6 +732,7 @@ 7FEB109A1F61019C0039A015 /* MattermostManaged.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MattermostManaged.m; path = Mattermost/MattermostManaged.m; sourceTree = ""; }; 7FEB109B1F61019C0039A015 /* UIImage+ImageEffects.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIImage+ImageEffects.h"; path = "Mattermost/UIImage+ImageEffects.h"; sourceTree = ""; }; 7FEB109C1F61019C0039A015 /* UIImage+ImageEffects.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIImage+ImageEffects.m"; path = "Mattermost/UIImage+ImageEffects.m"; sourceTree = ""; }; + 7FF2AF8A2086483E00FFBDF4 /* KeyShareConsumer.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = KeyShareConsumer.storyboard; sourceTree = ""; }; 7FF7BE6B1FDEE5E8005E55FE /* MattermostBucket.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MattermostBucket.h; sourceTree = ""; }; 7FF7BE6C1FDEE5E8005E55FE /* MattermostBucket.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MattermostBucket.m; sourceTree = ""; }; 7FFE329A1FD9CB640038C7A0 /* MattermostShare.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MattermostShare.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -832,6 +836,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 7FFE32EA1FD9CD050038C7A0 /* libReact.a in Frameworks */, 7FFE32E71FD9CCD00038C7A0 /* libART.a in Frameworks */, 7F3F66101FE4281A0085CA0E /* libRNSentry.a in Frameworks */, 7F642DF1209353A400F3165E /* libRNDeviceInfo.a in Frameworks */, @@ -846,7 +851,6 @@ 7FFE32ED1FD9CD450038C7A0 /* libRCTNetwork.a in Frameworks */, 7FFE32EC1FD9CD360038C7A0 /* libRCTText.a in Frameworks */, 7FFE32EB1FD9CD170038C7A0 /* libRCTImage.a in Frameworks */, - 7FFE32EA1FD9CD050038C7A0 /* libReact.a in Frameworks */, 7FFE32E91FD9CCF40038C7A0 /* libRCTAnimation.a in Frameworks */, 7FFE32E81FD9CCDE0038C7A0 /* libRCTCameraRoll.a in Frameworks */, 7FD8DEDE2029EDB7001AAC5E /* libRNTableView.a in Frameworks */, @@ -973,15 +977,17 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, 7F292AA41E8ABB1100A450A3 /* LaunchScreen.xib */, + 7FF2AF8A2086483E00FFBDF4 /* KeyShareConsumer.storyboard */, 13B07FB71A68108700A75B9A /* main.m */, 7F63D2C21E6DD98A001FAE12 /* Mattermost.entitlements */, 7FF7BE6B1FDEE5E8005E55FE /* MattermostBucket.h */, 7FF7BE6C1FDEE5E8005E55FE /* MattermostBucket.m */, 7FEB10991F61019C0039A015 /* MattermostManaged.h */, 7FEB109A1F61019C0039A015 /* MattermostManaged.m */, - 7F292AA51E8ABB1100A450A3 /* splash.png */, 7FEB109B1F61019C0039A015 /* UIImage+ImageEffects.h */, 7FEB109C1F61019C0039A015 /* UIImage+ImageEffects.m */, + 7F9512EA208646CD00616E3E /* 0155-keys.png */, + 7F292AA51E8ABB1100A450A3 /* splash.png */, ); name = Mattermost; sourceTree = ""; @@ -1328,6 +1334,7 @@ 7F5CA956208FE38F004F91CE /* RNDocumentPicker.xcodeproj */, 7F642DE72093530B00F3165E /* RNDeviceInfo.xcodeproj */, 74D116A4208A8D3000CF8A79 /* RNKeychain.xcodeproj */, + 546CB0A5BB8F453890CBA559 /* RNFetchBlob.xcodeproj */, 7FD8DE972029ECDE001AAC5E /* RNTableView.xcodeproj */, 7F7D7F52201645D300D31155 /* ReactNativePermissions.xcodeproj */, 7FDB92751F706F45006CDFD1 /* RNImagePicker.xcodeproj */, @@ -1355,7 +1362,6 @@ EBA6063A99C141098D40C67A /* RNPasscodeStatus.xcodeproj */, 27A6EA89298440439DA9F98D /* JailMonkey.xcodeproj */, 50ECB1D221E44F51B5690DF2 /* FastImage.xcodeproj */, - 546CB0A5BB8F453890CBA559 /* RNFetchBlob.xcodeproj */, 3B1E210BF3B649BBBF427977 /* ReactNativeExceptionHandler.xcodeproj */, 9552041247E749308CE7B412 /* RNSentry.xcodeproj */, 62A58E5E984E4CF1811620B8 /* RNCookieManagerIOS.xcodeproj */, @@ -1494,6 +1500,9 @@ com.apple.BackgroundModes = { enabled = 1; }; + com.apple.Keychain = { + enabled = 1; + }; com.apple.Push = { enabled = 1; }; @@ -1511,6 +1520,12 @@ com.apple.ApplicationGroups.iOS = { enabled = 1; }; + com.apple.Keychain = { + enabled = 1; + }; + com.apple.iCloud = { + enabled = 1; + }; }; }; }; @@ -2204,6 +2219,7 @@ F006936FC2884C24A1321FC0 /* MaterialIcons.ttf in Resources */, 7F292AA71E8ABB1100A450A3 /* splash.png in Resources */, 420A7328E12C4B72AEF420CE /* Octicons.ttf in Resources */, + 7FF2AF8B2086483E00FFBDF4 /* KeyShareConsumer.storyboard in Resources */, 7F292AA61E8ABB1100A450A3 /* LaunchScreen.xib in Resources */, 7F292A711E8AB73400A450A3 /* SplashScreenResource in Resources */, 2B4C9B708010475DA575B81D /* SimpleLineIcons.ttf in Resources */, @@ -2212,6 +2228,7 @@ 55C6561DDBBA45929D88B6D1 /* OpenSans-BoldItalic.ttf in Resources */, D719A67137964F08BE47A5FC /* OpenSans-ExtraBold.ttf in Resources */, 0111A42B7F264BCF8CBDE3ED /* OpenSans-ExtraBoldItalic.ttf in Resources */, + 7F95132F208646CE00616E3E /* 0155-keys.png in Resources */, 0C0D24F53F254F75869E5951 /* OpenSans-Italic.ttf in Resources */, 2D5296A8926B4D7FBAF2D6E2 /* OpenSans-Light.ttf in Resources */, F083DB472349411A8E6E7AAD /* OpenSans-LightItalic.ttf in Resources */, @@ -2637,16 +2654,15 @@ "$(SRCROOT)/../node_modules/react-native-passcode-status/ios", "$(SRCROOT)/../node_modules/jail-monkey/JailMonkey", "$(SRCROOT)/../node_modules/react-native/Libraries/**", + "$(SRCROOT)/../node_modules/react-native-navigation/ios/**", + "$(SRCROOT)/../node_modules/react-native-local-auth/**", + "$(SRCROOT)/../node_modules/react-native-passcode-status/ios/**", + "$(SRCROOT)/../node_modules/jail-monkey/JailMonkey/**", "$(SRCROOT)/../node_modules/react-native-fast-image/ios/FastImage/**", "$(SRCROOT)/../node_modules/react-native-fetch-blob/ios/**", - "$(SRCROOT)/../node_modules/react-native-exception-handler/ios", "$(SRCROOT)/../node_modules/react-native-sentry/ios/**", - "$(SRCROOT)/../node_modules/react-native-cookies/ios/RNCookieManagerIOS", - "$(SRCROOT)/../node_modules/react-native-image-picker/ios", - "$(SRCROOT)/../node_modules/react-native-youtube/**", - "$(SRCROOT)/../node_modules/react-native-video/ios", - "$(SRCROOT)/../node_modules/react-native-doc-viewer/ios/**", - "$(SRCROOT)/../node_modules/react-native-safe-area/ios/RNSafeArea", + "$(SRCROOT)/../node_modules/react-native-cookies/ios/RNCookieManagerIOS/**", + "$(SRCROOT)/../node_modules/react-native-video/ios/**", "$(SRCROOT)/../node_modules/react-native-tableview/**", "$(SRCROOT)/../node_modules/react-native-device-info/ios/**", ); @@ -2697,16 +2713,15 @@ "$(SRCROOT)/../node_modules/react-native-passcode-status/ios", "$(SRCROOT)/../node_modules/jail-monkey/JailMonkey", "$(SRCROOT)/../node_modules/react-native/Libraries/**", + "$(SRCROOT)/../node_modules/react-native-navigation/ios/**", + "$(SRCROOT)/../node_modules/react-native-local-auth/**", + "$(SRCROOT)/../node_modules/react-native-passcode-status/ios/**", + "$(SRCROOT)/../node_modules/jail-monkey/JailMonkey/**", "$(SRCROOT)/../node_modules/react-native-fast-image/ios/FastImage/**", "$(SRCROOT)/../node_modules/react-native-fetch-blob/ios/**", - "$(SRCROOT)/../node_modules/react-native-exception-handler/ios", "$(SRCROOT)/../node_modules/react-native-sentry/ios/**", - "$(SRCROOT)/../node_modules/react-native-cookies/ios/RNCookieManagerIOS", - "$(SRCROOT)/../node_modules/react-native-image-picker/ios", - "$(SRCROOT)/../node_modules/react-native-youtube/**", - "$(SRCROOT)/../node_modules/react-native-video/ios", - "$(SRCROOT)/../node_modules/react-native-doc-viewer/ios/**", - "$(SRCROOT)/../node_modules/react-native-safe-area/ios/RNSafeArea", + "$(SRCROOT)/../node_modules/react-native-cookies/ios/RNCookieManagerIOS/**", + "$(SRCROOT)/../node_modules/react-native-video/ios/**", "$(SRCROOT)/../node_modules/react-native-tableview/**", "$(SRCROOT)/../node_modules/react-native-device-info/ios/**", ); diff --git a/ios/Mattermost.xcodeproj/xcshareddata/xcschemes/Mattermost.xcscheme b/ios/Mattermost.xcodeproj/xcshareddata/xcschemes/Mattermost.xcscheme index b3aebbad1..bc78e5103 100644 --- a/ios/Mattermost.xcodeproj/xcshareddata/xcschemes/Mattermost.xcscheme +++ b/ios/Mattermost.xcodeproj/xcshareddata/xcschemes/Mattermost.xcscheme @@ -54,7 +54,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" shouldUseLaunchSchemeArgsEnv = "YES"> group.com.mattermost.rnbeta + keychain-access-groups + + $(AppIdentifierPrefix)com.mattermost.rnbeta + diff --git a/ios/MattermostShare/MattermostShare.entitlements b/ios/MattermostShare/MattermostShare.entitlements index 55fdcdea6..c3808dbda 100644 --- a/ios/MattermostShare/MattermostShare.entitlements +++ b/ios/MattermostShare/MattermostShare.entitlements @@ -2,9 +2,27 @@ + com.apple.developer.icloud-container-identifiers + + iCloud.$(CFBundleIdentifier) + + com.apple.developer.icloud-services + + CloudDocuments + + com.apple.developer.ubiquity-container-identifiers + + iCloud.$(CFBundleIdentifier) + + com.apple.developer.ubiquity-kvstore-identifier + $(TeamIdentifierPrefix)$(CFBundleIdentifier) com.apple.security.application-groups group.com.mattermost.rnbeta + keychain-access-groups + + $(AppIdentifierPrefix)com.mattermost.rnbeta + diff --git a/ios/MattermostShare/SessionManager.h b/ios/MattermostShare/SessionManager.h index 53823aed1..bb40d0c50 100644 --- a/ios/MattermostShare/SessionManager.h +++ b/ios/MattermostShare/SessionManager.h @@ -1,14 +1,20 @@ #import #import "MattermostBucket.h" +#import "KeyChainDataSource.h" @interface SessionManager : NSObject @property (nonatomic, copy) void (^savedCompletionHandler)(void); @property MattermostBucket *bucket; +@property (nonatomic, retain) KeyChainDataSource *keyChain; +@property (nonatomic, strong) NSString *requestWithGroup; +@property (nonatomic, strong) NSString *certificateName; +@property (nonatomic) BOOL isBackground; +(instancetype)sharedSession; -(NSString *)getAppGroupIdFromRequestIdentifier:(NSString *) requestWithGroup; -(NSURLSession *)createSessionForRequestRequest:(NSString *)requestId; +-(void)setRequestWithGroup:(NSString *)requestWithGroup certificateName:(NSString *)certificateName; -(void)setDataForRequest:(NSDictionary *)data forRequestWithGroup:(NSString *)requestId; -(void)createPostForRequest:(NSString *)requestId; -(NSURL*)tempContainerURL:(NSString*)appGroupId; diff --git a/ios/MattermostShare/SessionManager.m b/ios/MattermostShare/SessionManager.m index ae0e82387..64f8ca91d 100644 --- a/ios/MattermostShare/SessionManager.m +++ b/ios/MattermostShare/SessionManager.m @@ -2,6 +2,9 @@ #import "MattermostBucket.h" @implementation SessionManager + +@synthesize keyChain; + + (instancetype)sharedSession { static id sharedMyManager = nil; static dispatch_once_t onceToken; @@ -15,46 +18,213 @@ self = [super init]; if (self) { self.bucket = [[MattermostBucket alloc] init]; + self.keyChain = [[KeyChainDataSource alloc] initWithMode:KSM_Identities]; } return self; } +-(void)setRequestWithGroup:(NSString *)requestWithGroup certificateName:(NSString *)certificateName { + self.requestWithGroup = requestWithGroup; + self.certificateName = certificateName; + self.isBackground = certificateName == nil; +} + +-(void)setDataForRequest:(NSDictionary *)data forRequestWithGroup:(NSString *)requestWithGroup { + NSString *appGroupId = [self getAppGroupIdFromRequestIdentifier:requestWithGroup]; + [[self.bucket bucketByName:appGroupId] setObject:data forKey:requestWithGroup]; +} + + -(NSString *)getAppGroupIdFromRequestIdentifier:(NSString *) requestWithGroup { return [requestWithGroup componentsSeparatedByString:@"|"][1]; } --(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { - if (self.savedCompletionHandler) { - self.savedCompletionHandler(); - self.savedCompletionHandler = nil; +-(NSDictionary *)getCredentialsForRequest:(NSString *)requestWithGroup { + NSString * appGroupId = [self getAppGroupIdFromRequestIdentifier:requestWithGroup]; + NSString *entitiesString = [self.bucket readFromFile:@"entities" appGroupId:appGroupId]; + NSData *entitiesData = [entitiesString dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error; + NSDictionary *entities = [NSJSONSerialization JSONObjectWithData:entitiesData options:NSJSONReadingMutableContainers error:&error]; + if (error != nil) { + return nil; + } + return [[entities objectForKey:@"general"] objectForKey:@"credentials"]; +} + +-(NSDictionary *)getDataForRequest:(NSString *)requestWithGroup { + NSString *appGroupId = [self getAppGroupIdFromRequestIdentifier:requestWithGroup]; + return [[self.bucket bucketByName:appGroupId] objectForKey:requestWithGroup]; +} + +-(NSURLSession *)createSessionForRequestRequest:(NSString *)requestWithGroup { + NSString *appGroupId = [self getAppGroupIdFromRequestIdentifier:requestWithGroup]; + NSURLSessionConfiguration* config; + if (self.isBackground) { + config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:requestWithGroup]; + } else { + config = [NSURLSessionConfiguration defaultSessionConfiguration]; + } + + config.sharedContainerIdentifier = appGroupId; + return [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; +} + +-(void) createPost:(NSDictionary *) post + withFiles:(NSArray *)files + credentials:(NSDictionary *) credentials + requestWithGroup:(NSString *)requestWithGroup { + NSString *serverUrl = [credentials objectForKey:@"url"]; + NSString *token = [credentials objectForKey:@"token"]; + NSString *channelId = [post objectForKey:@"channel_id"]; + NSURLSession *session = [self createSessionForRequestRequest:requestWithGroup]; + NSString *appGroupId = [self getAppGroupIdFromRequestIdentifier:requestWithGroup]; + + for (id file in files) { + NSURL *filePath = [NSURL fileURLWithPath:[file objectForKey:@"filePath"]]; + NSString *fileName = [file objectForKey:@"filename"]; + + NSError *err; + NSURL *tempContainerURL = [self tempContainerURL:appGroupId]; + NSURL *destinationURL = [tempContainerURL URLByAppendingPathComponent: fileName]; + BOOL bVal = [[NSFileManager defaultManager] copyItemAtURL:filePath toURL:destinationURL error:&err]; + + NSCharacterSet *allowedCharacters = [NSCharacterSet URLQueryAllowedCharacterSet]; + NSString *encodedFilename = [[file objectForKey:@"filename"] stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters]; + NSString *url = [serverUrl stringByAppendingString:@"/api/v4/files"]; + NSString *queryString = [NSString stringWithFormat:@"?channel_id=%@&filename=%@", channelId, encodedFilename]; + NSURL *filesUrl = [NSURL URLWithString:[url stringByAppendingString:queryString]]; + NSMutableURLRequest *uploadRequest = [NSMutableURLRequest requestWithURL:filesUrl cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:120.0]; + [uploadRequest setHTTPMethod:@"POST"]; + [uploadRequest setValue:[@"Bearer " stringByAppendingString:token] forHTTPHeaderField:@"Authorization"]; + [uploadRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"]; + + NSURLSessionUploadTask *task = [session uploadTaskWithRequest:uploadRequest fromFile:destinationURL]; + NSLog(@"Executing file request %@", fileName); + [task resume]; } } --(void)URLSession:(NSURLSession *)session task:(NSURLSessionDataTask *)task didCompleteWithError:(nullable NSError *)error { - NSString *requestWithGroup = [[session configuration] identifier]; +-(void) createPost:(NSMutableDictionary *)post + withFileIds:(NSArray *)fileIds + credentials:(NSDictionary *) credentials { + NSString *serverUrl = [credentials objectForKey:@"url"]; + NSString *token = [credentials objectForKey:@"token"]; - if(error != nil) { - NSLog(@"ERROR %@", [error userInfo]); - NSLog(@"invalidating session %@", requestWithGroup); - [session invalidateAndCancel]; - } else if (requestWithGroup != nil) { - NSString *appGroupId = [self getAppGroupIdFromRequestIdentifier:requestWithGroup]; - NSURL *requestUrl = [[task originalRequest] URL]; + if (fileIds != nil && [fileIds count] > 0) { + [post setObject:fileIds forKey:@"file_ids"]; + } + + NSError *error; + NSData *postData = [NSJSONSerialization dataWithJSONObject:post options:NSJSONWritingPrettyPrinted error:&error]; + + if (error == nil) { + NSString* postAsString = [[NSString alloc] initWithData:postData encoding:NSUTF8StringEncoding]; - NSDictionary *data = [self getDataForRequest:requestWithGroup]; - NSArray *files = [data objectForKey:@"files"]; - NSMutableArray *fileIds = [data objectForKey:@"file_ids"]; + NSURL *createUrl = [NSURL URLWithString:[serverUrl stringByAppendingString:@"/api/v4/posts"]]; - if ([[requestUrl absoluteString] containsString:@"files"] && - [files count] == [fileIds count]) { - [self sendPostRequestForId:requestWithGroup]; - [[self.bucket bucketByName:appGroupId] removeObjectForKey:requestWithGroup]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:createUrl cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:5.0]; + [request setHTTPMethod:@"POST"]; + [request setValue:[@"Bearer " stringByAppendingString:token] forHTTPHeaderField:@"Authorization"]; + [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; + [request setValue:@"application/json; charset=utf-8" forHTTPHeaderField:@"Content-Type"]; + [request setHTTPBody:[postAsString dataUsingEncoding:NSUTF8StringEncoding]]; + + NSURLSessionConfiguration* config = [NSURLSessionConfiguration ephemeralSessionConfiguration]; + NSURLSession *createSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; + NSURLSessionDataTask *createTask = [createSession dataTaskWithRequest:request]; + NSLog(@"Executing post request"); + [createTask resume]; + } +} + +-(void)createPostForRequest:(NSString *)requestWithGroup { + NSDictionary *data = [self getDataForRequest:requestWithGroup]; + NSDictionary *post = [data objectForKey:@"post"]; + NSArray *files = [data objectForKey:@"files"]; + NSDictionary *credentials = [self getCredentialsForRequest:requestWithGroup]; + + if (credentials == nil) { + return; + } + + if (files != nil && [files count] > 0) { + [self createPost:post withFiles:files credentials:credentials requestWithGroup:requestWithGroup]; + } + else { + [self createPost:[post mutableCopy] withFileIds:nil credentials:credentials]; + } +} + +-(void)sendPostRequestForId:(NSString *)requestWithGroup { + NSDictionary *data = [self getDataForRequest:requestWithGroup]; + NSDictionary *credentials = [self getCredentialsForRequest:requestWithGroup]; + + NSMutableDictionary *post = [[data objectForKey:@"post"] mutableCopy]; + NSArray *fileIds = [data objectForKey:@"file_ids"]; + + [self createPost:post withFileIds:fileIds credentials:credentials]; +} + +-(NSURL*)tempContainerURL:(NSString*)appGroupId { + NSFileManager *manager = [NSFileManager defaultManager]; + NSURL *containerURL = [manager containerURLForSecurityApplicationGroupIdentifier:appGroupId]; + NSURL *tempDirectoryURL = [containerURL URLByAppendingPathComponent:@"shareTempItems"]; + if (![manager fileExistsAtPath:[tempDirectoryURL path]]) { + NSError *err; + [manager createDirectoryAtURL:tempDirectoryURL withIntermediateDirectories:YES attributes:nil error:&err]; + if (err) { + return nil; } } + + return tempDirectoryURL; +} + + +-(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler { + NSLog(@"completition handler from normal challenge"); + if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate]) { + if (self.certificateName) { + SecIdentityRef identity = [keyChain GetIdentityByName:self.certificateName]; + if (identity != nil) { + SecCertificateRef certificate = NULL; + OSStatus status = SecIdentityCopyCertificate(identity, &certificate); + if (!status) { + CFArrayRef emailAddresses = NULL; + SecCertificateCopyEmailAddresses(certificate, &emailAddresses); + NSArray *emails = (NSArray *)CFBridgingRelease(emailAddresses); + CFStringRef summaryRef = SecCertificateCopySubjectSummary(certificate); + NSString *tagstr = (NSString*)CFBridgingRelease(summaryRef); + NSString *email = @""; + if ([emails count] > 0){ + email = [emails objectAtIndex:0]; + } + NSLog(@"completition %@ %@", tagstr, email); + const void *certs[] = {certificate}; + CFArrayRef certArray = CFArrayCreate(kCFAllocatorDefault, certs, 1, NULL); + NSURLCredential *credential = [NSURLCredential credentialWithIdentity:identity certificates:(__bridge NSArray*)certArray persistence:NSURLCredentialPersistencePermanent]; + [challenge.sender useCredential:credential forAuthenticationChallenge:challenge]; + completionHandler(NSURLSessionAuthChallengeUseCredential,credential); + NSLog(@"completition handler for certificate"); + return; + } + } + } + completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); + NSLog(@"completition handler that should not be reached"); + } else { + NSLog(@"completition handler regular stuff"); + completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); + } } -(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { - NSString *requestWithGroup = [[session configuration] identifier]; + NSString *requestWithGroup; + if (self.isBackground) { + requestWithGroup = [[session configuration] identifier]; + } else { + requestWithGroup = self.requestWithGroup; + } NSURL *requestUrl = [[dataTask originalRequest] URL]; if ([[requestUrl absoluteString] containsString:@"files"]) { @@ -84,135 +254,41 @@ } } --(void)setDataForRequest:(NSDictionary *)data forRequestWithGroup:(NSString *)requestWithGroup { - NSString *appGroupId = [self getAppGroupIdFromRequestIdentifier:requestWithGroup]; - [[self.bucket bucketByName:appGroupId] setObject:data forKey:requestWithGroup]; -} - --(NSDictionary *)getDataForRequest:(NSString *)requestWithGroup { - NSString *appGroupId = [self getAppGroupIdFromRequestIdentifier:requestWithGroup]; - return [[self.bucket bucketByName:appGroupId] objectForKey:requestWithGroup]; -} - --(NSURLSession *)createSessionForRequestRequest:(NSString *)requestWithGroup { - NSString *appGroupId = [self getAppGroupIdFromRequestIdentifier:requestWithGroup]; - NSURLSessionConfiguration* config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:requestWithGroup]; - config.sharedContainerIdentifier = appGroupId; - return [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; -} - --(NSDictionary *)getCredentialsForRequest:(NSString *)requestWithGroup { - NSString * appGroupId = [self getAppGroupIdFromRequestIdentifier:requestWithGroup]; - NSString *entitiesString = [self.bucket readFromFile:@"entities" appGroupId:appGroupId]; - NSData *entitiesData = [entitiesString dataUsingEncoding:NSUTF8StringEncoding]; - NSError *error; - NSDictionary *entities = [NSJSONSerialization JSONObjectWithData:entitiesData options:NSJSONReadingMutableContainers error:&error]; - if (error != nil) { - return nil; +-(void)URLSession:(NSURLSession *)session task:(NSURLSessionDataTask *)task didCompleteWithError:(nullable NSError *)error { + NSString *requestWithGroup; + if (self.isBackground) { + requestWithGroup = [[session configuration] identifier]; + } else { + requestWithGroup = self.requestWithGroup; } - return [[entities objectForKey:@"general"] objectForKey:@"credentials"]; -} --(void)createPostForRequest:(NSString *)requestWithGroup { - NSDictionary *data = [self getDataForRequest:requestWithGroup]; - NSDictionary *post = [data objectForKey:@"post"]; - NSArray *files = [data objectForKey:@"files"]; - - if (files != nil && [files count] > 0) { - NSString *channelId = [post objectForKey:@"channel_id"]; - NSDictionary *credentials = [self getCredentialsForRequest:requestWithGroup]; + if(error != nil) { + NSLog(@"completition ERROR %@", [error userInfo]); + NSLog(@"invalidating session %@", requestWithGroup); + [session invalidateAndCancel]; + } else if (requestWithGroup != nil) { NSString *appGroupId = [self getAppGroupIdFromRequestIdentifier:requestWithGroup]; + NSURL *requestUrl = [[task originalRequest] URL]; - if (credentials == nil) { - return; - } + NSDictionary *data = [self getDataForRequest:requestWithGroup]; + NSArray *files = [data objectForKey:@"files"]; + NSMutableArray *fileIds = [data objectForKey:@"file_ids"]; - NSString *serverUrl = [credentials objectForKey:@"url"]; - NSString *token = [credentials objectForKey:@"token"]; - NSURLSession *session = [self createSessionForRequestRequest:requestWithGroup]; - - for (id file in files) { - NSURL *filePath = [NSURL fileURLWithPath:[file objectForKey:@"filePath"]]; - NSString *fileName = [file objectForKey:@"filename"]; - - NSError *err; - NSURL *tempContainerURL = [self tempContainerURL:appGroupId]; - NSURL *destinationURL = [tempContainerURL URLByAppendingPathComponent: fileName]; - BOOL bVal = [[NSFileManager defaultManager] copyItemAtURL:filePath toURL:destinationURL error:&err]; - - NSCharacterSet *allowedCharacters = [NSCharacterSet URLQueryAllowedCharacterSet]; - NSString *encodedFilename = [[file objectForKey:@"filename"] stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters]; - NSString *url = [serverUrl stringByAppendingString:@"/api/v4/files"]; - NSString *queryString = [NSString stringWithFormat:@"?channel_id=%@&filename=%@", channelId, encodedFilename]; - NSURL *filesUrl = [NSURL URLWithString:[url stringByAppendingString:queryString]]; - NSMutableURLRequest *uploadRequest = [NSMutableURLRequest requestWithURL:filesUrl cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:120.0]; - [uploadRequest setHTTPMethod:@"POST"]; - [uploadRequest setValue:[@"Bearer " stringByAppendingString:token] forHTTPHeaderField:@"Authorization"]; - [uploadRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"]; - - NSURLSessionUploadTask *task = [session uploadTaskWithRequest:uploadRequest fromFile:destinationURL]; - NSLog(@"Executing file request %@", fileName); - [task resume]; + if ([[requestUrl absoluteString] containsString:@"files"] && + [files count] == [fileIds count]) { + [self sendPostRequestForId:requestWithGroup]; + [[self.bucket bucketByName:appGroupId] removeObjectForKey:requestWithGroup]; } } else { - [self sendPostRequestForId:requestWithGroup]; + NSLog(@"SOMETHING ELSE"); } } --(void)sendPostRequestForId:(NSString *)requestWithGroup { - NSDictionary *data = [self getDataForRequest:requestWithGroup]; - NSDictionary *credentials = [self getCredentialsForRequest:requestWithGroup]; - - if (credentials == nil) { - return; - } - - NSString *serverUrl = [credentials objectForKey:@"url"]; - NSString *token = [credentials objectForKey:@"token"]; - NSMutableDictionary *post = [[data objectForKey:@"post"] mutableCopy]; - NSArray *fileIds = [data objectForKey:@"file_ids"]; - - if (fileIds != nil && [fileIds count] > 0) { - [post setObject:fileIds forKey:@"file_ids"]; - } - - NSError *error; - NSData *postData = [NSJSONSerialization dataWithJSONObject:post options:NSJSONWritingPrettyPrinted error:&error]; - - if (error == nil) { - NSString* postAsString = [[NSString alloc] initWithData:postData encoding:NSUTF8StringEncoding]; - - NSURL *createUrl = [NSURL URLWithString:[serverUrl stringByAppendingString:@"/api/v4/posts"]]; - - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:createUrl cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:5.0]; - [request setHTTPMethod:@"POST"]; - [request setValue:[@"Bearer " stringByAppendingString:token] forHTTPHeaderField:@"Authorization"]; - [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; - [request setValue:@"application/json; charset=utf-8" forHTTPHeaderField:@"Content-Type"]; - [request setHTTPBody:[postAsString dataUsingEncoding:NSUTF8StringEncoding]]; - - NSURLSessionConfiguration* config = [NSURLSessionConfiguration ephemeralSessionConfiguration]; - NSURLSession *createSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; - NSURLSessionDataTask *createTask = [createSession dataTaskWithRequest:request]; - NSLog(@"Executing post request"); - [createTask resume]; +-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { + if (self.savedCompletionHandler) { + self.savedCompletionHandler(); + self.savedCompletionHandler = nil; } } --(NSURL*)tempContainerURL:(NSString*)appGroupId { - NSFileManager *manager = [NSFileManager defaultManager]; - NSURL *containerURL = [manager containerURLForSecurityApplicationGroupIdentifier:appGroupId]; - NSURL *tempDirectoryURL = [containerURL URLByAppendingPathComponent:@"shareTempItems"]; - if (![manager fileExistsAtPath:[tempDirectoryURL path]]) { - NSError *err; - [manager createDirectoryAtURL:tempDirectoryURL withIntermediateDirectories:YES attributes:nil error:&err]; - if (err) { - return nil; - } - } - - return tempDirectoryURL; -} - - @end diff --git a/ios/MattermostShare/ShareViewController.m b/ios/MattermostShare/ShareViewController.m index ff9797715..14f406dfc 100644 --- a/ios/MattermostShare/ShareViewController.m +++ b/ios/MattermostShare/ShareViewController.m @@ -52,10 +52,12 @@ RCT_EXPORT_METHOD(close:(NSDictionary *)data appGroupId:(NSString *)appGroupId) if (data != nil) { NSString *requestId = [data objectForKey:@"requestId"]; NSString *useBackgroundUpload = [data objectForKey:@"useBackgroundUpload"]; - BOOL isBackgroundUpload = useBackgroundUpload ? [useBackgroundUpload boolValue] : NO; + NSString *certificateName = [data objectForKey:@"certificate"]; + BOOL tryToUploadInTheBackgound = useBackgroundUpload ? [useBackgroundUpload boolValue] : NO; - if (isBackgroundUpload) { + if (tryToUploadInTheBackgound) { NSString *requestWithGroup = [NSString stringWithFormat:@"%@|%@", requestId, appGroupId]; + [[SessionManager sharedSession] setRequestWithGroup:requestWithGroup certificateName:certificateName]; [[SessionManager sharedSession] setDataForRequest:data forRequestWithGroup:requestWithGroup]; [[SessionManager sharedSession] createPostForRequest:requestWithGroup]; diff --git a/packager/modulePaths.js b/packager/modulePaths.js index 271d6a544..b8d55d28d 100644 --- a/packager/modulePaths.js +++ b/packager/modulePaths.js @@ -143,6 +143,7 @@ module.exports = ['./node_modules/react-native/Libraries/ART/ARTSerializablePath './node_modules/app/actions/views/thread.js', './node_modules/app/actions/views/typing.js', './node_modules/app/app.js', + './node_modules/app/fetch_preconfig.js', './node_modules/app/components/announcement_banner/announcement_banner.js', './node_modules/app/components/announcement_banner/index.js', './node_modules/app/components/at_mention/at_mention.js', diff --git a/share_extension/ios/extension_post.js b/share_extension/ios/extension_post.js index 79b98fcf7..b4083eb9c 100644 --- a/share_extension/ios/extension_post.js +++ b/share_extension/ios/extension_post.js @@ -569,6 +569,7 @@ export default class ExtensionPost extends PureComponent { if (currentUserId && authenticated) { await this.emmAuthenticationIfNeeded(); + const certificate = await mattermostBucket.getPreference('cert', Config.AppGroupId); try { // Check to see if the use still belongs to the channel @@ -584,6 +585,7 @@ export default class ExtensionPost extends PureComponent { post, requestId: generateId().replace(/-/g, ''), useBackgroundUpload: this.useBackgroundUpload, + certificate, }; this.setState({sending: true}); diff --git a/share_extension/ios/index.js b/share_extension/ios/index.js index c2776600d..1d653eb4e 100644 --- a/share_extension/ios/index.js +++ b/share_extension/ios/index.js @@ -8,6 +8,7 @@ import DeviceInfo from 'react-native-device-info'; import Config from 'assets/config'; import {getTranslations} from 'app/i18n'; +import 'app/fetch_preconfig'; import {captureExceptionWithoutState, initializeSentry, LOGGER_EXTENSION} from 'app/utils/sentry';