Current File : /var/www/pediatribu/wp-content/plugins/google-site-kit/third-party/google/auth/src/OAuth2.php
<?php

/*
 * Copyright 2015 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
namespace Google\Site_Kit_Dependencies\Google\Auth;

use Google\Site_Kit_Dependencies\Firebase\JWT\JWT;
use Google\Site_Kit_Dependencies\Firebase\JWT\Key;
use Google\Site_Kit_Dependencies\Google\Auth\HttpHandler\HttpClientCache;
use Google\Site_Kit_Dependencies\Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Site_Kit_Dependencies\GuzzleHttp\Psr7\Query;
use Google\Site_Kit_Dependencies\GuzzleHttp\Psr7\Request;
use Google\Site_Kit_Dependencies\GuzzleHttp\Psr7\Utils;
use InvalidArgumentException;
use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestInterface;
use Google\Site_Kit_Dependencies\Psr\Http\Message\ResponseInterface;
use Google\Site_Kit_Dependencies\Psr\Http\Message\UriInterface;
/**
 * OAuth2 supports authentication by OAuth2 2-legged flows.
 *
 * It primary supports
 * - service account authorization
 * - authorization where a user already has an access token
 */
class OAuth2 implements \Google\Site_Kit_Dependencies\Google\Auth\FetchAuthTokenInterface
{
    const DEFAULT_EXPIRY_SECONDS = 3600;
    // 1 hour
    const DEFAULT_SKEW_SECONDS = 60;
    // 1 minute
    const JWT_URN = 'urn:ietf:params:oauth:grant-type:jwt-bearer';
    const STS_URN = 'urn:ietf:params:oauth:grant-type:token-exchange';
    private const STS_REQUESTED_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
    /**
     * TODO: determine known methods from the keys of JWT::methods.
     *
     * @var array<string>
     */
    public static $knownSigningAlgorithms = ['HS256', 'HS512', 'HS384', 'RS256'];
    /**
     * The well known grant types.
     *
     * @var array<string>
     */
    public static $knownGrantTypes = ['authorization_code', 'refresh_token', 'password', 'client_credentials'];
    /**
     * - authorizationUri
     *   The authorization server's HTTP endpoint capable of
     *   authenticating the end-user and obtaining authorization.
     *
     * @var ?UriInterface
     */
    private $authorizationUri;
    /**
     * - tokenCredentialUri
     *   The authorization server's HTTP endpoint capable of issuing
     *   tokens and refreshing expired tokens.
     *
     * @var UriInterface
     */
    private $tokenCredentialUri;
    /**
     * The redirection URI used in the initial request.
     *
     * @var ?string
     */
    private $redirectUri;
    /**
     * A unique identifier issued to the client to identify itself to the
     * authorization server.
     *
     * @var string
     */
    private $clientId;
    /**
     * A shared symmetric secret issued by the authorization server, which is
     * used to authenticate the client.
     *
     * @var string
     */
    private $clientSecret;
    /**
     * The resource owner's username.
     *
     * @var ?string
     */
    private $username;
    /**
     * The resource owner's password.
     *
     * @var ?string
     */
    private $password;
    /**
     * The scope of the access request, expressed either as an Array or as a
     * space-delimited string.
     *
     * @var ?array<string>
     */
    private $scope;
    /**
     * An arbitrary string designed to allow the client to maintain state.
     *
     * @var string
     */
    private $state;
    /**
     * The authorization code issued to this client.
     *
     * Only used by the authorization code access grant type.
     *
     * @var ?string
     */
    private $code;
    /**
     * The issuer ID when using assertion profile.
     *
     * @var ?string
     */
    private $issuer;
    /**
     * The target audience for assertions.
     *
     * @var string
     */
    private $audience;
    /**
     * The target sub when issuing assertions.
     *
     * @var string
     */
    private $sub;
    /**
     * The number of seconds assertions are valid for.
     *
     * @var int
     */
    private $expiry;
    /**
     * The signing key when using assertion profile.
     *
     * @var ?string
     */
    private $signingKey;
    /**
     * The signing key id when using assertion profile. Param kid in jwt header
     *
     * @var string
     */
    private $signingKeyId;
    /**
     * The signing algorithm when using an assertion profile.
     *
     * @var ?string
     */
    private $signingAlgorithm;
    /**
     * The refresh token associated with the access token to be refreshed.
     *
     * @var ?string
     */
    private $refreshToken;
    /**
     * The current access token.
     *
     * @var string
     */
    private $accessToken;
    /**
     * The current ID token.
     *
     * @var string
     */
    private $idToken;
    /**
     * The scopes granted to the current access token
     *
     * @var string
     */
    private $grantedScope;
    /**
     * The lifetime in seconds of the current access token.
     *
     * @var ?int
     */
    private $expiresIn;
    /**
     * The expiration time of the access token as a number of seconds since the
     * unix epoch.
     *
     * @var ?int
     */
    private $expiresAt;
    /**
     * The issue time of the access token as a number of seconds since the unix
     * epoch.
     *
     * @var ?int
     */
    private $issuedAt;
    /**
     * The current grant type.
     *
     * @var ?string
     */
    private $grantType;
    /**
     * When using an extension grant type, this is the set of parameters used by
     * that extension.
     *
     * @var array<mixed>
     */
    private $extensionParams;
    /**
     * When using the toJwt function, these claims will be added to the JWT
     * payload.
     *
     * @var array<mixed>
     */
    private $additionalClaims;
    /**
     * The code verifier for PKCE for OAuth 2.0. When set, the authorization
     * URI will contain the Code Challenge and Code Challenge Method querystring
     * parameters, and the token URI will contain the Code Verifier parameter.
     *
     * @see https://datatracker.ietf.org/doc/html/rfc7636
     * @var ?string
     */
    private $codeVerifier;
    /**
     * For STS requests.
     * A URI that indicates the target service or resource where the client
     * intends to use the requested security token.
     */
    private ?string $resource;
    /**
     * For STS requests.
     * A fetcher for the "subject_token", which is a security token that
     * represents the identity of the party on behalf of whom the request is
     * being made.
     */
    private ?\Google\Site_Kit_Dependencies\Google\Auth\ExternalAccountCredentialSourceInterface $subjectTokenFetcher;
    /**
     * For STS requests.
     * An identifier, that indicates the type of the security token in the
     * subjectToken parameter.
     */
    private ?string $subjectTokenType;
    /**
     * For STS requests.
     * A security token that represents the identity of the acting party.
     */
    private ?string $actorToken;
    /**
     * For STS requests.
     * An identifier that indicates the type of the security token in the
     * actorToken parameter.
     */
    private ?string $actorTokenType;
    /**
     * From STS response.
     * An identifier for the representation of the issued security token.
     */
    private ?string $issuedTokenType = null;
    /**
     * From STS response.
     * An identifier for the representation of the issued security token.
     *
     * @var array<mixed>
     */
    private array $additionalOptions;
    /**
     * Create a new OAuthCredentials.
     *
     * The configuration array accepts various options
     *
     * - authorizationUri
     *   The authorization server's HTTP endpoint capable of
     *   authenticating the end-user and obtaining authorization.
     *
     * - tokenCredentialUri
     *   The authorization server's HTTP endpoint capable of issuing
     *   tokens and refreshing expired tokens.
     *
     * - clientId
     *   A unique identifier issued to the client to identify itself to the
     *   authorization server.
     *
     * - clientSecret
     *   A shared symmetric secret issued by the authorization server,
     *   which is used to authenticate the client.
     *
     * - scope
     *   The scope of the access request, expressed either as an Array
     *   or as a space-delimited String.
     *
     * - state
     *   An arbitrary string designed to allow the client to maintain state.
     *
     * - redirectUri
     *   The redirection URI used in the initial request.
     *
     * - username
     *   The resource owner's username.
     *
     * - password
     *   The resource owner's password.
     *
     * - issuer
     *   Issuer ID when using assertion profile
     *
     * - audience
     *   Target audience for assertions
     *
     * - expiry
     *   Number of seconds assertions are valid for
     *
     * - signingKey
     *   Signing key when using assertion profile
     *
     * - signingKeyId
     *   Signing key id when using assertion profile
     *
     * - refreshToken
     *   The refresh token associated with the access token
     *   to be refreshed.
     *
     * - accessToken
     *   The current access token for this client.
     *
     * - idToken
     *   The current ID token for this client.
     *
     * - extensionParams
     *   When using an extension grant type, this is the set of parameters used
     *   by that extension.
     *
     * - codeVerifier
     *   The code verifier for PKCE for OAuth 2.0.
     *
     * - resource
     *   The target service or resource where the client ntends to use the
     *   requested security token.
     *
     * - subjectTokenFetcher
     *    A fetcher for the "subject_token", which is a security token that
     *    represents the identity of the party on behalf of whom the request is
     *    being made.
     *
     * - subjectTokenType
     *   An identifier that indicates the type of the security token in the
     *   subjectToken parameter.
     *
     * - actorToken
     *   A security token that represents the identity of the acting party.
     *
     * - actorTokenType
     *   An identifier for the representation of the issued security token.
     *
     * @param array<mixed> $config Configuration array
     */
    public function __construct(array $config)
    {
        $opts = \array_merge(['expiry' => self::DEFAULT_EXPIRY_SECONDS, 'extensionParams' => [], 'authorizationUri' => null, 'redirectUri' => null, 'tokenCredentialUri' => null, 'state' => null, 'username' => null, 'password' => null, 'clientId' => null, 'clientSecret' => null, 'issuer' => null, 'sub' => null, 'audience' => null, 'signingKey' => null, 'signingKeyId' => null, 'signingAlgorithm' => null, 'scope' => null, 'additionalClaims' => [], 'codeVerifier' => null, 'resource' => null, 'subjectTokenFetcher' => null, 'subjectTokenType' => null, 'actorToken' => null, 'actorTokenType' => null, 'additionalOptions' => []], $config);
        $this->setAuthorizationUri($opts['authorizationUri']);
        $this->setRedirectUri($opts['redirectUri']);
        $this->setTokenCredentialUri($opts['tokenCredentialUri']);
        $this->setState($opts['state']);
        $this->setUsername($opts['username']);
        $this->setPassword($opts['password']);
        $this->setClientId($opts['clientId']);
        $this->setClientSecret($opts['clientSecret']);
        $this->setIssuer($opts['issuer']);
        $this->setSub($opts['sub']);
        $this->setExpiry($opts['expiry']);
        $this->setAudience($opts['audience']);
        $this->setSigningKey($opts['signingKey']);
        $this->setSigningKeyId($opts['signingKeyId']);
        $this->setSigningAlgorithm($opts['signingAlgorithm']);
        $this->setScope($opts['scope']);
        $this->setExtensionParams($opts['extensionParams']);
        $this->setAdditionalClaims($opts['additionalClaims']);
        $this->setCodeVerifier($opts['codeVerifier']);
        // for STS
        $this->resource = $opts['resource'];
        $this->subjectTokenFetcher = $opts['subjectTokenFetcher'];
        $this->subjectTokenType = $opts['subjectTokenType'];
        $this->actorToken = $opts['actorToken'];
        $this->actorTokenType = $opts['actorTokenType'];
        $this->additionalOptions = $opts['additionalOptions'];
        $this->updateToken($opts);
    }
    /**
     * Verifies the idToken if present.
     *
     * - if none is present, return null
     * - if present, but invalid, raises DomainException.
     * - otherwise returns the payload in the idtoken as a PHP object.
     *
     * The behavior of this method varies depending on the version of
     * `firebase/php-jwt` you are using. In versions 6.0 and above, you cannot
     * provide multiple $allowed_algs, and instead must provide an array of Key
     * objects as the $publicKey.
     *
     * @param string|Key|Key[] $publicKey The public key to use to authenticate the token
     * @param string|array<string> $allowed_algs algorithm or array of supported verification algorithms.
     *        Providing more than one algorithm will throw an exception.
     * @throws \DomainException if the token is missing an audience.
     * @throws \DomainException if the audience does not match the one set in
     *         the OAuth2 class instance.
     * @throws \UnexpectedValueException If the token is invalid
     * @throws \InvalidArgumentException If more than one value for allowed_algs is supplied
     * @throws \Firebase\JWT\SignatureInvalidException If the signature is invalid.
     * @throws \Firebase\JWT\BeforeValidException If the token is not yet valid.
     * @throws \Firebase\JWT\ExpiredException If the token has expired.
     * @return null|object
     */
    public function verifyIdToken($publicKey = null, $allowed_algs = [])
    {
        $idToken = $this->getIdToken();
        if (\is_null($idToken)) {
            return null;
        }
        $resp = $this->jwtDecode($idToken, $publicKey, $allowed_algs);
        if (!\property_exists($resp, 'aud')) {
            throw new \DomainException('No audience found the id token');
        }
        if ($resp->aud != $this->getAudience()) {
            throw new \DomainException('Wrong audience present in the id token');
        }
        return $resp;
    }
    /**
     * Obtains the encoded jwt from the instance data.
     *
     * @param array<mixed> $config array optional configuration parameters
     * @return string
     */
    public function toJwt(array $config = [])
    {
        if (\is_null($this->getSigningKey())) {
            throw new \DomainException('No signing key available');
        }
        if (\is_null($this->getSigningAlgorithm())) {
            throw new \DomainException('No signing algorithm specified');
        }
        $now = \time();
        $opts = \array_merge(['skew' => self::DEFAULT_SKEW_SECONDS], $config);
        $assertion = ['iss' => $this->getIssuer(), 'exp' => $now + $this->getExpiry(), 'iat' => $now - $opts['skew']];
        foreach ($assertion as $k => $v) {
            if (\is_null($v)) {
                throw new \DomainException($k . ' should not be null');
            }
        }
        if (!\is_null($this->getAudience())) {
            $assertion['aud'] = $this->getAudience();
        }
        if (!\is_null($this->getScope())) {
            $assertion['scope'] = $this->getScope();
        }
        if (empty($assertion['scope']) && empty($assertion['aud'])) {
            throw new \DomainException('one of scope or aud should not be null');
        }
        if (!\is_null($this->getSub())) {
            $assertion['sub'] = $this->getSub();
        }
        $assertion += $this->getAdditionalClaims();
        return \Google\Site_Kit_Dependencies\Firebase\JWT\JWT::encode($assertion, $this->getSigningKey(), $this->getSigningAlgorithm(), $this->getSigningKeyId());
    }
    /**
     * Generates a request for token credentials.
     *
     * @param callable $httpHandler callback which delivers psr7 request
     * @return RequestInterface the authorization Url.
     */
    public function generateCredentialsRequest(?callable $httpHandler = null)
    {
        $uri = $this->getTokenCredentialUri();
        if (\is_null($uri)) {
            throw new \DomainException('No token credential URI was set.');
        }
        $grantType = $this->getGrantType();
        $params = ['grant_type' => $grantType];
        switch ($grantType) {
            case 'authorization_code':
                $params['code'] = $this->getCode();
                $params['redirect_uri'] = $this->getRedirectUri();
                if ($this->codeVerifier) {
                    $params['code_verifier'] = $this->codeVerifier;
                }
                $this->addClientCredentials($params);
                break;
            case 'password':
                $params['username'] = $this->getUsername();
                $params['password'] = $this->getPassword();
                $this->addClientCredentials($params);
                break;
            case 'refresh_token':
                $params['refresh_token'] = $this->getRefreshToken();
                $this->addClientCredentials($params);
                break;
            case self::JWT_URN:
                $params['assertion'] = $this->toJwt();
                break;
            case self::STS_URN:
                $token = $this->subjectTokenFetcher->fetchSubjectToken($httpHandler);
                $params['subject_token'] = $token;
                $params['subject_token_type'] = $this->subjectTokenType;
                $params += \array_filter(['resource' => $this->resource, 'audience' => $this->audience, 'scope' => $this->getScope(), 'requested_token_type' => self::STS_REQUESTED_TOKEN_TYPE, 'actor_token' => $this->actorToken, 'actor_token_type' => $this->actorTokenType]);
                if ($this->additionalOptions) {
                    $params['options'] = \json_encode($this->additionalOptions);
                }
                break;
            default:
                if (!\is_null($this->getRedirectUri())) {
                    # Grant type was supposed to be 'authorization_code', as there
                    # is a redirect URI.
                    throw new \DomainException('Missing authorization code');
                }
                unset($params['grant_type']);
                if (!\is_null($grantType)) {
                    $params['grant_type'] = $grantType;
                }
                $params = \array_merge($params, $this->getExtensionParams());
        }
        $headers = ['Cache-Control' => 'no-store', 'Content-Type' => 'application/x-www-form-urlencoded'];
        return new \Google\Site_Kit_Dependencies\GuzzleHttp\Psr7\Request('POST', $uri, $headers, \Google\Site_Kit_Dependencies\GuzzleHttp\Psr7\Query::build($params));
    }
    /**
     * Fetches the auth tokens based on the current state.
     *
     * @param callable $httpHandler callback which delivers psr7 request
     * @return array<mixed> the response
     */
    public function fetchAuthToken(?callable $httpHandler = null)
    {
        if (\is_null($httpHandler)) {
            $httpHandler = \Google\Site_Kit_Dependencies\Google\Auth\HttpHandler\HttpHandlerFactory::build(\Google\Site_Kit_Dependencies\Google\Auth\HttpHandler\HttpClientCache::getHttpClient());
        }
        $response = $httpHandler($this->generateCredentialsRequest($httpHandler));
        $credentials = $this->parseTokenResponse($response);
        $this->updateToken($credentials);
        if (isset($credentials['scope'])) {
            $this->setGrantedScope($credentials['scope']);
        }
        return $credentials;
    }
    /**
     * Obtains a key that can used to cache the results of #fetchAuthToken.
     *
     * The key is derived from the scopes.
     *
     * @return ?string a key that may be used to cache the auth token.
     */
    public function getCacheKey()
    {
        if (\is_array($this->scope)) {
            return \implode(':', $this->scope);
        }
        if ($this->audience) {
            return $this->audience;
        }
        // If scope has not set, return null to indicate no caching.
        return null;
    }
    /**
     * Parses the fetched tokens.
     *
     * @param ResponseInterface $resp the response.
     * @return array<mixed> the tokens parsed from the response body.
     * @throws \Exception
     */
    public function parseTokenResponse(\Google\Site_Kit_Dependencies\Psr\Http\Message\ResponseInterface $resp)
    {
        $body = (string) $resp->getBody();
        if ($resp->hasHeader('Content-Type') && $resp->getHeaderLine('Content-Type') == 'application/x-www-form-urlencoded') {
            $res = [];
            \parse_str($body, $res);
            return $res;
        }
        // Assume it's JSON; if it's not throw an exception
        if (null === ($res = \json_decode($body, \true))) {
            throw new \Exception('Invalid JSON response');
        }
        return $res;
    }
    /**
     * Updates an OAuth 2.0 client.
     *
     * Example:
     * ```
     * $oauth->updateToken([
     *     'refresh_token' => 'n4E9O119d',
     *     'access_token' => 'FJQbwq9',
     *     'expires_in' => 3600
     * ]);
     * ```
     *
     * @param array<mixed> $config
     *  The configuration parameters related to the token.
     *
     *  - refresh_token
     *    The refresh token associated with the access token
     *    to be refreshed.
     *
     *  - access_token
     *    The current access token for this client.
     *
     *  - id_token
     *    The current ID token for this client.
     *
     *  - expires_in
     *    The time in seconds until access token expiration.
     *
     *  - expires_at
     *    The time as an integer number of seconds since the Epoch
     *
     *  - issued_at
     *    The timestamp that the token was issued at.
     * @return void
     */
    public function updateToken(array $config)
    {
        $opts = \array_merge(['extensionParams' => [], 'access_token' => null, 'id_token' => null, 'expires_in' => null, 'expires_at' => null, 'issued_at' => null, 'scope' => null], $config);
        $this->setExpiresAt($opts['expires_at']);
        $this->setExpiresIn($opts['expires_in']);
        // By default, the token is issued at `Time.now` when `expiresIn` is set,
        // but this can be used to supply a more precise time.
        if (!\is_null($opts['issued_at'])) {
            $this->setIssuedAt($opts['issued_at']);
        }
        $this->setAccessToken($opts['access_token']);
        $this->setIdToken($opts['id_token']);
        // The refresh token should only be updated if a value is explicitly
        // passed in, as some access token responses do not include a refresh
        // token.
        if (\array_key_exists('refresh_token', $opts)) {
            $this->setRefreshToken($opts['refresh_token']);
        }
        // Required for STS response. An identifier for the representation of
        // the issued security token.
        if (\array_key_exists('issued_token_type', $opts)) {
            $this->issuedTokenType = $opts['issued_token_type'];
        }
    }
    /**
     * Builds the authorization Uri that the user should be redirected to.
     *
     * @param array<mixed> $config configuration options that customize the return url.
     * @return UriInterface the authorization Url.
     * @throws InvalidArgumentException
     */
    public function buildFullAuthorizationUri(array $config = [])
    {
        if (\is_null($this->getAuthorizationUri())) {
            throw new \InvalidArgumentException('requires an authorizationUri to have been set');
        }
        $params = \array_merge(['response_type' => 'code', 'access_type' => 'offline', 'client_id' => $this->clientId, 'redirect_uri' => $this->redirectUri, 'state' => $this->state, 'scope' => $this->getScope()], $config);
        // Validate the auth_params
        if (\is_null($params['client_id'])) {
            throw new \InvalidArgumentException('missing the required client identifier');
        }
        if (\is_null($params['redirect_uri'])) {
            throw new \InvalidArgumentException('missing the required redirect URI');
        }
        if (!empty($params['prompt']) && !empty($params['approval_prompt'])) {
            throw new \InvalidArgumentException('prompt and approval_prompt are mutually exclusive');
        }
        if ($this->codeVerifier) {
            $params['code_challenge'] = $this->getCodeChallenge($this->codeVerifier);
            $params['code_challenge_method'] = $this->getCodeChallengeMethod();
        }
        // Construct the uri object; return it if it is valid.
        $result = clone $this->authorizationUri;
        $existingParams = \Google\Site_Kit_Dependencies\GuzzleHttp\Psr7\Query::parse($result->getQuery());
        $result = $result->withQuery(\Google\Site_Kit_Dependencies\GuzzleHttp\Psr7\Query::build(\array_merge($existingParams, $params)));
        if ($result->getScheme() != 'https') {
            throw new \InvalidArgumentException('Authorization endpoint must be protected by TLS');
        }
        return $result;
    }
    /**
     * @return string|null
     */
    public function getCodeVerifier() : ?string
    {
        return $this->codeVerifier;
    }
    /**
     * A cryptographically random string that is used to correlate the
     * authorization request to the token request.
     *
     * The code verifier for PKCE for OAuth 2.0. When set, the authorization
     * URI will contain the Code Challenge and Code Challenge Method querystring
     * parameters, and the token URI will contain the Code Verifier parameter.
     *
     * @see https://datatracker.ietf.org/doc/html/rfc7636
     *
     * @param string|null $codeVerifier
     */
    public function setCodeVerifier(?string $codeVerifier) : void
    {
        $this->codeVerifier = $codeVerifier;
    }
    /**
     * Generates a random 128-character string for the "code_verifier" parameter
     * in PKCE for OAuth 2.0. This is a cryptographically random string that is
     * determined using random_int, hashed using "hash" and sha256, and base64
     * encoded.
     *
     * When this method is called, the code verifier is set on the object.
     *
     * @return string
     */
    public function generateCodeVerifier() : string
    {
        return $this->codeVerifier = $this->generateRandomString(128);
    }
    private function getCodeChallenge(string $randomString) : string
    {
        return \rtrim(\strtr(\base64_encode(\hash('sha256', $randomString, \true)), '+/', '-_'), '=');
    }
    private function getCodeChallengeMethod() : string
    {
        return 'S256';
    }
    private function generateRandomString(int $length) : string
    {
        $validChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~';
        $validCharsLen = \strlen($validChars);
        $str = '';
        $i = 0;
        while ($i++ < $length) {
            $str .= $validChars[\random_int(0, $validCharsLen - 1)];
        }
        return $str;
    }
    /**
     * Sets the authorization server's HTTP endpoint capable of authenticating
     * the end-user and obtaining authorization.
     *
     * @param string $uri
     * @return void
     */
    public function setAuthorizationUri($uri)
    {
        $this->authorizationUri = $this->coerceUri($uri);
    }
    /**
     * Gets the authorization server's HTTP endpoint capable of authenticating
     * the end-user and obtaining authorization.
     *
     * @return ?UriInterface
     */
    public function getAuthorizationUri()
    {
        return $this->authorizationUri;
    }
    /**
     * Gets the authorization server's HTTP endpoint capable of issuing tokens
     * and refreshing expired tokens.
     *
     * @return ?UriInterface
     */
    public function getTokenCredentialUri()
    {
        return $this->tokenCredentialUri;
    }
    /**
     * Sets the authorization server's HTTP endpoint capable of issuing tokens
     * and refreshing expired tokens.
     *
     * @param string $uri
     * @return void
     */
    public function setTokenCredentialUri($uri)
    {
        $this->tokenCredentialUri = $this->coerceUri($uri);
    }
    /**
     * Gets the redirection URI used in the initial request.
     *
     * @return ?string
     */
    public function getRedirectUri()
    {
        return $this->redirectUri;
    }
    /**
     * Sets the redirection URI used in the initial request.
     *
     * @param ?string $uri
     * @return void
     */
    public function setRedirectUri($uri)
    {
        if (\is_null($uri)) {
            $this->redirectUri = null;
            return;
        }
        // redirect URI must be absolute
        if (!$this->isAbsoluteUri($uri)) {
            // "postmessage" is a reserved URI string in Google-land
            // @see https://developers.google.com/identity/sign-in/web/server-side-flow
            if ('postmessage' !== (string) $uri) {
                throw new \InvalidArgumentException('Redirect URI must be absolute');
            }
        }
        $this->redirectUri = (string) $uri;
    }
    /**
     * Gets the scope of the access requests as a space-delimited String.
     *
     * @return ?string
     */
    public function getScope()
    {
        if (\is_null($this->scope)) {
            return $this->scope;
        }
        return \implode(' ', $this->scope);
    }
    /**
     * Sets the scope of the access request, expressed either as an Array or as
     * a space-delimited String.
     *
     * @param string|array<string>|null $scope
     * @return void
     * @throws InvalidArgumentException
     */
    public function setScope($scope)
    {
        if (\is_null($scope)) {
            $this->scope = null;
        } elseif (\is_string($scope)) {
            $this->scope = \explode(' ', $scope);
        } elseif (\is_array($scope)) {
            foreach ($scope as $s) {
                $pos = \strpos($s, ' ');
                if ($pos !== \false) {
                    throw new \InvalidArgumentException('array scope values should not contain spaces');
                }
            }
            $this->scope = $scope;
        } else {
            throw new \InvalidArgumentException('scopes should be a string or array of strings');
        }
    }
    /**
     * Gets the current grant type.
     *
     * @return ?string
     */
    public function getGrantType()
    {
        if (!\is_null($this->grantType)) {
            return $this->grantType;
        }
        // Returns the inferred grant type, based on the current object instance
        // state.
        if (!\is_null($this->code)) {
            return 'authorization_code';
        }
        if (!\is_null($this->refreshToken)) {
            return 'refresh_token';
        }
        if (!\is_null($this->username) && !\is_null($this->password)) {
            return 'password';
        }
        if (!\is_null($this->issuer) && !\is_null($this->signingKey)) {
            return self::JWT_URN;
        }
        if (!\is_null($this->subjectTokenFetcher) && !\is_null($this->subjectTokenType)) {
            return self::STS_URN;
        }
        return null;
    }
    /**
     * Sets the current grant type.
     *
     * @param string $grantType
     * @return void
     * @throws InvalidArgumentException
     */
    public function setGrantType($grantType)
    {
        if (\in_array($grantType, self::$knownGrantTypes)) {
            $this->grantType = $grantType;
        } else {
            // validate URI
            if (!$this->isAbsoluteUri($grantType)) {
                throw new \InvalidArgumentException('invalid grant type');
            }
            $this->grantType = (string) $grantType;
        }
    }
    /**
     * Gets an arbitrary string designed to allow the client to maintain state.
     *
     * @return string
     */
    public function getState()
    {
        return $this->state;
    }
    /**
     * Sets an arbitrary string designed to allow the client to maintain state.
     *
     * @param string $state
     * @return void
     */
    public function setState($state)
    {
        $this->state = $state;
    }
    /**
     * Gets the authorization code issued to this client.
     *
     * @return string
     */
    public function getCode()
    {
        return $this->code;
    }
    /**
     * Sets the authorization code issued to this client.
     *
     * @param string $code
     * @return void
     */
    public function setCode($code)
    {
        $this->code = $code;
    }
    /**
     * Gets the resource owner's username.
     *
     * @return string
     */
    public function getUsername()
    {
        return $this->username;
    }
    /**
     * Sets the resource owner's username.
     *
     * @param string $username
     * @return void
     */
    public function setUsername($username)
    {
        $this->username = $username;
    }
    /**
     * Gets the resource owner's password.
     *
     * @return string
     */
    public function getPassword()
    {
        return $this->password;
    }
    /**
     * Sets the resource owner's password.
     *
     * @param string $password
     * @return void
     */
    public function setPassword($password)
    {
        $this->password = $password;
    }
    /**
     * Sets a unique identifier issued to the client to identify itself to the
     * authorization server.
     *
     * @return string
     */
    public function getClientId()
    {
        return $this->clientId;
    }
    /**
     * Sets a unique identifier issued to the client to identify itself to the
     * authorization server.
     *
     * @param string $clientId
     * @return void
     */
    public function setClientId($clientId)
    {
        $this->clientId = $clientId;
    }
    /**
     * Gets a shared symmetric secret issued by the authorization server, which
     * is used to authenticate the client.
     *
     * @return string
     */
    public function getClientSecret()
    {
        return $this->clientSecret;
    }
    /**
     * Sets a shared symmetric secret issued by the authorization server, which
     * is used to authenticate the client.
     *
     * @param string $clientSecret
     * @return void
     */
    public function setClientSecret($clientSecret)
    {
        $this->clientSecret = $clientSecret;
    }
    /**
     * Gets the Issuer ID when using assertion profile.
     *
     * @return ?string
     */
    public function getIssuer()
    {
        return $this->issuer;
    }
    /**
     * Sets the Issuer ID when using assertion profile.
     *
     * @param string $issuer
     * @return void
     */
    public function setIssuer($issuer)
    {
        $this->issuer = $issuer;
    }
    /**
     * Gets the target sub when issuing assertions.
     *
     * @return ?string
     */
    public function getSub()
    {
        return $this->sub;
    }
    /**
     * Sets the target sub when issuing assertions.
     *
     * @param string $sub
     * @return void
     */
    public function setSub($sub)
    {
        $this->sub = $sub;
    }
    /**
     * Gets the target audience when issuing assertions.
     *
     * @return ?string
     */
    public function getAudience()
    {
        return $this->audience;
    }
    /**
     * Sets the target audience when issuing assertions.
     *
     * @param string $audience
     * @return void
     */
    public function setAudience($audience)
    {
        $this->audience = $audience;
    }
    /**
     * Gets the signing key when using an assertion profile.
     *
     * @return ?string
     */
    public function getSigningKey()
    {
        return $this->signingKey;
    }
    /**
     * Sets the signing key when using an assertion profile.
     *
     * @param string $signingKey
     * @return void
     */
    public function setSigningKey($signingKey)
    {
        $this->signingKey = $signingKey;
    }
    /**
     * Gets the signing key id when using an assertion profile.
     *
     * @return ?string
     */
    public function getSigningKeyId()
    {
        return $this->signingKeyId;
    }
    /**
     * Sets the signing key id when using an assertion profile.
     *
     * @param string $signingKeyId
     * @return void
     */
    public function setSigningKeyId($signingKeyId)
    {
        $this->signingKeyId = $signingKeyId;
    }
    /**
     * Gets the signing algorithm when using an assertion profile.
     *
     * @return ?string
     */
    public function getSigningAlgorithm()
    {
        return $this->signingAlgorithm;
    }
    /**
     * Sets the signing algorithm when using an assertion profile.
     *
     * @param ?string $signingAlgorithm
     * @return void
     */
    public function setSigningAlgorithm($signingAlgorithm)
    {
        if (\is_null($signingAlgorithm)) {
            $this->signingAlgorithm = null;
        } elseif (!\in_array($signingAlgorithm, self::$knownSigningAlgorithms)) {
            throw new \InvalidArgumentException('unknown signing algorithm');
        } else {
            $this->signingAlgorithm = $signingAlgorithm;
        }
    }
    /**
     * Gets the set of parameters used by extension when using an extension
     * grant type.
     *
     * @return array<mixed>
     */
    public function getExtensionParams()
    {
        return $this->extensionParams;
    }
    /**
     * Sets the set of parameters used by extension when using an extension
     * grant type.
     *
     * @param array<mixed> $extensionParams
     * @return void
     */
    public function setExtensionParams($extensionParams)
    {
        $this->extensionParams = $extensionParams;
    }
    /**
     * Gets the number of seconds assertions are valid for.
     *
     * @return int
     */
    public function getExpiry()
    {
        return $this->expiry;
    }
    /**
     * Sets the number of seconds assertions are valid for.
     *
     * @param int $expiry
     * @return void
     */
    public function setExpiry($expiry)
    {
        $this->expiry = $expiry;
    }
    /**
     * Gets the lifetime of the access token in seconds.
     *
     * @return int
     */
    public function getExpiresIn()
    {
        return $this->expiresIn;
    }
    /**
     * Sets the lifetime of the access token in seconds.
     *
     * @param ?int $expiresIn
     * @return void
     */
    public function setExpiresIn($expiresIn)
    {
        if (\is_null($expiresIn)) {
            $this->expiresIn = null;
            $this->issuedAt = null;
        } else {
            $this->issuedAt = \time();
            $this->expiresIn = (int) $expiresIn;
        }
    }
    /**
     * Gets the time the current access token expires at.
     *
     * @return ?int
     */
    public function getExpiresAt()
    {
        if (!\is_null($this->expiresAt)) {
            return $this->expiresAt;
        }
        if (!\is_null($this->issuedAt) && !\is_null($this->expiresIn)) {
            return $this->issuedAt + $this->expiresIn;
        }
        return null;
    }
    /**
     * Returns true if the acccess token has expired.
     *
     * @return bool
     */
    public function isExpired()
    {
        $expiration = $this->getExpiresAt();
        $now = \time();
        return !\is_null($expiration) && $now >= $expiration;
    }
    /**
     * Sets the time the current access token expires at.
     *
     * @param int $expiresAt
     * @return void
     */
    public function setExpiresAt($expiresAt)
    {
        $this->expiresAt = $expiresAt;
    }
    /**
     * Gets the time the current access token was issued at.
     *
     * @return ?int
     */
    public function getIssuedAt()
    {
        return $this->issuedAt;
    }
    /**
     * Sets the time the current access token was issued at.
     *
     * @param int $issuedAt
     * @return void
     */
    public function setIssuedAt($issuedAt)
    {
        $this->issuedAt = $issuedAt;
    }
    /**
     * Gets the current access token.
     *
     * @return ?string
     */
    public function getAccessToken()
    {
        return $this->accessToken;
    }
    /**
     * Sets the current access token.
     *
     * @param string $accessToken
     * @return void
     */
    public function setAccessToken($accessToken)
    {
        $this->accessToken = $accessToken;
    }
    /**
     * Gets the current ID token.
     *
     * @return ?string
     */
    public function getIdToken()
    {
        return $this->idToken;
    }
    /**
     * Sets the current ID token.
     *
     * @param string $idToken
     * @return void
     */
    public function setIdToken($idToken)
    {
        $this->idToken = $idToken;
    }
    /**
     * Get the granted space-separated scopes (if they exist) for the last
     * fetched token.
     *
     * @return string|null
     */
    public function getGrantedScope()
    {
        return $this->grantedScope;
    }
    /**
     * Sets the current ID token.
     *
     * @param string $grantedScope
     * @return void
     */
    public function setGrantedScope($grantedScope)
    {
        $this->grantedScope = $grantedScope;
    }
    /**
     * Gets the refresh token associated with the current access token.
     *
     * @return ?string
     */
    public function getRefreshToken()
    {
        return $this->refreshToken;
    }
    /**
     * Sets the refresh token associated with the current access token.
     *
     * @param string $refreshToken
     * @return void
     */
    public function setRefreshToken($refreshToken)
    {
        $this->refreshToken = $refreshToken;
    }
    /**
     * Sets additional claims to be included in the JWT token
     *
     * @param array<mixed> $additionalClaims
     * @return void
     */
    public function setAdditionalClaims(array $additionalClaims)
    {
        $this->additionalClaims = $additionalClaims;
    }
    /**
     * Gets the additional claims to be included in the JWT token.
     *
     * @return array<mixed>
     */
    public function getAdditionalClaims()
    {
        return $this->additionalClaims;
    }
    /**
     * Gets the additional claims to be included in the JWT token.
     *
     * @return ?string
     */
    public function getIssuedTokenType()
    {
        return $this->issuedTokenType;
    }
    /**
     * The expiration of the last received token.
     *
     * @return array<mixed>|null
     */
    public function getLastReceivedToken()
    {
        if ($token = $this->getAccessToken()) {
            // the bare necessity of an auth token
            $authToken = ['access_token' => $token, 'expires_at' => $this->getExpiresAt()];
        } elseif ($idToken = $this->getIdToken()) {
            $authToken = ['id_token' => $idToken, 'expires_at' => $this->getExpiresAt()];
        } else {
            return null;
        }
        if ($expiresIn = $this->getExpiresIn()) {
            $authToken['expires_in'] = $expiresIn;
        }
        if ($issuedAt = $this->getIssuedAt()) {
            $authToken['issued_at'] = $issuedAt;
        }
        if ($refreshToken = $this->getRefreshToken()) {
            $authToken['refresh_token'] = $refreshToken;
        }
        return $authToken;
    }
    /**
     * Get the client ID.
     *
     * Alias of {@see Google\Auth\OAuth2::getClientId()}.
     *
     * @param callable $httpHandler
     * @return string
     * @access private
     */
    public function getClientName(?callable $httpHandler = null)
    {
        return $this->getClientId();
    }
    /**
     * @todo handle uri as array
     *
     * @param ?string $uri
     * @return null|UriInterface
     */
    private function coerceUri($uri)
    {
        if (\is_null($uri)) {
            return null;
        }
        return \Google\Site_Kit_Dependencies\GuzzleHttp\Psr7\Utils::uriFor($uri);
    }
    /**
     * @param string $idToken
     * @param Key|Key[]|string|string[] $publicKey
     * @param string|string[] $allowedAlgs
     * @return object
     */
    private function jwtDecode($idToken, $publicKey, $allowedAlgs)
    {
        $keys = $this->getFirebaseJwtKeys($publicKey, $allowedAlgs);
        // Default exception if none are caught. We are using the same exception
        // class and message from firebase/php-jwt to preserve backwards
        // compatibility.
        $e = new \InvalidArgumentException('Key may not be empty');
        foreach ($keys as $key) {
            try {
                return \Google\Site_Kit_Dependencies\Firebase\JWT\JWT::decode($idToken, $key);
            } catch (\Exception $e) {
                // try next alg
            }
        }
        throw $e;
    }
    /**
     * @param Key|Key[]|string|string[] $publicKey
     * @param string|string[] $allowedAlgs
     * @return Key[]
     */
    private function getFirebaseJwtKeys($publicKey, $allowedAlgs)
    {
        // If $publicKey is instance of Key, return it
        if ($publicKey instanceof \Google\Site_Kit_Dependencies\Firebase\JWT\Key) {
            return [$publicKey];
        }
        // If $allowedAlgs is empty, $publicKey must be Key or Key[].
        if (empty($allowedAlgs)) {
            $keys = [];
            foreach ((array) $publicKey as $kid => $pubKey) {
                if (!$pubKey instanceof \Google\Site_Kit_Dependencies\Firebase\JWT\Key) {
                    throw new \InvalidArgumentException(\sprintf('When allowed algorithms is empty, the public key must' . 'be an instance of %s or an array of %s objects', \Google\Site_Kit_Dependencies\Firebase\JWT\Key::class, \Google\Site_Kit_Dependencies\Firebase\JWT\Key::class));
                }
                $keys[$kid] = $pubKey;
            }
            return $keys;
        }
        $allowedAlg = null;
        if (\is_string($allowedAlgs)) {
            $allowedAlg = $allowedAlgs;
        } elseif (\is_array($allowedAlgs)) {
            if (\count($allowedAlgs) > 1) {
                throw new \InvalidArgumentException('To have multiple allowed algorithms, You must provide an' . ' array of Firebase\\JWT\\Key objects.' . ' See https://github.com/firebase/php-jwt for more information.');
            }
            $allowedAlg = \array_pop($allowedAlgs);
        } else {
            throw new \InvalidArgumentException('allowed algorithms must be a string or array.');
        }
        if (\is_array($publicKey)) {
            // When publicKey is greater than 1, create keys with the single alg.
            $keys = [];
            foreach ($publicKey as $kid => $pubKey) {
                if ($pubKey instanceof \Google\Site_Kit_Dependencies\Firebase\JWT\Key) {
                    $keys[$kid] = $pubKey;
                } else {
                    $keys[$kid] = new \Google\Site_Kit_Dependencies\Firebase\JWT\Key($pubKey, $allowedAlg);
                }
            }
            return $keys;
        }
        return [new \Google\Site_Kit_Dependencies\Firebase\JWT\Key($publicKey, $allowedAlg)];
    }
    /**
     * Determines if the URI is absolute based on its scheme and host or path
     * (RFC 3986).
     *
     * @param string $uri
     * @return bool
     */
    private function isAbsoluteUri($uri)
    {
        $uri = $this->coerceUri($uri);
        return $uri->getScheme() && ($uri->getHost() || $uri->getPath());
    }
    /**
     * @param array<mixed> $params
     * @return array<mixed>
     */
    private function addClientCredentials(&$params)
    {
        $clientId = $this->getClientId();
        $clientSecret = $this->getClientSecret();
        if ($clientId && $clientSecret) {
            $params['client_id'] = $clientId;
            $params['client_secret'] = $clientSecret;
        }
        return $params;
    }
}
¿Qué es la limpieza dental de perros? - Clínica veterinaria


