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
13 changes: 9 additions & 4 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# Dependabot only reads this file from the default branch, 2.x, which is also the only branch it watches.
#
# Only facebook/php-business-sdk is watched. The Graph API version the client talks to is ApiConfig::APIVersion from that
# package, and Meta ships a new major of it for every Graph API version, a few times a year. Without a PR that widens the
# constraint, users of this library stay capped at the previous Graph API version.
# Two packages are watched:
#
# - facebook/php-business-sdk: the Graph API version the client talks to is ApiConfig::APIVersion from that package, and
# Meta ships a new major of it for every Graph API version, a few times a year. Without a PR that widens the
# constraint, users of this library stay capped at the previous Graph API version.
# - facebook/capi-param-builder-php: a dev dependency used as a test oracle for the _fbc/_fbp cookie format, see
# tests/ValueObject/ParamBuilderConformanceTest.php. A new major should be tested against as soon as it exists.
#
# No composer.lock is committed, so a PR is only opened when a release falls outside the allowed range, i.e. for a new
# major. The other dependencies are left out on purpose: their ranges already float, and the dev tools are held on the
# major. Releases inside the range produce no PR; the highest jobs install them the next time CI runs. The other dependencies are left out on purpose: their ranges already float, and the dev tools are held on the
# majors that still support PHP 8.1.
version: 2

Expand All @@ -16,6 +20,7 @@ updates:
interval: "weekly"
allow:
- dependency-name: "facebook/php-business-sdk"
- dependency-name: "facebook/capi-param-builder-php"
versioning-strategy: "widen"
labels:
- "dependencies"
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,6 @@ So to add a field: add the public property, map it in `getMapping()`, and regist

**Value objects (`src/ValueObject/`)** — `Fbc`/`Fbp` (extending `Fb`) model the `_fbc`/`_fbp` cookie values with `fromString()` validation and `value()` serialization; assignable to `User::$fbc`/`$fbp` as either the typed object or a raw string. Both accept the optional trailing appendix segment that Meta's parameter builder writes (`getAppendix()`/`withAppendix()`) and write it back unchanged, so a cookie value round-trips byte for byte.

The `facebook/php-business-sdk` dependency is used only for `Normalizer`, `Util::hash`, and `ApiConfig::APIVersion` (the API version is pinned to whatever that package ships). Dependabot (`.github/dependabot.yml`) watches this one package on `2.x` and opens a PR that widens the constraint when Meta releases a new major; `1.x` is not watched. CI covers the normalization and hashing on the new major; run `LiveClientTest` once against it before tagging a release.
The `facebook/php-business-sdk` dependency is used only for `Normalizer`, `Util::hash`, and `ApiConfig::APIVersion` (the API version is pinned to whatever that package ships). Dependabot (`.github/dependabot.yml`) watches this package and `facebook/capi-param-builder-php` on `2.x` and opens a PR that widens the constraint when Meta releases a new major; `1.x` is not watched. CI covers the normalization and hashing on the new major; run `LiveClientTest` once against it before tagging a release.

