api

package
v5.1.2+incompatible Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 6, 2021 License: Apache-2.0, BSD-2-Clause, BSD-3-Clause, + 1 more Imports: 30 Imported by: 289

Documentation

Overview

Package api provides general purpose tools for implementing the Traffic Ops API.

Index

Constants

View Source
const (
	DBContextKey      = "db"
	ConfigContextKey  = "context"
	ReqIDContextKey   = "reqid"
	APIRespWrittenKey = "respwritten"
)

Common context.Context value keys.

View Source
const (
	ApiChange = "APICHANGE"
	Updated   = "Updated"
	Created   = "Created"
	Deleted   = "Deleted"
)
View Source
const ConfigKey = "cfg"
View Source
const DBKey = "db"
View Source
const PathParamsKey = "pathParams"

Variables

This section is empty.

Functions

func AddLastModifiedHdr

func AddLastModifiedHdr(w http.ResponseWriter, t time.Time)

AddLastModifiedHdr adds the "last modified" header to the response.

func AddUserToReq

func AddUserToReq(r *http.Request, u auth.CurrentUser)

func AllParams

func AllParams(req *http.Request, required []string, ints []string) (map[string]string, map[string]int, error, error, int)

AllParams takes the request (in which the router has inserted context for path parameters), and an array of parameters required to be integers, and returns the map of combined parameters, and the map of int parameters; or a user or system error and the HTTP error code. The intParams may be nil if no integer parameters are required. This is a helper for the common case; not using this in unusual cases is perfectly acceptable.

func CheckIfUnModified

func CheckIfUnModified(h http.Header, tx *sqlx.Tx, ID int, tableName string) (error, error, int)

CheckIfUnModified checks to see if the resource was modified since the "If-Unmodified-Since" header value in the request. In case it was, the 412 error code is returned. If some other error was encountered while checking, the appropriate error code along with error details is returned. If the resource was not modified since the specified time, the UPDATE proceeds in the normal fashion.

func CreateChangeLog

func CreateChangeLog(level string, action string, i Identifier, user *auth.CurrentUser, tx *sql.Tx) error

func CreateChangeLogBuildMsg

func CreateChangeLogBuildMsg(level string, action string, user *auth.CurrentUser, tx *sql.Tx, objType string, auditName string, keys map[string]interface{}) error

func CreateChangeLogRawErr

func CreateChangeLogRawErr(level string, msg string, user *auth.CurrentUser, tx *sql.Tx) error

func CreateChangeLogRawTx

func CreateChangeLogRawTx(level string, msg string, user *auth.CurrentUser, tx *sql.Tx)

func CreateDeprecationAlerts

func CreateDeprecationAlerts(alternative *string) tc.Alerts

CreateDeprecationAlerts creates a deprecation notice with an optional alternative route suggestion.

func CreateHandler

func CreateHandler(creator Creator) http.HandlerFunc

CreateHandler creates a handler function from the pointer to a struct implementing the Creator interface

this generic handler encapsulates the logic for handling:
*current user
*decoding and validating the struct
*change log entry
*forming and writing the body over the wire

func DefaultSort

func DefaultSort(readerType *APIInfo, param string)

DefaultSort sorts alphabetically for a given readerType (eg: TOCDN, TODeliveryService, TOOrigin etc).

func DeleteHandler

func DeleteHandler(deleter Deleter) http.HandlerFunc

DeleteHandler creates a handler function from the pointer to a struct implementing the Deleter interface

this generic handler encapsulates the logic for handling:
*fetching the id from the path parameter
*current user
*change log entry
*forming and writing the body over the wire

func DeprecatedDeleteHandler

func DeprecatedDeleteHandler(deleter Deleter, alternative *string) http.HandlerFunc

DeprecatedDeleteHandler creates a handler function from the pointer to a struct implementing the Deleter interface with a optional deprecation notice

this generic handler encapsulates the logic for handling:
*fetching the id from the path parameter
*current user
*change log entry
*forming and writing the body over the wire

func DeprecatedReadHandler

func DeprecatedReadHandler(reader Reader, alternative *string) http.HandlerFunc

