-
-
Notifications
You must be signed in to change notification settings - Fork 17
/
CollectionSettingsApi.cs
763 lines (712 loc) · 31.3 KB
/
CollectionSettingsApi.cs
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
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Globalization;
using System.Linq;
using System.Text;
using Bloom.Api;
using Bloom.Book;
using Bloom.Collection;
using L10NSharp;
using Newtonsoft.Json;
using SIL.Code;
using SIL.IO;
using SIL.Progress;
namespace Bloom.web.controllers
{
/// <summary>
/// Used by the settings dialog (currently just the EnterpriseSettings tab) and various places that need to know
/// if Enterprise is enabled or not.
/// </summary>
public class CollectionSettingsApi
{
public const string kApiUrlPart = "settings/";
// These options must match the strings used in accessibileImage.tsx
public enum EnterpriseStatus
{
None,
Community,
Subscription
}
// These are static so they can easily be set by the collection settings dialog using SetSubscriptionCode()
private static string SubscriptionCode { get; set; }
private static DateTime _enterpriseExpiry = DateTime.MinValue;
// True if the part of the subscription code that identifies the branding is one this version of Bloom knows about
private static bool _knownBrandingInSubscriptionCode = false;
private static EnterpriseStatus _enterpriseStatus;
// While displaying the CollectionSettingsDialog, which is what this API mainly exists to serve
// we keep a reference to it here so pending settings can be updated there.
public static CollectionSettingsDialog DialogBeingEdited;
// This is set when we are running the collection settings dialog in a special mode where it is
// brought up automatically to inform the user that a previously used branding name is invalid.
// (It might be a legacy branding from an earlier Bloom that did not require a validation code,
// or one whose code has expired, or conceivably an invalid code, though I think that can only
// happen by hand-editing the .bloomCollection file.)
public static bool FixEnterpriseSubscriptionCodeMode;
// When in FixEnterpriseSubscriptionCodeMode, and we think it is a legacy branding problem
// (because the subscription code is missing or incomplete rather than wrong or expired or unknown),
// this keeps track of the branding the collection file specified but which was not validated by a current code.
public static string LegacyBrandingName { get; set; }
private readonly CollectionSettings _collectionSettings;
private readonly List<object> _numberingStyles = new List<object>();
private readonly XMatterPackFinder _xmatterPackFinder;
private readonly BookSelection _bookSelection;
public CollectionSettingsApi(
CollectionSettings collectionSettings,
XMatterPackFinder xmatterPackFinder,
BookSelection bookSelection
)
{
_collectionSettings = collectionSettings;
_xmatterPackFinder = xmatterPackFinder;
this._bookSelection = bookSelection;
SetSubscriptionCode(
_collectionSettings.SubscriptionCode,
_collectionSettings.IsSubscriptionCodeKnown(),
_collectionSettings.GetEnterpriseStatus()
);
}
private bool IsEnterpriseEnabled
{
get { return _collectionSettings.HaveEnterpriseFeatures; }
}
public void RegisterWithApiHandler(BloomApiHandler apiHandler)
{
apiHandler.RegisterEndpointHandler(
"collection/settings",
request =>
{
if (request.HttpMethod == HttpMethods.Get)
{
// Just a placeholder for the skeleton dialog for now.
request.ReplyWithJson("{}");
}
else if (request.HttpMethod == HttpMethods.Post)
{
request.PostSucceeded();
}
},
true
);
apiHandler.RegisterEndpointHandler(
kApiUrlPart + "enterpriseEnabled",
request =>
{
if (request.HttpMethod == HttpMethods.Get)
{
lock (request)
{
request.ReplyWithBoolean(IsEnterpriseEnabled);
}
}
else // post
{
System.Diagnostics.Debug.Fail(
"We shouldn't ever be using the 'post' version."
);
request.PostSucceeded();
}
},
true
);
apiHandler.RegisterEnumEndpointHandler(
kApiUrlPart + "enterpriseStatus",
request => _enterpriseStatus,
(request, status) =>
{
_enterpriseStatus = status;
if (_enterpriseStatus == EnterpriseStatus.None)
{
_knownBrandingInSubscriptionCode = true;
ResetBookshelf();
BrandingChangeHandler("Default", null);
}
else if (_enterpriseStatus == EnterpriseStatus.Community)
{
ResetBookshelf();
BrandingChangeHandler("Local-Community", null);
}
else
{
BrandingChangeHandler(
GetBrandingFromCode(SubscriptionCode),
SubscriptionCode
);
}
},
false
);
apiHandler.RegisterEndpointHandler(
kApiUrlPart + "legacyBrandingName",
request =>
{
request.ReplyWithText(LegacyBrandingName ?? "");
},
false
);
apiHandler.RegisterEndpointHandler(
kApiUrlPart + "subscriptionCode",
request =>
{
if (request.HttpMethod == HttpMethods.Get)
{
request.ReplyWithText(SubscriptionCode ?? "");
}
else // post
{
var requestData = DynamicJson.Parse(request.RequiredPostJson());
SubscriptionCode = requestData.subscriptionCode;
_enterpriseExpiry = GetExpirationDate(SubscriptionCode);
var newBranding = GetBrandingFromCode(SubscriptionCode);
var oldBranding = !string.IsNullOrEmpty(_collectionSettings.InvalidBranding)
? _collectionSettings.InvalidBranding
: "";
// If the user has entered a different subscription code then what was previously saved, we
// generally want to clear out the Bookshelf. But if the BrandingKey is the same as the old one,
// we'll leave it alone, since they probably renewed for another year or so and want to use the
// same bookshelf.
if (
SubscriptionCode != _collectionSettings.SubscriptionCode
&& newBranding != oldBranding
)
ResetBookshelf();
if (_enterpriseExpiry < DateTime.Now) // expired or invalid
{
BrandingChangeHandler("Default", null);
}
else
{
_knownBrandingInSubscriptionCode = BrandingChangeHandler(
GetBrandingFromCode(SubscriptionCode),
SubscriptionCode
);
if (!_knownBrandingInSubscriptionCode)
{
BrandingChangeHandler("Default", null); // Review: or just leave unchanged?
}
}
request.PostSucceeded();
}
},
false
);
apiHandler.RegisterEndpointHandler(
kApiUrlPart + "enterpriseSummary",
request =>
{
string branding = "";
if (_enterpriseStatus == EnterpriseStatus.Community)
branding = "Local-Community";
else if (_enterpriseStatus == EnterpriseStatus.Subscription)
branding =
_enterpriseExpiry == DateTime.MinValue
? ""
: GetBrandingFromCode(SubscriptionCode);
var html = GetSummaryHtml(branding);
request.ReplyWithText(html);
},
false
);
apiHandler.RegisterEndpointHandler(
kApiUrlPart + "enterpriseExpiry",
request =>
{
if (_enterpriseExpiry == DateTime.MinValue)
{
if (SubscriptionCodeLooksIncomplete(SubscriptionCode))
request.ReplyWithText("incomplete");
else
request.ReplyWithText("null");
}
else if (_knownBrandingInSubscriptionCode)
{
// O is ISO 8601, the only format I can find that C# ToString() can produce and JS is guaranteed to parse.
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse
request.ReplyWithText(
_enterpriseExpiry.ToString("O", CultureInfo.InvariantCulture)
);
}
else
{
request.ReplyWithText("unknown");
}
},
false
);
apiHandler.RegisterEndpointHandler(
kApiUrlPart + "hasSubscriptionFiles",
request =>
{
var haveFiles = BrandingProject.HaveFilesForBranding(
GetBrandingFromCode(SubscriptionCode)
);
if (haveFiles)
request.ReplyWithText("true");
else
request.ReplyWithText("false");
},
false
);
// Enhance: The get here has one signature {brandingProjectName, defaultBookshelf} while the post has another (defaultBookshelfId:string).
apiHandler.RegisterEndpointHandler(
kApiUrlPart + "bookShelfData",
request =>
{
if (request.HttpMethod == HttpMethods.Get)
{
var brandingProjectName = _collectionSettings.BrandingProjectKey;
var defaultBookshelfUrlKey = _collectionSettings.DefaultBookshelf;
request.ReplyWithJson(new { brandingProjectName, defaultBookshelfUrlKey });
}
else
{
// post: doesn't include the brandingProjectName, as this is not where we edit that.
var newShelf = request.RequiredPostString();
if (newShelf == "none")
newShelf = ""; // RequiredPostString won't allow us to just pass this
if (DialogBeingEdited != null)
DialogBeingEdited.PendingDefaultBookshelf = newShelf;
request.PostSucceeded();
}
},
false
);
// Calls to handle communication with new FontScriptControl on Book Making tab
apiHandler.RegisterEndpointHandler(
kApiUrlPart + "specialScriptSettings",
request =>
{
if (request.HttpMethod == HttpMethods.Get)
return; // Should be a post
// Should have a (1-based) language number.
var data = DynamicJson.Parse(request.RequiredPostJson());
var languageNumber = (int)data.languageNumber;
HandlePendingFontSettings(languageNumber);
request.PostSucceeded();
},
true
);
apiHandler.RegisterEndpointHandler(
kApiUrlPart + "setFontForLanguage",
request =>
{
if (request.HttpMethod == HttpMethods.Get)
return; // Should be a post
// Should contain a 1-based language number and a font name
var data = DynamicJson.Parse(request.RequiredPostJson());
var languageNumber = (int)data.languageNumber;
var fontName = (string)data.fontName;
UpdatePendingFontName(fontName, languageNumber);
request.PostSucceeded();
},
true
);
apiHandler.RegisterEndpointHandler(
kApiUrlPart + "currentFontData",
request =>
{
if (request.HttpMethod == HttpMethods.Post)
return; // Should be a get
// We want to return the data (languageName/fontName) for each active collection language
request.ReplyWithJson(GetLanguageData());
},
true
);
apiHandler.RegisterEndpointHandler(
kApiUrlPart + "numberingStyle",
request =>
{
if (request.HttpMethod == HttpMethods.Get)
{
// Should return all available numbering styles and the current style
request.ReplyWithJson(GetNumberingStyleData());
}
else
{
// We are receiving a pending numbering style change
var newNumberingStyle = request.RequiredPostString();
UpdatePendingNumberingStyle(newNumberingStyle);
request.PostSucceeded();
}
},
true
);
apiHandler.RegisterEndpointHandler(
kApiUrlPart + "branding",
request =>
{
if (request.HttpMethod == HttpMethods.Get)
{
request.ReplyWithJson(_collectionSettings.BrandingProjectKey);
}
else
{
// At least as of 5.6, this is only used by the visual regression tests
// Normally, we require a restart to change branding.
var key = request.RequiredPostString();
_collectionSettings.BrandingProjectKey = key;
_bookSelection.CurrentSelection?.BringBookUpToDate(new NullProgress()); // in case we changed the book's branding
request.PostSucceeded();
}
},
true
);
apiHandler.RegisterEndpointHandler(
kApiUrlPart + "xmatter",
request =>
{
if (request.HttpMethod == HttpMethods.Get)
{
// Should return all available xMatters and the current selected xMatter
request.ReplyWithJson(SetupXMatterList());
}
else
{
// We are receiving a pending xMatter change
var newXmatter = request.RequiredPostString();
UpdatePendingXmatter(newXmatter);
request.PostSucceeded();
}
},
true
);
apiHandler.RegisterEndpointHandler(
kApiUrlPart + "getCustomPaletteColors",
HandleGetCustomColorsRequest,
false
);
apiHandler.RegisterEndpointHandler(
kApiUrlPart + "addCustomPaletteColor",
HandleAddCustomColor,
false
);
apiHandler.RegisterEndpointHandler(kApiUrlPart + "webGoal", HandleWebGoalRequest, true);
apiHandler.RegisterEndpointHandler(
kApiUrlPart + "languageData",
HandleLanguageDataRequest,
true
);
apiHandler.RegisterEndpointHandler(
kApiUrlPart + "languageNames",
HandleGetLanguageNames,
false
);
}
private void ResetBookshelf()
{
if (DialogBeingEdited != null)
DialogBeingEdited.PendingDefaultBookshelf = "";
}
// Used by BooksOnBlorgProgressBar.
private void HandleWebGoalRequest(ApiRequest request)
{
if (request.HttpMethod == HttpMethods.Post)
return; // Should be a get
var goal = _collectionSettings.BooksOnWebGoal;
request.ReplyWithText(goal.ToString());
}
// Used by BooksOnBlorgProgressBar.
private void HandleLanguageDataRequest(ApiRequest request)
{
if (request.HttpMethod == HttpMethods.Post)
return; // Should be a get
var languageName = _collectionSettings.GetLanguageName(
_collectionSettings.Language1Tag,
_collectionSettings.Language1Tag
);
var langTag = _collectionSettings.Language1Tag;
// But if we have a Sign Language in the collection, use that for the Progress Bar.
if (!string.IsNullOrEmpty(_collectionSettings.SignLanguageTag))
{
langTag = _collectionSettings.SignLanguageTag;
languageName = _collectionSettings.GetLanguageName(
_collectionSettings.SignLanguageTag,
_collectionSettings.Language1Tag
);
}
var jsonString =
$"{{\"languageName\":\"{languageName}\",\"languageCode\":\"{langTag}\"}}";
request.ReplyWithJson(jsonString);
}
// Used by BookSettingsDialog
private void HandleGetLanguageNames(ApiRequest request)
{
var x = new ExpandoObject() as IDictionary<string, object>;
// The values set here should correspond to the declaration of ILanguageNameValues
// in BookSettingsDialog.tsx.
x["language1Name"] = _bookSelection.CurrentSelection.CollectionSettings.Language1.Name;
x["language2Name"] = _bookSelection.CurrentSelection.CollectionSettings.Language2.Name;
if (
!String.IsNullOrEmpty(
_bookSelection.CurrentSelection.CollectionSettings.Language3?.Name
)
)
x["language3Name"] = _bookSelection
.CurrentSelection
.CollectionSettings
.Language3
.Name;
request.ReplyWithJson(JsonConvert.SerializeObject(x));
}
private void HandleGetCustomColorsRequest(ApiRequest request)
{
var paletteKey = request.Parameters["palette"];
var jsonString = _collectionSettings.GetColorPaletteAsJson(paletteKey);
request.ReplyWithJson(jsonString);
}
private void HandleAddCustomColor(ApiRequest request)
{
var paletteTag = request.Parameters["palette"];
var colorString = request.GetPostJson();
_collectionSettings.AddColorToPalette(paletteTag, colorString);
request.PostSucceeded();
}
private object SetupXMatterList()
{
var xmatterOfferings = new List<object>();
string xmatterKeyForcedByBranding =
_collectionSettings.GetXMatterPackNameSpecifiedByBrandingOrNull();
var offerings = _xmatterPackFinder.GetXMattersToOfferInSettings(
xmatterKeyForcedByBranding
);
foreach (var pack in offerings)
{
var labelToShow = LocalizationManager.GetDynamicString(
"Bloom",
"CollectionSettingsDialog.BookMakingTab.Front/BackMatterPack."
+ pack.EnglishLabel,
pack.EnglishLabel,
"Name of a Front/Back Matter Pack"
);
var description = pack.GetDescription(); // already localized, if available
var item = new
{
displayName = labelToShow,
internalName = pack.Key,
description
};
xmatterOfferings.Add(item);
}
// This will switch to the default factory xmatter if the current one is not valid.
var currentXmatter = _xmatterPackFinder.GetValidXmatter(
xmatterKeyForcedByBranding,
_collectionSettings.XMatterPackName
);
return new { currentXmatter, xmatterOfferings = xmatterOfferings.ToArray() };
}
private object GetNumberingStyleData()
{
if (_numberingStyles.Count == 0)
{
foreach (var styleKey in CollectionSettings.CssNumberStylesToCultureOrDigits.Keys)
{
var localizedStyle = LocalizationManager.GetString(
"CollectionSettingsDialog.BookMakingTab.PageNumberingStyle." + styleKey,
styleKey
);
_numberingStyles.Add(new { localizedStyle, styleKey });
}
}
return new
{
currentPageNumberStyle = _collectionSettings.PageNumberStyle,
numberingStyleData = _numberingStyles.ToArray()
};
}
private object GetLanguageData()
{
var langData = new object[3];
for (var i = 0; i < 3; i++)
{
if (
_collectionSettings.LanguagesZeroBased[i] == null
|| string.IsNullOrEmpty(_collectionSettings.LanguagesZeroBased[i].Name)
)
continue;
var name = _collectionSettings.LanguagesZeroBased[i].Name;
var font = _collectionSettings.LanguagesZeroBased[i].FontName;
langData[i] = new { languageName = name, fontName = font };
}
return langData;
}
// languageNumber is 1-based
private void HandlePendingFontSettings(int languageNumber)
{
Guard.Against(
languageNumber == 0,
"'languageNumber' should be 1-based index, but is 0"
);
var zeroBasedLanguageNumber = languageNumber - 1;
if (
zeroBasedLanguageNumber == 2
&& _collectionSettings.LanguagesZeroBased[zeroBasedLanguageNumber] == null
)
return;
if (DialogBeingEdited != null)
{
var needRestart = DialogBeingEdited.FontSettingsLinkClicked(
zeroBasedLanguageNumber
);
if (needRestart)
DialogBeingEdited.ChangeThatRequiresRestart();
}
}
// languageNumber is 1-based
private void UpdatePendingFontName(string fontName, int languageNumber)
{
Guard.Against(
languageNumber == 0,
"'languageNumber' should be 1-based index, but is 0"
);
var zeroBasedLanguageNumber = languageNumber - 1;
if (
zeroBasedLanguageNumber == 2
&& _collectionSettings.LanguagesZeroBased[zeroBasedLanguageNumber] == null
)
return;
if (DialogBeingEdited != null)
DialogBeingEdited.PendingFontSelections[zeroBasedLanguageNumber] = fontName;
if (
fontName != _collectionSettings.LanguagesZeroBased[zeroBasedLanguageNumber].FontName
)
DialogBeingEdited.ChangeThatRequiresRestart();
}
private void UpdatePendingNumberingStyle(string numberingStyle)
{
if (DialogBeingEdited != null)
{
DialogBeingEdited.PendingNumberingStyle = numberingStyle;
if (numberingStyle != _collectionSettings.PageNumberStyle)
DialogBeingEdited.ChangeThatRequiresRestart();
}
}
private void UpdatePendingXmatter(string xMatterChoice)
{
if (DialogBeingEdited != null)
{
DialogBeingEdited.PendingXmatter = xMatterChoice;
if (xMatterChoice != _collectionSettings.XMatterPackName)
DialogBeingEdited.ChangeThatRequiresRestart();
}
}
public static string GetSummaryHtml(string branding)
{
BrandingSettings.ParseBrandingKey(branding, out var baseKey, out var flavor);
var summaryFile = BloomFileLocator.GetOptionalBrandingFile(baseKey, "summary.htm");
if (summaryFile == null)
return "";
var html = RobustFile.ReadAllText(summaryFile, Encoding.UTF8);
return html.Replace("{flavor}", flavor);
}
public static void PrepareForFixEnterpriseBranding(
string invalidBranding,
string subscriptionCode
)
{
FixEnterpriseSubscriptionCodeMode = true;
SetupLegacyBrandingForSettingsDiaog(invalidBranding, subscriptionCode);
}
public static void SetupLegacyBrandingForSettingsDiaog(
string invalidBranding,
string subscriptionCode
)
{
if (SubscriptionCodeLooksIncomplete(subscriptionCode))
LegacyBrandingName = invalidBranding; // otherwise we won't show the legacy branding message, just bring up the dialog and show whatever's wrong.
else
LegacyBrandingName = "";
}
public static void EndFixEnterpriseBranding()
{
FixEnterpriseSubscriptionCodeMode = false;
LegacyBrandingName = "";
}
// CollectionSettingsDialog sets this so we can call back with results from the tab.
public static Func<string, string, bool> BrandingChangeHandler;
public static bool SubscriptionCodeLooksIncomplete(string input)
{
if (input == null)
return true;
var parts = input.Split('-');
if (parts.Length < 3)
return true; // less than the required three components
int last = parts.Length - 1;
int dummy;
if (!Int32.TryParse(parts[last - 1], out dummy))
return true; // If they haven't started typing numbers, assume they're still in the name part, which could include a hyphen
// If they've typed one number, we expect another. (Might not be true...ethnos-360-guatemala is incomplete...)
// So, we already know the second-last part is a number, only short numbers or empty last part qualify as incomplete now.
// Moreover, for the whole thing to be incomplete in this case, the completed number must be the right length; otherwise,
// we consider it definitely wrong.
if (
parts[last - 1].Length == 6
&& parts[last].Length < 4
&& (parts[last].Length == 0 || Int32.TryParse(parts[last], out dummy))
)
return true;
return false;
}
// Parse a string like PNG-RISE-361769-363798 or SIL-LEAD-361769-363644,
// generated by a private google spreadsheet. The two last elements are numbers;
// the first is an encoding of an expiry date, the second is a simple hash of
// the project name (case-insensitive) and the expiry date, used to make it
// a little less trivial to fake codes. We're not aiming for something that
// would be difficult for someone willing to take the trouble to read this code.
public static DateTime GetExpirationDate(string input)
{
if (input == null)
return DateTime.MinValue;
var parts = input.Split('-');
if (parts.Length < 3)
return DateTime.MinValue;
int last = parts.Length - 1;
if (parts[last].Length != 4 || parts[last - 1].Length != 6)
return DateTime.MinValue;
int datePart;
if (!Int32.TryParse(parts[last - 1], out datePart))
return DateTime.MinValue;
int combinedChecksum;
if (!Int32.TryParse(parts[last], out combinedChecksum))
return DateTime.MinValue;
int checkSum = CheckSum(GetBrandingFromCode(input));
if ((Math.Floor(Math.Sqrt(datePart)) + checkSum) % 10000 != combinedChecksum)
return DateTime.MinValue;
int dateNum = datePart + 40000; // days since Dec 30 1899
return new DateTime(1899, 12, 30) + TimeSpan.FromDays(dateNum);
}
// From the same sort of code extract the project name,
// everything up to the second-last hyphen.
public static string GetBrandingFromCode(string input)
{
if (input == null)
return "";
var parts = input.Split('-').ToList();
if (parts.Count < 3)
return "";
parts.RemoveAt(parts.Count - 1);
parts.RemoveAt(parts.Count - 1);
return string.Join("-", parts.ToArray());
}
// Must match the function associated with the code generation google sheet
private static int CheckSum(string input)
{
var sum = 0;
input = input.ToUpperInvariant();
for (var i = 0; i < input.Length; i++)
{
sum += input[i] * i;
}
return sum;
}
// Used to initialize things in the constructor.
//
// Also used by the settings dialog to ensure things are initialized properly there for a special "legacy" case.
public static void SetSubscriptionCode(string code, bool knownCode, EnterpriseStatus status)
{
SubscriptionCode = code;
_enterpriseExpiry = GetExpirationDate(code);
_knownBrandingInSubscriptionCode = knownCode;
_enterpriseStatus = status;
}
}
}