`facebook/capi-param-builder-php` is a **dev** dependency used as a test oracle, never in `src/`. `tests/ValueObject/ParamBuilderConformanceTest.php` asserts that every `_fbc`/`_fbp` value Meta's own builder writes is parsed by `Fbc`/`Fbp` and written back byte for byte. The check is deliberately one-directional: the builder's own parser is only structural (it accepts `a.b.c.d`), so the value objects stay stricter, and they are more lenient about the two-character appendix, where the builder only accepts six language tokens. No `composer.lock` is committed, so a builder release inside the allowed range produces no Dependabot PR; the `highest` CI jobs install it the next time CI runs.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"require-dev": {
"ergebnis/composer-normalize": "^2.50",
"facebook/capi-param-builder-php": "^1.3.1",
"infection/infection": "^0.29",
"jangregor/phpstan-prophecy": "^2.3",
"nyholm/psr7": "^1.8",
Expand Down
11 changes: 6 additions & 5 deletions src/ValueObject/Fb.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@

abstract class Fb
{
/**
* The subdomain index is the number of dots in the domain the cookie is set on. These are the three values Meta
* documents, but a cookie on a deeper domain has a higher index: Meta's own parameter builder writes fb.3. for
* a.b.example.co.uk
*/
public const SUBDOMAIN_INDEX_COM = 0;

public const SUBDOMAIN_INDEX_FACEBOOK_COM = 1;
Expand Down Expand Up @@ -40,7 +45,7 @@

public function __construct()
{
$this->creationTime = (int) ceil(microtime(true) * 1000);

Check warning on line 48 in src/ValueObject/Fb.php

View workflow job for this annotation

GitHub Actions / Mutation tests (8.3, highest)

Escaped Mutant for Mutator "RoundingFamily": @@ @@ private ?string $appendix = null; public function __construct() { - $this->creationTime = (int) ceil(microtime(true) * 1000); + $this->creationTime = (int) round(microtime(true) * 1000); } /** * @throws InvalidArgumentException if the $value is not the correct format

Check warning on line 48 in src/ValueObject/Fb.php

View workflow job for this annotation

GitHub Actions / Mutation tests (8.3, highest)

Escaped Mutant for Mutator "RoundingFamily": @@ @@ private ?string $appendix = null; public function __construct() { - $this->creationTime = (int) ceil(microtime(true) * 1000); + $this->creationTime = (int) floor(microtime(true) * 1000); } /** * @throws InvalidArgumentException if the $value is not the correct format
}

/**
Expand All @@ -60,11 +65,7 @@
*/
public function withSubdomainIndex(int $subdomainIndex): self
{
Assert::oneOf($subdomainIndex, [
self::SUBDOMAIN_INDEX_COM,
self::SUBDOMAIN_INDEX_FACEBOOK_COM,
self::SUBDOMAIN_INDEX_WWW_FACEBOOK_COM,
]);
Assert::greaterThanEq($subdomainIndex, 0);

$obj = clone $this;
$obj->subdomainIndex = $subdomainIndex;
Expand All @@ -90,7 +91,7 @@

Assert::integer($creationTime);
Assert::greaterThanEq($creationTime, 1_075_590_000_000); // Facebooks founding date xD
Assert::lessThanEq($creationTime, (time() + 1) * 1000);

Check warning on line 94 in src/ValueObject/Fb.php

View workflow job for this annotation

GitHub Actions / Mutation tests (8.3, highest)

Escaped Mutant for Mutator "IncrementInteger": @@ @@ Assert::integer($creationTime); Assert::greaterThanEq($creationTime, 1075590000000); // Facebooks founding date xD - Assert::lessThanEq($creationTime, (time() + 1) * 1000); + Assert::lessThanEq($creationTime, (time() + 2) * 1000); $obj = clone $this; $obj->creationTime = $creationTime; return $obj;

$obj = clone $this;
$obj->creationTime = $creationTime;
Expand All @@ -101,7 +102,7 @@
public function getCreationTimeAsDateTime(): \DateTimeImmutable
{
$dateTime = \DateTimeImmutable::createFromFormat('U.v', (string) ($this->creationTime / 1000));
Assert::notFalse($dateTime);

Check warning on line 105 in src/ValueObject/Fb.php

View workflow job for this annotation

GitHub Actions / Mutation tests (8.3, highest)

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ public function getCreationTimeAsDateTime(): \DateTimeImmutable { $dateTime = \DateTimeImmutable::createFromFormat('U.v', (string) ($this->creationTime / 1000)); - Assert::notFalse($dateTime); + return $dateTime; } public function getAppendix(): ?string

return $dateTime;
}
Expand Down
2 changes: 1 addition & 1 deletion src/ValueObject/Fbc.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ final class Fbc extends Fb
* - fb.1.1657051589577.IwAR0rmfgHgxjdKoEopat9y2SPzyjGgfHm9AhdqygToWvarP59nPq15T07MiA
* - fb.1.1788781160733.IwAR1a-b_c.AQECAQMB
*/
private const REGEXP_FBC = '/^fb\.([012])\.(\d{13})\.([A-Za-z0-9_-]+)(?:\.([A-Za-z0-9_-]{2,8}))?$/';
private const REGEXP_FBC = '/^fb\.(0|[1-9]\d{0,2})\.(\d{13})\.([A-Za-z0-9_-]+)(?:\.([A-Za-z0-9_-]{2,8}))?$/';

private string $clickId;

Expand Down
2 changes: 1 addition & 1 deletion src/ValueObject/Fbp.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static function fromString(string $value): self
{
// Must match something like this: fb.1.1656874832584.1088522659 or fb.1.1656874832584.1088522659.AQEAAQMB
// NOTICE we match for 13 digits for the creation time. That number will be 14 digits in year 2286, so I guess it's safe to test for a specific number of digits ;)
if (preg_match('/^fb\.([012])\.(\d{13})\.(\d+)(?:\.([A-Za-z0-9_-]{2,8}))?$/', $value, $matches) !== 1) {
if (preg_match('/^fb\.(0|[1-9]\d{0,2})\.(\d{13})\.(\d+)(?:\.([A-Za-z0-9_-]{2,8}))?$/', $value, $matches) !== 1) {
throw new InvalidArgumentException(sprintf('The value "%s" didn\'t match the expected pattern for fbp', $value));
}

Expand Down
3 changes: 2 additions & 1 deletion tests/ValueObject/FbTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public static function subdomainIndexes(): \Generator
yield 'com' => [Fb::SUBDOMAIN_INDEX_COM];
yield 'facebook.com' => [Fb::SUBDOMAIN_INDEX_FACEBOOK_COM];
yield 'www.facebook.com' => [Fb::SUBDOMAIN_INDEX_WWW_FACEBOOK_COM];
yield 'a deeper domain' => [3];
}

/**
Expand All @@ -70,7 +71,7 @@ public function it_rejects_an_invalid_subdomain_index(int $subdomainIndex): void
public static function invalidSubdomainIndexes(): \Generator
{
yield [-1];
yield [3];
yield [\PHP_INT_MIN];
}

/**
Expand Down
34 changes: 34 additions & 0 deletions tests/ValueObject/FbcTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,38 @@ public function it_keeps_the_appendix_through_the_immutable_setters(): void
self::assertSame('AQECAQMB', $fbc->withClickId('Other')->getAppendix());
self::assertSame('fb.2.1657051589577.IwAR1a-b_c.AQECAQMB', $fbc->withSubdomainIndex(2)->value());
}

/**
* @test
*/
public function it_accepts_a_subdomain_index_above_two(): void
{
$fbc = Fbc::fromString('fb.3.1657051589577.ClickId');

self::assertSame(3, $fbc->getSubdomainIndex());
self::assertSame('fb.3.1657051589577.ClickId', $fbc->value());
}

/**
* @test
*
* @dataProvider invalidSubdomainIndexes
*/
public function it_rejects_an_invalid_subdomain_index(string $subdomainIndex): void
{
$this->expectException(InvalidArgumentException::class);

Fbc::fromString(sprintf('fb.%s.1657051589577.ClickId', $subdomainIndex));
}

/**
* @return \Generator<string, array{string}>
*/
public static function invalidSubdomainIndexes(): \Generator
{
yield 'a leading zero, which could not be written back unchanged' => ['01'];
yield 'negative' => ['-1'];
yield 'more digits than a domain can have dots' => ['1000'];
yield 'not a number' => ['x'];
}
}
34 changes: 34 additions & 0 deletions tests/ValueObject/FbpTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,38 @@ public function it_keeps_the_appendix_through_the_immutable_setters(): void

self::assertSame('fb.1.1656874832584.123123.AQEAAQMB', $fbp->withRandomNumber(123123)->value());
}

/**
* @test
*/
public function it_accepts_a_subdomain_index_above_two(): void
{
$fbp = Fbp::fromString('fb.3.1656874832584.1088522659');

self::assertSame(3, $fbp->getSubdomainIndex());
self::assertSame('fb.3.1656874832584.1088522659', $fbp->value());
}

/**
* @test
*
* @dataProvider invalidSubdomainIndexes
*/
public function it_rejects_an_invalid_subdomain_index(string $subdomainIndex): void
{
$this->expectException(InvalidArgumentException::class);

Fbp::fromString(sprintf('fb.%s.1656874832584.1088522659', $subdomainIndex));
}

/**
* @return \Generator<string, array{string}>
*/
public static function invalidSubdomainIndexes(): \Generator
{
yield 'a leading zero, which could not be written back unchanged' => ['01'];
yield 'negative' => ['-1'];
yield 'more digits than a domain can have dots' => ['1000'];
yield 'not a number' => ['x'];
}
}
Loading
Loading