DeprecatedReadHandler creates a net/http.HandlerFunc for the passed Reader object, and adds a deprecation notice, optionally with a passed alternative route suggestion.

func FormatLastModified

func FormatLastModified(t time.Time) string

FormatLastModified trims the time string and formats it according to RFC1123.

func GenericCreate

func GenericCreate(val GenericCreator) (error, error, int)

GenericCreate does a Create (POST) for the given GenericCreator object and type. This exists as a generic function, for the common use case of a single "id" key and a lastUpdated field.

func GenericCreateNameBasedID

func GenericCreateNameBasedID(val GenericCreator) (error, error, int)

GenericCreateNameBasedID does a Create (POST) for the given GenericCreator object and type. This exists as a generic function, for the use case of a single "name" key (not a numerical "id" key) and a lastUpdated field.

func GenericDelete

func GenericDelete(val GenericDeleter) (error, error, int)

GenericDelete does a Delete (DELETE) for the given GenericDeleter object and type. This exists as a generic function, for the common use case of a simple delete with query parameters defined in the sqlx struct tags.

func GenericOptionsDelete

func GenericOptionsDelete(val GenericOptionsDeleter) (error, error, int)

GenericOptionsDelete does a Delete (DELETE) for the given GenericOptionsDeleter object and type. Unlike GenericDelete, there is no requirement that a specific key is used as the parameter. GenericOptionsDeleter.DeleteKeyOptions() specifies which keys can be used.

func GenericRead

func GenericRead(h http.Header, val GenericReader, useIMS bool) ([]interface{}, error, error, int, *time.Time)

func GenericUpdate

func GenericUpdate(h http.Header, val GenericUpdater) (error, error, int)

GenericUpdate handles the common update case, where the update returns the new last_modified time.

func GetCombinedParams

func GetCombinedParams(r *http.Request) (map[string]string, error)

func GetConfig

func GetConfig(ctx context.Context) (*config.Config, error)

func GetDB

func GetDB(ctx context.Context) (*sqlx.DB, error)

GetDB returns the database from the context. This should very rarely be needed, rather `NewInfo` should always be used to get a transaction, except in extenuating circumstances.

func GetIntKey

func GetIntKey(s string) (interface{}, error)

func GetLastUpdated

func GetLastUpdated(tx *sqlx.Tx, ID int, tableName string) (*time.Time, bool, error)

GetLastUpdated checks for the resource by ID in the database, and returns its last_updated timestamp, if available.

func GetLastUpdatedByName

func GetLastUpdatedByName(tx *sqlx.Tx, name string, tableName string) (*time.Time, bool, error)

GetLastUpdatedByName checks for the resource by name in the database, and returns its last_updated timestamp, if available.

func GetPathParams

func GetPathParams(ctx context.Context) (map[string]string, error)

func GetStringKey

func GetStringKey(s string) (interface{}, error)

func GetUserFromReq

func GetUserFromReq(w http.ResponseWriter, r *http.Request, secret string) (auth.CurrentUser, error, error, int)

GetUserFromReq returns the current user, any user error, any system error, and an error code to be returned if either error was not nil. This also uses the given ResponseWriter to refresh the cookie, if it was valid.

func GoneHandler

func GoneHandler(w http.ResponseWriter, r *http.Request)

GoneHandler is an http.Handler function that just writes a 410 Gone response back to the client, along with an error-level alert stating that the endpoint is no longer available.

func HandleDeprecatedErr

func HandleDeprecatedErr(w http.ResponseWriter, r *http.Request, tx *sql.Tx, statusCode int, userErr error, sysErr error, alternative *string)

HandleDeprecatedErr handles an API error, adding a deprecation alert, rolling back the transaction, writing the given statusCode and userErr to the user, and logging the sysErr. If userErr is nil, the text of the HTTP statusCode is written.

The alternative may be nil if there is no alternative and the deprecation message will be selected appropriately.

The tx may be nil, if there is no transaction. Passing a nil tx is strongly discouraged if a transaction exists, because it will result in copy-paste errors for the common APIInfo use case.

This is a helper for the common case; not using this in unusual cases is perfectly acceptable.

func HandleErr

func HandleErr(w http.ResponseWriter, r *http.Request, tx *sql.Tx, statusCode int, userErr error, sysErr error)

