| 1 | <?php |
| 2 | |
| 3 | class Bd |
| 4 | { |
| 5 | |
| 6 | private static ?PDO $pdo = null; |
| 7 | |
| 8 | static function pdo(): PDO |
| 9 | { |
| 10 | if (self::$pdo === null) { |
| 11 | |
| 12 | self::$pdo = new PDO( |
| 13 | // cadena de conexión |
| 14 | "sqlite:" . __DIR__ . "/notipush.db", |
| 15 | // usuario |
| 16 | null, |
| 17 | // contraseña |
| 18 | null, |
| 19 | // Opciones: pdos no persistentes y lanza excepciones. |
| 20 | [PDO::ATTR_PERSISTENT => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] |
| 21 | ); |
| 22 | |
| 23 | self::$pdo->exec( |
| 24 | 'CREATE TABLE IF NOT EXISTS SUSCRIPCION ( |
| 25 | SUS_ENDPOINT TEXT NOT NULL, |
| 26 | SUS_PUB_KEY TEXT NOT NULL, |
| 27 | SUS_AUT_TOK TEXT NOT NULL, |
| 28 | SUS_CONT_ENCOD TEXT NOT NULL, |
| 29 | CONSTRAINT SUS_PK |
| 30 | PRIMARY KEY(SUS_ENDPOINT), |
| 31 | CONSTRAINT SUS_ENDPNT_NV |
| 32 | CHECK(LENGTH(SUS_ENDPOINT) > 0) |
| 33 | )' |
| 34 | ); |
| 35 | } |
| 36 | |
| 37 | return self::$pdo; |
| 38 | } |
| 39 | } |
| 40 |
| 1 | <!DOCTYPE html> |
| 2 | <html lang="es"> |
| 3 | |
| 4 | <head> |
| 5 | |
| 6 | <meta charset="UTF-8"> |
| 7 | <meta name="viewport" content="width=device-width"> |
| 8 | |
| 9 | <title>Llaves VAPID</title> |
| 10 | |
| 11 | </head> |
| 12 | |
| 13 | <body> |
| 14 | |
| 15 | <h1>Llaves VAPID</h1> |
| 16 | |
| 17 | <pre> |
| 18 | <?php |
| 19 | |
| 20 | require __DIR__ . "/../vendor/autoload.php"; |
| 21 | |
| 22 | use Minishlink\WebPush\VAPID; |
| 23 | |
| 24 | var_dump(VAPID::createVapidKeys()); |
| 25 | |
| 26 | ?> |
| 27 | </pre> |
| 28 | |
| 29 | </body> |
| 30 | |
| 31 | </html> |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/lib/manejaErrores.php"; |
| 4 | require_once __DIR__ . "/../vendor/autoload.php"; |
| 5 | require_once __DIR__ . "/lib/devuelveJson.php"; |
| 6 | require_once __DIR__ . "/Bd.php"; |
| 7 | require_once __DIR__ . "/Suscripcion.php"; |
| 8 | require_once __DIR__ . "/suscripcionElimina.php"; |
| 9 | |
| 10 | use Minishlink\WebPush\WebPush; |
| 11 | |
| 12 | const AUTH = [ |
| 13 | "VAPID" => [ |
| 14 | "subject" => "https://notipush.rf.gd/", |
| 15 | "publicKey" => "BMBlr6YznhYMX3NgcWIDRxZXs0sh7tCv7_YCsWcww0ZCv9WGg-tRCXfMEHTiBPCksSqeve1twlbmVAZFv7GSuj0", |
| 16 | "privateKey" => "vplfkITvu0cwHqzK9Kj-DYStbCH_9AhGx9LqMyaeI6w" |
| 17 | ] |
| 18 | ]; |
| 19 | |
| 20 | $webPush = new WebPush(AUTH); |
| 21 | $mensaje = "Hola! 👋"; |
| 22 | |
| 23 | // Envia el mensaje a todas las suscripciones. |
| 24 | |
| 25 | $bd = Bd::pdo(); |
| 26 | $stmt = $bd->query("SELECT * FROM SUSCRIPCION"); |
| 27 | $suscripciones = |
| 28 | $stmt->fetchAll(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, Suscripcion::class); |
| 29 | |
| 30 | foreach ($suscripciones as $suscripcion) { |
| 31 | $webPush->queueNotification($suscripcion, $mensaje); |
| 32 | } |
| 33 | $reportes = $webPush->flush(); |
| 34 | |
| 35 | // Genera el reporte de envio a cada suscripcion. |
| 36 | $reporteDeEnvios = ""; |
| 37 | foreach ($reportes as $reporte) { |
| 38 | $endpoint = $reporte->getRequest()->getUri(); |
| 39 | $htmlEndpoint = htmlentities($endpoint); |
| 40 | if ($reporte->isSuccess()) { |
| 41 | // Reporte de éxito. |
| 42 | $reporteDeEnvios .= "<dt>$htmlEndpoint</dt><dd>Éxito</dd>"; |
| 43 | } else { |
| 44 | if ($reporte->isSubscriptionExpired()) { |
| 45 | suscripcionElimina($bd, $endpoint); |
| 46 | } |
| 47 | // Reporte de fallo. |
| 48 | $explicacion = htmlentities($reporte->getReason()); |
| 49 | $reporteDeEnvios .= "<dt>$endpoint</dt><dd>Fallo: $explicacion</dd>"; |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | devuelveJson(["reporte" => ["innerHTML" => $reporteDeEnvios]]); |
| 54 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/lib/BAD_REQUEST.php"; |
| 4 | require_once __DIR__ . "/lib/recibeJson.php"; |
| 5 | require_once __DIR__ . "/lib/ProblemDetailsException.php"; |
| 6 | |
| 7 | function recibeSuscripcion() |
| 8 | { |
| 9 | |
| 10 | $objeto = recibeJson(); |
| 11 | |
| 12 | if ( |
| 13 | !isset($objeto->authToken) |
| 14 | || !is_string($objeto->authToken) |
| 15 | || $objeto->authToken === "" |
| 16 | ) |
| 17 | throw new ProblemDetailsException([ |
| 18 | "status" => BAD_REQUEST, |
| 19 | "title" => "El authToken debe ser texto que no esté en blanco.", |
| 20 | "type" => "/errors/authtokenincorrecto.html", |
| 21 | ]); |
| 22 | |
| 23 | if ( |
| 24 | !isset($objeto->contentEncoding) |
| 25 | || !is_string($objeto->contentEncoding) |
| 26 | || $objeto->contentEncoding === "" |
| 27 | ) |
| 28 | throw new ProblemDetailsException([ |
| 29 | "status" => BAD_REQUEST, |
| 30 | "title" => "La contentEncoding debe ser texto que no esté en blanco.", |
| 31 | "type" => "/errors/contentencodingincorrecta.html", |
| 32 | ]); |
| 33 | |
| 34 | if ( |
| 35 | !isset($objeto->endpoint) |
| 36 | || !is_string($objeto->endpoint) |
| 37 | || $objeto->endpoint === "" |
| 38 | ) |
| 39 | throw new ProblemDetailsException([ |
| 40 | "status" => BAD_REQUEST, |
| 41 | "title" => "El endpoint debe ser texto que no esté en blanco.", |
| 42 | "type" => "/errors/endpointincorrecto.html", |
| 43 | ]); |
| 44 | |
| 45 | if ( |
| 46 | !isset($objeto->publicKey) |
| 47 | || !is_string($objeto->publicKey) |
| 48 | || $objeto->publicKey === "" |
| 49 | ) |
| 50 | throw new ProblemDetailsException([ |
| 51 | "status" => BAD_REQUEST, |
| 52 | "title" => "La publicKey debe ser texto que no esté en blanco.", |
| 53 | "type" => "/errors/publickeyincorrecta.html", |
| 54 | ]); |
| 55 | |
| 56 | return [ |
| 57 | "SUS_AUT_TOK" => $objeto->authToken, |
| 58 | "SUS_CONT_ENCOD" => $objeto->contentEncoding, |
| 59 | "SUS_ENDPOINT" => $objeto->endpoint, |
| 60 | "SUS_PUB_KEY" => $objeto->publicKey, |
| 61 | ]; |
| 62 | } |
| 63 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/lib/manejaErrores.php"; |
| 4 | require_once __DIR__ . "/lib/devuelveNoContent.php"; |
| 5 | require_once __DIR__ . "/Bd.php"; |
| 6 | require_once __DIR__ . "/recibeSuscripcion.php"; |
| 7 | require_once __DIR__ . "/suscripcionElimina.php"; |
| 8 | |
| 9 | $modelo = recibeSuscripcion(); |
| 10 | suscripcionElimina(Bd::pdo(), $modelo["SUS_ENDPOINT"]); |
| 11 | devuelveNoContent(); |
| 12 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/lib/manejaErrores.php"; |
| 4 | require_once __DIR__ . "/lib/devuelveCreated.php"; |
| 5 | require_once __DIR__ . "/lib/devuelveJson.php"; |
| 6 | require_once __DIR__ . "/Bd.php"; |
| 7 | require_once __DIR__ . "/recibeSuscripcion.php"; |
| 8 | |
| 9 | $modelo = recibeSuscripcion(); |
| 10 | |
| 11 | $bd = Bd::pdo(); |
| 12 | |
| 13 | $stmt = |
| 14 | $bd->prepare("SELECT * FROM SUSCRIPCION WHERE SUS_ENDPOINT = :SUS_ENDPOINT"); |
| 15 | $stmt->execute([":SUS_ENDPOINT" => $modelo["SUS_ENDPOINT"]]); |
| 16 | $anterior = $stmt->fetch(PDO::FETCH_ASSOC); |
| 17 | |
| 18 | if ($anterior === false) { |
| 19 | |
| 20 | $stmt = $bd->prepare( |
| 21 | "INSERT INTO SUSCRIPCION ( |
| 22 | SUS_ENDPOINT, SUS_PUB_KEY, SUS_AUT_TOK, SUS_CONT_ENCOD |
| 23 | ) values ( |
| 24 | :SUS_ENDPOINT, :SUS_PUB_KEY, :SUS_AUT_TOK, :SUS_CONT_ENCOD |
| 25 | )" |
| 26 | ); |
| 27 | $stmt->execute([ |
| 28 | ":SUS_ENDPOINT" => $modelo["SUS_ENDPOINT"], |
| 29 | ":SUS_PUB_KEY" => $modelo["SUS_PUB_KEY"], |
| 30 | ":SUS_AUT_TOK" => $modelo["SUS_AUT_TOK"], |
| 31 | ":SUS_CONT_ENCOD" => $modelo["SUS_CONT_ENCOD"], |
| 32 | ]); |
| 33 | |
| 34 | devuelveCreated("", $modelo); |
| 35 | } else { |
| 36 | |
| 37 | $stmt = $bd->prepare( |
| 38 | "UPDATE SUSCRIPCION |
| 39 | SET |
| 40 | SUS_PUB_KEY = :SUS_PUB_KEY, |
| 41 | SUS_AUT_TOK = :SUS_AUT_TOK, |
| 42 | SUS_CONT_ENCOD = :SUS_CONT_ENCOD |
| 43 | WHERE |
| 44 | SUS_ENDPOINT = :SUS_ENDPOINT" |
| 45 | ); |
| 46 | $stmt->execute([ |
| 47 | ":SUS_PUB_KEY" => $modelo["SUS_PUB_KEY"], |
| 48 | ":SUS_AUT_TOK" => $modelo["SUS_AUT_TOK"], |
| 49 | ":SUS_CONT_ENCOD" => $modelo["SUS_CONT_ENCOD"], |
| 50 | ":SUS_ENDPOINT" => $modelo["SUS_ENDPOINT"], |
| 51 | ]); |
| 52 | |
| 53 | devuelveJson($modelo); |
| 54 | } |
| 55 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/../vendor/autoload.php"; |
| 4 | |
| 5 | use Minishlink\WebPush\SubscriptionInterface; |
| 6 | |
| 7 | class Suscripcion implements SubscriptionInterface |
| 8 | { |
| 9 | |
| 10 | public string $SUS_ENDPOINT; |
| 11 | public string $SUS_PUB_KEY; |
| 12 | public string $SUS_AUT_TOK; |
| 13 | public string $SUS_CONT_ENCOD; |
| 14 | |
| 15 | public function __construct( |
| 16 | string $SUS_ENDPOINT = "", |
| 17 | string $SUS_PUB_KEY = "", |
| 18 | string $SUS_AUT_TOK = "", |
| 19 | string $SUS_CONT_ENCOD = "" |
| 20 | ) { |
| 21 | $this->SUS_ENDPOINT = $SUS_ENDPOINT; |
| 22 | $this->SUS_PUB_KEY = $SUS_PUB_KEY; |
| 23 | $this->SUS_AUT_TOK = $SUS_AUT_TOK; |
| 24 | $this->SUS_CONT_ENCOD = $SUS_CONT_ENCOD; |
| 25 | } |
| 26 | |
| 27 | public function getEndpoint(): string |
| 28 | { |
| 29 | return $this->SUS_ENDPOINT; |
| 30 | } |
| 31 | |
| 32 | public function getPublicKey(): ?string |
| 33 | { |
| 34 | return $this->SUS_PUB_KEY; |
| 35 | } |
| 36 | |
| 37 | public function getAuthToken(): ?string |
| 38 | { |
| 39 | return $this->SUS_AUT_TOK; |
| 40 | } |
| 41 | |
| 42 | public function getContentEncoding(): ?string |
| 43 | { |
| 44 | return $this->SUS_CONT_ENCOD; |
| 45 | } |
| 46 | } |
| 47 |
| 1 | <?php |
| 2 | |
| 3 | function suscripcionElimina(\PDO $bd, string $endpoint) |
| 4 | { |
| 5 | $stmt = |
| 6 | $bd->prepare("DELETE FROM SUSCRIPCION WHERE SUS_ENDPOINT = :SUS_ENDPOINT"); |
| 7 | $stmt->execute([":SUS_ENDPOINT" => $endpoint]); |
| 8 | } |
| 9 |
| 1 | <?php |
| 2 | |
| 3 | const BAD_REQUEST = 400; |
| 4 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/devuelveResultadoNoJson.php"; |
| 4 | |
| 5 | function devuelveCreated($urlDelNuevo, $resultado) |
| 6 | { |
| 7 | $json = json_encode($resultado); |
| 8 | if ($json === false) { |
| 9 | devuelveResultadoNoJson(); |
| 10 | } else { |
| 11 | http_response_code(201); |
| 12 | header("Location: $urlDelNuevo"); |
| 13 | header("Content-Type: application/json; charset=utf-8"); |
| 14 | echo $json; |
| 15 | } |
| 16 | } |
| 17 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/devuelveResultadoNoJson.php"; |
| 4 | |
| 5 | function devuelveJson($resultado) |
| 6 | { |
| 7 | $json = json_encode($resultado); |
| 8 | if ($json === false) { |
| 9 | devuelveResultadoNoJson(); |
| 10 | } else { |
| 11 | header("Content-Type: application/json; charset=utf-8"); |
| 12 | echo $json; |
| 13 | } |
| 14 | exit(); |
| 15 | } |
| 16 |
| 1 | <?php |
| 2 | |
| 3 | function devuelveNoContent() |
| 4 | { |
| 5 | http_response_code(204); |
| 6 | } |
| 7 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php"; |
| 4 | |
| 5 | function devuelveResultadoNoJson() |
| 6 | { |
| 7 | http_response_code(INTERNAL_SERVER_ERROR); |
| 8 | header("Content-Type: application/problem+json; charset=utf-8"); |
| 9 | |
| 10 | echo '{' . |
| 11 | "status: " . INTERNAL_SERVER_ERROR . |
| 12 | '"title": "El resultado no puede representarse como JSON."' . |
| 13 | '"type": "/errors/resultadonojson.html"' . |
| 14 | '}'; |
| 15 | } |
| 16 |
| 1 | <?php |
| 2 | |
| 3 | const INTERNAL_SERVER_ERROR = 500; |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php"; |
| 4 | require_once __DIR__ . "/ProblemDetailsException.php"; |
| 5 | |
| 6 | // Hace que se lance una excepción automáticamente cuando se genere un error. |
| 7 | set_error_handler(function ($severity, $message, $file, $line) { |
| 8 | throw new ErrorException($message, 0, $severity, $file, $line); |
| 9 | }); |
| 10 | |
| 11 | // Código cuando una excepción no es atrapada. |
| 12 | set_exception_handler(function (Throwable $excepcion) { |
| 13 | if ($excepcion instanceof ProblemDetailsException) { |
| 14 | devuelveProblemDetails($excepcion->problemDetails); |
| 15 | } else { |
| 16 | devuelveProblemDetails([ |
| 17 | "status" => INTERNAL_SERVER_ERROR, |
| 18 | "title" => "Error interno del servidor", |
| 19 | "detail" => $excepcion->getMessage(), |
| 20 | "type" => "/errors/errorinterno.html", |
| 21 | ]); |
| 22 | } |
| 23 | exit(); |
| 24 | }); |
| 25 | |
| 26 | function devuelveProblemDetails(array $array) |
| 27 | { |
| 28 | $json = json_encode($array); |
| 29 | if ($json === false) { |
| 30 | devuelveResultadoNoJson(); |
| 31 | } else { |
| 32 | http_response_code(isset($array["status"]) ? $array["status"] : 500); |
| 33 | header("Content-Type: application/problem+json; charset=utf-8"); |
| 34 | echo $json; |
| 35 | } |
| 36 | } |
| 37 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php"; |
| 4 | |
| 5 | /** |
| 6 | * Detalle de los errores devueltos por un servicio. |
| 7 | */ |
| 8 | class ProblemDetailsException extends Exception |
| 9 | { |
| 10 | |
| 11 | public array $problemDetails; |
| 12 | |
| 13 | public function __construct( |
| 14 | array $problemDetails, |
| 15 | ) { |
| 16 | |
| 17 | parent::__construct( |
| 18 | isset($problemDetails["detail"]) |
| 19 | ? $problemDetails["detail"] |
| 20 | : (isset($problemDetails["title"]) |
| 21 | ? $problemDetails["title"] |
| 22 | : "Error"), |
| 23 | $problemDetails["status"] |
| 24 | ? $problemDetails["status"] |
| 25 | : INTERNAL_SERVER_ERROR |
| 26 | ); |
| 27 | |
| 28 | $this->problemDetails = $problemDetails; |
| 29 | } |
| 30 | } |
| 31 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/BAD_REQUEST.php"; |
| 4 | |
| 5 | function recibeJson() |
| 6 | { |
| 7 | $json = json_decode(file_get_contents("php://input")); |
| 8 | |
| 9 | if ($json === null) { |
| 10 | |
| 11 | http_response_code(BAD_REQUEST); |
| 12 | header("Content-Type: application/problem+json; charset=utf-8"); |
| 13 | |
| 14 | echo '{' . |
| 15 | "status: " . BAD_REQUEST . |
| 16 | '"title": "Los datos recibidos no están en formato JSON."' . |
| 17 | '"type": "/errors/datosnojson.html"' . |
| 18 | '}'; |
| 19 | |
| 20 | exit(); |
| 21 | } |
| 22 | |
| 23 | return $json; |
| 24 | } |
| 25 |