Обновление клиента (apps, 3rdparty, install)

This commit is contained in:
root
2026-03-16 08:42:57 +00:00
parent b8905de237
commit f390426546
3354 changed files with 505213 additions and 3 deletions
+20
View File
@@ -0,0 +1,20 @@
Copyright (c) 2014-2024 Fusonic GmbH (https://www.fusonic.net)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+142
View File
@@ -0,0 +1,142 @@
<?php
/*
* Copyright (c) Fusonic GmbH. All rights reserved.
* Licensed under the MIT License. See LICENSE file in the project root for license information.
*/
declare(strict_types=1);
namespace Fusonic\OpenGraph;
use Fusonic\OpenGraph\Objects\ObjectBase;
use Fusonic\OpenGraph\Objects\Website;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Symfony\Component\DomCrawler\Crawler;
/**
* Consumer that extracts Open Graph data from either a URL or an HTML string.
*/
class Consumer
{
/**
* When enabled, crawler will read content of title and meta description if no
* Open Graph data is provided by target page.
*/
public bool $useFallbackMode = false;
/**
* When enabled, crawler will throw exceptions for some crawling errors like unexpected
* Open Graph elements.
*/
public bool $debug = false;
/**
* @param ClientInterface|null $client a PSR-18 ClientInterface implementation
* @param RequestFactoryInterface|null $requestFactory a PSR-17 RequestFactoryInterface implementation
*/
public function __construct(
private ?ClientInterface $client = null,
private ?RequestFactoryInterface $requestFactory = null,
) {
}
/**
* Fetches HTML content from the given URL and then crawls it for Open Graph data.
*
* @param string $url URL to be crawled
*
* @throws ClientExceptionInterface
*/
public function loadUrl(string $url): ObjectBase
{
if (null === $this->client || null === $this->requestFactory) {
throw new \LogicException(
'To use loadUrl() you must provide $client and $requestFactory when instantiating the consumer.'
);
}
$request = $this->requestFactory->createRequest('GET', $url);
$response = $this->client->sendRequest($request);
return $this->loadHtml($response->getBody()->getContents(), $url);
}
/**
* Crawls the given HTML string for OpenGraph data.
*
* @param string $html HTML string, usually whole content of crawled web resource
* @param string|null $fallbackUrl URL to use when fallback mode is enabled
*/
public function loadHtml(string $html, ?string $fallbackUrl = null): ObjectBase
{
// Extract all data that can be found
$page = $this->extractOpenGraphData($html);
// Use the user's URL as fallback
if ($this->useFallbackMode && null === $page->url) {
$page->url = $fallbackUrl;
}
// Return result
return $page;
}
private function extractOpenGraphData(string $content): ObjectBase
{
$crawler = new Crawler();
$crawler->addHtmlContent(content: $content);
$properties = [];
foreach (['name', 'property'] as $t) {
// Get all meta-tags starting with "og:"
$ogMetaTags = $crawler->filter("meta[{$t}^='og:']");
// Create clean property array
$props = [];
/** @var \DOMElement $tag */
foreach ($ogMetaTags as $tag) {
$name = strtolower(trim($tag->getAttribute($t)));
$value = trim($tag->getAttribute('content'));
$props[] = new Property($name, $value);
}
$properties = array_merge($properties, $props);
}
// Create new object
$object = new Website();
// Assign all properties to the object
$object->assignProperties($properties, $this->debug);
// Fallback for url
if ($this->useFallbackMode && null === $object->url) {
$urlElement = $crawler->filter("link[rel='canonical']")->first();
if ($urlElement->count() > 0) {
$object->url = trim($urlElement->attr('href') ?? '');
}
}
// Fallback for title
if ($this->useFallbackMode && null === $object->title) {
$titleElement = $crawler->filter('title')->first();
if ($titleElement->count() > 0) {
$object->title = trim($titleElement->text());
}
}
// Fallback for description
if ($this->useFallbackMode && null === $object->description) {
$descriptionElement = $crawler->filter("meta[property='description']")->first();
if ($descriptionElement->count() > 0) {
$object->description = trim($descriptionElement->attr('content') ?? '');
}
}
return $object;
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
/*
* Copyright (c) Fusonic GmbH. All rights reserved.
* Licensed under the MIT License. See LICENSE file in the project root for license information.
*/
declare(strict_types=1);
namespace Fusonic\OpenGraph\Elements;
use Fusonic\OpenGraph\Property;
/**
* An Open Graph audio element.
*/
class Audio extends ElementBase
{
/**
* The URL of an audio resource associated with the object.
*/
public ?string $url = null;
/**
* An alternate URL to use if an audio resource requires HTTPS.
*/
public ?string $secureUrl = null;
/**
* The MIME type of an audio resource associated with the object.
*/
public ?string $type = null;
/**
* @param string $url URL to the audio file
*/
public function __construct(string $url)
{
$this->url = $url;
}
/**
* Gets all properties set on this element.
*
* @return Property[]
*/
public function getProperties(): array
{
$properties = [];
// URL must precede all other properties
if (null !== $this->url) {
$properties[] = new Property(Property::AUDIO_URL, $this->url);
}
if (null !== $this->secureUrl) {
$properties[] = new Property(Property::AUDIO_SECURE_URL, $this->secureUrl);
}
if (null !== $this->type) {
$properties[] = new Property(Property::AUDIO_TYPE, $this->type);
}
return $properties;
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
/*
* Copyright (c) Fusonic GmbH. All rights reserved.
* Licensed under the MIT License. See LICENSE file in the project root for license information.
*/
declare(strict_types=1);
namespace Fusonic\OpenGraph\Elements;
use Fusonic\OpenGraph\Property;
/**
* Abstract base class for all OpenGraph elements (e.g. images, videos etc.).
*/
abstract class ElementBase
{
/**
* Gets all properties set on this element.
*
* @return Property[]
*/
abstract public function getProperties(): array;
}
+93
View File
@@ -0,0 +1,93 @@
<?php
/*
* Copyright (c) Fusonic GmbH. All rights reserved.
* Licensed under the MIT License. See LICENSE file in the project root for license information.
*/
declare(strict_types=1);
namespace Fusonic\OpenGraph\Elements;
use Fusonic\OpenGraph\Property;
/**
* An Open Graph image element.
*/
class Image extends ElementBase
{
/**
* The URL of an image resource associated with the object.
*/
public ?string $url = null;
/**
* An alternate URL to use if an image resource requires HTTPS.
*/
public ?string $secureUrl = null;
/**
* The MIME type of an image resource.
*/
public ?string $type = null;
/**
* The width of an image resource in pixels.
*/
public ?int $width = null;
/**
* The height of an image resource in pixels.
*/
public ?int $height = null;
/**
* Whether the image is user-generated or not.
*/
public ?bool $userGenerated = null;
/**
* @param string $url URL to the image file
*/
public function __construct(string $url)
{
$this->url = $url;
}
/**
* Gets all properties set on this element.
*
* @return Property[]
*/
public function getProperties(): array
{
$properties = [];
// URL must precede all other properties
if (null !== $this->url) {
$properties[] = new Property(Property::IMAGE_URL, $this->url);
}
if (null !== $this->height) {
$properties[] = new Property(Property::IMAGE_HEIGHT, $this->height);
}
if (null !== $this->secureUrl) {
$properties[] = new Property(Property::IMAGE_SECURE_URL, $this->secureUrl);
}
if (null !== $this->type) {
$properties[] = new Property(Property::IMAGE_TYPE, $this->type);
}
if (null !== $this->width) {
$properties[] = new Property(Property::IMAGE_WIDTH, $this->width);
}
if (null !== $this->userGenerated) {
$properties[] = new Property(Property::IMAGE_USER_GENERATED, $this->userGenerated);
}
return $properties;
}
}
+84
View File
@@ -0,0 +1,84 @@
<?php
/*
* Copyright (c) Fusonic GmbH. All rights reserved.
* Licensed under the MIT License. See LICENSE file in the project root for license information.
*/
declare(strict_types=1);
namespace Fusonic\OpenGraph\Elements;
use Fusonic\OpenGraph\Property;
/**
* An OpenGraph video element.
*/
class Video extends ElementBase
{
/**
* The URL of a video resource associated with the object.
*/
public ?string $url = null;
/**
* An alternate URL to use if a video resource requires HTTPS.
*/
public ?string $secureUrl = null;
/**
* The MIME type of a video resource associated with the object.
*/
public ?string $type = null;
/**
* The width of a video resource associated with the object in pixels.
*/
public ?int $width = null;
/**
* The height of a video resource associated with the object in pixels.
*/
public ?int $height = null;
/**
* @param string $url URL to the video
*/
public function __construct(string $url)
{
$this->url = $url;
}
/**
* Gets all properties set on this element.
*
* @return Property[]
*/
public function getProperties(): array
{
$properties = [];
// URL must precede all other properties
if (null !== $this->url) {
$properties[] = new Property(Property::VIDEO_URL, $this->url);
}
if (null !== $this->height) {
$properties[] = new Property(Property::VIDEO_HEIGHT, $this->height);
}
if (null !== $this->secureUrl) {
$properties[] = new Property(Property::VIDEO_SECURE_URL, $this->secureUrl);
}
if (null !== $this->type) {
$properties[] = new Property(Property::VIDEO_TYPE, $this->type);
}
if (null !== $this->width) {
$properties[] = new Property(Property::VIDEO_WIDTH, $this->width);
}
return $properties;
}
}
+355
View File
@@ -0,0 +1,355 @@
<?php
/*
* Copyright (c) Fusonic GmbH. All rights reserved.
* Licensed under the MIT License. See LICENSE file in the project root for license information.
*/
declare(strict_types=1);
namespace Fusonic\OpenGraph\Objects;
use Fusonic\OpenGraph\Elements\Audio;
use Fusonic\OpenGraph\Elements\Image;
use Fusonic\OpenGraph\Elements\Video;
use Fusonic\OpenGraph\Property;
/**
* Abstract base class for all Open Graph objects (website, video, ...).
*/
abstract class ObjectBase
{
/**
* An array of audio resources attached to the object.
*
* @var Audio[]
*/
public array $audios = [];
/**
* A short description of the object.
*/
public ?string $description = null;
/**
* The word that appears before the object's title in a sentence. This is an list of words from 'a', 'an', 'the',
* ' "" ', or 'auto'. If 'auto' is chosen, the consumer of the object will chose between 'a' or 'an'. The default is
* the blank, "".
*/
public ?string $determiner = null;
/**
* An array of images attached to the object.
*
* @var Image[]
*/
public array $images = [];
/**
* The locale that the object's tags are marked up in, in the format language_TERRITORY.
*/
public ?string $locale = null;
/**
* An array of alternate locales in which the resource is available.
*
* @var string[]
*/
public array $localeAlternate = [];
public ?bool $richAttachment = null;
/**
* An array of URLs of related resources.
*
* @var string[]
*/
public array $seeAlso = [];
/**
* The name of the web site upon which the object resides.
*/
public ?string $siteName = null;
/**
* The title of the object as it should appear in the graph.
*/
public ?string $title = null;
/**
* The type of the object, such as 'article'.
*/
public ?string $type = null;
/**
* The time when the object was last updated.
*/
public ?\DateTimeImmutable $updatedTime = null;
/**
* The canonical URL of the object, used as its ID in the graph.
*/
public ?string $url = null;
/**
* An array of videos attached to the object.
*
* @var Video[]
*/
public array $videos = [];
/**
* Assigns all properties given to the this Object instance.
*
* @param array|Property[] $properties array of all properties to assign
* @param bool $debug throw exceptions when parsing or not
*
* @throws \UnexpectedValueException
*/
public function assignProperties(array $properties, bool $debug = false): void
{
foreach ($properties as $property) {
$name = $property->key;
$value = $property->value;
switch ($name) {
case Property::AUDIO:
case Property::AUDIO_URL:
$this->audios[] = new Audio($value);
break;
case Property::AUDIO_SECURE_URL:
case Property::AUDIO_TYPE:
if (\count($this->audios) > 0) {
$this->handleAudioAttribute($this->audios[\count($this->audios) - 1], $name, $value);
} elseif ($debug) {
throw new \UnexpectedValueException(
\sprintf(
"Found '%s' property but no audio was found before.",
$name
)
);
}
break;
case Property::DESCRIPTION:
if (null === $this->description) {
$this->description = $value;
}
break;
case Property::DETERMINER:
if (null === $this->determiner) {
$this->determiner = $value;
}
break;
case Property::IMAGE:
case Property::IMAGE_URL:
$this->images[] = new Image($value);
break;
case Property::IMAGE_HEIGHT:
case Property::IMAGE_SECURE_URL:
case Property::IMAGE_TYPE:
case Property::IMAGE_WIDTH:
case Property::IMAGE_USER_GENERATED:
if (\count($this->images) > 0) {
$this->handleImageAttribute($this->images[\count($this->images) - 1], $name, $value);
} elseif ($debug) {
throw new \UnexpectedValueException(
\sprintf(
"Found '%s' property but no image was found before.",
$name
)
);
}
break;
case Property::LOCALE:
if (null === $this->locale) {
$this->locale = $value;
}
break;
case Property::LOCALE_ALTERNATE:
$this->localeAlternate[] = $value;
break;
case Property::RICH_ATTACHMENT:
$this->richAttachment = $this->convertToBoolean($value);
break;
case Property::SEE_ALSO:
$this->seeAlso[] = $value;
break;
case Property::SITE_NAME:
if (null === $this->siteName) {
$this->siteName = $value;
}
break;
case Property::TITLE:
if (null === $this->title) {
$this->title = $value;
}
break;
case Property::UPDATED_TIME:
if (null === $this->updatedTime) {
$this->updatedTime = $this->convertToDateTime($value);
}
break;
case Property::URL:
if (null === $this->url) {
$this->url = $value;
}
break;
case Property::VIDEO:
case Property::VIDEO_URL:
$this->videos[] = new Video($value);
break;
case Property::VIDEO_HEIGHT:
case Property::VIDEO_SECURE_URL:
case Property::VIDEO_TYPE:
case Property::VIDEO_WIDTH:
if (\count($this->videos) > 0) {
$this->handleVideoAttribute($this->videos[\count($this->videos) - 1], $name, $value);
} elseif ($debug) {
throw new \UnexpectedValueException(\sprintf(
"Found '%s' property but no video was found before.",
$name
));
}
}
}
}
private function handleImageAttribute(Image $element, string $name, string $value): void
{
switch ($name) {
case Property::IMAGE_HEIGHT:
$element->height = (int) $value;
break;
case Property::IMAGE_WIDTH:
$element->width = (int) $value;
break;
case Property::IMAGE_TYPE:
$element->type = $value;
break;
case Property::IMAGE_SECURE_URL:
$element->secureUrl = $value;
break;
case Property::IMAGE_USER_GENERATED:
$element->userGenerated = $this->convertToBoolean($value);
break;
}
}
private function handleVideoAttribute(Video $element, string $name, string $value): void
{
switch ($name) {
case Property::VIDEO_HEIGHT:
$element->height = (int) $value;
break;
case Property::VIDEO_WIDTH:
$element->width = (int) $value;
break;
case Property::VIDEO_TYPE:
$element->type = $value;
break;
case Property::VIDEO_SECURE_URL:
$element->secureUrl = $value;
break;
}
}
private function handleAudioAttribute(Audio $element, string $name, string $value): void
{
switch ($name) {
case Property::AUDIO_TYPE:
$element->type = $value;
break;
case Property::AUDIO_SECURE_URL:
$element->secureUrl = $value;
break;
}
}
protected function convertToDateTime(string $value): ?\DateTimeImmutable
{
try {
return new \DateTimeImmutable($value);
} catch (\Exception $e) {
return null;
}
}
protected function convertToBoolean(string $value): bool
{
switch (strtolower($value)) {
case '1':
case 'true':
return true;
default:
return false;
}
}
/**
* Gets all properties set on this object.
*
* @return Property[]
*/
public function getProperties(): array
{
$properties = [];
foreach ($this->audios as $audio) {
$properties = array_merge($properties, $audio->getProperties());
}
if (null !== $this->title) {
$properties[] = new Property(Property::TITLE, $this->title);
}
if (null !== $this->description) {
$properties[] = new Property(Property::DESCRIPTION, $this->description);
}
if (null !== $this->determiner) {
$properties[] = new Property(Property::DETERMINER, $this->determiner);
}
foreach ($this->images as $image) {
$properties = array_merge($properties, $image->getProperties());
}
if (null !== $this->locale) {
$properties[] = new Property(Property::LOCALE, $this->locale);
}
foreach ($this->localeAlternate as $locale) {
$properties[] = new Property(Property::LOCALE_ALTERNATE, $locale);
}
if (null !== $this->richAttachment) {
$properties[] = new Property(Property::RICH_ATTACHMENT, (int) $this->richAttachment);
}
foreach ($this->seeAlso as $seeAlso) {
$properties[] = new Property(Property::SEE_ALSO, $seeAlso);
}
if (null !== $this->siteName) {
$properties[] = new Property(Property::SITE_NAME, $this->siteName);
}
if (null !== $this->type) {
$properties[] = new Property(Property::TYPE, $this->type);
}
if (null !== $this->updatedTime) {
$properties[] = new Property(Property::UPDATED_TIME, $this->updatedTime->format('c'));
}
if (null !== $this->url) {
$properties[] = new Property(Property::URL, $this->url);
}
foreach ($this->videos as $video) {
$properties = array_merge($properties, $video->getProperties());
}
return $properties;
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
/*
* Copyright (c) Fusonic GmbH. All rights reserved.
* Licensed under the MIT License. See LICENSE file in the project root for license information.
*/
declare(strict_types=1);
namespace Fusonic\OpenGraph\Objects;
/**
* This object type represents a website. It is a simple object type and uses only common Open Graph properties. For
* specific pages within a website, the article object type should be used.
*
* https://developers.facebook.com/docs/reference/opengraph/object-type/website/
*/
class Website extends ObjectBase
{
public const TYPE = 'website';
public function __construct()
{
$this->type = self::TYPE;
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
/*
* Copyright (c) Fusonic GmbH. All rights reserved.
* Licensed under the MIT License. See LICENSE file in the project root for license information.
*/
declare(strict_types=1);
namespace Fusonic\OpenGraph;
/**
* Class holding data for a single Open Graph property on a web page.
*/
class Property
{
public const AUDIO = 'og:audio';
public const AUDIO_SECURE_URL = 'og:audio:secure_url';
public const AUDIO_TYPE = 'og:audio:type';
public const AUDIO_URL = 'og:audio:url';
public const DESCRIPTION = 'og:description';
public const DETERMINER = 'og:determiner';
public const IMAGE = 'og:image';
public const IMAGE_HEIGHT = 'og:image:height';
public const IMAGE_SECURE_URL = 'og:image:secure_url';
public const IMAGE_TYPE = 'og:image:type';
public const IMAGE_URL = 'og:image:url';
public const IMAGE_WIDTH = 'og:image:width';
public const IMAGE_USER_GENERATED = 'og:image:user_generated';
public const LOCALE = 'og:locale';
public const LOCALE_ALTERNATE = 'og:locale:alternate';
public const RICH_ATTACHMENT = 'og:rich_attachment';
public const SEE_ALSO = 'og:see_also';
public const SITE_NAME = 'og:site_name';
public const TITLE = 'og:title';
public const TYPE = 'og:type';
public const UPDATED_TIME = 'og:updated_time';
public const URL = 'og:url';
public const VIDEO = 'og:video';
public const VIDEO_HEIGHT = 'og:video:height';
public const VIDEO_SECURE_URL = 'og:video:secure_url';
public const VIDEO_TYPE = 'og:video:type';
public const VIDEO_URL = 'og:video:url';
public const VIDEO_WIDTH = 'og:video:width';
public function __construct(
/**
* Key of the property without "og:" prefix.
*/
public string $key,
/**
* Value of the property.
*/
public mixed $value,
) {
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
/*
* Copyright (c) Fusonic GmbH. All rights reserved.
* Licensed under the MIT License. See LICENSE file in the project root for license information.
*/
declare(strict_types=1);
namespace Fusonic\OpenGraph;
use Fusonic\OpenGraph\Objects\ObjectBase;
/**
* Class for generating Open Graph tags from objects.
*/
class Publisher
{
public const DOCTYPE_HTML5 = 1;
public const DOCTYPE_XHTML = 2;
/**
* Defines the style in which HTML tags should be written. Use one of Publisher::DOCTYPE_HTML5 or
* Publisher::DOCTYPE_XHTML.
*/
public int $doctype = self::DOCTYPE_HTML5;
/**
* Generated HTML tags from the given object.
*/
public function generateHtml(ObjectBase $object): string
{
$html = '';
$format = '<meta property="%s" content="%s"'.(self::DOCTYPE_XHTML === $this->doctype ? ' />' : '>');
foreach ($object->getProperties() as $property) {
if ('' !== $html) {
$html .= "\n";
}
if (null === $property->value) {
continue;
} elseif ($property->value instanceof \DateTimeInterface) {
$value = $property->value->format('c');
} elseif (\is_object($property->value)) {
throw new \UnexpectedValueException(
\sprintf(
"Cannot handle value of type '%s' for property '%s'.",
\get_class($property->value),
$property->key
)
);
} elseif (true === $property->value) {
$value = '1';
} elseif (false === $property->value) {
$value = '0';
} else {
$value = (string) $property->value;
}
$html .= \sprintf($format, $property->key, htmlspecialchars($value));
}
return $html;
}
}