Es la eliminación del sarro y la placa adherida a la superficie de los dientes mediante un equipo de ultrasonidos que garantiza la integridad de las piezas dentales a la vez que elimina en profundidad cualquier resto de suciedad.

A continuación se procede al pulido de los dientes mediante una fresa especial que elimina la placa bacteriana y devuelve a los dientes el aspecto sano que deben tener.

Una vez terminado todo el proceso, se mantiene al perro en observación hasta que se despierta de la anestesia, bajo la atenta supervisión de un veterinario.

¿Cada cuánto tiempo tengo que hacerle una limpieza dental a mi perro?

A partir de cierta edad, los perros pueden necesitar una limpieza dental anual o bianual. Depende de cada caso. En líneas generales, puede decirse que los perros de razas pequeñas suelen acumular más sarro y suelen necesitar una atención mayor en cuanto a higiene dental.


Riesgos de una mala higiene


Los riesgos más evidentes de una mala higiene dental en los perros son los siguientes:

  • Cuando la acumulación de sarro no se trata, se puede producir una inflamación y retracción de las encías que puede descalzar el diente y provocar caídas.
  • Mal aliento (halitosis).
  • Sarro perros
  • Puede ir a más
  • Las bacterias de la placa pueden trasladarse a través del torrente circulatorio a órganos vitales como el corazón ocasionando problemas de endocarditis en las válvulas. Las bacterias pueden incluso acantonarse en huesos (La osteomielitis es la infección ósea, tanto cortical como medular) provocando mucho dolor y una artritis séptica).