HandleErr handles an API error, rolling back the transaction, writing the given statusCode and userErr to the user, and logging the sysErr. If userErr is nil, the text of the HTTP statusCode is written.

The tx may be nil, if there is no transaction. Passing a nil tx is strongly discouraged if a transaction exists, because it will result in copy-paste errors for the common APIInfo use case.

This is a helper for the common case; not using this in unusual cases is perfectly acceptable.

func HandleErrOptionalDeprecation

func HandleErrOptionalDeprecation(w http.ResponseWriter, r *http.Request, tx *sql.Tx, statusCode int, userErr error, sysErr error, deprecated bool, alternative *string)

func IntParams

func IntParams(params map[string]string, intParamNames []string) (map[string]int, error)

IntParams parses integer parameters, and returns map of the given params, or an error if any integer param is not an integer. The intParams may be nil if no integer parameters are required. Note this does not check existence; if an integer paramter is required, it should be included in the requiredParams given to NewInfo. This is a helper for the common case; not using this in unusual cases is perfectly acceptable.

func IsBool

func IsBool(s string) error

func IsInt

func IsInt(s string) error

func IsUnmodified

func IsUnmodified(h http.Header, lastUpdated time.Time) bool

IsUnmodified returns a boolean, saying whether or not the resource in question was modified since the time specified in the headers.

func LogErr

func LogErr(r *http.Request, errCode int, userErr error, sysErr error) error

LogErr handles the logging of errors and setting up possibly nil errors without actually writing anything to a http.ResponseWriter, unlike handleSimpleErr. It returns the userErr which will be initialized to the http.StatusText of errCode if it was passed as nil - otherwise left alone.

func LoginAuth

func LoginAuth(identity, username, password, host string) smtp.Auth

func ParamsHaveRequired

func ParamsHaveRequired(params map[string]string, required []string) error

ParamsHaveRequired checks that params have all the required parameters, and returns nil on success, or an error providing information on which params are missing.

func Parse

func Parse(r io.Reader, tx *sql.Tx, v ParseValidator) error

Parse decodes a JSON object from r into v, validating and sanitizing the input. Use this function instead of the json package when writing API endpoints to safely decode and validate PUT and POST requests.

TODO: change to take data loaded from db, to remove sql from tc package.

func ParseDBError

func ParseDBError(ierr error) (error, error, int)

ParseDBError parses pq errors for database constraint violations, and returns the (userErr, sysErr, httpCode) format expected by the API helpers.

func ReadHandler

func ReadHandler(reader Reader) http.HandlerFunc

ReadHandler creates a handler function from the pointer to a struct implementing the Reader interface

this handler retrieves the user from the context
combines the path and query parameters
produces the proper status code based on the error code returned
marshals the structs returned into the proper response json

func RespWriter

func RespWriter(w http.ResponseWriter, r *http.Request, tx *sql.Tx) func(v interface{}, err error)

RespWriter is a helper to allow a one-line response, for endpoints with a function that returns the object that needs to be written and an error. This is a helper for the common case; not using this in unusual cases is perfectly acceptable.

func RespWriterVals

func RespWriterVals(w http.ResponseWriter, r *http.Request, tx *sql.Tx, vals map[string]interface{}) func(v interface{}, err error)

RespWriterVals is like RespWriter, but also takes a map of root-level values to write. The API most commonly needs these for meta-parameters, like size, limit, and orderby. This is a helper for the common case; not using this in unusual cases is perfectly acceptable.

func SendEmailFromTemplate

func SendEmailFromTemplate(config config.Config, header string, data interface{}, templateFile string, toEmail string) (int, error, error)

SendEmailFromTemplate allows a user to input an html template to format an email. It parses the template and creates a message before calling the SendMail method. SendEmailFromTemplate returns (in order) an HTTP status code, a user-friendly error, and an error fit for logging to system error logs. If either the user or system error is non-nil, the operation failed, and the HTTP status code indicates the type of failure.

func SendMail

func SendMail(to rfc.EmailAddress, msg []byte, cfg *config.Config) (int, error, error)

