Commit bf00bf91 authored by Karamel's avatar Karamel
Browse files

Merge branch '8-kml-refactoring'

master is now v8, check branch 7 for previous changes.
parents 1bd81387 13646f8b
# legacy
config.php
config.ini
/.htaccess
vendor
composer.lock
config/config.ini
config/test-config.ini
#netbeans project tmp file
nbproject/
\ No newline at end of file
nbproject/
.idea
# Deny access to configuration files
<files *.ini>
order deny,allow
deny from all
</files>
# Uncomment to use a different configuration file
# <PATH> is your PT::$ABSPATH with all non alphanumeric characters
# replaced by _ (including the initial /)
# inifile is the full path of your config.ini file
# Ex: SetEnv PT_CONFIG__var_www_html_pasteque /etc/pasteque/config.ini
# SetEnv PT_CONFIG_<PATH> <ini file>
\ No newline at end of file
......@@ -6,6 +6,147 @@ Presentation
Pasteque Server is a POS (point of sale) project under the [GNU General Public License v3][gnu].
It is made in [PHP] with [PDO], [MySQL] and [PostgreSQL]
Quick Start
===========
Read documentation on wikibook : https://fr.wikibooks.org/wiki/Past%C3%A8que_%28logiciel%29
Installation
============
The installation procedure is not complete yet and requires a lot of manual tweaks.
First install dependencies with composer.
Then copy `/config/config-sample.ini` to `config/config.ini` and edit it to your liking.
Install a database with Doctrine. Put the following file in the root directory and name it `cli-config.php`
```
<?php
namespace Pasteque\Server;
use \Pasteque\Server\System\DAO\DoctrineDAO;
$cfg = parse_ini_file(__DIR__ . '/config/test-config.ini');
$dbInfo = ['type' => $cfg['database/type'], 'host' => $cfg['database/host'],
'port' => $cfg['database/port'], 'name' => $cfg['database/name'],
'user' => $cfg['database/user'], 'password' => $cfg['database/password']];
$dao = new DoctrineDAO($dbInfo, ['debug' => false]);
return \Doctrine\ORM\Tools\Console\ConsoleRunner::createHelperSet($dao->getEntityManager());
```
Then copy `/config/test-config-sample.ini` to `/config/test-config.ini`. Edit this file to point to the empty database to install.
Use `php vendor/bin/doctrine orm:schema-tool:update --force` to install the database structure. No data are provided yet.
Generate proxies with `php vendor/bin/doctrine orm:generate-proxies` then make the directory `src/generated` writeable by your web server user.
Make your web server point to `/src/http/public` and for apache, put the following `.htaccess` in `/src/http/public` and make sure `AllowOverride All` is set for the directory.
```
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
```
You can now call an API: `http(s)://<domain>/index.php/api/<route>`
Software architecture
=====================
```
/config All the ini files, copy and edit the samples to configure your server.
/res Business data, like default data and database schemes.
/src The code of Pasteque server.
/src/lib Internal code of Pasteque
/src/http External access (HTTP).
/src/http/public This is the directory your web server must point to.
/tests The tests of Pasteque server, see tests/README to run the tests
/vendor Third party libraries. Use composer to install them.
```
Internal code
-------------
Relative to /src/lib
```
/API The API classes and methods. This is what can be called from outside.
/Model The model declarations with annotations.
/System The non-business underlying code to run the API.
/System/API The magic under API calls lays here.
/System/SysModules How to read user login and database credentials.
/System/DAO The code relative to DAO (the Doctrine layer)
AppContext Read from the config file to run the server from external code.
```
External code
-------------
Relative to /src/http.
```
/middlewares Slim middlewares, including token management.
/public The files opened to public access. Contains index.php and .htaccess.
/routes Slim routes, one file per api
```
All routes are documented with Swagger. They are called through a given URL which contains the parameters.
Routes then make a internal call and transform the response back to HTTP. The routes must be all defined explicitely and annotated for Swagger to be able to generate the documentation.
How do I
========
Call an API from the external layer
-----------------------------------
An API call is made from \Pasteque\Server\System\API\APICaller::run. It takes the application context, the class name, the method name and arguments as parameters.
All the public methods from all classes from \Pasteque\Server\API namespace can be requested. Parameters can either be a flat array like a regular method call, or a named parameter call with an associative array.
You can call getToken($user, $password) from LoginAPI either with APICaller::run($app, 'login', 'getToken', ['myLogin', 'MyPassword']) or APICaller::run($app, 'login', 'getToken', ['password' => 'myPassword', 'user' => 'myLogin']).
The API name is case insensitive and the suffix "API" from the class name can be ommited. $app is the AppContext initialized in the external layer and shared across the layer.
It will return a \Pasteque\Server\System\API\APIResult with it's status and object content. To pass the content to the outside, use $response->getStructContent() or $response->toJson() to obtain raw data and not DAO-proxied objects.
Call an API from outside
------------------------
Look at the documentation for the URL to provide to the server. Authenticate with the login api and pass the fetched token for further calls. The HTTP layer put the token in a cookie to ease access. You can also use GET, POST or HTTP header to pass it.
Create an API
-------------
Add a new class which name ends by "API" and must have only the first letter capitalized into /src/lib/API, set the desired methods public and that's it. The API layer will magically map your code, do the parameters binding and format the response from and for the external layer.
The API classes takes a DAO object from constructor (see /src/lib/System/DAO/DAO.php) to access to the database. It is also provided by the API layer.
The method opened to the outside must then be mapped by a route un Slim.
Edit a model
------------
The models inside /src/lib/Model has annotations for Swagger and Doctrine. When editing a model, the fields will be automatically mapped by the DAO. Doctrine requires to declare attributes protected and add a getter and a setter. You will also have to list the primitive-typed fields and reference fields from both static methods getDirectFieldNames and getAssociationFieldNames. This is required to generate raw data output for the external layer because Doctrine uses proxies for each model attribute.
If the editions have an impact on the behaviour of the API, you will require to edit the corresponding API in /src/lib/API. Otherwise it will be automatically passed to the output and can be searchable without any further edit.
Add internal features
---------------------
For all the things relative to the database, see /src/lib/System/DAO: DAO.php for the general interface and DoctrineDAO.php for the implementation. The actual DAO instance is provided by DAOFactory to the API layer.
The database access is provided by a system module. See /src/lib/System/SysModules/Database/DBModule.php for the general interface. The implementations are provided by SystModuleFactory. This is the same scheme for account data with IdentModule.
The AppContext will glue all these tools to pass them afterward to the API. Given an ini configuration file, it will fetch the corresponding system modules and DAO.
Swagger and OAS
===============
We use Open API Specification (OAS, see <https://www.openapis.org/>) and <http://swagger.io> tools to generate the API documentation.
The JSON file is located in /bin/swagg.json
You can generate it with the script `generate_swagger_file.php`. The JSON /bin/swagg.json will get updated
$ php generate_swagger_file.php
You can use <http://petstore.swagger.io> and submit your own swagg.json file or you can either install swagger-UI, read the docs at <https://swagger.io/docs/swagger-tools/#swagger-ui-documentation-29>
<?php
// Pastèque Web back office
//
// Copyright (C) 2013 Scil (http://scil.coop)
//
// This file is part of Pastèque.
//
// Pastèque is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pastèque is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Pastèque. If not, see <http://www.gnu.org/licenses/>.
// API is the entry point for all API calls.
namespace Pasteque;
require_once(__DIR__ . '/inc/constants.php');
PT::$ABSPATH = __DIR__; // Base path. Also to check if a call
// originates from api.php
// Load
require_once(PT::$ABSPATH . '/inc/load.php');
require_once(PT::$ABSPATH . '/inc/load_api.php');
if (isset($_GET[PT::URL_ACTION_PARAM])) {
$api = $_GET[PT::URL_ACTION_PARAM];
$params = $_GET;
if (isset($_GET['action'])) {
$action = $_GET['action'];
} else {
$action = null;
}
} else {
if (isset($_POST[PT::URL_ACTION_PARAM])) {
$api = $_POST[PT::URL_ACTION_PARAM];
$params = $_POST;
if (isset($_POST['action'])) {
$action = $_POST['action'];
} else {
$action = null;
}
} else {
$api = null;
}
}
$broker = new APIBroker($api);
$result = $broker->run($action, $params);
if (array_key_exists('allowed_origin', getConfig()) && array_key_exists('HTTP_ORIGIN', $_SERVER)) {
// Handle CORS
$config = getConfig();
$cors = $config['allowed_origin'];
$origin = $_SERVER['HTTP_ORIGIN'];
// Accept *, single value match or in array
$allowedOrigin = ($cors === '*'
|| (!is_array($cors) && $cors === $origin)
|| (is_array($cors) && in_array($origin, $cors)));
if ($allowedOrigin) {
header('Access-Control-Allow-Origin: ' . $origin);
header('Access-Control-Allow-Credentials: false'); // Turned off for now, api should embed token in requests.
header('Access-Control-Max-Age: ' . $config['jwt_timeout']);
if (is_array($cors)) { header('Vary: Origin'); }
} else {
Log::info('Call from ' . $origin . ' probably rejected (CORS)');
}
}
if ($api == 'ImagesAPI') {
// Special case of images api with binary data
header('Cache-Control: max-age=864000');
echo($result);
} else {
header('Content-type: application/json');
echo json_encode($result);
}
<?php
// Pastèque Web back office
//
// Copyright (C) 2013 Scil (http://scil.coop)
//
// This file is part of Pastèque.
//
// Pastèque is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pastèque is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Pastèque. If not, see <http://www.gnu.org/licenses/>.
namespace Pasteque;
ob_clean();
$user_id = \Pasteque\Login::getLoggedUser();
$dbhost = \Pasteque\get_db_host($user_id);
$dbuser = \Pasteque\get_db_user($user_id);
$dbpasswd = \Pasteque\get_db_password($user_id);
$database = \Pasteque\get_db_name($user_id);
$dbport = \Pasteque\get_db_port($user_id);
header("Content-type: application/x-gzip-compressed");
header("Content-Disposition: attachment; filename=pasteque-".date(Ymd)."-" . $database . "-.sql.gz");
$output = fopen("php://output", "rb+");
$cmd ="mysqldump -u " . $dbuser . " --password=" . $dbpasswd . " " . $database ." --port=" . $dbport . " | gzip -c";
fputs($output,system($cmd));
{
"swagger": "2.0",
"info": {
"title": "Pasteque API",
"version": "0.1"
},
"paths": {
"/api/attribute/getAll": {
"get": {
"summary": "GET attributeGetAllGet\nSummary:\nNotes: Get an array of all Attributes\nOutput-Formats: [application/json]",
"responses": {
"200": {
"description": "An array of attributes"
}
}
}
},
"/api/attribute/{id}": {
"get": {
"summary": "GET attributeIdGet\nSummary:\nNotes: Get an Attribute\nOutput-Formats: [application/json]",
"responses": {
"200": {
"description": "get an attribute"
}
}
}
},
"/api/cash/getbycashRegister/{id}": {
"get": {
"summary": "GET cashGetbycashRegisterIdGet\nSummary:\nNotes: Get the last session Cash for a cash register\nOutput-Formats: [application/json]",
"responses": {
"200": {
"description": "Get the last session Cash for a cash register"
},
"500": {
"description": "Error"
}
}
}
},
"/api/cash/{id}": {
"get": {
"summary": "GET cashIdGet\nSummary:\nNotes: Get a Cash session\nOutput-Formats: [application/json]",
"responses": {
"200": {
"description": "Get a Cash session"
}
}
}
},
"/api/cash/zticket/{id}": {
"get": {
"summary": "GET cashZticketGet\nSummary:\nNotes: get a zticket of a cash session\nOutput-Formats: [application/json]",
"responses": {
"200": {
"description": "get a zticket of a cash session"
}
}
}
},
"/api/composition/getAll": {
"get": {
"summary": "GET compositionGetAllGet\nSummary:\nNotes: Get an array of all compositions\nOutput-Formats: [application/json]",
"responses": {
"200": {
"description": "An array of compositions"
}
}
}
},
"/api/discountprofile/getAll": {
"get": {
"summary": "GET discountprofileGetAllGet\nSummary:\nNotes: Get an array of all DiscountProfiles\nOutput-Formats: [application/json]",
"responses": {
"200": {
"description": "Get an array of all DiscountProfiles"
}
}
}
},
"/api/discountprofile/{id}": {
"get": {
"summary": "GET discountprofileIdGet\nSummary:\nNotes: Get a DiscountProfile\nOutput-Formats: [application/json]",
"responses": {
"200": {
"description": "Get a DiscountProfile"
}
}
}
},
"/api/image/category/{id}": {
"get": {
"summary": "GET imageCategoryIdGet\nSummary:\nNotes: get image of the category\nOutput-Formats: [image/jpeg]",
"responses": {
"200": {
"description": "get image of the payement mode"
}
}
}
},
"/api/image/payement_mode/{id}": {
"get": {
"summary": "GET imagePayementModeIdGet\nSummary:\nNotes: get image of the payement mode\nOutput-Formats: [image/png]",
"responses": {
"200": {
"description": "get image of the payement mode"
}
}
}
},
"/api/image/product/{id}": {
"get": {
"summary": "GET imageProductIdGet\nSummary:\nNotes: get image of the product\nOutput-Formats: [image/jpeg]",
"responses": {
"200": {
"description": "get image of the product"
}
}
}
},
"/api/image/ressources/{label}": {
"get": {
"summary": "GET imageRessourcesLabelGet\nSummary:\nNotes: get content of a ressource\nOutput-Formats: [image/jpeg]",
"responses": {
"200": {
"description": "get content of a ressource"
}
}
}
}
},
"definitions": {
"CashRegister": {
"properties": {
"id": {
"description": "ID of the cash register",
"type": "integer"
},
"reference": {
"description": "Code of the cash register, user-friendly ID.",
"type": "string"
},
"label": {
"description": "name of the cash register",
"type": "string"
},
"nextTicketId": {
"description": "Id (number) of the next Ticket. Starts to 1.",
"type": "integer",
"format": "int32"
},
"nextSessionId": {
"description": "Id (number) of the next session. Starts to 1.",
"type": "integer",
"format": "int32"
}
},
"type": "object"
},
"Category": {
"properties": {
"id": {
"description": "Internal ID of the category for performance issues.",
"type": "integer"
},
"reference": {
"description": "Code of the category, user-friendly ID.",
"type": "string"
},
"parent": {
"description": "Parent's category if any.",
"type": "integer"
},
"label": {
"description": "Label of the category",
"type": "string"
},
"image": {
"description": "Category image if any."
},
"dispOrder": {
"description": "order",
"type": "integer",
"format": "int32"
}
},
"type": "object"
},
"Product": {
"properties": {
"id": {
"description": "ID of the product",
"type": "integer"
},
"reference": {
"description": "Code of the product, user-friendly ID.",
"type": "string"
},
"barcode": {
"description": "barcode of a product",
"type": "string"
},
"label": {
"description": "name of a product",
"type": "string"
},
"priceBuy": {
"description": "Buy price without taxes, used for estimated margin computation.",
"type": "number",
"format": "float"
},
"priceSell": {
"description": "sell price without taxes.",
"type": "number",
"format": "float"
},
"visible": {
"description": "Is product currently in sale (visible on cash registers) ?",
"type": "boolean"
},
"scaled": {
"description": "Is the product sold by scale?\nisScale can be false with a SCALE_TYPE_WEIGHT (i.e. a box of 200g).\nWhen isScale is true, scaleValue is meaningless.",
"type": "boolean"
},
"scaleType": {
"description": "See SCALE_TYPE_* constants.\nUsed to compute reference prices like price per liter or kg.",
"type": "integer"
},
"scaleValue": {
"description": "The scale value for products referenced by weight or volume and\nnot soled by scale.",
"type": "number",
"format": "float"
},
"category": {
"description": "ID of the category",
"type": "integer"
},
"dispOrder": {
"description": "order",
"type": "integer",
"format": "int32"
},
"tax": {
"description": "ID of a tax",
"type": "integer"
},
"image": {
"description": "Product image if any."
},
"discountEnabled": {
"description": "Is discount currently enabled ?",
"type": "boolean"
},
"discountRate": {
"description": "rate of the discount",
"type": "number",
"format": "float"
}
},
"type": "object"
},
"Role": {
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"permissions": {
"description": "List of permissions. It is stored in database in a single string,\nvalues separated by ;. Used as an array once retreived from database."
}
},
"type": "object"
},
"Tax": {
"properties": {
"id": {
"description": "ID of the tax",
"type": "integer"
},
"label": {
"description": "Label of the tax",
"type": "string"
},
"rate": {
"description": "Rate of the tax",
"type": "number",
"format": "float"
}
},
"type": "object"
},
"User": {
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"password": {
"description": "User password.",
"type": "string"
},
"role": {
"description": "ID of the assigned role",
"type": "integer"
},
"active": {
"type": "boolean"
},
"image": {
"description": "User image if any."
},
"card": {
"description": "Code of User's card",
"type": "string"
}
},
"type": "object"
}
}