diff --git a/.vscode/launch.json b/.vscode/launch.json index 23d82f5..24ca420 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,11 +10,11 @@ "type": "go", "request": "launch", "mode": "auto", - "buildFlags": "", + "buildFlags": "-tags webui", "program": "./cmd/webapi", - "args": [ - "--db-filename", "/home/marco/wasa/wasadata/wasaphoto.db", "--data-path", "/home/marco/wasa/wasadata/data" - ] + //"args": [ + // "--db-filename", "/home/marco/wasa/wasadata/wasaphoto.db", "--data-path", "/home/marco/wasa/wasadata/data" + //] } ] } \ No newline at end of file diff --git a/Dockerfile.backend b/Dockerfile.backend new file mode 100644 index 0000000..839a3b4 --- /dev/null +++ b/Dockerfile.backend @@ -0,0 +1,22 @@ +FROM golang:1.19.1 AS builder + +### Copy Go code +WORKDIR /src/ +COPY . . + +### Build executables +RUN go build -o /app/webapi ./cmd/webapi + + +### Create final container +FROM debian:bullseye + +### Inform Docker about which port is used +EXPOSE 3000 4000 + +### Copy the build executable from the builder image +WORKDIR /app/ +COPY --from=builder /app/webapi ./ + +### Executable command +CMD ["/app/webapi", "--db-filename", "/data/wasaphoto.db", "--data-path", "/data/data"] \ No newline at end of file diff --git a/Dockerfile.embedded b/Dockerfile.embedded new file mode 100644 index 0000000..4a3e612 --- /dev/null +++ b/Dockerfile.embedded @@ -0,0 +1,34 @@ +FROM node:lts as ui-builder + +### Copy Vue.js code +WORKDIR /app +COPY webui webui + +### Build Vue.js into plain HTML/CSS/JS +WORKDIR /app/webui +RUN npm i +RUN npm run build-embed + +FROM golang:1.19.1 AS builder + +### Copy Go code +WORKDIR /src/ +COPY . . +COPY --from=ui-builder /app/webui webui + +### Build executables +RUN go build -tags webui -o /app/webapi ./cmd/webapi + + +### Create final container +FROM debian:bullseye + +### Inform Docker about which port is used +EXPOSE 3000 4000 + +### Copy the build executable from the builder image +WORKDIR /app/ +COPY --from=builder /app/webapi ./ + +### Executable command +CMD ["/app/webapi", "--db-filename", "/data/wasaphoto.db", "--data-path", "/data/data"] diff --git a/Dockerfile.frontend b/Dockerfile.frontend new file mode 100644 index 0000000..28608b4 --- /dev/null +++ b/Dockerfile.frontend @@ -0,0 +1,16 @@ +FROM node:lts as builder + +### Copy Vue.js code +WORKDIR /app +COPY webui webui + +### Build Vue.js into plain HTML/CSS/JS +WORKDIR /app/webui +RUN npm run build-prod + + +### Create final container +FROM nginx:stable + +### Copy the (built) app from the builder image +COPY --from=builder /app/webui/dist /usr/share/nginx/html \ No newline at end of file diff --git a/README.md b/README.md index 4a691d4..21cb69a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ *Keep in touch with your friends by sharing photos of special moments, thanks to WASAPhoto!* -*You canupload your photos directly from your PC, and they will be visible to everyone following you.* +*You can upload your photos directly from your PC, and they will be visible to everyone following you.* + +(Live demo: [https://wasaphoto.marcorealacci.me](https://wasaphoto.marcorealacci.me)) --- @@ -17,6 +19,56 @@ This is my project for the Web And Software Architecture (WASA) class * An API specification using the OpenAPI standard * A backend written in the Go language * A frontend in Vue.js -* ~~A Dockerfile to build a Docker image to deploy the project in a container.~~ +* Dockerfiles to deploy the backend and the frontend in containers. + * Dockerfile.backend builds the container for the backend + * Dockerfile.frontend builds the container for the frontend + * Dockerfile.embedded builds the backend container, but the backend's webserver also delivers the frontend -*(Strikethrough parts are work in progress or still need to be implemented)* +### Before building + +If you're building the project in production mode (see below), you need to specify the base URL for the backend in `vite.config.js`. + + +## Build & deploy + +The only (officially) supported method is via Docker containers. + +There are two supported methods. + +#### Embedded build + +This method is only recommended for testing purposes or instances with very few users (for performance reasons). + +The following commands will build a single container to serve both frontend and backend. + +``` +docker build -t wasaphoto -f Dockerfile.embedded . +docker run -p 3000:3000 -v :/data --name wasaphoto wasaphoto +``` + +Everything will be up and running on port 3000 (including the Web UI). + + +#### Production build + +This method build two containers, one for the backend and a container that running nginx to serve the frontend. + +This is very recommended on production envinoments. + +1. Build and run the backend + + ``` + docker build -t wasabackend -f Dockerfile.backend . + docker run -p 3000:3000 -v :/data --name wasaphoto-backend wasabackend + ``` +2. Edit the `vite.config.js` file and replace `` with the backend's base URL. +3. Build and run the frontend + + ``` + docker build -t wasafrontend -f Dockerfile.frontend . + docker run -p 8080:80 --name wasaphoto-frontend wasafrontend + ``` + +The Web UI will be up and running on port 8080! + + diff --git a/cmd/webapi/main.go b/cmd/webapi/main.go index 490fec3..2002051 100644 --- a/cmd/webapi/main.go +++ b/cmd/webapi/main.go @@ -81,6 +81,12 @@ func run() error { logger.Infof("application initializing") + // Create the directories if they don't exist + if err := os.MkdirAll(cfg.Data.Path, 0755); err != nil { + logger.WithError(err).Error("error creating data directory") + return fmt.Errorf("creating data directory: %w", err) + } + // Start Database logger.Println("initializing database support") dbconn, err := sql.Open("sqlite3", cfg.DB.Filename) @@ -120,6 +126,7 @@ func run() error { logger.WithError(err).Error("error creating the API server instance") return fmt.Errorf("creating the API server instance: %w", err) } + router := apirouter.Handler() router, err = registerWebUI(router) diff --git a/cmd/webapi/register-web-ui.go b/cmd/webapi/register-web-ui.go index 352374e..9cb0af9 100644 --- a/cmd/webapi/register-web-ui.go +++ b/cmd/webapi/register-web-ui.go @@ -4,10 +4,11 @@ package main import ( "fmt" - "github.com/notherealmarco/WASAPhoto/webui" "io/fs" "net/http" "strings" + + "github.com/notherealmarco/WASAPhoto/webui" ) func registerWebUI(hdl http.Handler) (http.Handler, error) { @@ -20,6 +21,10 @@ func registerWebUI(hdl http.Handler) (http.Handler, error) { if strings.HasPrefix(r.RequestURI, "/dashboard/") { http.StripPrefix("/dashboard/", http.FileServer(http.FS(distDirectory))).ServeHTTP(w, r) return + } else if r.RequestURI == "/" { + // Redirect to dashboard + http.Redirect(w, r, "/dashboard/", http.StatusTemporaryRedirect) + return } hdl.ServeHTTP(w, r) }), nil diff --git a/doc/api.yaml b/doc/api.yaml index 337065a..7e53b07 100644 --- a/doc/api.yaml +++ b/doc/api.yaml @@ -1,4 +1,4 @@ -openapi: 3.0.3 +openapi: 3.0.2 info: title: WASAPhoto API description: |- @@ -210,7 +210,7 @@ paths: $ref: "#/components/schemas/generic_response" example: status: "Resource not found" - '400': # todo: not sure if this is the right error code + '400': description: Trying to follow a user that does not exist. content: application/json: @@ -920,6 +920,7 @@ components: maxLength: 36 description: The user ID. format: uuid + pattern: '^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$' # RFC 4122 example: "1b4e28ba-2fa1-11d2-883f-0016d3cca427" name_object: type: object @@ -1080,9 +1081,9 @@ components: comment: $ref: "#/components/schemas/comment" comment: - minLength: 5 + minLength: 1 maxLength: 255 - pattern: ".*" #everything except newlines ^[*]{5, 255}$ + pattern: "^(.){1,255}$" # everything except newlines type: string example: "What a lovely picture! 😊" description: The comment's text @@ -1092,7 +1093,7 @@ components: format: binary minLength: 1 maxLength: 10485760 # 10 MB - pattern: "((.|\n)*)" # todo: review. Btw this means "any string" + pattern: "((.|\n)*)" # this accepts everything generic_response: type: object diff --git a/service/api/authorization/auth-anonymous.go b/service/api/authorization/auth-anonymous.go index 9fbeea3..687665a 100644 --- a/service/api/authorization/auth-anonymous.go +++ b/service/api/authorization/auth-anonymous.go @@ -7,9 +7,11 @@ import ( "github.com/notherealmarco/WASAPhoto/service/database" ) +// AnonymousAuth is the authentication provider for non logged-in users type AnonymousAuth struct { } +// Returns a newly created AnonymousAuth instance func BuildAnonymous() *AnonymousAuth { return &AnonymousAuth{} } @@ -18,14 +20,17 @@ func (u *AnonymousAuth) GetType() string { return "Anonymous" } +// Returns UNAUTHORIZED, as anonymous users are logged in func (u *AnonymousAuth) Authorized(db database.AppDatabase) (reqcontext.AuthStatus, error) { return reqcontext.UNAUTHORIZED, nil } +// Returns UNAUTHORIZED, as anonymous users are not logged in func (u *AnonymousAuth) UserAuthorized(db database.AppDatabase, uid string) (reqcontext.AuthStatus, error) { return reqcontext.UNAUTHORIZED, nil } +// Returns an empty string, as anonymous users have no user ID func (u *AnonymousAuth) GetUserID() string { return "" } diff --git a/service/api/authorization/auth-bearer.go b/service/api/authorization/auth-bearer.go index 633772d..58e0e14 100644 --- a/service/api/authorization/auth-bearer.go +++ b/service/api/authorization/auth-bearer.go @@ -8,6 +8,8 @@ import ( "github.com/notherealmarco/WASAPhoto/service/database" ) +// BearerAuth is the authentication provider that authorizes users by Bearer tokens +// In this case, a token is the unique identifier for a user. type BearerAuth struct { token string } @@ -16,6 +18,8 @@ func (b *BearerAuth) GetType() string { return "Bearer" } +// Given the content of the Authorization header, returns a BearerAuth instance for the user +// Returns an error if the header is not valid func BuildBearer(header string) (*BearerAuth, error) { if header == "" { return nil, errors.New("missing authorization header") @@ -29,10 +33,12 @@ func BuildBearer(header string) (*BearerAuth, error) { return &BearerAuth{token: header[7:]}, nil } +// Returns the user ID of the user that is currently logged in func (b *BearerAuth) GetUserID() string { return b.token } +// Checks if the token is valid func (b *BearerAuth) Authorized(db database.AppDatabase) (reqcontext.AuthStatus, error) { // this is the way we manage authorization, the bearer token is the user id state, err := db.UserExists(b.token) @@ -47,6 +53,7 @@ func (b *BearerAuth) Authorized(db database.AppDatabase) (reqcontext.AuthStatus, return reqcontext.UNAUTHORIZED, nil } +// Checks if the given user and the currently logged in user are the same user func (b *BearerAuth) UserAuthorized(db database.AppDatabase, uid string) (reqcontext.AuthStatus, error) { // If uid is not a valid user, return USER_NOT_FOUND @@ -60,6 +67,7 @@ func (b *BearerAuth) UserAuthorized(db database.AppDatabase, uid string) (reqcon } if b.token == uid { + // If the user is the same as the one in the token, check if the user does actually exist in the database auth, err := b.Authorized(db) if err != nil { @@ -68,5 +76,6 @@ func (b *BearerAuth) UserAuthorized(db database.AppDatabase, uid string) (reqcon return auth, nil } + // If the user is not the same as the one in the token, return FORBIDDEN return reqcontext.FORBIDDEN, nil } diff --git a/service/api/authorization/auth-manager.go b/service/api/authorization/auth-manager.go index 41d6da4..23449a9 100644 --- a/service/api/authorization/auth-manager.go +++ b/service/api/authorization/auth-manager.go @@ -10,6 +10,7 @@ import ( "github.com/sirupsen/logrus" ) +// BuildAuth returns an Authorization implementation for the currently logged in user func BuildAuth(header string) (reqcontext.Authorization, error) { auth, err := BuildBearer(header) if err != nil { @@ -21,6 +22,8 @@ func BuildAuth(header string) (reqcontext.Authorization, error) { return auth, nil } +// Given a user authorization function, if the function returns some error, it sends the error to the client and return false +// Otherwise it returns true without sending anything to the client func SendAuthorizationError(f func(db database.AppDatabase, uid string) (reqcontext.AuthStatus, error), uid string, db database.AppDatabase, w http.ResponseWriter, l logrus.FieldLogger, notFoundStatus int) bool { auth, err := f(db, uid) if err != nil { @@ -28,21 +31,25 @@ func SendAuthorizationError(f func(db database.AppDatabase, uid string) (reqcont return false } if auth == reqcontext.UNAUTHORIZED { + // The token is not valid helpers.SendStatus(http.StatusUnauthorized, w, "Unauthorized", l) return false } if auth == reqcontext.FORBIDDEN { + // The user is not authorized for this action helpers.SendStatus(http.StatusForbidden, w, "Forbidden", l) return false } - // requested user is not found -> 404 as the resource is not found if auth == reqcontext.USER_NOT_FOUND { + // Attempting to perform an action on a non-existent user helpers.SendStatus(notFoundStatus, w, "User not found", l) return false } return true } +// Given a function that validates a token, if the function returns some error, it sends the error to the client and return false +// Otherwise it returns true without sending anything to the client func SendErrorIfNotLoggedIn(f func(db database.AppDatabase) (reqcontext.AuthStatus, error), db database.AppDatabase, w http.ResponseWriter, l logrus.FieldLogger) bool { auth, err := f(db) @@ -53,6 +60,7 @@ func SendErrorIfNotLoggedIn(f func(db database.AppDatabase) (reqcontext.AuthStat } if auth == reqcontext.UNAUTHORIZED { + // The token is not valid helpers.SendStatus(http.StatusUnauthorized, w, "Unauthorized", l) return false } diff --git a/service/api/bans.go b/service/api/bans.go index 7f95ecc..256cd8c 100644 --- a/service/api/bans.go +++ b/service/api/bans.go @@ -66,6 +66,7 @@ func (rt *_router) PutBan(w http.ResponseWriter, r *http.Request, ps httprouter. return } + // Execute the query status, err := rt.db.BanUser(uid, banned) if err != nil { @@ -83,6 +84,13 @@ func (rt *_router) PutBan(w http.ResponseWriter, r *http.Request, ps httprouter. return } + // Removes the banning user to the banned user's followers (if present) + _, err = rt.db.UnfollowUser(banned, uid) + + if err != nil { + helpers.SendInternalError(err, "Database error: UnfollowUser", w, rt.baseLogger) + } + helpers.SendStatus(http.StatusCreated, w, "Success", rt.baseLogger) } @@ -95,6 +103,7 @@ func (rt *_router) DeleteBan(w http.ResponseWriter, r *http.Request, ps httprout return } + // Execute the query status, err := rt.db.UnbanUser(uid, banned) if err != nil { diff --git a/service/api/comments.go b/service/api/comments.go index b7f502f..4638b08 100644 --- a/service/api/comments.go +++ b/service/api/comments.go @@ -3,7 +3,6 @@ package api import ( "encoding/json" "net/http" - "regexp" "strconv" "github.com/julienschmidt/httprouter" @@ -56,6 +55,7 @@ func (rt *_router) GetComments(w http.ResponseWriter, r *http.Request, ps httpro } // send the response + w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(comments) if err != nil { @@ -89,15 +89,7 @@ func (rt *_router) PostComment(w http.ResponseWriter, r *http.Request, ps httpro } // check if the comment is valid (should not contain newlines and at be between 5 and 255 characters) - stat, err := regexp.Match(`^[*]{5, 255}$`, []byte(request_body.Comment)) - - if err != nil { - helpers.SendInternalError(err, "Error matching regex", w, rt.baseLogger) - return - } - - if !stat { - helpers.SendBadRequest(w, "Invalid comment", rt.baseLogger) + if !helpers.MatchCommentOrBadRequest(request_body.Comment, w, rt.baseLogger) { return } diff --git a/service/api/followers.go b/service/api/followers.go index de2a435..2696b59 100644 --- a/service/api/followers.go +++ b/service/api/followers.go @@ -79,6 +79,7 @@ func (rt *_router) PutFollow(w http.ResponseWriter, r *http.Request, ps httprout return } + // Execute the query status, err := rt.db.FollowUser(follower, uid) if err != nil { @@ -109,6 +110,7 @@ func (rt *_router) DeleteFollow(w http.ResponseWriter, r *http.Request, ps httpr return } + // Execute the query status, err := rt.db.UnfollowUser(follower, uid) if err != nil { diff --git a/service/api/helpers/api-helpers.go b/service/api/helpers/api-helpers.go index 7d1bd81..c536e14 100644 --- a/service/api/helpers/api-helpers.go +++ b/service/api/helpers/api-helpers.go @@ -10,6 +10,8 @@ import ( "github.com/sirupsen/logrus" ) +// Tries to decode a json, if it fails, it returns Bad Request to the client and the function returns false +// Otherwise it returns true without sending anything to the client func DecodeJsonOrBadRequest(r io.Reader, w http.ResponseWriter, v interface{}, l logrus.FieldLogger) bool { err := json.NewDecoder(r).Decode(v) @@ -20,6 +22,8 @@ func DecodeJsonOrBadRequest(r io.Reader, w http.ResponseWriter, v interface{}, l return true } +// Verifies if a user exists, if it doesn't, it returns Not Found to the client and the function returns false +// Otherwise it returns true without sending anything to the client func VerifyUserOrNotFound(db database.AppDatabase, uid string, w http.ResponseWriter, l logrus.FieldLogger) bool { user_exists, err := db.UserExists(uid) @@ -36,6 +40,8 @@ func VerifyUserOrNotFound(db database.AppDatabase, uid string, w http.ResponseWr return true } +// Sends a generic status response +// The response is a json object with a "status" field desribing the status of a request func SendStatus(httpStatus int, w http.ResponseWriter, description string, l logrus.FieldLogger) { w.WriteHeader(httpStatus) err := json.NewEncoder(w).Encode(structures.GenericResponse{Status: description}) @@ -44,40 +50,29 @@ func SendStatus(httpStatus int, w http.ResponseWriter, description string, l log } } +// Sends a Not Found error to the client func SendNotFound(w http.ResponseWriter, description string, l logrus.FieldLogger) { - w.WriteHeader(http.StatusNotFound) - err := json.NewEncoder(w).Encode(structures.GenericResponse{Status: description}) - if err != nil { - l.WithError(err).Error("Error encoding json") - } + SendStatus(http.StatusNotFound, w, description, l) } +// Sends a Bad Request error to the client func SendBadRequest(w http.ResponseWriter, description string, l logrus.FieldLogger) { - w.WriteHeader(http.StatusBadRequest) - err := json.NewEncoder(w).Encode(structures.GenericResponse{Status: description}) - if err != nil { - l.WithError(err).Error("Error encoding json") - } + SendStatus(http.StatusBadRequest, w, description, l) } +// Sends a Bad Request error to the client and logs the given error func SendBadRequestError(err error, description string, w http.ResponseWriter, l logrus.FieldLogger) { - w.WriteHeader(http.StatusBadRequest) l.WithError(err).Error(description) - err = json.NewEncoder(w).Encode(structures.GenericResponse{Status: description}) - if err != nil { - l.WithError(err).Error("Error encoding json") - } + SendBadRequest(w, description, l) } +// Sends an Internal Server Error to the client and logs the given error func SendInternalError(err error, description string, w http.ResponseWriter, l logrus.FieldLogger) { - w.WriteHeader(http.StatusInternalServerError) l.WithError(err).Error(description) - err = json.NewEncoder(w).Encode(structures.GenericResponse{Status: description}) - if err != nil { - l.WithError(err).Error("Error encoding json") - } + SendStatus(http.StatusInternalServerError, w, description, l) } +// Tries to roll back a transaction, if it fails it logs the error func RollbackOrLogError(tx database.DBTransaction, l logrus.FieldLogger) { err := tx.Rollback() if err != nil { @@ -85,6 +80,8 @@ func RollbackOrLogError(tx database.DBTransaction, l logrus.FieldLogger) { } } +// Checks if a user is banned by another user, then it returns Not Found to the client and the function returns false +// Otherwise it returns true whithout sending anything to the client func SendNotFoundIfBanned(db database.AppDatabase, uid string, banner string, w http.ResponseWriter, l logrus.FieldLogger) bool { banned, err := db.IsBanned(uid, banner) if err != nil { diff --git a/service/api/helpers/get-limits.go b/service/api/helpers/get-limits.go index 4e76960..ad25ec2 100644 --- a/service/api/helpers/get-limits.go +++ b/service/api/helpers/get-limits.go @@ -6,10 +6,12 @@ import ( ) const ( - DEFAULT_LIMIT = 15 // don't know if should be moved to config + DEFAULT_LIMIT = 30 DEFAULT_OFFSET = 0 ) +// Get the start index and limit from the query. +// If they are not present, use the default values. func GetLimits(query url.Values) (int, int, error) { limit := DEFAULT_LIMIT diff --git a/service/api/helpers/regex-helpers.go b/service/api/helpers/regex-helpers.go new file mode 100644 index 0000000..4d7a64e --- /dev/null +++ b/service/api/helpers/regex-helpers.go @@ -0,0 +1,44 @@ +package helpers + +import ( + "net/http" + "regexp" + + "github.com/sirupsen/logrus" +) + +// Given a string, a regex and an error description, if the string doesn't match the regex, it sends a bad request error to the client and return false +// Otherwise it returns true without sending anything to the client +func MatchRegexOrBadRequest(str string, regex string, error_description string, w http.ResponseWriter, l logrus.FieldLogger) bool { + + stat, err := regexp.Match(regex, []byte(str)) + + if err != nil { + SendInternalError(err, "Error while matching username regex", w, l) + return false + } + + if !stat { + // string didn't match the regex, so it's invalid, let's send a bad request error + SendBadRequest(w, error_description, l) + return false + } + // string matched the regex, so it's valid + return true +} + +// Validates a username (must be between 3 and 16 characters long and can only contain letters, numbers and underscores) +func MatchUsernameOrBadRequest(username string, w http.ResponseWriter, l logrus.FieldLogger) bool { + return MatchRegexOrBadRequest(username, + `^[a-zA-Z0-9_]{3,16}$`, "Username must be between 3 and 16 characters long and can only contain letters, numbers and underscores", + w, + l) +} + +// Validates a comment (must be between 1 and 255 characters long) +func MatchCommentOrBadRequest(comment string, w http.ResponseWriter, l logrus.FieldLogger) bool { + return MatchRegexOrBadRequest(comment, + `^(.){1,255}$`, "Comment must be between 1 and 255 characters long", + w, + l) +} diff --git a/service/api/liveness.go b/service/api/liveness.go index 1325bd8..8795962 100644 --- a/service/api/liveness.go +++ b/service/api/liveness.go @@ -1,16 +1,18 @@ package api import ( - "github.com/julienschmidt/httprouter" "net/http" + + "github.com/julienschmidt/httprouter" + "github.com/notherealmarco/WASAPhoto/service/api/helpers" ) // liveness is an HTTP handler that checks the API server status. If the server cannot serve requests (e.g., some // resources are not ready), this should reply with HTTP Status 500. Otherwise, with HTTP Status 200 func (rt *_router) liveness(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - /* Example of liveness check: - if err := rt.DB.Ping(); err != nil { + if err := rt.db.Ping(); err != nil { w.WriteHeader(http.StatusInternalServerError) return - }*/ + } + helpers.SendStatus(200, w, "Server is live!", rt.baseLogger) } diff --git a/service/api/photos.go b/service/api/photos.go index 98e5ddd..684608c 100644 --- a/service/api/photos.go +++ b/service/api/photos.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "github.com/julienschmidt/httprouter" "github.com/notherealmarco/WASAPhoto/service/api/authorization" @@ -32,36 +33,50 @@ func (rt *_router) PostPhoto(w http.ResponseWriter, r *http.Request, ps httprout } path := rt.dataPath + "/photos/" + uid + "/" + strconv.FormatInt(photo_id, 10) + ".jpg" - // todo: we should check if the body is a valid jpg image if err = os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { // perms = 511 helpers.SendInternalError(err, "Error creating directory", w, rt.baseLogger) return } - file, err := os.Create(path) + /*file, err := os.Create(path) if err != nil { helpers.SendInternalError(err, "Error creating file", w, rt.baseLogger) helpers.RollbackOrLogError(transaction, rt.baseLogger) return + }*/ + + bytes, err := io.ReadAll(r.Body) + + if err != nil { + helpers.SendInternalError(err, "Error checking the file", w, rt.baseLogger) + helpers.RollbackOrLogError(transaction, rt.baseLogger) + return } - if _, err = io.Copy(file, r.Body); err != nil { + mimeType := http.DetectContentType(bytes) + + if !strings.HasPrefix(mimeType, "image/") { + helpers.SendStatus(http.StatusBadRequest, w, mimeType+" file is not a valid image", rt.baseLogger) + helpers.RollbackOrLogError(transaction, rt.baseLogger) + return + } + + if err = os.WriteFile(path, bytes, 0644); err != nil { helpers.SendInternalError(err, "Error writing the file", w, rt.baseLogger) helpers.RollbackOrLogError(transaction, rt.baseLogger) return } - if err = file.Close(); err != nil { + /*if err = file.Close(); err != nil { helpers.SendInternalError(err, "Error closing file", w, rt.baseLogger) helpers.RollbackOrLogError(transaction, rt.baseLogger) - } + }*/ err = transaction.Commit() if err != nil { helpers.SendInternalError(err, "Error committing transaction", w, rt.baseLogger) - //todo: should I roll back? return } @@ -139,7 +154,7 @@ func (rt *_router) DeletePhoto(w http.ResponseWriter, r *http.Request, ps httpro if err != nil { helpers.SendInternalError(err, "Error deleting photo from database", w, rt.baseLogger) return - } // todo: maybe let's use a transaction also here + } if !deleted { helpers.SendNotFound(w, "Photo not found", rt.baseLogger) diff --git a/service/api/post-session.go b/service/api/post-session.go index dd67174..faeb5ef 100644 --- a/service/api/post-session.go +++ b/service/api/post-session.go @@ -25,19 +25,36 @@ func (rt *_router) PostSession(w http.ResponseWriter, r *http.Request, ps httpro var request _reqbody err := json.NewDecoder(r.Body).Decode(&request) - var uid string - if err == nil { // test if user exists - uid, err = rt.db.GetUserID(request.Name) - } - if db_errors.EmptySet(err) { // user does not exist - uid, err = rt.db.CreateUser(request.Name) - } - if err != nil { // handle any other error + if err != nil { helpers.SendBadRequestError(err, "Bad request body", w, rt.baseLogger) return } + // test if user exists + var uid string + uid, err = rt.db.GetUserID(request.Name) + + // check if the database returned an empty set error, if so, create the new user + if db_errors.EmptySet(err) { + + // before creating the user, check if the name is valid, otherwise send a bad request error + if !helpers.MatchUsernameOrBadRequest(request.Name, w, rt.baseLogger) { + return + } + + uid, err = rt.db.CreateUser(request.Name) + } + + // handle database errors + if err != nil { + helpers.SendInternalError(err, "Database error", w, rt.baseLogger) + return + } + + // set the response header w.Header().Set("content-type", "application/json") + + // encode the response body err = json.NewEncoder(w).Encode(_respbody{UID: uid}) if err != nil { diff --git a/service/api/put-updateusername.go b/service/api/put-updateusername.go index d5d23d2..35a3f32 100644 --- a/service/api/put-updateusername.go +++ b/service/api/put-updateusername.go @@ -2,7 +2,6 @@ package api import ( "net/http" - "regexp" "github.com/julienschmidt/httprouter" "github.com/notherealmarco/WASAPhoto/service/api/authorization" @@ -15,33 +14,32 @@ import ( func (rt *_router) UpdateUsername(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { uid := ps.ByName("user_id") + + // check if the user is changing his own username if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, uid, rt.db, w, rt.baseLogger, http.StatusNotFound) { return } + + // decode request body var req structures.UserDetails if !helpers.DecodeJsonOrBadRequest(r.Body, w, &req, rt.baseLogger) { return } - stat, err := regexp.Match(`^[a-zA-Z0-9_]{3,16}$`, []byte(req.Name)) - - if err != nil { - helpers.SendInternalError(err, "Error while matching username", w, rt.baseLogger) - return - } - - if !stat { //todo: sta regex non me piace - helpers.SendBadRequest(w, "Username must be between 3 and 16 characters long and can only contain letters, numbers and underscores", rt.baseLogger) + // check if the username is valid, and if it's not, send a bad request error + if !helpers.MatchUsernameOrBadRequest(req.Name, w, rt.baseLogger) { return } status, err := rt.db.UpdateUsername(uid, req.Name) + // check if the username already exists if status == database.ERR_EXISTS { helpers.SendStatus(http.StatusConflict, w, "Username already exists", rt.baseLogger) return } + // handle any other database error if err != nil { helpers.SendInternalError(err, "Database error: UpdateUsername", w, rt.baseLogger) return diff --git a/service/api/reqcontext/context-auth.go b/service/api/reqcontext/context-auth.go index a8dad60..8bc2eec 100644 --- a/service/api/reqcontext/context-auth.go +++ b/service/api/reqcontext/context-auth.go @@ -11,9 +11,17 @@ const ( USER_NOT_FOUND = 3 ) +// Authorization is the interface for an authorization provider type Authorization interface { + // Returns the type of the authorization provider GetType() string + + // Returns the ID of the currently logged in user GetUserID() string + + // Checks if the token is valid Authorized(db database.AppDatabase) (AuthStatus, error) + + // Checks if the given user and the currently logged in user are the same user UserAuthorized(db database.AppDatabase, uid string) (AuthStatus, error) } diff --git a/service/database/database.go b/service/database/database.go index 0aeb9ad..b875f90 100644 --- a/service/database/database.go +++ b/service/database/database.go @@ -79,8 +79,11 @@ type AppDatabase interface { Ping() error } +// DBTransaction is the interface for a generic database transaction type DBTransaction interface { + // Commit commits the transaction Commit() error + // Rollback rolls back the transaction Rollback() error } @@ -95,16 +98,17 @@ func New(db *sql.DB) (AppDatabase, error) { return nil, errors.New("database is required when building a AppDatabase") } - // Check if tables exist. If not, the database is empty, and we need to create the structure + // Check if some table exists. If not, the database is empty, and we need to create the structure var tableName string - //todo: check for all the tables, not just users + err := db.QueryRow(`SELECT name FROM sqlite_master WHERE type='table' AND name='users';`).Scan(&tableName) if errors.Is(err, sql.ErrNoRows) { + // Database is empty, let's create the structure sqlStmt := `CREATE TABLE "users" ( "uid" TEXT NOT NULL, "name" TEXT NOT NULL UNIQUE, PRIMARY KEY("uid") - )` // todo: one query is enough! We are we doing a query per table? + )` _, err = db.Exec(sqlStmt) if err != nil { return nil, fmt.Errorf("error creating database structure: %w", err) diff --git a/service/database/db-comments.go b/service/database/db-comments.go index 9e4ff0c..3708732 100644 --- a/service/database/db-comments.go +++ b/service/database/db-comments.go @@ -103,6 +103,7 @@ func (db *appdbimpl) GetComments(uid string, photo_id int64, requesting_uid stri AND "bans"."ban" = ? ) AND "u"."uid" = "c"."user" + ORDER BY "c"."date" DESC LIMIT ? OFFSET ?`, photo_id, requesting_uid, limit, start_index) diff --git a/service/database/db-likes.go b/service/database/db-likes.go index a8b9a59..07198b6 100644 --- a/service/database/db-likes.go +++ b/service/database/db-likes.go @@ -96,6 +96,7 @@ func (db *appdbimpl) UnlikePhoto(uid string, photo int64, liker_uid string) (Que // But our DB implementation only requires the photo id. exists, err := db.photoExists(uid, photo) if err != nil || !exists { + // The photo does not exist, or the user has been banned return ERR_NOT_FOUND, err } @@ -111,6 +112,7 @@ func (db *appdbimpl) UnlikePhoto(uid string, photo int64, liker_uid string) (Que return ERR_INTERNAL, err } + // The user was not liking the photo if rows == 0 { return ERR_NOT_FOUND, nil } diff --git a/service/database/db-photos.go b/service/database/db-photos.go index 3ff7607..85af748 100644 --- a/service/database/db-photos.go +++ b/service/database/db-photos.go @@ -18,7 +18,6 @@ func (db *appdbimpl) PostPhoto(uid string) (DBTransaction, int64, error) { err_rb := tx.Rollback() // If rollback fails, we return the original error plus the rollback error if err_rb != nil { - // todo: we are losing track of err_rb here err = fmt.Errorf("Rollback error. Rollback cause: %w", err) } @@ -30,7 +29,6 @@ func (db *appdbimpl) PostPhoto(uid string) (DBTransaction, int64, error) { err_rb := tx.Rollback() // If rollback fails, we return the original error plus the rollback error if err_rb != nil { - // todo: we are losing track of err_rb here err = fmt.Errorf("Rollback error. Rollback cause: %w", err) } @@ -66,6 +64,7 @@ func (db *appdbimpl) photoExists(uid string, photo int64) (bool, error) { return cnt > 0, nil } +// Check if a given photo owned by a given user exists, and the requesting user is not banned by the author func (db *appdbimpl) PhotoExists(uid string, photo int64, requesting_uid string) (bool, error) { var cnt int64 diff --git a/service/database/db-profile.go b/service/database/db-profile.go index bf63fe6..2ee5751 100644 --- a/service/database/db-profile.go +++ b/service/database/db-profile.go @@ -5,8 +5,6 @@ import ( "github.com/notherealmarco/WASAPhoto/service/structures" ) -//this should be changed, but we need to change OpenAPI first - // Get user profile, including username, followers, following, and photos func (db *appdbimpl) GetUserProfile(uid string, requesting_uid string) (QueryResult, *structures.UserProfile, error) { // Get user info diff --git a/service/database/db-transactions.go b/service/database/db-transactions.go index 84966db..d78b95e 100644 --- a/service/database/db-transactions.go +++ b/service/database/db-transactions.go @@ -2,14 +2,17 @@ package database import "database/sql" +// dbtransaction is a struct to represent an SQL transaction, it implements the DBTransaction interface type dbtransaction struct { c *sql.Tx } func (tx *dbtransaction) Commit() error { + // Commit the SQL transaction return tx.c.Commit() } func (tx *dbtransaction) Rollback() error { + // Rollback the SQL transaction return tx.c.Rollback() } diff --git a/service/database/db-users.go b/service/database/db-users.go index 3d0bd80..098ae85 100644 --- a/service/database/db-users.go +++ b/service/database/db-users.go @@ -2,6 +2,7 @@ package database import ( "database/sql" + "errors" "github.com/gofrs/uuid" "github.com/notherealmarco/WASAPhoto/service/database/db_errors" @@ -41,28 +42,57 @@ func (db *appdbimpl) UserExistsNotBanned(uid string, requesting_uid string) (boo // Get user id by username func (db *appdbimpl) GetUserID(name string) (string, error) { var uid string - err := db.c.QueryRow(`SELECT "uid" FROM "users" WHERE "name" = ?`, name).Scan(&uid) + err := db.c.QueryRow(`SELECT "uid" FROM "users" WHERE "name" LIKE ?`, name).Scan(&uid) return uid, err } // Create a new user func (db *appdbimpl) CreateUser(name string) (string, error) { + + // check if username is taken (case insensitive) + exists, err := db.nameExists(name) + + if err != nil { + return "", err + } else if exists { + return "", errors.New("username already exists") + } + + // create new user id uid, err := uuid.NewV4() if err != nil { return "", err } + + // insert the new user into the database _, err = db.c.Exec(`INSERT INTO "users" ("uid", "name") VALUES (?, ?)`, uid.String(), name) return uid.String(), err } +// Check if username exists +func (db *appdbimpl) nameExists(name string) (bool, error) { + var cnt int + err := db.c.QueryRow(`SELECT COUNT(*) FROM "users" WHERE "name" LIKE ?`, name).Scan(&cnt) + if err != nil { + return false, err + } + return cnt > 0, nil +} + // Update username func (db *appdbimpl) UpdateUsername(uid string, name string) (QueryResult, error) { - _, err := db.c.Exec(`UPDATE "users" SET "name" = ? WHERE "uid" = ?`, name, uid) - if db_errors.UniqueViolation(err) { + // check if username is taken (case insensitive) + exists, err := db.nameExists(name) + + if err != nil { + return ERR_INTERNAL, err + } else if exists { return ERR_EXISTS, nil } + _, err = db.c.Exec(`UPDATE "users" SET "name" = ? WHERE "uid" = ?`, name, uid) + if err != nil { return ERR_INTERNAL, err } @@ -95,7 +125,7 @@ func (db *appdbimpl) GetUserFollowers(uid string, requesting_uid string, start_i AND "followed" = ? LIMIT ? - OFFSET ?`, uid, requesting_uid, limit, start_index) + OFFSET ?`, requesting_uid, uid, limit, start_index) followers, err := db.uidNameQuery(rows, err) @@ -107,7 +137,7 @@ func (db *appdbimpl) GetUserFollowers(uid string, requesting_uid string, start_i } // Get user following -func (db *appdbimpl) GetUserFollowing(uid string, requesting_uid string, start_index int, offset int) (QueryResult, *[]structures.UIDName, error) { +func (db *appdbimpl) GetUserFollowing(uid string, requesting_uid string, start_index int, limit int) (QueryResult, *[]structures.UIDName, error) { // user may exist but have no followers exists, err := db.UserExistsNotBanned(uid, requesting_uid) @@ -120,7 +150,7 @@ func (db *appdbimpl) GetUserFollowing(uid string, requesting_uid string, start_i return ERR_NOT_FOUND, nil, nil } - rows, err := db.c.Query(`SELECT "followed", "user"."name" FROM "follows", "users" + rows, err := db.c.Query(`SELECT "followed", "users"."name" FROM "follows", "users" WHERE "follows"."followed" = "users"."uid" AND "follows"."followed" NOT IN ( @@ -131,7 +161,7 @@ func (db *appdbimpl) GetUserFollowing(uid string, requesting_uid string, start_i AND "follower" = ? LIMIT ? - OFFSET ?`, uid, requesting_uid, offset, start_index) + OFFSET ?`, requesting_uid, uid, limit, start_index) following, err := db.uidNameQuery(rows, err) diff --git a/service/database/db_errors/db-errors.go b/service/database/db_errors/db-errors.go index 4584c40..84d01b4 100644 --- a/service/database/db_errors/db-errors.go +++ b/service/database/db_errors/db-errors.go @@ -2,7 +2,7 @@ package db_errors import "strings" -// Returns true if the query result has no rows +// Returns true if the error is a "no rows in result set" error func EmptySet(err error) bool { if err == nil { return false @@ -10,6 +10,7 @@ func EmptySet(err error) bool { return strings.Contains(err.Error(), "no rows in result set") } +// Returns true if the error is a Unique constraint violation error func UniqueViolation(err error) bool { if err == nil { return false @@ -17,6 +18,7 @@ func UniqueViolation(err error) bool { return strings.Contains(err.Error(), "UNIQUE constraint failed") } +// Returns true if the error is a Foreign Key constraint violation error func ForeignKeyViolation(err error) bool { if err == nil { return false diff --git a/service/database/set-name.go b/service/database/set-name.go deleted file mode 100644 index 83c68da..0000000 --- a/service/database/set-name.go +++ /dev/null @@ -1,7 +0,0 @@ -package database - -// SetName is an example that shows you how to execute insert/update -func (db *appdbimpl) SetName(name string) error { - _, err := db.c.Exec("INSERT INTO example_table (id, name) VALUES (1, ?)", name) - return err -} diff --git a/wasaphoto.db b/wasaphoto.db deleted file mode 100644 index 1ae71fe..0000000 Binary files a/wasaphoto.db and /dev/null differ diff --git a/webapi b/webapi deleted file mode 100755 index 0046906..0000000 Binary files a/webapi and /dev/null differ diff --git a/webui/index.html b/webui/index.html index e3e9658..b4fb313 100644 --- a/webui/index.html +++ b/webui/index.html @@ -1,7 +1,7 @@ - Example app + WASAPhoto diff --git a/webui/node_modules/vite/dist/node/chunks/dep-94c1417a.js b/webui/node_modules/vite/dist/node/chunks/dep-94c1417a.js index 94eabfb..f04be91 100644 --- a/webui/node_modules/vite/dist/node/chunks/dep-94c1417a.js +++ b/webui/node_modules/vite/dist/node/chunks/dep-94c1417a.js @@ -159,83 +159,83 @@ var pify$1 = pify$2.exports = function (obj, P, opts) { pify$1.all = pify$1; -var fs = require$$0__default; -var path$2 = require$$0; -var pify = pify$2.exports; - -var stat = pify(fs.stat); -var readFile = pify(fs.readFile); -var resolve = path$2.resolve; - -var cache = Object.create(null); - -function convert(content, encoding) { - if (Buffer.isEncoding(encoding)) { - return content.toString(encoding); - } - return content; -} - -readCache$1.exports = function (path, encoding) { - path = resolve(path); - - return stat(path).then(function (stats) { - var item = cache[path]; - - if (item && item.mtime.getTime() === stats.mtime.getTime()) { - return convert(item.content, encoding); - } - - return readFile(path).then(function (data) { - cache[path] = { - mtime: stats.mtime, - content: data - }; - - return convert(data, encoding); - }); - }).catch(function (err) { - cache[path] = null; - return Promise.reject(err); - }); -}; - -readCache$1.exports.sync = function (path, encoding) { - path = resolve(path); - - try { - var stats = fs.statSync(path); - var item = cache[path]; - - if (item && item.mtime.getTime() === stats.mtime.getTime()) { - return convert(item.content, encoding); - } - - var data = fs.readFileSync(path); - - cache[path] = { - mtime: stats.mtime, - content: data - }; - - return convert(data, encoding); - } catch (err) { - cache[path] = null; - throw err; - } - -}; - -readCache$1.exports.get = function (path, encoding) { - path = resolve(path); - if (cache[path]) { - return convert(cache[path].content, encoding); - } - return null; -}; - -readCache$1.exports.clear = function () { - cache = Object.create(null); +var fs = require$$0__default; +var path$2 = require$$0; +var pify = pify$2.exports; + +var stat = pify(fs.stat); +var readFile = pify(fs.readFile); +var resolve = path$2.resolve; + +var cache = Object.create(null); + +function convert(content, encoding) { + if (Buffer.isEncoding(encoding)) { + return content.toString(encoding); + } + return content; +} + +readCache$1.exports = function (path, encoding) { + path = resolve(path); + + return stat(path).then(function (stats) { + var item = cache[path]; + + if (item && item.mtime.getTime() === stats.mtime.getTime()) { + return convert(item.content, encoding); + } + + return readFile(path).then(function (data) { + cache[path] = { + mtime: stats.mtime, + content: data + }; + + return convert(data, encoding); + }); + }).catch(function (err) { + cache[path] = null; + return Promise.reject(err); + }); +}; + +readCache$1.exports.sync = function (path, encoding) { + path = resolve(path); + + try { + var stats = fs.statSync(path); + var item = cache[path]; + + if (item && item.mtime.getTime() === stats.mtime.getTime()) { + return convert(item.content, encoding); + } + + var data = fs.readFileSync(path); + + cache[path] = { + mtime: stats.mtime, + content: data + }; + + return convert(data, encoding); + } catch (err) { + cache[path] = null; + throw err; + } + +}; + +readCache$1.exports.get = function (path, encoding) { + path = resolve(path); + if (cache[path]) { + return convert(cache[path].content, encoding); + } + return null; +}; + +readCache$1.exports.clear = function () { + cache = Object.create(null); }; const readCache = readCache$1.exports; diff --git a/webui/node_modules/vite/dist/node/chunks/dep-9d3f225a.js b/webui/node_modules/vite/dist/node/chunks/dep-9d3f225a.js index 9b3ae72..415fb80 100644 --- a/webui/node_modules/vite/dist/node/chunks/dep-9d3f225a.js +++ b/webui/node_modules/vite/dist/node/chunks/dep-9d3f225a.js @@ -22,599 +22,599 @@ function _mergeNamespaces(n, m) { var compilerDom_cjs$2 = {}; -/** - * Make a map and return a function for checking if a key - * is in that map. - * IMPORTANT: all calls of this function must be prefixed with - * \/\*#\_\_PURE\_\_\*\/ - * So that rollup can tree-shake them if necessary. - */ -function makeMap(str, expectsLowerCase) { - const map = Object.create(null); - const list = str.split(','); - for (let i = 0; i < list.length; i++) { - map[list[i]] = true; - } - return expectsLowerCase ? val => !!map[val.toLowerCase()] : val => !!map[val]; +/** + * Make a map and return a function for checking if a key + * is in that map. + * IMPORTANT: all calls of this function must be prefixed with + * \/\*#\_\_PURE\_\_\*\/ + * So that rollup can tree-shake them if necessary. + */ +function makeMap(str, expectsLowerCase) { + const map = Object.create(null); + const list = str.split(','); + for (let i = 0; i < list.length; i++) { + map[list[i]] = true; + } + return expectsLowerCase ? val => !!map[val.toLowerCase()] : val => !!map[val]; } -/** - * dev only flag -> name mapping - */ -const PatchFlagNames = { - [1 /* TEXT */]: `TEXT`, - [2 /* CLASS */]: `CLASS`, - [4 /* STYLE */]: `STYLE`, - [8 /* PROPS */]: `PROPS`, - [16 /* FULL_PROPS */]: `FULL_PROPS`, - [32 /* HYDRATE_EVENTS */]: `HYDRATE_EVENTS`, - [64 /* STABLE_FRAGMENT */]: `STABLE_FRAGMENT`, - [128 /* KEYED_FRAGMENT */]: `KEYED_FRAGMENT`, - [256 /* UNKEYED_FRAGMENT */]: `UNKEYED_FRAGMENT`, - [512 /* NEED_PATCH */]: `NEED_PATCH`, - [1024 /* DYNAMIC_SLOTS */]: `DYNAMIC_SLOTS`, - [2048 /* DEV_ROOT_FRAGMENT */]: `DEV_ROOT_FRAGMENT`, - [-1 /* HOISTED */]: `HOISTED`, - [-2 /* BAIL */]: `BAIL` +/** + * dev only flag -> name mapping + */ +const PatchFlagNames = { + [1 /* TEXT */]: `TEXT`, + [2 /* CLASS */]: `CLASS`, + [4 /* STYLE */]: `STYLE`, + [8 /* PROPS */]: `PROPS`, + [16 /* FULL_PROPS */]: `FULL_PROPS`, + [32 /* HYDRATE_EVENTS */]: `HYDRATE_EVENTS`, + [64 /* STABLE_FRAGMENT */]: `STABLE_FRAGMENT`, + [128 /* KEYED_FRAGMENT */]: `KEYED_FRAGMENT`, + [256 /* UNKEYED_FRAGMENT */]: `UNKEYED_FRAGMENT`, + [512 /* NEED_PATCH */]: `NEED_PATCH`, + [1024 /* DYNAMIC_SLOTS */]: `DYNAMIC_SLOTS`, + [2048 /* DEV_ROOT_FRAGMENT */]: `DEV_ROOT_FRAGMENT`, + [-1 /* HOISTED */]: `HOISTED`, + [-2 /* BAIL */]: `BAIL` }; -/** - * Dev only - */ -const slotFlagsText = { - [1 /* STABLE */]: 'STABLE', - [2 /* DYNAMIC */]: 'DYNAMIC', - [3 /* FORWARDED */]: 'FORWARDED' +/** + * Dev only + */ +const slotFlagsText = { + [1 /* STABLE */]: 'STABLE', + [2 /* DYNAMIC */]: 'DYNAMIC', + [3 /* FORWARDED */]: 'FORWARDED' }; -const GLOBALS_WHITE_LISTED = 'Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,' + - 'decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,' + - 'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt'; +const GLOBALS_WHITE_LISTED = 'Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,' + + 'decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,' + + 'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt'; const isGloballyWhitelisted = /*#__PURE__*/ makeMap(GLOBALS_WHITE_LISTED); -const range = 2; -function generateCodeFrame(source, start = 0, end = source.length) { - // Split the content into individual lines but capture the newline sequence - // that separated each line. This is important because the actual sequence is - // needed to properly take into account the full line length for offset - // comparison - let lines = source.split(/(\r?\n)/); - // Separate the lines and newline sequences into separate arrays for easier referencing - const newlineSequences = lines.filter((_, idx) => idx % 2 === 1); - lines = lines.filter((_, idx) => idx % 2 === 0); - let count = 0; - const res = []; - for (let i = 0; i < lines.length; i++) { - count += - lines[i].length + - ((newlineSequences[i] && newlineSequences[i].length) || 0); - if (count >= start) { - for (let j = i - range; j <= i + range || end > count; j++) { - if (j < 0 || j >= lines.length) - continue; - const line = j + 1; - res.push(`${line}${' '.repeat(Math.max(3 - String(line).length, 0))}| ${lines[j]}`); - const lineLength = lines[j].length; - const newLineSeqLength = (newlineSequences[j] && newlineSequences[j].length) || 0; - if (j === i) { - // push underline - const pad = start - (count - (lineLength + newLineSeqLength)); - const length = Math.max(1, end > count ? lineLength - pad : end - start); - res.push(` | ` + ' '.repeat(pad) + '^'.repeat(length)); - } - else if (j > i) { - if (end > count) { - const length = Math.max(Math.min(end - count, lineLength), 1); - res.push(` | ` + '^'.repeat(length)); - } - count += lineLength + newLineSeqLength; - } - } - break; - } - } - return res.join('\n'); +const range = 2; +function generateCodeFrame(source, start = 0, end = source.length) { + // Split the content into individual lines but capture the newline sequence + // that separated each line. This is important because the actual sequence is + // needed to properly take into account the full line length for offset + // comparison + let lines = source.split(/(\r?\n)/); + // Separate the lines and newline sequences into separate arrays for easier referencing + const newlineSequences = lines.filter((_, idx) => idx % 2 === 1); + lines = lines.filter((_, idx) => idx % 2 === 0); + let count = 0; + const res = []; + for (let i = 0; i < lines.length; i++) { + count += + lines[i].length + + ((newlineSequences[i] && newlineSequences[i].length) || 0); + if (count >= start) { + for (let j = i - range; j <= i + range || end > count; j++) { + if (j < 0 || j >= lines.length) + continue; + const line = j + 1; + res.push(`${line}${' '.repeat(Math.max(3 - String(line).length, 0))}| ${lines[j]}`); + const lineLength = lines[j].length; + const newLineSeqLength = (newlineSequences[j] && newlineSequences[j].length) || 0; + if (j === i) { + // push underline + const pad = start - (count - (lineLength + newLineSeqLength)); + const length = Math.max(1, end > count ? lineLength - pad : end - start); + res.push(` | ` + ' '.repeat(pad) + '^'.repeat(length)); + } + else if (j > i) { + if (end > count) { + const length = Math.max(Math.min(end - count, lineLength), 1); + res.push(` | ` + '^'.repeat(length)); + } + count += lineLength + newLineSeqLength; + } + } + break; + } + } + return res.join('\n'); } -/** - * On the client we only need to offer special cases for boolean attributes that - * have different names from their corresponding dom properties: - * - itemscope -> N/A - * - allowfullscreen -> allowFullscreen - * - formnovalidate -> formNoValidate - * - ismap -> isMap - * - nomodule -> noModule - * - novalidate -> noValidate - * - readonly -> readOnly - */ -const specialBooleanAttrs = `itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`; -const isSpecialBooleanAttr = /*#__PURE__*/ makeMap(specialBooleanAttrs); -/** - * The full list is needed during SSR to produce the correct initial markup. - */ -const isBooleanAttr = /*#__PURE__*/ makeMap(specialBooleanAttrs + - `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,` + - `loop,open,required,reversed,scoped,seamless,` + - `checked,muted,multiple,selected`); -/** - * Boolean attributes should be included if the value is truthy or ''. - * e.g. `` compiles to `{ multiple: '' }` + */ +function includeBooleanAttr(value) { + return !!value || value === ''; +} +const unsafeAttrCharRE = /[>/="'\u0009\u000a\u000c\u0020]/; +const attrValidationCache = {}; +function isSSRSafeAttrName(name) { + if (attrValidationCache.hasOwnProperty(name)) { + return attrValidationCache[name]; + } + const isUnsafe = unsafeAttrCharRE.test(name); + if (isUnsafe) { + console.error(`unsafe attribute name: ${name}`); + } + return (attrValidationCache[name] = !isUnsafe); +} +const propsToAttrMap = { + acceptCharset: 'accept-charset', + className: 'class', + htmlFor: 'for', + httpEquiv: 'http-equiv' +}; +/** + * CSS properties that accept plain numbers + */ +const isNoUnitNumericStyleProp = /*#__PURE__*/ makeMap(`animation-iteration-count,border-image-outset,border-image-slice,` + + `border-image-width,box-flex,box-flex-group,box-ordinal-group,column-count,` + + `columns,flex,flex-grow,flex-positive,flex-shrink,flex-negative,flex-order,` + + `grid-row,grid-row-end,grid-row-span,grid-row-start,grid-column,` + + `grid-column-end,grid-column-span,grid-column-start,font-weight,line-clamp,` + + `line-height,opacity,order,orphans,tab-size,widows,z-index,zoom,` + + // SVG + `fill-opacity,flood-opacity,stop-opacity,stroke-dasharray,stroke-dashoffset,` + + `stroke-miterlimit,stroke-opacity,stroke-width`); +/** + * Known attributes, this is used for stringification of runtime static nodes + * so that we don't stringify bindings that cannot be set from HTML. + * Don't also forget to allow `data-*` and `aria-*`! + * Generated from https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes + */ +const isKnownHtmlAttr = /*#__PURE__*/ makeMap(`accept,accept-charset,accesskey,action,align,allow,alt,async,` + + `autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,` + + `border,buffered,capture,challenge,charset,checked,cite,class,code,` + + `codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,` + + `coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,` + + `disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,` + + `formaction,formenctype,formmethod,formnovalidate,formtarget,headers,` + + `height,hidden,high,href,hreflang,http-equiv,icon,id,importance,integrity,` + + `ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,` + + `manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,` + + `open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,` + + `referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,` + + `selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,` + + `start,step,style,summary,tabindex,target,title,translate,type,usemap,` + + `value,width,wrap`); +/** + * Generated from https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute + */ +const isKnownSvgAttr = /*#__PURE__*/ makeMap(`xmlns,accent-height,accumulate,additive,alignment-baseline,alphabetic,amplitude,` + + `arabic-form,ascent,attributeName,attributeType,azimuth,baseFrequency,` + + `baseline-shift,baseProfile,bbox,begin,bias,by,calcMode,cap-height,class,` + + `clip,clipPathUnits,clip-path,clip-rule,color,color-interpolation,` + + `color-interpolation-filters,color-profile,color-rendering,` + + `contentScriptType,contentStyleType,crossorigin,cursor,cx,cy,d,decelerate,` + + `descent,diffuseConstant,direction,display,divisor,dominant-baseline,dur,dx,` + + `dy,edgeMode,elevation,enable-background,end,exponent,fill,fill-opacity,` + + `fill-rule,filter,filterRes,filterUnits,flood-color,flood-opacity,` + + `font-family,font-size,font-size-adjust,font-stretch,font-style,` + + `font-variant,font-weight,format,from,fr,fx,fy,g1,g2,glyph-name,` + + `glyph-orientation-horizontal,glyph-orientation-vertical,glyphRef,` + + `gradientTransform,gradientUnits,hanging,height,href,hreflang,horiz-adv-x,` + + `horiz-origin-x,id,ideographic,image-rendering,in,in2,intercept,k,k1,k2,k3,` + + `k4,kernelMatrix,kernelUnitLength,kerning,keyPoints,keySplines,keyTimes,` + + `lang,lengthAdjust,letter-spacing,lighting-color,limitingConeAngle,local,` + + `marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,` + + `mask,maskContentUnits,maskUnits,mathematical,max,media,method,min,mode,` + + `name,numOctaves,offset,opacity,operator,order,orient,orientation,origin,` + + `overflow,overline-position,overline-thickness,panose-1,paint-order,path,` + + `pathLength,patternContentUnits,patternTransform,patternUnits,ping,` + + `pointer-events,points,pointsAtX,pointsAtY,pointsAtZ,preserveAlpha,` + + `preserveAspectRatio,primitiveUnits,r,radius,referrerPolicy,refX,refY,rel,` + + `rendering-intent,repeatCount,repeatDur,requiredExtensions,requiredFeatures,` + + `restart,result,rotate,rx,ry,scale,seed,shape-rendering,slope,spacing,` + + `specularConstant,specularExponent,speed,spreadMethod,startOffset,` + + `stdDeviation,stemh,stemv,stitchTiles,stop-color,stop-opacity,` + + `strikethrough-position,strikethrough-thickness,string,stroke,` + + `stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,` + + `stroke-miterlimit,stroke-opacity,stroke-width,style,surfaceScale,` + + `systemLanguage,tabindex,tableValues,target,targetX,targetY,text-anchor,` + + `text-decoration,text-rendering,textLength,to,transform,transform-origin,` + + `type,u1,u2,underline-position,underline-thickness,unicode,unicode-bidi,` + + `unicode-range,units-per-em,v-alphabetic,v-hanging,v-ideographic,` + + `v-mathematical,values,vector-effect,version,vert-adv-y,vert-origin-x,` + + `vert-origin-y,viewBox,viewTarget,visibility,width,widths,word-spacing,` + + `writing-mode,x,x-height,x1,x2,xChannelSelector,xlink:actuate,xlink:arcrole,` + + `xlink:href,xlink:role,xlink:show,xlink:title,xlink:type,xml:base,xml:lang,` + `xml:space,y,y1,y2,yChannelSelector,z,zoomAndPan`); -function normalizeStyle(value) { - if (isArray(value)) { - const res = {}; - for (let i = 0; i < value.length; i++) { - const item = value[i]; - const normalized = isString(item) - ? parseStringStyle(item) - : normalizeStyle(item); - if (normalized) { - for (const key in normalized) { - res[key] = normalized[key]; - } - } - } - return res; - } - else if (isString(value)) { - return value; - } - else if (isObject(value)) { - return value; - } -} -const listDelimiterRE = /;(?![^(]*\))/g; -const propertyDelimiterRE = /:(.+)/; -function parseStringStyle(cssText) { - const ret = {}; - cssText.split(listDelimiterRE).forEach(item => { - if (item) { - const tmp = item.split(propertyDelimiterRE); - tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim()); - } - }); - return ret; -} -function stringifyStyle(styles) { - let ret = ''; - if (!styles || isString(styles)) { - return ret; - } - for (const key in styles) { - const value = styles[key]; - const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key); - if (isString(value) || - (typeof value === 'number' && isNoUnitNumericStyleProp(normalizedKey))) { - // only render valid values - ret += `${normalizedKey}:${value};`; - } - } - return ret; -} -function normalizeClass(value) { - let res = ''; - if (isString(value)) { - res = value; - } - else if (isArray(value)) { - for (let i = 0; i < value.length; i++) { - const normalized = normalizeClass(value[i]); - if (normalized) { - res += normalized + ' '; - } - } - } - else if (isObject(value)) { - for (const name in value) { - if (value[name]) { - res += name + ' '; - } - } - } - return res.trim(); -} -function normalizeProps(props) { - if (!props) - return null; - let { class: klass, style } = props; - if (klass && !isString(klass)) { - props.class = normalizeClass(klass); - } - if (style) { - props.style = normalizeStyle(style); - } - return props; +function normalizeStyle(value) { + if (isArray(value)) { + const res = {}; + for (let i = 0; i < value.length; i++) { + const item = value[i]; + const normalized = isString(item) + ? parseStringStyle(item) + : normalizeStyle(item); + if (normalized) { + for (const key in normalized) { + res[key] = normalized[key]; + } + } + } + return res; + } + else if (isString(value)) { + return value; + } + else if (isObject(value)) { + return value; + } +} +const listDelimiterRE = /;(?![^(]*\))/g; +const propertyDelimiterRE = /:(.+)/; +function parseStringStyle(cssText) { + const ret = {}; + cssText.split(listDelimiterRE).forEach(item => { + if (item) { + const tmp = item.split(propertyDelimiterRE); + tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim()); + } + }); + return ret; +} +function stringifyStyle(styles) { + let ret = ''; + if (!styles || isString(styles)) { + return ret; + } + for (const key in styles) { + const value = styles[key]; + const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key); + if (isString(value) || + (typeof value === 'number' && isNoUnitNumericStyleProp(normalizedKey))) { + // only render valid values + ret += `${normalizedKey}:${value};`; + } + } + return ret; +} +function normalizeClass(value) { + let res = ''; + if (isString(value)) { + res = value; + } + else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + const normalized = normalizeClass(value[i]); + if (normalized) { + res += normalized + ' '; + } + } + } + else if (isObject(value)) { + for (const name in value) { + if (value[name]) { + res += name + ' '; + } + } + } + return res.trim(); +} +function normalizeProps(props) { + if (!props) + return null; + let { class: klass, style } = props; + if (klass && !isString(klass)) { + props.class = normalizeClass(klass); + } + if (style) { + props.style = normalizeStyle(style); + } + return props; } -// These tag configs are shared between compiler-dom and runtime-dom, so they -// https://developer.mozilla.org/en-US/docs/Web/HTML/Element -const HTML_TAGS = 'html,body,base,head,link,meta,style,title,address,article,aside,footer,' + - 'header,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,' + - 'figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,' + - 'data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,' + - 'time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,' + - 'canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,' + - 'th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,' + - 'option,output,progress,select,textarea,details,dialog,menu,' + - 'summary,template,blockquote,iframe,tfoot'; -// https://developer.mozilla.org/en-US/docs/Web/SVG/Element -const SVG_TAGS = 'svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,' + - 'defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,' + - 'feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,' + - 'feDistanceLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,' + - 'feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,' + - 'fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,' + - 'foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,' + - 'mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,' + - 'polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,' + - 'text,textPath,title,tspan,unknown,use,view'; -const VOID_TAGS = 'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr'; -/** - * Compiler only. - * Do NOT use in runtime code paths unless behind `(process.env.NODE_ENV !== 'production')` flag. - */ -const isHTMLTag = /*#__PURE__*/ makeMap(HTML_TAGS); -/** - * Compiler only. - * Do NOT use in runtime code paths unless behind `(process.env.NODE_ENV !== 'production')` flag. - */ -const isSVGTag = /*#__PURE__*/ makeMap(SVG_TAGS); -/** - * Compiler only. - * Do NOT use in runtime code paths unless behind `(process.env.NODE_ENV !== 'production')` flag. - */ +// These tag configs are shared between compiler-dom and runtime-dom, so they +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element +const HTML_TAGS = 'html,body,base,head,link,meta,style,title,address,article,aside,footer,' + + 'header,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,' + + 'figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,' + + 'data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,' + + 'time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,' + + 'canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,' + + 'th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,' + + 'option,output,progress,select,textarea,details,dialog,menu,' + + 'summary,template,blockquote,iframe,tfoot'; +// https://developer.mozilla.org/en-US/docs/Web/SVG/Element +const SVG_TAGS = 'svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,' + + 'defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,' + + 'feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,' + + 'feDistanceLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,' + + 'feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,' + + 'fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,' + + 'foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,' + + 'mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,' + + 'polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,' + + 'text,textPath,title,tspan,unknown,use,view'; +const VOID_TAGS = 'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr'; +/** + * Compiler only. + * Do NOT use in runtime code paths unless behind `(process.env.NODE_ENV !== 'production')` flag. + */ +const isHTMLTag = /*#__PURE__*/ makeMap(HTML_TAGS); +/** + * Compiler only. + * Do NOT use in runtime code paths unless behind `(process.env.NODE_ENV !== 'production')` flag. + */ +const isSVGTag = /*#__PURE__*/ makeMap(SVG_TAGS); +/** + * Compiler only. + * Do NOT use in runtime code paths unless behind `(process.env.NODE_ENV !== 'production')` flag. + */ const isVoidTag = /*#__PURE__*/ makeMap(VOID_TAGS); -const escapeRE = /["'&<>]/; -function escapeHtml(string) { - const str = '' + string; - const match = escapeRE.exec(str); - if (!match) { - return str; - } - let html = ''; - let escaped; - let index; - let lastIndex = 0; - for (index = match.index; index < str.length; index++) { - switch (str.charCodeAt(index)) { - case 34: // " - escaped = '"'; - break; - case 38: // & - escaped = '&'; - break; - case 39: // ' - escaped = '''; - break; - case 60: // < - escaped = '<'; - break; - case 62: // > - escaped = '>'; - break; - default: - continue; - } - if (lastIndex !== index) { - html += str.slice(lastIndex, index); - } - lastIndex = index + 1; - html += escaped; - } - return lastIndex !== index ? html + str.slice(lastIndex, index) : html; -} -// https://www.w3.org/TR/html52/syntax.html#comments -const commentStripRE = /^-?>||--!>|]/; +function escapeHtml(string) { + const str = '' + string; + const match = escapeRE.exec(str); + if (!match) { + return str; + } + let html = ''; + let escaped; + let index; + let lastIndex = 0; + for (index = match.index; index < str.length; index++) { + switch (str.charCodeAt(index)) { + case 34: // " + escaped = '"'; + break; + case 38: // & + escaped = '&'; + break; + case 39: // ' + escaped = '''; + break; + case 60: // < + escaped = '<'; + break; + case 62: // > + escaped = '>'; + break; + default: + continue; + } + if (lastIndex !== index) { + html += str.slice(lastIndex, index); + } + lastIndex = index + 1; + html += escaped; + } + return lastIndex !== index ? html + str.slice(lastIndex, index) : html; +} +// https://www.w3.org/TR/html52/syntax.html#comments +const commentStripRE = /^-?>||--!>| looseEqual(item, val)); +function looseCompareArrays(a, b) { + if (a.length !== b.length) + return false; + let equal = true; + for (let i = 0; equal && i < a.length; i++) { + equal = looseEqual(a[i], b[i]); + } + return equal; +} +function looseEqual(a, b) { + if (a === b) + return true; + let aValidType = isDate(a); + let bValidType = isDate(b); + if (aValidType || bValidType) { + return aValidType && bValidType ? a.getTime() === b.getTime() : false; + } + aValidType = isSymbol(a); + bValidType = isSymbol(b); + if (aValidType || bValidType) { + return a === b; + } + aValidType = isArray(a); + bValidType = isArray(b); + if (aValidType || bValidType) { + return aValidType && bValidType ? looseCompareArrays(a, b) : false; + } + aValidType = isObject(a); + bValidType = isObject(b); + if (aValidType || bValidType) { + /* istanbul ignore if: this if will probably never be called */ + if (!aValidType || !bValidType) { + return false; + } + const aKeysCount = Object.keys(a).length; + const bKeysCount = Object.keys(b).length; + if (aKeysCount !== bKeysCount) { + return false; + } + for (const key in a) { + const aHasKey = a.hasOwnProperty(key); + const bHasKey = b.hasOwnProperty(key); + if ((aHasKey && !bHasKey) || + (!aHasKey && bHasKey) || + !looseEqual(a[key], b[key])) { + return false; + } + } + } + return String(a) === String(b); +} +function looseIndexOf(arr, val) { + return arr.findIndex(item => looseEqual(item, val)); } -/** - * For converting {{ interpolation }} values to displayed strings. - * @private - */ -const toDisplayString = (val) => { - return isString(val) - ? val - : val == null - ? '' - : isArray(val) || - (isObject(val) && - (val.toString === objectToString || !isFunction(val.toString))) - ? JSON.stringify(val, replacer, 2) - : String(val); -}; -const replacer = (_key, val) => { - // can't use isRef here since @vue/shared has no deps - if (val && val.__v_isRef) { - return replacer(_key, val.value); - } - else if (isMap(val)) { - return { - [`Map(${val.size})`]: [...val.entries()].reduce((entries, [key, val]) => { - entries[`${key} =>`] = val; - return entries; - }, {}) - }; - } - else if (isSet(val)) { - return { - [`Set(${val.size})`]: [...val.values()] - }; - } - else if (isObject(val) && !isArray(val) && !isPlainObject(val)) { - return String(val); - } - return val; +/** + * For converting {{ interpolation }} values to displayed strings. + * @private + */ +const toDisplayString = (val) => { + return isString(val) + ? val + : val == null + ? '' + : isArray(val) || + (isObject(val) && + (val.toString === objectToString || !isFunction(val.toString))) + ? JSON.stringify(val, replacer, 2) + : String(val); +}; +const replacer = (_key, val) => { + // can't use isRef here since @vue/shared has no deps + if (val && val.__v_isRef) { + return replacer(_key, val.value); + } + else if (isMap(val)) { + return { + [`Map(${val.size})`]: [...val.entries()].reduce((entries, [key, val]) => { + entries[`${key} =>`] = val; + return entries; + }, {}) + }; + } + else if (isSet(val)) { + return { + [`Set(${val.size})`]: [...val.values()] + }; + } + else if (isObject(val) && !isArray(val) && !isPlainObject(val)) { + return String(val); + } + return val; }; -const EMPTY_OBJ = (process.env.NODE_ENV !== 'production') - ? Object.freeze({}) - : {}; -const EMPTY_ARR = (process.env.NODE_ENV !== 'production') ? Object.freeze([]) : []; -const NOOP = () => { }; -/** - * Always return false. - */ -const NO = () => false; -const onRE = /^on[^a-z]/; -const isOn = (key) => onRE.test(key); -const isModelListener = (key) => key.startsWith('onUpdate:'); -const extend = Object.assign; -const remove = (arr, el) => { - const i = arr.indexOf(el); - if (i > -1) { - arr.splice(i, 1); - } -}; -const hasOwnProperty = Object.prototype.hasOwnProperty; -const hasOwn = (val, key) => hasOwnProperty.call(val, key); -const isArray = Array.isArray; -const isMap = (val) => toTypeString(val) === '[object Map]'; -const isSet = (val) => toTypeString(val) === '[object Set]'; -const isDate = (val) => toTypeString(val) === '[object Date]'; -const isFunction = (val) => typeof val === 'function'; -const isString = (val) => typeof val === 'string'; -const isSymbol = (val) => typeof val === 'symbol'; -const isObject = (val) => val !== null && typeof val === 'object'; -const isPromise = (val) => { - return isObject(val) && isFunction(val.then) && isFunction(val.catch); -}; -const objectToString = Object.prototype.toString; -const toTypeString = (value) => objectToString.call(value); -const toRawType = (value) => { - // extract "RawType" from strings like "[object RawType]" - return toTypeString(value).slice(8, -1); -}; -const isPlainObject = (val) => toTypeString(val) === '[object Object]'; -const isIntegerKey = (key) => isString(key) && - key !== 'NaN' && - key[0] !== '-' && - '' + parseInt(key, 10) === key; -const isReservedProp = /*#__PURE__*/ makeMap( -// the leading comma is intentional so empty string "" is also included -',key,ref,ref_for,ref_key,' + - 'onVnodeBeforeMount,onVnodeMounted,' + - 'onVnodeBeforeUpdate,onVnodeUpdated,' + - 'onVnodeBeforeUnmount,onVnodeUnmounted'); -const isBuiltInDirective = /*#__PURE__*/ makeMap('bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo'); -const cacheStringFunction$1 = (fn) => { - const cache = Object.create(null); - return ((str) => { - const hit = cache[str]; - return hit || (cache[str] = fn(str)); - }); -}; -const camelizeRE$1 = /-(\w)/g; -/** - * @private - */ -const camelize$1 = cacheStringFunction$1((str) => { - return str.replace(camelizeRE$1, (_, c) => (c ? c.toUpperCase() : '')); -}); -const hyphenateRE = /\B([A-Z])/g; -/** - * @private - */ -const hyphenate = cacheStringFunction$1((str) => str.replace(hyphenateRE, '-$1').toLowerCase()); -/** - * @private - */ -const capitalize = cacheStringFunction$1((str) => str.charAt(0).toUpperCase() + str.slice(1)); -/** - * @private - */ -const toHandlerKey = cacheStringFunction$1((str) => str ? `on${capitalize(str)}` : ``); -// compare whether a value has changed, accounting for NaN. -const hasChanged = (value, oldValue) => !Object.is(value, oldValue); -const invokeArrayFns = (fns, arg) => { - for (let i = 0; i < fns.length; i++) { - fns[i](arg); - } -}; -const def = (obj, key, value) => { - Object.defineProperty(obj, key, { - configurable: true, - enumerable: false, - value - }); -}; -const toNumber = (val) => { - const n = parseFloat(val); - return isNaN(n) ? val : n; -}; -let _globalThis; -const getGlobalThis = () => { - return (_globalThis || - (_globalThis = - typeof globalThis !== 'undefined' - ? globalThis - : typeof self !== 'undefined' - ? self - : typeof window !== 'undefined' - ? window - : typeof global !== 'undefined' - ? global - : {})); -}; -const identRE = /^[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*$/; -function genPropsAccessExp(name) { - return identRE.test(name) - ? `__props.${name}` - : `__props[${JSON.stringify(name)}]`; +const EMPTY_OBJ = (process.env.NODE_ENV !== 'production') + ? Object.freeze({}) + : {}; +const EMPTY_ARR = (process.env.NODE_ENV !== 'production') ? Object.freeze([]) : []; +const NOOP = () => { }; +/** + * Always return false. + */ +const NO = () => false; +const onRE = /^on[^a-z]/; +const isOn = (key) => onRE.test(key); +const isModelListener = (key) => key.startsWith('onUpdate:'); +const extend = Object.assign; +const remove = (arr, el) => { + const i = arr.indexOf(el); + if (i > -1) { + arr.splice(i, 1); + } +}; +const hasOwnProperty = Object.prototype.hasOwnProperty; +const hasOwn = (val, key) => hasOwnProperty.call(val, key); +const isArray = Array.isArray; +const isMap = (val) => toTypeString(val) === '[object Map]'; +const isSet = (val) => toTypeString(val) === '[object Set]'; +const isDate = (val) => toTypeString(val) === '[object Date]'; +const isFunction = (val) => typeof val === 'function'; +const isString = (val) => typeof val === 'string'; +const isSymbol = (val) => typeof val === 'symbol'; +const isObject = (val) => val !== null && typeof val === 'object'; +const isPromise = (val) => { + return isObject(val) && isFunction(val.then) && isFunction(val.catch); +}; +const objectToString = Object.prototype.toString; +const toTypeString = (value) => objectToString.call(value); +const toRawType = (value) => { + // extract "RawType" from strings like "[object RawType]" + return toTypeString(value).slice(8, -1); +}; +const isPlainObject = (val) => toTypeString(val) === '[object Object]'; +const isIntegerKey = (key) => isString(key) && + key !== 'NaN' && + key[0] !== '-' && + '' + parseInt(key, 10) === key; +const isReservedProp = /*#__PURE__*/ makeMap( +// the leading comma is intentional so empty string "" is also included +',key,ref,ref_for,ref_key,' + + 'onVnodeBeforeMount,onVnodeMounted,' + + 'onVnodeBeforeUpdate,onVnodeUpdated,' + + 'onVnodeBeforeUnmount,onVnodeUnmounted'); +const isBuiltInDirective = /*#__PURE__*/ makeMap('bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo'); +const cacheStringFunction$1 = (fn) => { + const cache = Object.create(null); + return ((str) => { + const hit = cache[str]; + return hit || (cache[str] = fn(str)); + }); +}; +const camelizeRE$1 = /-(\w)/g; +/** + * @private + */ +const camelize$1 = cacheStringFunction$1((str) => { + return str.replace(camelizeRE$1, (_, c) => (c ? c.toUpperCase() : '')); +}); +const hyphenateRE = /\B([A-Z])/g; +/** + * @private + */ +const hyphenate = cacheStringFunction$1((str) => str.replace(hyphenateRE, '-$1').toLowerCase()); +/** + * @private + */ +const capitalize = cacheStringFunction$1((str) => str.charAt(0).toUpperCase() + str.slice(1)); +/** + * @private + */ +const toHandlerKey = cacheStringFunction$1((str) => str ? `on${capitalize(str)}` : ``); +// compare whether a value has changed, accounting for NaN. +const hasChanged = (value, oldValue) => !Object.is(value, oldValue); +const invokeArrayFns = (fns, arg) => { + for (let i = 0; i < fns.length; i++) { + fns[i](arg); + } +}; +const def = (obj, key, value) => { + Object.defineProperty(obj, key, { + configurable: true, + enumerable: false, + value + }); +}; +const toNumber = (val) => { + const n = parseFloat(val); + return isNaN(n) ? val : n; +}; +let _globalThis; +const getGlobalThis = () => { + return (_globalThis || + (_globalThis = + typeof globalThis !== 'undefined' + ? globalThis + : typeof self !== 'undefined' + ? self + : typeof window !== 'undefined' + ? window + : typeof global !== 'undefined' + ? global + : {})); +}; +const identRE = /^[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*$/; +function genPropsAccessExp(name) { + return identRE.test(name) + ? `__props.${name}` + : `__props[${JSON.stringify(name)}]`; } var shared_esmBundler = { @@ -682,4855 +682,4855 @@ var shared_esmBundler = { toTypeString: toTypeString }; -function defaultOnError(error) { - throw error; -} -function defaultOnWarn(msg) { - (process.env.NODE_ENV !== 'production') && console.warn(`[Vue warn] ${msg.message}`); -} -function createCompilerError(code, loc, messages, additionalMessage) { - const msg = (process.env.NODE_ENV !== 'production') || !true - ? (messages || errorMessages)[code] + (additionalMessage || ``) - : code; - const error = new SyntaxError(String(msg)); - error.code = code; - error.loc = loc; - return error; -} -const errorMessages = { - // parse errors - [0 /* ABRUPT_CLOSING_OF_EMPTY_COMMENT */]: 'Illegal comment.', - [1 /* CDATA_IN_HTML_CONTENT */]: 'CDATA section is allowed only in XML context.', - [2 /* DUPLICATE_ATTRIBUTE */]: 'Duplicate attribute.', - [3 /* END_TAG_WITH_ATTRIBUTES */]: 'End tag cannot have attributes.', - [4 /* END_TAG_WITH_TRAILING_SOLIDUS */]: "Illegal '/' in tags.", - [5 /* EOF_BEFORE_TAG_NAME */]: 'Unexpected EOF in tag.', - [6 /* EOF_IN_CDATA */]: 'Unexpected EOF in CDATA section.', - [7 /* EOF_IN_COMMENT */]: 'Unexpected EOF in comment.', - [8 /* EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT */]: 'Unexpected EOF in script.', - [9 /* EOF_IN_TAG */]: 'Unexpected EOF in tag.', - [10 /* INCORRECTLY_CLOSED_COMMENT */]: 'Incorrectly closed comment.', - [11 /* INCORRECTLY_OPENED_COMMENT */]: 'Incorrectly opened comment.', - [12 /* INVALID_FIRST_CHARACTER_OF_TAG_NAME */]: "Illegal tag name. Use '<' to print '<'.", - [13 /* MISSING_ATTRIBUTE_VALUE */]: 'Attribute value was expected.', - [14 /* MISSING_END_TAG_NAME */]: 'End tag name was expected.', - [15 /* MISSING_WHITESPACE_BETWEEN_ATTRIBUTES */]: 'Whitespace was expected.', - [16 /* NESTED_COMMENT */]: "Unexpected '`; - case 5 /* INTERPOLATION */: - return shared.escapeHtml(shared.toDisplayString(evaluateConstant(node.content))); - case 8 /* COMPOUND_EXPRESSION */: - return shared.escapeHtml(evaluateConstant(node)); - case 12 /* TEXT_CALL */: - return stringifyNode(node.content, context); - default: - // static trees will not contain if/for nodes - return ''; - } - } - function stringifyElement(node, context) { - let res = `<${node.tag}`; - let innerHTML = ''; - for (let i = 0; i < node.props.length; i++) { - const p = node.props[i]; - if (p.type === 6 /* ATTRIBUTE */) { - res += ` ${p.name}`; - if (p.value) { - res += `="${shared.escapeHtml(p.value.content)}"`; - } - } - else if (p.type === 7 /* DIRECTIVE */) { - if (p.name === 'bind') { - const exp = p.exp; - if (exp.content[0] === '_') { - // internally generated string constant references - // e.g. imported URL strings via compiler-sfc transformAssetUrl plugin - res += ` ${p.arg.content}="__VUE_EXP_START__${exp.content}__VUE_EXP_END__"`; - continue; - } - // constant v-bind, e.g. :foo="1" - let evaluated = evaluateConstant(exp); - if (evaluated != null) { - const arg = p.arg && p.arg.content; - if (arg === 'class') { - evaluated = shared.normalizeClass(evaluated); - } - else if (arg === 'style') { - evaluated = shared.stringifyStyle(shared.normalizeStyle(evaluated)); - } - res += ` ${p.arg.content}="${shared.escapeHtml(evaluated)}"`; - } - } - else if (p.name === 'html') { - // #5439 v-html with constant value - // not sure why would anyone do this but it can happen - innerHTML = evaluateConstant(p.exp); - } - else if (p.name === 'text') { - innerHTML = shared.escapeHtml(shared.toDisplayString(evaluateConstant(p.exp))); - } - } - } - if (context.scopeId) { - res += ` ${context.scopeId}`; - } - res += `>`; - if (innerHTML) { - res += innerHTML; - } - else { - for (let i = 0; i < node.children.length; i++) { - res += stringifyNode(node.children[i], context); - } - } - if (!shared.isVoidTag(node.tag)) { - res += ``; - } - return res; - } - // __UNSAFE__ - // Reason: eval. - // It's technically safe to eval because only constant expressions are possible - // here, e.g. `{{ 1 }}` or `{{ 'foo' }}` - // in addition, constant exps bail on presence of parens so you can't even - // run JSFuck in here. But we mark it unsafe for security review purposes. - // (see compiler-core/src/transforms/transformExpression) - function evaluateConstant(exp) { - if (exp.type === 4 /* SIMPLE_EXPRESSION */) { - return new Function(`return ${exp.content}`)(); - } - else { - // compound - let res = ``; - exp.children.forEach(c => { - if (shared.isString(c) || shared.isSymbol(c)) { - return; - } - if (c.type === 2 /* TEXT */) { - res += c.content; - } - else if (c.type === 5 /* INTERPOLATION */) { - res += shared.toDisplayString(evaluateConstant(c.content)); - } - else { - res += evaluateConstant(c); - } - }); - return res; - } + /** + * This module is Node-only. + */ + /** + * Regex for replacing placeholders for embedded constant variables + * (e.g. import URL string constants generated by compiler-sfc) + */ + const expReplaceRE = /__VUE_EXP_START__(.*?)__VUE_EXP_END__/g; + /** + * Turn eligible hoisted static trees into stringified static nodes, e.g. + * + * ```js + * const _hoisted_1 = createStaticVNode(`
bar
`) + * ``` + * + * A single static vnode can contain stringified content for **multiple** + * consecutive nodes (element and plain text), called a "chunk". + * `@vue/runtime-dom` will create the content via innerHTML in a hidden + * container element and insert all the nodes in place. The call must also + * provide the number of nodes contained in the chunk so that during hydration + * we can know how many nodes the static vnode should adopt. + * + * The optimization scans a children list that contains hoisted nodes, and + * tries to find the largest chunk of consecutive hoisted nodes before running + * into a non-hoisted node or the end of the list. A chunk is then converted + * into a single static vnode and replaces the hoisted expression of the first + * node in the chunk. Other nodes in the chunk are considered "merged" and + * therefore removed from both the hoist list and the children array. + * + * This optimization is only performed in Node.js. + */ + const stringifyStatic = (children, context, parent) => { + // bail stringification for slot content + if (context.scopes.vSlot > 0) { + return; + } + let nc = 0; // current node count + let ec = 0; // current element with binding count + const currentChunk = []; + const stringifyCurrentChunk = (currentIndex) => { + if (nc >= 20 /* NODE_COUNT */ || + ec >= 5 /* ELEMENT_WITH_BINDING_COUNT */) { + // combine all currently eligible nodes into a single static vnode call + const staticCall = compilerCore.createCallExpression(context.helper(compilerCore.CREATE_STATIC), [ + JSON.stringify(currentChunk.map(node => stringifyNode(node, context)).join('')).replace(expReplaceRE, `" + $1 + "`), + // the 2nd argument indicates the number of DOM nodes this static vnode + // will insert / hydrate + String(currentChunk.length) + ]); + // replace the first node's hoisted expression with the static vnode call + replaceHoist(currentChunk[0], staticCall, context); + if (currentChunk.length > 1) { + for (let i = 1; i < currentChunk.length; i++) { + // for the merged nodes, set their hoisted expression to null + replaceHoist(currentChunk[i], null, context); + } + // also remove merged nodes from children + const deleteCount = currentChunk.length - 1; + children.splice(currentIndex - currentChunk.length + 1, deleteCount); + return deleteCount; + } + } + return 0; + }; + let i = 0; + for (; i < children.length; i++) { + const child = children[i]; + const hoisted = getHoistedNode(child); + if (hoisted) { + // presence of hoisted means child must be a stringifiable node + const node = child; + const result = analyzeNode(node); + if (result) { + // node is stringifiable, record state + nc += result[0]; + ec += result[1]; + currentChunk.push(node); + continue; + } + } + // we only reach here if we ran into a node that is not stringifiable + // check if currently analyzed nodes meet criteria for stringification. + // adjust iteration index + i -= stringifyCurrentChunk(i); + // reset state + nc = 0; + ec = 0; + currentChunk.length = 0; + } + // in case the last node was also stringifiable + stringifyCurrentChunk(i); + }; + const getHoistedNode = (node) => ((node.type === 1 /* ELEMENT */ && node.tagType === 0 /* ELEMENT */) || + node.type == 12 /* TEXT_CALL */) && + node.codegenNode && + node.codegenNode.type === 4 /* SIMPLE_EXPRESSION */ && + node.codegenNode.hoisted; + const dataAriaRE = /^(data|aria)-/; + const isStringifiableAttr = (name, ns) => { + return ((ns === 0 /* HTML */ + ? shared.isKnownHtmlAttr(name) + : ns === 1 /* SVG */ + ? shared.isKnownSvgAttr(name) + : false) || dataAriaRE.test(name)); + }; + const replaceHoist = (node, replacement, context) => { + const hoistToReplace = node.codegenNode.hoisted; + context.hoists[context.hoists.indexOf(hoistToReplace)] = replacement; + }; + const isNonStringifiable = /*#__PURE__*/ shared.makeMap(`caption,thead,tr,th,tbody,td,tfoot,colgroup,col`); + /** + * for a hoisted node, analyze it and return: + * - false: bailed (contains non-stringifiable props or runtime constant) + * - [nc, ec] where + * - nc is the number of nodes inside + * - ec is the number of element with bindings inside + */ + function analyzeNode(node) { + if (node.type === 1 /* ELEMENT */ && isNonStringifiable(node.tag)) { + return false; + } + if (node.type === 12 /* TEXT_CALL */) { + return [1, 0]; + } + let nc = 1; // node count + let ec = node.props.length > 0 ? 1 : 0; // element w/ binding count + let bailed = false; + const bail = () => { + bailed = true; + return false; + }; + // TODO: check for cases where using innerHTML will result in different + // output compared to imperative node insertions. + // probably only need to check for most common case + // i.e. non-phrasing-content tags inside `

` + function walk(node) { + for (let i = 0; i < node.props.length; i++) { + const p = node.props[i]; + // bail on non-attr bindings + if (p.type === 6 /* ATTRIBUTE */ && + !isStringifiableAttr(p.name, node.ns)) { + return bail(); + } + if (p.type === 7 /* DIRECTIVE */ && p.name === 'bind') { + // bail on non-attr bindings + if (p.arg && + (p.arg.type === 8 /* COMPOUND_EXPRESSION */ || + (p.arg.isStatic && !isStringifiableAttr(p.arg.content, node.ns)))) { + return bail(); + } + if (p.exp && + (p.exp.type === 8 /* COMPOUND_EXPRESSION */ || + p.exp.constType < 3 /* CAN_STRINGIFY */)) { + return bail(); + } + } + } + for (let i = 0; i < node.children.length; i++) { + nc++; + const child = node.children[i]; + if (child.type === 1 /* ELEMENT */) { + if (child.props.length > 0) { + ec++; + } + walk(child); + if (bailed) { + return false; + } + } + } + return true; + } + return walk(node) ? [nc, ec] : false; + } + function stringifyNode(node, context) { + if (shared.isString(node)) { + return node; + } + if (shared.isSymbol(node)) { + return ``; + } + switch (node.type) { + case 1 /* ELEMENT */: + return stringifyElement(node, context); + case 2 /* TEXT */: + return shared.escapeHtml(node.content); + case 3 /* COMMENT */: + return ``; + case 5 /* INTERPOLATION */: + return shared.escapeHtml(shared.toDisplayString(evaluateConstant(node.content))); + case 8 /* COMPOUND_EXPRESSION */: + return shared.escapeHtml(evaluateConstant(node)); + case 12 /* TEXT_CALL */: + return stringifyNode(node.content, context); + default: + // static trees will not contain if/for nodes + return ''; + } + } + function stringifyElement(node, context) { + let res = `<${node.tag}`; + let innerHTML = ''; + for (let i = 0; i < node.props.length; i++) { + const p = node.props[i]; + if (p.type === 6 /* ATTRIBUTE */) { + res += ` ${p.name}`; + if (p.value) { + res += `="${shared.escapeHtml(p.value.content)}"`; + } + } + else if (p.type === 7 /* DIRECTIVE */) { + if (p.name === 'bind') { + const exp = p.exp; + if (exp.content[0] === '_') { + // internally generated string constant references + // e.g. imported URL strings via compiler-sfc transformAssetUrl plugin + res += ` ${p.arg.content}="__VUE_EXP_START__${exp.content}__VUE_EXP_END__"`; + continue; + } + // constant v-bind, e.g. :foo="1" + let evaluated = evaluateConstant(exp); + if (evaluated != null) { + const arg = p.arg && p.arg.content; + if (arg === 'class') { + evaluated = shared.normalizeClass(evaluated); + } + else if (arg === 'style') { + evaluated = shared.stringifyStyle(shared.normalizeStyle(evaluated)); + } + res += ` ${p.arg.content}="${shared.escapeHtml(evaluated)}"`; + } + } + else if (p.name === 'html') { + // #5439 v-html with constant value + // not sure why would anyone do this but it can happen + innerHTML = evaluateConstant(p.exp); + } + else if (p.name === 'text') { + innerHTML = shared.escapeHtml(shared.toDisplayString(evaluateConstant(p.exp))); + } + } + } + if (context.scopeId) { + res += ` ${context.scopeId}`; + } + res += `>`; + if (innerHTML) { + res += innerHTML; + } + else { + for (let i = 0; i < node.children.length; i++) { + res += stringifyNode(node.children[i], context); + } + } + if (!shared.isVoidTag(node.tag)) { + res += ``; + } + return res; + } + // __UNSAFE__ + // Reason: eval. + // It's technically safe to eval because only constant expressions are possible + // here, e.g. `{{ 1 }}` or `{{ 'foo' }}` + // in addition, constant exps bail on presence of parens so you can't even + // run JSFuck in here. But we mark it unsafe for security review purposes. + // (see compiler-core/src/transforms/transformExpression) + function evaluateConstant(exp) { + if (exp.type === 4 /* SIMPLE_EXPRESSION */) { + return new Function(`return ${exp.content}`)(); + } + else { + // compound + let res = ``; + exp.children.forEach(c => { + if (shared.isString(c) || shared.isSymbol(c)) { + return; + } + if (c.type === 2 /* TEXT */) { + res += c.content; + } + else if (c.type === 5 /* INTERPOLATION */) { + res += shared.toDisplayString(evaluateConstant(c.content)); + } + else { + res += evaluateConstant(c); + } + }); + return res; + } } - const ignoreSideEffectTags = (node, context) => { - if (node.type === 1 /* ELEMENT */ && - node.tagType === 0 /* ELEMENT */ && - (node.tag === 'script' || node.tag === 'style')) { - context.onError(createDOMCompilerError(60 /* X_IGNORED_SIDE_EFFECT_TAG */, node.loc)); - context.removeNode(); - } + const ignoreSideEffectTags = (node, context) => { + if (node.type === 1 /* ELEMENT */ && + node.tagType === 0 /* ELEMENT */ && + (node.tag === 'script' || node.tag === 'style')) { + context.onError(createDOMCompilerError(60 /* X_IGNORED_SIDE_EFFECT_TAG */, node.loc)); + context.removeNode(); + } }; - const DOMNodeTransforms = [ - transformStyle, - ...([transformTransition] ) - ]; - const DOMDirectiveTransforms = { - cloak: compilerCore.noopDirectiveTransform, - html: transformVHtml, - text: transformVText, - model: transformModel, - on: transformOn, - show: transformShow - }; - function compile(template, options = {}) { - return compilerCore.baseCompile(template, shared.extend({}, parserOptions, options, { - nodeTransforms: [ - // ignore diff --git a/webui/src/components/Modal.vue b/webui/src/components/Modal.vue new file mode 100644 index 0000000..2d359da --- /dev/null +++ b/webui/src/components/Modal.vue @@ -0,0 +1,43 @@ + + + \ No newline at end of file diff --git a/webui/src/components/PostCard.vue b/webui/src/components/PostCard.vue index 3c1db9a..a218a6f 100644 --- a/webui/src/components/PostCard.vue +++ b/webui/src/components/PostCard.vue @@ -1,89 +1,199 @@ + \ No newline at end of file diff --git a/webui/src/views/LoginView.vue b/webui/src/views/LoginView.vue index 6a93065..f433fd4 100644 --- a/webui/src/views/LoginView.vue +++ b/webui/src/views/LoginView.vue @@ -1,105 +1,120 @@ diff --git a/webui/src/views/ProfileView.vue b/webui/src/views/ProfileView.vue index 6171a44..8e6861d 100644 --- a/webui/src/views/ProfileView.vue +++ b/webui/src/views/ProfileView.vue @@ -1,72 +1,105 @@ @@ -77,54 +110,42 @@ export default {

- + + - - -
-
-

{{ user_data["photos"] }}

-
Photos
-
-
-

{{ user_data["followers"] }}

-
Followers
-
-
-

{{ user_data["following"] }}

-
Following
-
-
+ + -
- + +
+ +
+ -
+ + + +
+ + + + + + + + +
- - - - + \ No newline at end of file diff --git a/webui/src/views/SearchView.vue b/webui/src/views/SearchView.vue index fec48ef..baa1811 100644 --- a/webui/src/views/SearchView.vue +++ b/webui/src/views/SearchView.vue @@ -1,62 +1,79 @@ @@ -68,23 +85,28 @@ export default {

WASASearch

+ -
- - -
- -
- + +
+ +
+ +
+ + +
+ +
+ + +
@@ -93,4 +115,5 @@ export default { diff --git a/webui/vite.config.js b/webui/vite.config.js index 38a0901..d06ea85 100644 --- a/webui/vite.config.js +++ b/webui/vite.config.js @@ -13,14 +13,18 @@ export default defineConfig(({command, mode, ssrBuild}) => { } }, }; - if (command === 'serve') { + if (command === 'serve' && mode !== 'developement-external') { ret.define = { "__API_URL__": JSON.stringify("http://localhost:3000"), }; - } else { + } else if (mode === 'embedded') { ret.define = { "__API_URL__": JSON.stringify("/"), }; + } else { + ret.define = { + "__API_URL__": JSON.stringify(""), + }; } return ret; })