SendMail sends an email msg to the address identified by to. The msg parameter should be an RFC822-style email with headers first, a blank line, and then the message body. The lines of msg should be CRLF terminated. The msg headers should usually include fields such as "From", "To", "Subject", and "Cc". Sending "Bcc" messages is accomplished by including an email address in the to parameter but not including it in the msg headers. The cfg parameter is used to set things like the "From" field, as well as for connection and authentication with an external SMTP server. SendMail returns (in order) an HTTP status code, a user-friendly error, and an error fit for logging to system error logs. If either the user or system error is non-nil, the operation failed, and the HTTP status code indicates the type of failure.

func SetLastModifiedHeader

func SetLastModifiedHeader(r *http.Request, useIMS bool) bool

SetLastModifiedHeader sets the Last-Modified header in case the "useIMS" is set to true in the config, and if there is an "If-Modified-Since" header in the incoming request

func StripParamJSON

func StripParamJSON(params map[string]string) map[string]string

StripParamJSON removes ".json" trailing any parameter value, and returns the modified params. This allows the API handlers to transparently accept /id.json routes, as allowed by the 1.x API.

func TryIfModifiedSinceQuery

func TryIfModifiedSinceQuery(val GenericReader, h http.Header, where string, orderBy string, pagination string, queryValues map[string]interface{}) (bool, time.Time)

TryIfModifiedSinceQuery checks to see the max time that an entity was changed, and then returns a boolean (which tells us whether or not to run the main query for the endpoint) along with the max time If the returned boolean is false, there is no need to run the main query for the GET API endpoint, and we return a 304 status

func UpdateHandler

func UpdateHandler(updater Updater) http.HandlerFunc

UpdateHandler creates a handler function from the pointer to a struct implementing the Updater interface

this generic handler encapsulates the logic for handling:
*fetching the id from the path parameter
*current user
*decoding and validating the struct
*change log entry
*forming and writing the body over the wire

func WriteAlerts

func WriteAlerts(w http.ResponseWriter, r *http.Request, code int, alerts tc.Alerts)

func WriteAlertsObj

func WriteAlertsObj(w http.ResponseWriter, r *http.Request, code int, alerts tc.Alerts, obj interface{})

func WriteResp

func WriteResp(w http.ResponseWriter, r *http.Request, v interface{})

WriteResp takes any object, serializes it as JSON, and writes that to w. Any errors are logged and written to w via tc.GetHandleErrorsFunc. This is a helper for the common case; not using this in unusual cases is perfectly acceptable.

func WriteRespAlert

func WriteRespAlert(w http.ResponseWriter, r *http.Request, level tc.AlertLevel, msg string)

WriteRespAlert creates an alert, serializes it as JSON, and writes that to w. Any errors are logged and written to w via tc.GetHandleErrorsFunc. This is a helper for the common case; not using this in unusual cases is perfectly acceptable.

func WriteRespAlertNotFound

func WriteRespAlertNotFound(w http.ResponseWriter, r *http.Request)

WriteRespAlertNotFound creates an alert indicating that the resource was not found and writes that to w.

func WriteRespAlertObj

func WriteRespAlertObj(w http.ResponseWriter, r *http.Request, level tc.AlertLevel, msg string, obj interface{})

WriteRespAlertObj Writes the given alert, and the given response object. This is a helper for the common case; not using this in unusual cases is perfectly acceptable.

func WriteRespRaw

func WriteRespRaw(w http.ResponseWriter, r *http.Request, v interface{})

WriteRespRaw acts like WriteResp, but doesn't wrap the object in a `{"response":` object. This should be used to respond with endpoints which don't wrap their response in a "response" object.

func WriteRespVals

func WriteRespVals(w http.ResponseWriter, r *http.Request, v interface{}, vals map[string]interface{})

WriteRespVals is like WriteResp, but also takes a map of root-level values to write. The API most commonly needs these for meta-parameters, like size, limit, and orderby. This is a helper for the common case; not using this in unusual cases is perfectly acceptable.

func WriteRespWithSummary

func WriteRespWithSummary(w http.ResponseWriter, r *http.Request, v interface{}, count uint64)

WriteRespWithSummary writes a JSON-encoded representation of an arbitrary object to the provided writer, and cleans up the corresponding request object. It also provides a "summary" section to the response object that contains the given "count".

