├── .gitignore ├── AzureFunction ├── function.json ├── project.json └── run.csx ├── LICENSE.txt ├── OneDriveDataRobot.sln ├── OneDriveDataRobot ├── App_Start │ ├── BundleConfig.cs │ ├── FilterConfig.cs │ ├── RouteConfig.cs │ ├── Startup.Auth.cs │ └── WebApiConfig.cs ├── AuthHelper.cs ├── Content │ ├── LoadingSpinner.css │ ├── Site.css │ ├── bootstrap-theme.css │ ├── bootstrap-theme.css.map │ ├── bootstrap-theme.min.css │ ├── bootstrap-theme.min.css.map │ ├── bootstrap.css │ ├── bootstrap.css.map │ ├── bootstrap.min.css │ └── bootstrap.min.css.map ├── Controllers │ ├── AccountController.cs │ ├── HomeController.cs │ └── SetupController.cs ├── Global.asax ├── Global.asax.cs ├── Models │ ├── DataRobotSetup.cs │ ├── HomeModel.cs │ └── UserInfo.cs ├── OneDriveDataRobot.csproj ├── Properties │ └── AssemblyInfo.cs ├── Scripts │ ├── _references.js │ ├── bootstrap.js │ ├── bootstrap.min.js │ ├── jquery-1.10.2.intellisense.js │ ├── jquery-1.10.2.js │ ├── jquery-1.10.2.min.js │ ├── jquery-1.10.2.min.map │ ├── jquery.validate-vsdoc.js │ ├── jquery.validate.js │ ├── jquery.validate.min.js │ ├── jquery.validate.unobtrusive.js │ ├── jquery.validate.unobtrusive.min.js │ ├── modernizr-2.6.2.js │ ├── respond.js │ └── respond.min.js ├── Startup.cs ├── Utils │ ├── ActionHelpers.cs │ ├── AzureStorage │ │ ├── AzureTableContext.cs │ │ └── AzureTableTokenCache.cs │ ├── HttpHelper.cs │ ├── SettingsHelper.cs │ └── StoredSubscriptionState.cs ├── Views │ ├── Account │ │ └── SignOutCallback.cshtml │ ├── Home │ │ ├── Error.cshtml │ │ └── Index.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ ├── _Layout.cshtml │ │ ├── _LayoutAsyncJob.cshtml │ │ ├── _LayoutFileHandler.cshtml │ │ └── _LoginPartial.cshtml │ ├── Web.config │ └── _ViewStart.cshtml ├── Web.Debug.config ├── Web.Release.config ├── Web.config ├── favicon.ico ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── images │ └── markdown.png └── packages.config └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | 24 | # Visual Studio 2015 cache/options directory 25 | .vs/ 26 | # Uncomment if you have tasks that create the project's static files in wwwroot 27 | #wwwroot/ 28 | 29 | # MSTest test Results 30 | [Tt]est[Rr]esult*/ 31 | [Bb]uild[Ll]og.* 32 | 33 | # NUNIT 34 | *.VisualState.xml 35 | TestResult.xml 36 | 37 | # Build Results of an ATL Project 38 | [Dd]ebugPS/ 39 | [Rr]eleasePS/ 40 | dlldata.c 41 | 42 | # DNX 43 | project.lock.json 44 | artifacts/ 45 | 46 | *_i.c 47 | *_p.c 48 | *_i.h 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.tmp_proj 63 | *.log 64 | *.vspscc 65 | *.vssscc 66 | .builds 67 | *.pidb 68 | *.svclog 69 | *.scc 70 | 71 | # Chutzpah Test files 72 | _Chutzpah* 73 | 74 | # Visual C++ cache files 75 | ipch/ 76 | *.aps 77 | *.ncb 78 | *.opendb 79 | *.opensdf 80 | *.sdf 81 | *.cachefile 82 | 83 | # Visual Studio profiler 84 | *.psess 85 | *.vsp 86 | *.vspx 87 | *.sap 88 | 89 | # TFS 2012 Local Workspace 90 | $tf/ 91 | 92 | # Guidance Automation Toolkit 93 | *.gpState 94 | 95 | # ReSharper is a .NET coding add-in 96 | _ReSharper*/ 97 | *.[Rr]e[Ss]harper 98 | *.DotSettings.user 99 | 100 | # JustCode is a .NET coding add-in 101 | .JustCode 102 | 103 | # TeamCity is a build add-in 104 | _TeamCity* 105 | 106 | # DotCover is a Code Coverage Tool 107 | *.dotCover 108 | 109 | # NCrunch 110 | _NCrunch_* 111 | .*crunch*.local.xml 112 | nCrunchTemp_* 113 | 114 | # MightyMoose 115 | *.mm.* 116 | AutoTest.Net/ 117 | 118 | # Web workbench (sass) 119 | .sass-cache/ 120 | 121 | # Installshield output folder 122 | [Ee]xpress/ 123 | 124 | # DocProject is a documentation generator add-in 125 | DocProject/buildhelp/ 126 | DocProject/Help/*.HxT 127 | DocProject/Help/*.HxC 128 | DocProject/Help/*.hhc 129 | DocProject/Help/*.hhk 130 | DocProject/Help/*.hhp 131 | DocProject/Help/Html2 132 | DocProject/Help/html 133 | 134 | # Click-Once directory 135 | publish/ 136 | 137 | # Publish Web Output 138 | *.[Pp]ublish.xml 139 | *.azurePubxml 140 | # TODO: Comment the next line if you want to checkin your web deploy settings 141 | # but database connection strings (with potential passwords) will be unencrypted 142 | *.pubxml 143 | *.publishproj 144 | 145 | # NuGet Packages 146 | *.nupkg 147 | # The packages folder can be ignored because of Package Restore 148 | **/packages/* 149 | # except build/, which is used as an MSBuild target. 150 | !**/packages/build/ 151 | # Uncomment if necessary however generally it will be regenerated when needed 152 | #!**/packages/repositories.config 153 | # NuGet v3's project.json files produces more ignoreable files 154 | *.nuget.props 155 | *.nuget.targets 156 | 157 | # Microsoft Azure Build Output 158 | csx/ 159 | *.build.csdef 160 | 161 | # Microsoft Azure Emulator 162 | ecf/ 163 | rcf/ 164 | 165 | # Microsoft Azure ApplicationInsights config file 166 | ApplicationInsights.config 167 | 168 | # Windows Store app package directory 169 | AppPackages/ 170 | BundleArtifacts/ 171 | 172 | # Visual Studio cache files 173 | # files ending in .cache can be ignored 174 | *.[Cc]ache 175 | # but keep track of directories ending in .cache 176 | !*.[Cc]ache/ 177 | 178 | # Others 179 | ClientBin/ 180 | ~$* 181 | *~ 182 | *.dbmdl 183 | *.dbproj.schemaview 184 | *.pfx 185 | *.publishsettings 186 | node_modules/ 187 | orleans.codegen.cs 188 | 189 | # RIA/Silverlight projects 190 | Generated_Code/ 191 | 192 | # Backup & report files from converting an old project file 193 | # to a newer Visual Studio version. Backup files are not needed, 194 | # because we have git ;-) 195 | _UpgradeReport_Files/ 196 | Backup*/ 197 | UpgradeLog*.XML 198 | UpgradeLog*.htm 199 | 200 | # SQL Server files 201 | *.mdf 202 | *.ldf 203 | 204 | # Business Intelligence projects 205 | *.rdl.data 206 | *.bim.layout 207 | *.bim_*.settings 208 | 209 | # Microsoft Fakes 210 | FakesAssemblies/ 211 | 212 | # GhostDoc plugin setting file 213 | *.GhostDoc.xml 214 | 215 | # Node.js Tools for Visual Studio 216 | .ntvs_analysis.dat 217 | 218 | # Visual Studio 6 build log 219 | *.plg 220 | 221 | # Visual Studio 6 workspace options file 222 | *.opt 223 | 224 | # Visual Studio LightSwitch build output 225 | **/*.HTMLClient/GeneratedArtifacts 226 | **/*.DesktopClient/GeneratedArtifacts 227 | **/*.DesktopClient/ModelManifest.xml 228 | **/*.Server/GeneratedArtifacts 229 | **/*.Server/ModelManifest.xml 230 | _Pvt_Extensions 231 | 232 | # Paket dependency manager 233 | .paket/paket.exe 234 | 235 | # FAKE - F# Make 236 | .fake/ 237 | -------------------------------------------------------------------------------- /AzureFunction/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "name": "req", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "methods": [ 9 | "post" 10 | ] 11 | }, 12 | { 13 | "name": "res", 14 | "type": "http", 15 | "direction": "out" 16 | }, 17 | { 18 | "type": "table", 19 | "name": "syncStateTable", 20 | "tableName": "syncState", 21 | "take": 50, 22 | "connection": "onedrivemagicstorage_STORAGE", 23 | "direction": "in" 24 | }, 25 | { 26 | "type": "table", 27 | "name": "tokenCacheTable", 28 | "tableName": "tokenCache", 29 | "take": 50, 30 | "connection": "onedrivemagicstorage_STORAGE", 31 | "direction": "in" 32 | } 33 | ], 34 | "disabled": false 35 | } -------------------------------------------------------------------------------- /AzureFunction/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "frameworks": { 3 | "net46": { 4 | "dependencies": { 5 | "WindowsAzure.Storage": "7.0.0", 6 | "Microsoft.IdentityModel.Clients.ActiveDirectory": "3.14.2", 7 | "Microsoft.Graph": "1.2.1" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /AzureFunction/run.csx: -------------------------------------------------------------------------------- 1 | #r "Newtonsoft.Json" 2 | 3 | using System; 4 | using System.Net; 5 | using Newtonsoft.Json; 6 | using Microsoft.IdentityModel.Clients.ActiveDirectory; 7 | using Microsoft.WindowsAzure.Storage; 8 | using Microsoft.WindowsAzure.Storage.Table; 9 | using Microsoft.Graph; 10 | 11 | private const string TriggerWord = "!odbot"; 12 | private const string idaClientId = "[ClientId]"; 13 | private const string idaClientSecret = "[ClientSecret]"; 14 | private const string idaAuthorityUrl = "https://login.microsoftonline.com/common"; 15 | private const string idaMicrosoftGraphUrl = "https://graph.microsoft.com"; 16 | 17 | // Main entry point for our Azure Function. Listens for webhooks from OneDrive and responds to the webhook with a 204 No Content. 18 | public static async Task Run(HttpRequestMessage req, CloudTable syncStateTable, CloudTable tokenCacheTable, TraceWriter log) 19 | { 20 | log.Info($"Webhook was triggered!"); 21 | 22 | // Handle validation scenario for creating a new webhook subscription 23 | Dictionary qs = req.GetQueryNameValuePairs() 24 | .ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase); 25 | if (qs.ContainsKey("validationToken")) 26 | { 27 | var token = qs["validationToken"]; 28 | log.Info($"Responding to validationToken: {token}"); 29 | return PlainTextResponse(token); 30 | } 31 | 32 | // If not the validation scenario, read the body of the request and parse the notification 33 | string jsonContent = await req.Content.ReadAsStringAsync(); 34 | log.Verbose($"Raw request content: {jsonContent}"); 35 | 36 | // Since webhooks can be batched together, loop over all the notifications we receive and process them individually. 37 | // In the real world, this shouldn't be done in the request handler, but rather queued to be done later. 38 | dynamic data = JsonConvert.DeserializeObject(jsonContent); 39 | if (data.value != null) 40 | { 41 | foreach (var subscription in data.value) 42 | { 43 | var clientState = subscription.clientState; 44 | var resource = subscription.resource; 45 | string subscriptionId = (string)subscription.subscriptionId; 46 | log.Info($"Notification for subscription: '{subscriptionId}' Resource: '{resource}', clientState: '{clientState}'"); 47 | 48 | // Process the individual subscription information 49 | bool exists = await ProcessSubscriptionNotificationAsync(subscriptionId, syncStateTable, tokenCacheTable, log); 50 | if (!exists) 51 | { 52 | return req.CreateResponse(HttpStatusCode.Gone); 53 | } 54 | } 55 | return req.CreateResponse(HttpStatusCode.NoContent); 56 | } 57 | 58 | log.Info($"Request was incorrect. Returning bad request."); 59 | return req.CreateResponse(HttpStatusCode.BadRequest); 60 | } 61 | 62 | // Do the work to retrieve deltas from this subscription and then find any changed Excel files 63 | private static async Task ProcessSubscriptionNotificationAsync(string subscriptionId, CloudTable syncStateTable, CloudTable tokenCacheTable, TraceWriter log) 64 | { 65 | // Retrieve our stored state from an Azure Table 66 | StoredSubscriptionState state = StoredSubscriptionState.Open(subscriptionId, syncStateTable); 67 | if (state == null) 68 | { 69 | log.Info($"Missing data for subscription '{subscriptionId}'."); 70 | return false; 71 | } 72 | 73 | log.Info($"Found subscription '{subscriptionId}' with lastDeltaUrl: '{state.LastDeltaToken}'."); 74 | 75 | GraphServiceClient client = new GraphServiceClient(new DelegateAuthenticationProvider(async (request) => { 76 | string accessToken = await RetrieveAccessTokenAsync(state.SignInUserId, tokenCacheTable, log); 77 | request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {accessToken}"); 78 | })); 79 | 80 | // Query for items that have changed since the last notification was received 81 | List changedExcelFileIds = await FindChangedExcelFilesInOneDrive(state, client, log); 82 | 83 | // Do work on the changed files 84 | foreach (var file in changedExcelFileIds) 85 | { 86 | log.Info($"Processing changes in file: {file}"); 87 | try 88 | { 89 | string sessionId = await StartOrResumeWorkbookSessionAsync(client, file, syncStateTable, log); 90 | log.Info($"File {file} is using sessionId: {sessionId}"); 91 | await ScanExcelFileForPlaceholdersAsync(client, file, sessionId, log); 92 | } 93 | catch (Exception ex) 94 | { 95 | log.Info($"Exception processing file: {ex.Message}"); 96 | } 97 | } 98 | 99 | // Update our saved state for this subscription 100 | state.Insert(syncStateTable); 101 | return true; 102 | } 103 | 104 | // Use the Excel REST API to look for queries that we can replace with real data 105 | private static async Task ScanExcelFileForPlaceholdersAsync(GraphServiceClient client, string fileId, string workbookSession, TraceWriter log) 106 | { 107 | const string SheetName = "Sheet1"; 108 | 109 | var dataRequest = client.Me.Drive.Items[fileId].Workbook.Worksheets[SheetName].UsedRange().Request(); 110 | if (null != workbookSession) 111 | { 112 | dataRequest.Headers.Add(new HeaderOption("workbook-session-id", workbookSession)); 113 | } 114 | var data = await dataRequest.Select("address,cellCount,columnCount,values").GetAsync(); 115 | 116 | var usedRangeId = data.Address; 117 | var sendPatch = false; 118 | dynamic range = data.Values; 119 | 120 | for (int rowIndex = 0; rowIndex < range.Count; rowIndex++) 121 | { 122 | var rowValues = range[rowIndex]; 123 | for (int columnIndex = 0; columnIndex < rowValues.Count; columnIndex++) 124 | { 125 | var value = (string)rowValues[columnIndex]; 126 | if (value.StartsWith($"{TriggerWord} ")) 127 | { 128 | log.Info($"Found cell [{rowIndex},{columnIndex}] with value: {value} "); 129 | rowValues[columnIndex] = await ReplacePlaceholderValueAsync(value); 130 | sendPatch = true; 131 | } 132 | else 133 | { 134 | // Replace the value with null so we don't overwrite anything on the PATCH 135 | rowValues[columnIndex] = null; 136 | } 137 | } 138 | } 139 | 140 | if (sendPatch) 141 | { 142 | log.Info($"Updating file {fileId} with replaced values."); 143 | await client.Me.Drive.Items[fileId].Workbook.Worksheets[SheetName].Range(data.Address).Request().PatchAsync(data); 144 | } 145 | } 146 | 147 | // Make a request to retrieve a response based on the input value 148 | private static async Task ReplacePlaceholderValueAsync(string inputValue) 149 | { 150 | // This is merely an example. A real solution would do something much richer 151 | if (inputValue.StartsWith($"{TriggerWord} ") && inputValue.EndsWith(" stock quote")) 152 | { 153 | // For demo purposes, return a random value instead of the stock quote 154 | Random rndNum = new Random(int.Parse(Guid.NewGuid().ToString().Substring(0, 8), System.Globalization.NumberStyles.HexNumber)); 155 | return rndNum.Next(20, 100).ToString(); 156 | } 157 | 158 | return inputValue; 159 | } 160 | 161 | // Request the delta stream from OneDrive to find files that have changed between notifications for this account 162 | // Request the delta stream from OneDrive to find files that have changed between notifications for this account 163 | private static async Task> FindChangedExcelFilesInOneDrive(StoredSubscriptionState state, GraphServiceClient client, TraceWriter log) 164 | { 165 | const string DefaultDeltaToken = idaMicrosoftGraphUrl + "/v1.0/me/drive/root/delta?token=latest"; 166 | 167 | // We default to reading the "latest" state of the drive, so we don't have to process all the files in the drive 168 | // when a new subscription comes in. 169 | string deltaUrl = DefaultDeltaToken; 170 | if (!String.IsNullOrEmpty(state.LastDeltaToken)) 171 | { 172 | deltaUrl = state.LastDeltaToken; 173 | } 174 | 175 | const int MaxLoopCount = 50; 176 | List changedFileIds = new List(); 177 | 178 | IDriveItemDeltaRequest request = new DriveItemDeltaRequest(deltaUrl, client, null); 179 | 180 | // Only allow reading 50 pages, if we read more than that, we're going to cancel out 181 | for (int loopCount = 0; loopCount < MaxLoopCount && request != null; loopCount++) 182 | { 183 | log.Info($"Making request for '{state.SubscriptionId}' to '{deltaUrl}' "); 184 | var deltaResponse = await request.GetAsync(); 185 | 186 | log.Verbose($"Found {deltaResponse.Count} files changed in this page."); 187 | try 188 | { 189 | var changedExcelFiles = (from f in deltaResponse 190 | where f.File != null && f.Name != null && f.Name.EndsWith(".xlsx") && f.Deleted == null 191 | select f.Id); 192 | log.Info($"Found {changedExcelFiles.Count()} changed Excel files in this page."); 193 | changedFileIds.AddRange(changedExcelFiles); 194 | } 195 | catch (Exception ex) 196 | { 197 | log.Info($"Exception enumerating changed files: {ex.ToString()}"); 198 | throw; 199 | } 200 | 201 | 202 | if (null != deltaResponse.NextPageRequest) 203 | { 204 | request = deltaResponse.NextPageRequest; 205 | } 206 | else if (null != deltaResponse.AdditionalData["@odata.deltaLink"]) 207 | { 208 | string deltaLink = (string)deltaResponse.AdditionalData["@odata.deltaLink"]; 209 | log.Verbose($"All changes requested, nextDeltaUrl: {deltaLink}"); 210 | state.LastDeltaToken = deltaLink; 211 | return changedFileIds; 212 | } 213 | else 214 | { 215 | request = null; 216 | } 217 | } 218 | 219 | // If we exit the For loop without returning, that means we read MaxLoopCount pages without finding a deltaToken 220 | log.Info($"Read through MaxLoopCount pages without finding an end. Too much data has changed."); 221 | state.LastDeltaToken = DefaultDeltaToken; 222 | 223 | return changedFileIds; 224 | 225 | } 226 | 227 | /// 228 | /// Ensure that we're working out of a shared session if multiple updates to a file are happening frequently. 229 | /// This improves performance and ensures consistency of the data between requests. 230 | /// 231 | private static async Task StartOrResumeWorkbookSessionAsync(GraphServiceClient client, string fileId, CloudTable table, TraceWriter log) 232 | { 233 | const string userId = "1234"; 234 | 235 | var fileItem = FileHistory.Open(userId, fileId, table); 236 | if (null == fileItem) 237 | { 238 | log.Info($"No existing Excel session found for file: {fileId}"); 239 | fileItem = FileHistory.CreateNew(userId, fileId); 240 | } 241 | 242 | if (!string.IsNullOrEmpty(fileItem.ExcelSessionId)) 243 | { 244 | // Verify session is still available 245 | TimeSpan lastUsed = DateTime.UtcNow.Subtract(fileItem.LastAccessedDateTime); 246 | if (lastUsed.TotalMinutes < 5) 247 | { 248 | fileItem.LastAccessedDateTime = DateTime.UtcNow; 249 | try 250 | { 251 | // Attempt to update the cache, but if we get a conflict, just ignore it 252 | fileItem.Insert(table); 253 | } 254 | catch { } 255 | log.Info($"Reusing existing session for file: {fileId}"); 256 | return fileItem.ExcelSessionId; 257 | } 258 | } 259 | 260 | string sessionId = null; 261 | try 262 | { 263 | // Create a new workbook session 264 | var session = await client.Me.Drive.Items[fileId].Workbook.CreateSession(true).Request().PostAsync(); 265 | fileItem.LastAccessedDateTime = DateTime.UtcNow; 266 | fileItem.ExcelSessionId = session.Id; 267 | log.Info($"Reusing existing session for file: {fileId}"); 268 | sessionId = session.Id; 269 | } 270 | catch { } 271 | 272 | try 273 | { 274 | fileItem.Insert(table); 275 | } 276 | catch { } 277 | 278 | return sessionId; 279 | } 280 | 281 | private static HttpResponseMessage PlainTextResponse(string text) 282 | { 283 | HttpResponseMessage response = new HttpResponseMessage() 284 | { 285 | StatusCode = HttpStatusCode.OK, 286 | Content = new StringContent( 287 | text, 288 | System.Text.Encoding.UTF8, 289 | "text/plain" 290 | ) 291 | }; 292 | return response; 293 | } 294 | 295 | // Retrieve a new access token from AAD 296 | private static async Task RetrieveAccessTokenAsync(string signInUserId, CloudTable tokenCacheTable, TraceWriter log) 297 | { 298 | log.Verbose($"Retrieving new accessToken for signInUser: {signInUserId}"); 299 | 300 | var tokenCache = new AzureTableTokenCache(signInUserId, tokenCacheTable); 301 | var authContext = new AuthenticationContext(idaAuthorityUrl, tokenCache); 302 | 303 | try 304 | { 305 | var userCredential = new UserIdentifier(signInUserId, UserIdentifierType.UniqueId); 306 | // Don't really store your clientId and clientSecret in your code. Read these from configuration. 307 | var clientCredential = new ClientCredential(idaClientId, idaClientSecret); 308 | var authResult = await authContext.AcquireTokenSilentAsync(idaMicrosoftGraphUrl, clientCredential, userCredential); 309 | return authResult.AccessToken; 310 | } 311 | catch (AdalSilentTokenAcquisitionException ex) 312 | { 313 | log.Info($"ADAL Error: Unable to retrieve access token: {ex.Message}"); 314 | return null; 315 | } 316 | } 317 | 318 | /*** SHARED CODE STARTS HERE ***/ 319 | 320 | /// 321 | /// Persists information about a subscription, userId, and deltaToken state. This class is shared between the Azure Function and the bootstrap project 322 | /// 323 | public class StoredSubscriptionState : TableEntity 324 | { 325 | public StoredSubscriptionState() 326 | { 327 | this.PartitionKey = "AAA"; 328 | } 329 | 330 | public string SignInUserId { get; set; } 331 | public string LastDeltaToken { get; set; } 332 | public string SubscriptionId { get; set; } 333 | public string ExcelSessionId { get; set; } 334 | 335 | 336 | public static StoredSubscriptionState CreateNew(string subscriptionId) 337 | { 338 | var newState = new StoredSubscriptionState(); 339 | newState.RowKey = subscriptionId; 340 | newState.SubscriptionId = subscriptionId; 341 | return newState; 342 | } 343 | 344 | public void Insert(CloudTable table) 345 | { 346 | TableOperation insert = TableOperation.InsertOrReplace(this); 347 | table.Execute(insert); 348 | } 349 | 350 | public static StoredSubscriptionState Open(string subscriptionId, CloudTable table) 351 | { 352 | TableOperation retrieve = TableOperation.Retrieve("AAA", subscriptionId); 353 | TableResult results = table.Execute(retrieve); 354 | return (StoredSubscriptionState)results.Result; 355 | } 356 | } 357 | 358 | /// 359 | /// Keep track of file specific information for a short period of time, so we can avoid repeatedly acting on the same file 360 | /// 361 | public class FileHistory : TableEntity 362 | { 363 | public FileHistory() 364 | { 365 | this.PartitionKey = "BBB"; 366 | } 367 | 368 | public string ExcelSessionId { get; set; } 369 | public DateTime LastAccessedDateTime { get; set; } 370 | 371 | public static FileHistory CreateNew(string userId, string fileId) 372 | { 373 | var newState = new FileHistory(); 374 | newState.RowKey = $"{userId},{fileId}"; 375 | return newState; 376 | } 377 | 378 | public void Insert(CloudTable table) 379 | { 380 | TableOperation insert = TableOperation.InsertOrReplace(this); 381 | table.Execute(insert); 382 | } 383 | 384 | public static FileHistory Open(string userId, string fileId, CloudTable table) 385 | { 386 | TableOperation retrieve = TableOperation.Retrieve("BBB", $"{userId},{fileId}"); 387 | TableResult results = table.Execute(retrieve); 388 | return (FileHistory)results.Result; 389 | } 390 | } 391 | 392 | /// 393 | /// ADAL TokenCache implementation that stores the token cache in the provided Azure CloudTable instance. 394 | /// This class is shared between the Azure Function and the bootstrap project. 395 | /// 396 | public class AzureTableTokenCache : TokenCache 397 | { 398 | private readonly string signInUserId; 399 | private readonly CloudTable tokenCacheTable; 400 | 401 | private TokenCacheEntity cachedEntity; // data entity stored in the Azure Table 402 | 403 | public AzureTableTokenCache(string userId, CloudTable cacheTable) 404 | { 405 | signInUserId = userId; 406 | tokenCacheTable = cacheTable; 407 | 408 | this.AfterAccess = AfterAccessNotification; 409 | 410 | cachedEntity = ReadFromTableStorage(); 411 | if (null != cachedEntity) 412 | { 413 | Deserialize(cachedEntity.CacheBits); 414 | } 415 | } 416 | 417 | private TokenCacheEntity ReadFromTableStorage() 418 | { 419 | TableOperation retrieve = TableOperation.Retrieve(TokenCacheEntity.PartitionKeyValue, signInUserId); 420 | TableResult results = tokenCacheTable.Execute(retrieve); 421 | return (TokenCacheEntity)results.Result; 422 | } 423 | 424 | private void AfterAccessNotification(TokenCacheNotificationArgs args) 425 | { 426 | if (this.HasStateChanged) 427 | { 428 | if (cachedEntity == null) 429 | { 430 | cachedEntity = new TokenCacheEntity(); 431 | } 432 | cachedEntity.RowKey = signInUserId; 433 | cachedEntity.CacheBits = Serialize(); 434 | cachedEntity.LastWrite = DateTime.Now; 435 | 436 | TableOperation insert = TableOperation.InsertOrReplace(cachedEntity); 437 | tokenCacheTable.Execute(insert); 438 | 439 | this.HasStateChanged = false; 440 | } 441 | } 442 | 443 | /// 444 | /// Representation of the data stored in the Azure Table 445 | /// 446 | private class TokenCacheEntity : TableEntity 447 | { 448 | public const string PartitionKeyValue = "tokenCache"; 449 | public TokenCacheEntity() 450 | { 451 | this.PartitionKey = PartitionKeyValue; 452 | } 453 | 454 | public byte[] CacheBits { get; set; } 455 | public DateTime LastWrite { get; set; } 456 | } 457 | 458 | } 459 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Microsoft Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /OneDriveDataRobot.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneDriveDataRobot", "OneDriveDataRobot\OneDriveDataRobot.csproj", "{79BBB307-673E-47DF-8E4B-00AA971CA65A}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{07085162-D421-4A92-A7C5-71E18E5AF934}" 9 | ProjectSection(SolutionItems) = preProject 10 | LICENSE.txt = LICENSE.txt 11 | README.md = README.md 12 | EndProjectSection 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AzureFunction Definition", "AzureFunction Definition", "{1DE5EE0D-E8F1-4EBE-ADB7-F93935791D5A}" 15 | ProjectSection(SolutionItems) = preProject 16 | AzureFunction\function.json = AzureFunction\function.json 17 | AzureFunction\project.json = AzureFunction\project.json 18 | AzureFunction\run.csx = AzureFunction\run.csx 19 | EndProjectSection 20 | EndProject 21 | Global 22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 23 | Debug|Any CPU = Debug|Any CPU 24 | Release|Any CPU = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {79BBB307-673E-47DF-8E4B-00AA971CA65A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {79BBB307-673E-47DF-8E4B-00AA971CA65A}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {79BBB307-673E-47DF-8E4B-00AA971CA65A}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {79BBB307-673E-47DF-8E4B-00AA971CA65A}.Release|Any CPU.Build.0 = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(SolutionProperties) = preSolution 33 | HideSolutionNode = FALSE 34 | EndGlobalSection 35 | GlobalSection(NestedProjects) = preSolution 36 | {1DE5EE0D-E8F1-4EBE-ADB7-F93935791D5A} = {07085162-D421-4A92-A7C5-71E18E5AF934} 37 | EndGlobalSection 38 | EndGlobal 39 | -------------------------------------------------------------------------------- /OneDriveDataRobot/App_Start/BundleConfig.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * OneDrive Data Robot - Sample Code 3 | * Copyright (c) Microsoft Corporation 4 | * All rights reserved. 5 | * 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | * this software and associated documentation files (the ""Software""), to deal in 10 | * the Software without restriction, including without limitation the rights to use, 11 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 12 | * Software, and to permit persons to whom the Software is furnished to do so, 13 | * subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | namespace OneDriveDataRobot 27 | { 28 | using System.Web.Optimization; 29 | 30 | public class BundleConfig 31 | { 32 | // For more information on bundling, visit http://go.microsoft.com/fwlink/?LinkId=301862 33 | public static void RegisterBundles(BundleCollection bundles) 34 | { 35 | bundles.Add(new ScriptBundle("~/bundles/jquery").Include( 36 | "~/Scripts/jquery-{version}.js")); 37 | 38 | bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include( 39 | "~/Scripts/jquery.validate*")); 40 | 41 | // Use the development version of Modernizr to develop with and learn from. Then, when you're 42 | // ready for production, use the build tool at http://modernizr.com to pick only the tests you need. 43 | bundles.Add(new ScriptBundle("~/bundles/modernizr").Include( 44 | "~/Scripts/modernizr-*")); 45 | 46 | bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include( 47 | "~/Scripts/bootstrap.js", 48 | "~/Scripts/respond.js")); 49 | 50 | bundles.Add(new StyleBundle("~/Content/css").Include( 51 | "~/Content/bootstrap.css", 52 | "~/Content/site.css")); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /OneDriveDataRobot/App_Start/FilterConfig.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * OneDrive Data Robot - Sample Code 3 | * Copyright (c) Microsoft Corporation 4 | * All rights reserved. 5 | * 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | * this software and associated documentation files (the ""Software""), to deal in 10 | * the Software without restriction, including without limitation the rights to use, 11 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 12 | * Software, and to permit persons to whom the Software is furnished to do so, 13 | * subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | namespace OneDriveDataRobot 27 | { 28 | using System.Web.Mvc; 29 | 30 | public class FilterConfig 31 | { 32 | public static void RegisterGlobalFilters(GlobalFilterCollection filters) 33 | { 34 | filters.Add(new HandleErrorAttribute()); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /OneDriveDataRobot/App_Start/RouteConfig.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * OneDrive Data Robot - Sample Code 3 | * Copyright (c) Microsoft Corporation 4 | * All rights reserved. 5 | * 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | * this software and associated documentation files (the ""Software""), to deal in 10 | * the Software without restriction, including without limitation the rights to use, 11 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 12 | * Software, and to permit persons to whom the Software is furnished to do so, 13 | * subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | namespace OneDriveDataRobot 27 | { 28 | using System.Web.Mvc; 29 | using System.Web.Routing; 30 | 31 | public class RouteConfig 32 | { 33 | public static void RegisterRoutes(RouteCollection routes) 34 | { 35 | routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 36 | 37 | routes.MapRoute( 38 | name: "Default", 39 | url: "{controller}/{action}/{id}", 40 | defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } 41 | ); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /OneDriveDataRobot/App_Start/Startup.Auth.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * OneDrive Data Robot - Sample Code 3 | * Copyright (c) Microsoft Corporation 4 | * All rights reserved. 5 | * 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | * this software and associated documentation files (the ""Software""), to deal in 10 | * the Software without restriction, including without limitation the rights to use, 11 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 12 | * Software, and to permit persons to whom the Software is furnished to do so, 13 | * subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | namespace OneDriveDataRobot 27 | { 28 | using System; 29 | using System.Threading.Tasks; 30 | using System.Web; 31 | using Microsoft.Owin.Security; 32 | using Microsoft.Owin.Security.Cookies; 33 | using Microsoft.Owin.Security.OpenIdConnect; 34 | using Owin; 35 | using Microsoft.IdentityModel.Clients.ActiveDirectory; 36 | using OneDriveDataRobot.Utils; 37 | using Models; 38 | using Controllers; 39 | 40 | public partial class Startup 41 | { 42 | 43 | public void ConfigureAuth(IAppBuilder app) 44 | { 45 | app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType); 46 | 47 | app.UseCookieAuthentication(new CookieAuthenticationOptions { }); 48 | 49 | app.UseOpenIdConnectAuthentication( 50 | new OpenIdConnectAuthenticationOptions 51 | { 52 | ClientId = SettingsHelper.ClientId, 53 | Authority = SettingsHelper.Authority, 54 | ClientSecret = SettingsHelper.AppKey, 55 | ResponseType = "code id_token", 56 | Resource = "https://graph.microsoft.com", 57 | PostLogoutRedirectUri = "/", 58 | TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters 59 | { 60 | // instead of using the default validation (validating against a single issuer value, as we do in line of business apps (single tenant apps)), 61 | // we turn off validation 62 | // 63 | // NOTE: 64 | // * In a multitenant scenario you can never validate against a fixed issuer string, as every tenant will send a different one. 65 | // * If you don’t care about validating tenants, as is the case for apps giving access to 1st party resources, you just turn off validation. 66 | // * If you do care about validating tenants, think of the case in which your app sells access to premium content and you want to limit access only to the tenant that paid a fee, 67 | // you still need to turn off the default validation but you do need to add logic that compares the incoming issuer to a list of tenants that paid you, 68 | // and block access if that’s not the case. 69 | // * Refer to the following sample for a custom validation logic: https://github.com/AzureADSamples/WebApp-WebAPI-MultiTenant-OpenIdConnect-DotNet 70 | ValidateIssuer = false 71 | }, 72 | Notifications = new OpenIdConnectAuthenticationNotifications() 73 | { 74 | SecurityTokenValidated = (context) => 75 | { 76 | // If your authentication logic is based on users then add your logic here 77 | return Task.FromResult(0); 78 | }, 79 | AuthenticationFailed = (context) => 80 | { 81 | // Pass in the context back to the app 82 | string message = Uri.EscapeDataString(context.Exception.Message); 83 | context.OwinContext.Response.Redirect("/Home/Error?msg=" + message); 84 | context.HandleResponse(); // Suppress the exception 85 | return Task.FromResult(0); 86 | }, 87 | AuthorizationCodeReceived = async (context) => 88 | { 89 | var code = context.Code; 90 | ClientCredential credential = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.AppKey); 91 | 92 | string tenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value; 93 | string signInUserId = context.AuthenticationTicket.Identity.FindFirst(AuthHelper.ObjectIdentifierClaim).Value; 94 | 95 | var authContext = new AuthenticationContext(SettingsHelper.Authority, 96 | new AzureTableTokenCache(signInUserId, AzureTableContext.Default.TokenCacheTable)); 97 | 98 | // Get the access token for AAD Graph. Doing this will also initialize the token cache associated with the authentication context 99 | // In theory, you could acquire token for any service your application has access to here so that you can initialize the token cache 100 | AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(code, 101 | new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), 102 | credential, 103 | SettingsHelper.MicrosoftGraphBaseUrl); 104 | }, 105 | RedirectToIdentityProvider = (context) => 106 | { 107 | // This ensures that the address used for sign in and sign out is picked up dynamically from the request 108 | // this allows you to deploy your app (to Azure Web Sites, for example)without having to change settings 109 | // Remember that the base URL of the address used here must be provisioned in Azure AD beforehand. 110 | string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase; 111 | context.ProtocolMessage.RedirectUri = appBaseUrl + "/"; 112 | context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl; 113 | 114 | // Allow us to change the prompt in consent mode if the challenge properties specify a prompt type 115 | var challengeProperties = context.OwinContext?.Authentication?.AuthenticationResponseChallenge?.Properties; 116 | if (null != challengeProperties && challengeProperties.Dictionary.ContainsKey("prompt")) 117 | { 118 | context.ProtocolMessage.Prompt = challengeProperties.Dictionary["prompt"]; 119 | } 120 | 121 | return Task.FromResult(0); 122 | } 123 | } 124 | }); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /OneDriveDataRobot/App_Start/WebApiConfig.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * OneDrive Data Robot - Sample Code 3 | * Copyright (c) Microsoft Corporation 4 | * All rights reserved. 5 | * 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | * this software and associated documentation files (the ""Software""), to deal in 10 | * the Software without restriction, including without limitation the rights to use, 11 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 12 | * Software, and to permit persons to whom the Software is furnished to do so, 13 | * subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | namespace OneDriveDataRobot 27 | { 28 | using System.Web.Http; 29 | 30 | public static class WebApiConfig 31 | { 32 | public static void Register(HttpConfiguration config) 33 | { 34 | config.MapHttpAttributeRoutes(); 35 | 36 | config.Routes.MapHttpRoute( 37 | name: "DefaultApi", 38 | routeTemplate: "api/{controller}/{action}/{id}", 39 | defaults: new { id = RouteParameter.Optional } 40 | ); 41 | 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /OneDriveDataRobot/AuthHelper.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Markdown File Handler - Sample Code 3 | * Copyright (c) Microsoft Corporation 4 | * All rights reserved. 5 | * 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | * this software and associated documentation files (the ""Software""), to deal in 10 | * the Software without restriction, including without limitation the rights to use, 11 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 12 | * Software, and to permit persons to whom the Software is furnished to do so, 13 | * subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | namespace OneDriveDataRobot 27 | { 28 | using System.Threading.Tasks; 29 | using Microsoft.IdentityModel.Clients.ActiveDirectory; 30 | using Utils; 31 | using Microsoft.WindowsAzure.Storage.Table; 32 | 33 | public static class AuthHelper 34 | { 35 | public const string ObjectIdentifierClaim = "http://schemas.microsoft.com/identity/claims/objectidentifier"; 36 | private static ClientCredential clientCredential = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.AppKey); 37 | private const string AuthContextCacheKey = "authContext"; 38 | 39 | /// 40 | /// Silently retrieve a new access token for the specified resource. If the request fails, null is returned. 41 | /// 42 | /// 43 | /// 44 | public static async Task GetUserAccessTokenSilentAsync(string resource, object cachedContext = null) 45 | { 46 | string signInUserId = GetUserId(); 47 | if (!string.IsNullOrEmpty(signInUserId)) 48 | { 49 | AuthenticationContext authContext = null; 50 | 51 | // Cache the authentication context in the session, so we don't have to reconstruct the cache for every call 52 | if (authContext == null) 53 | { 54 | authContext = new AuthenticationContext(SettingsHelper.Authority, false, new AzureTableTokenCache(signInUserId, AzureTableContext.Default.TokenCacheTable)); 55 | } 56 | 57 | try 58 | { 59 | var userCredential = new UserIdentifier(signInUserId, UserIdentifierType.UniqueId); 60 | var authResult = await authContext.AcquireTokenSilentAsync(resource, clientCredential, userCredential); 61 | return new AuthTokens { AccessToken = authResult.AccessToken, SignInUserId = signInUserId }; 62 | } 63 | catch (AdalSilentTokenAcquisitionException) 64 | { 65 | // We don't really care about why we couldn't get a token silently, since the resolution will always be the same 66 | } 67 | } 68 | return null; 69 | } 70 | 71 | public class AuthTokens 72 | { 73 | public string AccessToken { get; set; } 74 | public string SignInUserId { get; set; } 75 | } 76 | 77 | /// 78 | /// Return the signed in user's identifier 79 | /// 80 | /// 81 | public static string GetUserId() 82 | { 83 | var claim = System.Security.Claims.ClaimsPrincipal.Current.FindFirst(ObjectIdentifierClaim); 84 | if (null != claim) 85 | { 86 | return claim.Value; 87 | } 88 | return null; 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /OneDriveDataRobot/Content/LoadingSpinner.css: -------------------------------------------------------------------------------- 1 | .windows8 { 2 | position: relative; 3 | width: 40px; 4 | height:40px; 5 | margin:auto; 6 | } 7 | 8 | .windows8 .wBall { 9 | position: absolute; 10 | width: 38px; 11 | height: 38px; 12 | opacity: 0; 13 | transform: rotate(225deg); 14 | -o-transform: rotate(225deg); 15 | -ms-transform: rotate(225deg); 16 | -webkit-transform: rotate(225deg); 17 | -moz-transform: rotate(225deg); 18 | animation: orbit 2.4225s infinite; 19 | -o-animation: orbit 2.4225s infinite; 20 | -ms-animation: orbit 2.4225s infinite; 21 | -webkit-animation: orbit 2.4225s infinite; 22 | -moz-animation: orbit 2.4225s infinite; 23 | } 24 | 25 | .windows8 .wBall .wInnerBall{ 26 | position: absolute; 27 | width: 5px; 28 | height: 5px; 29 | background: rgb(0,0,0); 30 | left:0px; 31 | top:0px; 32 | border-radius: 5px; 33 | } 34 | 35 | .windows8 #wBall_1 { 36 | animation-delay: 0.526s; 37 | -o-animation-delay: 0.526s; 38 | -ms-animation-delay: 0.526s; 39 | -webkit-animation-delay: 0.526s; 40 | -moz-animation-delay: 0.526s; 41 | } 42 | 43 | .windows8 #wBall_2 { 44 | animation-delay: 0.103s; 45 | -o-animation-delay: 0.103s; 46 | -ms-animation-delay: 0.103s; 47 | -webkit-animation-delay: 0.103s; 48 | -moz-animation-delay: 0.103s; 49 | } 50 | 51 | .windows8 #wBall_3 { 52 | animation-delay: 0.2165s; 53 | -o-animation-delay: 0.2165s; 54 | -ms-animation-delay: 0.2165s; 55 | -webkit-animation-delay: 0.2165s; 56 | -moz-animation-delay: 0.2165s; 57 | } 58 | 59 | .windows8 #wBall_4 { 60 | animation-delay: 0.3195s; 61 | -o-animation-delay: 0.3195s; 62 | -ms-animation-delay: 0.3195s; 63 | -webkit-animation-delay: 0.3195s; 64 | -moz-animation-delay: 0.3195s; 65 | } 66 | 67 | .windows8 #wBall_5 { 68 | animation-delay: 0.423s; 69 | -o-animation-delay: 0.423s; 70 | -ms-animation-delay: 0.423s; 71 | -webkit-animation-delay: 0.423s; 72 | -moz-animation-delay: 0.423s; 73 | } 74 | 75 | 76 | 77 | @keyframes orbit { 78 | 0% { 79 | opacity: 1; 80 | z-index:99; 81 | transform: rotate(180deg); 82 | animation-timing-function: ease-out; 83 | } 84 | 85 | 7% { 86 | opacity: 1; 87 | transform: rotate(300deg); 88 | animation-timing-function: linear; 89 | origin:0%; 90 | } 91 | 92 | 30% { 93 | opacity: 1; 94 | transform:rotate(410deg); 95 | animation-timing-function: ease-in-out; 96 | origin:7%; 97 | } 98 | 99 | 39% { 100 | opacity: 1; 101 | transform: rotate(645deg); 102 | animation-timing-function: linear; 103 | origin:30%; 104 | } 105 | 106 | 70% { 107 | opacity: 1; 108 | transform: rotate(770deg); 109 | animation-timing-function: ease-out; 110 | origin:39%; 111 | } 112 | 113 | 75% { 114 | opacity: 1; 115 | transform: rotate(900deg); 116 | animation-timing-function: ease-out; 117 | origin:70%; 118 | } 119 | 120 | 76% { 121 | opacity: 0; 122 | transform:rotate(900deg); 123 | } 124 | 125 | 100% { 126 | opacity: 0; 127 | transform: rotate(900deg); 128 | } 129 | } 130 | 131 | @-o-keyframes orbit { 132 | 0% { 133 | opacity: 1; 134 | z-index:99; 135 | -o-transform: rotate(180deg); 136 | -o-animation-timing-function: ease-out; 137 | } 138 | 139 | 7% { 140 | opacity: 1; 141 | -o-transform: rotate(300deg); 142 | -o-animation-timing-function: linear; 143 | -o-origin:0%; 144 | } 145 | 146 | 30% { 147 | opacity: 1; 148 | -o-transform:rotate(410deg); 149 | -o-animation-timing-function: ease-in-out; 150 | -o-origin:7%; 151 | } 152 | 153 | 39% { 154 | opacity: 1; 155 | -o-transform: rotate(645deg); 156 | -o-animation-timing-function: linear; 157 | -o-origin:30%; 158 | } 159 | 160 | 70% { 161 | opacity: 1; 162 | -o-transform: rotate(770deg); 163 | -o-animation-timing-function: ease-out; 164 | -o-origin:39%; 165 | } 166 | 167 | 75% { 168 | opacity: 1; 169 | -o-transform: rotate(900deg); 170 | -o-animation-timing-function: ease-out; 171 | -o-origin:70%; 172 | } 173 | 174 | 76% { 175 | opacity: 0; 176 | -o-transform:rotate(900deg); 177 | } 178 | 179 | 100% { 180 | opacity: 0; 181 | -o-transform: rotate(900deg); 182 | } 183 | } 184 | 185 | @-ms-keyframes orbit { 186 | 0% { 187 | opacity: 1; 188 | z-index:99; 189 | -ms-transform: rotate(180deg); 190 | -ms-animation-timing-function: ease-out; 191 | } 192 | 193 | 7% { 194 | opacity: 1; 195 | -ms-transform: rotate(300deg); 196 | -ms-animation-timing-function: linear; 197 | -ms-origin:0%; 198 | } 199 | 200 | 30% { 201 | opacity: 1; 202 | -ms-transform:rotate(410deg); 203 | -ms-animation-timing-function: ease-in-out; 204 | -ms-origin:7%; 205 | } 206 | 207 | 39% { 208 | opacity: 1; 209 | -ms-transform: rotate(645deg); 210 | -ms-animation-timing-function: linear; 211 | -ms-origin:30%; 212 | } 213 | 214 | 70% { 215 | opacity: 1; 216 | -ms-transform: rotate(770deg); 217 | -ms-animation-timing-function: ease-out; 218 | -ms-origin:39%; 219 | } 220 | 221 | 75% { 222 | opacity: 1; 223 | -ms-transform: rotate(900deg); 224 | -ms-animation-timing-function: ease-out; 225 | -ms-origin:70%; 226 | } 227 | 228 | 76% { 229 | opacity: 0; 230 | -ms-transform:rotate(900deg); 231 | } 232 | 233 | 100% { 234 | opacity: 0; 235 | -ms-transform: rotate(900deg); 236 | } 237 | } 238 | 239 | @-webkit-keyframes orbit { 240 | 0% { 241 | opacity: 1; 242 | z-index:99; 243 | -webkit-transform: rotate(180deg); 244 | -webkit-animation-timing-function: ease-out; 245 | } 246 | 247 | 7% { 248 | opacity: 1; 249 | -webkit-transform: rotate(300deg); 250 | -webkit-animation-timing-function: linear; 251 | -webkit-origin:0%; 252 | } 253 | 254 | 30% { 255 | opacity: 1; 256 | -webkit-transform:rotate(410deg); 257 | -webkit-animation-timing-function: ease-in-out; 258 | -webkit-origin:7%; 259 | } 260 | 261 | 39% { 262 | opacity: 1; 263 | -webkit-transform: rotate(645deg); 264 | -webkit-animation-timing-function: linear; 265 | -webkit-origin:30%; 266 | } 267 | 268 | 70% { 269 | opacity: 1; 270 | -webkit-transform: rotate(770deg); 271 | -webkit-animation-timing-function: ease-out; 272 | -webkit-origin:39%; 273 | } 274 | 275 | 75% { 276 | opacity: 1; 277 | -webkit-transform: rotate(900deg); 278 | -webkit-animation-timing-function: ease-out; 279 | -webkit-origin:70%; 280 | } 281 | 282 | 76% { 283 | opacity: 0; 284 | -webkit-transform:rotate(900deg); 285 | } 286 | 287 | 100% { 288 | opacity: 0; 289 | -webkit-transform: rotate(900deg); 290 | } 291 | } 292 | 293 | @-moz-keyframes orbit { 294 | 0% { 295 | opacity: 1; 296 | z-index:99; 297 | -moz-transform: rotate(180deg); 298 | -moz-animation-timing-function: ease-out; 299 | } 300 | 301 | 7% { 302 | opacity: 1; 303 | -moz-transform: rotate(300deg); 304 | -moz-animation-timing-function: linear; 305 | -moz-origin:0%; 306 | } 307 | 308 | 30% { 309 | opacity: 1; 310 | -moz-transform:rotate(410deg); 311 | -moz-animation-timing-function: ease-in-out; 312 | -moz-origin:7%; 313 | } 314 | 315 | 39% { 316 | opacity: 1; 317 | -moz-transform: rotate(645deg); 318 | -moz-animation-timing-function: linear; 319 | -moz-origin:30%; 320 | } 321 | 322 | 70% { 323 | opacity: 1; 324 | -moz-transform: rotate(770deg); 325 | -moz-animation-timing-function: ease-out; 326 | -moz-origin:39%; 327 | } 328 | 329 | 75% { 330 | opacity: 1; 331 | -moz-transform: rotate(900deg); 332 | -moz-animation-timing-function: ease-out; 333 | -moz-origin:70%; 334 | } 335 | 336 | 76% { 337 | opacity: 0; 338 | -moz-transform:rotate(900deg); 339 | } 340 | 341 | 100% { 342 | opacity: 0; 343 | -moz-transform: rotate(900deg); 344 | } 345 | } -------------------------------------------------------------------------------- /OneDriveDataRobot/Content/Site.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | /* Set padding to keep content from hitting the edges */ 7 | .body-content { 8 | padding-left: 15px; 9 | padding-right: 15px; 10 | } 11 | 12 | /* Override the default bootstrap behavior where horizontal description lists 13 | will truncate terms that are too long to fit in the left column 14 | */ 15 | .dl-horizontal dt { 16 | white-space: normal; 17 | } 18 | 19 | /* Set width on the form input elements since they're 100% wide by default */ 20 | input, 21 | select, 22 | textarea { 23 | max-width: 280px; 24 | } 25 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Content/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.4.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;text-shadow:0 1px 0 #fff;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x;background-color:#e8e8e8}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x;background-color:#2e6da4}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} 6 | /*# sourceMappingURL=bootstrap-theme.min.css.map */ -------------------------------------------------------------------------------- /OneDriveDataRobot/Controllers/AccountController.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * OneDrive Data Robot - Sample Code 3 | * Copyright (c) Microsoft Corporation 4 | * All rights reserved. 5 | * 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | * this software and associated documentation files (the ""Software""), to deal in 10 | * the Software without restriction, including without limitation the rights to use, 11 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 12 | * Software, and to permit persons to whom the Software is furnished to do so, 13 | * subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | namespace OneDriveDataRobot.Controllers 27 | { 28 | using System.Web; 29 | using System.Web.Mvc; 30 | using Microsoft.Owin.Security.Cookies; 31 | using Microsoft.Owin.Security.OpenIdConnect; 32 | using Microsoft.Owin.Security; 33 | 34 | public class AccountController : Controller 35 | { 36 | public void SignIn(string force) 37 | { 38 | // Send an OpenID Connect sign-in request. 39 | bool forceLogin = false; 40 | if (!string.IsNullOrEmpty(force) && force == "1") forceLogin = true; 41 | if (forceLogin || !Request.IsAuthenticated) 42 | { 43 | HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/" }, 44 | OpenIdConnectAuthenticationDefaults.AuthenticationType); 45 | } 46 | } 47 | 48 | public void SignInAdmin() 49 | { 50 | // Send an OpenID Connect sign-in request. 51 | if (!Request.IsAuthenticated) 52 | { 53 | var authProps = new AuthenticationProperties { RedirectUri = "/" }; 54 | authProps.Dictionary["prompt"] = "admin_consent"; 55 | HttpContext.GetOwinContext().Authentication.Challenge(authProps, 56 | OpenIdConnectAuthenticationDefaults.AuthenticationType); 57 | // add &prompt=admin_consent 58 | } 59 | } 60 | 61 | public void SignOut() 62 | { 63 | string callbackUrl = Url.Action("SignOutCallback", "Account", routeValues: null, protocol: Request.Url.Scheme); 64 | 65 | HttpContext.GetOwinContext().Authentication.SignOut( 66 | new AuthenticationProperties { RedirectUri = callbackUrl }, 67 | OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType); 68 | } 69 | 70 | public ActionResult SignOutCallback() 71 | { 72 | if (Request.IsAuthenticated) 73 | { 74 | // Redirect to home page if the user is authenticated. 75 | return RedirectToAction("Index", "Home"); 76 | } 77 | 78 | return View(); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * OneDrive Data Robot - Sample Code 3 | * Copyright (c) Microsoft Corporation 4 | * All rights reserved. 5 | * 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | * this software and associated documentation files (the ""Software""), to deal in 10 | * the Software without restriction, including without limitation the rights to use, 11 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 12 | * Software, and to permit persons to whom the Software is furnished to do so, 13 | * subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | namespace OneDriveDataRobot.Controllers 27 | { 28 | using System; 29 | using System.Web.Mvc; 30 | using System.Threading.Tasks; 31 | public class HomeController : Controller 32 | { 33 | public async Task Index() 34 | { 35 | Models.HomeModel model = new Models.HomeModel(); 36 | 37 | model.SignInName = AuthHelper.GetUserId(); 38 | if (!string.IsNullOrEmpty(model.SignInName)) 39 | { 40 | try 41 | { 42 | var accessToken = await AuthHelper.GetUserAccessTokenSilentAsync(SettingsHelper.MicrosoftGraphBaseUrl); 43 | 44 | if (accessToken == null) 45 | { 46 | // Redirect to get new tokens 47 | return Redirect("/Account/SignIn?force=1"); 48 | } 49 | // Make an API request to get display name and MySite URL 50 | var response = await OneDriveDataRobot.Directory.UserInfo.GetUserInfoAsync(SettingsHelper.MicrosoftGraphBaseUrl, model.SignInName, accessToken.AccessToken); 51 | if (null != response) 52 | { 53 | model.DisplayName = response.DisplayName; 54 | model.OneDriveUrl = response.MySite; 55 | } 56 | else 57 | { 58 | model.DisplayName = "Error getting data from Microsoft Graph."; 59 | } 60 | } 61 | catch (Exception ex) 62 | { 63 | model.DisplayName = ex.ToString(); 64 | } 65 | } 66 | 67 | return View(model); 68 | } 69 | 70 | public ActionResult Error(string msg) 71 | { 72 | ViewBag.Message = msg; 73 | 74 | return View(); 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /OneDriveDataRobot/Controllers/SetupController.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace OneDriveDataRobot.Controllers 3 | { 4 | using OneDriveDataRobot.Models; 5 | using OneDriveDataRobot.Utils; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Net; 11 | using System.Net.Http; 12 | using System.Threading.Tasks; 13 | using System.Web.Http; 14 | using static OneDriveDataRobot.AuthHelper; 15 | using Microsoft.Graph; 16 | 17 | [Authorize] 18 | public class SetupController : ApiController 19 | { 20 | public async Task ActivateRobot() 21 | { 22 | // Setup a Microsoft Graph client for calls to the graph 23 | string graphBaseUrl = SettingsHelper.MicrosoftGraphBaseUrl; 24 | GraphServiceClient client = new GraphServiceClient(new DelegateAuthenticationProvider(async (req) => 25 | { 26 | // Get a fresh auth token 27 | var authToken = await AuthHelper.GetUserAccessTokenSilentAsync(graphBaseUrl); 28 | req.Headers.TryAddWithoutValidation("Authorization", $"Bearer {authToken.AccessToken}"); 29 | })); 30 | 31 | // Get an access token and signedInUserId 32 | AuthTokens tokens = null; 33 | try 34 | { 35 | tokens = await AuthHelper.GetUserAccessTokenSilentAsync(graphBaseUrl); 36 | } 37 | catch (Exception ex) 38 | { 39 | return Ok(new DataRobotSetup { Success = false, Error = ex.Message }); 40 | } 41 | 42 | // Check to see if this user is already wired up, so we avoid duplicate subscriptions 43 | var robotSubscription = StoredSubscriptionState.FindUser(tokens.SignInUserId, AzureTableContext.Default.SyncStateTable); 44 | 45 | var notificationSubscription = new Subscription() 46 | { 47 | ChangeType = "updated", 48 | NotificationUrl = SettingsHelper.NotificationUrl, 49 | Resource = "/me/drive/root", 50 | ExpirationDateTime = DateTime.UtcNow.AddDays(3), 51 | ClientState = "SecretClientState" 52 | }; 53 | 54 | Subscription createdSubscription = null; 55 | if (null != robotSubscription) 56 | { 57 | // See if our existing subscription can be extended to today + 3 days 58 | try 59 | { 60 | createdSubscription = await client.Subscriptions[robotSubscription.SubscriptionId].Request().UpdateAsync(notificationSubscription); 61 | } 62 | catch { } 63 | } 64 | 65 | if (null == createdSubscription) 66 | { 67 | // No existing subscription or we failed to update the existing subscription, so create a new one 68 | createdSubscription = await client.Subscriptions.Request().AddAsync(notificationSubscription); 69 | } 70 | 71 | var results = new DataRobotSetup() { 72 | SubscriptionId = createdSubscription.Id 73 | }; 74 | 75 | if (robotSubscription == null) 76 | { 77 | robotSubscription = StoredSubscriptionState.CreateNew(createdSubscription.Id); 78 | robotSubscription.SignInUserId = tokens.SignInUserId; 79 | } 80 | 81 | // Get the latest delta URL 82 | var latestDeltaResponse = await client.Me.Drive.Root.Delta("latest").Request().GetAsync(); 83 | robotSubscription.LastDeltaToken = latestDeltaResponse.AdditionalData["@odata.deltaLink"] as string; 84 | 85 | // Once we have a subscription, then we need to store that information into our Azure Table 86 | robotSubscription.Insert(AzureTableContext.Default.SyncStateTable); 87 | 88 | results.Success = true; 89 | results.ExpirationDateTime = createdSubscription.ExpirationDateTime; 90 | 91 | return Ok(results); 92 | } 93 | 94 | public async Task DisableRobot() 95 | { 96 | // Setup a Microsoft Graph client for calls to the graph 97 | string graphBaseUrl = SettingsHelper.MicrosoftGraphBaseUrl; 98 | GraphServiceClient client = new GraphServiceClient(new DelegateAuthenticationProvider(async (req) => 99 | { 100 | // Get a fresh auth token 101 | var authToken = await AuthHelper.GetUserAccessTokenSilentAsync(graphBaseUrl); 102 | req.Headers.TryAddWithoutValidation("Authorization", $"Bearer {authToken.AccessToken}"); 103 | })); 104 | 105 | // Get an access token and signedInUserId 106 | AuthTokens tokens = null; 107 | try 108 | { 109 | tokens = await AuthHelper.GetUserAccessTokenSilentAsync(graphBaseUrl); 110 | } 111 | catch (Exception ex) 112 | { 113 | return Ok(new DataRobotSetup { Success = false, Error = ex.Message }); 114 | } 115 | 116 | // See if the robot was previous activated for the signed in user. 117 | var robotSubscription = StoredSubscriptionState.FindUser(tokens.SignInUserId, AzureTableContext.Default.SyncStateTable); 118 | 119 | if (null == robotSubscription) 120 | { 121 | return Ok(new DataRobotSetup { Success = true, Error = "The robot wasn't activated for you anyway!" }); 122 | } 123 | 124 | // Remove the webhook subscription 125 | try 126 | { 127 | await client.Subscriptions[robotSubscription.SubscriptionId].Request().DeleteAsync(); 128 | } 129 | catch { } 130 | 131 | // Remove the robotSubscription information 132 | robotSubscription.Delete(AzureTableContext.Default.SyncStateTable); 133 | 134 | return Ok(new DataRobotSetup { Success = true, Error = "The robot was been deactivated from your account." }); 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /OneDriveDataRobot/Global.asax: -------------------------------------------------------------------------------- 1 | <%@ Application Codebehind="Global.asax.cs" Inherits="OneDriveDataRobot.MvcApplication" Language="C#" %> 2 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Global.asax.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using System.Web.Http; 6 | using System.Web.Mvc; 7 | using System.Web.Optimization; 8 | using System.Web.Routing; 9 | 10 | namespace OneDriveDataRobot 11 | { 12 | public class MvcApplication : System.Web.HttpApplication 13 | { 14 | protected void Application_Start() 15 | { 16 | AreaRegistration.RegisterAllAreas(); 17 | GlobalConfiguration.Configure(WebApiConfig.Register); // Register WebAPI framework first 18 | FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); 19 | RouteConfig.RegisterRoutes(RouteTable.Routes); 20 | BundleConfig.RegisterBundles(BundleTable.Bundles); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Models/DataRobotSetup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | 6 | namespace OneDriveDataRobot.Models 7 | { 8 | public class DataRobotSetup 9 | { 10 | public string SubscriptionId { get; set; } 11 | 12 | public bool Success { get; set; } 13 | 14 | public string Error { get; set; } 15 | public DateTimeOffset? ExpirationDateTime { get; internal set; } 16 | } 17 | } -------------------------------------------------------------------------------- /OneDriveDataRobot/Models/HomeModel.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * OneDrive Data Robot - Sample Code 3 | * Copyright (c) Microsoft Corporation 4 | * All rights reserved. 5 | * 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | * this software and associated documentation files (the ""Software""), to deal in 10 | * the Software without restriction, including without limitation the rights to use, 11 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 12 | * Software, and to permit persons to whom the Software is furnished to do so, 13 | * subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | namespace OneDriveDataRobot.Models 27 | { 28 | public class HomeModel 29 | { 30 | public string DisplayName { get; set; } 31 | public string SignInName { get; set; } 32 | public string OneDriveUrl { get; set; } 33 | } 34 | } -------------------------------------------------------------------------------- /OneDriveDataRobot/Models/UserInfo.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * OneDrive Data Robot - Sample Code 3 | * Copyright (c) Microsoft Corporation 4 | * All rights reserved. 5 | * 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | * this software and associated documentation files (the ""Software""), to deal in 10 | * the Software without restriction, including without limitation the rights to use, 11 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 12 | * Software, and to permit persons to whom the Software is furnished to do so, 13 | * subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | namespace OneDriveDataRobot.Directory 27 | { 28 | using System.Threading.Tasks; 29 | 30 | public class UserInfo 31 | { 32 | /// 33 | /// Converts a document into a PDF file asynchronously and saves it next to the original file. 34 | /// 35 | /// 36 | /// 37 | public static async Task GetUserInfoAsync(string graphBaseUrl, string userObjectId, string accessToken) 38 | { 39 | if (!string.IsNullOrEmpty(userObjectId)) 40 | { 41 | return await HttpHelper.Default.GetAsync($"{graphBaseUrl}/v1.0/users/{userObjectId}?$select=displayName,mysite", accessToken); 42 | } 43 | 44 | return null; 45 | } 46 | 47 | public string DisplayName { get; set; } 48 | public string MySite { get; set; } 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("OneDrive Data Robot Sample")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("OneDriveDataRobotSample")] 13 | [assembly: AssemblyCopyright("Copyright © Microsoft 2017")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("571c8b1e-a94d-4361-abfa-ac28b9b894ff")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Revision and Build Numbers 33 | // by using the '*' as shown below: 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Scripts/_references.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneDrive/onedrive-data-robot-azure-function/e8e4d89866799e7e1cf6284b02f15bcaa5ffbad8/OneDriveDataRobot/Scripts/_references.js -------------------------------------------------------------------------------- /OneDriveDataRobot/Scripts/jquery.validate.min.js: -------------------------------------------------------------------------------- 1 | /* NUGET: BEGIN LICENSE TEXT 2 | * 3 | * Microsoft grants you the right to use these script files for the sole 4 | * purpose of either: (i) interacting through your browser with the Microsoft 5 | * website or online service, subject to the applicable licensing or use 6 | * terms; or (ii) using the files as included with a Microsoft product subject 7 | * to that product's license terms. Microsoft reserves all other rights to the 8 | * files not expressly granted by Microsoft, whether by implication, estoppel 9 | * or otherwise. Insofar as a script file is dual licensed under GPL, 10 | * Microsoft neither took the code under GPL nor distributes it thereunder but 11 | * under the terms set out in this paragraph. All notices and licenses 12 | * below are for informational purposes only. 13 | * 14 | * NUGET: END LICENSE TEXT */ 15 | /*! jQuery Validation Plugin - v1.11.1 - 3/22/2013\n* https://github.com/jzaefferer/jquery-validation 16 | * Copyright (c) 2013 Jörn Zaefferer; Licensed MIT */(function(t){t.extend(t.fn,{validate:function(e){if(!this.length)return e&&e.debug&&window.console&&console.warn("Nothing selected, can't validate, returning nothing."),void 0;var i=t.data(this[0],"validator");return i?i:(this.attr("novalidate","novalidate"),i=new t.validator(e,this[0]),t.data(this[0],"validator",i),i.settings.onsubmit&&(this.validateDelegate(":submit","click",function(e){i.settings.submitHandler&&(i.submitButton=e.target),t(e.target).hasClass("cancel")&&(i.cancelSubmit=!0),void 0!==t(e.target).attr("formnovalidate")&&(i.cancelSubmit=!0)}),this.submit(function(e){function s(){var s;return i.settings.submitHandler?(i.submitButton&&(s=t("").attr("name",i.submitButton.name).val(t(i.submitButton).val()).appendTo(i.currentForm)),i.settings.submitHandler.call(i,i.currentForm,e),i.submitButton&&s.remove(),!1):!0}return i.settings.debug&&e.preventDefault(),i.cancelSubmit?(i.cancelSubmit=!1,s()):i.form()?i.pendingRequest?(i.formSubmitted=!0,!1):s():(i.focusInvalid(),!1)})),i)},valid:function(){if(t(this[0]).is("form"))return this.validate().form();var e=!0,i=t(this[0].form).validate();return this.each(function(){e=e&&i.element(this)}),e},removeAttrs:function(e){var i={},s=this;return t.each(e.split(/\s/),function(t,e){i[e]=s.attr(e),s.removeAttr(e)}),i},rules:function(e,i){var s=this[0];if(e){var r=t.data(s.form,"validator").settings,n=r.rules,a=t.validator.staticRules(s);switch(e){case"add":t.extend(a,t.validator.normalizeRule(i)),delete a.messages,n[s.name]=a,i.messages&&(r.messages[s.name]=t.extend(r.messages[s.name],i.messages));break;case"remove":if(!i)return delete n[s.name],a;var u={};return t.each(i.split(/\s/),function(t,e){u[e]=a[e],delete a[e]}),u}}var o=t.validator.normalizeRules(t.extend({},t.validator.classRules(s),t.validator.attributeRules(s),t.validator.dataRules(s),t.validator.staticRules(s)),s);if(o.required){var l=o.required;delete o.required,o=t.extend({required:l},o)}return o}}),t.extend(t.expr[":"],{blank:function(e){return!t.trim(""+t(e).val())},filled:function(e){return!!t.trim(""+t(e).val())},unchecked:function(e){return!t(e).prop("checked")}}),t.validator=function(e,i){this.settings=t.extend(!0,{},t.validator.defaults,e),this.currentForm=i,this.init()},t.validator.format=function(e,i){return 1===arguments.length?function(){var i=t.makeArray(arguments);return i.unshift(e),t.validator.format.apply(this,i)}:(arguments.length>2&&i.constructor!==Array&&(i=t.makeArray(arguments).slice(1)),i.constructor!==Array&&(i=[i]),t.each(i,function(t,i){e=e.replace(RegExp("\\{"+t+"\\}","g"),function(){return i})}),e)},t.extend(t.validator,{defaults:{messages:{},groups:{},rules:{},errorClass:"error",validClass:"valid",errorElement:"label",focusInvalid:!0,errorContainer:t([]),errorLabelContainer:t([]),onsubmit:!0,ignore:":hidden",ignoreTitle:!1,onfocusin:function(t){this.lastActive=t,this.settings.focusCleanup&&!this.blockFocusCleanup&&(this.settings.unhighlight&&this.settings.unhighlight.call(this,t,this.settings.errorClass,this.settings.validClass),this.addWrapper(this.errorsFor(t)).hide())},onfocusout:function(t){this.checkable(t)||!(t.name in this.submitted)&&this.optional(t)||this.element(t)},onkeyup:function(t,e){(9!==e.which||""!==this.elementValue(t))&&(t.name in this.submitted||t===this.lastElement)&&this.element(t)},onclick:function(t){t.name in this.submitted?this.element(t):t.parentNode.name in this.submitted&&this.element(t.parentNode)},highlight:function(e,i,s){"radio"===e.type?this.findByName(e.name).addClass(i).removeClass(s):t(e).addClass(i).removeClass(s)},unhighlight:function(e,i,s){"radio"===e.type?this.findByName(e.name).removeClass(i).addClass(s):t(e).removeClass(i).addClass(s)}},setDefaults:function(e){t.extend(t.validator.defaults,e)},messages:{required:"This field is required.",remote:"Please fix this field.",email:"Please enter a valid email address.",url:"Please enter a valid URL.",date:"Please enter a valid date.",dateISO:"Please enter a valid date (ISO).",number:"Please enter a valid number.",digits:"Please enter only digits.",creditcard:"Please enter a valid credit card number.",equalTo:"Please enter the same value again.",maxlength:t.validator.format("Please enter no more than {0} characters."),minlength:t.validator.format("Please enter at least {0} characters."),rangelength:t.validator.format("Please enter a value between {0} and {1} characters long."),range:t.validator.format("Please enter a value between {0} and {1}."),max:t.validator.format("Please enter a value less than or equal to {0}."),min:t.validator.format("Please enter a value greater than or equal to {0}.")},autoCreateRanges:!1,prototype:{init:function(){function e(e){var i=t.data(this[0].form,"validator"),s="on"+e.type.replace(/^validate/,"");i.settings[s]&&i.settings[s].call(i,this[0],e)}this.labelContainer=t(this.settings.errorLabelContainer),this.errorContext=this.labelContainer.length&&this.labelContainer||t(this.currentForm),this.containers=t(this.settings.errorContainer).add(this.settings.errorLabelContainer),this.submitted={},this.valueCache={},this.pendingRequest=0,this.pending={},this.invalid={},this.reset();var i=this.groups={};t.each(this.settings.groups,function(e,s){"string"==typeof s&&(s=s.split(/\s/)),t.each(s,function(t,s){i[s]=e})});var s=this.settings.rules;t.each(s,function(e,i){s[e]=t.validator.normalizeRule(i)}),t(this.currentForm).validateDelegate(":text, [type='password'], [type='file'], select, textarea, [type='number'], [type='search'] ,[type='tel'], [type='url'], [type='email'], [type='datetime'], [type='date'], [type='month'], [type='week'], [type='time'], [type='datetime-local'], [type='range'], [type='color'] ","focusin focusout keyup",e).validateDelegate("[type='radio'], [type='checkbox'], select, option","click",e),this.settings.invalidHandler&&t(this.currentForm).bind("invalid-form.validate",this.settings.invalidHandler)},form:function(){return this.checkForm(),t.extend(this.submitted,this.errorMap),this.invalid=t.extend({},this.errorMap),this.valid()||t(this.currentForm).triggerHandler("invalid-form",[this]),this.showErrors(),this.valid()},checkForm:function(){this.prepareForm();for(var t=0,e=this.currentElements=this.elements();e[t];t++)this.check(e[t]);return this.valid()},element:function(e){e=this.validationTargetFor(this.clean(e)),this.lastElement=e,this.prepareElement(e),this.currentElements=t(e);var i=this.check(e)!==!1;return i?delete this.invalid[e.name]:this.invalid[e.name]=!0,this.numberOfInvalids()||(this.toHide=this.toHide.add(this.containers)),this.showErrors(),i},showErrors:function(e){if(e){t.extend(this.errorMap,e),this.errorList=[];for(var i in e)this.errorList.push({message:e[i],element:this.findByName(i)[0]});this.successList=t.grep(this.successList,function(t){return!(t.name in e)})}this.settings.showErrors?this.settings.showErrors.call(this,this.errorMap,this.errorList):this.defaultShowErrors()},resetForm:function(){t.fn.resetForm&&t(this.currentForm).resetForm(),this.submitted={},this.lastElement=null,this.prepareForm(),this.hideErrors(),this.elements().removeClass(this.settings.errorClass).removeData("previousValue")},numberOfInvalids:function(){return this.objectLength(this.invalid)},objectLength:function(t){var e=0;for(var i in t)e++;return e},hideErrors:function(){this.addWrapper(this.toHide).hide()},valid:function(){return 0===this.size()},size:function(){return this.errorList.length},focusInvalid:function(){if(this.settings.focusInvalid)try{t(this.findLastActive()||this.errorList.length&&this.errorList[0].element||[]).filter(":visible").focus().trigger("focusin")}catch(e){}},findLastActive:function(){var e=this.lastActive;return e&&1===t.grep(this.errorList,function(t){return t.element.name===e.name}).length&&e},elements:function(){var e=this,i={};return t(this.currentForm).find("input, select, textarea").not(":submit, :reset, :image, [disabled]").not(this.settings.ignore).filter(function(){return!this.name&&e.settings.debug&&window.console&&console.error("%o has no name assigned",this),this.name in i||!e.objectLength(t(this).rules())?!1:(i[this.name]=!0,!0)})},clean:function(e){return t(e)[0]},errors:function(){var e=this.settings.errorClass.replace(" ",".");return t(this.settings.errorElement+"."+e,this.errorContext)},reset:function(){this.successList=[],this.errorList=[],this.errorMap={},this.toShow=t([]),this.toHide=t([]),this.currentElements=t([])},prepareForm:function(){this.reset(),this.toHide=this.errors().add(this.containers)},prepareElement:function(t){this.reset(),this.toHide=this.errorsFor(t)},elementValue:function(e){var i=t(e).attr("type"),s=t(e).val();return"radio"===i||"checkbox"===i?t("input[name='"+t(e).attr("name")+"']:checked").val():"string"==typeof s?s.replace(/\r/g,""):s},check:function(e){e=this.validationTargetFor(this.clean(e));var i,s=t(e).rules(),r=!1,n=this.elementValue(e);for(var a in s){var u={method:a,parameters:s[a]};try{if(i=t.validator.methods[a].call(this,n,e,u.parameters),"dependency-mismatch"===i){r=!0;continue}if(r=!1,"pending"===i)return this.toHide=this.toHide.not(this.errorsFor(e)),void 0;if(!i)return this.formatAndAdd(e,u),!1}catch(o){throw this.settings.debug&&window.console&&console.log("Exception occurred when checking element "+e.id+", check the '"+u.method+"' method.",o),o}}return r?void 0:(this.objectLength(s)&&this.successList.push(e),!0)},customDataMessage:function(e,i){return t(e).data("msg-"+i.toLowerCase())||e.attributes&&t(e).attr("data-msg-"+i.toLowerCase())},customMessage:function(t,e){var i=this.settings.messages[t];return i&&(i.constructor===String?i:i[e])},findDefined:function(){for(var t=0;arguments.length>t;t++)if(void 0!==arguments[t])return arguments[t];return void 0},defaultMessage:function(e,i){return this.findDefined(this.customMessage(e.name,i),this.customDataMessage(e,i),!this.settings.ignoreTitle&&e.title||void 0,t.validator.messages[i],"Warning: No message defined for "+e.name+"")},formatAndAdd:function(e,i){var s=this.defaultMessage(e,i.method),r=/\$?\{(\d+)\}/g;"function"==typeof s?s=s.call(this,i.parameters,e):r.test(s)&&(s=t.validator.format(s.replace(r,"{$1}"),i.parameters)),this.errorList.push({message:s,element:e}),this.errorMap[e.name]=s,this.submitted[e.name]=s},addWrapper:function(t){return this.settings.wrapper&&(t=t.add(t.parent(this.settings.wrapper))),t},defaultShowErrors:function(){var t,e;for(t=0;this.errorList[t];t++){var i=this.errorList[t];this.settings.highlight&&this.settings.highlight.call(this,i.element,this.settings.errorClass,this.settings.validClass),this.showLabel(i.element,i.message)}if(this.errorList.length&&(this.toShow=this.toShow.add(this.containers)),this.settings.success)for(t=0;this.successList[t];t++)this.showLabel(this.successList[t]);if(this.settings.unhighlight)for(t=0,e=this.validElements();e[t];t++)this.settings.unhighlight.call(this,e[t],this.settings.errorClass,this.settings.validClass);this.toHide=this.toHide.not(this.toShow),this.hideErrors(),this.addWrapper(this.toShow).show()},validElements:function(){return this.currentElements.not(this.invalidElements())},invalidElements:function(){return t(this.errorList).map(function(){return this.element})},showLabel:function(e,i){var s=this.errorsFor(e);s.length?(s.removeClass(this.settings.validClass).addClass(this.settings.errorClass),s.html(i)):(s=t("<"+this.settings.errorElement+">").attr("for",this.idOrName(e)).addClass(this.settings.errorClass).html(i||""),this.settings.wrapper&&(s=s.hide().show().wrap("<"+this.settings.wrapper+"/>").parent()),this.labelContainer.append(s).length||(this.settings.errorPlacement?this.settings.errorPlacement(s,t(e)):s.insertAfter(e))),!i&&this.settings.success&&(s.text(""),"string"==typeof this.settings.success?s.addClass(this.settings.success):this.settings.success(s,e)),this.toShow=this.toShow.add(s)},errorsFor:function(e){var i=this.idOrName(e);return this.errors().filter(function(){return t(this).attr("for")===i})},idOrName:function(t){return this.groups[t.name]||(this.checkable(t)?t.name:t.id||t.name)},validationTargetFor:function(t){return this.checkable(t)&&(t=this.findByName(t.name).not(this.settings.ignore)[0]),t},checkable:function(t){return/radio|checkbox/i.test(t.type)},findByName:function(e){return t(this.currentForm).find("[name='"+e+"']")},getLength:function(e,i){switch(i.nodeName.toLowerCase()){case"select":return t("option:selected",i).length;case"input":if(this.checkable(i))return this.findByName(i.name).filter(":checked").length}return e.length},depend:function(t,e){return this.dependTypes[typeof t]?this.dependTypes[typeof t](t,e):!0},dependTypes:{"boolean":function(t){return t},string:function(e,i){return!!t(e,i.form).length},"function":function(t,e){return t(e)}},optional:function(e){var i=this.elementValue(e);return!t.validator.methods.required.call(this,i,e)&&"dependency-mismatch"},startRequest:function(t){this.pending[t.name]||(this.pendingRequest++,this.pending[t.name]=!0)},stopRequest:function(e,i){this.pendingRequest--,0>this.pendingRequest&&(this.pendingRequest=0),delete this.pending[e.name],i&&0===this.pendingRequest&&this.formSubmitted&&this.form()?(t(this.currentForm).submit(),this.formSubmitted=!1):!i&&0===this.pendingRequest&&this.formSubmitted&&(t(this.currentForm).triggerHandler("invalid-form",[this]),this.formSubmitted=!1)},previousValue:function(e){return t.data(e,"previousValue")||t.data(e,"previousValue",{old:null,valid:!0,message:this.defaultMessage(e,"remote")})}},classRuleSettings:{required:{required:!0},email:{email:!0},url:{url:!0},date:{date:!0},dateISO:{dateISO:!0},number:{number:!0},digits:{digits:!0},creditcard:{creditcard:!0}},addClassRules:function(e,i){e.constructor===String?this.classRuleSettings[e]=i:t.extend(this.classRuleSettings,e)},classRules:function(e){var i={},s=t(e).attr("class");return s&&t.each(s.split(" "),function(){this in t.validator.classRuleSettings&&t.extend(i,t.validator.classRuleSettings[this])}),i},attributeRules:function(e){var i={},s=t(e),r=s[0].getAttribute("type");for(var n in t.validator.methods){var a;"required"===n?(a=s.get(0).getAttribute(n),""===a&&(a=!0),a=!!a):a=s.attr(n),/min|max/.test(n)&&(null===r||/number|range|text/.test(r))&&(a=Number(a)),a?i[n]=a:r===n&&"range"!==r&&(i[n]=!0)}return i.maxlength&&/-1|2147483647|524288/.test(i.maxlength)&&delete i.maxlength,i},dataRules:function(e){var i,s,r={},n=t(e);for(i in t.validator.methods)s=n.data("rule-"+i.toLowerCase()),void 0!==s&&(r[i]=s);return r},staticRules:function(e){var i={},s=t.data(e.form,"validator");return s.settings.rules&&(i=t.validator.normalizeRule(s.settings.rules[e.name])||{}),i},normalizeRules:function(e,i){return t.each(e,function(s,r){if(r===!1)return delete e[s],void 0;if(r.param||r.depends){var n=!0;switch(typeof r.depends){case"string":n=!!t(r.depends,i.form).length;break;case"function":n=r.depends.call(i,i)}n?e[s]=void 0!==r.param?r.param:!0:delete e[s]}}),t.each(e,function(s,r){e[s]=t.isFunction(r)?r(i):r}),t.each(["minlength","maxlength"],function(){e[this]&&(e[this]=Number(e[this]))}),t.each(["rangelength","range"],function(){var i;e[this]&&(t.isArray(e[this])?e[this]=[Number(e[this][0]),Number(e[this][1])]:"string"==typeof e[this]&&(i=e[this].split(/[\s,]+/),e[this]=[Number(i[0]),Number(i[1])]))}),t.validator.autoCreateRanges&&(e.min&&e.max&&(e.range=[e.min,e.max],delete e.min,delete e.max),e.minlength&&e.maxlength&&(e.rangelength=[e.minlength,e.maxlength],delete e.minlength,delete e.maxlength)),e},normalizeRule:function(e){if("string"==typeof e){var i={};t.each(e.split(/\s/),function(){i[this]=!0}),e=i}return e},addMethod:function(e,i,s){t.validator.methods[e]=i,t.validator.messages[e]=void 0!==s?s:t.validator.messages[e],3>i.length&&t.validator.addClassRules(e,t.validator.normalizeRule(e))},methods:{required:function(e,i,s){if(!this.depend(s,i))return"dependency-mismatch";if("select"===i.nodeName.toLowerCase()){var r=t(i).val();return r&&r.length>0}return this.checkable(i)?this.getLength(e,i)>0:t.trim(e).length>0},email:function(t,e){return this.optional(e)||/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i.test(t)},url:function(t,e){return this.optional(e)||/^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(t)},date:function(t,e){return this.optional(e)||!/Invalid|NaN/.test(""+new Date(t))},dateISO:function(t,e){return this.optional(e)||/^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/.test(t)},number:function(t,e){return this.optional(e)||/^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(t)},digits:function(t,e){return this.optional(e)||/^\d+$/.test(t)},creditcard:function(t,e){if(this.optional(e))return"dependency-mismatch";if(/[^0-9 \-]+/.test(t))return!1;var i=0,s=0,r=!1;t=t.replace(/\D/g,"");for(var n=t.length-1;n>=0;n--){var a=t.charAt(n);s=parseInt(a,10),r&&(s*=2)>9&&(s-=9),i+=s,r=!r}return 0===i%10},minlength:function(e,i,s){var r=t.isArray(e)?e.length:this.getLength(t.trim(e),i);return this.optional(i)||r>=s},maxlength:function(e,i,s){var r=t.isArray(e)?e.length:this.getLength(t.trim(e),i);return this.optional(i)||s>=r},rangelength:function(e,i,s){var r=t.isArray(e)?e.length:this.getLength(t.trim(e),i);return this.optional(i)||r>=s[0]&&s[1]>=r},min:function(t,e,i){return this.optional(e)||t>=i},max:function(t,e,i){return this.optional(e)||i>=t},range:function(t,e,i){return this.optional(e)||t>=i[0]&&i[1]>=t},equalTo:function(e,i,s){var r=t(s);return this.settings.onfocusout&&r.unbind(".validate-equalTo").bind("blur.validate-equalTo",function(){t(i).valid()}),e===r.val()},remote:function(e,i,s){if(this.optional(i))return"dependency-mismatch";var r=this.previousValue(i);if(this.settings.messages[i.name]||(this.settings.messages[i.name]={}),r.originalMessage=this.settings.messages[i.name].remote,this.settings.messages[i.name].remote=r.message,s="string"==typeof s&&{url:s}||s,r.old===e)return r.valid;r.old=e;var n=this;this.startRequest(i);var a={};return a[i.name]=e,t.ajax(t.extend(!0,{url:s,mode:"abort",port:"validate"+i.name,dataType:"json",data:a,success:function(s){n.settings.messages[i.name].remote=r.originalMessage;var a=s===!0||"true"===s;if(a){var u=n.formSubmitted;n.prepareElement(i),n.formSubmitted=u,n.successList.push(i),delete n.invalid[i.name],n.showErrors()}else{var o={},l=s||n.defaultMessage(i,"remote");o[i.name]=r.message=t.isFunction(l)?l(e):l,n.invalid[i.name]=!0,n.showErrors(o)}r.valid=a,n.stopRequest(i,a)}},s)),"pending"}}}),t.format=t.validator.format})(jQuery),function(t){var e={};if(t.ajaxPrefilter)t.ajaxPrefilter(function(t,i,s){var r=t.port;"abort"===t.mode&&(e[r]&&e[r].abort(),e[r]=s)});else{var i=t.ajax;t.ajax=function(s){var r=("mode"in s?s:t.ajaxSettings).mode,n=("port"in s?s:t.ajaxSettings).port;return"abort"===r?(e[n]&&e[n].abort(),e[n]=i.apply(this,arguments),e[n]):i.apply(this,arguments)}}}(jQuery),function(t){t.extend(t.fn,{validateDelegate:function(e,i,s){return this.bind(i,function(i){var r=t(i.target);return r.is(e)?s.apply(r,arguments):void 0})}})}(jQuery); -------------------------------------------------------------------------------- /OneDriveDataRobot/Scripts/jquery.validate.unobtrusive.js: -------------------------------------------------------------------------------- 1 | /* NUGET: BEGIN LICENSE TEXT 2 | * 3 | * Microsoft grants you the right to use these script files for the sole 4 | * purpose of either: (i) interacting through your browser with the Microsoft 5 | * website or online service, subject to the applicable licensing or use 6 | * terms; or (ii) using the files as included with a Microsoft product subject 7 | * to that product's license terms. Microsoft reserves all other rights to the 8 | * files not expressly granted by Microsoft, whether by implication, estoppel 9 | * or otherwise. Insofar as a script file is dual licensed under GPL, 10 | * Microsoft neither took the code under GPL nor distributes it thereunder but 11 | * under the terms set out in this paragraph. All notices and licenses 12 | * below are for informational purposes only. 13 | * 14 | * NUGET: END LICENSE TEXT */ 15 | /*! 16 | ** Unobtrusive validation support library for jQuery and jQuery Validate 17 | ** Copyright (C) Microsoft Corporation. All rights reserved. 18 | */ 19 | 20 | /*jslint white: true, browser: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true, strict: false */ 21 | /*global document: false, jQuery: false */ 22 | 23 | (function ($) { 24 | var $jQval = $.validator, 25 | adapters, 26 | data_validation = "unobtrusiveValidation"; 27 | 28 | function setValidationValues(options, ruleName, value) { 29 | options.rules[ruleName] = value; 30 | if (options.message) { 31 | options.messages[ruleName] = options.message; 32 | } 33 | } 34 | 35 | function splitAndTrim(value) { 36 | return value.replace(/^\s+|\s+$/g, "").split(/\s*,\s*/g); 37 | } 38 | 39 | function escapeAttributeValue(value) { 40 | // As mentioned on http://api.jquery.com/category/selectors/ 41 | return value.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g, "\\$1"); 42 | } 43 | 44 | function getModelPrefix(fieldName) { 45 | return fieldName.substr(0, fieldName.lastIndexOf(".") + 1); 46 | } 47 | 48 | function appendModelPrefix(value, prefix) { 49 | if (value.indexOf("*.") === 0) { 50 | value = value.replace("*.", prefix); 51 | } 52 | return value; 53 | } 54 | 55 | function onError(error, inputElement) { // 'this' is the form element 56 | var container = $(this).find("[data-valmsg-for='" + escapeAttributeValue(inputElement[0].name) + "']"), 57 | replaceAttrValue = container.attr("data-valmsg-replace"), 58 | replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) !== false : null; 59 | 60 | container.removeClass("field-validation-valid").addClass("field-validation-error"); 61 | error.data("unobtrusiveContainer", container); 62 | 63 | if (replace) { 64 | container.empty(); 65 | error.removeClass("input-validation-error").appendTo(container); 66 | } 67 | else { 68 | error.hide(); 69 | } 70 | } 71 | 72 | function onErrors(event, validator) { // 'this' is the form element 73 | var container = $(this).find("[data-valmsg-summary=true]"), 74 | list = container.find("ul"); 75 | 76 | if (list && list.length && validator.errorList.length) { 77 | list.empty(); 78 | container.addClass("validation-summary-errors").removeClass("validation-summary-valid"); 79 | 80 | $.each(validator.errorList, function () { 81 | $("
  • ").html(this.message).appendTo(list); 82 | }); 83 | } 84 | } 85 | 86 | function onSuccess(error) { // 'this' is the form element 87 | var container = error.data("unobtrusiveContainer"), 88 | replaceAttrValue = container.attr("data-valmsg-replace"), 89 | replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) : null; 90 | 91 | if (container) { 92 | container.addClass("field-validation-valid").removeClass("field-validation-error"); 93 | error.removeData("unobtrusiveContainer"); 94 | 95 | if (replace) { 96 | container.empty(); 97 | } 98 | } 99 | } 100 | 101 | function onReset(event) { // 'this' is the form element 102 | var $form = $(this), 103 | key = '__jquery_unobtrusive_validation_form_reset'; 104 | if ($form.data(key)) { 105 | return; 106 | } 107 | // Set a flag that indicates we're currently resetting the form. 108 | $form.data(key, true); 109 | try { 110 | $form.data("validator").resetForm(); 111 | } finally { 112 | $form.removeData(key); 113 | } 114 | 115 | $form.find(".validation-summary-errors") 116 | .addClass("validation-summary-valid") 117 | .removeClass("validation-summary-errors"); 118 | $form.find(".field-validation-error") 119 | .addClass("field-validation-valid") 120 | .removeClass("field-validation-error") 121 | .removeData("unobtrusiveContainer") 122 | .find(">*") // If we were using valmsg-replace, get the underlying error 123 | .removeData("unobtrusiveContainer"); 124 | } 125 | 126 | function validationInfo(form) { 127 | var $form = $(form), 128 | result = $form.data(data_validation), 129 | onResetProxy = $.proxy(onReset, form), 130 | defaultOptions = $jQval.unobtrusive.options || {}, 131 | execInContext = function (name, args) { 132 | var func = defaultOptions[name]; 133 | func && $.isFunction(func) && func.apply(form, args); 134 | } 135 | 136 | if (!result) { 137 | result = { 138 | options: { // options structure passed to jQuery Validate's validate() method 139 | errorClass: defaultOptions.errorClass || "input-validation-error", 140 | errorElement: defaultOptions.errorElement || "span", 141 | errorPlacement: function () { 142 | onError.apply(form, arguments); 143 | execInContext("errorPlacement", arguments); 144 | }, 145 | invalidHandler: function () { 146 | onErrors.apply(form, arguments); 147 | execInContext("invalidHandler", arguments); 148 | }, 149 | messages: {}, 150 | rules: {}, 151 | success: function () { 152 | onSuccess.apply(form, arguments); 153 | execInContext("success", arguments); 154 | } 155 | }, 156 | attachValidation: function () { 157 | $form 158 | .off("reset." + data_validation, onResetProxy) 159 | .on("reset." + data_validation, onResetProxy) 160 | .validate(this.options); 161 | }, 162 | validate: function () { // a validation function that is called by unobtrusive Ajax 163 | $form.validate(); 164 | return $form.valid(); 165 | } 166 | }; 167 | $form.data(data_validation, result); 168 | } 169 | 170 | return result; 171 | } 172 | 173 | $jQval.unobtrusive = { 174 | adapters: [], 175 | 176 | parseElement: function (element, skipAttach) { 177 | /// 178 | /// Parses a single HTML element for unobtrusive validation attributes. 179 | /// 180 | /// The HTML element to be parsed. 181 | /// [Optional] true to skip attaching the 182 | /// validation to the form. If parsing just this single element, you should specify true. 183 | /// If parsing several elements, you should specify false, and manually attach the validation 184 | /// to the form when you are finished. The default is false. 185 | var $element = $(element), 186 | form = $element.parents("form")[0], 187 | valInfo, rules, messages; 188 | 189 | if (!form) { // Cannot do client-side validation without a form 190 | return; 191 | } 192 | 193 | valInfo = validationInfo(form); 194 | valInfo.options.rules[element.name] = rules = {}; 195 | valInfo.options.messages[element.name] = messages = {}; 196 | 197 | $.each(this.adapters, function () { 198 | var prefix = "data-val-" + this.name, 199 | message = $element.attr(prefix), 200 | paramValues = {}; 201 | 202 | if (message !== undefined) { // Compare against undefined, because an empty message is legal (and falsy) 203 | prefix += "-"; 204 | 205 | $.each(this.params, function () { 206 | paramValues[this] = $element.attr(prefix + this); 207 | }); 208 | 209 | this.adapt({ 210 | element: element, 211 | form: form, 212 | message: message, 213 | params: paramValues, 214 | rules: rules, 215 | messages: messages 216 | }); 217 | } 218 | }); 219 | 220 | $.extend(rules, { "__dummy__": true }); 221 | 222 | if (!skipAttach) { 223 | valInfo.attachValidation(); 224 | } 225 | }, 226 | 227 | parse: function (selector) { 228 | /// 229 | /// Parses all the HTML elements in the specified selector. It looks for input elements decorated 230 | /// with the [data-val=true] attribute value and enables validation according to the data-val-* 231 | /// attribute values. 232 | /// 233 | /// Any valid jQuery selector. 234 | 235 | // $forms includes all forms in selector's DOM hierarchy (parent, children and self) that have at least one 236 | // element with data-val=true 237 | var $selector = $(selector), 238 | $forms = $selector.parents() 239 | .addBack() 240 | .filter("form") 241 | .add($selector.find("form")) 242 | .has("[data-val=true]"); 243 | 244 | $selector.find("[data-val=true]").each(function () { 245 | $jQval.unobtrusive.parseElement(this, true); 246 | }); 247 | 248 | $forms.each(function () { 249 | var info = validationInfo(this); 250 | if (info) { 251 | info.attachValidation(); 252 | } 253 | }); 254 | } 255 | }; 256 | 257 | adapters = $jQval.unobtrusive.adapters; 258 | 259 | adapters.add = function (adapterName, params, fn) { 260 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation. 261 | /// The name of the adapter to be added. This matches the name used 262 | /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name). 263 | /// [Optional] An array of parameter names (strings) that will 264 | /// be extracted from the data-val-nnnn-mmmm HTML attributes (where nnnn is the adapter name, and 265 | /// mmmm is the parameter name). 266 | /// The function to call, which adapts the values from the HTML 267 | /// attributes into jQuery Validate rules and/or messages. 268 | /// 269 | if (!fn) { // Called with no params, just a function 270 | fn = params; 271 | params = []; 272 | } 273 | this.push({ name: adapterName, params: params, adapt: fn }); 274 | return this; 275 | }; 276 | 277 | adapters.addBool = function (adapterName, ruleName) { 278 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where 279 | /// the jQuery Validate validation rule has no parameter values. 280 | /// The name of the adapter to be added. This matches the name used 281 | /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name). 282 | /// [Optional] The name of the jQuery Validate rule. If not provided, the value 283 | /// of adapterName will be used instead. 284 | /// 285 | return this.add(adapterName, function (options) { 286 | setValidationValues(options, ruleName || adapterName, true); 287 | }); 288 | }; 289 | 290 | adapters.addMinMax = function (adapterName, minRuleName, maxRuleName, minMaxRuleName, minAttribute, maxAttribute) { 291 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where 292 | /// the jQuery Validate validation has three potential rules (one for min-only, one for max-only, and 293 | /// one for min-and-max). The HTML parameters are expected to be named -min and -max. 294 | /// The name of the adapter to be added. This matches the name used 295 | /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name). 296 | /// The name of the jQuery Validate rule to be used when you only 297 | /// have a minimum value. 298 | /// The name of the jQuery Validate rule to be used when you only 299 | /// have a maximum value. 300 | /// The name of the jQuery Validate rule to be used when you 301 | /// have both a minimum and maximum value. 302 | /// [Optional] The name of the HTML attribute that 303 | /// contains the minimum value. The default is "min". 304 | /// [Optional] The name of the HTML attribute that 305 | /// contains the maximum value. The default is "max". 306 | /// 307 | return this.add(adapterName, [minAttribute || "min", maxAttribute || "max"], function (options) { 308 | var min = options.params.min, 309 | max = options.params.max; 310 | 311 | if (min && max) { 312 | setValidationValues(options, minMaxRuleName, [min, max]); 313 | } 314 | else if (min) { 315 | setValidationValues(options, minRuleName, min); 316 | } 317 | else if (max) { 318 | setValidationValues(options, maxRuleName, max); 319 | } 320 | }); 321 | }; 322 | 323 | adapters.addSingleVal = function (adapterName, attribute, ruleName) { 324 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where 325 | /// the jQuery Validate validation rule has a single value. 326 | /// The name of the adapter to be added. This matches the name used 327 | /// in the data-val-nnnn HTML attribute(where nnnn is the adapter name). 328 | /// [Optional] The name of the HTML attribute that contains the value. 329 | /// The default is "val". 330 | /// [Optional] The name of the jQuery Validate rule. If not provided, the value 331 | /// of adapterName will be used instead. 332 | /// 333 | return this.add(adapterName, [attribute || "val"], function (options) { 334 | setValidationValues(options, ruleName || adapterName, options.params[attribute]); 335 | }); 336 | }; 337 | 338 | $jQval.addMethod("__dummy__", function (value, element, params) { 339 | return true; 340 | }); 341 | 342 | $jQval.addMethod("regex", function (value, element, params) { 343 | var match; 344 | if (this.optional(element)) { 345 | return true; 346 | } 347 | 348 | match = new RegExp(params).exec(value); 349 | return (match && (match.index === 0) && (match[0].length === value.length)); 350 | }); 351 | 352 | $jQval.addMethod("nonalphamin", function (value, element, nonalphamin) { 353 | var match; 354 | if (nonalphamin) { 355 | match = value.match(/\W/g); 356 | match = match && match.length >= nonalphamin; 357 | } 358 | return match; 359 | }); 360 | 361 | if ($jQval.methods.extension) { 362 | adapters.addSingleVal("accept", "mimtype"); 363 | adapters.addSingleVal("extension", "extension"); 364 | } else { 365 | // for backward compatibility, when the 'extension' validation method does not exist, such as with versions 366 | // of JQuery Validation plugin prior to 1.10, we should use the 'accept' method for 367 | // validating the extension, and ignore mime-type validations as they are not supported. 368 | adapters.addSingleVal("extension", "extension", "accept"); 369 | } 370 | 371 | adapters.addSingleVal("regex", "pattern"); 372 | adapters.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url"); 373 | adapters.addMinMax("length", "minlength", "maxlength", "rangelength").addMinMax("range", "min", "max", "range"); 374 | adapters.addMinMax("minlength", "minlength").addMinMax("maxlength", "minlength", "maxlength"); 375 | adapters.add("equalto", ["other"], function (options) { 376 | var prefix = getModelPrefix(options.element.name), 377 | other = options.params.other, 378 | fullOtherName = appendModelPrefix(other, prefix), 379 | element = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(fullOtherName) + "']")[0]; 380 | 381 | setValidationValues(options, "equalTo", element); 382 | }); 383 | adapters.add("required", function (options) { 384 | // jQuery Validate equates "required" with "mandatory" for checkbox elements 385 | if (options.element.tagName.toUpperCase() !== "INPUT" || options.element.type.toUpperCase() !== "CHECKBOX") { 386 | setValidationValues(options, "required", true); 387 | } 388 | }); 389 | adapters.add("remote", ["url", "type", "additionalfields"], function (options) { 390 | var value = { 391 | url: options.params.url, 392 | type: options.params.type || "GET", 393 | data: {} 394 | }, 395 | prefix = getModelPrefix(options.element.name); 396 | 397 | $.each(splitAndTrim(options.params.additionalfields || options.element.name), function (i, fieldName) { 398 | var paramName = appendModelPrefix(fieldName, prefix); 399 | value.data[paramName] = function () { 400 | var field = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(paramName) + "']"); 401 | // For checkboxes and radio buttons, only pick up values from checked fields. 402 | if (field.is(":checkbox")) { 403 | return field.filter(":checked").val() || field.filter(":hidden").val() || ''; 404 | } 405 | else if (field.is(":radio")) { 406 | return field.filter(":checked").val() || ''; 407 | } 408 | return field.val(); 409 | }; 410 | }); 411 | 412 | setValidationValues(options, "remote", value); 413 | }); 414 | adapters.add("password", ["min", "nonalphamin", "regex"], function (options) { 415 | if (options.params.min) { 416 | setValidationValues(options, "minlength", options.params.min); 417 | } 418 | if (options.params.nonalphamin) { 419 | setValidationValues(options, "nonalphamin", options.params.nonalphamin); 420 | } 421 | if (options.params.regex) { 422 | setValidationValues(options, "regex", options.params.regex); 423 | } 424 | }); 425 | 426 | $(function () { 427 | $jQval.unobtrusive.parse(document); 428 | }); 429 | }(jQuery)); -------------------------------------------------------------------------------- /OneDriveDataRobot/Scripts/jquery.validate.unobtrusive.min.js: -------------------------------------------------------------------------------- 1 | /* NUGET: BEGIN LICENSE TEXT 2 | * 3 | * Microsoft grants you the right to use these script files for the sole 4 | * purpose of either: (i) interacting through your browser with the Microsoft 5 | * website or online service, subject to the applicable licensing or use 6 | * terms; or (ii) using the files as included with a Microsoft product subject 7 | * to that product's license terms. Microsoft reserves all other rights to the 8 | * files not expressly granted by Microsoft, whether by implication, estoppel 9 | * or otherwise. Insofar as a script file is dual licensed under GPL, 10 | * Microsoft neither took the code under GPL nor distributes it thereunder but 11 | * under the terms set out in this paragraph. All notices and licenses 12 | * below are for informational purposes only. 13 | * 14 | * NUGET: END LICENSE TEXT */ 15 | /* 16 | ** Unobtrusive validation support library for jQuery and jQuery Validate 17 | ** Copyright (C) Microsoft Corporation. All rights reserved. 18 | */ 19 | (function(a){var d=a.validator,b,e="unobtrusiveValidation";function c(a,b,c){a.rules[b]=c;if(a.message)a.messages[b]=a.message}function j(a){return a.replace(/^\s+|\s+$/g,"").split(/\s*,\s*/g)}function f(a){return a.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g,"\\$1")}function h(a){return a.substr(0,a.lastIndexOf(".")+1)}function g(a,b){if(a.indexOf("*.")===0)a=a.replace("*.",b);return a}function m(c,e){var b=a(this).find("[data-valmsg-for='"+f(e[0].name)+"']"),d=b.attr("data-valmsg-replace"),g=d?a.parseJSON(d)!==false:null;b.removeClass("field-validation-valid").addClass("field-validation-error");c.data("unobtrusiveContainer",b);if(g){b.empty();c.removeClass("input-validation-error").appendTo(b)}else c.hide()}function l(e,d){var c=a(this).find("[data-valmsg-summary=true]"),b=c.find("ul");if(b&&b.length&&d.errorList.length){b.empty();c.addClass("validation-summary-errors").removeClass("validation-summary-valid");a.each(d.errorList,function(){a("
  • ").html(this.message).appendTo(b)})}}function k(d){var b=d.data("unobtrusiveContainer"),c=b.attr("data-valmsg-replace"),e=c?a.parseJSON(c):null;if(b){b.addClass("field-validation-valid").removeClass("field-validation-error");d.removeData("unobtrusiveContainer");e&&b.empty()}}function n(){var b=a(this),c="__jquery_unobtrusive_validation_form_reset";if(b.data(c))return;b.data(c,true);try{b.data("validator").resetForm()}finally{b.removeData(c)}b.find(".validation-summary-errors").addClass("validation-summary-valid").removeClass("validation-summary-errors");b.find(".field-validation-error").addClass("field-validation-valid").removeClass("field-validation-error").removeData("unobtrusiveContainer").find(">*").removeData("unobtrusiveContainer")}function i(b){var c=a(b),f=c.data(e),i=a.proxy(n,b),g=d.unobtrusive.options||{},h=function(e,d){var c=g[e];c&&a.isFunction(c)&&c.apply(b,d)};if(!f){f={options:{errorClass:g.errorClass||"input-validation-error",errorElement:g.errorElement||"span",errorPlacement:function(){m.apply(b,arguments);h("errorPlacement",arguments)},invalidHandler:function(){l.apply(b,arguments);h("invalidHandler",arguments)},messages:{},rules:{},success:function(){k.apply(b,arguments);h("success",arguments)}},attachValidation:function(){c.off("reset."+e,i).on("reset."+e,i).validate(this.options)},validate:function(){c.validate();return c.valid()}};c.data(e,f)}return f}d.unobtrusive={adapters:[],parseElement:function(b,h){var d=a(b),f=d.parents("form")[0],c,e,g;if(!f)return;c=i(f);c.options.rules[b.name]=e={};c.options.messages[b.name]=g={};a.each(this.adapters,function(){var c="data-val-"+this.name,i=d.attr(c),h={};if(i!==undefined){c+="-";a.each(this.params,function(){h[this]=d.attr(c+this)});this.adapt({element:b,form:f,message:i,params:h,rules:e,messages:g})}});a.extend(e,{__dummy__:true});!h&&c.attachValidation()},parse:function(c){var b=a(c),e=b.parents().addBack().filter("form").add(b.find("form")).has("[data-val=true]");b.find("[data-val=true]").each(function(){d.unobtrusive.parseElement(this,true)});e.each(function(){var a=i(this);a&&a.attachValidation()})}};b=d.unobtrusive.adapters;b.add=function(c,a,b){if(!b){b=a;a=[]}this.push({name:c,params:a,adapt:b});return this};b.addBool=function(a,b){return this.add(a,function(d){c(d,b||a,true)})};b.addMinMax=function(e,g,f,a,d,b){return this.add(e,[d||"min",b||"max"],function(b){var e=b.params.min,d=b.params.max;if(e&&d)c(b,a,[e,d]);else if(e)c(b,g,e);else d&&c(b,f,d)})};b.addSingleVal=function(a,b,d){return this.add(a,[b||"val"],function(e){c(e,d||a,e.params[b])})};d.addMethod("__dummy__",function(){return true});d.addMethod("regex",function(b,c,d){var a;if(this.optional(c))return true;a=(new RegExp(d)).exec(b);return a&&a.index===0&&a[0].length===b.length});d.addMethod("nonalphamin",function(c,d,b){var a;if(b){a=c.match(/\W/g);a=a&&a.length>=b}return a});if(d.methods.extension){b.addSingleVal("accept","mimtype");b.addSingleVal("extension","extension")}else b.addSingleVal("extension","extension","accept");b.addSingleVal("regex","pattern");b.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url");b.addMinMax("length","minlength","maxlength","rangelength").addMinMax("range","min","max","range");b.addMinMax("minlength","minlength").addMinMax("maxlength","minlength","maxlength");b.add("equalto",["other"],function(b){var i=h(b.element.name),j=b.params.other,d=g(j,i),e=a(b.form).find(":input").filter("[name='"+f(d)+"']")[0];c(b,"equalTo",e)});b.add("required",function(a){(a.element.tagName.toUpperCase()!=="INPUT"||a.element.type.toUpperCase()!=="CHECKBOX")&&c(a,"required",true)});b.add("remote",["url","type","additionalfields"],function(b){var d={url:b.params.url,type:b.params.type||"GET",data:{}},e=h(b.element.name);a.each(j(b.params.additionalfields||b.element.name),function(i,h){var c=g(h,e);d.data[c]=function(){var d=a(b.form).find(":input").filter("[name='"+f(c)+"']");return d.is(":checkbox")?d.filter(":checked").val()||d.filter(":hidden").val()||"":d.is(":radio")?d.filter(":checked").val()||"":d.val()}});c(b,"remote",d)});b.add("password",["min","nonalphamin","regex"],function(a){a.params.min&&c(a,"minlength",a.params.min);a.params.nonalphamin&&c(a,"nonalphamin",a.params.nonalphamin);a.params.regex&&c(a,"regex",a.params.regex)});a(function(){d.unobtrusive.parse(document)})})(jQuery); -------------------------------------------------------------------------------- /OneDriveDataRobot/Scripts/respond.js: -------------------------------------------------------------------------------- 1 | /* NUGET: BEGIN LICENSE TEXT 2 | * 3 | * Microsoft grants you the right to use these script files for the sole 4 | * purpose of either: (i) interacting through your browser with the Microsoft 5 | * website or online service, subject to the applicable licensing or use 6 | * terms; or (ii) using the files as included with a Microsoft product subject 7 | * to that product's license terms. Microsoft reserves all other rights to the 8 | * files not expressly granted by Microsoft, whether by implication, estoppel 9 | * or otherwise. Insofar as a script file is dual licensed under GPL, 10 | * Microsoft neither took the code under GPL nor distributes it thereunder but 11 | * under the terms set out in this paragraph. All notices and licenses 12 | * below are for informational purposes only. 13 | * 14 | * NUGET: END LICENSE TEXT */ 15 | /*! matchMedia() polyfill - Test a CSS media type/query in JS. Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas. Dual MIT/BSD license */ 16 | /*! NOTE: If you're already including a window.matchMedia polyfill via Modernizr or otherwise, you don't need this part */ 17 | window.matchMedia = window.matchMedia || (function(doc, undefined){ 18 | 19 | var bool, 20 | docElem = doc.documentElement, 21 | refNode = docElem.firstElementChild || docElem.firstChild, 22 | // fakeBody required for 23 | fakeBody = doc.createElement('body'), 24 | div = doc.createElement('div'); 25 | 26 | div.id = 'mq-test-1'; 27 | div.style.cssText = "position:absolute;top:-100em"; 28 | fakeBody.style.background = "none"; 29 | fakeBody.appendChild(div); 30 | 31 | return function(q){ 32 | 33 | div.innerHTML = '­'; 34 | 35 | docElem.insertBefore(fakeBody, refNode); 36 | bool = div.offsetWidth == 42; 37 | docElem.removeChild(fakeBody); 38 | 39 | return { matches: bool, media: q }; 40 | }; 41 | 42 | })(document); 43 | 44 | 45 | 46 | 47 | /*! Respond.js v1.2.0: min/max-width media query polyfill. (c) Scott Jehl. MIT/GPLv2 Lic. j.mp/respondjs */ 48 | (function( win ){ 49 | //exposed namespace 50 | win.respond = {}; 51 | 52 | //define update even in native-mq-supporting browsers, to avoid errors 53 | respond.update = function(){}; 54 | 55 | //expose media query support flag for external use 56 | respond.mediaQueriesSupported = win.matchMedia && win.matchMedia( "only all" ).matches; 57 | 58 | //if media queries are supported, exit here 59 | if( respond.mediaQueriesSupported ){ return; } 60 | 61 | //define vars 62 | var doc = win.document, 63 | docElem = doc.documentElement, 64 | mediastyles = [], 65 | rules = [], 66 | appendedEls = [], 67 | parsedSheets = {}, 68 | resizeThrottle = 30, 69 | head = doc.getElementsByTagName( "head" )[0] || docElem, 70 | base = doc.getElementsByTagName( "base" )[0], 71 | links = head.getElementsByTagName( "link" ), 72 | requestQueue = [], 73 | 74 | //loop stylesheets, send text content to translate 75 | ripCSS = function(){ 76 | var sheets = links, 77 | sl = sheets.length, 78 | i = 0, 79 | //vars for loop: 80 | sheet, href, media, isCSS; 81 | 82 | for( ; i < sl; i++ ){ 83 | sheet = sheets[ i ], 84 | href = sheet.href, 85 | media = sheet.media, 86 | isCSS = sheet.rel && sheet.rel.toLowerCase() === "stylesheet"; 87 | 88 | //only links plz and prevent re-parsing 89 | if( !!href && isCSS && !parsedSheets[ href ] ){ 90 | // selectivizr exposes css through the rawCssText expando 91 | if (sheet.styleSheet && sheet.styleSheet.rawCssText) { 92 | translate( sheet.styleSheet.rawCssText, href, media ); 93 | parsedSheets[ href ] = true; 94 | } else { 95 | if( (!/^([a-zA-Z:]*\/\/)/.test( href ) && !base) 96 | || href.replace( RegExp.$1, "" ).split( "/" )[0] === win.location.host ){ 97 | requestQueue.push( { 98 | href: href, 99 | media: media 100 | } ); 101 | } 102 | } 103 | } 104 | } 105 | makeRequests(); 106 | }, 107 | 108 | //recurse through request queue, get css text 109 | makeRequests = function(){ 110 | if( requestQueue.length ){ 111 | var thisRequest = requestQueue.shift(); 112 | 113 | ajax( thisRequest.href, function( styles ){ 114 | translate( styles, thisRequest.href, thisRequest.media ); 115 | parsedSheets[ thisRequest.href ] = true; 116 | makeRequests(); 117 | } ); 118 | } 119 | }, 120 | 121 | //find media blocks in css text, convert to style blocks 122 | translate = function( styles, href, media ){ 123 | var qs = styles.match( /@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi ), 124 | ql = qs && qs.length || 0, 125 | //try to get CSS path 126 | href = href.substring( 0, href.lastIndexOf( "/" )), 127 | repUrls = function( css ){ 128 | return css.replace( /(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g, "$1" + href + "$2$3" ); 129 | }, 130 | useMedia = !ql && media, 131 | //vars used in loop 132 | i = 0, 133 | j, fullq, thisq, eachq, eql; 134 | 135 | //if path exists, tack on trailing slash 136 | if( href.length ){ href += "/"; } 137 | 138 | //if no internal queries exist, but media attr does, use that 139 | //note: this currently lacks support for situations where a media attr is specified on a link AND 140 | //its associated stylesheet has internal CSS media queries. 141 | //In those cases, the media attribute will currently be ignored. 142 | if( useMedia ){ 143 | ql = 1; 144 | } 145 | 146 | 147 | for( ; i < ql; i++ ){ 148 | j = 0; 149 | 150 | //media attr 151 | if( useMedia ){ 152 | fullq = media; 153 | rules.push( repUrls( styles ) ); 154 | } 155 | //parse for styles 156 | else{ 157 | fullq = qs[ i ].match( /@media *([^\{]+)\{([\S\s]+?)$/ ) && RegExp.$1; 158 | rules.push( RegExp.$2 && repUrls( RegExp.$2 ) ); 159 | } 160 | 161 | eachq = fullq.split( "," ); 162 | eql = eachq.length; 163 | 164 | for( ; j < eql; j++ ){ 165 | thisq = eachq[ j ]; 166 | mediastyles.push( { 167 | media : thisq.split( "(" )[ 0 ].match( /(only\s+)?([a-zA-Z]+)\s?/ ) && RegExp.$2 || "all", 168 | rules : rules.length - 1, 169 | hasquery: thisq.indexOf("(") > -1, 170 | minw : thisq.match( /\(min\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/ ) && parseFloat( RegExp.$1 ) + ( RegExp.$2 || "" ), 171 | maxw : thisq.match( /\(max\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/ ) && parseFloat( RegExp.$1 ) + ( RegExp.$2 || "" ) 172 | } ); 173 | } 174 | } 175 | 176 | applyMedia(); 177 | }, 178 | 179 | lastCall, 180 | 181 | resizeDefer, 182 | 183 | // returns the value of 1em in pixels 184 | getEmValue = function() { 185 | var ret, 186 | div = doc.createElement('div'), 187 | body = doc.body, 188 | fakeUsed = false; 189 | 190 | div.style.cssText = "position:absolute;font-size:1em;width:1em"; 191 | 192 | if( !body ){ 193 | body = fakeUsed = doc.createElement( "body" ); 194 | body.style.background = "none"; 195 | } 196 | 197 | body.appendChild( div ); 198 | 199 | docElem.insertBefore( body, docElem.firstChild ); 200 | 201 | ret = div.offsetWidth; 202 | 203 | if( fakeUsed ){ 204 | docElem.removeChild( body ); 205 | } 206 | else { 207 | body.removeChild( div ); 208 | } 209 | 210 | //also update eminpx before returning 211 | ret = eminpx = parseFloat(ret); 212 | 213 | return ret; 214 | }, 215 | 216 | //cached container for 1em value, populated the first time it's needed 217 | eminpx, 218 | 219 | //enable/disable styles 220 | applyMedia = function( fromResize ){ 221 | var name = "clientWidth", 222 | docElemProp = docElem[ name ], 223 | currWidth = doc.compatMode === "CSS1Compat" && docElemProp || doc.body[ name ] || docElemProp, 224 | styleBlocks = {}, 225 | lastLink = links[ links.length-1 ], 226 | now = (new Date()).getTime(); 227 | 228 | //throttle resize calls 229 | if( fromResize && lastCall && now - lastCall < resizeThrottle ){ 230 | clearTimeout( resizeDefer ); 231 | resizeDefer = setTimeout( applyMedia, resizeThrottle ); 232 | return; 233 | } 234 | else { 235 | lastCall = now; 236 | } 237 | 238 | for( var i in mediastyles ){ 239 | var thisstyle = mediastyles[ i ], 240 | min = thisstyle.minw, 241 | max = thisstyle.maxw, 242 | minnull = min === null, 243 | maxnull = max === null, 244 | em = "em"; 245 | 246 | if( !!min ){ 247 | min = parseFloat( min ) * ( min.indexOf( em ) > -1 ? ( eminpx || getEmValue() ) : 1 ); 248 | } 249 | if( !!max ){ 250 | max = parseFloat( max ) * ( max.indexOf( em ) > -1 ? ( eminpx || getEmValue() ) : 1 ); 251 | } 252 | 253 | // if there's no media query at all (the () part), or min or max is not null, and if either is present, they're true 254 | if( !thisstyle.hasquery || ( !minnull || !maxnull ) && ( minnull || currWidth >= min ) && ( maxnull || currWidth <= max ) ){ 255 | if( !styleBlocks[ thisstyle.media ] ){ 256 | styleBlocks[ thisstyle.media ] = []; 257 | } 258 | styleBlocks[ thisstyle.media ].push( rules[ thisstyle.rules ] ); 259 | } 260 | } 261 | 262 | //remove any existing respond style element(s) 263 | for( var i in appendedEls ){ 264 | if( appendedEls[ i ] && appendedEls[ i ].parentNode === head ){ 265 | head.removeChild( appendedEls[ i ] ); 266 | } 267 | } 268 | 269 | //inject active styles, grouped by media type 270 | for( var i in styleBlocks ){ 271 | var ss = doc.createElement( "style" ), 272 | css = styleBlocks[ i ].join( "\n" ); 273 | 274 | ss.type = "text/css"; 275 | ss.media = i; 276 | 277 | //originally, ss was appended to a documentFragment and sheets were appended in bulk. 278 | //this caused crashes in IE in a number of circumstances, such as when the HTML element had a bg image set, so appending beforehand seems best. Thanks to @dvelyk for the initial research on this one! 279 | head.insertBefore( ss, lastLink.nextSibling ); 280 | 281 | if ( ss.styleSheet ){ 282 | ss.styleSheet.cssText = css; 283 | } 284 | else { 285 | ss.appendChild( doc.createTextNode( css ) ); 286 | } 287 | 288 | //push to appendedEls to track for later removal 289 | appendedEls.push( ss ); 290 | } 291 | }, 292 | //tweaked Ajax functions from Quirksmode 293 | ajax = function( url, callback ) { 294 | var req = xmlHttp(); 295 | if (!req){ 296 | return; 297 | } 298 | req.open( "GET", url, true ); 299 | req.onreadystatechange = function () { 300 | if ( req.readyState != 4 || req.status != 200 && req.status != 304 ){ 301 | return; 302 | } 303 | callback( req.responseText ); 304 | } 305 | if ( req.readyState == 4 ){ 306 | return; 307 | } 308 | req.send( null ); 309 | }, 310 | //define ajax obj 311 | xmlHttp = (function() { 312 | var xmlhttpmethod = false; 313 | try { 314 | xmlhttpmethod = new XMLHttpRequest(); 315 | } 316 | catch( e ){ 317 | xmlhttpmethod = new ActiveXObject( "Microsoft.XMLHTTP" ); 318 | } 319 | return function(){ 320 | return xmlhttpmethod; 321 | }; 322 | })(); 323 | 324 | //translate CSS 325 | ripCSS(); 326 | 327 | //expose update for re-running respond later on 328 | respond.update = ripCSS; 329 | 330 | //adjust on resize 331 | function callMedia(){ 332 | applyMedia( true ); 333 | } 334 | if( win.addEventListener ){ 335 | win.addEventListener( "resize", callMedia, false ); 336 | } 337 | else if( win.attachEvent ){ 338 | win.attachEvent( "onresize", callMedia ); 339 | } 340 | })(this); 341 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Scripts/respond.min.js: -------------------------------------------------------------------------------- 1 | /* NUGET: BEGIN LICENSE TEXT 2 | * 3 | * Microsoft grants you the right to use these script files for the sole 4 | * purpose of either: (i) interacting through your browser with the Microsoft 5 | * website or online service, subject to the applicable licensing or use 6 | * terms; or (ii) using the files as included with a Microsoft product subject 7 | * to that product's license terms. Microsoft reserves all other rights to the 8 | * files not expressly granted by Microsoft, whether by implication, estoppel 9 | * or otherwise. Insofar as a script file is dual licensed under GPL, 10 | * Microsoft neither took the code under GPL nor distributes it thereunder but 11 | * under the terms set out in this paragraph. All notices and licenses 12 | * below are for informational purposes only. 13 | * 14 | * NUGET: END LICENSE TEXT */ 15 | /*! matchMedia() polyfill - Test a CSS media type/query in JS. Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas. Dual MIT/BSD license */ 16 | /*! NOTE: If you're already including a window.matchMedia polyfill via Modernizr or otherwise, you don't need this part */ 17 | window.matchMedia=window.matchMedia||(function(e,f){var c,a=e.documentElement,b=a.firstElementChild||a.firstChild,d=e.createElement("body"),g=e.createElement("div");g.id="mq-test-1";g.style.cssText="position:absolute;top:-100em";d.style.background="none";d.appendChild(g);return function(h){g.innerHTML='­';a.insertBefore(d,b);c=g.offsetWidth==42;a.removeChild(d);return{matches:c,media:h}}})(document); 18 | 19 | /*! Respond.js v1.2.0: min/max-width media query polyfill. (c) Scott Jehl. MIT/GPLv2 Lic. j.mp/respondjs */ 20 | (function(e){e.respond={};respond.update=function(){};respond.mediaQueriesSupported=e.matchMedia&&e.matchMedia("only all").matches;if(respond.mediaQueriesSupported){return}var w=e.document,s=w.documentElement,i=[],k=[],q=[],o={},h=30,f=w.getElementsByTagName("head")[0]||s,g=w.getElementsByTagName("base")[0],b=f.getElementsByTagName("link"),d=[],a=function(){var D=b,y=D.length,B=0,A,z,C,x;for(;B-1,minw:F.match(/\(min\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:F.match(/\(max\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}}j()},l,r,v=function(){var z,A=w.createElement("div"),x=w.body,y=false;A.style.cssText="position:absolute;font-size:1em;width:1em";if(!x){x=y=w.createElement("body");x.style.background="none"}x.appendChild(A);s.insertBefore(x,s.firstChild);z=A.offsetWidth;if(y){s.removeChild(x)}else{x.removeChild(A)}z=p=parseFloat(z);return z},p,j=function(I){var x="clientWidth",B=s[x],H=w.compatMode==="CSS1Compat"&&B||w.body[x]||B,D={},G=b[b.length-1],z=(new Date()).getTime();if(I&&l&&z-l-1?(p||v()):1)}if(!!J){J=parseFloat(J)*(J.indexOf(y)>-1?(p||v()):1)}if(!K.hasquery||(!A||!L)&&(A||H>=C)&&(L||H<=J)){if(!D[K.media]){D[K.media]=[]}D[K.media].push(k[K.rules])}}for(var E in q){if(q[E]&&q[E].parentNode===f){f.removeChild(q[E])}}for(var E in D){var M=w.createElement("style"),F=D[E].join("\n");M.type="text/css";M.media=E;f.insertBefore(M,G.nextSibling);if(M.styleSheet){M.styleSheet.cssText=F}else{M.appendChild(w.createTextNode(F))}q.push(M)}},n=function(x,z){var y=c();if(!y){return}y.open("GET",x,true);y.onreadystatechange=function(){if(y.readyState!=4||y.status!=200&&y.status!=304){return}z(y.responseText)};if(y.readyState==4){return}y.send(null)},c=(function(){var x=false;try{x=new XMLHttpRequest()}catch(y){x=new ActiveXObject("Microsoft.XMLHTTP")}return function(){return x}})();a();respond.update=a;function t(){j(true)}if(e.addEventListener){e.addEventListener("resize",t,false)}else{if(e.attachEvent){e.attachEvent("onresize",t)}}})(this); -------------------------------------------------------------------------------- /OneDriveDataRobot/Startup.cs: -------------------------------------------------------------------------------- 1 | using Owin; 2 | 3 | namespace OneDriveDataRobot 4 | { 5 | public partial class Startup 6 | { 7 | public void Configuration(IAppBuilder app) 8 | { 9 | ConfigureAuth(app); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Utils/ActionHelpers.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * OneDrive Data Robot - Sample Code 3 | * Copyright (c) Microsoft Corporation 4 | * All rights reserved. 5 | * 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | * this software and associated documentation files (the ""Software""), to deal in 10 | * the Software without restriction, including without limitation the rights to use, 11 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 12 | * Software, and to permit persons to whom the Software is furnished to do so, 13 | * subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | namespace OneDriveDataRobot 27 | { 28 | using System; 29 | using System.Web; 30 | 31 | public static class ActionHelpers 32 | { 33 | 34 | /// 35 | /// Trims the API URL at /drive to return the base URL we can use to build other API calls 36 | /// 37 | /// 38 | /// 39 | public static string ParseBaseUrl(string oneDriveApiSourceUrl) 40 | { 41 | var trimPoint = oneDriveApiSourceUrl.IndexOf("/drive"); 42 | return oneDriveApiSourceUrl.Substring(0, trimPoint); 43 | } 44 | 45 | 46 | public static string BuildApiUrl(string baseUrl, string driveId, string itemId, string extra = "") 47 | { 48 | return $"{baseUrl}/drives/{driveId}/items/{itemId}/{extra}"; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Utils/AzureStorage/AzureTableContext.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * OneDrive Data Robot - Sample Code 3 | * Copyright (c) Microsoft Corporation 4 | * All rights reserved. 5 | * 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | * this software and associated documentation files (the ""Software""), to deal in 10 | * the Software without restriction, including without limitation the rights to use, 11 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 12 | * Software, and to permit persons to whom the Software is furnished to do so, 13 | * subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | namespace OneDriveDataRobot.Utils 27 | { 28 | using System; 29 | using Microsoft.Azure; 30 | using Microsoft.WindowsAzure.Storage; 31 | using Microsoft.WindowsAzure.Storage.Table; 32 | 33 | public class AzureTableContext 34 | { 35 | #region Static Default Member 36 | private static AzureTableContext defaultInstance; 37 | public static AzureTableContext Default 38 | { 39 | get 40 | { 41 | if (defaultInstance == null) 42 | { 43 | defaultInstance = new AzureTableContext(); 44 | } 45 | return defaultInstance; 46 | } 47 | } 48 | #endregion 49 | 50 | private readonly CloudTableClient client; 51 | public readonly CloudTable TokenCacheTable; 52 | public readonly CloudTable SyncStateTable; 53 | 54 | 55 | private CloudStorageAccount StorageAccount 56 | { 57 | get { return CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting("StorageConnectionString")); } 58 | } 59 | 60 | private AzureTableContext() 61 | { 62 | client = this.StorageAccount.CreateCloudTableClient(); 63 | 64 | TokenCacheTable = client.GetTableReference("tokenCache"); 65 | TokenCacheTable.CreateIfNotExists(); 66 | 67 | SyncStateTable = client.GetTableReference("syncState"); 68 | SyncStateTable.CreateIfNotExists(); 69 | } 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /OneDriveDataRobot/Utils/AzureStorage/AzureTableTokenCache.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * OneDrive Data Robot - Sample Code 3 | * Copyright (c) Microsoft Corporation 4 | * All rights reserved. 5 | * 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | * this software and associated documentation files (the ""Software""), to deal in 10 | * the Software without restriction, including without limitation the rights to use, 11 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 12 | * Software, and to permit persons to whom the Software is furnished to do so, 13 | * subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | namespace OneDriveDataRobot.Utils 27 | { 28 | using System; 29 | using Microsoft.IdentityModel.Clients.ActiveDirectory; 30 | using Microsoft.WindowsAzure.Storage.Table; 31 | 32 | /// 33 | /// ADAL TokenCache implementation that stores the token cache in the provided Azure CloudTable instance. 34 | /// This class is shared between the Azure Function and the bootstrap project. 35 | /// 36 | public class AzureTableTokenCache : TokenCache 37 | { 38 | private readonly string signInUserId; 39 | private readonly CloudTable tokenCacheTable; 40 | 41 | private TokenCacheEntity cachedEntity; // data entity stored in the Azure Table 42 | 43 | public AzureTableTokenCache(string userId, CloudTable cacheTable) 44 | { 45 | signInUserId = userId; 46 | tokenCacheTable = cacheTable; 47 | 48 | this.AfterAccess = AfterAccessNotification; 49 | 50 | cachedEntity = ReadFromTableStorage(); 51 | if (null != cachedEntity) 52 | { 53 | Deserialize(cachedEntity.CacheBits); 54 | } 55 | } 56 | 57 | private TokenCacheEntity ReadFromTableStorage() 58 | { 59 | TableOperation retrieve = TableOperation.Retrieve(TokenCacheEntity.PartitionKeyValue, signInUserId); 60 | TableResult results = tokenCacheTable.Execute(retrieve); 61 | return (TokenCacheEntity)results.Result; 62 | } 63 | 64 | private void AfterAccessNotification(TokenCacheNotificationArgs args) 65 | { 66 | if (this.HasStateChanged) 67 | { 68 | if (cachedEntity == null) 69 | { 70 | cachedEntity = new TokenCacheEntity(); 71 | } 72 | cachedEntity.RowKey = signInUserId; 73 | cachedEntity.CacheBits = Serialize(); 74 | cachedEntity.LastWrite = DateTime.Now; 75 | 76 | TableOperation insert = TableOperation.InsertOrReplace(cachedEntity); 77 | tokenCacheTable.Execute(insert); 78 | 79 | this.HasStateChanged = false; 80 | } 81 | } 82 | 83 | /// 84 | /// Representation of the data stored in the Azure Table 85 | /// 86 | private class TokenCacheEntity : TableEntity 87 | { 88 | public const string PartitionKeyValue = "tokenCache"; 89 | public TokenCacheEntity() 90 | { 91 | this.PartitionKey = PartitionKeyValue; 92 | } 93 | 94 | public byte[] CacheBits { get; set; } 95 | public DateTime LastWrite { get; set; } 96 | } 97 | 98 | } 99 | } -------------------------------------------------------------------------------- /OneDriveDataRobot/Utils/HttpHelper.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * OneDrive Data Robot - Sample Code 3 | * Copyright (c) Microsoft Corporation 4 | * All rights reserved. 5 | * 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | * this software and associated documentation files (the ""Software""), to deal in 10 | * the Software without restriction, including without limitation the rights to use, 11 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 12 | * Software, and to permit persons to whom the Software is furnished to do so, 13 | * subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | namespace OneDriveDataRobot 27 | { 28 | using Models; 29 | using System; 30 | using System.Collections.Generic; 31 | using System.IO; 32 | using System.Net.Http; 33 | using System.Threading.Tasks; 34 | 35 | public class HttpHelper 36 | { 37 | private const int MAX_UPLOAD_SIZE = 4 * 1024 * 1024; 38 | private HttpClient httpClient = new HttpClient(); 39 | public static readonly HttpHelper Default = new HttpHelper(); 40 | 41 | 42 | 43 | #region Metadata operations 44 | /// 45 | /// Update an existing item with the changes specified in patchBody. 46 | /// 47 | public async Task PatchAsync(object patchBody, string itemUrl, string accessToken) where T : class 48 | { 49 | var forcePatch = new Dictionary(); 50 | forcePatch["X-HTTP-Method"] = "PATCH"; 51 | 52 | return await PostAsync(patchBody, itemUrl, accessToken, forcePatch); 53 | } 54 | 55 | /// 56 | /// Post an object to a URL and return the response converted back into an object. 57 | /// 58 | public async Task PostAsync(object body, string itemUrl, string accessToken, Dictionary additionalHeaders = null) where T : class 59 | { 60 | var requestMessage = new HttpRequestMessage(HttpMethod.Post, itemUrl); 61 | if (additionalHeaders != null) 62 | { 63 | foreach(var header in additionalHeaders) 64 | { 65 | requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value); 66 | } 67 | } 68 | if (!string.IsNullOrEmpty(accessToken)) 69 | { 70 | requestMessage.Headers.TryAddWithoutValidation("Authorization", "Bearer " + accessToken); 71 | } 72 | 73 | if (null != body) 74 | { 75 | var contentText = Newtonsoft.Json.JsonConvert.SerializeObject(body); 76 | var content = new StringContent(contentText, System.Text.Encoding.UTF8, "application/json"); 77 | requestMessage.Content = content; 78 | } 79 | 80 | var responseMessage = await httpClient.SendAsync(requestMessage); 81 | responseMessage.EnsureSuccessStatusCode(); 82 | 83 | return await ParseJsonFromResponseAsync(responseMessage); 84 | } 85 | 86 | 87 | /// 88 | /// Return a new object of type T by performing a GET on the URL and converting to an object. 89 | /// 90 | public async Task GetAsync(string requestUri, string accessToken) where T : class 91 | { 92 | var requestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri); 93 | 94 | if (!string.IsNullOrEmpty(accessToken)) 95 | { 96 | requestMessage.Headers.TryAddWithoutValidation("Authorization", "Bearer " + accessToken); 97 | } 98 | 99 | var responseMessage = await httpClient.SendAsync(requestMessage); 100 | 101 | responseMessage.EnsureSuccessStatusCode(); 102 | return await ParseJsonFromResponseAsync(responseMessage); 103 | } 104 | 105 | #endregion 106 | 107 | #region Utility functions 108 | 109 | /// 110 | /// Convert the contents of an HttpResponseMessage into an object by using a JSON parser. 111 | /// 112 | private async Task ParseJsonFromResponseAsync(HttpResponseMessage response) 113 | { 114 | if (response.Content.Headers.ContentType.MediaType.ToLower() != "application/json") 115 | { 116 | throw new InvalidOperationException($"MediaType for the response message was {response.Content.Headers.ContentType.MediaType} instead of \"application/json\"."); 117 | } 118 | 119 | var responseData = await response.Content.ReadAsStringAsync(); 120 | return Newtonsoft.Json.JsonConvert.DeserializeObject(responseData); 121 | } 122 | #endregion 123 | 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Utils/SettingsHelper.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * OneDrive Data Robot - Sample Code 3 | * Copyright (c) Microsoft Corporation 4 | * All rights reserved. 5 | * 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | * this software and associated documentation files (the ""Software""), to deal in 10 | * the Software without restriction, including without limitation the rights to use, 11 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 12 | * Software, and to permit persons to whom the Software is furnished to do so, 13 | * subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | namespace OneDriveDataRobot 27 | { 28 | using System.Configuration; 29 | 30 | 31 | public class SettingsHelper 32 | { 33 | private static string _clientId = ConfigurationManager.AppSettings["ida:ClientId"]; 34 | private static string _appKey = ConfigurationManager.AppSettings["ida:ClientSecret"] ?? ConfigurationManager.AppSettings["ida:AppKey"]; 35 | 36 | private static string _graphResourceId = ConfigurationManager.AppSettings["ida:GraphResourceId"]; 37 | private static string _authority = ConfigurationManager.AppSettings["ida:AADInstance"]; 38 | 39 | private static string _consentUri = _authority + "oauth2/authorize?response_type=code&client_id={0}&resource={1}&redirect_uri={2}"; 40 | private static string _adminConsentUri = _authority + "oauth2/authorize?response_type=code&client_id={0}&resource={1}&redirect_uri={2}&prompt={3}"; 41 | 42 | private static string _notificationUrl = ConfigurationManager.AppSettings["ida:NotificationUrl"]; 43 | 44 | public static string ClientId 45 | { 46 | get 47 | { 48 | return _clientId; 49 | } 50 | } 51 | 52 | public static string AppKey 53 | { 54 | get 55 | { 56 | return _appKey; 57 | } 58 | } 59 | 60 | public static string Authority 61 | { 62 | get 63 | { 64 | return _authority; 65 | } 66 | } 67 | 68 | public static string MicrosoftGraphBaseUrl 69 | { 70 | get 71 | { 72 | return _graphResourceId; 73 | } 74 | } 75 | 76 | public static string NotificationUrl { get { return _notificationUrl; } } 77 | } 78 | } -------------------------------------------------------------------------------- /OneDriveDataRobot/Utils/StoredSubscriptionState.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * OneDrive Data Robot - Sample Code 3 | * Copyright (c) Microsoft Corporation 4 | * All rights reserved. 5 | * 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | * this software and associated documentation files (the ""Software""), to deal in 10 | * the Software without restriction, including without limitation the rights to use, 11 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 12 | * Software, and to permit persons to whom the Software is furnished to do so, 13 | * subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | namespace OneDriveDataRobot.Utils 27 | { 28 | using Microsoft.WindowsAzure.Storage.Table; 29 | using System; 30 | using System.Linq; 31 | 32 | /// 33 | /// Persists information about a subscription, userId, and deltaToken state. This class is shared between the Azure Function and the bootstrap project 34 | /// 35 | public class StoredSubscriptionState : TableEntity 36 | { 37 | public StoredSubscriptionState() 38 | { 39 | this.PartitionKey = "AAA"; 40 | } 41 | 42 | public string SignInUserId { get; set; } 43 | public string LastDeltaToken { get; set; } 44 | public string SubscriptionId { get; set; } 45 | 46 | public static StoredSubscriptionState CreateNew(string subscriptionId) 47 | { 48 | var newState = new StoredSubscriptionState(); 49 | newState.RowKey = subscriptionId; 50 | newState.SubscriptionId = subscriptionId; 51 | return newState; 52 | } 53 | 54 | public void Insert(CloudTable table) 55 | { 56 | TableOperation insert = TableOperation.InsertOrReplace(this); 57 | table.Execute(insert); 58 | } 59 | 60 | public static StoredSubscriptionState Open(string subscriptionId, CloudTable table) 61 | { 62 | try 63 | { 64 | TableOperation retrieve = TableOperation.Retrieve("AAA", subscriptionId); 65 | TableResult results = table.Execute(retrieve); 66 | return (StoredSubscriptionState)results.Result; 67 | } catch 68 | { 69 | return null; 70 | } 71 | } 72 | 73 | public static StoredSubscriptionState FindUser(string userId, CloudTable table) 74 | { 75 | try 76 | { 77 | var partitionFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, "AAA"); 78 | var userIdFilter = TableQuery.GenerateFilterCondition("SignInUserId", QueryComparisons.Equal, userId); 79 | string filter = TableQuery.CombineFilters(partitionFilter, TableOperators.And, userIdFilter); 80 | 81 | var query = new TableQuery().Where(filter).Take(1); 82 | var matchingEntry = table.ExecuteQuery(query).FirstOrDefault(); 83 | return matchingEntry; 84 | } 85 | catch (Exception ex) 86 | { 87 | System.Diagnostics.Debug.WriteLine($"Error while finding existing user subscription: {ex.Message}."); 88 | } 89 | return null; 90 | } 91 | 92 | internal void Delete(CloudTable syncStateTable) 93 | { 94 | try 95 | { 96 | TableOperation remove = TableOperation.Delete(this); 97 | syncStateTable.Execute(remove); 98 | } catch { } 99 | } 100 | } 101 | 102 | } -------------------------------------------------------------------------------- /OneDriveDataRobot/Views/Account/SignOutCallback.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Sign Out"; 3 | } 4 |

    @ViewBag.Title.

    5 |

    You have successfully signed out.

    6 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Views/Home/Error.cshtml: -------------------------------------------------------------------------------- 1 |  2 | @{ 3 | ViewBag.Title = "Error"; 4 | } 5 | 6 |

    Error

    7 | 8 |
    @ViewBag.Message
    9 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model OneDriveDataRobot.Models.HomeModel 2 | @{ 3 | ViewBag.Title = "Home Page"; 4 | } 5 | 6 |
    7 |
    8 |
    9 |
    10 |

    OneDrive Data Robot

    11 |

    Sample code project using Microsoft Graph

    12 |
    13 |
     
    14 |
     
    15 |
    16 |
    17 |
    18 | 19 |
    20 |
    21 |

    Getting started

    22 |

    23 | This sample code project provides an example of using Azure Functions to respond to webhook notifications generated by OneDrive users. 24 | The data robot can monitor changes to files, and respond in near real-time based to in context requests for more information inside an Excel co-authoring session. 25 | For more information, see the sample code source: onedrive-data-robot-azure-function. 26 |

    27 |
    28 |
    29 | @if (!string.IsNullOrEmpty(Model.SignInName)) 30 | { 31 |

    Signed In

    32 |

    Hello @Model.DisplayName! I'm the OneDrive data robot.

    33 |

    34 |

    Activate 35 | Disable

    36 | } 37 | else 38 | { 39 |

    Sign-In

    40 |

    @Html.ActionLink("Sign in", "SignIn", "Account") to get started.

    41 | } 42 |
    43 |
    44 | 45 | 85 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model System.Web.Mvc.HandleErrorInfo 2 | 3 | @{ 4 | ViewBag.Title = "Error"; 5 | } 6 | 7 |

    Error.

    8 |

    An error occurred while processing your request.

    9 | 10 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @ViewBag.Title - Data Robot Bootstrap 7 | @Styles.Render("~/Content/css") 8 | @Scripts.Render("~/bundles/modernizr") 9 | 10 | 11 | 12 | @if (ViewBag.HideNavigationBar != true) 13 | { 14 | 32 | } 33 |
    34 | @RenderBody() 35 |
    36 | 37 | @Scripts.Render("~/bundles/jquery") 38 | @Scripts.Render("~/bundles/bootstrap") 39 | @RenderSection("scripts", required: false) 40 | 41 | 42 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Views/Shared/_LayoutAsyncJob.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @ViewBag.Title - Markdown Editor 7 | @Styles.Render("~/Content/css") 8 | @Styles.Render("~/Content/LoadingSpinner.css") 9 | @Scripts.Render("~/bundles/modernizr") 10 | @Scripts.Render("~/bundles/jquery") 11 | 12 | 13 | 14 |
    15 | @RenderBody() 16 |
    17 | 18 | 19 | @Scripts.Render("~/bundles/bootstrap") 20 | @RenderSection("scripts", required: false) 21 | 22 | 23 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Views/Shared/_LayoutFileHandler.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @ViewBag.Title - Markdown Editor 7 | @Styles.Render("~/Content/css") 8 | @Scripts.Render("~/bundles/modernizr") 9 | @Scripts.Render("~/bundles/jquery") 10 | @RenderSection("Head", required: false) 11 | 12 | 13 | @if (ViewBag.HideNavigationBar != true) 14 | { 15 | 32 | } 33 |
    34 | @RenderBody() 35 |
    36 | 37 | 38 | @Scripts.Render("~/bundles/bootstrap") 39 | @RenderSection("scripts", required: false) 40 | 41 | 42 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Views/Shared/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @if (Request.IsAuthenticated) 2 | { 3 | 4 | 12 | 13 | } 14 | else 15 | { 16 | 19 | } 20 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Views/Web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 |
    7 |
    8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "~/Views/Shared/_Layout.cshtml"; 3 | } 4 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Web.Debug.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 17 | 18 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Web.Release.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 17 | 18 | 19 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /OneDriveDataRobot/Web.config: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 |
    10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /OneDriveDataRobot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneDrive/onedrive-data-robot-azure-function/e8e4d89866799e7e1cf6284b02f15bcaa5ffbad8/OneDriveDataRobot/favicon.ico -------------------------------------------------------------------------------- /OneDriveDataRobot/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneDrive/onedrive-data-robot-azure-function/e8e4d89866799e7e1cf6284b02f15bcaa5ffbad8/OneDriveDataRobot/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /OneDriveDataRobot/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneDrive/onedrive-data-robot-azure-function/e8e4d89866799e7e1cf6284b02f15bcaa5ffbad8/OneDriveDataRobot/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /OneDriveDataRobot/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneDrive/onedrive-data-robot-azure-function/e8e4d89866799e7e1cf6284b02f15bcaa5ffbad8/OneDriveDataRobot/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /OneDriveDataRobot/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneDrive/onedrive-data-robot-azure-function/e8e4d89866799e7e1cf6284b02f15bcaa5ffbad8/OneDriveDataRobot/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /OneDriveDataRobot/images/markdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneDrive/onedrive-data-robot-azure-function/e8e4d89866799e7e1cf6284b02f15bcaa5ffbad8/OneDriveDataRobot/images/markdown.png -------------------------------------------------------------------------------- /OneDriveDataRobot/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OneDrive Data Robot Azure Function Sample Code 2 | 3 | **If you are looking for the sample from Ignite 2017** that used the Bing Speech API and the List API together, the URL posted was incorrect. That sample code is available here: https://github.com/OneDrive/document-library-audio-transcription-robot-sample 4 | 5 | This project provides an example implementation for connecting Azure Functions to OneDrive to enable your solution to react to changes in files in OneDrive nearly instantly. 6 | 7 | The project consists of two parts: 8 | 9 | * An [Azure Function](https://azure.microsoft.com/services/functions/) definition that handles the processing of webhook notifications and the resulting work from those notifications 10 | * An ASP.NET MVC application that activates and deactivates the OneDrive Data Robot for a signed in user. 11 | 12 | In this scenario, the benefit of using Azure Function is that the load is required by the data robot is dynamic and hard to predict. 13 | Instead of scaling out an entire web application to handle the load, Azure Functions can scale dynamically based on the load required at any given time. 14 | This provides a cost-savings measure for hosting the application while still ensuring high performance results. 15 | 16 | ## Getting Started 17 | 18 | To get started with the sample, you need to complete the following steps: 19 | 20 | 1. Register a new application with Azure Active Directory, generate an app password, and provide a redirect URI for the application. 21 | 2. Create a new Azure Function and upload the code files in the **AzureFunction** folder into the function's definition. 22 | 3. Run the sample project and sign-in with your Office 365 account and activate the data robot by clicking the **Activate** button. 23 | 4. Navigate to OneDrive and modify a file (see below for details). 24 | 5. Watch the data robot update the file automatically. 25 | 26 | ### Register a new application 27 | 28 | To register a new application with Azure Active Directory, log into the [Azure Portal](https://portal.azure.com). 29 | 30 | After logging into the Azure Portal, follow these steps to register the sample application: 31 | 32 | 1. Navigate to the **Azure Active Directory** module. 33 | 2. Select **App registrations** and click **New application registration**. 34 | 1. Type the name of your file handler application. 35 | 2. Ensure **Application Type** is set to **Web app / API** 36 | 3. Enter a sign-on URL for your application, for this sample use `https://localhost:44382`. 37 | 4. Click **Create** to create the app. 38 | 3. After the app has been created successfully, select the app from the list of applications. It should be at the bottom of the list. 39 | 4. Copy the **Application ID** for the app you registered and paste it into two places: 40 | * In the [`Web.config`](OneDriveDataRobot/Web.config) file on the line: `` 41 | * In the [`run.csx`](AzureFunction/run.csx) file on the line: `private const string idaClientId = "[ClientId]";` 42 | 5. Configure the application settings for this sample: 43 | 1. Select **Reply URLs** and ensure that `https://localhost:44382` is listed. 44 | 2. Select **Required Permissions** and then **Add**. 45 | 3. Select **Select an API** and then choose **Microsoft Graph** and click **Select**. 46 | 4. Find the permission **Have full access to user files** and check the box next to it, then click **Select**, and then **Done**. 47 | 5. Select **Keys** and generate a new application key by entering a description for the key, selecting a duration, and then click **Save**. Copy the value of the displayed key since it will only be displayed once. Paste it into two places: 48 | * In the `Web.config` file on the line: `` 49 | * In the `run.csx` file on the line: `private const string idaClientSecret = "[ClientSecret]"` 50 | 51 | ### Create an Azure Function 52 | 53 | To create the Azure Function portion of this sample, you will need to be logged into the [Azure Portal](https://portal.azure.com). 54 | 55 | 1. Click **New** and select **Function App** in the Azure Portal. 56 | 2. Enter a name for your function app, such as datarobot99. The name of your function app must be unique, so you'll need to modify the name to find a unique one. 57 | 3. Select your existing Azure subscription, desired resource group, hosting plan, and location for the Azure Function app. 58 | 4. Choose to create a new storage for this azure function, and provide a unique name for the storage. 59 | 5. Click create to have the Azure Portal create everything for you. 60 | 61 | After the required components have been provisioned, click on the **App Services** module in the portal and find the Azure Function App we just created. 62 | 63 | #### Register a new Azure Function 64 | 65 | To create a new Azure Function application and setup a function for this project: 66 | 67 | 1. Click the `+` next to **Functions**. 68 | 2. Select the **Webhook + API** scenario, choose **CSharp** as the language, and then **Create this function**. 69 | 3. Copy the code from [`run.csx`](AzureFunction/run.csx) and paste it into the code editor and then click **Save**. 70 | 4. On the right side, select **View files** to expand the files that make up this function. 71 | 5. Click **Upload** and then navigate to the [`project.json`](AzureFunction/project.json) file in the AzureFunction folder and upload it. This file configures the dependencies for the Azure Function, and will add the Azure authentication library and the Microsoft Graph SDK to the function project. 72 | 6. Click **Integrate** on the left side, under the **HttpTriggerCSharp1** function name (or if your function has a different name, select **Integrate** under that). Configure your function accordingly: 73 | 1. Select **HTTP (req)** under **Triggers** and configure the values accordingly then click **Save**. 74 | * **Allowed HTTP methods:** Selected methods 75 | * **Mode:** Standard 76 | * **Request parameter name:** req 77 | * **Route template:** default value (empty) 78 | * **Authorization level:** Anonymous 79 | * **Selected HTTP methods:** Uncheck everything except POST 80 | 2. Select **New Input** on the Inputs column and choose **Azure Table Storage**. Set the parameters accordingly: 81 | * **Table parameter name:** syncStateTable 82 | * **Table name:** syncState 83 | * **Storage account connection:** Click **new** and then connect it to the storage connection you created (named something like datarobot99ae20). 84 | * Leave the other parameters with their default values, and then click **Save**. 85 | 3. Select **New Input** again, and again choose **Azure Table Storage**. Set the parameters accordingly: 86 | * **Table parameter name:** tokenCacheTable 87 | * **Table name:** tokenCache 88 | * **Storage account connection:** Choose the existing storage connection from step 2, something like datarobot99ae20_STORAGE. 89 | * Leave the other parameters with their default values, and then click **Save**. 90 | 7. Click back on the function name in the left navigation column to bring up the code editor. 91 | 8. Click **Get function URL** and copy the URL for this function. 92 | * Paste this value into the `Web.config` file on the line: `` 93 | 10. Navigate to the [Azure Portal](https://portal.azure.com) and go to **Storage Accounts** on the left-hand navigation bar. 94 | 1. Click on the storage account created in step 6.2 (e.g., datarobot99ae20). 95 | 2. Click on **Access keys** (under **Settings**). 96 | 3. Copy one of the **Connection String** values under **Default keys**. 97 | 4. Paste this value into the `Web.config` file on the line: ``. 98 | 99 | ### Run the project and sign-in 100 | 101 | Now that everything is properly configured, open the web project in Visual Studio and press F5 launch the project in the debugger. 102 | 103 | 1. Sign in to the data robot project and authorize the application to have access to the data in your OneDrive. 104 | 2. After you authorize the data robot, you should see a Subscription ID and Expiration date/time. 105 | These values are returned from the Microsoft Graph webhook notification subscription that powers the data robot. 106 | By default the expiration time is 3 days from when the robot is activated. 107 | 108 | If no value is returned, check to ensure that the notification URL is correct in the `Web.config` file, and verify in the Azure Function console that you are seeing a request successfully processed by your function code. 109 | 110 | ### Navigate to OneDrive and try out the data robot 111 | 112 | This sample data robot uses a Web API to insert live stock quotes into Excel files while you are editing them. 113 | To invoke the data robot and ask it to insert a stock quote into a cell, you can do the following: 114 | 115 | 1. Open your OneDrive ({tenant}.onedrive.com). 116 | 2. Click **New** then **Excel Workbook**. A new Excel workbook will open in the Excel web application. 117 | 3. The data robot looks for the keyword `!odbot` followed by what you are asking for: 118 | * To retrieve a stock quote, use `!odbot MSFT stock quote` where MSFT can be replaced with the stock ticker symbol of your choice. 119 | 120 | If the data robot is unable to retrieve a real stock quote, it will make one up so the demo always works. 121 | 122 | **Note:** OneDrive webhooks can take up to 5 minutes to be delivered, depending on load and other conditions. 123 | As a result, requests in the workbook may take a few minutes to be updated with real data. 124 | 125 | ## Related references 126 | 127 | For more information about Microsoft Graph API, see [Microsoft Graph](https://graph.microsoft.com). 128 | 129 | ## License 130 | 131 | See [License](LICENSE.txt) for the license agreement convering this sample code. 132 | --------------------------------------------------------------------------------