# ============================================================================
#  modules/ParsVT/ws/.htaccess
#  ParsVT Web Services — Apache configuration
#  Path: modules/ParsVT/ws/.htaccess
# =============================================================================
#
#  DESIGN CONSTRAINT
#  -----------------
#  This file deliberately contains NO Options or AcceptPathInfo directives.
#  Both are core directives, not module directives, which means:
#
#    * <IfModule> does not protect them. IfModule only tests whether a module
#      is loaded; the "not allowed here" failure comes from AllowOverride,
#      which is unrelated to module loading.
#
#    * When AllowOverride lacks Options / FileInfo, Apache rejects the ENTIRE
#      .htaccess file and the whole web service returns 500 — not just that
#      one line. Worst possible failure mode on customer installations.
#
#  Everything that remains below is wrapped in <IfModule> and is safe under
#  any AllowOverride setting.
#
#  If you do control the server, prefer httpd.conf over .htaccess:
#
#      <Directory ".../modules/ParsVT/ws">
#          AllowOverride Options FileInfo Limit
#          Options -MultiViews -Indexes +SymLinksIfOwnerMatch
#          AcceptPathInfo On
#      </Directory>
#
#  Replacement for "Options -Indexes": drop an index.php into this directory
#  that returns 403. Apache always prefers an existing index file over
#  generating a listing, and that needs no configuration at all.
#
# =============================================================================


# -----------------------------------------------------------------------------
#  1) HTTP Basic Auth under CGI / FastCGI / PHP-FPM
# -----------------------------------------------------------------------------
#  The Authorization header is not forwarded to PHP by default in CGI-style
#  SAPIs. Three independent mechanisms are used because no single one is
#  available on every install.
#
#  Note: CGIPassAuth is guarded by mod_version, which is frequently NOT loaded.
#  When that happens this block silently does nothing — which is exactly why
#  the SetEnvIf and RewriteRule fallbacks below must stay.
# -----------------------------------------------------------------------------

<IfModule mod_version.c>
    <IfVersion >= 2.4.13>
        CGIPassAuth on
    </IfVersion>
</IfModule>

<IfModule mod_setenvif.c>
    # Pattern is .+ rather than (.*) on purpose: (.*) also matches the empty
    # string and would set HTTP_AUTHORIZATION to an empty value, overwriting
    # a correct value set elsewhere.
    SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0
    SetEnvIf Accept "(.*)" HTTP_ACCEPT=$1
</IfModule>


# -----------------------------------------------------------------------------
#  2) CORS headers
# -----------------------------------------------------------------------------
#  SECURITY REVIEW NEEDED — current permissive behaviour is preserved.
#
#  "*" means any site on the internet may call this API and read the response.
#  Allow-Credentials is not set, so session cookies are never sent and an
#  attacker without an Access Key can do nothing. However, if a key is ever
#  exposed in third-party JavaScript, this header makes it exploitable from
#  any origin. A restricted variant is provided at the bottom of this file.
#
#  Allow-Headers previously listed only Authorization. Browsers reject
#  cross-origin requests carrying Content-Type: application/json or a custom
#  Accept header unless those headers are allowed here — this broke the
#  customer portal, which is cross-origin by design.
# -----------------------------------------------------------------------------

<IfModule mod_headers.c>
    Header unset Access-Control-Allow-Origin
    Header always unset Access-Control-Allow-Origin
    Header always set Access-Control-Allow-Origin "*"

    Header unset Access-Control-Allow-Methods
    Header always unset Access-Control-Allow-Methods
    Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"

    Header unset Access-Control-Allow-Headers
    Header always unset Access-Control-Allow-Headers
    Header always set Access-Control-Allow-Headers "Authorization, Content-Type, Accept, X-Requested-With, Content-Range, Content-Disposition"

    Header unset Access-Control-Max-Age
    Header always unset Access-Control-Max-Age
    Header always set Access-Control-Max-Age "86400"

    # Prevents intermediate caches from serving one origin's response to another
    Header always append Vary "Origin"
