diff --git a/changelog/unreleased/enhancement-add-open-with-web-endpoint.md b/changelog/unreleased/enhancement-add-open-with-web-endpoint.md new file mode 100644 index 0000000000..442b2949b3 --- /dev/null +++ b/changelog/unreleased/enhancement-add-open-with-web-endpoint.md @@ -0,0 +1,7 @@ +Enhancement: Add /app/open-with-web endpoint + +We've added an /app/open-with-web endpoint to the app provider, so that +clients that are no browser or have only limited browser access can also open apps with the help of a Web URL. + +https://github.com/cs3org/reva/pull/3143 +https://github.com/owncloud/ocis/pull/4376 diff --git a/internal/http/services/appprovider/appprovider.go b/internal/http/services/appprovider/appprovider.go index 3b8f44e525..a428b7382f 100644 --- a/internal/http/services/appprovider/appprovider.go +++ b/internal/http/services/appprovider/appprovider.go @@ -21,6 +21,7 @@ package appprovider import ( "encoding/json" "net/http" + "net/url" "path" appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1" @@ -52,6 +53,14 @@ type Config struct { Prefix string `mapstructure:"prefix"` GatewaySvc string `mapstructure:"gatewaysvc"` Insecure bool `mapstructure:"insecure"` + WebBaseURI string `mapstructure:"webbaseuri"` + Web Web `mapstructure:"web"` +} + +// Web holds the config options for the URL parameters for Web +type Web struct { + URLParamsMapping map[string]string `mapstructure:"urlparamsmapping"` + StaticURLParams map[string]string `mapstructure:"staticurlparams"` } func (c *Config) init() { @@ -88,10 +97,16 @@ func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) return s, nil } +const ( + openModeNormal = iota + openModeWeb +) + func (s *svc) routerInit() error { s.router.Get("/list", s.handleList) s.router.Post("/new", s.handleNew) - s.router.Post("/open", s.handleOpen) + s.router.Post("/open", s.handleOpen(openModeNormal)) + s.router.Post("/open-with-web", s.handleOpen(openModeWeb)) return nil } @@ -324,97 +339,164 @@ func (s *svc) handleList(w http.ResponseWriter, r *http.Request) { } } -func (s *svc) handleOpen(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() +func (s *svc) handleOpen(openMode int) http.HandlerFunc { - client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc) - if err != nil { - writeError(w, r, appErrorServerError, "Internal error with the gateway, please try again later", err) - return - } + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() - err = r.ParseForm() - if err != nil { - writeError(w, r, appErrorInvalidParameter, "parameters could not be parsed", nil) - } + client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc) + if err != nil { + writeError(w, r, appErrorServerError, "Internal error with the gateway, please try again later", err) + return + } - fileID := r.Form.Get("file_id") + err = r.ParseForm() + if err != nil { + writeError(w, r, appErrorInvalidParameter, "parameters could not be parsed", nil) + } - if fileID == "" { - writeError(w, r, appErrorInvalidParameter, "missing file ID", nil) - return - } + fileID := r.Form.Get("file_id") - resourceID, err := storagespace.ParseID(fileID) - if err != nil { - writeError(w, r, appErrorInvalidParameter, "invalid file ID", nil) - return - } + if fileID == "" { + writeError(w, r, appErrorInvalidParameter, "missing file ID", nil) + return + } - fileRef := &provider.Reference{ - ResourceId: &resourceID, - Path: ".", - } + resourceID, err := storagespace.ParseID(fileID) + if err != nil { + writeError(w, r, appErrorInvalidParameter, "invalid file ID", nil) + return + } - statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: fileRef}) - if err != nil { - writeError(w, r, appErrorServerError, "Internal error accessing the file, please try again later", err) - return - } + fileRef := &provider.Reference{ + ResourceId: &resourceID, + Path: ".", + } - if statRes.Status.Code == rpc.Code_CODE_NOT_FOUND { - writeError(w, r, appErrorNotFound, "file does not exist", nil) - return - } else if statRes.Status.Code != rpc.Code_CODE_OK { - writeError(w, r, appErrorServerError, "failed to stat the file", nil) - return - } + statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: fileRef}) + if err != nil { + writeError(w, r, appErrorServerError, "Internal error accessing the file, please try again later", err) + return + } - if statRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_FILE { - writeError(w, r, appErrorInvalidParameter, "the given file id does not point to a file", nil) - return - } + if statRes.Status.Code == rpc.Code_CODE_NOT_FOUND { + writeError(w, r, appErrorNotFound, "file does not exist", nil) + return + } else if statRes.Status.Code != rpc.Code_CODE_OK { + writeError(w, r, appErrorServerError, "failed to stat the file", nil) + return + } - viewMode := getViewMode(statRes.Info, r.Form.Get("view_mode")) - if viewMode == gateway.OpenInAppRequest_VIEW_MODE_INVALID { - writeError(w, r, appErrorInvalidParameter, "invalid view mode", err) - return - } + if statRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_FILE { + writeError(w, r, appErrorInvalidParameter, "the given file id does not point to a file", nil) + return + } - openReq := gateway.OpenInAppRequest{ - Ref: fileRef, - ViewMode: viewMode, - App: r.Form.Get("app_name"), - } - openRes, err := client.OpenInApp(ctx, &openReq) - if err != nil { - writeError(w, r, appErrorServerError, - "Error contacting the requested application, please use a different one or try again later", err) - return - } - if openRes.Status.Code != rpc.Code_CODE_OK { - if openRes.Status.Code == rpc.Code_CODE_NOT_FOUND { - writeError(w, r, appErrorNotFound, openRes.Status.Message, nil) + viewMode := getViewMode(statRes.Info, r.Form.Get("view_mode")) + if viewMode == gateway.OpenInAppRequest_VIEW_MODE_INVALID { + writeError(w, r, appErrorInvalidParameter, "invalid view mode", err) + return + } + + openReq := gateway.OpenInAppRequest{ + Ref: fileRef, + ViewMode: viewMode, + App: r.Form.Get("app_name"), + } + openRes, err := client.OpenInApp(ctx, &openReq) + if err != nil { + writeError(w, r, appErrorServerError, + "Error contacting the requested application, please use a different one or try again later", err) + return + } + if openRes.Status.Code != rpc.Code_CODE_OK { + if openRes.Status.Code == rpc.Code_CODE_NOT_FOUND { + writeError(w, r, appErrorNotFound, openRes.Status.Message, nil) + return + } + writeError(w, r, appErrorServerError, openRes.Status.Message, + status.NewErrorFromCode(openRes.Status.Code, "error calling OpenInApp")) + return + } + + var payload interface{} + + switch openMode { + case openModeNormal: + payload = openRes.AppUrl + + case openModeWeb: + payload, err = newOpenInWebResponse(s.conf.WebBaseURI, s.conf.Web.URLParamsMapping, s.conf.Web.StaticURLParams, fileID, r.Form.Get("app_name"), r.Form.Get("view_mode")) + if err != nil { + writeError(w, r, appErrorServerError, "Internal error", + errors.Wrap(err, "error building OpenInWeb response")) + return + } + + default: + writeError(w, r, appErrorServerError, "Internal error with the open mode", + errors.New("unknown open mode")) + return + + } + + js, err := json.Marshal(payload) + if err != nil { + writeError(w, r, appErrorServerError, "Internal error with JSON payload", + errors.Wrap(err, "error marshalling JSON response")) + return + } + + w.Header().Set("Content-Type", "application/json") + if _, err = w.Write(js); err != nil { + writeError(w, r, appErrorServerError, "Internal error with JSON payload", + errors.Wrap(err, "error writing JSON response")) return } - writeError(w, r, appErrorServerError, openRes.Status.Message, - status.NewErrorFromCode(openRes.Status.Code, "error calling OpenInApp")) - return } +} + +type openInWebResponse struct { + URI string `json:"uri"` +} + +func newOpenInWebResponse(baseURI string, params, staticParams map[string]string, fileID, appName, viewMode string) (openInWebResponse, error) { - js, err := json.Marshal(openRes.AppUrl) + uri, err := url.Parse(baseURI) if err != nil { - writeError(w, r, appErrorServerError, "Internal error with JSON payload", - errors.Wrap(err, "error marshalling JSON response")) - return + return openInWebResponse{}, err } - w.Header().Set("Content-Type", "application/json") - if _, err = w.Write(js); err != nil { - writeError(w, r, appErrorServerError, "Internal error with JSON payload", - errors.Wrap(err, "error writing JSON response")) - return + query := uri.Query() + + for key, val := range params { + + switch val { + case "fileid": + if fileID != "" { + query.Add(key, fileID) + } + case "appname": + if appName != "" { + query.Add(key, appName) + } + case "viewmode": + if viewMode != "" { + query.Add(key, viewMode) + } + default: + return openInWebResponse{}, errors.New("unknown parameter mapper") + } + + } + + for key, val := range staticParams { + query.Add(key, val) } + + uri.RawQuery = query.Encode() + + return openInWebResponse{URI: uri.String()}, nil } func filterAppsByUserAgent(mimeTypes []*appregistry.MimeTypeInfo, userAgent string) []*appregistry.MimeTypeInfo { diff --git a/internal/http/services/owncloud/ocs/data/capabilities.go b/internal/http/services/owncloud/ocs/data/capabilities.go index 37314dc371..c1658d27ce 100644 --- a/internal/http/services/owncloud/ocs/data/capabilities.go +++ b/internal/http/services/owncloud/ocs/data/capabilities.go @@ -117,11 +117,12 @@ type CapabilitiesArchiver struct { // CapabilitiesAppProvider holds available app provider information type CapabilitiesAppProvider struct { - Enabled bool `json:"enabled" xml:"enabled" mapstructure:"enabled"` - Version string `json:"version" xml:"version" mapstructure:"version"` - AppsURL string `json:"apps_url" xml:"apps_url" mapstructure:"apps_url"` - OpenURL string `json:"open_url" xml:"open_url" mapstructure:"open_url"` - NewURL string `json:"new_url" xml:"new_url" mapstructure:"new_url"` + Enabled bool `json:"enabled" xml:"enabled" mapstructure:"enabled"` + Version string `json:"version" xml:"version" mapstructure:"version"` + AppsURL string `json:"apps_url" xml:"apps_url" mapstructure:"apps_url"` + OpenURL string `json:"open_url" xml:"open_url" mapstructure:"open_url"` + OpenWebURL string `json:"open_web_url" xml:"open_web_url" mapstructure:"open_web_url"` + NewURL string `json:"new_url" xml:"new_url" mapstructure:"new_url"` } // CapabilitiesFiles TODO this is storage specific, not global. What effect do these options have on the clients?