package api import ( "errors" "fmt" "net/http" "os" "path/filepath" "strings" "github.com/gin-gonic/gin" "github.com/gotify/server/v2/auth" "github.com/gotify/server/v2/model" "github.com/h2non/filetype" ) // The ApplicationDatabase interface for encapsulating database access. type ApplicationDatabase interface { CreateApplication(application *model.Application) error GetApplicationByToken(token string) (*model.Application, error) GetApplicationByID(id uint) (*model.Application, error) GetApplicationsByUser(userID uint) ([]*model.Application, error) DeleteApplicationByID(id uint) error UpdateApplication(application *model.Application) error } // The ApplicationAPI provides handlers for managing applications. type ApplicationAPI struct { DB ApplicationDatabase ImageDir string } // Application Params Model // // Params allowed to create or update Applications. // // swagger:model ApplicationParams type ApplicationParams struct { // The application name. This is how the application should be displayed to the user. // // required: true // example: Backup Server Name string `form:"name" query:"name" json:"name" binding:"required"` // The description of the application. // // example: Backup server for the interwebs Description string `form:"description" query:"description" json:"description"` // The default priority of messages sent by this application. Defaults to 0. // // example: 5 DefaultPriority int `form:"defaultPriority" query:"defaultPriority" json:"defaultPriority"` } // CreateApplication creates an application and returns the access token. // swagger:operation POST /application application createApp // // Create an application. // // --- // consumes: [application/json] // produces: [application/json] // security: [clientTokenAuthorizationHeader: [], clientTokenHeader: [], clientTokenQuery: [], basicAuth: []] // parameters: // - name: body // in: body // description: the application to add // required: true // schema: // $ref: "#/definitions/ApplicationParams" // responses: // 200: // description: Ok // schema: // $ref: "#/definitions/Application" // 400: // description: Bad Request // schema: // $ref: "#/definitions/Error" // 401: // description: Unauthorized // schema: // $ref: "#/definitions/Error" // 403: // description: Forbidden // schema: // $ref: "#/definitions/Error" func (a *ApplicationAPI) CreateApplication(ctx *gin.Context) { applicationParams := ApplicationParams{} if err := ctx.Bind(&applicationParams); err == nil { app := model.Application{ Name: applicationParams.Name, Description: applicationParams.Description, DefaultPriority: applicationParams.DefaultPriority, Token: auth.GenerateNotExistingToken(generateApplicationToken, a.applicationExists), UserID: auth.GetUserID(ctx), Internal: false, } if success := successOrAbort(ctx, 500, a.DB.CreateApplication(&app)); !success { return } ctx.JSON(200, withResolvedImage(&app)) } } // GetApplications returns all applications a user has. // swagger:operation GET /application application getApps // // Return all applications. // // --- // consumes: [application/json] // produces: [application/json] // security: [clientTokenAuthorizationHeader: [], clientTokenHeader: [], clientTokenQuery: [], basicAuth: []] // responses: // 200: // description: Ok // schema: // type: array // items: // $ref: "#/definitions/Application" // 401: // description: Unauthorized // schema: // $ref: "#/definitions/Error" // 403: // description: Forbidden // schema: // $ref: "#/definitions/Error" func (a *ApplicationAPI) GetApplications(ctx *gin.Context) { userID := auth.GetUserID(ctx) apps, err := a.DB.GetApplicationsByUser(userID) if success := successOrAbort(ctx, 500, err); !success { return } for _, app := range apps { withResolvedImage(app) } ctx.JSON(200, apps) } // DeleteApplication deletes an application by its id. // swagger:operation DELETE /application/{id} application deleteApp // // Delete an application. // // --- // consumes: [application/json] // produces: [application/json] // parameters: // - name: id // in: path // description: the application id // required: true // type: integer // format: int64 // security: [clientTokenAuthorizationHeader: [], clientTokenHeader: [], clientTokenQuery: [], basicAuth: []] // responses: // 200: // description: Ok // 400: // description: Bad Request // schema: // $ref: "#/definitions/Error" // 401: // description: Unauthorized // schema: // $ref: "#/definitions/Error" // 403: // description: Forbidden // schema: // $ref: "#/definitions/Error" // 404: // description: Not Found // schema: // $ref: "#/definitions/Error" func (a *ApplicationAPI) DeleteApplication(ctx *gin.Context) { withID(ctx, "id", func(id uint) { app, err := a.DB.GetApplicationByID(id) if success := successOrAbort(ctx, 500, err); !success { return } if app != nil && app.UserID == auth.GetUserID(ctx) { if app.Internal { ctx.AbortWithError(400, errors.New("cannot delete internal application")) return } if success := successOrAbort(ctx, 500, a.DB.DeleteApplicationByID(id)); !success { return } if app.Image != "" { os.Remove(a.ImageDir + app.Image) } } else { ctx.AbortWithError(404, fmt.Errorf("app with id %d doesn't exists", id)) } }) } // UpdateApplication updates an application info by its id. // swagger:operation PUT /application/{id} application updateApplication // // Update an application. // // --- // consumes: [application/json] // produces: [application/json] // security: [clientTokenAuthorizationHeader: [], clientTokenHeader: [], clientTokenQuery: [], basicAuth: []] // parameters: // - name: body // in: body // description: the application to update // required: true // schema: // $ref: "#/definitions/ApplicationParams" // - name: id // in: path // description: the application id // required: true // type: integer // format: int64 // responses: // 200: // description: Ok // schema: // $ref: "#/definitions/Application" // 400: // description: Bad Request // schema: // $ref: "#/definitions/Error" // 401: // description: Unauthorized // schema: // $ref: "#/definitions/Error" // 403: // description: Forbidden // schema: // $ref: "#/definitions/Error" // 404: // description: Not Found // schema: // $ref: "#/definitions/Error" func (a *ApplicationAPI) UpdateApplication(ctx *gin.Context) { withID(ctx, "id", func(id uint) { app, err := a.DB.GetApplicationByID(id) if success := successOrAbort(ctx, 500, err); !success { return } if app != nil && app.UserID == auth.GetUserID(ctx) { applicationParams := ApplicationParams{} if err := ctx.Bind(&applicationParams); err == nil { app.Description = applicationParams.Description app.Name = applicationParams.Name app.DefaultPriority = applicationParams.DefaultPriority if success := successOrAbort(ctx, 500, a.DB.UpdateApplication(app)); !success { return } ctx.JSON(200, withResolvedImage(app)) } } else { ctx.AbortWithError(404, fmt.Errorf("app with id %d doesn't exists", id)) } }) } // UploadApplicationImage uploads an image for an application. // swagger:operation POST /application/{id}/image application uploadAppImage // // Upload an image for an application. // // --- // consumes: // - multipart/form-data // produces: [application/json] // security: [clientTokenAuthorizationHeader: [], clientTokenHeader: [], clientTokenQuery: [], basicAuth: []] // parameters: // - name: file // in: formData // description: the application image // required: true // type: file // - name: id // in: path // description: the application id // required: true // type: integer // format: int64 // responses: // 200: // description: Ok // schema: // $ref: "#/definitions/Application" // 400: // description: Bad Request // schema: // $ref: "#/definitions/Error" // 401: // description: Unauthorized // schema: // $ref: "#/definitions/Error" // 403: // description: Forbidden // schema: // $ref: "#/definitions/Error" // 404: // description: Not Found // schema: // $ref: "#/definitions/Error" // 500: // description: Server Error // schema: // $ref: "#/definitions/Error" func (a *ApplicationAPI) UploadApplicationImage(ctx *gin.Context) { withID(ctx, "id", func(id uint) { app, err := a.DB.GetApplicationByID(id) if success := successOrAbort(ctx, 500, err); !success { return } if app != nil && app.UserID == auth.GetUserID(ctx) { file, err := ctx.FormFile("file") if err == http.ErrMissingFile { ctx.AbortWithError(400, errors.New("file with key 'file' must be present")) return } else if err != nil { ctx.AbortWithError(500, err) return } head := make([]byte, 261) open, _ := file.Open() open.Read(head) if !filetype.IsImage(head) { ctx.AbortWithError(400, errors.New("file must be an image")) return } ext := filepath.Ext(file.Filename) if !ValidApplicationImageExt(ext) { ctx.AbortWithError(400, errors.New("invalid file extension")) return } name := generateNonExistingImageName(a.ImageDir, func() string { return generateImageName() + ext }) err = ctx.SaveUploadedFile(file, a.ImageDir+name) if err != nil { ctx.AbortWithError(500, err) return } if app.Image != "" { os.Remove(a.ImageDir + app.Image) } app.Image = name if success := successOrAbort(ctx, 500, a.DB.UpdateApplication(app)); !success { return } ctx.JSON(200, withResolvedImage(app)) } else { ctx.AbortWithError(404, fmt.Errorf("app with id %d doesn't exists", id)) } }) } // RemoveApplicationImage deletes an image of an application. // swagger:operation DELETE /application/{id}/image application removeAppImage // // Deletes an image of an application. // // --- // consumes: [application/json] // produces: [application/json] // parameters: // - name: id // in: path // description: the application id // required: true // type: integer // format: int64 // security: [clientTokenAuthorizationHeader: [], clientTokenHeader: [], clientTokenQuery: [], basicAuth: []] // responses: // 200: // description: Ok // 400: // description: Bad Request // schema: // $ref: "#/definitions/Error" // 401: // description: Unauthorized // schema: // $ref: "#/definitions/Error" // 403: // description: Forbidden // schema: // $ref: "#/definitions/Error" // 404: // description: Not Found // schema: // $ref: "#/definitions/Error" // 500: // description: Server Error // schema: // $ref: "#/definitions/Error" func (a *ApplicationAPI) RemoveApplicationImage(ctx *gin.Context) { withID(ctx, "id", func(id uint) { app, err := a.DB.GetApplicationByID(id) if success := successOrAbort(ctx, 500, err); !success { return } if app != nil && app.UserID == auth.GetUserID(ctx) { if app.Image == "" { ctx.AbortWithError(400, fmt.Errorf("app with id %d does not have a customized image", id)) return } image := app.Image app.Image = "" if success := successOrAbort(ctx, 500, a.DB.UpdateApplication(app)); !success { return } os.Remove(a.ImageDir + image) ctx.JSON(200, withResolvedImage(app)) } else { ctx.AbortWithError(404, fmt.Errorf("app with id %d doesn't exists", id)) } }) } func withResolvedImage(app *model.Application) *model.Application { if app.Image == "" { app.Image = "static/defaultapp.png" } else { app.Image = "image/" + app.Image } return app } func (a *ApplicationAPI) applicationExists(token string) bool { app, _ := a.DB.GetApplicationByToken(token) return app != nil } func exist(path string) bool { if _, err := os.Stat(path); os.IsNotExist(err) { return false } return true } func generateNonExistingImageName(imgDir string, gen func() string) string { for { name := gen() if !exist(imgDir + name) { return name } } } func ValidApplicationImageExt(ext string) bool { switch strings.ToLower(ext) { case ".gif", ".png", ".jpg", ".jpeg": return true default: return false } }