</IfModule>


# -----------------------------------------------------------------------------
#  3) Routing
# -----------------------------------------------------------------------------

<IfModule mod_rewrite.c>
    RewriteEngine On
    Options -MultiViews
    # -- 3.1 Authorization header passthrough (fallback for CGIPassAuth) ------
    RewriteCond %{HTTP:Authorization} .
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

    # -- 3.2 CORS preflight ---------------------------------------------------
    #  Without this, OPTIONS requests reach Api.php, arrive without an
    #  Authorization header, and are answered with 401 — causing the browser
    #  to reject the entire cross-origin request. Answer preflight before PHP.
    #  CORS headers above use "always", so they apply to this 204 as well.
    RewriteCond %{REQUEST_METHOD} =OPTIONS
    RewriteRule ^ - [R=204,L]

    # -- 3.3 Pass through real files, directories and symlinks ----------------
    #  This is what makes direct-file URLs work: a request for
    #  Api.php/V2/vtiger/default/ping resolves REQUEST_FILENAME to Api.php,
    #  which is a real file, so no rewriting is attempted and the remainder
    #  of the URL reaches PHP as PATH_INFO.
    RewriteCond %{REQUEST_FILENAME} -f [OR]
    RewriteCond %{REQUEST_FILENAME} -d [OR]
    RewriteCond %{REQUEST_FILENAME} -l
    RewriteRule ^ - [L]

    # -- 3.4 Dedicated front controllers --------------------------------------
    #  ORDER MATTERS. These three must precede the generic V1 rules below.
    #
    #  Previously the api rule sat at the very END of the file, after:
    #      RewriteRule ^([^/]+)/([^/]+)/?$ index.php?module=$1&token=$2 [L]
    #  which swallows any two-segment URL. A request for ws/api/ping was
    #  therefore routed to index.php?module=api&token=ping instead of Api.php.
    #  Real API calls have more than two segments and survived by accident.
    RewriteRule ^api/(.+)$    Api.php/$1    [QSA,L,NC]
    RewriteRule ^soap/(.+)$   soap.php/$1   [QSA,L,NC]
    RewriteRule ^graphql/?$   graphql.php   [QSA,L]

    # -- 3.4a MCP endpoint ----------------------------------------------------
    #  MUST come before the generic V1 rules in 3.10, and the first line MUST
    #  come before everything else in this group.
    #
    #  WHY THE FIRST LINE EXISTS
    #  Rule 3.3 only passes a request through when REQUEST_FILENAME is a real
    #  file. For "mcp.php/token" it is not: the path info makes the filename
    #  ".../ws/mcp.php/token", which does not exist on disk, so 3.3 does not
    #  match and the request falls through to
    #      RewriteRule ^([^/]+)/([^/]+)/?$ index.php?module=$1&token=$2
    #  in 3.10, which happily rewrites it to index.php?module=mcp.php&token=token.
    #  Every two-segment MCP path - mcp.php/token, mcp.php/authorize,
    #  mcp.php/register, mcp.php/revoke - breaks without this line, while
    #  three-segment paths such as the .well-known documents survive by accident.
    #  Api.php has the same latent problem; it has simply never been hit because
    #  real API calls always have more than two segments.
    RewriteRule ^mcp\.php(/.*)?$ - [L]

    #  Pretty form. Also required when the CRM root .htaccess forwards /mcp
    #  and /.well-known/* here.
    RewriteRule ^mcp/?$    mcp.php    [QSA,L,NC]
    RewriteRule ^mcp/(.+)$ mcp.php/$1 [QSA,L,NC]

    #  Portal endpoint: support both with and without path segments
    RewriteRule ^portal/?$    Portal.php    [QSA,L,NC]
    RewriteRule ^portal/(.+)$ Portal.php/$1 [QSA,L,NC]

    # -- 3.5 REST API V1 — authentication -------------------------------------
    RewriteRule ^auth/([^/]+)$          index.php?module=Auth&key=$1                [L,NC]
    RewriteRule ^auth/([^/]+)/([^/]+)$  index.php?module=Auth&login=$1&password=$2  [L,NC]

    # -- 3.6 REST API V1 — single record by id --------------------------------
    RewriteRule ^([^/]+)/([0-9]+)/([A-Za-z0-9]{32})/?$ index.php?module=$1&id=$2&token=$3 [L]

    # -- 3.7 REST API V1 — search ---------------------------------------------
    RewriteRule ^([^/]+)/criteria/([^/]+)/([A-Za-z0-9]{32})/?$       index.php?module=$1&criteria=$2&token=$3            [L]
    RewriteRule ^([^/]+)/criteria/([^/]+)/(.+)/([A-Za-z0-9]{32})/?$  index.php?module=$1&criteria=$2&params=$3&token=$4  [L]

    # -- 3.8 REST API V1 — picklist -------------------------------------------
    RewriteRule ^([^/]+)/picklist/([^/]+)/([A-Za-z0-9]{32})/?$ index.php?module=$1&picklist=$2&token=$3 [L]

    #  Two segment orders are accepted because code and documentation disagree.
    #
    #    Form A (accepted by the original rule):
    #        /{module}/picklist/picklistdep/{field}/{token}
    #
    #    Form B (documented in Readme.md, previously NOT matched — it fell
    #    through to the generic params rule):
    #        /{module}/picklist/{field}/picklistdep/1/{token}
    #
    #  Both are supported so integrations built from either source keep
    #  working. One of the two should eventually become the single source
    #  of truth and the other should be deprecated.
    RewriteRule ^([^/]+)/picklist/picklistdep/([^/]+)/([A-Za-z0-9]{32})/?$ index.php?picklistdep=1&module=$1&picklist=$2&token=$3 [L]
    RewriteRule ^([^/]+)/picklist/([^/]+)/picklistdep/([01])/([A-Za-z0-9]{32})/?$ index.php?picklistdep=$3&module=$1&picklist=$2&token=$4 [L]

    # -- 3.9 REST API V1 — module action --------------------------------------
    RewriteRule ^([^/]+)/action/([^/]+)/(.+)/([A-Za-z0-9]{32})/?$ index.php?module=$1&action=$2&params=$3&token=$4 [L]

    # -- 3.10 REST API V1 — generic fallbacks ---------------------------------
    #  Keep these last. Anything between the module name and the 32-character
    #  token is handed to index.php as params.
    RewriteRule ^([^/]+)/(.+)/([A-Za-z0-9]{32})/?$ index.php?module=$1&params=$2&token=$3 [L]
    RewriteRule ^([^/]+)/([^/]+)/?$                index.php?module=$1&token=$2          [L]