Types

type APIInfo

type APIInfo struct {
	Params    map[string]string
	IntParams map[string]int
	User      *auth.CurrentUser
	ReqID     uint64
	Version   *Version
	Tx        *sqlx.Tx
	Config    *config.Config
}

func NewInfo

func NewInfo(r *http.Request, requiredParams []string, intParamNames []string) (*APIInfo, error, error, int)

NewInfo get and returns the context info needed by handlers. It also returns any user error, any system error, and the status code which should be returned to the client if an error occurred.

It is encouraged to call APIInfo.Tx.Tx.Commit() manually when all queries are finished, to release database resources early, and also to return an error to the user if the commit failed.

NewInfo guarantees the returned APIInfo.Tx is non-nil and APIInfo.Tx.Tx is nil or valid, even if a returned error is not nil. Hence, it is safe to pass the Tx.Tx to HandleErr when this returns errors.

Close() must be called to free resources, and should be called in a defer immediately after NewInfo(), to finish the transaction.

Example:

func handler(w http.ResponseWriter, r *http.Request) {
  inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
  if userErr != nil || sysErr != nil {
    api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
    return
  }
  defer inf.Close()

  respObj, err := finalDatabaseOperation(inf.Tx)
  if err != nil {
    api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("final db op: " + err.Error()))
    return
  }
  if err := inf.Tx.Tx.Commit(); err != nil {
    api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("committing transaction: " + err.Error()))
    return
  }
  api.WriteResp(w, r, respObj)
}

func (*APIInfo) Close

func (inf *APIInfo) Close()

Close implements the io.Closer interface. It should be called in a defer immediately after NewInfo().

Close will commit the transaction, if it hasn't been rolled back.

func (*APIInfo) CreateInfluxClient

func (inf *APIInfo) CreateInfluxClient() (*influx.Client, error)

CreateInfluxClient constructs and returns an InfluxDB HTTP client, if enabled and when possible. The error this returns should not be exposed to the user; it's for logging purposes only.

If Influx connections are not enabled, this will return `nil` - but also no error. It is expected that the caller will handle this situation appropriately.

func (*APIInfo) IsResourceAuthorizedToCurrentUser

func (inf *APIInfo) IsResourceAuthorizedToCurrentUser(resourceTenantID int) (bool, error)

IsResourceAuthorizedToCurrentUser is a convenience method used to call github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant.IsResourceAuthorizedToUserTx using an APIInfo structure to provide the current user and database transaction.

func (*APIInfo) SendMail

func (inf *APIInfo) SendMail(to rfc.EmailAddress, msg []byte) (int, error, error)

SendMail is a convenience method used to call SendMail using an APIInfo structure's configuration.

type APIInfoImpl

type APIInfoImpl struct {
	ReqInfo *APIInfo
}

APIInfoImpl implements APIInfo via the APIInfoer interface

func (APIInfoImpl) APIInfo

func (val APIInfoImpl) APIInfo() *APIInfo

func (*APIInfoImpl) SetInfo

func (val *APIInfoImpl) SetInfo(inf *APIInfo)

type APIInfoer

type APIInfoer interface {
	SetInfo(*APIInfo)
	APIInfo() *APIInfo
}

APIInfoer is an interface that guarantees the existance of a variable through its setters and getters. Every CRUD operation uses this login session context

type APIResponse

type APIResponse struct {
	Response interface{} `json:"response"`
}

type APIResponseWithSummary

type APIResponseWithSummary struct {
	Response interface{} `json:"response"`
	Summary  struct {
		Count uint64 `json:"count"`
	} `json:"summary"`
}

type AlertsResponse

type AlertsResponse interface {
	// GetAlerts retrieves an array of alerts that were generated over the course of handling an endpoint.
	GetAlerts() tc.Alerts
}

type CRUDer

type CRUDer interface {
	Create() (error, error, int)
	Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time)
	Update(http.Header) (error, error, int)
	Delete() (error, error, int)
	APIInfoer
	Identifier
	Validator
}

type ChangeLog

