Skip to content

Instantly share code, notes, and snippets.

@agungsugiarto
Last active April 14, 2024 15:00
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save agungsugiarto/28ff3eaf8e64b5a87de85e7f5e2e6b58 to your computer and use it in GitHub Desktop.
Save agungsugiarto/28ff3eaf8e64b5a87de85e7f5e2e6b58 to your computer and use it in GitHub Desktop.
Dealing with CORS in CodeIgniter 3

1. Add a config file named cors.php in the application/config directory.

<?php

defined('BASEPATH') or exit('No direct script access allowed');

$config = [
    /*
     |--------------------------------------------------------------------------
     | Cross-Origin Resource Sharing (CORS) Configuration
     |--------------------------------------------------------------------------
     |
     | Here you may configure your settings for cross-origin resource sharing
     | or "CORS". This determines what cross-origin operations may execute
     | in web browsers. You are free to adjust these settings as needed.
     |
     | To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
     |
     */
    'allowed_methods' => ['*'],

    'allowed_origins' => ['*'],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => false,
];

Options

Option Description Default value
allowed_methods Matches the request method. ['*']
allowed_origins Matches the request origin. ['*']
allowed_origins_patterns Matches the request origin with preg_match. []
allowed_headers Sets the Access-Control-Allow-Headers response header. ['*']
exposed_headers Sets the Access-Control-Expose-Headers response header. []
max_age Sets the Access-Control-Max-Age response header. 0
supports_credentials Sets the Access-Control-Allow-Credentials header. false

The allowed_methods and allowed_headers options are case-insensitive.

You don't need to provide both allowed_origins and allowed_origins_patterns. If one of the strings passed matches, it is considered a valid origin. A wildcard in allowed_origins will be converted to a pattern.

If ['*'] is provided to allowed_methods, allowed_origins or allowed_headers all methods / origins / headers are allowed.

Note: Allowing a single static origin will improve cacheability.

2. Next, add the Cors class to the application/libraries folder and name the file Cors.php.

<?php

defined('BASEPATH') or exit('No direct script access allowed');

class Cors
{
    /** @var CI_Controller */
    protected $ci;

    /** @var string[]  */
    private array $allowedOrigins = [];
    /** @var string[] */
    private array $allowedOriginsPatterns = [];
    /** @var string[] */
    private array $allowedMethods = [];
    /** @var string[] */
    private array $allowedHeaders = [];
    /** @var string[] */
    private array $exposedHeaders = [];
    private bool $supportsCredentials = false;
    private ?int $maxAge = 0;

    private bool $allowAllOrigins = false;
    private bool $allowAllMethods = false;
    private bool $allowAllHeaders = false;

    public function __construct()
    {
        $this->ci = &get_instance();
        $this->ci->load->config('cors', true);

        $this->setOptions($this->ci->config->item('cors'));
    }

    public function handle()
    {
        // For Preflight, return the Preflight response
        if ($this->isPreflightRequest($this->ci->input)) {
            $this->handlePreflightRequest($this->ci->input);
            $this->varyHeader($this->ci->output, 'Access-Control-Request-Method');
        }

        if ($this->ci->input->method(true) === 'OPTIONS') {
            $this->varyHeader($this->ci->output, 'Access-Control-Request-Method');
        }

        if (!$this->ci->output->get_header('Access-Control-Allow-Origin')) {
            // Add the CORS headers to the Response
            $this->addActualRequestHeaders($this->ci->output, $this->ci->input);
        }
    }

    public function setOptions(array $options): void
    {
        $this->allowedOrigins = $options['allowed_origins'] ?? $this->allowedOrigins;
        $this->allowedOriginsPatterns = $options['allowed_origins_patterns'] ?? $this->allowedOriginsPatterns;
        $this->allowedMethods = $options['allowed_methods'] ?? $this->allowedMethods;
        $this->allowedHeaders = $options['allowed_headers'] ?? $this->allowedHeaders;
        $this->supportsCredentials = $options['supports_credentials'] ?? $this->supportsCredentials;

        $maxAge = $this->maxAge;
        if (array_key_exists('maxAge', $options)) {
            $maxAge = $options['maxAge'];
        } elseif (array_key_exists('max_age', $options)) {
            $maxAge = $options['max_age'];
        }
        $this->maxAge = $maxAge === null ? null : (int)$maxAge;

        $exposedHeaders =  $options['exposed_headers'] ?? $this->exposedHeaders;
        $this->exposedHeaders = $exposedHeaders === false ? [] : $exposedHeaders;

        $this->normalizeOptions();
    }

