1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 16:53:34 +00:00

Move to libs

This commit is contained in:
Hinton
2022-06-03 16:24:40 +02:00
parent 28d15bfe2a
commit d7492e3cf3
878 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
import { Attachment } from "../domain/attachment";
import { SymmetricCryptoKey } from "../domain/symmetricCryptoKey";
import { View } from "./view";
export class AttachmentView implements View {
id: string = null;
url: string = null;
size: string = null;
sizeName: string = null;
fileName: string = null;
key: SymmetricCryptoKey = null;
constructor(a?: Attachment) {
if (!a) {
return;
}
this.id = a.id;
this.url = a.url;
this.size = a.size;
this.sizeName = a.sizeName;
}
get fileSize(): number {
try {
if (this.size != null) {
return parseInt(this.size, null);
}
} catch {
// Invalid file size.
}
return 0;
}
}

View File

@@ -0,0 +1,82 @@
import { CardLinkedId as LinkedId } from "../../enums/linkedIdType";
import { linkedFieldOption } from "../../misc/linkedFieldOption.decorator";
import { ItemView } from "./itemView";
export class CardView extends ItemView {
@linkedFieldOption(LinkedId.CardholderName)
cardholderName: string = null;
@linkedFieldOption(LinkedId.ExpMonth, "expirationMonth")
expMonth: string = null;
@linkedFieldOption(LinkedId.ExpYear, "expirationYear")
expYear: string = null;
@linkedFieldOption(LinkedId.Code, "securityCode")
code: string = null;
private _brand: string = null;
private _number: string = null;
private _subTitle: string = null;
constructor() {
super();
}
get maskedCode(): string {
return this.code != null ? "•".repeat(this.code.length) : null;
}
get maskedNumber(): string {
return this.number != null ? "•".repeat(this.number.length) : null;
}
@linkedFieldOption(LinkedId.Brand)
get brand(): string {
return this._brand;
}
set brand(value: string) {
this._brand = value;
this._subTitle = null;
}
@linkedFieldOption(LinkedId.Number)
get number(): string {
return this._number;
}
set number(value: string) {
this._number = value;
this._subTitle = null;
}
get subTitle(): string {
if (this._subTitle == null) {
this._subTitle = this.brand;
if (this.number != null && this.number.length >= 4) {
if (this._subTitle != null && this._subTitle !== "") {
this._subTitle += ", ";
} else {
this._subTitle = "";
}
// Show last 5 on amex, last 4 for all others
const count =
this.number.length >= 5 && this.number.match(new RegExp("^3[47]")) != null ? 5 : 4;
this._subTitle += "*" + this.number.substr(this.number.length - count);
}
}
return this._subTitle;
}
get expiration(): string {
if (!this.expMonth && !this.expYear) {
return null;
}
let exp = this.expMonth != null ? ("0" + this.expMonth).slice(-2) : "__";
exp += " / " + (this.expYear != null ? this.formatYear(this.expYear) : "____");
return exp;
}
private formatYear(year: string): string {
return year.length === 2 ? "20" + year : year;
}
}

View File