¿Cómo se forma el sarro?

El sarro es la calcificación de la placa dental. Los restos de alimentos, junto con las bacterias presentes en la boca, van a formar la placa bacteriana o placa dental. Si la placa no se retira, al mezclarse con la saliva y los minerales presentes en ella, reaccionará formando una costra. La placa se calcifica y se forma el sarro.

El sarro, cuando se forma, es de color blanquecino pero a medida que pasa el tiempo se va poniendo amarillo y luego marrón.

Síntomas de una pobre higiene dental
La señal más obvia de una mala salud dental canina es el mal aliento.

Sin embargo, a veces no es tan fácil de detectar
Y hay perros que no se dejan abrir la boca por su dueño. Por ejemplo…

Recientemente nos trajeron a la clínica a un perro que parpadeaba de un ojo y decía su dueño que le picaba un lado de la cara. Tenía molestias y dificultad para comer, lo que había llevado a sus dueños a comprarle comida blanda (que suele ser un poco más cara y llevar más contenido en grasa) durante medio año. Después de una exploración oftalmológica, nos dimos cuenta de que el ojo tenía una úlcera en la córnea probablemente de rascarse . Además, el canto lateral del ojo estaba inflamado. Tenía lo que en humanos llamamos flemón pero como era un perro de pelo largo, no se le notaba a simple vista. Al abrirle la boca nos llamó la atención el ver una muela llena de sarro. Le realizamos una radiografía y encontramos una fístula que llegaba hasta la parte inferior del ojo.

