' . '' . ''; } public function testParsesActionsAndDoesNotPermitEditingWithoutHostCapabilities(): void { $server = new ServerAddress('https://OFFICE.test:443/'); self::assertSame('https://office.test/hosting/discovery', $server->discoveryUrl()); $document = DiscoveryDocument::parse($this->xml(), $server); self::assertTrue($document->hasProofKeys); self::assertTrue($document->actions[0]->supportedBy([])); $edit = DiscoveryDocument::parse($this->xml('edit'), $server)->actions[0]; self::assertFalse($edit->supportedBy([])); self::assertFalse($edit->supportedBy(['locks'])); self::assertTrue($edit->supportedBy(['locks', 'update'])); $extra = DiscoveryDocument::parse($this->xml('view', requires: ' containers, future '), $server)->actions[0]; self::assertFalse($extra->supportedBy([])); } public function testAcceptsEuroOfficeExternalHttpLabelOnlyWithTrustedHttpsUrls(): void { $xml = str_replace('external-https', 'external-http', $this->xml( url: 'https://office.test/editor?&', )); $document = DiscoveryDocument::parse($xml, new ServerAddress('https://office.test')); $source = 'https://app.test/wopi/files/123'; parse_str(parse_url($document->actions[0]->launchUrl($source), PHP_URL_QUERY), $parameters); self::assertSame(['wopisrc' => $source], $parameters); $this->expectException(InvalidArgumentException::class); DiscoveryDocument::parse(str_replace('https://office.test', 'http://office.test', $xml), new ServerAddress('https://office.test')); } #[DataProvider('invalidServers')] public function testRejectsInvalidServerAddresses(string $url): void { $this->expectException(InvalidArgumentException::class); new ServerAddress($url); } public static function invalidServers(): array { return array_map(static fn ($url) => [$url], [ 'http://office.test', 'file:///etc/passwd', 'https://user:pass@office.test', 'https://office.test?secret=x', 'https://office.test/#fragment', "https://office.test/\r\n", 'https://office.test\\@other.test', ]); } #[DataProvider('invalidDocuments')] public function testRejectsInvalidDiscovery(string $xml): void { $this->expectException(RuntimeException::class); DiscoveryDocument::parse($xml, new ServerAddress('https://office.test')); } public static function invalidDocuments(): array { return [ [''], ['Welcome'], [''], [']>&x;'], [''], [str_repeat('x', DiscoveryDocument::MAX_BYTES + 1)], ]; } #[DataProvider('untrustedActions')] public function testRejectsUntrustedActions(string $url): void { $this->expectException(InvalidArgumentException::class); DiscoveryDocument::parse($this->xml(url: $url), new ServerAddress('https://office.test')); } public static function untrustedActions(): array { return array_map(static fn ($url) => [$url], [ 'http://office.test/editor', 'https://evil.test/editor', 'https://office.test.evil.test/editor', 'https://office.test:8443/editor', 'https://user@office.test/editor', '//office.test/editor', 'javascript:alert(1)', 'https://office.test/editor#fragment', ]); } #[DataProvider('launchTemplates')] public function testBuildsLaunchUrlWithoutTokenAndRemovesUnknownPlaceholders(string $template): void { $action = DiscoveryDocument::parse($this->xml(url: $template), new ServerAddress('https://office.test'))->actions[0]; $source = 'https://app.test/m/service_wopi/files/opaque-id'; $url = $action->launchUrl($source); self::assertStringNotContainsString('<', $url); self::assertStringNotContainsString('access_token', $url); parse_str(parse_url($url, PHP_URL_QUERY), $parameters); self::assertSame($source, $parameters['WOPISrc']); self::assertCount(1, $parameters); } public static function launchTemplates(): array { return [ ['https://office.test/editor'], ['https://office.test/editor?'], ['https://office.test/editor?'], ['https://office.test/editor?'], ]; } public function testCommandReports404WithoutClaimingConnectionSuccess(): void { $client = $this->createStub(DiscoveryClient::class); $client->method('fetch')->willThrowException(new RuntimeException('Discovery returned HTTP 404.')); $command = new CommandTester(new CheckCommand($client)); self::assertSame(1, $command->execute(['server' => 'https://office.test'])); self::assertStringContainsString('HTTP 404', $command->getDisplay()); } public function testCommandQualifiesViewingOnly(): void { $client = $this->createStub(DiscoveryClient::class); $client->method('fetch')->willReturn(DiscoveryDocument::parse($this->xml(), new ServerAddress('https://office.test'))); $command = new CommandTester(new CheckCommand($client)); self::assertSame(0, $command->execute(['server' => 'https://office.test'])); self::assertStringContainsString('does not verify', $command->getDisplay()); } public function testEditOnlyDiscoveryDoesNotPassReadOnlyQualification(): void { $client = $this->createStub(DiscoveryClient::class); $client->method('fetch')->willReturn(DiscoveryDocument::parse($this->xml('edit'), new ServerAddress('https://office.test'))); $command = new CommandTester(new CheckCommand($client)); self::assertSame(1, $command->execute(['server' => 'https://office.test'])); } }