@@ -0,0 +1,134 @@
import { CipherRepromptType } from "../../enums/cipherRepromptType";
import { CipherType } from "../../enums/cipherType";
import { LinkedIdType } from "../../enums/linkedIdType";
import { Cipher } from "../domain/cipher";
import { AttachmentView } from "./attachmentView";
import { CardView } from "./cardView";
import { FieldView } from "./fieldView";
import { IdentityView } from "./identityView";
import { LoginView } from "./loginView";
import { PasswordHistoryView } from "./passwordHistoryView";
import { SecureNoteView } from "./secureNoteView";
import { View } from "./view";
export class CipherView implements View {
id: string = null;
organizationId: string = null;
folderId: string = null;
name: string = null;
notes: string = null;
type: CipherType = null;
favorite = false;
organizationUseTotp = false;
edit = false;
viewPassword = true;
localData: any;
login = new LoginView();
identity = new IdentityView();
card = new CardView();
secureNote = new SecureNoteView();
attachments: AttachmentView[] = null;
fields: FieldView[] = null;
passwordHistory: PasswordHistoryView[] = null;
collectionIds: string[] = null;
revisionDate: Date = null;
deletedDate: Date = null;
reprompt: CipherRepromptType = CipherRepromptType.None;
constructor(c?: Cipher) {
if (!c) {
return;
}
this.id = c.id;
this.organizationId = c.organizationId;
this.folderId = c.folderId;
this.favorite = c.favorite;
this.organizationUseTotp = c.organizationUseTotp;
this.edit = c.edit;
this.viewPassword = c.viewPassword;
this.type = c.type;
this.localData = c.localData;
this.collectionIds = c.collectionIds;
this.revisionDate = c.revisionDate;
this.deletedDate = c.deletedDate;
// Old locally stored ciphers might have reprompt == null. If so set it to None.
this.reprompt = c.reprompt ?? CipherRepromptType.None;
}
private get item() {
switch (this.type) {
case CipherType.Login:
return this.login;
case CipherType.SecureNote:
return this.secureNote;
case CipherType.Card:
return this.card;
case CipherType.Identity:
return this.identity;
default:
break;
}
return null;
}
get subTitle(): string {
return this.item.subTitle;
}
get hasPasswordHistory(): boolean {
return this.passwordHistory && this.passwordHistory.length > 0;
}
get hasAttachments(): boolean {
return this.attachments && this.attachments.length > 0;
}
get hasOldAttachments(): boolean {
if (this.hasAttachments) {
for (let i = 0; i < this.attachments.length; i++) {
if (this.attachments[i].key == null) {
return true;
}
}
}
return false;
}
get hasFields(): boolean {
return this.fields && this.fields.length > 0;
}
get passwordRevisionDisplayDate(): Date {
if (this.type !== CipherType.Login || this.login == null) {
return null;
} else if (this.login.password == null || this.login.password === "") {
return null;
}
return this.login.passwordRevisionDate;
}
get isDeleted(): boolean {
return this.deletedDate != null;
}
get linkedFieldOptions() {
return this.item.linkedFieldOptions;
}
linkedFieldValue(id: LinkedIdType) {
const linkedFieldOption = this.linkedFieldOptions?.get(id);
if (linkedFieldOption == null) {
return null;
}
const item = this.item;
return this.item[linkedFieldOption.propertyKey as keyof typeof item];
}
linkedFieldI18nKey(id: LinkedIdType): string {
return this.linkedFieldOptions.get(id)?.i18nKey;
}
}

View File

@@ -0,0 +1,28 @@
import { Collection } from "../domain/collection";
import { ITreeNodeObject } from "../domain/treeNode";
import { CollectionGroupDetailsResponse } from "../response/collectionResponse";
import { View } from "./view";
export class CollectionView implements View, ITreeNodeObject {
id: string = null;
organizationId: string = null;
name: string = null;
externalId: string = null;
readOnly: boolean = null;
hidePasswords: boolean = null;
constructor(c?: Collection | CollectionGroupDetailsResponse) {
if (!c) {
return;
}
this.id = c.id;
this.organizationId = c.organizationId;
this.externalId = c.externalId;
if (c instanceof Collection) {
this.readOnly = c.readOnly;
this.hidePasswords = c.hidePasswords;
}
}
}

View File

@@ -0,0 +1,29 @@
import { EventType } from "../../enums/eventType";
export class EventView {
message: string;
humanReadableMessage: string;
appIcon: string;
appName: string;
userId: string;
userName: string;
userEmail: string;
date: string;
ip: string;
type: EventType;
installationId: string;
constructor(data: Required<EventView>) {
this.message = data.message;
this.humanReadableMessage = data.humanReadableMessage;
this.appIcon = data.appIcon;
this.appName = data.appName;
this.userId = data.userId;
this.userName = data.userName;
this.userEmail = data.userEmail;
this.date = data.date;
this.ip = data.ip;
this.type = data.type;
this.installationId = data.installationId;
}
}

View File

@@ -0,0 +1,28 @@
import { FieldType } from "../../enums/fieldType";
import { LinkedIdType } from "../../enums/linkedIdType";
import { Field } from "../domain/field";
import { View } from "./view";
export class FieldView implements View {
name: string = null;
value: string = null;
type: FieldType = null;
newField = false; // Marks if the field is new and hasn't been saved
showValue = false;
showCount = false;
linkedId: LinkedIdType = null;
constructor(f?: Field) {
if (!f) {
return;
}
this.type = f.type;
this.linkedId = f.linkedId;
}
get maskedValue(): string {
return this.value != null ? "••••••••" : null;
}
}