type ChangeLog struct {
	ID          int          `json:"id" db:"id"`
	Level       string       `json:"level" db:"level"`
	Message     string       `json:"message" db:"message"`
	TMUser      int          `json:"tmUser" db:"tm_user"`
	TicketNum   string       `json:"ticketNum" db:"ticketnum"`
	LastUpdated tc.TimeNoMod `json:"lastUpdated" db:"last_updated"`
}

type ChangeLogger

type ChangeLogger interface {
	ChangeLogMessage(action string) (string, error)
}

type Creator

type Creator interface {
	// Create returns any user error, any system error, and the HTTP error code to be returned if there was an error.
	Create() (error, error, int)
	APIInfoer
	Identifier
	Validator
}

type Deleter

type Deleter interface {
	// Delete returns any user error, any system error, and the HTTP error code to be returned if there was an error.
	Delete() (error, error, int)
	APIInfoer
	Identifier
}

type GenericCreator

type GenericCreator interface {
	GetType() string
	APIInfo() *APIInfo
	SetKeys(map[string]interface{})
	SetLastUpdated(tc.TimeNoMod)
	InsertQuery() string
}

type GenericDeleter

type GenericDeleter interface {
	GetType() string
	APIInfo() *APIInfo
	DeleteQuery() string
}

type GenericOptionsDeleter

type GenericOptionsDeleter interface {
	GetType() string
	APIInfo() *APIInfo
	DeleteKeyOptions() map[string]dbhelpers.WhereColumnInfo
	DeleteQueryBase() string
}

GenericOptionsDeleter can use any key listed in DeleteKeyOptions() to delete a resource.

type GenericReader

type GenericReader interface {
	GetType() string
	APIInfo() *APIInfo
	ParamColumns() map[string]dbhelpers.WhereColumnInfo
	NewReadObj() interface{}
	SelectQuery() string
	SelectMaxLastUpdatedQuery(where string, orderBy string, pagination string, tableName string) string
}

type GenericUpdater

type GenericUpdater interface {
	GetType() string
	APIInfo() *APIInfo
	SetLastUpdated(tc.TimeNoMod)
	UpdateQuery() string
	GetLastUpdated() (*time.Time, bool, error)
}

type Identifier

type Identifier interface {

	// Getters and Setters for key data
	// The current common case is a single numerical id
	SetKeys(map[string]interface{})
	GetKeys() (map[string]interface{}, bool)

	// GetType gives the name of the implementing struct
	GetType() string

	// GetAuditName returns the name of an object instance. If no name is availible, the id should be returned. "unknown" is the final case
	GetAuditName() string

	// This should define the key getters and setters
	GetKeyFieldsInfo() []KeyFieldInfo
}

type KeyFieldInfo

type KeyFieldInfo struct {
	Field string
	Func  func(string) (interface{}, error)
}

type MultipleCreator

type MultipleCreator interface {
	AllowMultipleCreates() bool
}

MultipleCreator indicates whether an object using the shared handlers allows an array of objects in the POST

type OptionsDeleter

type OptionsDeleter interface {
	// OptionsDelete returns any user error, any system error, and the HTTP error code to be returned if there was an
	// error.
	OptionsDelete() (error, error, int)
	APIInfoer
	Identifier
	DeleteKeyOptions() map[string]dbhelpers.WhereColumnInfo
}

OptionsDeleter calls the OptionsDelete() generic CRUD function, unlike Deleter, which calls Delete().

type ParseValidator

type ParseValidator interface {
	Validate(tx *sql.Tx) error
}

ParseValidator objects can make use of api.Parse to handle parsing and validating at the same time.

TODO: Rework validation to be able to return system-level errors

type Reader

type Reader interface {
	// Read returns the object to write to the user, any user error, any system error, and the HTTP error code to be returned if there was an error.
	Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time)
	APIInfoer
}

type Tenantable

type Tenantable interface {
	IsTenantAuthorized(user *auth.CurrentUser) (bool, error)
}

type Updater

type Updater interface {
	// Update returns any user error, any system error, and the HTTP error code to be returned if there was an error.
	Update(h http.Header) (error, error, int)
	APIInfoer
	Identifier
	Validator
}

type Validator

type Validator interface {
	Validate() error
}

type Version

type Version struct {
	Major uint64
	Minor uint64
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL