Restler icon indicating copy to clipboard operation
Restler copied to clipboard

clean the baseUrl so restler can be not in root

Open timmit-nl opened this issue 3 years ago • 13 comments

I have noticed the baseUrl doens't work as I would expect. If baseUrl is /endpoint/rest/ and the explorer lives in "explorer" (so with path /endpoint/rest/explorer) the route can't be found, because it will do in router.php /endpoint/rest/explorer==explorer so it can't be found.

If you remove the baseUrl from the path it will work path /endpoint/rest/explorer wil be cleaned to explorer in in router.php explorer==explorer

timmit-nl avatar Sep 01 '21 12:09 timmit-nl

any change someone can review this and merge it into the code base?

timmit-nl avatar Oct 28 '21 14:10 timmit-nl

Let's discuss this here!

What is your use case? I could not understand your statement above

Once you provide your use case, let me come up with an example that makes it work without your fix. If I could not do that we can see how we can fix the issue

Agreed?

Arul- avatar Oct 30 '21 08:10 Arul-

First sorry for the delay:

Our use case is that we have an endpoint in our application. Our application is / and the endpoint lives in /endpoint/ the rest endpoint lives in /endpoint/rest/ (and the soap in /endpoint/soap/). As we really like the integration and api view and security this package provides: we are embeding your package so it is only called on /endpoint/rest/ by our application.

As we can set an baseUrl (so you can let the package live not in / but in /endpoint/rest/) the path's are not "cleaned" against the baseUrl.

We are initation restler:

                Defaults::$throttle = 20; //time in milliseconds for bandwidth throttling
                $cacheDir = Config::$dirs['tmp'] . "/restler/";
                if (!file_exists($cacheDir)) {
                    mkdir($cacheDir);
                }

                Defaults::setProperty('cacheDirectory', $cacheDir);
                Defaults::setProperty('apiAccessLevel', 0);

                $productionMode = false;
                if (strtolower(Config::getEnv()) != 'dev') {
                    $productionMode = true;
                }

                $Restler = new Restler($productionMode);
                Restler::addListener('onRespond', function () {
                    header('X-Powered-By: Mosquito Framework');
                });
                $Restler->setBaseUrls(Router::generateUrlFromHostName() . Router::addVars2activePath(array('rest')));
                $Restler->addAPIClass('\\Luracast\\Restler\\Explorer\\v2\\Explorer', 'explorer'); //this creates resources.json at API Root
                $Restler->addFilterClass('\\Luracast\\Restler\\Filter\\RateLimit'); //Add Filters as needed

                foreach ($routes as $key => $className) {
                    preg_match_all("/\{vars\[[\d]\]\}/", $className, $varsA);
                    foreach ($varsA[0] as $var) {
                        $index = ((int)str_replace(array('{vars[', ']}'), array('', ''), $var) + $offset);
                        if (!empty($vars[$index])) {
                            $className = str_replace($var, $vars[$index], $class);
                        }
                    }
                    if (class_exists($className)) {
                        $Restler->addAPIClass($className, $key);
                    }
                }
                $Restler->addAPIClass(__NAMESPACE__ . '\\Endpoint\\Auth', 'auth');

                $Restler->addAuthenticationClass(__NAMESPACE__ . '\\Endpoint\\AccessControl');
                $Restler->handle();

then the path isn't reconised because 'sims' != '/endpoint/rest/sims'

So this fix will '/endpoint/rest/sims' convert to sims because the basepath is Router::generateUrlFromHostName() . Router::addVars2activePath(array('rest')) and that generated /endpoint/rest/. So /endpoint/rest/sims 'minus' /endpoint/rest/ is sims

So this fixes makes that if you use $Restler->setBaseUrls() it now works correctly. hope this clarifies more ;-)

timmit-nl avatar Nov 22 '21 09:11 timmit-nl

You are using Apache server or Nginx?

Arul- avatar Nov 23 '21 00:11 Arul-

Both. That is for the user of our simpel inhouse framework, mostly it is apache. But we write everything so it is compatible with both.

timmit-nl avatar Nov 23 '21 10:11 timmit-nl

Ok I have an possible other fix:

If we can set the path from our end and the getPath uses that as the input for the $path instead of the

        list($base, $path) = Util::splitCommonPath(
            strtok(urldecode($_SERVER['REQUEST_URI']), '?'), //remove query string
            $_SERVER['SCRIPT_NAME']
        );

then a user can bypass the hole $_SERVER part, because it can feed that to Restler.

So an simple setPath() that sets the $this->path

and the getPath begin that changes from:

    /**
     * Parses the request url and get the api path
     *
     * @return string api path
     */
    protected function getPath()
    {
        // fix SCRIPT_NAME for PHP 5.4 built-in web server
        if (false === strpos($_SERVER['SCRIPT_NAME'], '.php'))
            $_SERVER['SCRIPT_NAME']
                = '/' . substr($_SERVER['SCRIPT_FILENAME'], strlen($_SERVER['DOCUMENT_ROOT']) + 1);

        list($base, $path) = Util::splitCommonPath(
            strtok(urldecode($_SERVER['REQUEST_URI']), '?'), //remove query string
            $_SERVER['SCRIPT_NAME']
        );

to

    /**
     * Parses the request url and get the api path
     *
     * @return string api path
     */
    protected function getPath()
    {
        if (isset($this->path) && is_string($this->path)) {
            $base = '';
            $path = $this->path;
        } else {
            // fix SCRIPT_NAME for PHP 5.4 built-in web server
            if (false === strpos($_SERVER['SCRIPT_NAME'], '.php'))
                $_SERVER['SCRIPT_NAME']
                    = '/' . substr($_SERVER['SCRIPT_FILENAME'], strlen($_SERVER['DOCUMENT_ROOT']) + 1);

            list($base, $path) = Util::splitCommonPath(
                strtok(urldecode($_SERVER['REQUEST_URI']), '?'), //remove query string
                $_SERVER['SCRIPT_NAME']
            );
        }

this way, you can use possible more systems then apache/nginx if you feed restler on your own and correctly....

timmit-nl avatar Dec 02 '21 13:12 timmit-nl

Here is an example that is tested in my localhost endpoint.zip under Apache webserver. Unzip the content under webroot and try it out

Screenshot 2021-12-02 at 10 22 43 PM

It shows BMI example API under /endpoint/rest/BMI

Hope this helps

Arul- avatar Dec 02 '21 14:12 Arul-

No that doesn't help.

Let me explain furter:

/endpoint/rest/ is in your example handled by the script /endpoint/rest/index.php. So restler sees the basepath /endpoint/rest/

But with our code the /endpoint/rest/ is handled by: /index.php but also a normal page like /dashboard/ is handled by the same code.

So /endpoint/rest/ will call the script below because our internal router will call only that part of the script when on /endpoint/rest/ if it is /endpoint/soap/ it will initiate an soap server and so on.

Defaults::$throttle = 20; //time in milliseconds for bandwidth throttling
                $cacheDir = Config::$dirs['tmp'] . "/restler/";
                if (!file_exists($cacheDir)) {
                    mkdir($cacheDir);
                }

                Defaults::setProperty('cacheDirectory', $cacheDir);
                Defaults::setProperty('apiAccessLevel', 0);

                $productionMode = false;
                if (strtolower(Config::getEnv()) != 'dev') {
                    $productionMode = true;
                }

                $Restler = new Restler($productionMode);
                Restler::addListener('onRespond', function () {
                    header('X-Powered-By: Mosquito Framework');
                });
                $Restler->setBaseUrls(Router::generateUrlFromHostName() . Router::addVars2activePath(array('rest')));
                $Restler->addAPIClass('\\Luracast\\Restler\\Explorer\\v2\\Explorer', 'explorer'); //this creates resources.json at API Root
                $Restler->addFilterClass('\\Luracast\\Restler\\Filter\\RateLimit'); //Add Filters as needed

                foreach ($routes as $key => $className) {
                    preg_match_all("/\{vars\[[\d]\]\}/", $className, $varsA);
                    foreach ($varsA[0] as $var) {
                        $index = ((int)str_replace(array('{vars[', ']}'), array('', ''), $var) + $offset);
                        if (!empty($vars[$index])) {
                            $className = str_replace($var, $vars[$index], $class);
                        }
                    }
                    if (class_exists($className)) {
                        $Restler->addAPIClass($className, $key);
                    }
                }
                $Restler->addAPIClass(__NAMESPACE__ . '\\Endpoint\\Auth', 'auth');

                $Restler->addAuthenticationClass(__NAMESPACE__ . '\\Endpoint\\AccessControl');
                $Restler->handle();

Do you understand it now? We are not looking to add an fysical folder in the webroot with an custom index.php. Our webroot only contains /index.php and an .htaccess.

timmit-nl avatar Dec 02 '21 15:12 timmit-nl

If you want the index.php in the webroot, you can add the API as follows

<?php

require_once '../vendor/autoload.php';

use Luracast\Restler\Restler;

$r = new Restler();
$r->addAPIClass('BMI', 'endpoint/rest/bmi');
$r->handle();

I would strongly recommend the above method instead using symlinked public folder as a subfolder in the webroot. This gives the possibility of

  • keeping each app sleek
  • different framework / language for each app

Arul- avatar Dec 02 '21 15:12 Arul-

If I do it like you says, the explorer is completly empty: (but the rest api will work)

{
    "swagger": "2.0",
    "host": "tim.dev.tool.nl",
    "basePath": "",
    "produces": [
        "application/json"
    ],
    "consumes": [
        "application/json"
    ],
    "paths": [],
    "definitions": {},
    "securityDefinitions": {
        "api_key": {
            "type": "apiKey",
            "name": "api_key",
            "in": "query"
        }
    },
    "info": {
        "version": "1",
        "title": "Restler API Explorer",
        "description": "Live API Documentation",
        "contact": {
            "name": "Restler Support",
            "email": "[email protected]",
            "url": "luracast.com/products/restler"
        },
        "license": {
            "name": "LGPL-2.1",
            "url": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html"
        }
    }
}

location of json: https://tim.dev.tool.nl/endpoint-v2/rest/explorer/swagger.json explorer lives at: https://tim.dev.tool.nl/endpoint-v2/rest/explorer/

and the endpoint lives at: https://tim.dev.tool.nl/endpoint-v2/rest/

We are NOT symlinking. As those would not survive git correctly so that is not an option. We have an internal php router that simply does: when url contains at the beginning: /endpoint-v2/rest/

run the class with the code above in previous posting.

In my opinion I am now poluting restler routes with endpoint-v2/rest/ as all restler routes will start with that. And the baseUrl is in fact the hostname with port/http scheme... But in my humble opinion a baseUrl is more then a hostname+scheme. The baseUrl is: https://tim.dev.tool.nl/endpoint-v2/rest/ and so the routes are clean.

(And we need the explorer for our customers! ;-))

timmit-nl avatar Dec 03 '21 08:12 timmit-nl

also with my fix the explorer is correctly populated with the correct info, so you can also use an external swagger:

{
    "swagger": "2.0",
    "host": "tim.dev.tool.nl",
    "basePath": "/endpoint-v2/rest",
    "produces": [
        "application/json"
    ],
    "consumes": [
        "application/json"
    ],
    "paths": {
        "/say/hello": {
            "post": {
                "operationId": "sayHello",
                "tags": [
                    "say"
                ],
                "parameters": [
                    {
                        "name": "sayHelloModel",
                        "description": "**session** (required)  \nto  \n",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/sayHelloModel"
                        }
                    }
                ],
                "summary": "hello 🔐",
                "description": "",
                "responses": {
                    "200": {
                        "description": "Success",
                        "schema": {
                            "type": "string"
                        }
                    }
                },
                "security": [
                    {
                        "api_key": []
                    }
                ]
            }
        },
        "/say/hi": {
            "get": {
                "operationId": "sayHi",
                "tags": [
                    "say"
                ],
                "parameters": [
                    {
                        "name": "to",
                        "type": "string",
                        "description": "",
                        "in": "query",
                        "required": true
                    }
                ],
                "summary": "hi ◑",
                "description": "",
                "responses": {
                    "200": {
                        "description": "Success",
                        "schema": {
                            "type": "string"
                        }
                    }
                },
                "security": [
                    {
                        "api_key": []
                    }
                ]
            }
        },
        "/auth/inloggen": {
            "post": {
                "operationId": "authCreateInloggen",
                "tags": [
                    "auth"
                ],
                "parameters": [
                    {
                        "name": "authCreateInloggenModel",
                        "description": "**username** (required)  \n**password** (required)  \n",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/authCreateInloggenModel"
                        }
                    }
                ],
                "summary": "Functie om in te loggen op de webservice 🔓",
                "description": "",
                "responses": {
                    "200": {
                        "description": "$session",
                        "schema": {
                            "type": "string"
                        }
                    }
                }
            }
        }
    },
    "definitions": {
        "sayHelloModel": {
            "properties": {
                "session": {
                    "type": "string",
                    "description": ""
                },
                "to": {
                    "type": "string",
                    "description": "",
                    "defaultValue": "world"
                }
            },
            "required": [
                "session"
            ]
        },
        "authCreateInloggenModel": {
            "properties": {
                "username": {
                    "type": "string",
                    "description": ""
                },
                "password": {
                    "type": "string",
                    "description": ""
                }
            },
            "required": [
                "username",
                "password"
            ]
        }
    },
    "securityDefinitions": {
        "api_key": {
            "type": "apiKey",
            "name": "api_key",
            "in": "query"
        }
    },
    "info": {
        "version": "1",
        "title": "API Explorer",
        "description": "Live API Documentation",
        "contact": {
            "name": "tool.nl",
            "email": "[email protected]",
            "url": "https://tim.dev.tool.nl"
        },
        "license": {
            "name": "",
            "url": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html"
        }
    }
}

as you can see the basePath is correctly filled. I can even execute from the external swagger to our server. We feed Restlker with this populated BaseUrl: $Restler->setBaseUrls('https://tim.dev.tool.nl/endpoint-v2/rest');

As the explorer and swagger are correctly populated, I am more certain that our fix is the correct one.

timmit-nl avatar Dec 03 '21 08:12 timmit-nl

Hi Arul,

Can you please make the suggested change and merge this?

I hate it when we are on p[roduction with an temparly forked version because of an merge that is stall

timmit-nl avatar Jan 20 '22 11:01 timmit-nl

Hi Arul,

Sorry for the ping, but had you time to check this merge?

timmit-nl avatar Mar 02 '22 15:03 timmit-nl