</IfModule>


# -----------------------------------------------------------------------------
#  4) Legacy ModSecurity v1
# -----------------------------------------------------------------------------
#  WARNING: this block disables the web application firewall for the entire
#  web service directory.
#
#  mod_security version 1 has been obsolete for years and is almost never
#  loaded on current servers, so in practice this block does nothing. But on
#  a server where it IS loaded, these two lines remove protection completely.
#
#  If a specific rule ever causes a false positive, disable that rule with
#  SecRuleRemoveById (ModSecurity 2+) instead of turning the engine off.
#
#  Recommendation: delete this block. Existing behaviour is preserved here
#  only to avoid changing the security posture of live installations without
#  an explicit decision.
# -----------------------------------------------------------------------------

<IfModule mod_security.c>
    SecFilterEngine Off
    SecFilterScanPOST Off
</IfModule>


# =============================================================================
#  APPENDIX A — Restricted CORS (recommended for production)
# =============================================================================
#  Replace the mod_headers block in section 2 with this and list your own
#  portal domains. Allow-Credentials becomes possible here because the origin
#  is no longer a wildcard.
#
#  <IfModule mod_headers.c>
#      SetEnvIf Origin "^https://(portal\.example\.com|app\.example\.com)$" PARSVT_CORS=$0
#      Header always set Access-Control-Allow-Origin      "%{PARSVT_CORS}e" env=PARSVT_CORS
#      Header always set Access-Control-Allow-Credentials "true"            env=PARSVT_CORS
#      Header always set Access-Control-Allow-Methods     "GET, POST, PUT, DELETE, OPTIONS"
#      Header always set Access-Control-Allow-Headers     "Authorization, Content-Type, Accept, X-Requested-With"
#      Header always set Access-Control-Max-Age           "86400"
#      Header always append Vary "Origin"
#  </IfModule>


