From f50766874ef9ef23c37ad772fd36509c9a826cf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Anne?= Date: Fri, 21 Apr 2023 16:58:24 +0200 Subject: [PATCH 01/17] Add upgrade guidelines for GLPI 10.1 --- source/index.rst | 1 + source/upgradeguides/glpi-10.1.rst | 55 ++++++++++++++++++++++++++++++ source/upgradeguides/index.rst | 12 +++++++ 3 files changed, 68 insertions(+) create mode 100644 source/upgradeguides/glpi-10.1.rst create mode 100644 source/upgradeguides/index.rst diff --git a/source/index.rst b/source/index.rst index 30bacca..33a8e48 100644 --- a/source/index.rst +++ b/source/index.rst @@ -19,6 +19,7 @@ GLPI Developer Documentation checklists/index plugins/index packaging + upgradeguides If you want to help us improve the current documentation, feel free to open pull requests! You can `see open issues `_ and `join the documentation mailing list `_. diff --git a/source/upgradeguides/glpi-10.1.rst b/source/upgradeguides/glpi-10.1.rst new file mode 100644 index 0000000..7ddabd8 --- /dev/null +++ b/source/upgradeguides/glpi-10.1.rst @@ -0,0 +1,55 @@ +Upgrade to GLPI 10.1 +-------------------- + +Removal of input variables auto-sanitize +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Prior to GLPI 10.1, PHP superglobals ``$_GET``, ``$_POST`` and ``$_REQUEST`` were automatically sanitized. +It means that SQL special characters were escaped (prefixed by a ``\``), and HTML special characters ``<``, ``>`` and ``&`` were encoded into HTML entities. +This caused issues because it was difficult, for some pieces of code, to know if the received variables were "secure" or not. + +In GLPI 10.1, we removed this auto-sanitization, and any data, whether it comes from a form, the database, or the API, will always be in its raw state. + +Protection against SQL injection +++++++++++++++++++++++++++++++++ + +Protection against SQL injection is now automatically done when DB query is crafted. + +All the ``addslashes()`` usages that were used for this purpose have to be removed from your code. + +.. code-block:: diff + + - $item->add(Toolbox::addslashes_deep($properties)); + + $item->add($properties); + +Protection against XSS +++++++++++++++++++++++ + +HTML special characters are no longer encoded automatically. Even existing data will be seamlessly decoded when it will be fetched from database. +Code must be updated to ensure that all dynamic variables are correctly escaped in HTML views. + +Views built with ``Twig`` templates no longer require usage of the ``|verbatim_value`` filter to correctly display HTML special characters. +Also, Twig automatically escapes special characters, which protects against XSS. + +.. code-block:: diff + + -

{{ content|verbatim_value }}

+ +

{{ content }}

+ +Code that outputs HTML code directly must be adapted to use the ``htmlspecialchars()`` function. + +.. code-block:: diff + + - echo '

' . $content . '

'; + + echo '

' . htmlspecialchars($content) . '