View File

@@ -0,0 +1,19 @@
import { Folder } from "../domain/folder";
import { ITreeNodeObject } from "../domain/treeNode";
import { View } from "./view";
export class FolderView implements View, ITreeNodeObject {
id: string = null;
name: string = null;
revisionDate: Date = null;
constructor(f?: Folder) {
if (!f) {
return;
}
this.id = f.id;
this.revisionDate = f.revisionDate;
}
}

View File

@@ -0,0 +1,142 @@
import { IdentityLinkedId as LinkedId } from "../../enums/linkedIdType";
import { linkedFieldOption } from "../../misc/linkedFieldOption.decorator";
import { Utils } from "../../misc/utils";
import { ItemView } from "./itemView";
export class IdentityView extends ItemView {
@linkedFieldOption(LinkedId.Title)
title: string = null;
@linkedFieldOption(LinkedId.MiddleName)
middleName: string = null;
@linkedFieldOption(LinkedId.Address1)
address1: string = null;
@linkedFieldOption(LinkedId.Address2)
address2: string = null;
@linkedFieldOption(LinkedId.Address3)
address3: string = null;
@linkedFieldOption(LinkedId.City, "cityTown")
city: string = null;
@linkedFieldOption(LinkedId.State, "stateProvince")
state: string = null;
@linkedFieldOption(LinkedId.PostalCode, "zipPostalCode")
postalCode: string = null;
@linkedFieldOption(LinkedId.Country)
country: string = null;
@linkedFieldOption(LinkedId.Company)
company: string = null;
@linkedFieldOption(LinkedId.Email)
email: string = null;
@linkedFieldOption(LinkedId.Phone)
phone: string = null;
@linkedFieldOption(LinkedId.Ssn)
ssn: string = null;
@linkedFieldOption(LinkedId.Username)
username: string = null;
@linkedFieldOption(LinkedId.PassportNumber)
passportNumber: string = null;
@linkedFieldOption(LinkedId.LicenseNumber)
licenseNumber: string = null;
private _firstName: string = null;
private _lastName: string = null;
private _subTitle: string = null;
constructor() {
super();
}
@linkedFieldOption(LinkedId.FirstName)
get firstName(): string {
return this._firstName;
}
set firstName(value: string) {
this._firstName = value;
this._subTitle = null;
}
@linkedFieldOption(LinkedId.LastName)
get lastName(): string {
return this._lastName;
}
set lastName(value: string) {
this._lastName = value;
this._subTitle = null;
}
get subTitle(): string {
if (this._subTitle == null && (this.firstName != null || this.lastName != null)) {
this._subTitle = "";
if (this.firstName != null) {
this._subTitle = this.firstName;
}
if (this.lastName != null) {
if (this._subTitle !== "") {
this._subTitle += " ";
}
this._subTitle += this.lastName;
}
}
return this._subTitle;
}
@linkedFieldOption(LinkedId.FullName)
get fullName(): string {
if (
this.title != null ||
this.firstName != null ||
this.middleName != null ||
this.lastName != null
) {
let name = "";
if (this.title != null) {
name += this.title + " ";
}
if (this.firstName != null) {
name += this.firstName + " ";
}
if (this.middleName != null) {
name += this.middleName + " ";
}
if (this.lastName != null) {
name += this.lastName;
}
return name.trim();
}
return null;
}
get fullAddress(): string {
let address = this.address1;
if (!Utils.isNullOrWhitespace(this.address2)) {
if (!Utils.isNullOrWhitespace(address)) {
address += ", ";
}
address += this.address2;
}
if (!Utils.isNullOrWhitespace(this.address3)) {
if (!Utils.isNullOrWhitespace(address)) {
address += ", ";
}
address += this.address3;
}
return address;
}
get fullAddressPart2(): string {
if (this.city == null && this.state == null && this.postalCode == null) {
return null;
}
const city = this.city || "-";
const state = this.state;
const postalCode = this.postalCode || "-";
let addressPart2 = city;
if (!Utils.isNullOrWhitespace(state)) {
addressPart2 += ", " + state;
}
addressPart2 += ", " + postalCode;
return addressPart2;
}
}

