├── CHANGELOG.md ├── LICENSE ├── README.md ├── blueprints.yaml ├── languages.yaml ├── shortcodes └── SqlTableShortcode.php ├── sqlite.php ├── sqlite.yaml └── templates └── partials ├── sql-db-error.html.twig ├── sql-json.html.twig ├── sql-sql-error.html.twig └── sql-table.html.twig /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v1.5.4 2 | ## 22 Dec 2020 3 | 1. [](#typo) 4 | ``* fixed readme 5 | 6 | # v1.5.3 7 | ## 18 July 2020 8 | 1. []](#bugfix) Extra security measure added - credit @hughbris 9 | 10 | # v1.5.2 11 | ## 19 December 2018 12 | 1. [](#bugfix) 13 | * propagate id in shortcode to table, 14 | * credit to Matt Marsh @marshmn 15 | 2. [](#bugfix) 16 | * log function at line 16 SqlTableShortcode requires two parameters, not one. 17 | * credit to @dlannan 18 | 19 | # v1.5.1 20 | ## 1 August 2018 21 | 1. [](#documentation) 22 | * A more elaborate example is added to show how to obtain data from a database, change data by creating a form, then update the row in the database. 23 | 24 | # v1.5.0 25 | ## 16 June 2018 26 | 1. [](#enhancement) 27 | * Extra security option. Allows for more paranoia 28 | * When enabled, each page with an [sql-table] shortcode must have explicit header permission 29 | * This is to prevent shortcode being added in the front end by an editor 30 | 1. [](#minorbug) 31 | * fix file permission from 0666 to 0664 32 | 33 | # v1.4.0 34 | ## 23 June 2018 35 | 1. [](#enhancement) 36 | * More logging options, allowing for the SELECT, INSERT and UPDATE stanzas (in addition to Errors) 37 | to be trapped. 38 | * Append the logged data to the directory `user/data/sqlite` as a 'log.txt' file 39 | * This allows an Administrator to view the data from within the Admin panel using the DataManager plugin. 40 | 2. [](#change of configuration) 41 | * The default configuration for the placement of the sqlite3 database is now `user/data/sqlite` 42 | 43 | # v1.3.0 44 | ## 11 June 2018 45 | 1. [](#enhancement) 46 | * Add `ignore` field in `sql-insert` and `sql-update` 47 | * Add `error_logging` configuration option, so that SQL errors are written to hard drive 48 | 49 | # v1.2.3 50 | ## 7 June 2018 51 | 1. [](#update) 52 | * Add shortcode dependency to blueprints 53 | 54 | # v1.2.2 55 | ## 5/05/2018 56 | 1. [*](#bug and enhancement) 57 | * Process `sqlite-insert` corrected to `sql-insert` 58 | * Removed fields provided by `Form` plugin, viz. `_xxx` & `form-nonce` 59 | 60 | # v1.2.1 61 | ## 5 May 2018 62 | 1. [*](#minor) 63 | * error in translate call 64 | 65 | #v1.2.0 66 | ## 28 April 2018 67 | 1. [*](#major ) 68 | * Allow for Twig variables to be used in SELECT stanza in shortcode, and in 'where' field of UPDATE Form process. 69 | 70 | # v1.0.0 - v1.2.0 71 | ## < 28 April 2018 72 | 73 | 1. [*](#new) 74 | * Initial work 75 | 2. [*](#minor) 76 | * Removal of debug code/messages 77 | * refactor in case of zero data in form. 78 | 3. [*](#minor) 79 | * Allow for `where` to be a Form Field as well as a Process parameters 80 | 4. [*](#major) 81 | * New sql option to provide json serialisation instead of HTML serialisation of data 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Richard Hainsworth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sqlite Plugin 2 | 3 | The **Sqlite** Plugin is for [Grav CMS](http://github.com/getgrav/grav). The shortcode `[sql-table]` and the ***Form*** actions `sql-insert` and `sql-update` are provided to interact with an ***Sqlite3*** database. 4 | 5 | ## Installation 6 | 7 | Installing the Sqlite plugin can be done in one of two ways. The GPM (Grav Package Manager) installation method enables you to quickly and easily install the plugin with a simple terminal command, while the manual method enables you to do so via a zip file. 8 | 9 | ### GPM Installation (Preferred) 10 | 11 | The simplest way to install this plugin is via the [Grav Package Manager (GPM)](http://learn.getgrav.org/advanced/grav-gpm) through your system's terminal (also called the command line). From the root of your Grav install type: 12 | 13 | bin/gpm install sqlite 14 | 15 | This will install the Sqlite plugin into your `/user/plugins` directory within Grav. Its files can be found under `/your/site/grav/user/plugins/sqlite`. 16 | 17 | ### Manual Installation 18 | 19 | To install this plugin, just download the zip version of this repository and unzip it under `/your/site/grav/user/plugins`. Then, rename the folder to `sqlite`. You can find these files on [GitHub](https://github.com/finanalyst/grav-plugin-sqlite) or via [GetGrav.org](http://getgrav.org/downloads/plugins#extras). 20 | 21 | You should now have all the plugin files under 22 | 23 | /your/site/grav/user/plugins/sqlite 24 | 25 | > NOTE: This plugin is a modular component for Grav which requires [Grav](http://github.com/getgrav/grav) and the [Error](https://github.com/getgrav/grav-plugin-error), 26 | [Form](https://github.com/getgrav/grav-plugin-form), [ShortcodeCore](https://github.com/getgrav/grav-plugin-shortcode-core) 27 | and [Problems](https://github.com/getgrav/grav-plugin-problems) to operate. 28 | The plugin also requires that the **SQLite3** extension is available with the version of php operating on your site. 29 | 30 | ### Database Installation 31 | An adminstrator must create a directory for the database and place within it the *sqlite3* database file. It is recommended that the directory is `user/data/sqlite` (see configuration). 32 | 33 | ## Configuration 34 | 35 | Before configuring this plugin, you should copy the `user/plugins/sqlite/sqlite.yaml` to `user/config/plugins/sqlite.yaml` and only edit that copy. 36 | 37 | Here is the default configuration and an explanation of available options: 38 | 39 | ```yaml 40 | enabled: true 41 | database_route: data/sqlite 42 | database_name: db.sqlite3 43 | extra_security: false 44 | logging: false 45 | all_logging: false # this option only becomes active when logging is True 46 | error_logging: false # this option only becomes active when logging is True and all_logging is False 47 | select_logging: false 48 | insert_logging: false 49 | update_logging: false 50 | ``` 51 | - `enabled` turns on the plugin for the whole site. If `false`, then making it active on a page will have no effect. 52 | - `database_route` is the Grav route (relative to the 'user' subdirectory) to the location of the `SQLite3` database. 53 | - `database_name` is the full name (typically with the extension .sqlite3) of the database file. It is the responsibility of the site developer/maintainer to create the database. 54 | - `extra_security` enables a more paranoid setting. When `true`, a page may only contain an [sql-table] shortcode if the page header explicitly allows for on. (See below for onpage configuration when option is enabled.) 55 | - `logging` when false, nothing extra happens. When `true`, SQL related data is logged to a file called `sqlite.txt` in the directory given by `database_route`. If however there is an error in setting `database_route`, 56 | then the directory is `user/data/sqlite`. 57 | 58 | >SUGGESTION: If the DataManager plugin is installed and the default route is retained, then the SQL logs can be viewed from the Admin panel. 59 | 60 | - `all_logging` only become active when `logging` is enabled. If true, then all stanzas and errors are recorded. 61 | - `error_logging` only becomes active when `logging` is enabled and `all_logging` is not enabled. 62 | - `select_logging` only becomes active when `logging` is enabled and `all_logging` is not enabled. 63 | - `insert_logging` only becomes active when `logging` is enabled and `all_logging` is not enabled. 64 | - `update_logging` only becomes active when `logging` is enabled and `all_logging` is not enabled. 65 | 66 | >NOTE: The database must exist. If it does not, then an error is generated. 67 | `logging` should not be used in production settings as it writes to the hard drive, slowing performance. 68 | 69 | ### Per page configuration 70 | * Shortcodes can be enabled separately using the `shortcode-core` configuration. To disable shortcodes being used on all pages, but only used on selected pages, configure the shortcode-core plugin inside the Admin panel with `enabled=true` and `active=false`. Then on each page where shortcodes are used, include in the front section of the page: 71 | ```yaml 72 | shortcode-core: 73 | active: true 74 | ``` 75 | * When `extra_security` is enabled, then on a page in which the `[sql-table]` shortcode may be used, the page header must contain 76 | ```yaml 77 | sqliteSelect: allow 78 | ``` 79 | 80 | ## Usage 81 | A shortcode and a Form action are provided. 82 | 1. `[sql-table]` to generate a table (or ***json*** string) from data in the database 83 | 2. the `sql-insert` action for a ***Form*** is used to move data from a form to the database. 84 | 1. the `sql-update` action for a ***Form*** is used to update an existing row of data in the database. 85 | 86 | When the plugin is first initialised, it verifies that the database exists. If it does not exist, then every instance of the `[sql-table]` shortcode is replaced with an error message and the Form generates an error message when the submit button is pressed. 87 | 88 | >NOTE: Although as many errors as possible are trapped, it should be remembered that GRAV uses redirects 89 | extensively, eg., in login forms or sequential forms, which means the error messages may be overwritten. If 90 | this happens, then set `error_logging` to **true** whilst debugging. 91 | 92 | ### [sql-table] Shortcode 93 | 94 | In the page content the shortcode is used as follows: 95 | ```md 96 | [sql-table]SELECT stanza[/sql-table] 97 | ``` 98 | 99 | If `extra_security` is not enabled, or `extra_security` is enabled ** AND ** the page header contains the field `sqliteSelect: allow`, then 100 | the plugin then generates an html table (or json, see below) with the headers as returned by the select stanza, and the body containing the row data. (In the remainder of this documentation, it is assumed that `extra_security` is **NOT** enabled.) 101 | 102 | The SELECT stanza can be complex referring to multiple tables in the database. An SQLite3 query will return a table of rows with the same number of elements, which will fit into a simple HTML table. 103 | 104 | Since the data would normally be updated, it is recommended that the page header contains: 105 | ```yaml 106 | cache-enabled: false 107 | ``` 108 | 109 | The `[sql-table]...[/sql-table]` stanza can be embedded in other shortcodes, such as [ScrolledTableShortcode](https://github.com/finanalyst/grav-scrolled-table-shortcode) plugin, or configured with the [Tablesorter](https://github.com/Perlkonig/grav-plugin-tablesorter) plugin. 110 | 111 | #### Example 112 | Assuming that: 113 | - The default plugin configuration is not changed 114 | - The file `/user/data/db.sqlite3` exists 115 | - The database has a table `people`, which in turn has the fields 116 | - `name` 117 | - `surname` 118 | - `telephone` 119 | - `gender` 120 | 121 | Then the following code and sql stanza (standard SQLite3 allows new lines and indentation for clarity) 122 | ```md 123 | [sql-table] 124 | SELECT name, surname, telephone, gender 125 | FROM people 126 | LIMIT 4 127 | [/sql-table] 128 | ``` 129 | will be rendered something like 130 | ```html 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 |
namesurnametelephonegender
xyzqwe1234Male
xyz1qwe1234Male
xyz2qwe1234Male
xyz3qwe1234Male
152 | ``` 153 | It is also possible for the SELECT stanza in the xxx.md file to contain a Twig variable. For example: 154 | ```md 155 | [sql-table] 156 | SELECT name, surname, telephone, gender 157 | FROM people 158 | LIMIT {{ userinfo.lines }} 159 | [/sql-table] 160 | ``` 161 | For this to work, Twig processing must be enabled, viz., in the page header there should be the line 162 | ```Yaml 163 | process: 164 | twig: true 165 | ``` 166 | 167 | #### Options 168 | The following options are allowed: 169 | - class 170 | - id 171 | - hidden 172 | - json 173 | 174 | ##### class 175 | Provided so that a `css` class can be added to the table. Thus 176 | ```md 177 | [sql-table class=SomeName]SELECT stanza[/sql-table] 178 | ``` 179 | will be rendered as 180 | ```html 181 | 182 | ... 183 |
184 | ``` 185 | ##### id 186 | Provided so that an `id` can be added to the table. Thus 187 | ```md 188 | [sql-table id=SomeId]SELECT stanza[/sql-table] 189 | ``` 190 | will be rendered as 191 | ```html 192 | 193 | ... 194 |
195 | ``` 196 | ##### hidden 197 | If a list of column names is provided, then the column will not be displayed. The column names must be the same as the column names returned by the SQL stanza, eg., 198 | ```md 199 | [sql-table hidden="id idnum"]SELECT row-id as id, Passport as idnum, name, surname, telephone from people 200 | [/sql-table] 201 | ``` 202 | >Notes: 203 | 1. Column names are matched using `\s+` or whitespace. 204 | This means that the column heads must be a single word (including `_`). This can be done by using `AS` to rename column names in the `SELECT` statement. Since the column headers are to be hidden, it does not matter what they look like. 205 | 2. Shortcode parameters must be in double quotes `"..."` not single quotes `'...'`. 206 | 207 | Column hiding is accomplished by adding a `style="display: none;"` to the relevant `` and `` elements. Consequently, the data still exists in the HTML table, and so can be scrubbed or viewed by looking at the page source. 208 | 209 | However, the intent of this option is to make the data available for use by JS or Jquery functions, or for updating the SQL database, but for it not to be immediately visible. For example, in order to update a row (a feature to be added), a row-id will be needed, but usually it is irrelevant for the user to see the row-id. 210 | 211 | ##### json 212 | 213 | Sometimes data is required as a `json` string, eg., to include as data for other shortcodes, rather than as an **HTML** ``. 214 | 215 | For this purpose, the json option is provided: 216 | ```md 217 | [sql-table json]SELECT stanza[/sql-table] 218 | ``` 219 | 220 | The **json** string will be an array `[]` of hash elements `{}`, one hash for each row of the table. 221 | 222 | The keys of the hash are the names of the columns in the SELECT stanza. For example, 223 | ```md 224 | [sql-table json] 225 | SELECT strftime('%H:%m',time,'unixepoch','localtime') as key, latitude as lat, longitude as lng 226 | FROM tracking 227 | WHERE id=1 228 | [/sql-table] 229 | ``` 230 | This assumes a table of latitude and longitude readings over time in unix seconds for units with a given id. 231 | 232 | This will be rendered by the `sqlite-plugin` as 233 | ```md 234 | [ 235 | { "key": "20:10", "lat": 123.012, "lng": 22.1234}, 236 | { "key": "20:20", "lat": 123.546, "lng": 22.112} 237 | ] 238 | ``` 239 | 240 | When this option is used, the values of the other options `class`, `id`, or `hidden` are ignored because they only have significance for an **HTML** `
`. 241 | 242 | ### Form Action `sql-insert` 243 | A GRAV form is created within the page as described by the GRAV documentation. However, the `process` list contains the word `sql-insert`. 244 | 245 | #### Example 246 | In the page header, assuming the page is form.md 247 | ```yaml 248 | form: 249 | name: Input data 250 | method: POST 251 | fields: 252 | - name: name # the value here must be the same as a field in the database 253 | label: Name of person 254 | type: text 255 | - name: surname 256 | label: Surname of the person 257 | type: text 258 | - name: telephone 259 | type: text 260 | label: Telephone number of the person 261 | - name: gender 262 | type: select 263 | label: Person's gender 264 | options: 265 | male: male 266 | female: female 267 | process: 268 | - sql-insert: # this is the crucial one 269 | table: people # this must match the table the data is being added to 270 | - redirect: showdata # this is optional (see note below) 271 | buttons: 272 | - type: submit 273 | value: Add person to database 274 | - type: reset 275 | value: Reset 276 | reset: true # this is advised to prevent the same data being added multiple times. 277 | ``` 278 | When the submit button is pressed, the following stanza is sent to the database: 279 | ```sql 280 | INSERT INTO people (name, surname, telephone, gender) VALUES (...) 281 | ``` 282 | 283 | The form plugin offers considerable flexibility for validating data before being sent to the database. 284 | 285 | >NOTE1: No further validation of the data is carried out by the plugin. 286 | 287 | In the example above, the process list has a redirect to another slug. This is optional. However, if the data is added correctly, it can be viewed using an `[sql-table]` shortcode with an appropriate `SELECT` stanza. The redirect action replaces the Form so that if, as recommended, the `reset` option is set to `true`, returning to the Form will set to default the fields, thus preventing inadvertent data duplication. 288 | 289 | If there is an error (non-unique data, or incorrect fields), the `redirect` action is short-circuited. 290 | 291 | ### Form Action `sql-update` 292 | A GRAV form is created within the page as described by the GRAV documentation. However, the `process` list contains the word `sql-update`. 293 | 294 | #### Example 295 | In the page header, assuming the page is form.md 296 | ```yaml 297 | form: 298 | name: Input data 299 | method: POST 300 | fields: 301 | - name: telephone 302 | type: text 303 | label: Telephone number of the person 304 | - name: status 305 | type: select 306 | label: Club membership 307 | options: 308 | ordinary: Ordinary 309 | VIP: VIP 310 | Senior: Senior 311 | - name: where # a mandatory option (note lower case only). It is the full WHERE expression 312 | type: hidden # this is a where FIELD 313 | content: ' row-id = "3" ' # the single quotes are needed to ensure the double quotes are included 314 | process: 315 | - sql-update: # this is the crucial one 316 | table: people # this must match the table the data is being added to 317 | where: ' row-id = "3" ' # an alternative to the where field. 318 | # a where field takes precedence over a where parameter 319 | - redirect: showdata # this is optional 320 | buttons: 321 | - type: submit 322 | value: Update person to database 323 | - type: reset 324 | value: Reset 325 | reset: true # this is advised to prevent the same data being added multiple times. 326 | ``` 327 | > NOTE: It is mandatory to provide `where` data, either as a form field, or a process attribute. 328 | 329 | When the submit button is pressed, the following stanza is sent to the database: 330 | ```sql 331 | UPDATE people 332 | SET telephone = , 333 | status = 334 | WHERE row-id = "3" 335 | ``` 336 | Here <...> is the value given in the ***Form*** for the relevant field. 337 | 338 | It is possible to include a Twig variable in the `WHERE` data, eg., 339 | ```Yaml 340 | form: 341 | process: 342 | - sql-update: 343 | table: people 344 | where: ' row-id = "{{ userinfo.userid }}" ' 345 | ``` 346 | Then it is possible to use another mechanism, such as the `persistent-data` plugin, to arrange for a Twig variable to contain the necessary information. 347 | 348 | For this to work, Twig processing in headers needs to be set for the site. 349 | 350 | ## Fields for `sql-update` & `sql-insert` 351 | The following fields are defined for these two **Form** processes: 352 | 353 | 1. `table` - This is mandatory, and is the table to which the sql stanza is applied. 354 | 1. `where` - This is mandatory for `sql-update` & ignored for `sql-insert`. 355 | 1. `ignore` - This is optional for both. It is followed by an array of field names that are not included in the stanza. Eg. 356 | ```yaml 357 | form: 358 | process: 359 | - sql-insert: 360 | table: people 361 | ignore: 362 | - status 363 | ``` 364 | 365 | ## Security 366 | Security is an issue because a `sql-insert` and `sql-update` form actions allows a 367 | page user to modify an existing database, and therefore corrupt it - at the very least by adding unnecessary data. 368 | 369 | The website designer should therefore make sure that Forms with `sql-insert` and `sql-update` actions are only available on Grav pages that are protected. 370 | 371 | For example, using the `Login` plugin, only users with certain privileges or belonging to certain groups can be allowed in. 372 | 373 | Alternatively, using the `Private` plugin, a password can be created for the page. 374 | 375 | Some plugins allow for authorised users to modify content in the frontend. This would allow a user to add an `[sql-table]` within the markdown content of a page, and thus to access data on a website database. In order to allow a website designer to protect against such an accidental or malicious intrusion, the `extra_security` option is provided in the `sqlite` plugin configuration. It is `false` by default, to allow for backward compatibility. (See above for more information about usage.) 376 | 377 | ## Example 378 | 379 | The following is part of a page to show how to combine `sqlite` and the `datatable` shortcode, together with jQuery code to update a database. 380 | 381 | When the page is generated, the Form plugin creates the html of a `
` where xxx is the name of the form in the header. Then the outer shortcode is called, which calls the inner shortcodes. The outer shortcode is a `[datatables]`, which initialises a DataTables object and links to the DataTables jQuery. The DataTables query limits the number rows on the page, provides ordering and search functionality. When a row is selected, it also provides the funactionality for extracting the data from the row on a column by column basis. 382 | 383 | The `[datatables]` shortcode expects to have an html `
`as its content. This is provided by the `[sql-table]` shortcode. 384 | 385 | In order to provide for JQuery code that can be triggered by clicking on row, and so transfering data from the table to the form, a `[dt-script]` shortcode is added. When a row is first selected, the class selected is added to the row, which can then be rendered differently. When the row is clicked again, it is deselected. 386 | 387 | When the `submit` button is clicked on the form, the data in the fields, which has been transfered from the DataTable, is proceessed by the `sql-update` form action, and the database is updated. 388 | 389 | The following code would be in `form.md` file. 390 | ``` 391 | --- 392 | title: Alter User data 393 | form: 394 | name: alter-client-form # this is the name given to the form and is the select for jQuery code. 395 | fields: 396 | - name: name # this field is for displaying the selected client 397 | label: Client 398 | type: display 399 | content: undefined 400 | - name: client # this field is needed in the redirect page to report on the changes 401 | type: hidden 402 | - name: client_id # this is the PRIMARY key 403 | type: hidden 404 | - name: telephone # a field that is to be altered depending on input 405 | label: 'Telephone # [ eg. 1234 5678 ]' 406 | type: text 407 | validate: 408 | pattern: '[0-9]{4}\s[0-9]{4}' 409 | help: Telephone should be like: 1234 5678 410 | placeholder: 1234 5678 411 | - name: idserial 412 | label: 'HKID # eg. A123456(7), use capital letters' 413 | type: text 414 | validate: 415 | pattern: '[A-Z]{1,2}[0-9]{6}\([0-9A]\)' 416 | help: Should be like: A123456(7) 417 | placeholder: A123456(7) 418 | - name: dtest 419 | label: 'Driving Test result' 420 | type: text 421 | validate: 422 | pattern: '[0-2]\|.+?\|2[0-9]{3}-[0-9]{2}-[0-9]{2}' 423 | - name: where # this is essential for an UPDATE stanza. 424 | type: hidden 425 | - name: type 426 | label: Client registration type 427 | type: select 428 | options: 429 | Ordinary: Ordinary 430 | VIP: VIP 431 | Senior: Senior 432 | Support: Support 433 | Blacklist: Blacklist 434 | buttons: 435 | - type: submit 436 | value: "Alter Client Data" 437 | - type: reset 438 | value: Reset 439 | process: 440 | - sql-update: 441 | table: clients # the table that is to be updated 442 | ignore: 443 | - client # we wanted the client field for the redirect, but we want to exclude it from the UPDATE stanza 444 | - redirect: operations/alteruser/info # at this route, there is an file with twig to display the updated info 445 | reset: true 446 | cache_enable: false 447 | --- 448 | # Clients 449 | [datatables] 450 | [sql-table hidden="client_id"] 451 | SELECT client_id, name || " " || upper( surname ) as client, telephone, type, idserial as 'HKID #', dtest as 'Driver Status' 452 | FROM clients 453 | /* This is a simple SELECT statement. client_id is the PRIMARY key and replaces row-id. 454 | This short code will generate a table with five columns (the client_id column is hidden) */ 455 | [/sql-table] 456 | [dt-script] 457 | /* the dt-script shortcode is part of the datatables plugin. It is included as part of jQuery function 458 | ** that initialises the datatables jQuery plugin. 459 | ** It is jQuery code. 460 | ** The variable 'selector' is generated by the [datatables] shortcode. 461 | */ 462 | var table = $(selector).DataTable(); 463 | $(selector + ' tbody').on( 'click', 'tr', function () { 464 | /* the function is triggered when a row of the table is selected with a mouse click. 465 | ** the class selected is styled by css associated with Datatables. 466 | */ 467 | if ( $(this).hasClass('selected') ) { 468 | // if a row is already selected when clicked, it is deselected 469 | // the data transfered when a row is selected is re-initialised. 470 | $(this).removeClass('selected'); 471 | $('#alter-client-form input[name="data[where]"]').val(''); 472 | /* the form name is defined in the page header 473 | ** GRAV Form generates input elements with the attribute 474 | ** name=data[xxxx] where xxxx is the name of the field in the form definition 475 | */ 476 | $('#alter-client-form input[name="data[client]"]').val(''); 477 | $('#alter-client-form input[name="data[telephone]"]').val(''); 478 | $('#alter-client-form select[name="data[type]"]').val('Ordinary'); 479 | $('#alter-client-form input[name="data[idserial]"]').val(''); 480 | $('#alter-client-form input[name="data[dtest]"]').val(''); 481 | $('#alter-client-form div:first-of-type div:nth-of-type(2) div').html('undefined'); 482 | /* This selector refers to where the Display field is generated in the FORM. 483 | */ 484 | } 485 | else { 486 | table.$('tr.selected').removeClass('selected'); 487 | $(this).addClass('selected'); 488 | var rd = table.row('.selected').data(); 489 | // get a row using a function provided by DataTables 490 | $('#alter-client-form input[name="data[where]"]').val('client_id=' + rd[0]); 491 | /* this is vital for an UPDATE. It is provided as the WHERE clause. 492 | ** Here we get the client_id from the selected row of the table. It is in the 493 | ** first (zeroth) column 494 | */ 495 | $('#alter-client-form input[name="data[client]"]').val(rd[1]); 496 | // transfer data from the DataTable to the form input elements 497 | $('#alter-client-form input[name="data[telephone]"]').val(rd[2]); 498 | $('#alter-client-form select[name="data[type]"]').val(rd[3]); 499 | $('#alter-client-form input[name="data[idserial]"]').val(rd[4]); 500 | $('#alter-client-form input[name="data[dtest]"]').val(rd[5]); 501 | // Transfer data to the display field to show the Name of the selected client 502 | $('#alter-client-form div:first-of-type div:nth-of-type(2) div').html(rd[1]); 503 | } 504 | } ); 505 | $('#alter-client-form').on('reset', function(e) { 506 | setTimeout( function() { 507 | // a reset button is provided, so when clicked re-initialise for form 508 | table.$('tr.selected').removeClass('selected'); 509 | $('#alter-client-form input[name="data[where]"]').val(''); 510 | $('#alter-client-form input[name="data[client]"]').val(''); 511 | $('#alter-client-form input[name="data[telephone]"]').val(''); 512 | $('#alter-client-form input[name="data[idserial]"]').val(''); 513 | $('#alter-client-form input[name="data[dtest]"]').val(''); 514 | $('#alter-client-form select[name="data[type]"]').val('Ordinary'); 515 | $('#alter-client-form div:first-of-type div:nth-of-type(2) div').html('undefined'); 516 | }); 517 | }); 518 | [/dt-script] 519 | [/datatables] 520 | ``` 521 | 522 | ## Credits 523 | For bug fixes, thanks to 524 | - Matt Marsh @marshmn 525 | - @dlannan 526 | - @hughbri - for extra security 527 | 528 | ## To Do 529 | - Internationalise. Add more languages to `langages.yaml` 530 | -------------------------------------------------------------------------------- /blueprints.yaml: -------------------------------------------------------------------------------- 1 | name: Sqlite 2 | version: '1.5.4' 3 | description: Plugin to select, update and insert into an sqlite3 database 4 | icon: database 5 | author: 6 | name: Richard N Hainsworth 7 | email: rnhainsworth@gmail.com 8 | homepage: https://github.com/finanalyst/grav-plugin-sqlite 9 | bugs: https://github.com/finanalyst/grav-plugin-sqlite/issues 10 | keywords: [sqlite, grav, plugin] 11 | readme: https://github.com/finanalyst/grav-plugin-sqlite/README.md 12 | license: MIT 13 | 14 | dependencies: 15 | - shortcode-core 16 | 17 | form: 18 | fields: 19 | database_name: 20 | type: text 21 | default: db.sqlite3 22 | label: PLUGIN_SQLITE.DATABASE_NAME 23 | help: PLUGIN_SQLITE.DATABASE_NAME_HELP 24 | database_route: 25 | type: text 26 | default: data 27 | label: PLUGIN_SQLITE.DATABASE_ROUTE 28 | help: PLUGIN_SQLITE.DATABASE_ROUTE_HELP 29 | extra_security: 30 | type: toggle 31 | default: 0 32 | highlight: 0 33 | options: 34 | 1: Enabled 35 | 0: Disabled 36 | validate: 37 | type: bool 38 | label: PLUGIN_SQLITE.EXTRA_SECURITY 39 | help: PLUGIN_SQLITE.EXTRA_SECURITY_HELP 40 | logging: 41 | type: toggle 42 | highlight: 0 43 | default: 0 44 | options: 45 | 1: Enabled 46 | 0: Disabled 47 | validate: 48 | type: bool 49 | label: PLUGIN_SQLITE.LOGGING 50 | help: PLUGIN_SQLITE.LOGGING_HELP 51 | log_check: 52 | type: conditional 53 | condition: config.plugins.sqlite.logging 54 | fields: 55 | all_logging: 56 | type: toggle 57 | highlight: 0 58 | default: 0 59 | options: 60 | 1: Enabled 61 | 0: Disabled 62 | validate: 63 | type: bool 64 | label: PLUGIN_SQLITE.ALL_LOGGING 65 | help: PLUGIN_SQLITE.ALL_LOGGING_HELP 66 | discrete_logging: 67 | type: conditional 68 | condition: "config.plugins.sqlite.logging and not config.plugins.sqlite.all_logging " 69 | fields: 70 | error_logging: 71 | type: toggle 72 | highlight: 0 73 | default: 0 74 | options: 75 | 1: Enabled 76 | 0: Disabled 77 | validate: 78 | type: bool 79 | label: PLUGIN_SQLITE.ERROR_LOGGING 80 | help: PLUGIN_SQLITE.ERROR_LOGGING_HELP 81 | select_logging: 82 | type: toggle 83 | highlight: 0 84 | default: 0 85 | options: 86 | 1: Enabled 87 | 0: Disabled 88 | validate: 89 | type: bool 90 | label: PLUGIN_SQLITE.SELECT_LOGGING 91 | help: PLUGIN_SQLITE.SELECT_LOGGING_HELP 92 | insert_logging: 93 | type: toggle 94 | highlight: 0 95 | default: 0 96 | options: 97 | 1: Enabled 98 | 0: Disabled 99 | validate: 100 | type: bool 101 | label: PLUGIN_SQLITE.INSERT_LOGGING 102 | help: PLUGIN_SQLITE.INSERT_LOGGING_HELP 103 | update_logging: 104 | type: toggle 105 | highlight: 0 106 | default: 0 107 | options: 108 | 1: Enabled 109 | 0: Disabled 110 | validate: 111 | type: bool 112 | label: PLUGIN_SQLITE.UPDATE_LOGGING 113 | help: PLUGIN_SQLITE.UPDATE_LOGGING_HELP 114 | -------------------------------------------------------------------------------- /languages.yaml: -------------------------------------------------------------------------------- 1 | en: 2 | PLUGIN_SQLITE: 3 | DATABASE_ERROR: The database file "%s" does not exist. 4 | UNIQUE_FIELD_ERROR:
One (or more) of the fields is required to be UNIQUE, but it already exists in the database.
Is the same data being added again? 5 | OTHER_SQL_ERROR:
The form data is causing a database error. Contact the site developer. 6 | DATABASE_NAME: Name of database 7 | DATABASE_NAME_HELP: The name of the sqlite3 file with extention 8 | DATABASE_ROUTE: Route to database 9 | DATABASE_ROUTE_HELP: The GRAV route to the database (relative to the 'user' directory) 10 | DATABASE_ERROR_TITLE: Database Error 11 | FILE_ERROR_1: The file %s%s%s does not exist. Check that: 12 | FILE_ERROR_2: the database file exists in the correct location, 13 | FILE_ERROR_3: the %slocation%s and %sfilename%s are correct in the sqlite plugin configuration file. 14 | SQL_ERROR_1: SQL Error 15 | SQL_ERROR_2: The SQL statement between the shortcode, viz. 16 | SQL_ERROR_3: generates the error. 17 | UPDATE_WHERE: A where expression is mandatory (either as a parameter or as a form field) when using an "sql-update" Form action. 18 | UPDATE_ERROR: The following UPDATE error was generated:
%s 19 | LOGGING: Turn on Logging. (Not for production!!) 20 | LOGGING_HELP: Log file is appended when an error is detected in DATABASE_ROUTE, or /user/data if former fails. 21 | ALL_LOGGING: Turn on All Logging. 22 | ALL_LOGGING_HELP: Log file is appended when an error is detected in DATABASE_ROUTE, or /user/data if former fails. 23 | ERROR_LOGGING: Turn on Error Logging. 24 | ERROR_LOGGING_HELP: Log file is appended when an error is detected in DATABASE_ROUTE, or /user/data if former fails. 25 | SELECT_LOGGING: Turn on Select Logging. 26 | SELECT_LOGGING_HELP: Log file is appended when a SELECT stanza is sent to database. 27 | INSERT_LOGGING: Turn on Insert Logging. 28 | INSERT_LOGGING_HELP: Log file is appended when an INSERT stanza is sent to database. 29 | UPDATE_LOGGING: Turn on Update Logging. 30 | UPDATE_LOGGING_HELP: Log file is appended when an UPDATE stanza is sent to database. 31 | EXTRA_SECURITY: Enable more paranoid security 32 | EXTRA_SECURITY_HELP: Require explicit permission in header of page where SQL table shortcode is used 33 | -------------------------------------------------------------------------------- /shortcodes/SqlTableShortcode.php: -------------------------------------------------------------------------------- 1 | grav['sqlite']['extraSecurity'] ? 'sqlSEC-table' : 'sql-table'; 14 | $this->shortcode->getHandlers()->add($tagName, function(ShortcodeInterface $sc) { 15 | if ( isset($this->grav['sqlite']['error']) && $this->grav['sqlite']['error'] ) { 16 | $this->log(self::ERROR, $this->grav['sqlite']['error']); 17 | return 18 | $this->twig->processTemplate( 19 | 'partials/sql-db-error.html.twig', 20 | [ 'message' => $this->grav['sqlite']['error'] ] 21 | ); 22 | } 23 | // database exists 24 | $s = $sc->getContent(); 25 | // process any twig variables in the SQL stanza 26 | $s = $this->grav['twig']->processString($s); 27 | $stanza = html_entity_decode(preg_replace('/\<\/?p.*?\>\s*|\n\s*/i',' ',$s)); // remove

embedded by markdown 28 | $this->log(self::SELECT, $stanza); 29 | $params = $sc->getParameters(); 30 | $db = $this->grav['sqlite']['db']; 31 | try { 32 | $query = $db->query($stanza); 33 | if ( ! $query ) throw new \Exception('No sql output from ' . $stanza); 34 | $fields = array(); 35 | $cols = $query->numColumns(); 36 | if ( $cols < 1 ) throw new \Exception('No columns from ' . $stanza); 37 | for ( $i = 0; $i < $cols; $i ++) { 38 | array_push($fields, $query->columnName($i)); 39 | } 40 | $rows = array(); 41 | while ( $row = $query->fetchArray(SQLITE3_ASSOC) ) { 42 | array_push($rows,$row); 43 | } 44 | // first check whether json option is present, if so, ignore other options 45 | if ( array_key_exists( 'json', $params) ) { 46 | $output = $this->twig->processTemplate('partials/sql-json.html.twig', 47 | [ 48 | 'rows' => $rows 49 | ]); 50 | } else { 51 | // find if there are hidden columns 52 | $hidden = array(); 53 | if ( isset( $params['hidden'])) { 54 | $hidden = array_fill_keys(preg_split('/\s+/', $params['hidden'] ), 1); 55 | } 56 | $output = $this->twig->processTemplate('partials/sql-table.html.twig', 57 | [ 58 | 'fields' => $fields, 59 | 'rows' => $rows, 60 | 'hidden' => $hidden, 61 | 'class' => isset( $params['class']) ? $params['class'] : '', 62 | 'id' => isset($params['id']) ? $params['id'] : '' 63 | ] 64 | ); 65 | } 66 | return $output; 67 | } catch( \Exception $e) { 68 | $this->log(self::ERROR, 'message: ' . $e->getMessage() . "\ncontent: $stanza"); 69 | return 70 | $this->twig->processTemplate( 71 | 'partials/sql-sql-error.html.twig', 72 | [ 73 | 'message' => $e->getMessage(), 74 | 'content' => $stanza 75 | ] 76 | ); 77 | } 78 | }); 79 | } 80 | 81 | public function log($type, $msg) { 82 | $log_val =$this->grav['sqlite']['logging']; 83 | if ( $log_val == 0 ) return; 84 | 85 | $path = $this->grav['sqlite']['path'] . DS . 'sqlite.html'; 86 | $datafh = File::instance($path); 87 | if ( ($log_val & self::ERROR) && ($type & self::ERROR) 88 | || ($log_val & self::SELECT) && ($type & self::SELECT) 89 | ) { 90 | if ( file_exists($path) ) { 91 | $datafh->save($datafh->content() . '
' . date('Y-m-d:H:i') . ': ' . $msg); 92 | } else { 93 | $datafh->save('' . date('Y-m-d:H:i') . ': ' . $msg); 94 | chmod($path, 0664); 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /sqlite.php: -------------------------------------------------------------------------------- 1 | ['onPluginsInitialized', 0] 23 | ]; 24 | } 25 | public function onPluginsInitialized() 26 | { 27 | if ($this->isAdmin()) { 28 | $this->active = false; 29 | return; 30 | } 31 | $route = $this->config->get('plugins.sqlite.database_route'); 32 | $dbname = $this->config->get('plugins.sqlite.database_name'); 33 | $path = $this->grav['locator']->findResource("user://$route", true); 34 | // path is also used for error logging, so there must be a valid route in case user supplied route fails. 35 | $this->sqlite['path'] = $path?: $this->grav['locator']->findResource("user://data/sqlite", true); 36 | $this->sqlite['logging'] = $this->config->get('plugins.sqlite.logging') * // is either 0 or 1 37 | ( $this->config->get('plugins.sqlite.all_logging') ? (self::ERROR+self::SELECT+self::INSERT+self::UPDATE) 38 | : ( $this->config->get('plugins.sqlite.error_logging') * self::ERROR 39 | + $this->config->get('plugins.sqlite.select_logging') * self::SELECT 40 | + $this->config->get('plugins.sqlite.insert_logging') * self::INSERT 41 | + $this->config->get('plugins.sqlite.update_logging') * self::UPDATE 42 | ) 43 | ); 44 | $this->sqlite['extraSecurity'] = $this->config->get('plugins.sqlite.extra_security'); 45 | $dbloc = $path . DS . $dbname; 46 | if ( file_exists($dbloc) ) { 47 | $this->sqlite['db'] = new SQLite3($dbloc); 48 | $this->sqlite['db']->enableExceptions(true); 49 | } else { 50 | $this->sqlite['error'] = "No database found at --user://$route/$dbname--"; 51 | } 52 | $this->grav['sqlite'] = $this->sqlite; 53 | $this->enable([ 54 | 'onShortcodeHandlers' => ['onShortcodeHandlers', 0], 55 | 'onTwigTemplatePaths' => ['onTwigTemplatePaths',0], 56 | 'onFormProcessed' => ['onFormProcessed', 0], 57 | 'onPageContentRaw' => ['onPageContentRaw', 0] 58 | ]); 59 | } 60 | 61 | public function onPageContentRaw() { 62 | // Not called if page cached. 63 | // Don't proceed if we are in the admin plugin 64 | if ($this->isAdmin()) { 65 | $this->active = false; 66 | return; 67 | } 68 | if ( ! $this->sqlite['extraSecurity'] ) return; // only continue if extraSecurity is enabled 69 | $page = $this->grav['page']; 70 | // is there explicit permission for this page? 71 | $frontmatter = $page->header(); 72 | if ( property_exists($frontmatter, 'sqliteSelect') AND $frontmatter->sqliteSelect !== 'allow' ) { 73 | return; 74 | } 75 | // extra security is on, so change every occurence of '[sql' to '[sql-sec' 76 | $raw = $page->getRawContent(); 77 | $processed = str_replace( [ '[sql' , '[/sql' ], [ '[sqlSEC' , '[/sqlSEC' ], $raw ); 78 | $page->setRawContent( $processed ); 79 | return; 80 | } 81 | 82 | public function onFormProcessed(Event $event) 83 | { 84 | if ( isset($this->grav['sqlite']['error']) && $this->grav['sqlite']['error'] ) { 85 | $this->log(self::ERROR,$this->grav['sqlite']['error']); 86 | $this->grav->fireEvent('onFormValidationError', new Event([ 87 | 'form' => $event['form'], 88 | 'message' => sprintf($this->grav['language']->translate(['PLUGIN_SQLITE.DATABASE_ERROR']), $this->grav['sqlite']['error']) 89 | ])); 90 | $event->stopPropagation(); 91 | return; 92 | } 93 | $action = $event['action']; 94 | $params = $event['params']; 95 | $form = $event['form']; 96 | switch ($action) { 97 | case 'sql-insert': 98 | $data = $form->value()->toArray(); 99 | if ( isset($params['ignore'])) { 100 | foreach ( $params['ignore'] as $k ) unset( $data[$k] ); 101 | } 102 | $fields = ''; 103 | $values = ''; 104 | $nxt = false; 105 | foreach ( $data as $field => $value ) { 106 | // remove fields associated with Form plugins 107 | if ( preg_match('/^\\_|form\\-nonce/', $field ) ) continue; // next iteration if error (false) in match, or match succeeds. 108 | $fields .= ( $nxt ? ',' : '') . $field; 109 | $values .= ( $nxt ? ',' : '' ) . '"' . $value . '"' ; 110 | $nxt = true; 111 | } 112 | if (isset($data['where'])) { 113 | unset($data['where']); // dont want it polluting UPDATE as a field. Should be ignored 114 | } 115 | $set = 'SET '; 116 | $nxt = false; 117 | foreach ( $data as $field => $value ) { 118 | $set .= ( $nxt ? ', ' : '') ; 119 | $set .= $field . '="' . $value . '"' ; 120 | $nxt = true; 121 | } 122 | $sql ="INSERT INTO {$params['table']} ( $fields ) VALUES ( $values )"; 123 | $this->log(self::INSERT,$sql); 124 | $db = $this->grav['sqlite']['db']; 125 | try { 126 | $db->exec($sql) ; 127 | } catch ( \Exception $e ) { 128 | $msg = $e->getMessage(); 129 | if ( stripos($msg, 'unique') !== false ) { 130 | $msg .= $this->grav['language']->translate(['PLUGIN_SQLITE.UNIQUE_FIELD_ERROR']); 131 | } else { 132 | $msg .= $this->grav['language']->translate(['PLUGIN_SQLITE.OTHER_SQL_ERROR']) . "
$sql"; 133 | } 134 | $this->log(self::ERROR,$msg); 135 | $this->grav->fireEvent('onFormValidationError', new Event([ 136 | 'form' => $event['form'], 137 | 'message' => $msg 138 | ])); 139 | $event->stopPropagation(); 140 | } 141 | break; 142 | case 'sql-update': 143 | $data = $form->value()->toArray(); 144 | if ( isset($params['ignore']) ) { 145 | foreach ( $params['ignore'] as $k ) unset( $data[$k] ); 146 | } 147 | if ( ! isset( $params['where'] ) and ! isset($data['where'])) { 148 | // where expression is mandatory, so fail if not set 149 | $this->log(self::ERROR,$this->grav['language']->translate(['PLUGIN_SQLITE.UPDATE_WHERE'])); 150 | $this->grav->fireEvent('onFormValidationError', new Event([ 151 | 'form' => $event['form'], 152 | 'message' => $this->grav['language']->translate(['PLUGIN_SQLITE.UPDATE_WHERE']) 153 | ])); 154 | $event->stopPropagation(); 155 | break; 156 | } 157 | if (isset($data['where'])) { 158 | // priority to where in form 159 | $where = $data['where']; 160 | unset($data['where']); // dont want it polluting UPDATE as a field 161 | } else { 162 | $where = $params['where']; 163 | } 164 | // allows for use of inpage twig 165 | $where = $this->grav['twig']->processString($where); 166 | $set = 'SET '; 167 | $nxt = false; 168 | foreach ( $data as $field => $value ) { 169 | $set .= ( $nxt ? ', ' : '') ; 170 | $set .= $field . '="' . $value . '"' ; 171 | $nxt = true; 172 | } 173 | $sql ="UPDATE {$params['table']} $set WHERE $where"; 174 | $this->log(self::UPDATE,$sql); 175 | $db = $this->grav['sqlite']['db']; 176 | try { 177 | $db->exec($sql) ; 178 | } catch ( \Exception $e ) { 179 | $this->log(self::ERROR,sprintf($this->grav['language']->translate(['PLUGIN_SQLITE.UPDATE_ERROR']),$e->getMessage())); 180 | $this->grav->fireEvent('onFormValidationError', new Event([ 181 | 'form' => $event['form'], 182 | 'message' => sprintf($this->grav['language']->translate(['PLUGIN_SQLITE.UPDATE_ERROR']),$e->getMessage()) 183 | ])); 184 | $event->stopPropagation(); 185 | } 186 | break; 187 | } 188 | } 189 | 190 | public function onTwigTemplatePaths() 191 | { 192 | $this->grav['twig']->twig_paths[] = __DIR__ . '/templates'; 193 | } 194 | 195 | /** 196 | * Initialize configuration 197 | * @param Event $e 198 | */ 199 | public function onShortcodeHandlers(Event $e) 200 | { 201 | $this->grav['shortcode']->registerAllShortcodes(__DIR__.'/shortcodes'); 202 | } 203 | 204 | public function log($type, $msg) { 205 | $log_val =$this->grav['sqlite']['logging']; 206 | if ( $log_val == 0 ) return; 207 | 208 | $path = $this->grav['sqlite']['path'] . DS . 'sqlite.html'; 209 | $datafh = File::instance($path); 210 | if ( ($log_val & self::ERROR) && ($type & self::ERROR) 211 | || ($log_val & self::INSERT) && ($type & self::INSERT) 212 | || ($log_val & self::UPDATE) && ($type & self::UPDATE) 213 | ) { 214 | if ( file_exists($path) ) { 215 | $datafh->save($datafh->content() . '
' . date('Y-m-d:H:i') . ': ' . $msg); 216 | } else { 217 | $datafh->save('' . date('Y-m-d:H:i') . ': ' . $msg); 218 | chmod($path, 0664); 219 | } 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /sqlite.yaml: -------------------------------------------------------------------------------- 1 | enabled: true 2 | database_route: data/sqlite 3 | database_name: db.sqlite3 4 | extra_security: false 5 | logging: false 6 | error_logging: false 7 | select_logging: false 8 | insert_logging: false 9 | update_logging: false 10 | -------------------------------------------------------------------------------- /templates/partials/sql-db-error.html.twig: -------------------------------------------------------------------------------- 1 |

2 | {{ "PLUGIN_SQLITE.DATABASE_ERROR_TITLE"|t }} 3 |

{{ "PLUGIN_SQLITE.FILE_ERROR_1"|t('', message, '') }} 4 |

8 |

9 |
10 | -------------------------------------------------------------------------------- /templates/partials/sql-json.html.twig: -------------------------------------------------------------------------------- 1 | [ 2 | {% for row in rows %} 3 | { 4 | {% for col,item in row %} 5 | "{{col}}": {% if item matches '/^\s*[-+]?[0-9]+\\.?[0-9]+\s*$/' %} {{ item }} {% else %}"{{ item }}"{% endif %}{{ not loop.last ? ',' }} 6 | {% endfor %} 7 | }{{ not loop.last ? ',' }} 8 | {% endfor %} 9 | ] 10 | -------------------------------------------------------------------------------- /templates/partials/sql-sql-error.html.twig: -------------------------------------------------------------------------------- 1 |
2 | {{"PLUGIN_SQLITE.SQL_ERROR_1"|t}} 3 |

{{"PLUGIN_SQLITE.SQL_ERROR_2"|t}}

4 |

{{ content }}

5 |

{{"PLUGIN_SQLITE.SQL_ERROR_3"|t}}

6 |

{{ message }}

7 |
8 | -------------------------------------------------------------------------------- /templates/partials/sql-table.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% for item in fields %} 5 | {{ item }} 6 | {% endfor %} 7 | 8 | 9 | {% for row in rows %} 10 | {% for col,item in row %} 11 | {{ item }} 12 | {% endfor %} 13 | {% endfor %} 14 |
15 | --------------------------------------------------------------------------------