# =============================================================================
#  APPENDIX B — Endpoint URL forms
# =============================================================================
#  Prefer the direct-file form. It works regardless of server configuration:
#
#      ws/Api.php/V2/vtiger/default/ping        instead of  ws/API/V2/...
#      ws/Portal.php/Ping                       instead of  ws/Portal/Ping
#      ws/soap.php/json                         (already in use)
#
#  Reason: mod_negotiation runs during map-to-storage, BEFORE mod_rewrite.
#  With MultiViews enabled and no "API" directory present, Apache looks for a
#  variant and — on case-insensitive filesystems such as Windows — matches
#  Api.php. Two outcomes follow:
#
#    * Client sent "Accept: application/json" (Swagger UI, browser fetch,
#      Power BI, Zapier): no variant matches that media type, so Apache
#      answers 406 Not Acceptable.
#
#    * Client sent "Accept: */*" (Postman, curl): negotiation succeeds and
#      Apache serves Api.php itself, so the ^api/(.+)$ rule never runs.
#
#  "Options -MultiViews" fixes both, but cannot be used here (see the header
#  of this file). The direct-file form sidesteps negotiation entirely because
#  the target is a real file. Pretty URLs still work wherever they worked
#  before; the rewrite rules are unchanged.


# =============================================================================
#  APPENDIX C — Verification
# =============================================================================
#
#  1. Endpoint reachable with an explicit Accept header (was 406):
#       curl -i -H "Accept: application/json" -u USER:ACCESSKEY \
#            "http://CRM/modules/ParsVT/ws/Api.php/V2/vtiger/default/ping"
#       Expect: 200 with a JSON body
#
#  2. CORS preflight answered without touching PHP:
#       curl -i -X OPTIONS -H "Origin: https://example.com" \
#            -H "Access-Control-Request-Method: POST" \
#            "http://CRM/modules/ParsVT/ws/Api.php/V2/vtiger/default/ping"
#       Expect: 204 with Access-Control-* headers
#
#  3. Customer portal:
#       curl -i -u PORTALUSER:PORTALPASS \
#            "http://CRM/modules/ParsVT/ws/Portal.php/Ping"
#
#  4. V1 and SOAP unchanged:
#       curl -i "http://CRM/modules/ParsVT/ws/auth/ACCESSKEY"
#       curl -i -X POST -H "Content-Type: text/xml" \
#            "http://CRM/modules/ParsVT/ws/soap/json"
#
#  5. No directory listing (requires the index.php guard described in the
#     file header, since Options -Indexes is not used):
#       curl -i "http://CRM/modules/ParsVT/ws/"
#       Expect: 403 or an index response, never a file list
#
#  6. MCP endpoint reachable and NOT swallowed by the V1 fallback rules:
#       curl -i -X POST -H "Content-Type: application/json" \
#            -d '{"jsonrpc":"2.0","id":1,"method":"ping"}' \
#            "http://CRM/modules/ParsVT/ws/mcp.php"
#       Expect: 401 with a WWW-Authenticate header containing resource_metadata
#
#  7. The two-segment path that rule 3.4a protects:
#       curl -i "http://CRM/modules/ParsVT/ws/mcp.php/.well-known/oauth-protected-resource"
#       curl -i -X POST "http://CRM/modules/ParsVT/ws/mcp.php/token"
#       Expect: JSON from the MCP router.
#       If you get an HTML page or a V1 API error instead, rule 3.4a is missing
#       or has been moved below 3.10.
#
# =============================================================================