View File

@@ -0,0 +1,8 @@
import { LinkedMetadata } from "../../misc/linkedFieldOption.decorator";
import { View } from "./view";
export abstract class ItemView implements View {
linkedFieldOptions: Map<number, LinkedMetadata>;
abstract get subTitle(): string;
}

View File

@@ -0,0 +1,127 @@
import { UriMatchType } from "../../enums/uriMatchType";
import { Utils } from "../../misc/utils";
import { LoginUri } from "../domain/loginUri";
import { View } from "./view";
const CanLaunchWhitelist = [
"https://",
"http://",
"ssh://",
"ftp://",
"sftp://",
"irc://",
"vnc://",
// https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/remote-desktop-uri
"rdp://", // Legacy RDP URI scheme
"ms-rd:", // Preferred RDP URI scheme
"chrome://",
"iosapp://",
"androidapp://",
];
export class LoginUriView implements View {
match: UriMatchType = null;
private _uri: string = null;
private _domain: string = null;
private _hostname: string = null;
private _host: string = null;
private _canLaunch: boolean = null;
constructor(u?: LoginUri) {
if (!u) {
return;
}
this.match = u.match;
}
get uri(): string {
return this._uri;
}
set uri(value: string) {
this._uri = value;
this._domain = null;
this._canLaunch = null;
}
get domain(): string {
if (this._domain == null && this.uri != null) {
this._domain = Utils.getDomain(this.uri);
if (this._domain === "") {
this._domain = null;
}
}
return this._domain;
}
get hostname(): string {
if (this.match === UriMatchType.RegularExpression) {
return null;
}
if (this._hostname == null && this.uri != null) {
this._hostname = Utils.getHostname(this.uri);
if (this._hostname === "") {
this._hostname = null;
}
}
return this._hostname;
}
get host(): string {
if (this.match === UriMatchType.RegularExpression) {
return null;
}
if (this._host == null && this.uri != null) {
this._host = Utils.getHost(this.uri);
if (this._host === "") {
this._host = null;
}
}
return this._host;
}
get hostnameOrUri(): string {
return this.hostname != null ? this.hostname : this.uri;
}
get hostOrUri(): string {
return this.host != null ? this.host : this.uri;
}
get isWebsite(): boolean {
return (
this.uri != null &&
(this.uri.indexOf("http://") === 0 ||
this.uri.indexOf("https://") === 0 ||
(this.uri.indexOf("://") < 0 && Utils.tldEndingRegex.test(this.uri)))
);
}
get canLaunch(): boolean {
if (this._canLaunch != null) {
return this._canLaunch;
}
if (this.uri != null && this.match !== UriMatchType.RegularExpression) {
const uri = this.launchUri;
for (let i = 0; i < CanLaunchWhitelist.length; i++) {
if (uri.indexOf(CanLaunchWhitelist[i]) === 0) {
this._canLaunch = true;
return this._canLaunch;
}
}
}
this._canLaunch = false;
return this._canLaunch;
}
get launchUri(): string {
return this.uri.indexOf("://") < 0 && Utils.tldEndingRegex.test(this.uri)
? "http://" + this.uri
: this.uri;
}
}

View File

@@ -0,0 +1,63 @@
import { LoginLinkedId as LinkedId } from "../../enums/linkedIdType";
import { linkedFieldOption } from "../../misc/linkedFieldOption.decorator";
import { Utils } from "../../misc/utils";
import { Login } from "../domain/login";
import { ItemView } from "./itemView";
import { LoginUriView } from "./loginUriView";
export class LoginView extends ItemView {
@linkedFieldOption(LinkedId.Username)
username: string = null;
@linkedFieldOption(LinkedId.Password)
password: string = null;
passwordRevisionDate?: Date = null;
totp: string = null;
uris: LoginUriView[] = null;
autofillOnPageLoad: boolean = null;
constructor(l?: Login) {
super();
if (!l) {
return;
}
this.passwordRevisionDate = l.passwordRevisionDate;
this.autofillOnPageLoad = l.autofillOnPageLoad;
}
get uri(): string {
return this.hasUris ? this.uris[0].uri : null;
}
get maskedPassword(): string {
return this.password != null ? "••••••••" : null;
}
get subTitle(): string {
return this.username;
}
get canLaunch(): boolean {
return this.hasUris && this.uris.some((u) => u.canLaunch);
}
get hasTotp(): boolean {
return !Utils.isNullOrWhitespace(this.totp);
}
get launchUri(): string {
if (this.hasUris) {
const uri = this.uris.find((u) => u.canLaunch);
if (uri != null) {
return uri.launchUri;
}
}
return null;
}
get hasUris(): boolean {
return this.uris != null && this.uris.length > 0;
}
}

