OroPlatform API - Deleting Entity - API Testing - WSSE Authentication

Posted About 5 years ago. Visible to the public.

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:

Image

Confirmation before deletion:

Image

Testing the REST API

Generate API Key

Image

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.

Image

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;
    }
}
kiatng
Last edit
Over 4 years ago
kiatng
Posted by kiatng to Oro (2019-02-27 09:13)