# htaccess.conf Elimination — Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Eliminate `libraries/htaccess.conf` as a template file and move all remaining hardcoded URL routes into `pp_routes`, leaving only true Apache-level directives in the generated `.htaccess`. **Architecture:** `Helpers::htacces()` generates the full `.htaccess` content from PHP strings instead of loading a template. All URL→PHP mappings (static system routes + dynamic per language/producer) are inserted into `pp_routes` with `type='system'`, deleted and reinserted on every `htacces()` call. Apache-level rules (HTTPS redirect, admin routing, thumb.php) stay in `.htaccess` only. **Tech Stack:** PHP 7.4, Medoo ORM (`$mdb`), Redis (CacheHandler), PHPUnit 9.6 --- ## Context ### Current `Helpers::htacces()` structure (before this plan) 1. Loads `libraries/htaccess.conf` template (contains many hardcoded URL routes) 2. Appends language switch rules to `$htaccess_data` 3. Appends newsletter and producer rules to `$htaccess_data` 4. Inserts category/product/page/article routes into `pp_routes` (done in v0.329) 5. Replaces `{HTACCESS_CACHE}` placeholder 6. Appends catch-all, writes files ### What stays in `.htaccess` after this plan - `RewriteEngine On`, `RewriteBase /`, `Options` - www→https redirect - http→https redirect (with tpay/przelewy24/hotpay exclusion) - Trailing slash removal (excluding `/admin/`) - Admin routing: `^admin/([^/]*)/([^/]*)/(.*)$` - `^admin/$` - Thumbnail: `^thumb/([0-9]*)/([0-9]*)/(.*)$` → `/libraries/thumb.php` (different PHP file, cannot use pp_routes) - `THE_REQUEST` index.php redirect - Cache headers block (gzip/expires or no-cache based on `$settings['htaccess_cache']`) - File protection: ``, ``, `` - Start page 301 redirects (generated dynamically in pages loop) - Custom htaccess from `pp_settings` (param=htaccess) - Catch-all: `RewriteRule ^ index.php [L]` ### New `type` column in `pp_routes` - `NULL` = entity route (product/category/page/article) - `'system'` = system route (all routes in this plan) - On every `htacces()` call: `DELETE WHERE type='system'`, then reinsert all --- ## Task 1: Update SQL migration — add `type` column **Files:** - Modify: `migrations/0.329.sql` **Step 1: Add `type` column to the migration** Open `migrations/0.329.sql` (currently has 4 lines). Append the `type` column: ```sql ALTER TABLE pp_routes ADD COLUMN category_id INT NULL AFTER product_id, ADD COLUMN page_id INT NULL AFTER category_id, ADD COLUMN article_id INT NULL AFTER page_id, ADD COLUMN type VARCHAR(20) NULL AFTER article_id; ``` **Step 2: Apply migration on server** Run on the production/staging database: ```sql ALTER TABLE pp_routes ADD COLUMN type VARCHAR(20) NULL AFTER article_id; ``` (The other 3 columns from 0.329 should already be applied from the previous deployment.) **Step 3: No test needed** — pure schema change, verified when routes are inserted in Task 2. --- ## Task 2: Refactor `Helpers::htacces()` — replace template + move all routes to pp_routes **Files:** - Modify: `autoload/Shared/Helpers/Helpers.php` (method `htacces()`, lines ~408–773) This is the core task. The entire method is refactored. Here is the complete new body: **Step 1: Replace the method body** Find the opening of `htacces()` at line ~408. Replace everything from the start of the method body through the end (line ~773) with the code below. The key structural changes: - Remove `file_get_contents(htaccess.conf)` and `str_replace('{PAGE}', ...)` - Remove `str_replace('{HTACCESS_CACHE}', ...)` — cache block is now inline - Build `$htaccess_data` directly as PHP string - Delete all `type='system'` routes, then reinsert static + dynamic ones - Language switch → `pp_routes` (remove from `$htaccess_data`) - Newsletter → `pp_routes` (remove from `$htaccess_data`) - Producenci/producent → `pp_routes` (remove from `$htaccess_data`) **New `htacces()` method body** — replace lines 409–773 with: ```php { global $mdb; $settings = ( new \Domain\Settings\SettingsRepository( $mdb ) )->allSettings( true ); $url = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] ); $robots = 'User-agent: *' . PHP_EOL; $robots .= 'Allow: /' . PHP_EOL; $site_map = '' . PHP_EOL; $site_map .= '' . PHP_EOL; $site_map .= '' . PHP_EOL; $site_map .= 'https://' . $url . '' . PHP_EOL; $site_map .= '' . date( 'Y-m-d' ) . '' . PHP_EOL; $site_map .= 'daily' . PHP_EOL; $site_map .= '1' . PHP_EOL; $site_map .= '' . PHP_EOL; // // SYSTEM ROUTES — delete all and reinsert // $mdb->delete( 'pp_routes', [ 'type' => 'system' ] ); // Static system routes (hardcoded, never change) $systemRoutes = [ // Wyszukiwarka [ 'pattern' => '^wyszukiwarka/([^/]+)/([0-9]+)$', 'destination' => 'index.php?module=search&action=search_results&query=$1&bs=$2' ], [ 'pattern' => '^wyszukiwarka/([^/]+)$', 'destination' => 'index.php?module=search&action=search_results&query=$1&bs=1' ], // Zamowienia [ 'pattern' => '^zamowienie/([a-zA-Z0-9-]+)$', 'destination' => 'index.php?module=shop_order&action=order_details&order_hash=$1' ], [ 'pattern' => '^potwierdzenie-platnosci/([a-zA-Z0-9-]+)$', 'destination' => 'index.php?module=shop_order&action=payment_confirmation&order_hash=$1' ], // Platnosci [ 'pattern' => '^tpay-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_tpay' ], [ 'pattern' => '^platnosc-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_hotpay' ], [ 'pattern' => '^przelewy24-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_przelewy24pl' ], // Koszyk [ 'pattern' => '^koszyk$', 'destination' => 'index.php?module=shop_basket&action=main_view' ], [ 'pattern' => '^koszyk-podsumowanie$', 'destination' => 'index.php?module=shop_basket&action=summary_view' ], [ 'pattern' => '^zloz-zamowienie$', 'destination' => 'index.php?module=shop_basket&action=basket_save' ], // Klient [ 'pattern' => '^rejestracja$', 'destination' => 'index.php?module=shop_client&action=register_form' ], [ 'pattern' => '^logowanie$', 'destination' => 'index.php?module=shop_client&action=login_form' ], [ 'pattern' => '^wylogowanie$', 'destination' => 'index.php?module=shop_client&action=logout' ], [ 'pattern' => '^odzyskiwanie-hasla$', 'destination' => 'index.php?module=shop_client&action=recover_password' ], [ 'pattern' => '^panel-klienta/zamowienia$', 'destination' => 'index.php?module=shop_client&action=client_orders' ], [ 'pattern' => '^panel-klienta/adresy$', 'destination' => 'index.php?module=shop_client&action=client_addresses' ], [ 'pattern' => '^panel-klienta/nowy-adres$', 'destination' => 'index.php?module=shop_client&action=address_edit' ], [ 'pattern' => '^panel-klienta/edytuj-adres/([0-9]+)$', 'destination' => 'index.php?module=shop_client&action=address_edit&id=$1' ], [ 'pattern' => '^panel-klienta/usun-adres/([0-9]+)$', 'destination' => 'index.php?module=shop_client&action=address_delete&id=$1' ], // Newsletter [ 'pattern' => '^newsletter/signin$', 'destination' => 'index.php?module=newsletter&action=signin' ], [ 'pattern' => '^newsletter/confirm/hash=(.+)$', 'destination' => 'index.php?module=newsletter&action=confirm&hash=$1' ], [ 'pattern' => '^newsletter/unsubscribe/hash=(.+)$', 'destination' => 'index.php?module=newsletter&action=unsubscribe&hash=$1' ], // Moduły AJAX (shopBasket, shopClient, shopProduct, shopCoupon, search) [ 'pattern' => '^shopBasket/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopBasket&action=$1&$2' ], [ 'pattern' => '^shopBasket/([^/]+)$', 'destination' => 'index.php?module=shopBasket&action=$1' ], [ 'pattern' => '^shopClient/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopClient&action=$1&$2' ], [ 'pattern' => '^shopClient/([^/]+)$', 'destination' => 'index.php?module=shopClient&action=$1' ], [ 'pattern' => '^shopProduct/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopProduct&action=$1&$2' ], [ 'pattern' => '^shopProduct/([^/]+)$', 'destination' => 'index.php?module=shopProduct&action=$1' ], [ 'pattern' => '^shopCoupon/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopCoupon&action=$1&$2' ], [ 'pattern' => '^shopCoupon/([^/]+)$', 'destination' => 'index.php?module=shopCoupon&action=$1' ], [ 'pattern' => '^search/([^/]+)/(.+)$', 'destination' => 'index.php?module=search&action=$1&$2' ], [ 'pattern' => '^search/([^/]+)$', 'destination' => 'index.php?module=search&action=$1' ], ]; foreach ( $systemRoutes as $route ) { $mdb->insert( 'pp_routes', [ 'type' => 'system', 'lang_id' => 0, 'pattern' => $route['pattern'], 'destination' => $route['destination'], ] ); } // Dynamic system routes — languages $results = $mdb->select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); if ( is_array( $results ) ) foreach ( $results as $row ) { $mdb->insert( 'pp_routes', [ 'type' => 'system', 'lang_id' => 0, 'pattern' => '^' . $row['id'] . '$', 'destination' => 'index.php?a=change_language&id=' . $row['id'], ] ); } // Dynamic system routes — producenci $categoryDefaultLayoutId = ( new \Domain\Layouts\LayoutsRepository( $mdb ) )->categoryDefaultLayoutId(); $mdb->insert( 'pp_routes', [ 'type' => 'system', 'lang_id' => 0, 'pattern' => '^producenci$', 'destination' => 'index.php?module=shop_producer&action=list&layout_id=' . $categoryDefaultLayoutId, ] ); $rows = $mdb->select( 'pp_shop_producer', '*', [ 'status' => 1 ] ); if ( self::is_array_fix( $rows ) ) foreach ( $rows as $row ) { $mdb->insert( 'pp_routes', [ 'type' => 'system', 'lang_id' => 0, 'pattern' => '^producent/' . self::seo( $row['name'] ) . '$', 'destination' => 'index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId, ] ); $mdb->insert( 'pp_routes', [ 'type' => 'system', 'lang_id' => 0, 'pattern' => '^producent/' . self::seo( $row['name'] ) . '/([0-9]+)$', 'destination' => 'index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId . '&bs=$1', ] ); } // // HTACCESS — generuj z PHP (bez szablonu htaccess.conf) // $htaccess_data = 'RewriteEngine On' . PHP_EOL; $htaccess_data .= 'RewriteBase /' . PHP_EOL; $htaccess_data .= 'Options +FollowSymlinks' . PHP_EOL; $htaccess_data .= 'Options -Indexes' . PHP_EOL; $htaccess_data .= PHP_EOL; $htaccess_data .= '# Przekierowanie z www na bez www i z http na https w jednym kroku' . PHP_EOL; $htaccess_data .= 'RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]' . PHP_EOL; $htaccess_data .= 'RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]' . PHP_EOL; $htaccess_data .= PHP_EOL; $htaccess_data .= '# Przekierowanie z http na https, jesli nie zawiera www' . PHP_EOL; $htaccess_data .= 'RewriteCond %{HTTPS} off' . PHP_EOL; $htaccess_data .= 'RewriteCond %{REQUEST_URI} !^/(tpay-status|platnosc-status|przelewy24-status)$ [NC]' . PHP_EOL; $htaccess_data .= 'RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]' . PHP_EOL; $htaccess_data .= PHP_EOL; $htaccess_data .= '# Usuwanie koncowego slasha dla niekatalogów' . PHP_EOL; $htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-d' . PHP_EOL; $htaccess_data .= 'RewriteCond %{REQUEST_URI} !^/admin/.*$ [NC]' . PHP_EOL; $htaccess_data .= 'RewriteCond %{REQUEST_URI} (.+)/$' . PHP_EOL; $htaccess_data .= 'RewriteRule ^ %1 [R=301,L]' . PHP_EOL; $htaccess_data .= PHP_EOL; $htaccess_data .= 'RewriteCond %{REQUEST_URI} !^(.*)/libraries/(.*) [NC]' . PHP_EOL; $htaccess_data .= 'RewriteCond %{REQUEST_URI} !^(.*)/layout/(.*) [NC]' . PHP_EOL; $htaccess_data .= 'RewriteRule ^admin/([^/]*)/([^/]*)/(.*)$ admin/index.php?module=$1&action=$2&$3 [L]' . PHP_EOL; $htaccess_data .= PHP_EOL; $htaccess_data .= 'RewriteRule ^admin/$ admin/index.php [L]' . PHP_EOL; $htaccess_data .= PHP_EOL; $htaccess_data .= 'RewriteRule ^thumb/([0-9]*)/([0-9]*)/(.*)$ /libraries/thumb.php?img=$3&w=$1&h=$2 [L]' . PHP_EOL; $htaccess_data .= PHP_EOL; $htaccess_data .= 'RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /index.php' . PHP_EOL; $htaccess_data .= 'RewriteRule ^ /%1 [R=301,L]' . PHP_EOL; /* cache — zastąpienie placeholdera {HTACCESS_CACHE} */ if ( $settings['htaccess_cache'] ) { $htaccess_data .= '' . PHP_EOL . 'AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml application/xhtml+xml text/css text/javascript application/javascript application/x-javascript' . PHP_EOL . '' . PHP_EOL . '' . PHP_EOL . 'Header set Access-Control-Allow-Origin "*"' . PHP_EOL . '' . PHP_EOL . '' . PHP_EOL . 'ExpiresActive on' . PHP_EOL . 'ExpiresDefault "access plus 1 month"' . PHP_EOL . 'ExpiresByType text/css "access plus 1 year"' . PHP_EOL . 'ExpiresByType application/json "access plus 0 seconds"' . PHP_EOL . 'ExpiresByType application/xml "access plus 0 seconds"' . PHP_EOL . 'ExpiresByType text/xml "access plus 0 seconds"' . PHP_EOL . 'ExpiresByType image/x-icon "access plus 1 week"' . PHP_EOL . 'ExpiresByType text/x-component "access plus 1 month"' . PHP_EOL . 'ExpiresByType text/html "access plus 0 seconds"' . PHP_EOL . 'ExpiresByType application/javascript "access plus 1 year"' . PHP_EOL . 'ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds"' . PHP_EOL . 'ExpiresByType text/cache-manifest "access plus 0 seconds"' . PHP_EOL . 'ExpiresByType audio/ogg "access plus 1 month"' . PHP_EOL . 'ExpiresByType image/gif "access plus 1 month"' . PHP_EOL . 'ExpiresByType image/jpeg "access plus 1 month"' . PHP_EOL . 'ExpiresByType image/png "access plus 1 month"' . PHP_EOL . 'ExpiresByType video/mp4 "access plus 1 month"' . PHP_EOL . 'ExpiresByType video/ogg "access plus 1 month"' . PHP_EOL . 'ExpiresByType video/webm "access plus 1 month"' . PHP_EOL . 'ExpiresByType application/atom+xml "access plus 1 hour"' . PHP_EOL . 'ExpiresByType application/rss+xml "access plus 1 hour"' . PHP_EOL . 'ExpiresByType application/font-woff "access plus 1 month"' . PHP_EOL . 'ExpiresByType application/vnd.ms-fontobject "access plus 1 month"' . PHP_EOL . 'ExpiresByType application/x-font-ttf "access plus 1 month"' . PHP_EOL . 'ExpiresByType font/opentype "access plus 1 month"' . PHP_EOL . 'ExpiresByType image/svg+xml "access plus 1 month"' . PHP_EOL . '' . PHP_EOL; } else { $htaccess_data .= '' . PHP_EOL . 'Header set Cache-Control "no-cache, no-store, must-revalidate"' . PHP_EOL . 'Header set Pragma "no-cache"' . PHP_EOL . 'Header set Expires 0' . PHP_EOL . '' . PHP_EOL; } $htaccess_data .= '' . PHP_EOL; $htaccess_data .= ' Order Deny,Allow' . PHP_EOL; $htaccess_data .= ' Deny from all' . PHP_EOL; $htaccess_data .= '' . PHP_EOL; $htaccess_data .= '' . PHP_EOL; $htaccess_data .= ' Order Deny,Allow' . PHP_EOL; $htaccess_data .= ' Deny from all' . PHP_EOL; $htaccess_data .= '' . PHP_EOL; $htaccess_data .= '' . PHP_EOL; $htaccess_data .= ' Order Deny,Allow' . PHP_EOL; $htaccess_data .= ' Deny from all' . PHP_EOL; $htaccess_data .= '' . PHP_EOL; // // KATEGORIE — sitemap + pp_routes (bez zmian) // $results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); if ( is_array( $results ) ) foreach ( $results as $row ) { !$row['start'] ? $language_link = $row['id'] . '/' : $language_link = ''; $results2 = $mdb->select( 'pp_shop_categories_langs', [ '[><]pp_shop_categories' => [ 'category_id' => 'id' ] ], [ 'seo_link', 'title', 'category_id' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'o' => 'ASC' ] ] ); if ( is_array( $results2 ) ) foreach ( $results2 as $row2 ) { if ( $row2['title'] ) { $site_map .= '' . PHP_EOL; if ( $row2['seo_link'] ) $site_map .= 'https://' . $url . '/' . $language_link . self::seo( $row2['seo_link'] ) . '' . PHP_EOL; else $site_map .= 'https://' . $url . '/' . $language_link . 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ) . '' . PHP_EOL; $site_map .= '' . date( 'Y-m-d' ) . '' . PHP_EOL; $site_map .= 'daily' . PHP_EOL; $site_map .= '1' . PHP_EOL; $site_map .= '' . PHP_EOL; $seoSlug = $row2['seo_link'] ? self::seo( $row2['seo_link'] ) : 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ); $mdb->delete( 'pp_routes', [ 'AND' => [ 'category_id' => $row2['category_id'], 'lang_id' => $row['id'] ] ] ); $mdb->insert( 'pp_routes', [ 'category_id' => $row2['category_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . $seoSlug . '$', 'destination' => 'index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'], ] ); $mdb->insert( 'pp_routes', [ 'category_id' => $row2['category_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . $seoSlug . '/([0-9]+)$', 'destination' => 'index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&bs=$1', ] ); } } } // // PRODUKTY — sitemap + pp_routes (bez zmian) // $results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); if ( is_array( $results ) ) { foreach ( $results as $row ) { !$row['start'] ? $language_link = $row['id'] . '/' : $language_link = ''; $results2 = $mdb->select( 'pp_shop_products_langs', [ '[><]pp_shop_products' => [ 'product_id' => 'id' ] ], [ 'seo_link', 'name', 'product_id' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'name' => 'ASC' ] ] ); if ( is_array( $results2 ) ) { foreach ( $results2 as $row2 ) { $mdb->delete( 'pp_routes', [ 'AND' => [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'] ] ] ); if ( $row2['name'] ) { $site_map .= '' . PHP_EOL; if ( $row2['seo_link'] ) $site_map .= 'https://' . $url . '/' . $language_link . self::seo( $row2['seo_link'] ) . '' . PHP_EOL; else $site_map .= 'https://' . $url . '/' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '' . PHP_EOL; $site_map .= '' . date( 'Y-m-d' ) . '' . PHP_EOL; $site_map .= 'daily' . PHP_EOL; $site_map .= '1' . PHP_EOL; $site_map .= '' . PHP_EOL; if ( $row2['seo_link'] ) { $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] ); $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] ); } else { $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] ); $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] ); } } } } } } // // STRONY + ARTYKULY — sitemap + pp_routes (bez zmian) // $results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); if ( is_array( $results ) ) foreach ( $results as $row ) { ( !$row['start'] and count( $results ) > 1 ) ? $language_link = $row['id'] . '/' : $language_link = ''; $results2 = $mdb->select( 'pp_pages_langs', [ '[><]pp_pages' => [ 'page_id' => 'id' ] ], [ 'seo_link', 'title', 'page_id', 'noindex', 'start', 'link', 'page_type' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'start' => 'DESC', 'o' => 'ASC' ] ] ); if ( is_array( $results2 ) ) foreach ( $results2 as $row2 ) { if ( $row2['title'] and $row2['page_type'] != 3 and $row2['page_type'] != 5 ) { if ( !$row2['noindex'] ) { $site_map .= '' . PHP_EOL; if ( $row2['seo_link'] ) $site_map .= 'https://' . $url . '/' . self::seo( $row2['seo_link'] ) . '' . PHP_EOL; else $site_map .= 'https://' . $url . '/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '' . PHP_EOL; $site_map .= '' . date( 'Y-m-d' ) . '' . PHP_EOL; $site_map .= 'daily' . PHP_EOL; $site_map .= '1' . PHP_EOL; $site_map .= '' . PHP_EOL; } else if ( $row2['noindex'] and $row2['seo_link'] ) { $robots .= 'User-agent: GoogleBot' . PHP_EOL; $robots .= 'Disallow: /' . $row2['seo_link'] . PHP_EOL; } if ( $row2['start'] ) { if ( $row2['seo_link'] ) { $htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/' . self::seo( $row2['seo_link'] ) . '$'; $htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]'; $htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/' . self::seo( $row2['seo_link'] ) . '-1$'; $htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]'; } else { $htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '$'; $htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]'; $htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '-1$'; $htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]'; } $htaccess_data .= PHP_EOL . 'RewriteRule ^$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . ' [L]'; } $seoSlug = $row2['seo_link'] ? self::seo( $row2['seo_link'] ) : 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ); $langPrefix = $row2['start'] ? '' : $language_link; $mdb->delete( 'pp_routes', [ 'AND' => [ 'page_id' => $row2['page_id'], 'lang_id' => $row['id'] ] ] ); $mdb->insert( 'pp_routes', [ 'page_id' => $row2['page_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $langPrefix . $seoSlug . '$', 'destination' => 'index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'], ] ); $mdb->insert( 'pp_routes', [ 'page_id' => $row2['page_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $langPrefix . $seoSlug . '/([0-9]+)$', 'destination' => 'index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&bs=$1', ] ); } } $results2 = $mdb->select( 'pp_articles_langs', [ '[><]pp_articles' => [ 'article_id' => 'id' ] ], [ 'seo_link', 'title', 'article_id', 'noindex', 'copy_from' ], [ 'AND' => [ 'status' => 1, 'lang_id' => $row['id'], 'block_direct_access' => 0 ] ] ); if ( is_array( $results2 ) ) foreach ( $results2 as $row2 ) { if ( $row2['copy_from'] != null ) { $results_tmp = $mdb->get( 'pp_articles_langs', [ 'seo_link', 'title' ], [ 'AND' => [ 'article_id' => $row2['article_id'], 'lang_id' => $row2['copy_from'] ] ] ); $row2['seo_link'] = $results_tmp['seo_link']; $row2['title'] = $results_tmp['title']; } if ( !$row2['noindex'] ) { $site_map .= '' . PHP_EOL; if ( $row2['seo_link'] ) $site_map .= 'https://' . $url . '/' . self::seo( $row2['seo_link'] ) . '' . PHP_EOL; else $site_map .= 'https://' . $url . '/a-' . $row2['article_id'] . '-' . self::seo( $row2['title'] ) . '' . PHP_EOL; $site_map .= '' . date( 'Y-m-d' ) . '' . PHP_EOL; $site_map .= 'daily' . PHP_EOL; $site_map .= '1' . PHP_EOL; $site_map .= '' . PHP_EOL; } else if ( $row2['noindex'] and $row2['seo_link'] ) { $robots .= 'User-agent: GoogleBot' . PHP_EOL; $robots .= 'Disallow: /' . $row2['seo_link'] . PHP_EOL; } $mdb->delete( 'pp_routes', [ 'AND' => [ 'article_id' => $row2['article_id'], 'lang_id' => $row['id'] ] ] ); if ( $row2['seo_link'] ) { $mdb->insert( 'pp_routes', [ 'article_id' => $row2['article_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '$', 'destination' => 'index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'], ] ); } else if ( $row2['title'] != null ) { $mdb->insert( 'pp_routes', [ 'article_id' => $row2['article_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'a-' . $row2['article_id'] . '-' . self::seo( $row2['title'] ) . '$', 'destination' => 'index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'], ] ); } } } // Invalidacja cache tras try { ( new \Shared\Cache\CacheHandler() )->delete( 'pp_routes:all' ); } catch ( \Exception $e ) { // Redis niedostepny — ignorujemy } $results = $mdb->get( 'pp_settings', 'value', [ 'param' => 'htaccess' ] ); if ( $results ) $htaccess_data .= PHP_EOL . $results; $results = $mdb->get( 'pp_settings', 'value', [ 'param' => 'robots' ] ); if ( $results ) $robots .= PHP_EOL . $results; $site_map .= ''; $htaccess_data .= PHP_EOL; $htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-f' . PHP_EOL; $htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-d' . PHP_EOL; $htaccess_data .= 'RewriteRule ^ index.php [L]'; // Niektore hostingi blokuja zmiane wersji PHP przez .htaccess. $htaccess_data = preg_replace( '/^(\\s*)(AddHandler|SetHandler|ForceType)\\b/im', '$1# $2', $htaccess_data ); $fp = fopen( $dir . '.htaccess', 'w' ); fwrite( $fp, $htaccess_data ); fclose( $fp ); $fp = fopen( $dir . 'sitemap.xml', 'w' ); fwrite( $fp, $site_map ); fclose( $fp ); $fp = fopen( $dir . 'robots.txt', 'w' ); fwrite( $fp, $robots ); fclose( $fp ); } ``` **Step 2: Run tests** ``` php phpunit.phar --configuration phpunit.xml ``` Expected: all tests pass (htacces() has no unit tests, covered by integration). --- ## Task 3: Delete `libraries/htaccess.conf` **Files:** - Delete: `libraries/htaccess.conf` **Step 1: Verify htacces() no longer references the file** Search for any remaining `file_get_contents` referencing `htaccess.conf`: ```bash grep -r "htaccess.conf" autoload/ ``` Expected: no results. **Step 2: Delete the file** ```bash rm libraries/htaccess.conf ``` **Step 3: Run tests** ``` php phpunit.phar --configuration phpunit.xml ``` Expected: all tests still pass. --- ## Task 4: Update `docs/DATABASE_STRUCTURE.md` **Files:** - Modify: `docs/DATABASE_STRUCTURE.md` (section `## pp_routes`) **Step 1: Add `type` column to the pp_routes table description** Find the `## pp_routes` section and add the `type` row to the column table: ```markdown | type | Typ trasy: NULL = encja (produkt/kategoria/strona/artykuł), 'system' = trasa systemowa | ``` Also update the description paragraph to mention that system routes are managed automatically. --- ## Task 5: Manual integration test on server **Step 1: Apply migration** ```sql ALTER TABLE pp_routes ADD COLUMN type VARCHAR(20) NULL AFTER article_id; ``` **Step 2: Trigger `htacces()` regeneration** Log in to admin panel → save any product or category → this calls `htacces()`. **Step 3: Verify pp_routes has system routes** ```sql SELECT COUNT(*) FROM pp_routes WHERE type = 'system'; ``` Expected: ~35+ rows (32 static + language rows + producer rows). **Step 4: Verify .htaccess was generated correctly** Open `.htaccess` — should NOT contain `RewriteRule ^koszyk$`, `^logowanie$`, etc. Should contain HTTPS redirect, admin routing, thumb routing, cache block. **Step 5: Test URLs in browser** - `/koszyk` → koszyk page ✓ - `/logowanie` → login page ✓ - `/wyszukiwarka/test` → search results ✓ - `/zamowienie/abc123` → order details ✓ - `/shopClient/confirm/hash=xyz` → client confirm action ✓ - Category URL → category page ✓ - Product URL → product page ✓ **Step 6: Run full test suite** ``` php phpunit.phar --configuration phpunit.xml ``` Expected: 807 tests, all pass. --- ## Task 6: Commit **Step 1: Stage and commit** ```bash git add migrations/0.329.sql git add autoload/Shared/Helpers/Helpers.php git add docs/DATABASE_STRUCTURE.md git add docs/plans/2026-02-27-htaccess-conf-elimination.md git add docs/plans/2026-02-27-htaccess-to-routes-design.md git rm libraries/htaccess.conf git commit -m "feat: eliminate htaccess.conf, move all routes to pp_routes (v0.330)" ```