View File

@@ -0,0 +1,16 @@
import { Password } from "../domain/password";
import { View } from "./view";
export class PasswordHistoryView implements View {
password: string = null;
lastUsedDate: Date = null;
constructor(ph?: Password) {
if (!ph) {
return;
}
this.lastUsedDate = ph.lastUsedDate;
}
}

View File

@@ -0,0 +1,21 @@
import { SecureNoteType } from "../../enums/secureNoteType";
import { SecureNote } from "../domain/secureNote";
import { ItemView } from "./itemView";
export class SecureNoteView extends ItemView {
type: SecureNoteType = null;
constructor(n?: SecureNote) {
super();
if (!n) {
return;
}
this.type = n.type;
}
get subTitle(): string {
return null;
}
}

View File

@@ -0,0 +1,27 @@
import { SendType } from "../../enums/sendType";
import { SendAccess } from "../domain/sendAccess";
import { SendFileView } from "./sendFileView";
import { SendTextView } from "./sendTextView";
import { View } from "./view";
export class SendAccessView implements View {
id: string = null;
name: string = null;
type: SendType = null;
text = new SendTextView();
file = new SendFileView();
expirationDate: Date = null;
creatorIdentifier: string = null;
constructor(s?: SendAccess) {
if (!s) {
return;
}
this.id = s.id;
this.type = s.type;
this.expirationDate = s.expirationDate;
this.creatorIdentifier = s.creatorIdentifier;
}
}

View File

@@ -0,0 +1,31 @@
import { SendFile } from "../domain/sendFile";
import { View } from "./view";
export class SendFileView implements View {
id: string = null;
size: string = null;
sizeName: string = null;
fileName: string = null;
constructor(f?: SendFile) {
if (!f) {
return;
}
this.id = f.id;
this.size = f.size;
this.sizeName = f.sizeName;
}
get fileSize(): number {
try {
if (this.size != null) {
return parseInt(this.size, null);
}
} catch {
// Invalid file size.
}
return 0;
}
}

View File

@@ -0,0 +1,20 @@
import { SendText } from "../domain/sendText";
import { View } from "./view";
export class SendTextView implements View {
text: string = null;
hidden: boolean;
constructor(t?: SendText) {
if (!t) {
return;
}
this.hidden = t.hidden;
}
get maskedText(): string {
return this.text != null ? "••••••••" : null;
}
}

View File

@@ -0,0 +1,68 @@
import { SendType } from "../../enums/sendType";
import { Utils } from "../../misc/utils";
import { Send } from "../domain/send";
import { SymmetricCryptoKey } from "../domain/symmetricCryptoKey";
import { SendFileView } from "./sendFileView";
import { SendTextView } from "./sendTextView";
import { View } from "./view";
export class SendView implements View {
id: string = null;
accessId: string = null;
name: string = null;
notes: string = null;
key: ArrayBuffer;
cryptoKey: SymmetricCryptoKey;
type: SendType = null;
text = new SendTextView();
file = new SendFileView();
maxAccessCount?: number = null;
accessCount = 0;
revisionDate: Date = null;
deletionDate: Date = null;
expirationDate: Date = null;
password: string = null;
disabled = false;
hideEmail = false;
constructor(s?: Send) {
if (!s) {
return;
}
this.id = s.id;
this.accessId = s.accessId;
this.type = s.type;
this.maxAccessCount = s.maxAccessCount;
this.accessCount = s.accessCount;
this.revisionDate = s.revisionDate;
this.deletionDate = s.deletionDate;
this.expirationDate = s.expirationDate;
this.disabled = s.disabled;
this.password = s.password;
this.hideEmail = s.hideEmail;
}
get urlB64Key(): string {
return Utils.fromBufferToUrlB64(this.key);
}
get maxAccessCountReached(): boolean {
if (this.maxAccessCount == null) {
return false;
}
return this.accessCount >= this.maxAccessCount;
}
get expired(): boolean {
if (this.expirationDate == null) {
return false;
}
return this.expirationDate <= new Date();
}
get pendingDelete(): boolean {
return this.deletionDate <= new Date();
}
}

