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
8 changes: 6 additions & 2 deletions config/v3-to-v4.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
<?php
use Rector\Config\RectorConfig;

use phpseclib\rectorRules\Rector\V3toV4\X509NodeVisitor;
use phpseclib\rectorRules\Rector\V3toV4\CryptRandom;
use phpseclib\rectorRules\Rector\V3toV4\Namespace_;
use phpseclib\rectorRules\Rector\V3toV4\SFTPChmod;
use phpseclib\rectorRules\Rector\V3toV4\X509;

return RectorConfig::configure()
->registerDecoratingNodeVisitor(X509NodeVisitor::class)
->withRules([
Namespace_::class,
CryptRandom::class,
SFTPChmod::class,
X509::class,
])
->withPreparedSets(
Expand Down
63 changes: 63 additions & 0 deletions src/Rector/V3toV4/CryptRandom.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace phpseclib\rectorRules\Rector\V3toV4;

use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\UseItem;
use PhpParser\Node\Stmt\Use_;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\NodeTraverser;
use Rector\Rector\AbstractRector;

/**
* Replaces phpseclib3\Crypt\Random::string($n) with random_bytes($n).
*
* phpseclib3\Crypt\Random existed because PHP 5.6 had no built-in CSPRNG.
* PHP 7+ ships random_bytes(), which phpseclib 4 uses directly — the class
* was removed entirely. Both the use-import and every call site are rewritten.
*
* Before:
* use phpseclib3\Crypt\Random;
* $bytes = Random::string(32);
*
* After:
* $bytes = random_bytes(32);
*/
final class CryptRandom extends AbstractRector
{
public function getNodeTypes(): array
{
return [Use_::class, StaticCall::class];
}

public function refactor(Node $node): int|null|Node
{
if ($node instanceof Use_) {
$node->uses = array_values(array_filter(
$node->uses,
fn(UseItem $item) => !$this->isName($item->name, 'phpseclib3\Crypt\Random')
));

// Remove the Use_ statement entirely if it now has no items
return count($node->uses) > 0 ? $node : NodeTraverser::REMOVE_NODE;
}

if (!$node instanceof StaticCall) {
return null;
}

if (!$this->isNames($node->class, ['Random', 'phpseclib3\Crypt\Random'])) {
return null;
}

if (!$this->isName($node->name, 'string')) {
return null;
}

return new FuncCall(new Name('random_bytes'), $node->args);
}
}
69 changes: 69 additions & 0 deletions src/Rector/V3toV4/Namespace_.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace phpseclib\rectorRules\Rector\V3toV4;

use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\UseItem;
use Rector\Rector\AbstractRector;

/**
* Renames the phpseclib3\ root namespace to phpseclib4\ everywhere — in use
* statements and in fully-qualified names used inline.
*
* Short names (e.g. RSA in "use phpseclib3\Crypt\RSA; new RSA()") are left
* alone: renaming the use statement is sufficient for them to resolve correctly.
*
* Classes deliberately skipped here (handled by dedicated rules):
* - phpseclib3\File\X509 → split into X509 / CSR / CRL / SPKAC (X509 rule)
* - phpseclib3\Crypt\Random → removed; replaced by random_bytes() (CryptRandom rule)
*/
final class Namespace_ extends AbstractRector
{
private const SKIP = [
'phpseclib3\File\X509',
'phpseclib3\Crypt\Random',
];

public function getNodeTypes(): array
{
return [UseItem::class, FullyQualified::class];
}

public function refactor(Node $node): ?Node
{
if ($node instanceof UseItem) {
$name = $node->name->toString();
if (!str_starts_with($name, 'phpseclib3\\')) {
return null;
}
if (in_array($name, self::SKIP, true)) {
return null;
}
$node->name = new Name('phpseclib4\\' . substr($name, strlen('phpseclib3\\')));
return $node;
}

// FullyQualified — only rename if written explicitly in source (not resolved from a short name).
// Resolved short names occupy file-character span of the alias only (e.g. 4 chars for "SFTP"),
// while explicit FQNs span the full string including the leading backslash.
$name = $node->toString();
if (!str_starts_with($name, 'phpseclib3\\')) {
return null;
}
if (in_array($name, self::SKIP, true)) {
return null;
}
$expectedSpan = strlen('\\' . $name);
$actualSpan = $node->getEndFilePos() - $node->getStartFilePos() + 1;
if ($actualSpan !== $expectedSpan) {
// Span doesn't match the full FQN — this was resolved from a short name via a use import.
return null;
}

return new FullyQualified('phpseclib4\\' . substr($name, strlen('phpseclib3\\')));
}
}
15 changes: 8 additions & 7 deletions src/Rector/V3toV4/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ The rule performs the following transformations:
- Converts instance method calls into the corresponding static calls on the appropriate phpseclib4 class.
- Migrates calls such as:
- `loadX509()` → `X509::load()`
- `loadCSR()` → `CSR::loadCSR()`
- `loadCRL()` → `CRL::loadCRL()`
- `loadCSR()` → `CSR::load()`
- `loadCRL()` → `CRL::load()`
- `loadSPKAC()` → `SPKAC::load()`
- Rewrites API changes:
- `getDN()` → `getSubjectDN(X509::DN_ARRAY)`
- `setDNProp()` → `addDNProp()`
Expand Down Expand Up @@ -112,7 +113,7 @@ $csr = $x509->loadCSR(file_get_contents('csr.csr'));
```
will be refactored to
```php
$csr = \phpseclib4\File\CSR::loadCSR(file_get_contents('csr.csr'));
$csr = \phpseclib4\File\CSR::load(file_get_contents('csr.csr'));
```

#### Set DN Prop
Expand Down Expand Up @@ -159,7 +160,7 @@ $crl = $x509->loadCRL(file_get_contents('crl.bin'));
```
will be refactored to
```php
$crl = \phpseclib4\File\CRL::loadCRL(file_get_contents('crl.bin'));
$crl = \phpseclib4\File\CRL::load(file_get_contents('crl.bin'));
```

### SPKAC
Expand All @@ -172,10 +173,10 @@ $spkac = $x509->loadSPKAC(file_get_contents('spkac.txt'));
```
will be refactored to
```php
$spkac = \phpseclib4\File\CRL::loadCRL(file_get_contents('spkac.txt'));
$spkac = \phpseclib4\File\SPKAC::load(file_get_contents('spkac.txt'));
```

#### Read cert
#### Create SPKAC

```php
$x509 = new X509();
Expand All @@ -185,7 +186,7 @@ $spkac = $x509->signSPKAC();
```
will be refactored to
```php
$spkac = \phpseclib4\File\CRL::loadCRL($privKey->getPublicKey());
$spkac = new \phpseclib4\File\SPKAC($privKey->getPublicKey());
$spkac->setChallenge('123456789');
$privKey->sign($spkac);
```
53 changes: 53 additions & 0 deletions src/Rector/V3toV4/SFTPChmod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace phpseclib\rectorRules\Rector\V3toV4;

use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Scalar\Int_;
use Rector\Rector\AbstractRector;

/**
* Swaps the argument order of SFTP::chmod() from v3's (mode, path) to v4's (path, mode).
*
* phpseclib 3: $sftp->chmod(0777, 'file.txt')
* phpseclib 4: $sftp->chmod('file.txt', 0777)
*
* Detection heuristic: any ->chmod() call whose first argument is an integer
* literal. PHP's built-in chmod() is a function (not a method), so there is
* no false-positive risk there; and no other common class exposes chmod(int, string).
*/
final class SFTPChmod extends AbstractRector
{
public function getNodeTypes(): array
{
return [MethodCall::class];
}

public function refactor(Node $node): ?Node
{
if (!$node instanceof MethodCall) {
return null;
}

if (!$this->isName($node->name, 'chmod')) {
return null;
}

// Only act when the v3 signature is present: first arg is an int literal (octal mode)
if (!isset($node->args[0], $node->args[1])) {
return null;
}

if (!$node->args[0]->value instanceof Int_) {
return null;
}

// Swap path and mode — args[2] (recursive flag) stays in place
[$node->args[0], $node->args[1]] = [$node->args[1], $node->args[0]];

return $node;
}
}
Loading
Loading