OroPlatform API - Deleting Entity - API Testing - WSSE Authentication
In the Creating a Simple CRUD Show archive.org snapshot cookbook, it uses REST API to delete entities Show archive.org snapshot . However, the description isn't enough to properly implement the delete operation.
However, there are examples on how to do it in the \vendor\oro\platform
codebase. Reference vendor\oro\platform\src\Oro\Bundle\OrganizationBundle\
on coding for REST API. The files of interest in the bundle are:
Resources\config\oro\routing.yml
Controller\Api\Rest\BusinessUnitController.php
Resources\config\services.yml
Handler\BusinessUnitDeleteHandler.php
So, I can continue with what I have done in Cookbook: Creating a Simple CRUD.
Reinstate the Delete Button
In the twig files:
src\InventoryBundle\Resources\views\Vehicle\update.html.twig
src\InventoryBundle\Resources\views\Vehicle\view.html.twig
Remove 0 and
from the if
statement, eg: {% if 0 and form.vars.value.id and is_granted('DELETE', form.vars.value) %}
.
In src\InventoryBundle\Resources\config\oro\datagrids.yml
, uncomment the data_link
property.
Register the route for API
In the config file src\InventoryBundle\Resources\config\oro\routing.yml
, add the API routing info inventory_api
:
inventory_bundle:
resource: "@InventoryBundle/Controller"
type: annotation
prefix: /inventory
inventory_api:
resource: "@InventoryBundle/Controller/Api/Rest/VehicleController.php"
type: rest
prefix: api/rest/{version}/
requirements:
version: latest|v1
_format: json
defaults:
version: latest
Create the API Controller
Add the file src\InventoryBundle\Controller\Api\Rest\VehicleController.php
:
<?php
namespace InventoryBundle\Controller\Api\Rest;
use FOS\RestBundle\Controller\Annotations\NamePrefix;
use FOS\RestBundle\Controller\Annotations\QueryParam;
use FOS\RestBundle\Controller\Annotations\RouteResource;
use FOS\RestBundle\Routing\ClassResourceInterface;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Oro\Bundle\SecurityBundle\Annotation\Acl;
use Oro\Bundle\SecurityBundle\Annotation\AclAncestor;
use Oro\Bundle\SoapBundle\Controller\Api\Rest\RestController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use InventoryBundle\Entity\Vehicle;
/**
* @RouteResource("vehicle")
* @NamePrefix("inventory_api_")
*/
class VehicleController extends RestController implements ClassResourceInterface
{
/**
* REST GET list
*
* @QueryParam(
* name="page",
* requirements="\d+",
* nullable=true,
* description="Page number, starting from 1. Defaults to 1."
* )
* @QueryParam(
* name="limit",
* requirements="\d+",
* nullable=true,
* description="Number of items per page. defaults to 10."
* )
* @ApiDoc(
* description="Get all vehicles items",
* resource=true,
* filters={
* {"name"="page", "dataType"="integer"},
* {"name"="limit", "dataType"="integer"}
* }
* )
* @AclAncestor("inventory.vehicle_view")
* @param Request $request
* @return Response
*/
public function cgetAction(Request $request)
{
$page = (int)$request->get('page', 1);
$limit = (int)$request->get('limit', self::ITEMS_PER_PAGE);
return $this->handleGetListRequest($page, $limit);
}
/**
* Create new vehicle
*
* @ApiDoc(
* description="Create new vehicle",
* resource=true
* )
* @AclAncestor("inventory.vehicle_create")
*/
public function postAction()
{
return $this->handleCreateRequest();
}
/**
* REST PUT
*
* @param int $id Business unit item id
*
* @ApiDoc(
* description="Update vehicle",
* resource=true
* )
* @AclAncestor("inventory.vehicle_update")
* @return Response
*/
public function putAction($id)
{
return $this->handleUpdateRequest($id);
}
/**
* REST GET item
*
* @param string $id
*
* @ApiDoc(
* description="Get vehicle item",
* resource=true
* )
* @AclAncestor("inventory.vehicle_view")
* @return Response
*/
public function getAction($id)
{
return $this->handleGetRequest($id);
}
/**
* {@inheritdoc}
*/
protected function transformEntityField($field, &$value)
{
switch ($field) {
case 'vehicle':
if ($value) {
/** @var Vehicle $value */
$value = array(
'id' => $value->getId(),
'name' => $value->getName(),
);
}
break;
case 'car':
if ($value) {
$value = array(
'id' => $value->getId(),
'name' => $value->getName()
);
}
break;
default:
parent::transformEntityField($field, $value);
}
}
/**
* {@inheritdoc}
*/
protected function getPreparedItem($entity, $resultFields = [])
{
$result = parent::getPreparedItem($entity);
//unset($result['users']);
return $result;
}
/**
* REST DELETE
*
* @param int $id
*
* @ApiDoc(
* description="Delete vehicle",
* resource=true
* )
* @Acl(
* id="inventory.vehicle_delete",
* type="entity",
* class="InventoryBundle:Vehicle",
* permission="DELETE"
* )
* @return Response
*/
public function deleteAction($id)
{
return $this->handleDeleteRequest($id);
}
/**
* {@inheritdoc}
*/
public function getManager()
{
return $this->get('inventory.vehicle.manager.api');
}
/**
* {@inheritdoc}
*/
public function getForm()
{
return $this->get('inventory.form.vehicle.api');
}
/**
* {@inheritdoc}
*/
public function getFormHandler()
{
return $this->get('inventory.form.handler.vehicle.api');
}
/**
* {@inheritdoc}
*/
protected function getDeleteHandler()
{
return $this->get('inventory.vehicle.handler.delete');
}
}
Register the Services to Handle API and Deletion
Create the config file src\InventoryBundle\Resources\config\services.yml
:
parameters:
inventory.vehicle.entity.class: InventoryBundle\Entity\Vehicle
inventory.vehicle.manager.api.class: Oro\Bundle\SoapBundle\Entity\Manager\ApiEntityManager
services:
#
# Vehicle API
#
inventory.vehicle.manager.api:
class: '%inventory.vehicle.manager.api.class%'
parent: oro_soap.manager.entity_manager.abstract
arguments:
- '%inventory.vehicle.entity.class%'
- '@doctrine.orm.entity_manager'
#
# Entity config dumper extension
#
inventory.vehicle.handler.delete:
class: InventoryBundle\Handler\VehicleDeleteHandler
parent: oro_soap.handler.delete.abstract
And we also need to load it in src\InventoryBundle\DependencyInjection\InventoryExtension.php
:
<?php
namespace InventoryBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
class InventoryExtension extends Extension
{
/**
* {@inheritDoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('form.yml');
$loader->load('services.yml');
}
}
Create the Delete Handler
Create the handler file src\InventoryBundle\Handler\VehicleDeleteHandler.php
:
<?php
namespace InventoryBundle\Handler;
use Doctrine\Common\Persistence\ObjectManager;
use Oro\Bundle\SecurityBundle\Exception\ForbiddenException;
use Oro\Bundle\SoapBundle\Handler\DeleteHandler;
class VehicleDeleteHandler extends DeleteHandler
{
/**
* {@inheritdoc}
*/
protected function checkPermissions($entity, ObjectManager $em)
{
parent::checkPermissions($entity, $em);
$repository = $em->getRepository('InventoryBundle:Vehicle');
if (0&&$repository->getVehiclesCount() <= 1) {
throw new ForbiddenException('Unable to remove the last vehicle');
}
}
}
Note that I disable the ability the delete the last vehicle.
That's all. It's important to refresh the cache in bash shell: php bin/console cache:clear
Delete button is rendered:
Confirmation before deletion:
Testing the REST API
Generate API Key
Generate WSSE Header in Bash Shell
The command to generate WSSE header in bash is
php bin/console oro:wsse:generate-header {api_key}
Example:
kiat@win10 MINGW64 /d/Work/wamp64/www/oro/platform
$ php bin/console oro:wsse:generate-header 39cc1cadcd7baccd5cc899b1f3b2edf0fe0c2c0f
To use WSSE authentication add following headers to the request:
Authorization: WSSE profile="UsernameToken"
X-WSSE: UsernameToken Username="admin", PasswordDigest="4fUg5ttcXQJ1ytT23JrhP6GqYEQ=", Nonce="NGY5ZWVmMDdlZmM2OWNjMg==", Created="2019-02-27T10:12:13+00:00"
Install Chrome Extension Restlet Client in Chrome
Restlet Client is designed and developed by developers for developers to make REST API testing and automation easy. Our mission is to bring you the best visual tool to increase your productivity by letting you focus on what to do instead of how to do it.
WSSE Authentication Show archive.org snapshot
API Rest Server
# src\Google\Bundle\VisionBundle\Resources\config\oro\bundles.yml
bundles:
- Google\Bundle\VisionBundle\GoogleVisionBundle
# src\Google\Bundle\VisionBundle\Resources\config\services.yml
services:
Google\Bundle\VisionBundle\Util\Ocr:
autowire: true
Google\Bundle\VisionBundle\Controller\OcrController:
tags: ['controller.service_arguments']
Google\Bundle\VisionBundle\Controller\Api\Rest\OcrController:
tags: ['controller.service_arguments']
# src\Google\Bundle\VisionBundle\Resources\config\oro\routing.yml
google_vision_bundle:
resource: "@GoogleVisionBundle/Controller"
type: annotation
google_vision_api:
resource: "@GoogleVisionBundle/Controller/Api/Rest" # to specify specific file, append '/OcrController.php'
type: rest
prefix: api/rest/{version}/
requirements:
version: latest|v1
_format: json
defaults:
version: latest
# src\Google\Bundle\VisionBundle\Controller\Api\Rest\OcrController.php
<?php
namespace Google\Bundle\VisionBundle\Controller\Api\Rest;
use FOS\RestBundle\Controller\Annotations\NamePrefix;
use FOS\RestBundle\Controller\Annotations\RouteResource;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Oro\Bundle\SecurityBundle\Annotation\AclAncestor;
use Symfony\Component\HttpFoundation\JsonResponse;
use Google\Bundle\VisionBundle\Util\Ocr;
/**
* @RouteResource("googleocr", pluralize=false)
* @NamePrefix("google_vision_ocr_api_")
*/
class OcrController
{
/**
* REST GET text from an image using Google Vision
*
* @param string $base64Path filename of the image file or URL to the image file
* @ApiDoc(
* description="Get text from an image using Google Vision",
* resource=true
* )
* @AclAncestor("google_vision_ocr_api.get")
* @return JsonResponse
*/
public function getAction($base64Path, Ocr $ocr)
{
$t2 = microtime(true);
$path = base64_decode($base64Path);
$output = $ocr->image($path);
$response = [
'path' => $path,
'tat' => microtime(true) - $t2,
'output' => $output
];
return new JsonResponse($response);
}
}
[kiat@reporting misoro]$ sudo -u nginx php bin/console cache:clear
// Clearing the cache for the dev environment with debug
// true
[OK] Cache for the "dev" environment (debug=true) was successfully cleared.
[kiat@reporting misoro]$ sudo -unginx php bin/console debug:router google
Select one of the matching routes:
[0] oro_user_google_login
[1] oro_sso_google_login
[2] google_vision_ocr_pdf
[3] google_vision_ocr_image
[4] google_vision_ocr_detect
[5] google_vision_ocr_api_get_googleocr
> 5
+--------------+---------------------------------------------------------------------------------------------------+
| Property | Value |
+--------------+---------------------------------------------------------------------------------------------------+
| Route Name | google_vision_ocr_api_get_googleocr |
| Path | /api/rest/{version}/googleocr/{base64Path}.{_format} |
| Path Regex | #^/api/rest/(?P<version>latest|v1)/googleocr/(?P<base64Path>[^/\.]++)(?:\.(?P<_format>json))?$#sD |
| Host | ANY |
| Host Regex | |
| Scheme | ANY |
| Method | GET |
| Requirements | _format: json |
| | version: latest|v1 |
| Class | Symfony\Component\Routing\Route |
| Defaults | _controller: Google\Bundle\VisionBundle\Controller\Api\Rest\OcrController:getAction |
| | _format: json |
| | version: latest |
| Options | compiler_class: Symfony\Component\Routing\RouteCompiler |
+--------------+---------------------------------------------------------------------------------------------------+
[kiat@reporting misoro]$
API Rest Client
<?php
/**
* ocr Module
*
* @category Sxxm
* @package Sxxm_Ocr
* @copyright Copyright (c) 2019 Ng Kiat Siong
* @author Ng Kiat Siong, Celera eShop, kiatsiong.ng@gmail.com
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/
class Sxxm_Ocr_Helper_Data extends Mage_Core_Helper_Abstract
{
/**
* @param string eg: index_dev.php/api/rest/latest/googleocr/aGVsbG8=.json
* @return string
*/
public function curl($uri)
{
$url = 'http://oro.mis.sc/'.$uri;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->_getWsseHeader());
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FAILONERROR, true);
//curl_setopt($ch, CURLOPT_VERBOSE, 1);
$result = curl_exec($ch);
if ($result === false) {
$result = curl_error($ch);
}
curl_close($ch);
return $result;
}
/**
* The generated header has a lifetime of 3600s and it expires if not used during this time.
* Each nonce might be used only once in specific time for generation of the password digest.
* By default, the nonce cooldown time is also set to 3600s.
* This rule is aimed to improve safety of the application and prevent “replay” attacks.
*
* @return string
*/
protected function _getWsseHeader()
{
$userName = 'kiat';
$userApiKey = 'some_string'; // generate from Oro Platform
$nonce = base64_encode(substr(md5(uniqid()), 0, 16));
$created = date('c');
$digest = base64_encode(sha1(base64_decode($nonce) . $created . $userApiKey, true));
$wsseProfile = sprintf(
'X-WSSE: UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"',
$userName,
$digest,
$nonce,
$created
);
return [
'Authorization: WSSE profile="UsernameToken"',
$wsseProfile
];
}
}
# src\Google\Bundle\VisionBundle\Util\Ocr.php
<?php
namespace Google\Bundle\VisionBundle\Util;
use Google\Cloud\Vision\V1\ImageAnnotatorClient;
use Google\Cloud\Vision\V1\EntityAnnotation;
class Ocr
{
/**
* Detect text in image
*
*/
public function image(string $filename): array
{
$content = file_get_contents($filename);
if (!$content) {
return ['error' => 'invalid path: '.$filename];
}
putenv('GOOGLE_APPLICATION_CREDENTIALS='.__DIR__.'/keyfile.json');
return $this->textDetect($content);
}
/**
* Feature TEXT_DETECTION
*/
protected function textDetect($content): array
{
$imageAnnotator = new ImageAnnotatorClient();
$response = $imageAnnotator->textDetection($content);
$texts = $response->getTextAnnotations();
$imageAnnotator->close();
$output = [];
/** @var EntityAnnotation $text */
foreach ($texts as $text) {
$output[] = $text->getDescription();
}
return $output;
}
}