View File

@@ -0,0 +1,104 @@
import {
OpenIdConnectRedirectBehavior,
Saml2BindingType,
Saml2NameIdFormat,
Saml2SigningBehavior,
SsoType,
} from "../../enums/ssoEnums";
import { SsoConfigApi } from "../api/ssoConfigApi";
import { View } from "./view";
export class SsoConfigView extends View {
configType: SsoType;
keyConnectorEnabled: boolean;
keyConnectorUrl: string;
openId: {
authority: string;
clientId: string;
clientSecret: string;
metadataAddress: string;
redirectBehavior: OpenIdConnectRedirectBehavior;
getClaimsFromUserInfoEndpoint: boolean;
additionalScopes: string;
additionalUserIdClaimTypes: string;
additionalEmailClaimTypes: string;
additionalNameClaimTypes: string;
acrValues: string;
expectedReturnAcrValue: string;
};
saml: {
spNameIdFormat: Saml2NameIdFormat;
spOutboundSigningAlgorithm: string;
spSigningBehavior: Saml2SigningBehavior;
spMinIncomingSigningAlgorithm: boolean;
spWantAssertionsSigned: boolean;
spValidateCertificates: boolean;
idpEntityId: string;
idpBindingType: Saml2BindingType;
idpSingleSignOnServiceUrl: string;
idpSingleLogoutServiceUrl: string;
idpX509PublicCert: string;
idpOutboundSigningAlgorithm: string;
idpAllowUnsolicitedAuthnResponse: boolean;
idpAllowOutboundLogoutRequests: boolean;
idpWantAuthnRequestsSigned: boolean;
};
constructor(api: SsoConfigApi) {
super();
if (api == null) {
return;
}
this.configType = api.configType;
this.keyConnectorEnabled = api.keyConnectorEnabled;
this.keyConnectorUrl = api.keyConnectorUrl;
if (this.configType === SsoType.OpenIdConnect) {
this.openId = {
authority: api.authority,
clientId: api.clientId,
clientSecret: api.clientSecret,
metadataAddress: api.metadataAddress,
redirectBehavior: api.redirectBehavior,
getClaimsFromUserInfoEndpoint: api.getClaimsFromUserInfoEndpoint,
additionalScopes: api.additionalScopes,
additionalUserIdClaimTypes: api.additionalUserIdClaimTypes,
additionalEmailClaimTypes: api.additionalEmailClaimTypes,
additionalNameClaimTypes: api.additionalNameClaimTypes,
acrValues: api.acrValues,
expectedReturnAcrValue: api.expectedReturnAcrValue,
};
} else if (this.configType === SsoType.Saml2) {
this.saml = {
spNameIdFormat: api.spNameIdFormat,
spOutboundSigningAlgorithm: api.spOutboundSigningAlgorithm,
spSigningBehavior: api.spSigningBehavior,
spMinIncomingSigningAlgorithm: api.spMinIncomingSigningAlgorithm,
spWantAssertionsSigned: api.spWantAssertionsSigned,
spValidateCertificates: api.spValidateCertificates,
idpEntityId: api.idpEntityId,
idpBindingType: api.idpBindingType,
idpSingleSignOnServiceUrl: api.idpSingleSignOnServiceUrl,
idpSingleLogoutServiceUrl: api.idpSingleLogoutServiceUrl,
idpX509PublicCert: api.idpX509PublicCert,
idpOutboundSigningAlgorithm: api.idpOutboundSigningAlgorithm,
idpAllowUnsolicitedAuthnResponse: api.idpAllowUnsolicitedAuthnResponse,
idpWantAuthnRequestsSigned: api.idpWantAuthnRequestsSigned,
// Value is inverted in the view model (allow instead of disable)
idpAllowOutboundLogoutRequests:
api.idpDisableOutboundLogoutRequests == null
? null
: !api.idpDisableOutboundLogoutRequests,
};
}
}
}

View File

@@ -0,0 +1 @@
export class View {}