    private function normalizeOptions(): void
    {
        // Normalize case
        $this->allowedHeaders = array_map('strtolower', $this->allowedHeaders);
        $this->allowedMethods = array_map('strtoupper', $this->allowedMethods);

        // Normalize ['*'] to true
        $this->allowAllOrigins = in_array('*', $this->allowedOrigins);
        $this->allowAllHeaders = in_array('*', $this->allowedHeaders);
        $this->allowAllMethods = in_array('*', $this->allowedMethods);

        // Transform wildcard pattern
        if (!$this->allowAllOrigins) {
            foreach ($this->allowedOrigins as $origin) {
                if (strpos($origin, '*') !== false) {
                    $this->allowedOriginsPatterns[] = $this->convertWildcardToPattern($origin);
                }
            }
        }
    }

    /**
     * Create a pattern for a wildcard, based on Str::is() from Laravel
     *
     * @see https://github.com/laravel/framework/blob/5.5/src/Illuminate/Support/Str.php
     * @param string $pattern
     * @return string
     */
    private function convertWildcardToPattern($pattern)
    {
        $pattern = preg_quote($pattern, '#');

        // Asterisks are translated into zero-or-more regular expression wildcards
        // to make it convenient to check if the strings starts with the given
        // pattern such as "*.example.com", making any string check convenient.
        $pattern = str_replace('\*', '.*', $pattern);

        return '#^' . $pattern . '\z#u';
    }

    public function isCorsRequest(CI_Input $request): bool
    {
        return $request->get_request_header('Origin') !== null;
    }

    public function isPreflightRequest(CI_Input $request): bool
    {
        return $request->method(true) === 'OPTIONS' && $request->get_request_header('Access-Control-Request-Method');
    }

    public function handlePreflightRequest(CI_Input $request): CI_Output
    {
        /** @var CI_Output $response */
        $response = $this->ci->output;

        $response->set_status_header(204);

        return $this->addPreflightRequestHeaders($response, $request);
    }

    public function addPreflightRequestHeaders(CI_Output $response, CI_Input $request): CI_Output
    {
        $this->configureAllowedOrigin($response, $request);

        if ($response->get_header('Access-Control-Allow-Origin')) {
            $this->configureAllowCredentials($response);
            $this->configureAllowedMethods($response, $request);
            $this->configureAllowedHeaders($response, $request);
            $this->configureMaxAge($response);
        }

        return $response;
    }

    public function isOriginAllowed(CI_Input $request): bool
    {
        if ($this->allowAllOrigins === true) {
            return true;
        }

        $origin = $request->get_request_header('Origin');

        if (in_array($origin, $this->allowedOrigins)) {
            return true;
        }

        foreach ($this->allowedOriginsPatterns as $pattern) {
            if (preg_match($pattern, $origin)) {
                return true;
            }
        }

        return false;
    }

    public function addActualRequestHeaders(CI_Output $response, CI_Input $request): CI_Output
    {
        $this->configureAllowedOrigin($response, $request);

        if ($response->get_header('Access-Control-Allow-Origin')) {
            $this->configureAllowCredentials($response);
            $this->configureExposedHeaders($response);
        }

        return $response;
    }

