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 diff --git a/source/devapi/controllers.rst b/source/devapi/controllers.rst new file mode 100644 index 0000000..1fd1491 --- /dev/null +++ b/source/devapi/controllers.rst @@ -0,0 +1,379 @@ +Controllers +----------- + +You need a `Controller` any time you want an URL access. + +.. note:: + + `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 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`. + +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->get("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 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. Exceptions will automatically converted to error pages. + +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 +++++++++++++++ + +.. code-block:: php + + headers->get('Content-Type') !== 'application/json') { + throw new \Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException(); + } + } + +Invalid input ++++++++++++++ + +.. code-block:: php + + request->getInt('id'); + if ($id == 0) { + throw new \Glpi\Exception\Http\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 + + 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 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/devapi/hlapi/index.rst b/source/devapi/hlapi/index.rst new file mode 100644 index 0000000..69fbfcb --- /dev/null +++ b/source/devapi/hlapi/index.rst @@ -0,0 +1,14 @@ +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 + versioning diff --git a/source/devapi/hlapi/schemas.rst b/source/devapi/hlapi/schemas.rst new file mode 100644 index 0000000..36e4566 --- /dev/null +++ b/source/devapi/hlapi/schemas.rst @@ -0,0 +1,264 @@ +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-version-introduced' => '2.0.0', + '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-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 + - 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 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 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 diff --git a/source/devapi/index.rst b/source/devapi/index.rst index aa6ab37..1b67ab8 100644 --- a/source/devapi/index.rst +++ b/source/devapi/index.rst @@ -9,10 +9,13 @@ Apart from the current documentation, you can also generate the full PHP documen mainobjects database/index search + controllers + hlapi/index massiveactions rules translations 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/index.rst b/source/index.rst index 30bacca..61f10f1 100644 --- a/source/index.rst +++ b/source/index.rst @@ -19,6 +19,7 @@ GLPI Developer Documentation checklists/index plugins/index packaging + 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/plugins/hooks.rst b/source/plugins/hooks.rst index 13d94c7..e4148a1 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 @@ -473,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 +++++++++++++ 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..ee6c78f --- /dev/null +++ b/source/plugins/javascript.rst @@ -0,0 +1,112 @@ +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: 'window _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. +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. + +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. diff --git a/source/upgradeguides/glpi-11.0.rst b/source/upgradeguides/glpi-11.0.rst new file mode 100644 index 0000000..aa45d75 --- /dev/null +++ b/source/upgradeguides/glpi-11.0.rst @@ -0,0 +1,193 @@ +Upgrade to GLPI 11.0 +-------------------- + +Removal of input variables auto-sanitize +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +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 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 +++++++++++++++++++++++++++++++++ + +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 ' + + '; + +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. + +If 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. + +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"); + +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::addPluginStrategyForLegacyScripts()`` 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 ++++++++++++++++++++++ + +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 + + - $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 new file mode 100644 index 0000000..cafed5a --- /dev/null +++ b/source/upgradeguides/index.rst @@ -0,0 +1,14 @@ +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 + + glpi-11.0