Le tuvimos que extraer la muela. Tras esto, el ojo se curó completamente con unos colirios y una lentilla protectora de úlcera. Afortunadamente, la úlcera no profundizó y no perforó el ojo. Ahora el perro come perfectamente a pesar de haber perdido una muela.

¿Cómo mantener la higiene dental de tu perro?
Hay varias maneras de prevenir problemas derivados de la salud dental de tu perro.

Limpiezas de dientes en casa
Es recomendable limpiar los dientes de tu perro semanal o diariamente si se puede. Existe una gran variedad de productos que se pueden utilizar:

Pastas de dientes.
Cepillos de dientes o dedales para el dedo índice, que hacen más fácil la limpieza.
Colutorios para echar en agua de bebida o directamente sobre el diente en líquido o en spray.

En la Clínica Tus Veterinarios enseñamos a nuestros clientes a tomar el hábito de limpiar los dientes de sus perros desde que son cachorros. Esto responde a nuestro compromiso con la prevención de enfermedades caninas.

Hoy en día tenemos muchos clientes que limpian los dientes todos los días a su mascota, y como resultado, se ahorran el dinero de hacer limpiezas dentales profesionales y consiguen una mejor salud de su perro.


Limpiezas dentales profesionales de perros y gatos

Recomendamos hacer una limpieza dental especializada anualmente. La realizamos con un aparato de ultrasonidos que utiliza agua para quitar el sarro. Después, procedemos a pulir los dientes con un cepillo de alta velocidad y una pasta especial. Hacemos esto para proteger el esmalte.