'; + +Also, code that ouputs javascript must be adapted to prevent XSS with both HTML special characters and quotes. + +.. code-block:: diff + + echo ' + + '; diff --git a/source/upgradeguides/index.rst b/source/upgradeguides/index.rst new file mode 100644 index 0000000..e5dad36 --- /dev/null +++ b/source/upgradeguides/index.rst @@ -0,0 +1,12 @@ +Upgrade guides +============== + +The upgrade guides are intended to help you adapt your plugins to the changes introduced in the new versions of GLPI. + +.. note:: + + Only the most important changes and those requiring support are documented here. + If you are having trouble migrating your code, feel free to suggest a documentation update. + +.. toctree:: + :maxdepth: 2 From f24fbf8a70578b4ef98da6c954f13324e364ad64 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Thu, 14 Sep 2023 09:43:50 +0200 Subject: [PATCH 02/17] Add required readthedocs configuration file --- .readthedocs.yaml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..eeb06b8 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,27 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: source/conf.py + +formats: + - htmlzip + - pdf + - epub + +# We recommend specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: requirements.txt From 646631d1c69a1602679c1e5b0693bb7dac17eebf Mon Sep 17 00:00:00 2001 From: Curtis Conard Date: Mon, 22 Jan 2024 10:28:23 -0500 Subject: [PATCH 03/17] High-Level API Internals Documentation (#149) * start documenting HL API * remove x-default-contexts --- source/devapi/hlapi/index.rst | 13 ++ source/devapi/hlapi/schemas.rst | 250 ++++++++++++++++++++++++++++++++ source/devapi/hlapi/search.rst | 34 +++++ 3 files changed, 297 insertions(+) create mode 100644 source/devapi/hlapi/index.rst create mode 100644 source/devapi/hlapi/schemas.rst create mode 100644 source/devapi/hlapi/search.rst diff --git a/source/devapi/hlapi/index.rst b/source/devapi/hlapi/index.rst new file mode 100644 index 0000000..1b30ae1 --- /dev/null +++ b/source/devapi/hlapi/index.rst @@ -0,0 +1,13 @@ +High-Level API +============== + +The High-Level API (HL API) is a new API system provided in GLPI starting with version 10.1.0. +While the user experience is more simplified than the legacy API (the REST API available in previous versions), the implementation is quite a bit more complex. +The following sections explain the various components of the new API. +These sections are sorted by the recommended reading order. It is recommended that you read the High-Level API user documentation first if you have no experience with the API at all. + +.. toctree:: + :maxdepth: 2 + + schemas + search diff --git a/source/devapi/hlapi/schemas.rst b/source/devapi/hlapi/schemas.rst new file mode 100644 index 0000000..ad7b6d6 --- /dev/null +++ b/source/devapi/hlapi/schemas.rst @@ -0,0 +1,250 @@ +Schemas +======= + +Schemas are the definitions of the various item types in GLPI, or facades, for how they are exposed to the API. +In the legacy API, all classes that extend ``CommonDBTM`` were exposed along with all of their search options. +This is not the case with the High-Level API. + +Schema Format +^^^^^^^^^^^^^ + +The schemas loosely follow the `OpenAPI 3 specification `_ to make it easier to implement the Swagger UI documentation tool. +GLPI utilizes multiple custom extension fields (fields starting with 'x-') in schemas to enable advanced behavior. +Schemas are defined in an array with their name as the key and definition as the value. + +There exists the ``\Glpi\API\HL\Doc\Schema`` class which is used to represent a schema definition in some cases, but also provides constants and static methods for working with schema arrays. +This includes constants for the supported property types and formats. + +Let's look at a partial version of the schema definition for a User since it shows most of the possibilities: + +.. code-block:: php + + 'User' => [ + 'x-itemtype' => User::class, + 'type' => Doc\Schema::TYPE_OBJECT, + 'x-rights-conditions' => [ // Object-level extra permissions + 'read' => static function () { + if (!\Session::canViewAllEntities()) { + return [ + 'LEFT JOIN' => [ + 'glpi_profiles_users' => [ + 'ON' => [ + 'glpi_profiles_users' => 'users_id', + 'glpi_users' => 'id' + ] + ] + ], + 'WHERE' => [ + 'glpi_profiles_users.entities_id' => $_SESSION['glpiactiveentities'] + ] + ]; + } + return true; + } + ], + 'properties' => [ + 'id' => [ + 'type' => Doc\Schema::TYPE_INTEGER, + 'format' => Doc\Schema::FORMAT_INTEGER_INT64, + 'description' => 'ID', + 'x-readonly' => true, + ], + 'username' => [ + 'x-field' => 'name', + 'type' => Doc\Schema::TYPE_STRING, + 'description' => 'Username', + ], + 'realname' => [ + 'type' => Doc\Schema::TYPE_STRING, + 'description' => 'Real name', + ], + 'emails' => [ + 'type' => Doc\Schema::TYPE_ARRAY, + 'description' => 'Email addresses', + 'items' => [ + 'type' => Doc\Schema::TYPE_OBJECT, + 'x-full-schema' => 'EmailAddress', + 'x-join' => [ + 'table' => 'glpi_useremails', + 'fkey' => 'id', + 'field' => 'users_id', + 'x-primary-property' => 'id' // Help the search engine understand the 'id' property is this object's primary key since the fkey and field params are reversed for this join. + ], + 'properties' => [ + 'id' => [ + 'type' => Doc\Schema::TYPE_INTEGER, + 'format' => Doc\Schema::FORMAT_INTEGER_INT64, + 'description' => 'ID', + ], + 'email' => [ + 'type' => Doc\Schema::TYPE_STRING, + 'description' => 'Email address', + ], + 'is_default' => [ + 'type' => Doc\Schema::TYPE_BOOLEAN, + 'description' => 'Is default', + ], + 'is_dynamic' => [ + 'type' => Doc\Schema::TYPE_BOOLEAN, + 'description' => 'Is dynamic', + ], + ] + ] + ], + 'password' => [ + 'type' => Doc\Schema::TYPE_STRING, + 'format' => Doc\Schema::FORMAT_STRING_PASSWORD, + 'description' => 'Password', + 'x-writeonly' => true, + ], + 'password2' => [ + 'type' => Doc\Schema::TYPE_STRING, + 'format' => Doc\Schema::FORMAT_STRING_PASSWORD, + 'description' => 'Password confirmation', + 'x-writeonly' => true, + ], + 'picture' => [ + 'type' => Doc\Schema::TYPE_STRING, + 'x-mapped-from' => 'picture', + 'x-mapper' => static function ($v) { + global $CFG_GLPI; + $path = \Toolbox::getPictureUrl($v, false); + if (!empty($path)) { + return $path; + } + return $CFG_GLPI["root_doc"] . '/pics/picture.png'; + } + ] + ] + ] + +The first property in the definition, 'x-itemtype' is used to link the schema with an actual GLPI class. +This is used to determine which table to use to access direct properties and access more data like entity restrictions and extra visiblity restrictions (when implementing the ``ExtraVisibilityCriteria`` class). +This property is required. + +Next, is a 'type' property which is part of the standard OpenAPI specification. In this case, it defines a User as an object. In general, all schemas would be objects. + +Third, is an 'x-rights-conditions' property which defines special visiblity restrictions. This property may be excluded if there are no special restrictions. +Currently, only 'read' restrictions can be defined here. +Each type of restriction must be a callable that returns an array of criteria, or just an array of criteria, in the format used by ``DBmysqlIterator``. +If the criteria is reliant on data from a session or is expensive, it should use a callable so that the criteria is resolved only at the time it is needed. + +Finally, the 'properties' are defined. +Each property has its unique name as the key and the definition as the value in the array. +Property names do not have to match the name of the column in the database. You can specify a different column name using an 'x-field' field; +Each property must have an OpenAPI 'type' defined. They may optionally define a specific 'format'. If no 'format' is specified, the generic format for that type will be used. +For example, a type of ``Doc\Schema::TYPE_STRING`` will default to the ``Doc\Schema::FORMAT_STRING_STRING`` format. +Properties may also optionally define a description for that property. + +In this example, the 'emails' property actually refers to multiple email addresses associated with the user. +The 'type' in this case is ``Doc\Schema::TYPE_ARRAY``. The schema for the individual items in defined inside the 'items' property. +Of course, email addresses are not stored in the same database table as users and are their own item type ``EmailAddress``. +Therefore, 'emails' is considered a joined object property. +In joined objects, we specify which properties will be included in the data but that can be a subset of the properties of the full schema (see :ref:`Partial vs Full Schema `). +The full schema can be specified using the 'x-full-schema' field. +The criteria for the join is specified in the 'x-join' field (more on that in the :ref:`Joins section `). + +Users have two password fields which we would never want to show via the API, but we do want them to exist in the schema to allow setting/resetting a password. +In this case, both 'password' and 'password2' have a 'x-writeonly' field present and set to true. + +The last property shown, 'picture', is an example of a mapped property. +In some cases, the data we want the user to see will differ from the raw value in the database. +In this example, pictures are stored as the path relative to the pictures folder such as '16/2_649182f5c5216.jpg'. +To a user of the API, this is useless. However, we can use that data to convert it to the front-end URL needed to access that picture such as '/front/document.send.php?file=_pictures/16/2_649182f5c5216.jpg'. +To accomplish this, mapped properties have the 'x-mapped-from' and 'x-mapper' fields. +'x-mapped-from' indicates the property we are mapping from. In this case, it maps from itself. +'x-mapper' is a callable that transforms the raw value to the display value. +The mapper used here takes the relative path and converts it to the front-end URL. It then handles returning the default user picture if it cannot get the user's specific picture. + +.. _partial_full_schema: + +Partial vs Full Schema +^^^^^^^^^^^^^^^^^^^^^^ + +A full schema is the defacto representation of an item in the API. +In some cases, we do not want every property for an item to be visible such as dropdown types related to a main item. +In ``Computer`` item we may show the ID and name of the computer's location, but the Location type itself has additional data like geolocation coordinates. +The partial schema contains only the properties needed for the user to know where to look for the full details and some basic information about it. + +.. _joins: + +Joins +^^^^^ + +Schemas may include data from tables other than the table for the main item. +Most of the item, joins are used in 'object' type properties such as when bringing in an ID and name for a dropdown type. +In some cases though, joins may be defined on scalar properties (not array or object). + +The information required to join data from outside of the main item's table is defined inside of an 'x-join' array. +The supported properties of the 'x-join' definition are: +- table: The database table to pull the data from +- fkey: The SQL field in the main table to use to identify which records in the other table are related +- field: The SQL field in the other table to match against the fkey. +- primary-property: Optional property which indicates the primary property of the foreign data. Typically, this is the 'id' field. + By default, the API will assume the field specified in 'field' is the primary property. If it isn't, it is required to specify it here. + In the User schema example, email addresses have a many-to-one relation with users. So, we use the user's ID field and match it against the 'users_id' field of the email addresses. + In that case, the 'field' is 'users_id' but the primary property is 'id', so we need to hint to the API that 'id' is still the primary property. +- ref-join: In some cases, there is no direct connection between the main item's table and the table with the data desired (typically seen with many-to-many relations). + In that case, a reference or in-between join can be specified. The 'ref_join' property follows the same format as 'x-join' except that you cannot have another 'ref_join'. + +Extension Properties +^^^^^^^^^^^^^^^^^^^^ + +Below is a complete list of supported extension fields/properties used in OpenAPI schemas. + +.. list-table:: Extension Properties + :widths: 25 50 25 25 + :header-rows: 1 + + * - Property + - Description + - Applicable Locations + - Visible in Swagger UI + * - x-controller + - Set and used internally by the OpenAPI documentation generator to track which controller defined the schema. + - Main schema + - Debug mode only + * - x-field + - Specifies the column that contains the data for the property if it differs from the property name. + - Schema properties + - Debug mode only + * - x-full-schema + - Indicates which schema is the full representation of the joined property. + This enables the accessing of properties not in the partial schema in certain conditions such as a GraphQL query. + - Schema join properties + - Yes + * - x-itemtype + - Specifies the PHP class related to the schema. + - Main schema + - Debug mode only + * - x-join + - Join definition. See Joins section for more information. + - Schema join properties + - Debug mode only + * - x-mapped-from + - Indicates the property to use with an 'x-mapper' to modify a value before returning it in an API response. + - Schema properties + - Debug mode only + * - x-mapper + - A callable that transforms the raw value specified by 'x-mapped-from' to the display value. + - Schema properties + - Debug mode only + * - x-readonly + - Specifies the property can only be read and not written to. + - Schema properties + - Yes + * - x-rights-conditions + - Array of arrays or callables that returns an array of SQL criteria for special visibility restrictions. Only 'read' restrictions are currently supported. + The type of restriction should be specified as the array key, and the callable or array as the value. + - Schema properties + - Debug mode only + * - x-subtypes + - Indicates array of arrays containing 'schema_name' and 'itemtype' properties. + This is used for unique cases where you want to allow searching across multiple schemas at once such as "All assets". + Typically you would find all shared properties between the different schemas and use that as the properties for this shared schema. + - Main schema + - Debug mode only + * - x-writeonly + - Specifies the property can only be written to and not read. + - Schema properties + - Yes \ No newline at end of file diff --git a/source/devapi/hlapi/search.rst b/source/devapi/hlapi/search.rst new file mode 100644 index 0000000..64f00da --- /dev/null +++ b/source/devapi/hlapi/search.rst @@ -0,0 +1,34 @@ +Search +====== + +As the High-Level API is decoupled from the PHP classes and search options system, a new search engine was developed to handle interacting with the database. +This new search engine exists in the ``\Glpi\Api\HL\Search`` class. +For simplicity, the search engine class also provides static methods to perform item creation, update and deletion in addition to the search/get actions. + +These entrypoint methods are: + +- getOneBySchema +- searchBySchema +- createBySchema +- updateBySchema +- deleteBySchema + +See the PHPDoc for each method for more information. + +While the standard search engine constructs a single database query to retreive item(s), the High-Level API takes multiple distinct steps and multiple queries to fetch and assemble the data given the potential complexity of schemas while keeping the schemas themselves relatively simple. + +The steps are: + +1. Initializing a new search. + This step consists of making a new instance of the ``\Glpi\Api\HL\Search`` class, generating a flattened array of properties (flattens properties where the keys are the full property name in dot notation to make lookups easier) in the schema and identifying joins. +2. Construct a request to get the 'dehydrated' result. + In this context, that means a result without all of the desired data. It only contains the identification data (the main item ID(s) and the IDs of joined records) and the scalar join values. + Each dehydrated result is an array where the keys are the primary ID field and any full join property name. The '.' in the names are replaced with 0x1F characters (Unit separator character) to avoid confusion about what is a table/field identifier. + In the case that a join property is for an array of items, the IDs are separated by a 0x1D character (Group separator character). + If there are no results for a specific join, a null byte character will be used. + The reason a dehydrated result is fetched first is that we don't need to either worry about grouping data or handling the multiple rows returned that relate to a single main item. +3. Hydrate each of the dehydrated results. In separate queries, the search engine will fetch the data for the main item and each join. + Each time a new record is fetched, it is stored in a separate array that acts like a cache to avoid fetching the same record twice. +4. Assemble the hydrated records into the final result(s). The search engine enumerates each property in the dehydrated result starting with the main item's ID and maps the hydrated data into a result that matches the expected schema. +5. Fixup the assembled records. Some post-processing is done after the record is fully assembled to clean some of the artifacts from the assembly process such as removing the keys for array type properties and replacing empty array values for object type properties with null. +6. Returning the result(s). \ No newline at end of file From 81d8b9edb9b430b3b8b135ff3150678e7402abeb Mon Sep 17 00:00:00 2001 From: Curtis Conard Date: Tue, 22 Aug 2023 11:05:11 -0400 Subject: [PATCH 04/17] basic vue docs --- source/devapi/index.rst | 1 + source/devapi/javascript.rst | 53 ++++++++++++++++ source/plugins/index.rst | 1 + source/plugins/javascript.rst | 110 ++++++++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+) create mode 100644 source/devapi/javascript.rst create mode 100644 source/plugins/javascript.rst diff --git a/source/devapi/index.rst b/source/devapi/index.rst index aa6ab37..e8d1929 100644 --- a/source/devapi/index.rst +++ b/source/devapi/index.rst @@ -15,4 +15,5 @@ Apart from the current documentation, you can also generate the full PHP documen acl crontasks tools + javascript extra diff --git a/source/devapi/javascript.rst b/source/devapi/javascript.rst new file mode 100644 index 0000000..69aeff0 --- /dev/null +++ b/source/devapi/javascript.rst @@ -0,0 +1,53 @@ +Javascript +========== + +Vue.js +------ + +Starting in GLPI 10.1, we have added support for Vue. +.. note:: + + Only SFCs (Single-file Components) using the Components API is supported. Do not use the Options API. + +To ease integration, there is no Vue app mounted on the page body itself. Instead, each specific feature that uses Vue such as the debug toolbar mounts its own Vue app on a container element. +Components must all be located in the ``js/src/vue`` folder for them to be built. +Components should be grouped into subfolders as a sort of namespace separation. +There are some helpers stored in the ``window.Vue`` global to help manage components and mount apps. + +### Building + +Two npm commands exist which can be used to build or watch (auto-build when the sources change) the Vue components. + +.. code-block:: bash + + npm run build:vue + +.. code-block:: bash + + npm run watch:vue + + +The ``npm run build`` command will also build the Vue components in addition to the regular JS bundles. + +To improve performance, the components are not built into a single file. Instead, webpack chunking is utilized. +This results a single smaller entrypoint ``app.js`` being generated and a separate file for each component. +The components that are automatically built utilize ``defineAsyncComponent`` to enable the loading of those components on demand. + +Further optimizations can be done by directly including a Vue component inside a main component to ensure it is built into the main component's chunk to reduce the number of requests. +This could be useful if the component wouldn't be reused elsewhere. Just note that the child component would also have its own chunk generated since there is no way to exclude it. + +### Mounting + +The Vue `createApp` function can be located at `window.Vue.createApp`. +Each automatically built component is automatically tracked in `window.Vue.components`. + +To create an app and mount a component, you can use the following code: + +.. code-block:: javascript + + const app = window.Vue.createApp(window.Vue.components['Debug/Toolbar'].component); + app.mount('#my-app-wrapper'); + +Replace ``Debug/Toolbar`` with the relative path to your component without the ``.vue`` extension and ``#my-app-wrapper`` with an ID selector for the wrapper element which would need to already existing in the DOM. + +For more information about Vue, please refer to the `official documentation `_. \ No newline at end of file diff --git a/source/plugins/index.rst b/source/plugins/index.rst index 4502662..b115ad6 100644 --- a/source/plugins/index.rst +++ b/source/plugins/index.rst @@ -26,3 +26,4 @@ If you want to see more advanced examples of what it is possible to do with plug tips notifications test + javascript diff --git a/source/plugins/javascript.rst b/source/plugins/javascript.rst new file mode 100644 index 0000000..5a8c3db --- /dev/null +++ b/source/plugins/javascript.rst @@ -0,0 +1,110 @@ +Javascript +========== + +Vue.js +------ + +Please refer to :doc:`the core Vue developer documentation first <../devapi/javascript>`. + +Plugins that wish to use custom Vue components must implement their own webpack config to build the components and add them to the `window.Vue.components` object. + +Sample webpack config (derived from the config used in GLPI itself for Vue): +.. code-block:: javascript + + const webpack = require('webpack'); + const path = require('path'); + const VueLoaderPlugin = require('vue-loader').VueLoaderPlugin; + + const config = { + entry: { + 'vue': './js/src/vue/app.js', + }, + externals: { + // prevent duplicate import of Vue library (already done in ../../public/build/vue/app.js) + vue: 'import vue', + }, + output: { + filename: 'app.js', + chunkFilename: "[name].js", + chunkFormat: 'module', + path: path.resolve(__dirname, 'public/build/vue'), + publicPath: '/public/build/vue/', + asyncChunks: true, + clean: true, + }, + module: { + rules: [ + { + // Vue SFC + test: /\.vue$/, + loader: 'vue-loader' + }, + { + // Build styles + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }, + ] + }, + plugins: [ + new VueLoaderPlugin(), // Vue SFC support + new webpack.ProvidePlugin( + { + process: 'process/browser' + } + ), + new webpack.DefinePlugin({ + __VUE_OPTIONS_API__: false, // We will only use composition API + __VUE_PROD_DEVTOOLS__: false, + }), + ], + resolve: { + fallback: { + 'process/browser': require.resolve('process/browser.js') + }, + }, + mode: 'none', // Force 'none' mode, as optimizations will be done on release process + devtool: 'source-map', // Add sourcemap to files + stats: { + // Limit verbosity to only usefull information + all: false, + errors: true, + errorDetails: true, + warnings: true, + + entrypoints: true, + timings: true, + }, + target: "es2020" + }; + + module.exports = config + +Note the use of the ``externals`` option. This will prevent webpack from including Vue itself when building your components since it is already imported by the bundle in GLPI itself. +This will drastically reduce the size of your imports. + +For your entrypoint, it is mostly the same as the core GLPi one except you should use the ``defineAsyncComponent`` method in ``window.Vue`` instead of importing it from Vue itself. + +Example entrypoint: + +.. code-block:: javascript + + // Require all Vue SFCs in js/src directory + const component_context = import.meta.webpackContext('.', { + regExp: /\.vue$/i, + recursive: true, + mode: 'lazy', + chunkName: '/vue-sfc/[request]' + }); + const components = {}; + component_context.keys().forEach((f) => { + const component_name = f.replace(/^\.\/(.+)\.vue$/, '$1'); + components[component_name] = { + component: window.Vue.defineAsyncComponent(() => component_context(f)), + }; + }); + // Save components in global scope + window.Vue.components = Object.assign(window.Vue.components || {}, components); + +To keep your components from colliding with core components or other plugins, it you should organize them inside the `js/src/Plugin/Yourplugin` folder. +This will ensure plugin components are registered as ``Plugin/Yourplugin/YourComponent``. You can organize components further with additional subfolders. \ No newline at end of file From 8dbee57f20c1420b816ba5bdd04b817d8b6cbd22 Mon Sep 17 00:00:00 2001 From: Curtis Conard Date: Tue, 22 Aug 2023 15:08:39 -0400 Subject: [PATCH 05/17] update external requirement --- source/plugins/javascript.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/plugins/javascript.rst b/source/plugins/javascript.rst index 5a8c3db..3bf7a25 100644 --- a/source/plugins/javascript.rst +++ b/source/plugins/javascript.rst @@ -21,7 +21,7 @@ Sample webpack config (derived from the config used in GLPI itself for Vue): }, externals: { // prevent duplicate import of Vue library (already done in ../../public/build/vue/app.js) - vue: 'import vue', + vue: 'window _vue', }, output: { filename: 'app.js', @@ -81,9 +81,10 @@ Sample webpack config (derived from the config used in GLPI itself for Vue): module.exports = config Note the use of the ``externals`` option. This will prevent webpack from including Vue itself when building your components since it is already imported by the bundle in GLPI itself. +The core GLPI bundle sets ``window._vue`` to the vue module and the plugin's externals option will map any imports from 'vue' to that. This will drastically reduce the size of your imports. -For your entrypoint, it is mostly the same as the core GLPi one except you should use the ``defineAsyncComponent`` method in ``window.Vue`` instead of importing it from Vue itself. +For your entrypoint, it is mostly the same as the core GLPI one except you should use the ``defineAsyncComponent`` method in ``window.Vue`` instead of importing it from Vue itself. Example entrypoint: From 0b0df275f9a5d1b2b687bbbeebb5d432673acbb6 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Mon, 15 Apr 2024 16:17:15 +0200 Subject: [PATCH 06/17] Missing in ToC --- source/devapi/index.rst | 1 + source/index.rst | 2 +- source/upgradeguides/index.rst | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/source/devapi/index.rst b/source/devapi/index.rst index e8d1929..6e4c851 100644 --- a/source/devapi/index.rst +++ b/source/devapi/index.rst @@ -9,6 +9,7 @@ Apart from the current documentation, you can also generate the full PHP documen mainobjects database/index search + hlapi/index massiveactions rules translations diff --git a/source/index.rst b/source/index.rst index 33a8e48..61f10f1 100644 --- a/source/index.rst +++ b/source/index.rst @@ -19,7 +19,7 @@ GLPI Developer Documentation checklists/index plugins/index packaging - upgradeguides + upgradeguides/index If you want to help us improve the current documentation, feel free to open pull requests! You can `see open issues `_ and `join the documentation mailing list `_. diff --git a/source/upgradeguides/index.rst b/source/upgradeguides/index.rst index e5dad36..5d5c526 100644 --- a/source/upgradeguides/index.rst +++ b/source/upgradeguides/index.rst @@ -10,3 +10,5 @@ The upgrade guides are intended to help you adapt your plugins to the changes in .. toctree:: :maxdepth: 2 + + glpi-10.1 From 67f0370196645b56d25061fd4254bc3d884edc0d Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Mon, 15 Apr 2024 16:18:53 +0200 Subject: [PATCH 07/17] Fix syntax --- source/devapi/hlapi/schemas.rst | 13 +++++++------ source/plugins/javascript.rst | 3 ++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/source/devapi/hlapi/schemas.rst b/source/devapi/hlapi/schemas.rst index ad7b6d6..3c4f333 100644 --- a/source/devapi/hlapi/schemas.rst +++ b/source/devapi/hlapi/schemas.rst @@ -177,14 +177,15 @@ In some cases though, joins may be defined on scalar properties (not array or ob The information required to join data from outside of the main item's table is defined inside of an 'x-join' array. The supported properties of the 'x-join' definition are: -- table: The database table to pull the data from -- fkey: The SQL field in the main table to use to identify which records in the other table are related -- field: The SQL field in the other table to match against the fkey. -- primary-property: Optional property which indicates the primary property of the foreign data. Typically, this is the 'id' field. + +* table: The database table to pull the data from +* fkey: The SQL field in the main table to use to identify which records in the other table are related +* field: The SQL field in the other table to match against the fkey. +* primary-property: Optional property which indicates the primary property of the foreign data. Typically, this is the 'id' field. By default, the API will assume the field specified in 'field' is the primary property. If it isn't, it is required to specify it here. In the User schema example, email addresses have a many-to-one relation with users. So, we use the user's ID field and match it against the 'users_id' field of the email addresses. In that case, the 'field' is 'users_id' but the primary property is 'id', so we need to hint to the API that 'id' is still the primary property. -- ref-join: In some cases, there is no direct connection between the main item's table and the table with the data desired (typically seen with many-to-many relations). +* ref-join: In some cases, there is no direct connection between the main item's table and the table with the data desired (typically seen with many-to-many relations). In that case, a reference or in-between join can be specified. The 'ref_join' property follows the same format as 'x-join' except that you cannot have another 'ref_join'. Extension Properties @@ -247,4 +248,4 @@ Below is a complete list of supported extension fields/properties used in OpenAP * - x-writeonly - Specifies the property can only be written to and not read. - Schema properties - - Yes \ No newline at end of file + - Yes diff --git a/source/plugins/javascript.rst b/source/plugins/javascript.rst index 3bf7a25..ee6c78f 100644 --- a/source/plugins/javascript.rst +++ b/source/plugins/javascript.rst @@ -9,6 +9,7 @@ Please refer to :doc:`the core Vue developer documentation first <../devapi/java Plugins that wish to use custom Vue components must implement their own webpack config to build the components and add them to the `window.Vue.components` object. Sample webpack config (derived from the config used in GLPI itself for Vue): + .. code-block:: javascript const webpack = require('webpack'); @@ -108,4 +109,4 @@ Example entrypoint: window.Vue.components = Object.assign(window.Vue.components || {}, components); To keep your components from colliding with core components or other plugins, it you should organize them inside the `js/src/Plugin/Yourplugin` folder. -This will ensure plugin components are registered as ``Plugin/Yourplugin/YourComponent``. You can organize components further with additional subfolders. \ No newline at end of file +This will ensure plugin components are registered as ``Plugin/Yourplugin/YourComponent``. You can organize components further with additional subfolders. From 0c343df96ddae4062cc67f020d5c42e5115749a7 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Tue, 16 Apr 2024 13:43:41 +0200 Subject: [PATCH 08/17] Iterator syntax (among other) * Array syntax is now enforced * A word about criteria unicity * Warning about raw queries --- source/devapi/database/dbiterator.rst | 227 +++++++++++++++----------- source/upgradeguides/glpi-10.1.rst | 20 +++ 2 files changed, 153 insertions(+), 94 deletions(-) diff --git a/source/devapi/database/dbiterator.rst b/source/devapi/database/dbiterator.rst index 787c8cf..21febf3 100644 --- a/source/devapi/database/dbiterator.rst +++ b/source/devapi/database/dbiterator.rst @@ -5,6 +5,7 @@ GLPI framework provides a simple request generator: * without having to write SQL * without having to quote table and field name +* without having to escape values to prevent SQL injections * without having to take care of freeing resources * iterable * countable @@ -32,32 +33,12 @@ Basic usage Arguments ^^^^^^^^^ -The ``request`` method takes two arguments: +The ``request`` method takes as argument an array of criteria with explicit SQL clauses (``FROM``, ``WHERE`` and so on) -* `table name(s)`: a `string` or an `array` of `string` - (optional when given as ``FROM`` option) -* `option(s)`: `array` of options - - -Giving full SQL statement -^^^^^^^^^^^^^^^^^^^^^^^^^ - -If the only option is a full SQL statement, it will be used. -This usage is deprecated, and should be avoid when possible. - -.. note:: - - To make a database query that could not be done using recommanded way (calling SQL functions such as ``NOW()``, ``ADD_DATE()``, ... for example), you can do: - - .. code-block:: php - - request('SELECT id FROM glpi_users WHERE end_date > NOW()'); - -Without option -^^^^^^^^^^^^^^ +FROM clause +^^^^^^^^^^^ -In this case, all the data from the selected table is iterated: +The SQL ``FROM`` clause can be a string or an array of strings: .. code-block:: php @@ -65,35 +46,23 @@ In this case, all the data from the selected table is iterated: $DB->request(['FROM' => 'glpi_computers']); // => SELECT * FROM `glpi_computers` - $DB->request('glpi_computers'); - // => SELECT * FROM `glpi_computers` + $DB->request(['FROM' => ['glpi_computers', 'glpi_monitors']); + // => SELECT * FROM `glpi_computers`, `glpi_monitors` Fields selection ^^^^^^^^^^^^^^^^ You can use either the ``SELECT`` or ``FIELDS`` options, an additional ``DISTINCT`` option might be specified. -.. note:: - - .. versionchanged:: 9.5.0 - - Using ``DISTINCT FIELDS`` or ``SELECT DISTINCT`` options is deprecated. - .. code-block:: php request(['SELECT' => 'id', 'FROM' => 'glpi_computers']); // => SELECT `id` FROM `glpi_computers` - $DB->request('glpi_computers', ['FIELDS' => 'id']); - // => SELECT `id` FROM `glpi_computers` - $DB->request(['SELECT' => 'name', 'DISTINCT' => true, 'FROM' => 'glpi_computers']); // => SELECT DISTINCT `name` FROM `glpi_computers` - $DB->request('glpi_computers', ['FIELDS' => 'name', 'DISTINCT' => true]); - // => SELECT DISTINCT `name` FROM `glpi_computers` - The fields array can also contain per table sub-array: .. code-block:: php @@ -105,42 +74,27 @@ The fields array can also contain per table sub-array: Using JOINs ^^^^^^^^^^^ -You need to use criteria, usually a ``FKEY`` to describe how to join the tables. - -.. note:: - - .. versionadded:: 9.3.1 - - The ``ON`` keyword can aslo be used as an alias of ``FKEY``. - -Multiple tables, native join -++++++++++++++++++++++++++++ - -You need to use criteria, usually a ``FKEY`` (or the ``ON`` equivalent), to describe how to join the tables: - -.. code-block:: php - - request(['FROM' => ['glpi_computers', 'glpi_computerdisks'], - 'FKEY' => ['glpi_computers'=>'id', - 'glpi_computerdisks'=>'computer_id']]); - $DB->request(['glpi_computers', 'glpi_computerdisks'], - ['FKEY' => ['glpi_computers'=>'id', - 'glpi_computerdisks'=>'computer_id']]); - // => SELECT * FROM `glpi_computers`, `glpi_computerdisks` - // WHERE `glpi_computers`.`id` = `glpi_computerdisks`.`computer_id` +You need to use criteria, usually a ``ON`` (or the ``FKEY`` equivalent), to describe how to join the tables. Left join +++++++++ -Using the ``LEFT JOIN`` option, with some criteria, usually a ``FKEY`` (or the ``ON`` equivalent): +Using the ``LEFT JOIN`` option, with some criteria: .. code-block:: php request(['FROM' => 'glpi_computers', - 'LEFT JOIN' => ['glpi_computerdisks' => ['FKEY' => ['glpi_computers' => 'id', - 'glpi_computerdisks' => 'computer_id']]]]); + $DB->request([ + 'FROM' => 'glpi_computers', + 'LEFT JOIN' => [ + 'glpi_computerdisks' => [ + 'ON' => [ + 'glpi_computers' => 'id', + 'glpi_computerdisks' => 'computer_id' + ] + ] + ] + ]); // => SELECT * FROM `glpi_computers` // LEFT JOIN `glpi_computerdisks` // ON (`glpi_computers`.`id` = `glpi_computerdisks`.`computer_id`) @@ -148,14 +102,22 @@ Using the ``LEFT JOIN`` option, with some criteria, usually a ``FKEY`` (or the ` Inner join ++++++++++ -Using the ``INNER JOIN`` option, with some criteria, usually a ``FKEY`` (or the ``ON`` equivalent): +Using the ``INNER JOIN`` option, with some criteria: .. code-block:: php request(['FROM' => 'glpi_computers', - 'INNER JOIN' => ['glpi_computerdisks' => ['FKEY' => ['glpi_computers' => 'id', - 'glpi_computerdisks' => 'computer_id']]]]); + $DB->request([ + 'FROM' => 'glpi_computers', + 'INNER JOIN' => [ + 'glpi_computerdisks' => [ + 'ON' => [ + 'glpi_computers' => 'id', + 'glpi_computerdisks' => 'computer_id' + ] + ] + ] + ]); // => SELECT * FROM `glpi_computers` // INNER JOIN `glpi_computerdisks` // ON (`glpi_computers`.`id` = `glpi_computerdisks`.`computer_id`) @@ -163,14 +125,22 @@ Using the ``INNER JOIN`` option, with some criteria, usually a ``FKEY`` (or the Right join ++++++++++ -Using the ``RIGHT JOIN`` option, with some criteria, usually a ``FKEY`` (or the ``ON`` equivalent): +Using the ``RIGHT JOIN`` option, with some criteria: .. code-block:: php request(['FROM' => 'glpi_computers', - 'RIGHT JOIN' => ['glpi_computerdisks' => ['FKEY' => ['glpi_computers' => 'id', - 'glpi_computerdisks' => 'computer_id']]]]); + $DB->request([ + 'FROM' => 'glpi_computers', + 'RIGHT JOIN' => [ + 'glpi_computerdisks' => [ + 'ON' => [ + 'glpi_computers' => 'id', + 'glpi_computerdisks' => 'computer_id' + ] + ] + ] + ]); // => SELECT * FROM `glpi_computers` // RIGHT JOIN `glpi_computerdisks` // ON (`glpi_computers`.`id` = `glpi_computerdisks`.`computer_id`) @@ -189,7 +159,7 @@ It is also possible to add an extra criterion for any `JOIN` clause. You have to 'FROM' => 'glpi_computers', 'INNER JOIN' => [ 'glpi_computerdisks' => [ - 'FKEY' => [ + 'ON' => [ 'glpi_computers' => 'id', 'glpi_computerdisks' => 'computer_id', ['OR' => ['glpi_computers.field' => ['>', 42]]] @@ -275,9 +245,6 @@ Using the ``GROUPBY`` option, which contains a field name or an array of field n $DB->request(['FROM' => 'glpi_computers', 'GROUPBY' => 'name']); // => SELECT * FROM `glpi_computers` GROUP BY `name` - $DB->request('glpi_computers', ['GROUPBY' => ['name', 'states_id']]); - // => SELECT * FROM `glpi_computers` GROUP BY `name`, `states_id` - Order ^^^^^ @@ -289,9 +256,6 @@ Using the ``ORDER`` option, with value a field or an array of fields. Field name $DB->request(['FROM' => 'glpi_computers', 'ORDER' => 'name']); // => SELECT * FROM `glpi_computers` ORDER BY `name` - $DB->request('glpi_computers', ['ORDER' => ['date_mod DESC', 'name ASC']]); - // => SELECT * FROM `glpi_computers` ORDER BY `date_mod` DESC, `name` ASC - Request pager ^^^^^^^^^^^^^ @@ -324,11 +288,10 @@ A field name and its wanted value: $DB->request(['FROM' => 'glpi_computers', 'WHERE' => ['is_deleted' => 0]]); // => SELECT * FROM `glpi_computers` WHERE `is_deleted` = 0 - $DB->request('glpi_computers', ['is_deleted' => 0, - 'name' => 'foo']); + $DB->request(['FROM' => 'glpi_computers', 'WHERE' => ['is_deleted' => 0, 'name' => 'foo']]); // => SELECT * FROM `glpi_computers` WHERE `is_deleted` = 0 AND `name` = 'foo' - $DB->request('glpi_computers', ['users_id' => [1,5,7]]); + $DB->request('FROM' => 'glpi_computers', 'WHERE' => ['users_id' => [1,5,7]]]); // => SELECT * FROM `glpi_computers` WHERE `users_id` IN (1, 5, 7) Logical ``OR``, ``AND``, ``NOT`` @@ -339,11 +302,25 @@ Using the ``OR``, ``AND``, or ``NOT`` option with an array of criteria: .. code-block:: php request('glpi_computers', ['OR' => ['is_deleted' => 0, - 'name' => 'foo']]); + $DB->request([ + 'FROM' => 'glpi_computers', + 'WHERE' => [ + 'OR' => [ + 'is_deleted' => 0, + 'name' => 'foo' + ] + ] + ]); // => SELECT * FROM `glpi_computers` WHERE (`is_deleted` = 0 OR `name` = 'foo')" - $DB->request('glpi_computers', ['NOT' => ['id' => [1,2,7]]]); + $DB->request([ + 'FROM' => 'glpi_computers', + 'WHERE' => [ + 'NOT' => [ + 'id' => [1, 2, 7] + ] + ] + ]); // => SELECT * FROM `glpi_computers` WHERE NOT (`id` IN (1, 2, 7)) @@ -352,12 +329,57 @@ Using a more complex expression with ``AND`` and ``OR``: .. code-block:: php request('glpi_computers', ['is_deleted' => 0, - ['OR' => ['name' => 'foo', 'otherserial' => 'otherunique']], - ['OR' => ['locations_id' => 1, 'serial' => 'unique']] + $DB->request([ + 'FROM' => 'glpi_computers', + 'WHERE' => [ + 'is_deleted' => 0, + ['OR' => ['name' => 'foo', 'otherserial' => 'otherunique']], + ['OR' => ['locations_id' => 1, 'serial' => 'unique']] + ] ]); // => SELECT * FROM `glpi_computers` WHERE `is_deleted` = '0' AND ((`name` = 'foo' OR `otherserial` = 'otherunique')) AND ((`locations_id` = '1' OR `serial` = 'unique')) +Criteria unicity +++++++++++++++++ + + +Indexed array entries must be unique; otherwise PHP will only take the last one. The following example is incorrect: + +.. code-block:: php + + request([ + 'FROM' => 'glpi_computers', + 'WHERE' => [ + [ + 'OR' => [ + 'name' => 'a name', + 'name' => 'another name' + ] + ], + ] + ]); + // => SELECT * FROM `glpi_computers` WHERE `name` = 'another name' + +The right way would be to enclose each condition in another array, like: + +.. code-block:: php + + request([ + 'FROM' => 'glpi_computers', + 'WHERE' => [ + [ + 'OR' => [ + ['name' => 'a name'], + ['name' => 'another name'] + ] + ], + ] + ]); + // => SELECT * FROM `glpi_computers` WHERE (`name = 'a name' OR `name` = 'another name') + + Operators +++++++++ @@ -366,10 +388,15 @@ Default operator is ``=``, but other operators can be used, by giving an array c .. code-block:: php request('glpi_computers', ['date_mod' => ['>' , '2016-10-01']]); + $DB->request([ + 'FROM' => 'glpi_computers', + 'WHERE' => [ + 'date_mod' => ['>' , '2016-10-01'] + ] + ]); // => SELECT * FROM `glpi_computers` WHERE `date_mod` > '2016-10-01' - $DB->request('glpi_computers', ['name' => ['LIKE' , 'pc00%']]); + $DB->request(['FROM' => 'glpi_computers', 'WHERE' => ['name' => ['LIKE' , 'pc00%']]]); // => SELECT * FROM `glpi_computers` WHERE `name` LIKE 'pc00%' Known operators are ``=``, ``!=``, ``<``, ``<=``, ``>``, ``>=``, ``LIKE``, ``REGEXP``, ``NOT LIKE``, ``NOT REGEX``, ``&`` (BITWISE AND), and ``|`` (BITWISE OR). @@ -382,7 +409,7 @@ You can use SQL aliases (SQL ``AS`` keyword). To achieve that, just write the al .. code-block:: php request('glpi_computers AS c'); + $DB->request(['FROM' => 'glpi_computers AS c']); // => SELECT * FROM `glpi_computers` AS `c` $DB->request(['SELECT' => 'field AS f', 'FROM' => 'glpi_computers AS c']); @@ -515,7 +542,7 @@ You can use a QueryExpression object in the FIELDS statement: 'FROM' => 'glpi_computers', 'LEFT JOIN' => [ 'glpi_domains' => [ - 'FKEY' => [ + 'ON' => [ 'glpi_computers' => 'domains_id', 'glpi_domains' => 'id', ] @@ -533,3 +560,15 @@ You can use a QueryExpression object in the FROM statement: 'FROM' => new QueryExpression('(SELECT * FROM glpi_computers) as computers'), ]); // => SELECT * FROM (SELECT * FROM glpi_computers) as computers + +.. warning:: + + If you really cannot use any of the above, you still can make raw SQL queries: + + .. code-block:: php + + doQuery('SHOW COLUMNS FROM ' . $DB::quoteName('glpi_computers')); + + **You have to ensure the query is proprely escaped!** + diff --git a/source/upgradeguides/glpi-10.1.rst b/source/upgradeguides/glpi-10.1.rst index 7ddabd8..548916b 100644 --- a/source/upgradeguides/glpi-10.1.rst +++ b/source/upgradeguides/glpi-10.1.rst @@ -53,3 +53,23 @@ Also, code that ouputs javascript must be adapted to prevent XSS with both HTML + $(body).append(json_encode('

' . htmlspecialchars($content) . '

')); '; + +Query builder usage ++++++++++++++++++++ + +Since it has been implemented, internal query builder (named `DBMysqlIterator`) do accept several syntaxes; that make things complex: + +1. conditions (including table name as `FROM` array key) as first (and only) parameter. +2. table name as first parameter and condition as second parameter, +3. raw SQL queries, + +The most used and easiest to maintain was the first. The second has been deprecated and the thrird has been prohibited or security reasons. + +I you were using the second syntax, you will need to replace as follows: + +.. code-block:: diff + + - $iterator = $DB->request('mytable', ['field' => 'condition']); + + $iterator = $DB->request(['FROM' => 'mytable', 'WHERE' => ['field' => 'condition']]); + +Using raw SQL queries must be replaced with query builder call, among other to prevent syntax issues, and SQL injections; please refer to :doc:devapi/database/dbiterator. From d4cb9b32b2f7d417c17bc41e080ebc2093fb18a0 Mon Sep 17 00:00:00 2001 From: Stanislas Date: Tue, 17 Sep 2024 09:55:26 +0200 Subject: [PATCH 09/17] add doc for new hook pre/post_itil_info_section --- source/plugins/hooks.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/source/plugins/hooks.rst b/source/plugins/hooks.rst index 13d94c7..a8738d2 100644 --- a/source/plugins/hooks.rst +++ b/source/plugins/hooks.rst @@ -124,7 +124,7 @@ These hooks will work just as the :ref:`hooks with item as parameter ``. + + +``post_itil_info_section`` + .. versionadded:: 11 + + After displaying ITIL object sections (ticket, Change, Problem) Waits for a ``
``. + ``pre_item_form`` .. versionadded:: 9.1.2 From 8c6024a29c8b88b9a381a2a67c853fabe2d1597e Mon Sep 17 00:00:00 2001 From: Curtis Conard Date: Thu, 1 Aug 2024 15:18:39 -0400 Subject: [PATCH 10/17] add hlapi versioning info --- source/devapi/hlapi/index.rst | 1 + source/devapi/hlapi/schemas.rst | 13 +++++++++++++ source/devapi/hlapi/versioning.rst | 30 ++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 source/devapi/hlapi/versioning.rst diff --git a/source/devapi/hlapi/index.rst b/source/devapi/hlapi/index.rst index 1b30ae1..69fbfcb 100644 --- a/source/devapi/hlapi/index.rst +++ b/source/devapi/hlapi/index.rst @@ -11,3 +11,4 @@ These sections are sorted by the recommended reading order. It is recommended th schemas search + versioning diff --git a/source/devapi/hlapi/schemas.rst b/source/devapi/hlapi/schemas.rst index 3c4f333..36e4566 100644 --- a/source/devapi/hlapi/schemas.rst +++ b/source/devapi/hlapi/schemas.rst @@ -20,6 +20,7 @@ Let's look at a partial version of the schema definition for a User since it sho .. code-block:: php 'User' => [ + 'x-version-introduced' => '2.0.0', 'x-itemtype' => User::class, 'type' => Doc\Schema::TYPE_OBJECT, 'x-rights-conditions' => [ // Object-level extra permissions @@ -214,6 +215,18 @@ Below is a complete list of supported extension fields/properties used in OpenAP This enables the accessing of properties not in the partial schema in certain conditions such as a GraphQL query. - Schema join properties - Yes + * - x-version-introduced + - Indicates which API version the schema or property first becomes available in. This is required for all schemas. Any individual properties without this will use the introduction version from the schema. + - Main schema and schema properties + - Yes + * - x-version-deprecated + - Indicates which API version the schema or property becomes deprecated in. Any individual properties without this will use the deprecated version from the schema if specified. + - Main schema and schema properties + - Yes + * - x-version-removed + - Indicates which API version the schema or property becomes removed in. Any individual properties without this will use the removed version from the schema if specified. + - Main schema and schema properties + - Yes * - x-itemtype - Specifies the PHP class related to the schema. - Main schema diff --git a/source/devapi/hlapi/versioning.rst b/source/devapi/hlapi/versioning.rst new file mode 100644 index 0000000..64962dd --- /dev/null +++ b/source/devapi/hlapi/versioning.rst @@ -0,0 +1,30 @@ +Versioning +========== + +The High-Level API will actively filter the routes and schema definitions based on the API version requested by the user (or default to the latest API version). +The version being used is stored by the router in a `GLPI-API-Version` header in the request after being normalized based on version pinning rules (See the getting started documentation for the High-Level API). +Controllers that extend `Glpi\Api\HL\Controller\AbstractController` can pass the request to the `getAPIVersion` helper function to get the API version. + + +Route Versions +^^^^^^^^^^^^^^ + +All routes must have a `Glpi\Api\HL\RouteVersion` attribute present. +This attribute allows specifying an introduction, deprecated, and removal version. +The introduction version is required. + +When the router attempts to match a request to a route, it will take the versions specified on each route into account. +So if a user requests API version 3, routes introduced in v4 will not be considered. +Additionally, routes removed in v3 will also not be considered. +Deprecation versions do not affect the route matching logic. + +Schema Versions +^^^^^^^^^^^^^^^ + +All schemas must have a `x-version-introduced` property present. +They may also have `x-version-deprecated` and `x-version-removed` properties if applicable. +Individual properties within schemas may declare these version properties as well, but will use the versions from the schema itself if not. + +When schemas are requested from each controller, they will be filtered based on the API version requested by the user (or default to the latest API version). +If the versions on a schema make it inapplicable to the requested version, it is not returned at all from the controller. +If the schema itself is applicable, each property is evaluated and inapplicable properties are removed. \ No newline at end of file From 5b0794790ba69e10e6ac54ad448638fbcf91d46e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Anne?= Date: Wed, 25 Sep 2024 12:37:35 +0200 Subject: [PATCH 11/17] Add routing changes section to the GLPI 11.0 upgrade guidelines --- .../{glpi-10.1.rst => glpi-11.0.rst} | 45 ++++++++++++++++--- source/upgradeguides/index.rst | 2 +- 2 files changed, 40 insertions(+), 7 deletions(-) rename source/upgradeguides/{glpi-10.1.rst => glpi-11.0.rst} (60%) diff --git a/source/upgradeguides/glpi-10.1.rst b/source/upgradeguides/glpi-11.0.rst similarity index 60% rename from source/upgradeguides/glpi-10.1.rst rename to source/upgradeguides/glpi-11.0.rst index 548916b..42bb1bb 100644 --- a/source/upgradeguides/glpi-10.1.rst +++ b/source/upgradeguides/glpi-11.0.rst @@ -1,14 +1,14 @@ -Upgrade to GLPI 10.1 +Upgrade to GLPI 11.0 -------------------- Removal of input variables auto-sanitize ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Prior to GLPI 10.1, PHP superglobals ``$_GET``, ``$_POST`` and ``$_REQUEST`` were automatically sanitized. +Prior to GLPI 11.0, PHP superglobals ``$_GET``, ``$_POST`` and ``$_REQUEST`` were automatically sanitized. It means that SQL special characters were escaped (prefixed by a ``\``), and HTML special characters ``<``, ``>`` and ``&`` were encoded into HTML entities. This caused issues because it was difficult, for some pieces of code, to know if the received variables were "secure" or not. -In GLPI 10.1, we removed this auto-sanitization, and any data, whether it comes from a form, the database, or the API, will always be in its raw state. +In GLPI 11.0, we removed this auto-sanitization, and any data, whether it comes from a form, the database, or the API, will always be in its raw state. Protection against SQL injection ++++++++++++++++++++++++++++++++ @@ -57,15 +57,15 @@ Also, code that ouputs javascript must be adapted to prevent XSS with both HTML Query builder usage +++++++++++++++++++ -Since it has been implemented, internal query builder (named `DBMysqlIterator`) do accept several syntaxes; that make things complex: +Since it has been implemented, internal query builder (named ``DBMysqlIterator``) do accept several syntaxes; that make things complex: -1. conditions (including table name as `FROM` array key) as first (and only) parameter. +1. conditions (including table name as ``FROM`` array key) as first (and only) parameter. 2. table name as first parameter and condition as second parameter, 3. raw SQL queries, The most used and easiest to maintain was the first. The second has been deprecated and the thrird has been prohibited or security reasons. -I you were using the second syntax, you will need to replace as follows: +If you were using the second syntax, you will need to replace as follows: .. code-block:: diff @@ -73,3 +73,36 @@ I you were using the second syntax, you will need to replace as follows: + $iterator = $DB->request(['FROM' => 'mytable', 'WHERE' => ['field' => 'condition']]); Using raw SQL queries must be replaced with query builder call, among other to prevent syntax issues, and SQL injections; please refer to :doc:devapi/database/dbiterator. + +Changes related to URLs routing +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Crafting plugins URLs ++++++++++++++++++++++ + +In GLPI 11.0, we changed the way to handle URLs to plugin resources so that they no longer need to reflect the location of the plugin on the file system. +For instance, the same URL could be used to access a plugin file whether it was installed manually in the ``/plugins`` directory or via the marketplace. + +The ``Plugin::getWebDir()`` PHP method has been deprecated. + +.. code-block:: diff + + - $path = Plugin::getWebDir('myplugin', false) . '/front/myscript.php'; + + $path = '/plugins/myplugin/front/myscript.php'; + + - $path = Plugin::getWebDir('myplugin', true) . '/front/myscript.php'; + + $path = $CFG_GLPI['root_doc'] . '/plugins/myplugin/front/myscript.php'; + +The ``GLPI_PLUGINS_PATH`` javascript variable has been deprecated. + +.. code-block:: diff + + - var url = CFG_GLPI.root_doc + '/' + GLPI_PLUGINS_PATH.myplugin + '/ajax/script.php'; + + var url = CFG_GLPI.root_doc + '/plugins/myplugin/ajax/script.php'; + +The ``get_plugin_web_dir`` Twig function has been deprecated. + +.. code-block:: diff + + -
+ + diff --git a/source/upgradeguides/index.rst b/source/upgradeguides/index.rst index 5d5c526..cafed5a 100644 --- a/source/upgradeguides/index.rst +++ b/source/upgradeguides/index.rst @@ -11,4 +11,4 @@ The upgrade guides are intended to help you adapt your plugins to the changes in .. toctree:: :maxdepth: 2 - glpi-10.1 + glpi-11.0 From 291aa87c604e14c6442b8c5314447351c132f466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Anne?= Date: Wed, 25 Sep 2024 15:44:49 +0200 Subject: [PATCH 12/17] A mention about support of /markeplace URLs --- source/upgradeguides/glpi-11.0.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/source/upgradeguides/glpi-11.0.rst b/source/upgradeguides/glpi-11.0.rst index 42bb1bb..4477e98 100644 --- a/source/upgradeguides/glpi-11.0.rst +++ b/source/upgradeguides/glpi-11.0.rst @@ -83,6 +83,9 @@ Crafting plugins URLs In GLPI 11.0, we changed the way to handle URLs to plugin resources so that they no longer need to reflect the location of the plugin on the file system. For instance, the same URL could be used to access a plugin file whether it was installed manually in the ``/plugins`` directory or via the marketplace. +To maintain backwards compatibility with previous behavior, we will continue to support URLs using the ``/marketplace`` path prefix. +However, their use is deprecated and may be removed in a future version of GLPI. + The ``Plugin::getWebDir()`` PHP method has been deprecated. .. code-block:: diff From 8a7672161935cfbce9dfe6ad82b69a92180b4573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Anne?= Date: Mon, 28 Oct 2024 13:59:55 +0100 Subject: [PATCH 13/17] Add more upgrade instructions for GLPI 11.0 (#157) * Add more upgrade instructions for GLPI 11.0 * Apply suggestions from code review Co-authored-by: Curtis Conard --------- Co-authored-by: Curtis Conard --- source/upgradeguides/glpi-11.0.rst | 76 ++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/source/upgradeguides/glpi-11.0.rst b/source/upgradeguides/glpi-11.0.rst index 4477e98..893fc4c 100644 --- a/source/upgradeguides/glpi-11.0.rst +++ b/source/upgradeguides/glpi-11.0.rst @@ -74,13 +74,83 @@ If you were using the second syntax, you will need to replace as follows: Using raw SQL queries must be replaced with query builder call, among other to prevent syntax issues, and SQL injections; please refer to :doc:devapi/database/dbiterator. -Changes related to URLs routing -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Changes related to web requests handling +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In GLPI 11.0, all the web requests are now handled by a unique entry point, the ``/public/index.php`` script. +This allowed us to centralize a large number of things, including GLPI's initialization mechanics and error management. + +Removal of the ``/inc/includes.php`` script ++++++++++++++++++++++++++++++++++++++++++++ + +All the logic that was executed by the inclusion of the ``/inc/includes.php`` script is now made automatically. +Therefore, it is no longer necessary to include it, even if it is still present to ease the migration to GLPI 11.0. + +.. code-block:: diff + + - include("../../../inc/includes.php"); + +Legacy scripts access policy +++++++++++++++++++++++++++++ + +By default, the access to any PHP script will be allowed only to authenticated users. +If you need to change this default policy for some of your PHP scripts, you will need to do this in your plugin ``init`` function, +using the ``Glpi\Http\Firewall::addPluginFallbackStrategy()`` method. + +.. code-block:: php + + getFromDB($_GET['id']) === false) { + - http_response_code(404); + - exit(); + + throw new \Glpi\Exception\Http\NotFoundHttpException(); + } + +In case the ``exit()``/``die()`` language construct was used to just ignore the following line of code in the script, you can replace it with a ``return`` instruction. + +.. code-block:: diff + + if ($action === 'foo') { + // specific action + echo "foo action executed"; + - exit(); + + return; + } + + MypluginItem::displayFullPageForItem($_GET['id']); Crafting plugins URLs +++++++++++++++++++++ -In GLPI 11.0, we changed the way to handle URLs to plugin resources so that they no longer need to reflect the location of the plugin on the file system. +We changed the way to handle URLs to plugin resources so that they no longer need to reflect the location of the plugin on the file system. For instance, the same URL could be used to access a plugin file whether it was installed manually in the ``/plugins`` directory or via the marketplace. To maintain backwards compatibility with previous behavior, we will continue to support URLs using the ``/marketplace`` path prefix. From 54d25be5d949d168d0aad4fc5d99e3d2c91e6800 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Wed, 6 Nov 2024 10:16:37 +0100 Subject: [PATCH 14/17] Add documentation on controllers --- source/devapi/controllers.rst | 326 ++++++++++++++++++++++++++++++++++ source/devapi/index.rst | 1 + 2 files changed, 327 insertions(+) create mode 100644 source/devapi/controllers.rst diff --git a/source/devapi/controllers.rst b/source/devapi/controllers.rst new file mode 100644 index 0000000..0da25f8 --- /dev/null +++ b/source/devapi/controllers.rst @@ -0,0 +1,326 @@ +Controllers +----------- + +You need a `Controller` any time you want an URL access. + +.. note:: + + `Controllers` is the "modern" way that replaces all files previousely present in ``front/`` and ``ajax/`` directories. + +.. warning:: + + Currently, not all existing front or ajax files has been migrated to `Controllers`, mainly because of specific stuff or no time to work on that yet. + + Any new feature added to GLPI >= 11 **must** use `Controllers`. + +Creating a controller +^^^^^^^^^^^^^^^^^^^^^ + +Minimal requirements to have a working controller: + +* The controller file must be placed in the src/Glpi/Controller/** folder. +* The name of the controller must end with Controller. +* The controller must extends the ``Glpi\Controller\AbstractController`` class. +* The controller must define a route using the Route attribute. +* The controller must return some kind of response. + +Example: + +.. code-block:: php + + # src/Controller/Form/TagsListController.php + query->getString('filter'); + + return new JsonResponse($tag_manager->getTags($filter)); + } + } + +Routing +^^^^^^^ + +Routing is done with the ``Symfony\Component\Routing\Attribute\Route`` attribute. Read more from `Symfony Routing documentation `_. + +Basic route ++++++++++++ + +.. code-block:: php + + #[Symfony\Component\Routing\Attribute\Route("/my/route/url", name: "glpi_my_route_name")] + +Dynamic route parameter ++++++++++++++++++++++++ + +.. code-block:: php + + #[Symfony\Component\Routing\Attribute\Route("/Ticket/{$id}", name: "glpi_ticket")] + +Restricting a route to a specific HTTP method ++++++++++++++++++++++++++++++++++++++++++++++ + +.. code-block:: php + + #[Symfony\Component\Routing\Attribute\Route("/Tickets", name: "glpi_tickets", methods: "GET")] + +Known limitation for ajax routes +++++++++++++++++++++++++++++++++ + +If an ajax route will be accessed by multiple POST requests without a page reload then you will run into CRSF issues. + +This is because GLPI’s solution for this is to check a special CRSF token that is valid for multiples requests, but this special token is only checked if your url start with ``/ajax``. + +You will thus need to prefix your route by ``/ajax`` until we find a better way to handle this. + +Reading query parameters +^^^^^^^^^^^^^^^^^^^^^^^^ + +These parameters are found in the $request object: + +* ``$request->query`` for ``$_GET`` +* ``$request->request`` for ``$_POST`` +* ``$request->files`` for ``$_FILES`` + +Read more from `Symfony Request documentation `_ + +Reading a string parameter from $_GET ++++++++++++++++++++++++++++++++++++++ + +.. code-block:: php + + query->getString('filter'); + } + +Reading an integer parameter from $_POST +++++++++++++++++++++++++++++++++++++++++ + +.. code-block:: php + + request->getInt('my_int'); + } + +Reading an array of values from $_POST +++++++++++++++++++++++++++++++++++++++ + +.. code-block:: php + + request->all()["ids"] ?? []; + } + +Reading a file +++++++++++++++ + +.. code-block:: php + + files->get('my_file_input_name'); + $content = $file->getContent(); + } + +Single vs multi action controllers +++++++++++++++++++++++++++++++++++ + +The examples in this documentation use the magic ``__invoke`` method to force the controller to have only one action (see https://symfony.com/doc/current/controller/service.html#invokable-controllers). + +In general, this is recommended way to proceed but we do not force it and you are allowed to use multi actions controllers if you need them. + +Handling errors (missing rights, bad request, …) +++++++++++++++++++++++++++++++++++++++++++++++++ + +A controller may throw some exceptions if it receive an invalid request. + +You can use any exception that extends ``Symfony\Component\HttpKernel\Exception``, see below examples. + +Missing rights +++++++++++++++ + +.. code-block:: php + + request->getInt('id'); + if ($id == 0) { + throw new Symfony\Component\HttpKernel\Exception\BadRequestHttpException(); + } + } + +Firewall +^^^^^^^^ + +By default, the GLPI firewall will not allow unauthenticated user to access your routes. You can change the firewall strategy with the ``Glpi\Security\Attribute\SecurityStrategy`` attribute. + +.. code-block:: php + + 'John', 'age' => 67]); + +Sending a file from memory +++++++++++++++++++++++++++ + +.. code-block:: php + + headers->set('Content-Disposition', $disposition); + $response->headers->set('Content-Type', 'text/plain'); + return $response + +Sending a file from disk +++++++++++++++++++++++++ + +.. code-block:: php + + render('/path/to/my/template.html.twig', [ + 'parameter_1' => 'value_1', + 'parameter_2' => 'value_2', + ]); + +Redirection ++++++++++++ + +.. code-block:: php + + Date: Wed, 6 Nov 2024 14:12:10 +0100 Subject: [PATCH 15/17] Update controllers docs --- source/devapi/controllers.rst | 99 +++++++++++++++++++++++++++-------- 1 file changed, 76 insertions(+), 23 deletions(-) diff --git a/source/devapi/controllers.rst b/source/devapi/controllers.rst index 0da25f8..1fd1491 100644 --- a/source/devapi/controllers.rst +++ b/source/devapi/controllers.rst @@ -5,21 +5,21 @@ You need a `Controller` any time you want an URL access. .. note:: - `Controllers` is the "modern" way that replaces all files previousely present in ``front/`` and ``ajax/`` directories. + `Controllers` are the modern way that replace all files previousely present in ``front/`` and ``ajax/`` directories. .. warning:: - Currently, not all existing front or ajax files has been migrated to `Controllers`, mainly because of specific stuff or no time to work on that yet. + Currently, not all existing ``front/`` or ``ajax/`` files have been migrated to `Controllers`, mainly because of specific behaviors or lack of time to work on migrating them. - Any new feature added to GLPI >= 11 **must** use `Controllers`. + Any new feature added to GLPI >=11 **must** use `Controllers`. Creating a controller ^^^^^^^^^^^^^^^^^^^^^ Minimal requirements to have a working controller: -* The controller file must be placed in the src/Glpi/Controller/** folder. -* The name of the controller must end with Controller. +* The controller file must be placed in the ``src/Glpi/Controller/`` folder. +* The name of the controller must end with ``Controller``. * The controller must extends the ``Glpi\Controller\AbstractController`` class. * The controller must define a route using the Route attribute. * The controller must return some kind of response. @@ -76,7 +76,7 @@ Dynamic route parameter .. code-block:: php - #[Symfony\Component\Routing\Attribute\Route("/Ticket/{$id}", name: "glpi_ticket")] + #[Symfony\Component\Routing\Attribute\Route("/Ticket/{id}", name: "glpi_ticket")] Restricting a route to a specific HTTP method +++++++++++++++++++++++++++++++++++++++++++++ @@ -97,7 +97,7 @@ You will thus need to prefix your route by ``/ajax`` until we find a better way Reading query parameters ^^^^^^^^^^^^^^^^^^^^^^^^ -These parameters are found in the $request object: +These parameters are found in the ``$request`` object: * ``$request->query`` for ``$_GET`` * ``$request->request`` for ``$_POST`` @@ -135,7 +135,7 @@ Reading an array of values from $_POST request->all()["ids"] ?? []; + $ids = $request->request->get("ids", []); } Reading a file @@ -156,14 +156,16 @@ Single vs multi action controllers The examples in this documentation use the magic ``__invoke`` method to force the controller to have only one action (see https://symfony.com/doc/current/controller/service.html#invokable-controllers). -In general, this is recommended way to proceed but we do not force it and you are allowed to use multi actions controllers if you need them. +In general, this is a recommended way to proceed but we do not force it and you are allowed to use multi actions controllers if you need them, by adding another public method and configuring it with the ``#[Route(...)]`` attribute. Handling errors (missing rights, bad request, …) ++++++++++++++++++++++++++++++++++++++++++++++++ -A controller may throw some exceptions if it receive an invalid request. +A controller may throw some exceptions if it receive an invalid request. Exceptions will automatically converted to error pages. -You can use any exception that extends ``Symfony\Component\HttpKernel\Exception``, see below examples. +If you need exceptions with specific HTTP codes (like 4xx or 5xx codes), you can use any exception that extends ``Symfony\Component\HttpKernel\Exception\HttpException``. + +GLPI also provide some custom Http exceptions in the ``Glpi\Exception\Http\`` namespace. Missing rights ++++++++++++++ @@ -174,7 +176,20 @@ Missing rights public function __invoke(Symfony\Component\HttpFoundation\Request $request): Response { if (!Form::canUpdate()) { - throw new Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException(); + throw new \Glpi\Exception\Http\AccessDeniedHttpException(); + } + } + +Invalid header +++++++++++++++ + +.. code-block:: php + + headers->get('Content-Type') !== 'application/json') { + throw new \Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException(); } } @@ -188,7 +203,7 @@ Invalid input { $id = $request->request->getInt('id'); if ($id == 0) { - throw new Symfony\Component\HttpKernel\Exception\BadRequestHttpException(); + throw new \Glpi\Exception\Http\BadRequestHttpException(); } } @@ -208,7 +223,7 @@ Possible responses You may use different responses classes depending on what your controller is doing (sending json content, outputting a file, …). -There is also a render helper method that helps you return a rendered twig content as a response. +There is also a render helper method that helps you return a rendered Twig template as a Response object. Sending JSON ++++++++++++ @@ -232,7 +247,7 @@ Sending a file from memory $filename, ); - $response = new Symfony\Component\HttpFoundation;\Response($file_content); + $response = new Symfony\Component\HttpFoundation\Response($file_content); $response->headers->set('Content-Disposition', $disposition); $response->headers->set('Content-Type', 'text/plain'); return $response @@ -243,8 +258,8 @@ Sending a file from disk .. code-block:: php render('/path/to/my/template.html.twig', [ + return $this->render('path/to/my/template.html.twig', [ 'parameter_1' => 'value_1', 'parameter_2' => 'value_2', ]); @@ -271,7 +286,7 @@ General best practices Use thin controllers ++++++++++++++++++++ -Controller should be *thin*, which mean they should contains the minimal code needed to *glue* together the pieces of GLPI needed to handle the request. +Controller should be *thin*, which mean they should contain the minimal code needed to *glue* together the pieces of GLPI needed to handle the request. A good controller does only the following actions: @@ -279,7 +294,7 @@ A good controller does only the following actions: * Validate the request * Extract what it needs from the request * Call some methods from a dedicated service class that can process the data (using DI in the future, not possible at this time) -* Return a response +* Return a ``Response`` object Most of the time, this will take between 5 and 15 instructions, resulting in a small method. @@ -297,9 +312,9 @@ Unless you are making a generic controller that is explicitly made to be extende Always restrict the HTTP method +++++++++++++++++++++++++++++++ -If your controller is only meant to be used with a specific HTTP method (e.g. `POST`), it is best to define it. +If your controller is only meant to be used with a specific HTTP method (e.g. `POST`), it is best to define it in the ``Route`` attribute. -It helps others developers to understand how this route must be used and help debugging when miss-using the route. +It helps others developers understand how this route must be used and help debugging when misusing the route. .. code-block:: php @@ -323,4 +338,42 @@ URL generation Ideally, URLs should not be hard-coded but should instead be generated using their route names. -This is not yet possible in many places so we have to rely on hard-coded urls at this time. +In your Controllers, you can inject the Symfony router in the constructor in order to generate URLs based on route names: + +.. code-block:: php + + router->generate('my_route'); + + // ... + } + } + +You can also do it in Twig templates, using the ``url()`` or ``path()`` functions: + +.. code-block:: twig + + {{ path('my_route') }} {# Shows the url like "/my_route" #} + {{ url('my_route') }} {# Shows the url like "http://localhost/my_route" #} + +Check out the Symfony documentation for more details about these functions: + +* ``url()`` https://symfony.com/doc/current/reference/twig_reference.html#url +* ``path()`` https://symfony.com/doc/current/reference/twig_reference.html#path From 3b9ba5c36977a28828af885953005d809755e5d0 Mon Sep 17 00:00:00 2001 From: "Romain B." <8530352+Rom1-B@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:36:41 +0100 Subject: [PATCH 16/17] Add migration guide related resource access restrictions --- source/upgradeguides/glpi-11.0.rst | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/source/upgradeguides/glpi-11.0.rst b/source/upgradeguides/glpi-11.0.rst index 893fc4c..aa45d75 100644 --- a/source/upgradeguides/glpi-11.0.rst +++ b/source/upgradeguides/glpi-11.0.rst @@ -90,12 +90,24 @@ Therefore, it is no longer necessary to include it, even if it is still present - include("../../../inc/includes.php"); +Resource access restrictions +++++++++++++++++++++++++++++ + +In GLPI 11.0, we restrict the resources that can be accessed through a web request. + +We still support access to the PHP scripts located in the ``/ajax``, ``/front`` and ``/report`` directories. +Their URL remains unchanged, for instance, the URL of the ``/front/index.php`` script of your plugin remains ``/plugins/myplugin/front/index.php``. + +The static assets must be moved in the ``/public`` directory to be accessible. +Their URL must not contain the ``/public`` path. +For instance, the URL of the ``/public/css/styles.css`` stylesheet of your plugin will be ``/plugins/myplugin/css/styles.css``. + Legacy scripts access policy ++++++++++++++++++++++++++++ By default, the access to any PHP script will be allowed only to authenticated users. If you need to change this default policy for some of your PHP scripts, you will need to do this in your plugin ``init`` function, -using the ``Glpi\Http\Firewall::addPluginFallbackStrategy()`` method. +using the ``Glpi\Http\Firewall::addPluginStrategyForLegacyScripts()`` method. .. code-block:: php @@ -104,8 +116,8 @@ using the ``Glpi\Http\Firewall::addPluginFallbackStrategy()`` method. use Glpi\Http\Firewall; function plugin_init_myplugin() { - Firewall::addPluginFallbackStrategy('myplugin', '#^/front/api.php/#', Firewall::STRATEGY_NO_CHECK); - Firewall::addPluginFallbackStrategy('myplugin', '#^/front/dashboard.php$#', Firewall::STRATEGY_CENTRAL_ACCESS); + Firewall::addPluginStrategyForLegacyScripts('myplugin', '#^/front/api.php/#', Firewall::STRATEGY_NO_CHECK); + Firewall::addPluginStrategyForLegacyScripts('myplugin', '#^/front/dashboard.php$#', Firewall::STRATEGY_CENTRAL_ACCESS); } The following strategies are available: From 86f91771bfd694bd263272679cc775b501d92cb0 Mon Sep 17 00:00:00 2001 From: Curtis Conard Date: Sun, 19 Jan 2025 18:03:56 -0500 Subject: [PATCH 17/17] update timeline hooks --- source/plugins/hooks.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/plugins/hooks.rst b/source/plugins/hooks.rst index a8738d2..e4148a1 100644 --- a/source/plugins/hooks.rst +++ b/source/plugins/hooks.rst @@ -484,12 +484,12 @@ Hooks that permits to add display on items. ``timeline_answer_actions`` .. versionadded:: 10.0.0 - Display new actions in the ITIL object's answer dropdown + Add new types of items for the ITIL object and optionally show them in the answer actions dropdown -``show_in_timeline`` +``timeline_items`` .. versionadded:: 10.0.0 - Display forms in the ITIL object's timeline + Modify the array of items in the ITIL object's timeline Notifications +++++++++++++