Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0

Expand All @@ -34,7 +34,7 @@ jobs:

- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
Expand Down Expand Up @@ -66,7 +66,7 @@ jobs:
echo "✅ Coverage is acceptable ($COVERAGE%)"

- name: Comment PR with results
uses: actions/github-script@v7
uses: actions/github-script@v8
if: always()
with:
script: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
},
"require-dev": {
"phpunit/phpunit": "^10.5 || ^11.0",
"orchestra/testbench": "^8.0 || ^9.0 || ^10.0",
"orchestra/testbench": "^10.6.0",
"squizlabs/php_codesniffer": "^3.6",
"mockery/mockery": "^1.6"
},
Expand Down
70 changes: 65 additions & 5 deletions src/Middleware/InsertDNSPrefetch.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,76 @@ class InsertDNSPrefetch extends PageSpeed
{
public function apply($buffer)
{
// Extract URLs only from HTML attributes, not from script/style content
$urls = [];

// Step 1: Extract URLs from script src/href attributes
preg_match_all(
'#\bhttps?://[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/))#',
'#<script[^>]+src=["\']([^"\']+)["\']#i',
$buffer,
$matches,
PREG_OFFSET_CAPTURE
$scriptMatches
);
if (!empty($scriptMatches[1])) {
$urls = array_merge($urls, $scriptMatches[1]);
}

// Step 2: Extract URLs from link href attributes
preg_match_all(
'#<link[^>]+href=["\']([^"\']+)["\']#i',
$buffer,
$linkMatches
);
if (!empty($linkMatches[1])) {
$urls = array_merge($urls, $linkMatches[1]);
}

// Step 3: Extract URLs from img src attributes
preg_match_all(
'#<img[^>]+src=["\']?([^"\'\s>]+)["\']?#i',
$buffer,
$imgMatches
);
if (!empty($imgMatches[1])) {
$urls = array_merge($urls, $imgMatches[1]);
}

// Step 4: Extract URLs from anchor href attributes
preg_match_all(
'#<a[^>]+href=["\']([^"\']+)["\']#i',
$buffer,
$anchorMatches
);
if (!empty($anchorMatches[1])) {
$urls = array_merge($urls, $anchorMatches[1]);
}

// Step 5: Extract URLs from iframe src attributes
preg_match_all(
'#<iframe[^>]+src=["\']([^"\']+)["\']#i',
$buffer,
$iframeMatches
);
if (!empty($iframeMatches[1])) {
$urls = array_merge($urls, $iframeMatches[1]);
}

// Step 6: Extract URLs from video/audio source elements
preg_match_all(
'#<(?:video|audio|source)[^>]+src=["\']([^"\']+)["\']#i',
$buffer,
$mediaMatches
);
if (!empty($mediaMatches[1])) {
$urls = array_merge($urls, $mediaMatches[1]);
}

$dnsPrefetch = collect($matches[0])->map(function ($item) {
// Filter to keep only external URLs (http:// or https://)
$externalUrls = array_filter($urls, function ($url) {
return preg_match('#^https?://#i', $url);
});

$domain = (new TrimUrls)->apply($item[0]);
$dnsPrefetch = collect($externalUrls)->map(function ($url) {
$domain = (new TrimUrls)->apply($url);
$domain = explode(
'/',
str_replace('//', '', $domain)
Expand Down
11 changes: 10 additions & 1 deletion src/Middleware/PageSpeed.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,16 @@ public function handle($request, Closure $next)
*/
protected function replace(array $replace, $buffer)
{
return preg_replace(array_keys($replace), array_values($replace), $buffer);
$result = preg_replace(array_keys($replace), array_values($replace), $buffer);

// Check for PCRE errors (e.g., backtrack limit, recursion limit exceeded)
if ($result === null && preg_last_error() !== PREG_NO_ERROR) {
// Log the error or handle it appropriately
// For now, return the original buffer to prevent blank pages
return $buffer;
}

return $result;
}

/**
Expand Down
36 changes: 18 additions & 18 deletions src/Middleware/RemoveComments.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public function apply($buffer)
{
// First, remove multi-line comments (/* ... */)
$buffer = $this->replaceInsideHtmlTags(['script', 'style'], self::REGEX_MATCH_MULTILINE_COMMENTS, '', $buffer);

// Then, remove single-line comments (//) more carefully
$buffer = $this->removeSingleLineComments($buffer);

Expand All @@ -21,7 +21,7 @@ public function apply($buffer)

return $this->replace($replaceHtmlRules, $buffer);
}

/**
* Remove single-line comments (//) from script tags while preserving them inside strings
*
Expand All @@ -34,10 +34,10 @@ protected function removeSingleLineComments($buffer)
$tagAfterReplace = $this->removeCommentsFromTag($tagMatched);
$buffer = str_replace($tagMatched, $tagAfterReplace, $buffer);
}

return $buffer;
}

/**
* Remove // comments from a script/style tag content
*
Expand All @@ -53,18 +53,18 @@ protected function removeCommentsFromTag($tag)
} elseif (strpos($tag, "\r") !== false) {
$lineEnding = "\r";
}

// Split by lines to process each line
$lines = preg_split('/\r\n|\r|\n/', $tag);
$processedLines = [];

foreach ($lines as $line) {
$processedLines[] = $this->removeSingleLineCommentFromLine($line);
}

return implode($lineEnding, $processedLines);
}

/**
* Remove // comment from a single line while preserving // inside strings
*
Expand All @@ -79,38 +79,38 @@ protected function removeSingleLineCommentFromLine($line)
$inDoubleQuote = false;
$inRegex = false;
$escaped = false;

for ($i = 0; $i < $length; $i++) {
$char = $line[$i];
$nextChar = $i + 1 < $length ? $line[$i + 1] : '';
$prevChar = $i > 0 ? $line[$i - 1] : '';

// Handle escape sequences
if ($escaped) {
$result .= $char;
$escaped = false;
continue;
}

if ($char === '\\' && ($inSingleQuote || $inDoubleQuote || $inRegex)) {
$result .= $char;
$escaped = true;
continue;
}

// Toggle quote states
if ($char === '"' && !$inSingleQuote && !$inRegex) {
$inDoubleQuote = !$inDoubleQuote;
$result .= $char;
continue;
}

if ($char === "'" && !$inDoubleQuote && !$inRegex) {
$inSingleQuote = !$inSingleQuote;
$result .= $char;
continue;
}

// Handle regex literals (basic detection)
if ($char === '/' && !$inSingleQuote && !$inDoubleQuote) {
// Check if this might be a regex literal
Expand All @@ -123,15 +123,15 @@ protected function removeSingleLineCommentFromLine($line)
continue;
}
}

// End of regex literal
if ($inRegex) {
$inRegex = false;
$result .= $char;
continue;
}
}

// Check for // comment outside of strings
if (!$inSingleQuote && !$inDoubleQuote && !$inRegex && $char === '/' && $nextChar === '/') {
// Check if this is not part of a URL (preceded by :)
Expand All @@ -140,10 +140,10 @@ protected function removeSingleLineCommentFromLine($line)
break;
}
}

$result .= $char;
}

return $result;
}
}
6 changes: 3 additions & 3 deletions tests/Middleware/CollapseWhitespaceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,21 @@ public function test_collapse_whitespace(): void
$this->response->getContent()
);
}

public function test_javascript_not_broken_by_comment_removal_and_whitespace_collapse(): void
{
// This test ensures that when comments are removed and whitespace is collapsed,
// the JavaScript code remains functional and nothing is accidentally commented out
$content = $this->response->getContent();

// Ensure all expected JavaScript statements are present
$this->assertStringContainsString("console.log('Laravel');", $content);
$this->assertStringContainsString("console.log('Page');", $content);
$this->assertStringContainsString("console.log('Speed!');", $content);
$this->assertStringContainsString('var url = "http://example.com";', $content);
$this->assertStringContainsString('var text = "Some text";', $content);
$this->assertStringContainsString("console.log('Important code');", $content);

// Ensure comments are removed
$this->assertStringNotContainsString("// This comment should be removed", $content);
$this->assertStringNotContainsString("// This comment should also be removed", $content);
Expand Down
Loading