La frecuencia de limpiezas dentales necesaria varía mucho entre razas. En general, las razas grandes tienen buena calidad de esmalte, por lo que no necesitan hacerlo tan a menudo e incluso pueden pasarse la vida sin requerir una limpieza. Sin embargo, razas pequeñas como el Yorkshire o el Maltés, deben hacérselas todos los años desde cachorros si se quiere conservar sus piezas dentales.

Otro factor fundamental es la calidad del pienso. Algunas marcas han diseñado croquetas que limpian la superficie del diente y de la muela al masticarse.

Ultrasonido para perros

¿Se necesita anestesia para las limpiezas dentales de perros y gatos?

La limpieza dental en perros no es una técnica que pueda practicarse sin anestesia general , aunque hay veces que los propietarios no quieren anestesiar y si tiene poco sarro y el perro es muy bueno se puede intentar…… , pero no se va a poder pulir ni acceder a todas la zona de la boca …. Además los limpiadores dentales van a irrigar agua y hay riesgo de aspiración a vías respiratorias si no se realiza una anestesia correcta con intubación traqueal . En resumen , sin anestesia no se va hacer una correcta limpieza dental.

Tampoco sirve la sedación ya que necesitamos que el animal esté totalmente quieto, y el veterinario tenga un acceso completo a todas sus piezas dentales y encías.

Alimentos para la limpieza dental

Hay que tener cierto cuidado a la hora de comprar determinados alimentos porque no todos son saludables. Algunos tienen demasiado contenido graso, que en exceso puede causar problemas cardiovasculares y obesidad.

Los mejores alimentos para los dientes son aquellos que están elaborados por empresas farmacéuticas y llevan componentes químicos con tratamientos específicos para el diente del perro. Esto implica no solo limpieza a través de la acción mecánica de morder sino también un tratamiento antibacteriano para prevenir el sarro.

Conclusión

Si eres como la mayoría de dueños, por falta de tiempo , es probable que no estés prestando la suficiente atención a la limpieza dental de tu perro. Por eso te animamos a que comiences a limpiar los dientes de tu perro y consideres atender a su higiene bucal con frecuencia.

Estas simples medidas pueden conllevar a que tu perro tenga una vida más larga y mucho más saludable.

Si te resulta imposible introducir un cepillo de dientes a tu perro en la boca, pásate con él por clínica Tus Veterinarios y te explicamos cómo hacerlo.

Necesitas hacer una limpieza dental profesional a tu mascota?
Llámanos al 622575274 o contacta con nosotros

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

¡Hola!