    private function configureAllowedOrigin(CI_Output $response, CI_Input $request): void
    {
        if ($this->allowAllOrigins === true && !$this->supportsCredentials) {
            // Safe+cacheable, allow everything
            $response->set_header('Access-Control-Allow-Origin: *');
        } elseif ($this->isSingleOriginAllowed()) {
            // Single origins can be safely set
            $response->set_header(sprintf('Access-Control-Allow-Origin: %s', array_values($this->allowedOrigins)[0]));
        } else {
            // For dynamic headers, set the requested Origin header when set and allowed
            if ($this->isCorsRequest($request) && $this->isOriginAllowed($request)) {
                $response->set_header("Access-Control-Allow-Origin: {$request->get_request_header('origin')}");
            }

            $this->varyHeader($response, 'Origin');
        }
    }

    private function isSingleOriginAllowed(): bool
    {
        if ($this->allowAllOrigins === true || count($this->allowedOriginsPatterns) > 0) {
            return false;
        }

        return count($this->allowedOrigins) === 1;
    }

    private function configureAllowedMethods(CI_Output $response, CI_Input $request): void
    {
        if ($this->allowAllMethods === true) {
            $allowMethods = strtoupper($request->get_request_header('Access-Control-Request-Method'));
            $this->varyHeader($response, 'Access-Control-Request-Method');
        } else {
            $allowMethods = implode(', ', $this->allowedMethods);
        }

        $response->set_header("Access-Control-Allow-Methods: {$allowMethods}");
    }

    private function configureAllowedHeaders(CI_Output $response, CI_Input $request): void
    {
        if ($this->allowAllHeaders === true) {
            $allowHeaders = $request->get_request_header('Access-Control-Request-Headers');
            $this->varyHeader($response, 'Access-Control-Request-Headers');
        } else {
            $allowHeaders = implode(', ', $this->allowedHeaders);
        }
        $response->set_header("Access-Control-Allow-Headers: {$allowHeaders}");
    }

    private function configureAllowCredentials(CI_Output $response): void
    {
        if ($this->supportsCredentials) {
            $response->set_header('Access-Control-Allow-Credentials: true');
        }
    }

    private function configureExposedHeaders(CI_Output $response): void
    {
        if ($this->exposedHeaders) {
            $response->set_header(sprintf('Access-Control-Expose-Headers: %s', implode(', ', $this->exposedHeaders)));
        }
    }

    private function configureMaxAge(CI_Output $response): void
    {
        if ($this->maxAge !== null) {
            $response->set_header("Access-Control-Max-Age: {$this->maxAge}");
        }
    }

    public function varyHeader(CI_Output $response, string $header): CI_Output
    {
        if (!$response->get_header('Vary')) {
            $response->set_header("Vary: {$header}");
        } elseif (!in_array($header, explode(', ', $response->get_header('Vary')))) {
            $response->set_header("Vary: {$response->get_header('Vary')}, {$header}");
        }

        return $response;
    }
}

3. In the constructor of your controller, add this line of code.

<?php

class API_Controller extends CI_Controller
{
    public function __construct()
    {
        parent::__construct();

        $this->load->library('cors');
        $this->cors->handle();
    }
}

Reference

  1. Cross-Origin Resource Sharing (CORS)
  2. CORS for PHP (using the Symfony HttpFoundation)
  3. CORS Filter for CodeIgniter 4
@mdestafadilah
Copy link

cara penggunaan yang lebih spesifik gak ada pak? seperti hanya domain tertentu ya bisa akses API tersebut

@agungsugiarto
Copy link
Author

agungsugiarto commented Aug 17, 2023

cara penggunaan yang lebih spesifik gak ada pak? seperti hanya domain tertentu ya bisa akses API tersebut

@mdestafadilah bisa di setting melalui config corsnya pada bagian 'allowed_origins' => ['*'], contoh 'allowed_origins' => ['https://google.com', '*.google.com'],

@joko2020
Copy link

joko2020 commented Dec 2, 2023

ini nyoba cross domain , manggil login page... tetep nggak bisa ya kalo dimasukin ke Iframe

@sogrbilja
Copy link

The syntax private array $allowedOrigins = []; won't work in PHP 7.3.33. Typed properties were introduced in PHP 7.4. You should remove these.

@mdestafadilah
Copy link

The syntax private array $allowedOrigins = []; won't work in PHP 7.3.33. Typed properties were introduced in PHP 7.4. You should remove these.

thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment