Fossil SCM

fossil-scm / www / json-api / hacking.md
Source Blame History 353 lines
c95f11b… stephan 1 # JSON API: Hacker's Guide
c95f11b… stephan 2 ([⬑JSON API Index](index.md))
c95f11b… stephan 3
c95f11b… stephan 4 Jump to:
c95f11b… stephan 5
c95f11b… stephan 6 * [Before Committing Changes](#before-committing)
c95f11b… stephan 7 * [JSON C API](#json-c-api)
c95f11b… stephan 8 * [Reporting Errors](#reporting-errors)
c95f11b… stephan 9 * [Getting Command Arguments](#command-args)
c95f11b… stephan 10 * [Creating JSON Data](#creating-json)
c95f11b… stephan 11 * [Creating JSON Values](#creating-json-values)
c95f11b… stephan 12 * [Converting SQL Query Results to JSON](#query-to-json)
c95f11b… stephan 13
c95f11b… stephan 14 This section will only be of interest to those wanting to work on the
c95f11b… stephan 15 Fossil/JSON code. That said...
c95f11b… stephan 16
c95f11b… stephan 17 If you happen to hack on the code and find something worth noting here
c95f11b… stephan 18 for others, please feel free to expand this section. It will only
c95f11b… stephan 19 improve via feedback from those working on the code.
c95f11b… stephan 20
c95f11b… stephan 21 ---
c95f11b… stephan 22
c95f11b… stephan 23 <a id="before-committing"></a>
c95f11b… stephan 24 # Before Committing Changes...
c95f11b… stephan 25
c95f11b… stephan 26 Because this code lives in the trunk, there are certain
c95f11b… stephan 27 guidelines which must be followed before committing any changes:
c95f11b… stephan 28
c95f11b… stephan 29 1. Read the [checkin preparation list](/doc/trunk/www/checkin.wiki).
c95f11b… stephan 30 2. Changes to the files `src/json_*.*`, and its related support code
c95f11b… stephan 31 (e.g. `ajax/*.*`), may be made freely without affecting mainline
c95f11b… stephan 32 users. Changes to other files, unless they are trivial or made for
c95f11b… stephan 33 purposes outside the JSON API (e.g. an unrelated bug fix), must be
c95f11b… stephan 34 reviewed carefully before committing. When in doubt, create a branch
c95f11b… stephan 35 and post a request for a review.
c95f11b… stephan 36 3. The Golden Rule is: *do not break the trunk build*.
c95f11b… stephan 37
c95f11b… stephan 38
c95f11b… stephan 39 <a id="json-c-api"></a>
c95f11b… stephan 40 # JSON C API
c95f11b… stephan 41
c95f11b… stephan 42 libcson, the underlying JSON API, is a separate project, included in
73373b9… stephan 43 fossil in "amalgamation" form: see `extsrc/cson_amalgamation.[ch]`. It has
c95f11b… stephan 44 thorough API docs and a good deal of information is in its wiki:
c95f11b… stephan 45
c95f11b… stephan 46 [](https://fossil.wanderinghorse.net/wikis/cson/)
c95f11b… stephan 47
c95f11b… stephan 48 In particular:
c95f11b… stephan 49
c95f11b… stephan 50 [](https://fossil.wanderinghorse.net/wikis/cson/?page=CsonArchitecture)
c95f11b… stephan 51
c95f11b… stephan 52 gives an overview of its architecture. Occasionally new versions of it
c95f11b… stephan 53 are pulled into the Fossil tree, but other developers generally need not
c95f11b… stephan 54 concern themselves with that.
c95f11b… stephan 55
73373b9… stephan 56 (Trivia: the cson wiki's back-end is fossil using this very JSON API,
73373b9… stephan 57 living on top of a custom JavaScript+HTML5 application.)
c95f11b… stephan 58
c95f11b… stephan 59 Only a small handful of low-level fossil routines actually input or
c95f11b… stephan 60 output JSON text (only for reading in POST data and sending the
c95f11b… stephan 61 response). In the C code we work with the higher-level JSON value
c95f11b… stephan 62 abstractions provided by cson (conceptually similar to an XML DOM). All
c95f11b… stephan 63 of the JSON-defined data types are supported, and we can construct JSON
c95f11b… stephan 64 output of near arbitrary complexity with the caveat that *cyclic data
c95f11b… stephan 65 structures are strictly forbidden*, and *will* cause memory corruption,
c95f11b… stephan 66 crashes, double free()'s, or other undefined behaviour. Because JSON
c95f11b… stephan 67 cannot, without client-specific semantic extensions to JSON, represent
c95f11b… stephan 68 cyclic structures, it is not anticipated that this will be a
c95f11b… stephan 69 problem/limitation when generating output for fossil.
c95f11b… stephan 70
c95f11b… stephan 71
c95f11b… stephan 72
c95f11b… stephan 73 <a id="json-commands"></a>
c95f11b… stephan 74 # Architecture of JSON Commands
c95f11b… stephan 75
c95f11b… stephan 76 In order to consolidate CLI/HTTP modes for JSON handling, this code
c95f11b… stephan 77 foregoes fossil's conventional command/path dispatching mechanism. Only
c95f11b… stephan 78 the top-most "json" command/path is dispatched directly by fossil's
c95f11b… stephan 79 core. The disadvantages of this are that we lose fossil's conventional
c95f11b… stephan 80 help text mechanism (which is based on code comments in the
c95f11b… stephan 81 command/path's dispatcher impl) and the ability to write abbreviated
c95f11b… stephan 82 command names in CLI mode ("json" itself may be abbreviated, but not the
c95f11b… stephan 83 subcommands). The advantages are that we can handle CLI/HTTP modes
a3be0b8… drh 84 almost identically (there are a couple of minor differences) by unifying
c95f11b… stephan 85 them under the same callback functions much more easily.
c95f11b… stephan 86
c95f11b… stephan 87 The top-level "json" command/path uses its own dispatching mechanism
c95f11b… stephan 88 which uses either the path (in HTTP mode) or CLI positional arguments to
c95f11b… stephan 89 dispatch commands (stopping at the first "flag option" (e.g. -foo) in
c95f11b… stephan 90 CLI mode). The command handlers are simply callback functions which
c95f11b… stephan 91 return a cson\_value pointer (the C representation of an arbitrary JSON
c95f11b… stephan 92 value), representing the "payload" of the response (or NULL - not all
c95f11b… stephan 93 responses need a payload). On error these callbacks set the internal
c95f11b… stephan 94 JSON error state (detailed in a subsection below) and return NULL. The
c95f11b… stephan 95 top-level dispatcher then creates a response envelope and returns the
c95f11b… stephan 96 "payload" from the command (if any) to the caller. If a callback sets
c95f11b… stephan 97 the error state, the top-level dispatcher takes care to set the error
c95f11b… stephan 98 information in the response envelope. In summary:
c95f11b… stephan 99
c95f11b… stephan 100 - The top-level dispatchers (`json_page_top()` and `json_cmd_top()`)
c95f11b… stephan 101 are called by fossil's core when the "json" command/path is called.
c95f11b… stephan 102 They initialize the JSON-mode global state, dispatch the requested
c95f11b… stephan 103 command, and handle the creation of the response envelope. They
c95f11b… stephan 104 prepare all the basic things which the individual subcommands need
c95f11b… stephan 105 in order to function.
c95f11b… stephan 106 - The command handlers (most are named `json_page_something()`)
c95f11b… stephan 107 implement the `fossil_json_f()` callback interface (see
c95f11b… stephan 108 [`src/json_detail.h`](/finfo/src/json_detail.h)). They are
c95f11b… stephan 109 responsible for permissions checking, setting any error state, and
c95f11b… stephan 110 passing back a payload (if needed - not all commands return a
c95f11b… stephan 111 payload). It is strictly forbidden for these callbacks to produce
c95f11b… stephan 112 any output on stdout/stderr, and doing so effectively corrupts the
c95f11b… stephan 113 out-bound JSON and HTTP headers.
c95f11b… stephan 114
c95f11b… stephan 115 There is a wrench in all of that, however: the vast majority of fossil's
c95f11b… stephan 116 commands "fail fast" - they will `exit()` if they encounter an error. To
c95f11b… stephan 117 handle that, the fossil core error reporting routines have been
c95f11b… stephan 118 refactored a small bit to operate differently when we are running in
c95f11b… stephan 119 JSON mode. Instead of the conventional output, they generate a JSON
c95f11b… stephan 120 error response. In HTTP mode they exit with code 0 to avoid causing an
c95f11b… stephan 121 HTTP 500 error, whereas in CLI mode they will exit with a non-0 code.
c95f11b… stephan 122 Those routines still `exit()`, as in the conventional CLI/HTTP modes, but
c95f11b… stephan 123 they will exit differently. Because of this, it is perfectly fine for a
c95f11b… stephan 124 command handler to exit via one of fossil's conventional mechanisms
c95f11b… stephan 125 (e.g. `db_prepare()` can be fatal, and callbacks may call `fossil_panic()`
c95f11b… stephan 126 if they really want to). One exception is `fossil_exit()`, which does
c95f11b… stephan 127 _not_ generate any extra output and will `exit()` the app. In the JSON
c95f11b… stephan 128 API, as a rule of thumb, `fossil_exit()` is only used when we *want* a
c95f11b… stephan 129 failed request to cause an HTTP 500 error, and it is reserved for
a3be0b8… drh 130 allocation errors and similar truly catastrophic failures. That said...
c95f11b… stephan 131 libcson has been hacked to use `fossil_alloc()` and friends for memory
c95f11b… stephan 132 management, and those routines exit on error, so alloc error handling in
c95f11b… stephan 133 the JSON command handler code can afford to be a little lax (the
c95f11b… stephan 134 majority of *potential* errors clients get from the cson API have
c95f11b… stephan 135 allocation failure as their root cause).
c95f11b… stephan 136
c95f11b… stephan 137 As a side-note: the vast majority (if not all) of the cson API calls are
c95f11b… stephan 138 "NULL-safe", meaning that will return an error code (or be a no-op) if
c95f11b… stephan 139 passed NULL arguments. e.g. the following chain of calls will not crash
c95f11b… stephan 140 if the value we're looking for does not exist, is-not-a String (see
c95f11b… stephan 141 `cson_value_get_string()` for important details), or if `myObj` is NULL:
c95f11b… stephan 142
c95f11b… stephan 143 ```c
c95f11b… stephan 144 const char * str =
c95f11b… stephan 145 cson_string_cstr( // get the C-string form of a cson_string
c95f11b… stephan 146 cson_value_get_string( // get its cson_string form
c95f11b… stephan 147 cson_object_get(myObj,"foo") // search for key in an Object
c95f11b… stephan 148 )
c95f11b… stephan 149 );
c95f11b… stephan 150 ```
c95f11b… stephan 151
c95f11b… stephan 152 If `"foo"` is not found in `myObj` (or if `myObj` is NULL) then v will be
c95f11b… stephan 153 NULL, as opposed to stepping on a NULL pointer somewhere in that call
c95f11b… stephan 154 chain.
c95f11b… stephan 155
c95f11b… stephan 156 Note that all cson JSON values except Arrays and Objects are *immutable*
c95f11b… stephan 157 - you cannot change a string's or number's value, for example. They also
c95f11b… stephan 158 use reference counting to manage ownership, as documented and
c95f11b… stephan 159 demonstrated on this page:
c95f11b… stephan 160
c95f11b… stephan 161 [](https://fossil.wanderinghorse.net/wikis/cson/?page=TipsAndTricks)
c95f11b… stephan 162
c95f11b… stephan 163 In short, after creating a new value you must eventually *either* add it
c95f11b… stephan 164 to a container (Object or Array) to transfer ownership *or* call
c95f11b… stephan 165 `cson_value_free()` to clean it up (exception: the Fossil/JSON command
c95f11b… stephan 166 callbacks *return* a value to transfer ownership to the dispatcher).
c95f11b… stephan 167 Sometimes it's more complex than that, but not normally. Any given value
c95f11b… stephan 168 may legally be stored in any number of containers (or multiple times
c95f11b… stephan 169 within one container), as long as *no cycles* are introduced (cycles
c95f11b… stephan 170 *will* cause undefined behaviour). Ownership is shared using reference
c95f11b… stephan 171 counting and the value will eventually be freed up when its last
c95f11b… stephan 172 remaining reference is freed (e.g. when the last container holding it is
c95f11b… stephan 173 cleaned up). For many examples of using cson in the context of fossil,
c95f11b… stephan 174 see the existing `json_page_xxx()` functions in `json_*.c`.
c95f11b… stephan 175
c95f11b… stephan 176 <a id="reporting-errors"></a>
c95f11b… stephan 177 # Reporting Errors
c95f11b… stephan 178
c95f11b… stephan 179 To report an error from a command callback, one abstractly needs to:
c95f11b… stephan 180
c95f11b… stephan 181 - Set g.json.resultCode to one of the `FSL_JSON_E_xxx` values
c95f11b… stephan 182 (defined in [`src/json_detail.h`](/finfo/src/json_detail.h)).
c95f11b… stephan 183 - *Optionally* set `g.zErrMsg` to contain the (dynamically-allocated!)
c95f11b… stephan 184 error string to be sent to the client. If no error string is set
c95f11b… stephan 185 then a standard/generic string is used for the given error code.
c95f11b… stephan 186 - Clean up any resources created so far by the handler.
c95f11b… stephan 187 - Return NULL. If it returns non-NULL, the dispatcher will destroy the
c95f11b… stephan 188 value and not include it in the error response.
c95f11b… stephan 189
c95f11b… stephan 190 That normally looks something like this:
c95f11b… stephan 191
c95f11b… stephan 192 ```
c95f11b… stephan 193 if(!g.perm.Read){
c95f11b… stephan 194 json_set_err(FSL_JSON_E_DENIED, "Requires 'o' permissions.");
c95f11b… stephan 195 return NULL;
c95f11b… stephan 196 }
c95f11b… stephan 197 ```
c95f11b… stephan 198
c95f11b… stephan 199 `json_set_err()` is a variadic printf-like function, and can use the
c95f11b… stephan 200 printf extensions supported by mprintf() and friends (e.g. `%Q` and `%q`)
c95f11b… stephan 201 (but they are normally not needed in the context of JSON). If the error
c95f11b… stephan 202 string is NULL or empty then `json_err_cstr(errorCode)` is used to fetch
c95f11b… stephan 203 the standard/generic error string for the given code.
c95f11b… stephan 204
c95f11b… stephan 205 When control returns to the top-level dispatching function it will check
c95f11b… stephan 206 `g.json.resultCode` and, if it is not 0, create an error response using
c95f11b… stephan 207 the `g.json.resultCode` and `g.zErrMsg` to construct the response's
c95f11b… stephan 208 `resultCode` and `resultText` properties.
c95f11b… stephan 209
c95f11b… stephan 210 If a function wants to output an error and exit by itself, as opposed
c95f11b… stephan 211 to returning to the dispatcher, then it must behave slightly
c95f11b… stephan 212 differently. See the docs for `json_err()` (in
c95f11b… stephan 213 [`src/json.c`](/finfo/src/json.c)) for details, and search that file
c95f11b… stephan 214 for various examples of its usage. It is also used by fossil's core
c95f11b… stephan 215 error-reporting APIs, e.g. `fossil_panic()` (defined in [`src/main.c`](/finfo/src/main.c)).
c95f11b… stephan 216 That said, it would be "highly unusual" for a callback to need to do
c95f11b… stephan 217 this - it is *far* simpler (and more consistent/reliable) to set the
c95f11b… stephan 218 error state and return to the dispatcher.
c95f11b… stephan 219
c95f11b… stephan 220 <a id="command-args"></a>
c95f11b… stephan 221 # Getting Command Arguments
c95f11b… stephan 222
c95f11b… stephan 223 Positional parameters can be fetched usinig `json_command_arg(N)`, where
c95f11b… stephan 224 N is the argument position, with position 0 being the "json"
c95f11b… stephan 225 command/path. In CLI mode positional arguments have their obvious
c95f11b… stephan 226 meaning. In HTTP mode the request path (or the "command" request
c95f11b… stephan 227 property) is used to build up the "command path" instead. For example:
c95f11b… stephan 228
c95f11b… stephan 229 CLI: `fossil json a b c`
c95f11b… stephan 230
c95f11b… stephan 231 HTTP: `/json/a/b/c`
c95f11b… stephan 232
c95f11b… stephan 233 HTTP POST or CLI with `--json-input`: /json with POSTed envelope
c95f11b… stephan 234 `{"command": "a/b/c" …}`
c95f11b… stephan 235
c95f11b… stephan 236 Those will have identical "command paths," and `json_command_path(2)`
c95f11b… stephan 237 would return the "b" part.
c95f11b… stephan 238
c95f11b… stephan 239 Caveat: a limitation of this support is that all CLI flags must come
c95f11b… stephan 240 *after* all *non-flag* positional arguments (e.g. file names or
c95f11b… stephan 241 subcommand names). Any argument starting with a dash ("-") is considered
c95f11b… stephan 242 by this code to be a potential "flag" argument, and all arguments after
c95f11b… stephan 243 it are ignored (because the generic handling cannot know if a flag
c95f11b… stephan 244 requires an argument, which changes how the rest of the arguments need
c95f11b… stephan 245 to be interpreted).
c95f11b… stephan 246
c95f11b… stephan 247 To get named parameters, there are several approaches (plus some special
c95f11b… stephan 248 cases). Named parameters can normally come from any of the following
c95f11b… stephan 249 sources:
c95f11b… stephan 250
c95f11b… stephan 251 - CLI arguments, e.g. `--foo bar`
c95f11b… stephan 252 - GET parameters: `/json/...?foo=bar`
c95f11b… stephan 253 - Properties of the POST envelope
c95f11b… stephan 254 - Properties of the `POST.payload` object (if any).
c95f11b… stephan 255
c95f11b… stephan 256 To try to simplify the guessing process the API has a number of
c95f11b… stephan 257 functions which behave ever so slightly differently. A summary:
c95f11b… stephan 258
c95f11b… stephan 259 - `json_getenv()` and `json_getenv_TYPE()` search the so-called "JSON
c95f11b… stephan 260 environment," which is a superset of the GET/POST/`POST.payload` (if
c95f11b… stephan 261 `POST.payload` is-a Object).
c95f11b… stephan 262 - `json_find_option_TYPE()`: searches the CLI args (only when in CLI
c95f11b… stephan 263 mode) and the JSON environment.
a3be0b8… drh 264 - The use of fossil's `P()` and `PD()` macros is discouraged in JSON
c95f11b… stephan 265 callbacks because they can only handle String data from the CLI or
c95f11b… stephan 266 GET parameters (not POST/`POST.payload`). (Note that `P()` and `PD()`
c95f11b… stephan 267 *normally* also handle POSTed keys, but they only "see" values
c95f11b… stephan 268 posted as form-urlencoded fields, and not JSON format.)
c95f11b… stephan 269 - `find_option()` (from `src/main.c`) "should" also be avoided in
c95f11b… stephan 270 JSON API handlers because it removes flag from the g.argv
c95f11b… stephan 271 arguments list. That said, the JSON API does use `find_option()` in
c95f11b… stephan 272 several of its option-finding convenience wrappers.
c95f11b… stephan 273
c95f11b… stephan 274 For example code: the existing command callbacks demonstrate all kinds
c95f11b… stephan 275 of uses and the various styles of parameter/option inspection. Check out
c95f11b… stephan 276 any of the functions named `json_page_SOMETHING()`.
c95f11b… stephan 277
c95f11b… stephan 278 <a href="creating-json"></a>
c95f11b… stephan 279 # Creating JSON Data
c95f11b… stephan 280
c95f11b… stephan 281 <a href="creating-json-values"></a>
c95f11b… stephan 282 ## Creating JSON Values
c95f11b… stephan 283
c95f11b… stephan 284 cson has a fairly rich API for creating and manipulating the various
a3be0b8… drh 285 JSON-defined value types. For a detailed overview and demonstration I
c95f11b… stephan 286 recommend reading:
c95f11b… stephan 287
c95f11b… stephan 288 [](https://fossil.wanderinghorse.net/wikis/cson/?page=HowTo)
c95f11b… stephan 289
c95f11b… stephan 290 That said, the Fossil/JSON API has several convenience wrappers to save
c95f11b… stephan 291 a few bytes of typing:
c95f11b… stephan 292
c95f11b… stephan 293 - `json_new_string("foo")` is easier to use than
c95f11b… stephan 294 `cson_value_new_string("foo", 3)`, and
c95f11b… stephan 295 `json_new_string_f("%s","foo")` is more flexible.
c95f11b… stephan 296 - `json_new_int()` is easier to type than `cson_value_new_integer()`.
c95f11b… stephan 297 - `cson_output_Blob()` and `cson_parse_Blob()` can write/read JSON
c95f11b… stephan 298 to/from fossil `Blob`-type objects.
c95f11b… stephan 299
c95f11b… stephan 300 It also provides several lower-level JSON features which aren't of
c95f11b… stephan 301 general utility but provide necessary functionality for some of the
c95f11b… stephan 302 framework-level code (e.g. `cson_data_dest_cgi()`), which is only used
c95f11b… stephan 303 by the deepest of the JSON internals).
c95f11b… stephan 304
c95f11b… stephan 305
c95f11b… stephan 306 <a href="query-to-json"></a>
c95f11b… stephan 307 ## Converting SQL Query Results to JSON
c95f11b… stephan 308
c95f11b… stephan 309 The `cson_sqlite3_xxx()` family of functions convert `sqlite3_stmt` rows
c95f11b… stephan 310 to Arrays or Objects, or convert single columns to a JSON-compatible
c95f11b… stephan 311 form. See `json_stmt_to_array_of_obj()`,
c95f11b… stephan 312 `json_stmt_to_array_of_array()` (both in `src/json.c`), and
c95f11b… stephan 313 `cson_sqlite3_column_to_value()` and friends (in
73373b9… stephan 314 `extsrc/cson_amalgamation.h`). They work in an intuitive way for numeric
a3be0b8… drh 315 types, but they optimistically/naively *assume* that any fields of type
c95f11b… stephan 316 TEXT or BLOB are actually UTF8 data, and treat them as such. cson's
c95f11b… stephan 317 string class only handles UTF8 data and it is semantically illegal to
c95f11b… stephan 318 feed them anything but UTF8. Violating this will likely result in
a3be0b8… drh 319 down-stream errors (e.g. when emitting the JSON string output). **The
c95f11b… stephan 320 moral of this story is:** *do not use these APIs to fetch binary data*.
c95f11b… stephan 321 JSON doesn't do binary and the `cson_string` class does not
c95f11b… stephan 322 protect itself against clients feeding it non-UTF8 data.
c95f11b… stephan 323
c95f11b… stephan 324 Here's a basic example of using these features:
c95f11b… stephan 325
c95f11b… stephan 326 ```c
c95f11b… stephan 327 Stmt q = empty_Stmt;
c95f11b… stephan 328 cson_value * rows = NULL;
c95f11b… stephan 329 db_prepare(&q, "SELECT a AS a, b AS b, c AS c FROM foo");
c95f11b… stephan 330 rows = json_stmt_to_array_of_obj( &sql, NULL );
c95f11b… stephan 331 db_finalize(&q);
c95f11b… stephan 332 // side note: if db_prepare()/finalize() fail (==they exit())
c95f11b… stephan 333 // then a JSON-format error reponse will be generated.
c95f11b… stephan 334 ```
c95f11b… stephan 335
c95f11b… stephan 336 On success (and if there were results), `rows` is now an Array value,
c95f11b… stephan 337 each entry of which contains an Object containing the fields (key/value
c95f11b… stephan 338 pairs) of each row. `json_stmt_to_array_of_array()` returns each row
c95f11b… stephan 339 as an Array containing the column values (with no column name
c95f11b… stephan 340 information).
c95f11b… stephan 341
c95f11b… stephan 342 **Note the seemingly superfluous use of the "AS" clause in the above
c95f11b… stephan 343 SQL.** Having them is actually significant! If a query does *not* use AS
c95f11b… stephan 344 clauses, the row names returned by the db driver *might* be different
c95f11b… stephan 345 than they appear in the query (this is documented behaviour of sqlite3).
c95f11b… stephan 346 Because the JSON API needs to return stable field names, we need to use
c95f11b… stephan 347 AS clauses to be guaranteed that the db driver will return the column
c95f11b… stephan 348 names we want. Note that the AS clause is often used to translate column
c95f11b… stephan 349 names into something more JSON-conventional or user-friendly, e.g.
c95f11b… stephan 350 "SELECT cap AS capabilities...". Alternately, we can convert the
c95f11b… stephan 351 individual `sqlite3_stmt` column values to JSON using
a3be0b8… drh 352 `cson_sqlite3_column_to_value()`, without referring directly to the
c95f11b… stephan 353 db-reported column name.

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button