En esta lección se muestra un ejemplo de notificaciones push.
Puedes probar el ejemplo en https://notipush.rf.gd/.
Primero suscríbete y luego envía notificaciones a todos los que estén conectados.
Prueba el ejemplo en https://notipush.rf.gd/.
Copia la url de la app y pégala en varios navegadores y dispositivos.
Suscríbanse y envíen notificaciones ntre los dispositivos conectados.
Descarga el archivo /src/notipush.zip y descompáctalo.
Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com
Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.
Crea un repositorio nuevo. En el nombre del repositorio debes poner el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io
Edita los archivos que desees.
Prueba tu sitio localmente.
Necesitas un hosting. En este ejemplo se muestra como usar el hosting. https://infinityfree.com/ Si no lo has usado, lo primero que tienes que hacer es entrar a registrar tu email con el botón Registrar. Si ya tienes tu email registrado, omite este paso.
Crea una cuenta. Si ya tienes cuenta, entra a ella y crea un nuevo dominio. En este ejemplo no se crean los archivos directamente en el hosting.
Sube tus archivos a GitHub. En este ejemplo no hay archivo sw.js ni necesitas esperar 11 o más minutos.
Este ejercicio usa la librería WebPush para PHP. Puedes profundizar en este tema en la URL https://github.com/web-push-libs/web-push-php
Prueba el ejemplo en https://notipush.rf.gd/.
Copia la url de la app y pégala en varios navegadores y dispositivos.
Suscríbanse y envíen notificaciones ntre los dispositivos conectados.
Descarga el archivo /src/notipush.zip y descompáctalo.
Crea tu proyecto en GitHub:
Crea una cuenta de email, por ejemplo, pepito@google.com
Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo pepito.
Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.
En la página Create a new repository introduce los siguientes datos:
Proporciona el nombre de tu repositorio debajo de donde dice Repository name *.
Mantén la selección Public para que otros usuarios puedan ver tu proyecto.
Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.
Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.
Cliquea Create repository.
Importa el proyecto en GitHub:
En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.
En Visual Studio Code, usa el botón de la izquierda para Source Control.
Cliquea el botón Clone Repository.
Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.
Selecciona la carpeta donde se guardará la carpeta del proyecto.
Abre la carpeta del proyecto importado.
Añade el contenido de la carpeta descompactada que contiene el código del ejemplo.
Edita los archivos que desees.
El ejercicio usa una llave pública en el servidor y la repite el código de JavaScript en el navegador. También se requiere la correspondiente llave privada que se almacena solo en el servidor.
Las llaves se pueden generar con el enlace Genera llaves del ejemplo. Para copiar los valores despliega el código fuente de la página, porque en el navegador algunos caracteres pueden tener interpretación diferente.
El proyecto ya contiene la carpeta vendor
y el archivo
composer.lock
, pero es posible crearlos con estos pasos:
Instalar composer. Para Windows, usa el instalador de https://getcomposer.org/download/.
Abre una terminal y ejecuta el comando
composer update
Haz clic derecho en index.html
, selecciona
PHP Server: serve project y se abre el navegador para que puedas
probar localmente el ejemplo.
Para depurar paso a paso haz lo siguiente:
En el navegador, haz clic derecho en la página que deseas depurar y selecciona inspeccionar.
Recarga la página, de preferencia haciendo clic derecho en el ícono de
volver a cargar la página
y
seleccionando vaciar caché y volver a cargar de manera forzada (o
algo parecido). Si no aparece un menú emergente, simplemente cliquea
volver a cargar la página
.
Revisa que no aparezca ningún error ni en la pestañas Consola, ni
en Red.
Selecciona la pestaña Fuentes (o Sources si tu navegador está en Inglés).
Selecciona el archivo donde vas a empezar a depurar.
Haz clic en el número de la línea donde vas a empezar a depurar.
En Visual Studio Code, abre el archivo de PHP donde vas a empezar a depurar.
Haz clic en Run and Debug
.
Si no está configurada la depuración, haz clic en create a launch json file.
Haz clic en la flechita RUN AND DEBUG, al lado de la cual debe
decir Listen for Xdebug
.
Aparece un cuadro con los controles de depuración.
Selecciona otra vez el archivo de PHP y haz clic en el número de la línea donde vas a empezar a depurar.
Regresa al navegador, recarga la página de manera normal y empieza a usarla.
Si se ejecuta alguna de las líneas de código seleccionadas, aparece resaltada en la pestaña de fuentes. Usa los controles de depuración para avanzar, como se muestra en este video.
Sube el proyecto al hosting que elijas.
Crea una nueva carpeta para crear un nuevo proyecto que estará conectado directamente al servidor web por ftp.
Abre la nueva carpeta con Visual Studio Code.
Tecle al mismo Mayúsculas+Control+P y selecciona SFTP: Config. Aparece un archivo de configuración de FTP. Llena los datos con la configuración de FTP de tu servidor, excepto la contraseña.
Cliquea el botón de SFTP y luego haz clic en la URL de tu servidos. En la barra superior te pide la contraseña y ENTER.
Pásate a la parte de archivos y coloca tus archivos.
Cliquea con el botón derecho en la sección de archivos y selecciona Sync: Local -> Remote.
Abre un navegador y prueba el proyecto en tu hosting.
En el hosting InfinityFree, la primera vez que corres la página, puede marcar un mensaje de error, pero al recargar funciona correctamente. Puedes evitar este problema usando un dominio propio.
Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.
Haz clic en los triángulos para expandir las carpetas
| 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>Notificaciones Push</title> |
| 10 | |
| 11 | <script type="module" src="js/lib/manejaErrores.js"></script> |
| 12 | |
| 13 | </head> |
| 14 | |
| 15 | <body> |
| 16 | |
| 17 | <h1>Notificaciones Push</h1> |
| 18 | |
| 19 | <menu style="display: flex; list-style: none; flex-wrap: wrap; gap: 0.5rem;"> |
| 20 | <li> |
| 21 | <button id="btnSuscribe" type="button" hidden> |
| 22 | Suscríbete |
| 23 | </button> |
| 24 | </li> |
| 25 | <li> |
| 26 | <button id="btnCancela" type="button" hidden> |
| 27 | Cancela suscripción |
| 28 | </button> |
| 29 | </li> |
| 30 | <li> |
| 31 | <button id="btnNotifica" type="button" hidden> |
| 32 | Notifica |
| 33 | </button> |
| 34 | </li> |
| 35 | <li> |
| 36 | <a href="srv/genera-llaves.php" target="_blank">Genera llaves</a> |
| 37 | </li> |
| 38 | </menu> |
| 39 | |
| 40 | <p> |
| 41 | <output id="outEstado"> |
| 42 | <progress max="100">Cargando…</progress> |
| 43 | </output> |
| 44 | </p> |
| 45 | |
| 46 | <fieldset> |
| 47 | <legend>Reporte de envío a endpoints</legend> |
| 48 | |
| 49 | <dl id="reporte"></dl> |
| 50 | |
| 51 | </fieldset> |
| 52 | |
| 53 | <script type="module"> |
| 54 | |
| 55 | import { |
| 56 | activaNotificacionesPush |
| 57 | } from "./js/lib/activaNotificacionesPush.js" |
| 58 | import { getSuscripcionPush } from "./js/lib/getSuscripcionPush.js" |
| 59 | import { suscribeAPush } from "./js/lib/suscribeAPush.js" |
| 60 | import { cancelaSuscripcionPush } from "./js/lib/cancelaSuscripcionPush.js" |
| 61 | import { urlBase64ToUint8Array } from "./js/lib/urlBase64ToUint8Array.js" |
| 62 | import { |
| 63 | calculaDtoParaSuscripcion |
| 64 | } from "./js/lib/calculaDtoParaSuscripcion.js" |
| 65 | import { consume } from "./js/lib/consume.js" |
| 66 | import { enviaJsonRecibeJson } from "./js/lib/enviaJsonRecibeJson.js" |
| 67 | import { descargaVista } from "./js/lib/descargaVista.js" |
| 68 | import { muestraError } from "./js/lib/muestraError.js" |
| 69 | |
| 70 | const applicationServerKey = urlBase64ToUint8Array( |
| 71 | "BMBlr6YznhYMX3NgcWIDRxZXs0sh7tCv7_YCsWcww0ZCv9WGg-tRCXfMEHTiBPCksSqeve1twlbmVAZFv7GSuj0") |
| 72 | /** @enum {string} */ |
| 73 | const Estado = { |
| 74 | CALCULANDO: "Calculando…", |
| 75 | SUSCRITO: "Suscrito", |
| 76 | DESUSCRITO: "Sin suscripción", |
| 77 | INCOMPATIBLE: "Incompatible" |
| 78 | } |
| 79 | |
| 80 | preparaVista() |
| 81 | btnSuscribe.addEventListener("click", suscribe) |
| 82 | btnCancela.addEventListener("click", cancela) |
| 83 | btnNotifica.addEventListener("click", notificaDesdeElServidor) |
| 84 | |
| 85 | async function preparaVista() { |
| 86 | try { |
| 87 | await activaNotificacionesPush("sw.js") |
| 88 | const suscripcion = await getSuscripcionPush() |
| 89 | if (suscripcion === null) { |
| 90 | muestraEstado(Estado.DESUSCRITO) |
| 91 | } else { |
| 92 | // Modifica la suscripción en el servidor, |
| 93 | const dto = calculaDtoParaSuscripcion(suscripcion) |
| 94 | await consume(enviaJsonRecibeJson("php/suscripcion-modifica.php", dto)) |
| 95 | muestraEstado(Estado.SUSCRITO) |
| 96 | } |
| 97 | } catch (error) { |
| 98 | muestraEstado(Estado.INCOMPATIBLE) |
| 99 | muestraError(error) |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | async function notificaDesdeElServidor() { |
| 104 | reporte.innerHTML = |
| 105 | /* html */ `<progress max="100">Cargando…</progress>` |
| 106 | await descargaVista("php/notifica.php", "POST") |
| 107 | } |
| 108 | |
| 109 | async function suscribe() { |
| 110 | muestraEstado(Estado.CALCULANDO) |
| 111 | const suscripcion = await suscribeAPush(applicationServerKey) |
| 112 | // Agrega la suscripción al servidor, |
| 113 | const dto = calculaDtoParaSuscripcion(suscripcion) |
| 114 | await consume(enviaJsonRecibeJson("php/suscripcion-modifica.php", dto) |
| 115 | ) |
| 116 | muestraEstado(Estado.SUSCRITO) |
| 117 | } |
| 118 | |
| 119 | async function cancela() { |
| 120 | muestraEstado(Estado.CALCULANDO) |
| 121 | const suscripcion = await cancelaSuscripcionPush() |
| 122 | if (suscripcion !== null) { |
| 123 | // Elimina la suscripción en el servidor, |
| 124 | const dto = calculaDtoParaSuscripcion(suscripcion) |
| 125 | await consume(enviaJsonRecibeJson("php/suscripcion-elimina.php", dto)) |
| 126 | muestraEstado(Estado.DESUSCRITO) |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | /** |
| 131 | * @param {Estado} estado |
| 132 | */ |
| 133 | function muestraEstado(estado) { |
| 134 | outEstado.value = estado |
| 135 | if (estado === Estado.INCOMPATIBLE || estado === Estado.CALCULANDO) { |
| 136 | btnSuscribe.hidden = true |
| 137 | btnCancela.hidden = true |
| 138 | btnNotifica.hidden = true |
| 139 | } else if (estado === Estado.SUSCRITO) { |
| 140 | btnSuscribe.hidden = true |
| 141 | btnCancela.hidden = false |
| 142 | btnNotifica.hidden = false |
| 143 | } else if (estado === Estado.DESUSCRITO) { |
| 144 | btnSuscribe.hidden = false |
| 145 | btnCancela.hidden = true |
| 146 | btnNotifica.hidden = true |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | </script> |
| 151 | |
| 152 | </body> |
| 153 | |
| 154 | </html> |
| 1 | const URL_SERVIDOR = "https://notipush.rf.gd" |
| 2 | |
| 3 | if (self instanceof ServiceWorkerGlobalScope) { |
| 4 | |
| 5 | // El siguiente código se activa cuando llega una notificación push. |
| 6 | self.addEventListener("push", (/** @type {PushEvent} */ event) => { |
| 7 | const notificacion = event.data |
| 8 | /* Si el navegador tiene permitido mostrar notificaciones push, |
| 9 | * nuestra la que se ha recibido. */ |
| 10 | if (notificacion !== null && self.Notification.permission === 'granted') { |
| 11 | event.waitUntil(muestraNotificacion(notificacion)) |
| 12 | } |
| 13 | }) |
| 14 | |
| 15 | self.addEventListener("notificationclick", |
| 16 | (/** @type {NotificationEvent} */ event) => { |
| 17 | event.notification.close() |
| 18 | event.waitUntil(muestraVentana()) |
| 19 | }) |
| 20 | } |
| 21 | |
| 22 | /** |
| 23 | * @param {PushMessageData} notificacion |
| 24 | */ |
| 25 | async function muestraNotificacion(notificacion) { |
| 26 | if (self instanceof ServiceWorkerGlobalScope) { |
| 27 | const mensaje = notificacion.text() |
| 28 | await self.registration.showNotification(mensaje) |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | async function muestraVentana() { |
| 33 | if (self instanceof ServiceWorkerGlobalScope) { |
| 34 | const clientes = await self.clients.matchAll({ type: "window" }) |
| 35 | for (const cliente of clientes) { |
| 36 | if (cliente.url.startsWith(URL_SERVIDOR)) { |
| 37 | return cliente.focus() |
| 38 | } |
| 39 | } |
| 40 | return self.clients.openWindow("/") |
| 41 | } |
| 42 | } |
| 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 |
| 1 | /** |
| 2 | * @param { string | URL } urlDeServiceWorkerQueRecibeNotificaciones |
| 3 | */ |
| 4 | export async function activaNotificacionesPush( |
| 5 | |
| 6 | urlDeServiceWorkerQueRecibeNotificaciones) { |
| 7 | |
| 8 | // Valida que el navegador soporte notificaciones push. |
| 9 | if (!('PushManager' in window)) |
| 10 | throw new Error("Este navegador no soporta notificaciones push.") |
| 11 | |
| 12 | // Valida que el navegador soporte notificaciones, |
| 13 | if (!("Notification" in window)) |
| 14 | throw new Error("Este navegador no soporta notificaciones push.") |
| 15 | |
| 16 | // Valida que el navegador soporte service workers, |
| 17 | if (!("serviceWorker" in navigator)) |
| 18 | throw new Error("Este navegador no soporta service workers.") |
| 19 | |
| 20 | // Recupera el permiso para usar notificaciones |
| 21 | let permiso = Notification.permission |
| 22 | if (permiso === "default") { |
| 23 | // Permiso no asignado. Pide al usuario su autorización. |
| 24 | permiso = await Notification.requestPermission() |
| 25 | } |
| 26 | |
| 27 | // Valida que el usuario haya permitido usar notificaciones.. |
| 28 | if (permiso === "denied") |
| 29 | throw new Error("Notificaciones bloqueadas.") |
| 30 | |
| 31 | const registro = await navigator.serviceWorker.register( |
| 32 | urlDeServiceWorkerQueRecibeNotificaciones) |
| 33 | console.log(urlDeServiceWorkerQueRecibeNotificaciones, "registrado.") |
| 34 | console.log(registro) |
| 35 | |
| 36 | if (!("showNotification" in registro)) |
| 37 | throw new Error("Este navegador no soporta notificaciones.") |
| 38 | } |
| 1 | /** |
| 2 | * Devuelve una literal de objeto que se puede usar para enviar |
| 3 | * en formato JSON al servidor. |
| 4 | * DTO es un acrónimo para Data Transder Object, u |
| 5 | * objeto para transferencia de datos. |
| 6 | * @param { PushSubscription } suscripcion |
| 7 | */ |
| 8 | export function calculaDtoParaSuscripcion(suscripcion) { |
| 9 | const key = suscripcion.getKey("p256dh") |
| 10 | const token = suscripcion.getKey("auth") |
| 11 | const supported = PushManager.supportedContentEncodings |
| 12 | const encodings = Array.isArray(supported) && supported.length > 0 |
| 13 | ? supported |
| 14 | : ["aesgcm"] |
| 15 | const endpoint = suscripcion.endpoint |
| 16 | const publicKey = key === null |
| 17 | ? null |
| 18 | : btoa(String.fromCharCode.apply(null, new Uint8Array(key))) |
| 19 | const authToken = token === null |
| 20 | ? null |
| 21 | : btoa(String.fromCharCode.apply(null, new Uint8Array(token))) |
| 22 | const contentEncoding = encodings[0] |
| 23 | return { |
| 24 | endpoint, |
| 25 | publicKey, |
| 26 | authToken, |
| 27 | contentEncoding |
| 28 | } |
| 29 | } |
| 30 |
| 1 | import { getSuscripcionPush } from "./getSuscripcionPush.js" |
| 2 | |
| 3 | export async function cancelaSuscripcionPush() { |
| 4 | const suscripcion = await getSuscripcionPush() |
| 5 | const resultado = suscripcion === null |
| 6 | ? false |
| 7 | : await suscripcion.unsubscribe() |
| 8 | return resultado === true ? suscripcion : null |
| 9 | } |
| 1 | import { ProblemDetailsError } from "./ProblemDetailsError.js" |
| 2 | |
| 3 | /** |
| 4 | * Espera a que la promesa de un fetch termine. Si |
| 5 | * hay error, lanza una excepción. |
| 6 | * |
| 7 | * @param {Promise<Response> } servicio |
| 8 | */ |
| 9 | export async function consume(servicio) { |
| 10 | const respuesta = await servicio |
| 11 | if (respuesta.ok) { |
| 12 | return respuesta |
| 13 | } else { |
| 14 | const contentType = respuesta.headers.get("Content-Type") |
| 15 | if ( |
| 16 | contentType !== null && contentType.startsWith("application/problem+json") |
| 17 | ) |
| 18 | throw new ProblemDetailsError(await respuesta.json()) |
| 19 | else |
| 20 | throw new Error(respuesta.statusText) |
| 21 | } |
| 22 | } |
| 1 | import { consume } from "./consume.js" |
| 2 | import { muestraObjeto } from "./muestraObjeto.js" |
| 3 | import { recibeJson } from "./recibeJson.js" |
| 4 | |
| 5 | /** |
| 6 | * @param {string} url |
| 7 | * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS" |
| 8 | * | "CONNECT" | "HEAD" } metodoHttp |
| 9 | */ |
| 10 | export async function descargaVista(url, metodoHttp = "GET") { |
| 11 | const respuesta = await consume(recibeJson(url, metodoHttp)) |
| 12 | const json = await respuesta.json() |
| 13 | muestraObjeto(document, json) |
| 14 | return json |
| 15 | } |
| 1 | |
| 2 | /** |
| 3 | * @param { string } url |
| 4 | * @param { Object } body |
| 5 | * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS" |
| 6 | * | "CONNECT" | "HEAD" } metodoHttp |
| 7 | */ |
| 8 | export async function enviaJsonRecibeJson(url, body, metodoHttp = "POST") { |
| 9 | return fetch( |
| 10 | url, |
| 11 | { |
| 12 | method: metodoHttp, |
| 13 | headers: { |
| 14 | "Content-Type": "application/json", |
| 15 | "Accept": "application/json, application/problem+json" |
| 16 | }, |
| 17 | body: JSON.stringify(body) |
| 18 | } |
| 19 | ) |
| 20 | } |
| 1 | export async function getSuscripcionPush() { |
| 2 | // Recupera el service worker registrado. |
| 3 | const registro = await navigator.serviceWorker.ready |
| 4 | return registro.pushManager.getSubscription() |
| 5 | } |
| 1 | import { muestraError } from "./muestraError.js" |
| 2 | |
| 3 | /** |
| 4 | * Intercepta Response.prototype.json para capturar errores de parseo |
| 5 | * y asegurar que se reporten correctamente en navegadores Chromium. |
| 6 | */ |
| 7 | { |
| 8 | const originalJson = Response.prototype.json |
| 9 | |
| 10 | Response.prototype.json = function () { |
| 11 | // Llamamos al método original usando el contexto (this) de la respuesta |
| 12 | return originalJson.apply(this, arguments) |
| 13 | .catch((/** @type {any} */ error) => { |
| 14 | // Corrige un error de Chrome que evita el manejo correcto de errores. |
| 15 | throw new Error(error) |
| 16 | }) |
| 17 | } |
| 18 | } |
| 19 | |
| 20 | window.onerror = function ( |
| 21 | /** @type {string} */ _message, |
| 22 | /** @type {string} */ _url, |
| 23 | /** @type {number} */ _line, |
| 24 | /** @type {number} */ _column, |
| 25 | /** @type {Error} */ errorObject |
| 26 | ) { |
| 27 | muestraError(errorObject) |
| 28 | return true |
| 29 | } |
| 30 | |
| 31 | window.addEventListener('unhandledrejection', event => { |
| 32 | muestraError(event.reason) |
| 33 | event.preventDefault() |
| 34 | }) |
| 35 |
| 1 | import { ProblemDetailsError } from "./ProblemDetailsError.js" |
| 2 | |
| 3 | /** |
| 4 | * Muestra los datos de una Error en la consola y en un cuadro de alerta. |
| 5 | * @param { ProblemDetailsError | Error | null } error descripción del error. |
| 6 | */ |
| 7 | export function muestraError(error) { |
| 8 | |
| 9 | if (error === null) { |
| 10 | |
| 11 | console.error("Error") |
| 12 | alert("Error") |
| 13 | |
| 14 | } else if (error instanceof ProblemDetailsError) { |
| 15 | |
| 16 | const problemDetails = error.problemDetails |
| 17 | |
| 18 | let mensaje = |
| 19 | typeof problemDetails["title"] === "string" ? problemDetails["title"] : "" |
| 20 | if (typeof problemDetails["detail"] === "string") { |
| 21 | if (mensaje !== "") { |
| 22 | mensaje += "\n\n" |
| 23 | } |
| 24 | mensaje += problemDetails["detail"] |
| 25 | } |
| 26 | if (mensaje === "") { |
| 27 | mensaje = "Error" |
| 28 | } |
| 29 | console.error(error, problemDetails) |
| 30 | alert(mensaje) |
| 31 | |
| 32 | } else { |
| 33 | |
| 34 | console.error(error) |
| 35 | alert(error.message) |
| 36 | |
| 37 | } |
| 38 | |
| 39 | } |
| 1 | /** |
| 2 | * @param {Document | HTMLElement | ShadowRoot} raizHtml |
| 3 | * @param { any } objeto |
| 4 | */ |
| 5 | export function muestraObjeto(raizHtml, objeto) { |
| 6 | for (const [nombre, definiciones] of Object.entries(objeto)) { |
| 7 | if (Array.isArray(definiciones)) { |
| 8 | muestraArray(raizHtml, nombre, definiciones) |
| 9 | } else if (definiciones !== undefined && definiciones !== null) { |
| 10 | muestraElemento(raizHtml, nombre, definiciones) |
| 11 | } |
| 12 | } |
| 13 | } |
| 14 | |
| 15 | /** |
| 16 | * @param { string } nombre |
| 17 | */ |
| 18 | export function selectorDeNombre(nombre) { |
| 19 | return `[id="${nombre}"],[name="${nombre}"],[data-name="${nombre}"]` |
| 20 | } |
| 21 | |
| 22 | /** |
| 23 | * @param { Document | HTMLElement | ShadowRoot } raizHtml |
| 24 | * @param { string } propiedad |
| 25 | * @param {any[]} valores |
| 26 | */ |
| 27 | function muestraArray(raizHtml, propiedad, valores) { |
| 28 | const conjunto = new Set(valores) |
| 29 | const elementos = raizHtml.querySelectorAll(selectorDeNombre(propiedad)) |
| 30 | if (elementos.length === 1 && elementos[0] instanceof HTMLSelectElement) { |
| 31 | muestraOptions(elementos[0], conjunto) |
| 32 | } else { |
| 33 | muestraInputs(elementos, conjunto) |
| 34 | } |
| 35 | |
| 36 | } |
| 37 | |
| 38 | /** |
| 39 | * @param {HTMLSelectElement} select |
| 40 | * @param {Set<any>} conjunto |
| 41 | */ |
| 42 | function muestraOptions(select, conjunto) { |
| 43 | for (let i = 0, options = select.options, len = options.length; i < len; i++) { |
| 44 | const option = options[i] |
| 45 | option.selected = conjunto.has(option.value) |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | /** |
| 50 | * @param {NodeListOf<Element>} elementos |
| 51 | * @param {Set<any>} conjunto |
| 52 | */ |
| 53 | function muestraInputs(elementos, conjunto) { |
| 54 | for (let i = 0, len = elementos.length; i < len; i++) { |
| 55 | const elemento = elementos[i] |
| 56 | if (elemento instanceof HTMLInputElement) { |
| 57 | elemento.checked = conjunto.has(elemento.value) |
| 58 | } |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | const data_ = "data-" |
| 63 | const data_Length = data_.length |
| 64 | |
| 65 | /** |
| 66 | * @param {Document | HTMLElement | ShadowRoot} raizHtml |
| 67 | * @param {string} nombre |
| 68 | * @param {{ [s: string]: any; } } definiciones |
| 69 | */ |
| 70 | function muestraElemento(raizHtml, nombre, definiciones) { |
| 71 | const elemento = raizHtml.querySelector(selectorDeNombre(nombre)) |
| 72 | if (elemento !== null) { |
| 73 | for (const [propiedad, valor] of Object.entries(definiciones)) { |
| 74 | if (propiedad in elemento) { |
| 75 | elemento[propiedad] = valor |
| 76 | } else if ( |
| 77 | propiedad.length > data_Length |
| 78 | && propiedad.startsWith(data_) |
| 79 | && elemento instanceof HTMLElement |
| 80 | ) { |
| 81 | elemento.dataset[propiedad.substring(data_Length)] = valor |
| 82 | } |
| 83 | } |
| 84 | } |
| 85 | } |
| 1 | export class ProblemDetailsError extends Error { |
| 2 | |
| 3 | /** |
| 4 | * Detalle de los errores devueltos por un servicio. |
| 5 | * Crea una instancia de ProblemDetailsError. |
| 6 | * @param {object} problemDetails Objeto con la descripcipon del error. |
| 7 | */ |
| 8 | constructor(problemDetails) { |
| 9 | |
| 10 | super(typeof problemDetails["detail"] === "string" |
| 11 | ? problemDetails["detail"] |
| 12 | : (typeof problemDetails["title"] === "string" |
| 13 | ? problemDetails["title"] |
| 14 | : "Error")) |
| 15 | |
| 16 | this.problemDetails = problemDetails |
| 17 | |
| 18 | } |
| 19 | |
| 20 | } |
| 1 | |
| 2 | /** |
| 3 | * @param {string} url |
| 4 | * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS" |
| 5 | * | "CONNECT" | "HEAD" } metodoHttp |
| 6 | */ |
| 7 | export async function recibeJson(url, metodoHttp = "GET") { |
| 8 | return fetch( |
| 9 | url, |
| 10 | { |
| 11 | method: metodoHttp, |
| 12 | headers: { "Accept": "application/json, application/problem+json" } |
| 13 | } |
| 14 | ) |
| 15 | } |
| 1 | /** |
| 2 | * @param { ArrayBuffer } applicationServerKey |
| 3 | */ |
| 4 | export async function suscribeAPush(applicationServerKey) { |
| 5 | // Recupera el service worker registrado. |
| 6 | const registro = await navigator.serviceWorker.ready |
| 7 | return registro.pushManager.subscribe({ |
| 8 | userVisibleOnly: true, |
| 9 | applicationServerKey |
| 10 | }) |
| 11 | } |
| 1 | /** |
| 2 | * @param { string } base64String |
| 3 | */ |
| 4 | export function urlBase64ToUint8Array(base64String) { |
| 5 | const padding = "=".repeat((4 - (base64String.length % 4)) % 4) |
| 6 | const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/') |
| 7 | const rawData = atob(base64) |
| 8 | const outputArray = new Uint8Array(rawData.length) |
| 9 | for (let i = 0; i < rawData.length; ++i) { |
| 10 | outputArray[i] = rawData.charCodeAt(i) |
| 11 | } |
| 12 | return outputArray |
| 13 | } |
| 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>El authToken debe ser texto que no esté en blanco</title> |
| 10 | |
| 11 | <body> |
| 12 | |
| 13 | <h1>El authToken debe ser texto que no esté en blanco</h1> |
| 14 | |
| 15 | </body> |
| 16 | |
| 17 | </html> |
| 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>La contentEncoding debe ser texto que no esté en blanco</title> |
| 10 | |
| 11 | <body> |
| 12 | |
| 13 | <h1>La contentEncoding debe ser texto que no esté en blanco</h1> |
| 14 | |
| 15 | </body> |
| 16 | |
| 17 | </html> |
| 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>Los datos recibidos no están en formato JSON</title> |
| 10 | |
| 11 | </head> |
| 12 | |
| 13 | <body> |
| 14 | |
| 15 | <h1>Los datos recibidos no están en formato JSON</h1> |
| 16 | |
| 17 | </body> |
| 18 | |
| 19 | </html> |
| 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>El endpoint debe ser texto que no esté en blanco</title> |
| 10 | |
| 11 | <body> |
| 12 | |
| 13 | <h1>El endpoint debe ser texto que no esté en blanco</h1> |
| 14 | |
| 15 | </body> |
| 16 | |
| 17 | </html> |
| 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>Error interno del servidor</title> |
| 10 | |
| 11 | </head> |
| 12 | |
| 13 | <body> |
| 14 | |
| 15 | <h1>Error interno del servidor</h1> |
| 16 | |
| 17 | <p>Se presentó de forma inesperada un error interno del servidor.</p> |
| 18 | |
| 19 | </body> |
| 20 | |
| 21 | </html> |
| 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>La publicKey debe ser texto que no esté en blanco</title> |
| 10 | |
| 11 | <body> |
| 12 | |
| 13 | <h1>La publicKey debe ser texto que no esté en blanco</h1> |
| 14 | |
| 15 | </body> |
| 16 | |
| 17 | </html> |
| 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>El resultado no puede representarse como JSON</title> |
| 10 | |
| 11 | </head> |
| 12 | |
| 13 | <body> |
| 14 | |
| 15 | <h1>El resultado no puede representarse como JSON</h1> |
| 16 | |
| 17 | <p> |
| 18 | Debido a un error interno del servidor, el resultado generado, no se puede |
| 19 | recuperar. |
| 20 | </p> |
| 21 | |
| 22 | </body> |
| 23 | |
| 24 | </html> |
| 1 | { |
| 2 | "require": { |
| 3 | "minishlink/web-push": "^10.0.1" |
| 4 | } |
| 5 | } |
| 1 | { |
| 2 | "_readme": [ |
| 3 | "This file locks the dependencies of your project to a known state", |
| 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", |
| 5 | "This file is @generated automatically" |
| 6 | ], |
| 7 | "content-hash": "5d03d83a0c68b97c1b883f5e5d450861", |
| 8 | "packages": [ |
| 9 | { |
| 10 | "name": "brick/math", |
| 11 | "version": "0.14.1", |
| 12 | "source": { |
| 13 | "type": "git", |
| 14 | "url": "https://github.com/brick/math.git", |
| 15 | "reference": "f05858549e5f9d7bb45875a75583240a38a281d0" |
| 16 | }, |
| 17 | "dist": { |
| 18 | "type": "zip", |
| 19 | "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0", |
| 20 | "reference": "f05858549e5f9d7bb45875a75583240a38a281d0", |
| 21 | "shasum": "" |
| 22 | }, |
| 23 | "require": { |
| 24 | "php": "^8.2" |
| 25 | }, |
| 26 | "require-dev": { |
| 27 | "php-coveralls/php-coveralls": "^2.2", |
| 28 | "phpstan/phpstan": "2.1.22", |
| 29 | "phpunit/phpunit": "^11.5" |
| 30 | }, |
| 31 | "type": "library", |
| 32 | "autoload": { |
| 33 | "psr-4": { |
| 34 | "Brick\\Math\\": "src/" |
| 35 | } |
| 36 | }, |
| 37 | "notification-url": "https://packagist.org/downloads/", |
| 38 | "license": [ |
| 39 | "MIT" |
| 40 | ], |
| 41 | "description": "Arbitrary-precision arithmetic library", |
| 42 | "keywords": [ |
| 43 | "Arbitrary-precision", |
| 44 | "BigInteger", |
| 45 | "BigRational", |
| 46 | "arithmetic", |
| 47 | "bigdecimal", |
| 48 | "bignum", |
| 49 | "bignumber", |
| 50 | "brick", |
| 51 | "decimal", |
| 52 | "integer", |
| 53 | "math", |
| 54 | "mathematics", |
| 55 | "rational" |
| 56 | ], |
| 57 | "support": { |
| 58 | "issues": "https://github.com/brick/math/issues", |
| 59 | "source": "https://github.com/brick/math/tree/0.14.1" |
| 60 | }, |
| 61 | "funding": [ |
| 62 | { |
| 63 | "url": "https://github.com/BenMorel", |
| 64 | "type": "github" |
| 65 | } |
| 66 | ], |
| 67 | "time": "2025-11-24T14:40:29+00:00" |
| 68 | }, |
| 69 | { |
| 70 | "name": "guzzlehttp/guzzle", |
| 71 | "version": "7.10.0", |
| 72 | "source": { |
| 73 | "type": "git", |
| 74 | "url": "https://github.com/guzzle/guzzle.git", |
| 75 | "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" |
| 76 | }, |
| 77 | "dist": { |
| 78 | "type": "zip", |
| 79 | "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", |
| 80 | "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", |
| 81 | "shasum": "" |
| 82 | }, |
| 83 | "require": { |
| 84 | "ext-json": "*", |
| 85 | "guzzlehttp/promises": "^2.3", |
| 86 | "guzzlehttp/psr7": "^2.8", |
| 87 | "php": "^7.2.5 || ^8.0", |
| 88 | "psr/http-client": "^1.0", |
| 89 | "symfony/deprecation-contracts": "^2.2 || ^3.0" |
| 90 | }, |
| 91 | "provide": { |
| 92 | "psr/http-client-implementation": "1.0" |
| 93 | }, |
| 94 | "require-dev": { |
| 95 | "bamarni/composer-bin-plugin": "^1.8.2", |
| 96 | "ext-curl": "*", |
| 97 | "guzzle/client-integration-tests": "3.0.2", |
| 98 | "php-http/message-factory": "^1.1", |
| 99 | "phpunit/phpunit": "^8.5.39 || ^9.6.20", |
| 100 | "psr/log": "^1.1 || ^2.0 || ^3.0" |
| 101 | }, |
| 102 | "suggest": { |
| 103 | "ext-curl": "Required for CURL handler support", |
| 104 | "ext-intl": "Required for Internationalized Domain Name (IDN) support", |
| 105 | "psr/log": "Required for using the Log middleware" |
| 106 | }, |
| 107 | "type": "library", |
| 108 | "extra": { |
| 109 | "bamarni-bin": { |
| 110 | "bin-links": true, |
| 111 | "forward-command": false |
| 112 | } |
| 113 | }, |
| 114 | "autoload": { |
| 115 | "files": [ |
| 116 | "src/functions_include.php" |
| 117 | ], |
| 118 | "psr-4": { |
| 119 | "GuzzleHttp\\": "src/" |
| 120 | } |
| 121 | }, |
| 122 | "notification-url": "https://packagist.org/downloads/", |
| 123 | "license": [ |
| 124 | "MIT" |
| 125 | ], |
| 126 | "authors": [ |
| 127 | { |
| 128 | "name": "Graham Campbell", |
| 129 | "email": "hello@gjcampbell.co.uk", |
| 130 | "homepage": "https://github.com/GrahamCampbell" |
| 131 | }, |
| 132 | { |
| 133 | "name": "Michael Dowling", |
| 134 | "email": "mtdowling@gmail.com", |
| 135 | "homepage": "https://github.com/mtdowling" |
| 136 | }, |
| 137 | { |
| 138 | "name": "Jeremy Lindblom", |
| 139 | "email": "jeremeamia@gmail.com", |
| 140 | "homepage": "https://github.com/jeremeamia" |
| 141 | }, |
| 142 | { |
| 143 | "name": "George Mponos", |
| 144 | "email": "gmponos@gmail.com", |
| 145 | "homepage": "https://github.com/gmponos" |
| 146 | }, |
| 147 | { |
| 148 | "name": "Tobias Nyholm", |
| 149 | "email": "tobias.nyholm@gmail.com", |
| 150 | "homepage": "https://github.com/Nyholm" |
| 151 | }, |
| 152 | { |
| 153 | "name": "Márk Sági-Kazár", |
| 154 | "email": "mark.sagikazar@gmail.com", |
| 155 | "homepage": "https://github.com/sagikazarmark" |
| 156 | }, |
| 157 | { |
| 158 | "name": "Tobias Schultze", |
| 159 | "email": "webmaster@tubo-world.de", |
| 160 | "homepage": "https://github.com/Tobion" |
| 161 | } |
| 162 | ], |
| 163 | "description": "Guzzle is a PHP HTTP client library", |
| 164 | "keywords": [ |
| 165 | "client", |
| 166 | "curl", |
| 167 | "framework", |
| 168 | "http", |
| 169 | "http client", |
| 170 | "psr-18", |
| 171 | "psr-7", |
| 172 | "rest", |
| 173 | "web service" |
| 174 | ], |
| 175 | "support": { |
| 176 | "issues": "https://github.com/guzzle/guzzle/issues", |
| 177 | "source": "https://github.com/guzzle/guzzle/tree/7.10.0" |
| 178 | }, |
| 179 | "funding": [ |
| 180 | { |
| 181 | "url": "https://github.com/GrahamCampbell", |
| 182 | "type": "github" |
| 183 | }, |
| 184 | { |
| 185 | "url": "https://github.com/Nyholm", |
| 186 | "type": "github" |
| 187 | }, |
| 188 | { |
| 189 | "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", |
| 190 | "type": "tidelift" |
| 191 | } |
| 192 | ], |
| 193 | "time": "2025-08-23T22:36:01+00:00" |
| 194 | }, |
| 195 | { |
| 196 | "name": "guzzlehttp/promises", |
| 197 | "version": "2.3.0", |
| 198 | "source": { |
| 199 | "type": "git", |
| 200 | "url": "https://github.com/guzzle/promises.git", |
| 201 | "reference": "481557b130ef3790cf82b713667b43030dc9c957" |
| 202 | }, |
| 203 | "dist": { |
| 204 | "type": "zip", |
| 205 | "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", |
| 206 | "reference": "481557b130ef3790cf82b713667b43030dc9c957", |
| 207 | "shasum": "" |
| 208 | }, |
| 209 | "require": { |
| 210 | "php": "^7.2.5 || ^8.0" |
| 211 | }, |
| 212 | "require-dev": { |
| 213 | "bamarni/composer-bin-plugin": "^1.8.2", |
| 214 | "phpunit/phpunit": "^8.5.44 || ^9.6.25" |
| 215 | }, |
| 216 | "type": "library", |
| 217 | "extra": { |
| 218 | "bamarni-bin": { |
| 219 | "bin-links": true, |
| 220 | "forward-command": false |
| 221 | } |
| 222 | }, |
| 223 | "autoload": { |
| 224 | "psr-4": { |
| 225 | "GuzzleHttp\\Promise\\": "src/" |
| 226 | } |
| 227 | }, |
| 228 | "notification-url": "https://packagist.org/downloads/", |
| 229 | "license": [ |
| 230 | "MIT" |
| 231 | ], |
| 232 | "authors": [ |
| 233 | { |
| 234 | "name": "Graham Campbell", |
| 235 | "email": "hello@gjcampbell.co.uk", |
| 236 | "homepage": "https://github.com/GrahamCampbell" |
| 237 | }, |
| 238 | { |
| 239 | "name": "Michael Dowling", |
| 240 | "email": "mtdowling@gmail.com", |
| 241 | "homepage": "https://github.com/mtdowling" |
| 242 | }, |
| 243 | { |
| 244 | "name": "Tobias Nyholm", |
| 245 | "email": "tobias.nyholm@gmail.com", |
| 246 | "homepage": "https://github.com/Nyholm" |
| 247 | }, |
| 248 | { |
| 249 | "name": "Tobias Schultze", |
| 250 | "email": "webmaster@tubo-world.de", |
| 251 | "homepage": "https://github.com/Tobion" |
| 252 | } |
| 253 | ], |
| 254 | "description": "Guzzle promises library", |
| 255 | "keywords": [ |
| 256 | "promise" |
| 257 | ], |
| 258 | "support": { |
| 259 | "issues": "https://github.com/guzzle/promises/issues", |
| 260 | "source": "https://github.com/guzzle/promises/tree/2.3.0" |
| 261 | }, |
| 262 | "funding": [ |
| 263 | { |
| 264 | "url": "https://github.com/GrahamCampbell", |
| 265 | "type": "github" |
| 266 | }, |
| 267 | { |
| 268 | "url": "https://github.com/Nyholm", |
| 269 | "type": "github" |
| 270 | }, |
| 271 | { |
| 272 | "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", |
| 273 | "type": "tidelift" |
| 274 | } |
| 275 | ], |
| 276 | "time": "2025-08-22T14:34:08+00:00" |
| 277 | }, |
| 278 | { |
| 279 | "name": "guzzlehttp/psr7", |
| 280 | "version": "2.8.0", |
| 281 | "source": { |
| 282 | "type": "git", |
| 283 | "url": "https://github.com/guzzle/psr7.git", |
| 284 | "reference": "21dc724a0583619cd1652f673303492272778051" |
| 285 | }, |
| 286 | "dist": { |
| 287 | "type": "zip", |
| 288 | "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", |
| 289 | "reference": "21dc724a0583619cd1652f673303492272778051", |
| 290 | "shasum": "" |
| 291 | }, |
| 292 | "require": { |
| 293 | "php": "^7.2.5 || ^8.0", |
| 294 | "psr/http-factory": "^1.0", |
| 295 | "psr/http-message": "^1.1 || ^2.0", |
| 296 | "ralouphie/getallheaders": "^3.0" |
| 297 | }, |
| 298 | "provide": { |
| 299 | "psr/http-factory-implementation": "1.0", |
| 300 | "psr/http-message-implementation": "1.0" |
| 301 | }, |
| 302 | "require-dev": { |
| 303 | "bamarni/composer-bin-plugin": "^1.8.2", |
| 304 | "http-interop/http-factory-tests": "0.9.0", |
| 305 | "phpunit/phpunit": "^8.5.44 || ^9.6.25" |
| 306 | }, |
| 307 | "suggest": { |
| 308 | "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" |
| 309 | }, |
| 310 | "type": "library", |
| 311 | "extra": { |
| 312 | "bamarni-bin": { |
| 313 | "bin-links": true, |
| 314 | "forward-command": false |
| 315 | } |
| 316 | }, |
| 317 | "autoload": { |
| 318 | "psr-4": { |
| 319 | "GuzzleHttp\\Psr7\\": "src/" |
| 320 | } |
| 321 | }, |
| 322 | "notification-url": "https://packagist.org/downloads/", |
| 323 | "license": [ |
| 324 | "MIT" |
| 325 | ], |
| 326 | "authors": [ |
| 327 | { |
| 328 | "name": "Graham Campbell", |
| 329 | "email": "hello@gjcampbell.co.uk", |
| 330 | "homepage": "https://github.com/GrahamCampbell" |
| 331 | }, |
| 332 | { |
| 333 | "name": "Michael Dowling", |
| 334 | "email": "mtdowling@gmail.com", |
| 335 | "homepage": "https://github.com/mtdowling" |
| 336 | }, |
| 337 | { |
| 338 | "name": "George Mponos", |
| 339 | "email": "gmponos@gmail.com", |
| 340 | "homepage": "https://github.com/gmponos" |
| 341 | }, |
| 342 | { |
| 343 | "name": "Tobias Nyholm", |
| 344 | "email": "tobias.nyholm@gmail.com", |
| 345 | "homepage": "https://github.com/Nyholm" |
| 346 | }, |
| 347 | { |
| 348 | "name": "Márk Sági-Kazár", |
| 349 | "email": "mark.sagikazar@gmail.com", |
| 350 | "homepage": "https://github.com/sagikazarmark" |
| 351 | }, |
| 352 | { |
| 353 | "name": "Tobias Schultze", |
| 354 | "email": "webmaster@tubo-world.de", |
| 355 | "homepage": "https://github.com/Tobion" |
| 356 | }, |
| 357 | { |
| 358 | "name": "Márk Sági-Kazár", |
| 359 | "email": "mark.sagikazar@gmail.com", |
| 360 | "homepage": "https://sagikazarmark.hu" |
| 361 | } |
| 362 | ], |
| 363 | "description": "PSR-7 message implementation that also provides common utility methods", |
| 364 | "keywords": [ |
| 365 | "http", |
| 366 | "message", |
| 367 | "psr-7", |
| 368 | "request", |
| 369 | "response", |
| 370 | "stream", |
| 371 | "uri", |
| 372 | "url" |
| 373 | ], |
| 374 | "support": { |
| 375 | "issues": "https://github.com/guzzle/psr7/issues", |
| 376 | "source": "https://github.com/guzzle/psr7/tree/2.8.0" |
| 377 | }, |
| 378 | "funding": [ |
| 379 | { |
| 380 | "url": "https://github.com/GrahamCampbell", |
| 381 | "type": "github" |
| 382 | }, |
| 383 | { |
| 384 | "url": "https://github.com/Nyholm", |
| 385 | "type": "github" |
| 386 | }, |
| 387 | { |
| 388 | "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", |
| 389 | "type": "tidelift" |
| 390 | } |
| 391 | ], |
| 392 | "time": "2025-08-23T21:21:41+00:00" |
| 393 | }, |
| 394 | { |
| 395 | "name": "minishlink/web-push", |
| 396 | "version": "v10.0.1", |
| 397 | "source": { |
| 398 | "type": "git", |
| 399 | "url": "https://github.com/web-push-libs/web-push-php.git", |
| 400 | "reference": "08463189d3501cbd78a8625c87ab6680a7397aad" |
| 401 | }, |
| 402 | "dist": { |
| 403 | "type": "zip", |
| 404 | "url": "https://api.github.com/repos/web-push-libs/web-push-php/zipball/08463189d3501cbd78a8625c87ab6680a7397aad", |
| 405 | "reference": "08463189d3501cbd78a8625c87ab6680a7397aad", |
| 406 | "shasum": "" |
| 407 | }, |
| 408 | "require": { |
| 409 | "ext-curl": "*", |
| 410 | "ext-json": "*", |
| 411 | "ext-mbstring": "*", |
| 412 | "ext-openssl": "*", |
| 413 | "guzzlehttp/guzzle": "^7.9.2", |
| 414 | "php": ">=8.2", |
| 415 | "spomky-labs/base64url": "^2.0.4", |
| 416 | "web-token/jwt-library": "^3.4.9|^4.0.6" |
| 417 | }, |
| 418 | "require-dev": { |
| 419 | "friendsofphp/php-cs-fixer": "^v3.91.3", |
| 420 | "phpstan/phpstan": "^2.1.33", |
| 421 | "phpstan/phpstan-strict-rules": "^2.0", |
| 422 | "phpunit/phpunit": "^11.5.46|^12.5.2", |
| 423 | "symfony/polyfill-iconv": "^1.33" |
| 424 | }, |
| 425 | "suggest": { |
| 426 | "ext-bcmath": "Optional for performance.", |
| 427 | "ext-gmp": "Optional for performance." |
| 428 | }, |
| 429 | "type": "library", |
| 430 | "autoload": { |
| 431 | "psr-4": { |
| 432 | "Minishlink\\WebPush\\": "src" |
| 433 | } |
| 434 | }, |
| 435 | "notification-url": "https://packagist.org/downloads/", |
| 436 | "license": [ |
| 437 | "MIT" |
| 438 | ], |
| 439 | "authors": [ |
| 440 | { |
| 441 | "name": "Louis Lagrange", |
| 442 | "email": "lagrange.louis@gmail.com", |
| 443 | "homepage": "https://github.com/Minishlink" |
| 444 | } |
| 445 | ], |
| 446 | "description": "Web Push library for PHP", |
| 447 | "homepage": "https://github.com/web-push-libs/web-push-php", |
| 448 | "keywords": [ |
| 449 | "Push API", |
| 450 | "WebPush", |
| 451 | "notifications", |
| 452 | "push", |
| 453 | "web" |
| 454 | ], |
| 455 | "support": { |
| 456 | "issues": "https://github.com/web-push-libs/web-push-php/issues", |
| 457 | "source": "https://github.com/web-push-libs/web-push-php/tree/v10.0.1" |
| 458 | }, |
| 459 | "time": "2025-12-15T10:04:28+00:00" |
| 460 | }, |
| 461 | { |
| 462 | "name": "psr/clock", |
| 463 | "version": "1.0.0", |
| 464 | "source": { |
| 465 | "type": "git", |
| 466 | "url": "https://github.com/php-fig/clock.git", |
| 467 | "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" |
| 468 | }, |
| 469 | "dist": { |
| 470 | "type": "zip", |
| 471 | "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", |
| 472 | "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", |
| 473 | "shasum": "" |
| 474 | }, |
| 475 | "require": { |
| 476 | "php": "^7.0 || ^8.0" |
| 477 | }, |
| 478 | "type": "library", |
| 479 | "autoload": { |
| 480 | "psr-4": { |
| 481 | "Psr\\Clock\\": "src/" |
| 482 | } |
| 483 | }, |
| 484 | "notification-url": "https://packagist.org/downloads/", |
| 485 | "license": [ |
| 486 | "MIT" |
| 487 | ], |
| 488 | "authors": [ |
| 489 | { |
| 490 | "name": "PHP-FIG", |
| 491 | "homepage": "https://www.php-fig.org/" |
| 492 | } |
| 493 | ], |
| 494 | "description": "Common interface for reading the clock.", |
| 495 | "homepage": "https://github.com/php-fig/clock", |
| 496 | "keywords": [ |
| 497 | "clock", |
| 498 | "now", |
| 499 | "psr", |
| 500 | "psr-20", |
| 501 | "time" |
| 502 | ], |
| 503 | "support": { |
| 504 | "issues": "https://github.com/php-fig/clock/issues", |
| 505 | "source": "https://github.com/php-fig/clock/tree/1.0.0" |
| 506 | }, |
| 507 | "time": "2022-11-25T14:36:26+00:00" |
| 508 | }, |
| 509 | { |
| 510 | "name": "psr/http-client", |
| 511 | "version": "1.0.3", |
| 512 | "source": { |
| 513 | "type": "git", |
| 514 | "url": "https://github.com/php-fig/http-client.git", |
| 515 | "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" |
| 516 | }, |
| 517 | "dist": { |
| 518 | "type": "zip", |
| 519 | "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", |
| 520 | "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", |
| 521 | "shasum": "" |
| 522 | }, |
| 523 | "require": { |
| 524 | "php": "^7.0 || ^8.0", |
| 525 | "psr/http-message": "^1.0 || ^2.0" |
| 526 | }, |
| 527 | "type": "library", |
| 528 | "extra": { |
| 529 | "branch-alias": { |
| 530 | "dev-master": "1.0.x-dev" |
| 531 | } |
| 532 | }, |
| 533 | "autoload": { |
| 534 | "psr-4": { |
| 535 | "Psr\\Http\\Client\\": "src/" |
| 536 | } |
| 537 | }, |
| 538 | "notification-url": "https://packagist.org/downloads/", |
| 539 | "license": [ |
| 540 | "MIT" |
| 541 | ], |
| 542 | "authors": [ |
| 543 | { |
| 544 | "name": "PHP-FIG", |
| 545 | "homepage": "https://www.php-fig.org/" |
| 546 | } |
| 547 | ], |
| 548 | "description": "Common interface for HTTP clients", |
| 549 | "homepage": "https://github.com/php-fig/http-client", |
| 550 | "keywords": [ |
| 551 | "http", |
| 552 | "http-client", |
| 553 | "psr", |
| 554 | "psr-18" |
| 555 | ], |
| 556 | "support": { |
| 557 | "source": "https://github.com/php-fig/http-client" |
| 558 | }, |
| 559 | "time": "2023-09-23T14:17:50+00:00" |
| 560 | }, |
| 561 | { |
| 562 | "name": "psr/http-factory", |
| 563 | "version": "1.1.0", |
| 564 | "source": { |
| 565 | "type": "git", |
| 566 | "url": "https://github.com/php-fig/http-factory.git", |
| 567 | "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" |
| 568 | }, |
| 569 | "dist": { |
| 570 | "type": "zip", |
| 571 | "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", |
| 572 | "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", |
| 573 | "shasum": "" |
| 574 | }, |
| 575 | "require": { |
| 576 | "php": ">=7.1", |
| 577 | "psr/http-message": "^1.0 || ^2.0" |
| 578 | }, |
| 579 | "type": "library", |
| 580 | "extra": { |
| 581 | "branch-alias": { |
| 582 | "dev-master": "1.0.x-dev" |
| 583 | } |
| 584 | }, |
| 585 | "autoload": { |
| 586 | "psr-4": { |
| 587 | "Psr\\Http\\Message\\": "src/" |
| 588 | } |
| 589 | }, |
| 590 | "notification-url": "https://packagist.org/downloads/", |
| 591 | "license": [ |
| 592 | "MIT" |
| 593 | ], |
| 594 | "authors": [ |
| 595 | { |
| 596 | "name": "PHP-FIG", |
| 597 | "homepage": "https://www.php-fig.org/" |
| 598 | } |
| 599 | ], |
| 600 | "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", |
| 601 | "keywords": [ |
| 602 | "factory", |
| 603 | "http", |
| 604 | "message", |
| 605 | "psr", |
| 606 | "psr-17", |
| 607 | "psr-7", |
| 608 | "request", |
| 609 | "response" |
| 610 | ], |
| 611 | "support": { |
| 612 | "source": "https://github.com/php-fig/http-factory" |
| 613 | }, |
| 614 | "time": "2024-04-15T12:06:14+00:00" |
| 615 | }, |
| 616 | { |
| 617 | "name": "psr/http-message", |
| 618 | "version": "2.0", |
| 619 | "source": { |
| 620 | "type": "git", |
| 621 | "url": "https://github.com/php-fig/http-message.git", |
| 622 | "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" |
| 623 | }, |
| 624 | "dist": { |
| 625 | "type": "zip", |
| 626 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", |
| 627 | "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", |
| 628 | "shasum": "" |
| 629 | }, |
| 630 | "require": { |
| 631 | "php": "^7.2 || ^8.0" |
| 632 | }, |
| 633 | "type": "library", |
| 634 | "extra": { |
| 635 | "branch-alias": { |
| 636 | "dev-master": "2.0.x-dev" |
| 637 | } |
| 638 | }, |
| 639 | "autoload": { |
| 640 | "psr-4": { |
| 641 | "Psr\\Http\\Message\\": "src/" |
| 642 | } |
| 643 | }, |
| 644 | "notification-url": "https://packagist.org/downloads/", |
| 645 | "license": [ |
| 646 | "MIT" |
| 647 | ], |
| 648 | "authors": [ |
| 649 | { |
| 650 | "name": "PHP-FIG", |
| 651 | "homepage": "https://www.php-fig.org/" |
| 652 | } |
| 653 | ], |
| 654 | "description": "Common interface for HTTP messages", |
| 655 | "homepage": "https://github.com/php-fig/http-message", |
| 656 | "keywords": [ |
| 657 | "http", |
| 658 | "http-message", |
| 659 | "psr", |
| 660 | "psr-7", |
| 661 | "request", |
| 662 | "response" |
| 663 | ], |
| 664 | "support": { |
| 665 | "source": "https://github.com/php-fig/http-message/tree/2.0" |
| 666 | }, |
| 667 | "time": "2023-04-04T09:54:51+00:00" |
| 668 | }, |
| 669 | { |
| 670 | "name": "ralouphie/getallheaders", |
| 671 | "version": "3.0.3", |
| 672 | "source": { |
| 673 | "type": "git", |
| 674 | "url": "https://github.com/ralouphie/getallheaders.git", |
| 675 | "reference": "120b605dfeb996808c31b6477290a714d356e822" |
| 676 | }, |
| 677 | "dist": { |
| 678 | "type": "zip", |
| 679 | "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", |
| 680 | "reference": "120b605dfeb996808c31b6477290a714d356e822", |
| 681 | "shasum": "" |
| 682 | }, |
| 683 | "require": { |
| 684 | "php": ">=5.6" |
| 685 | }, |
| 686 | "require-dev": { |
| 687 | "php-coveralls/php-coveralls": "^2.1", |
| 688 | "phpunit/phpunit": "^5 || ^6.5" |
| 689 | }, |
| 690 | "type": "library", |
| 691 | "autoload": { |
| 692 | "files": [ |
| 693 | "src/getallheaders.php" |
| 694 | ] |
| 695 | }, |
| 696 | "notification-url": "https://packagist.org/downloads/", |
| 697 | "license": [ |
| 698 | "MIT" |
| 699 | ], |
| 700 | "authors": [ |
| 701 | { |
| 702 | "name": "Ralph Khattar", |
| 703 | "email": "ralph.khattar@gmail.com" |
| 704 | } |
| 705 | ], |
| 706 | "description": "A polyfill for getallheaders.", |
| 707 | "support": { |
| 708 | "issues": "https://github.com/ralouphie/getallheaders/issues", |
| 709 | "source": "https://github.com/ralouphie/getallheaders/tree/develop" |
| 710 | }, |
| 711 | "time": "2019-03-08T08:55:37+00:00" |
| 712 | }, |
| 713 | { |
| 714 | "name": "spomky-labs/base64url", |
| 715 | "version": "v2.0.4", |
| 716 | "source": { |
| 717 | "type": "git", |
| 718 | "url": "https://github.com/Spomky-Labs/base64url.git", |
| 719 | "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d" |
| 720 | }, |
| 721 | "dist": { |
| 722 | "type": "zip", |
| 723 | "url": "https://api.github.com/repos/Spomky-Labs/base64url/zipball/7752ce931ec285da4ed1f4c5aa27e45e097be61d", |
| 724 | "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d", |
| 725 | "shasum": "" |
| 726 | }, |
| 727 | "require": { |
| 728 | "php": ">=7.1" |
| 729 | }, |
| 730 | "require-dev": { |
| 731 | "phpstan/extension-installer": "^1.0", |
| 732 | "phpstan/phpstan": "^0.11|^0.12", |
| 733 | "phpstan/phpstan-beberlei-assert": "^0.11|^0.12", |
| 734 | "phpstan/phpstan-deprecation-rules": "^0.11|^0.12", |
| 735 | "phpstan/phpstan-phpunit": "^0.11|^0.12", |
| 736 | "phpstan/phpstan-strict-rules": "^0.11|^0.12" |
| 737 | }, |
| 738 | "type": "library", |
| 739 | "autoload": { |
| 740 | "psr-4": { |
| 741 | "Base64Url\\": "src/" |
| 742 | } |
| 743 | }, |
| 744 | "notification-url": "https://packagist.org/downloads/", |
| 745 | "license": [ |
| 746 | "MIT" |
| 747 | ], |
| 748 | "authors": [ |
| 749 | { |
| 750 | "name": "Florent Morselli", |
| 751 | "homepage": "https://github.com/Spomky-Labs/base64url/contributors" |
| 752 | } |
| 753 | ], |
| 754 | "description": "Base 64 URL Safe Encoding/Decoding PHP Library", |
| 755 | "homepage": "https://github.com/Spomky-Labs/base64url", |
| 756 | "keywords": [ |
| 757 | "base64", |
| 758 | "rfc4648", |
| 759 | "safe", |
| 760 | "url" |
| 761 | ], |
| 762 | "support": { |
| 763 | "issues": "https://github.com/Spomky-Labs/base64url/issues", |
| 764 | "source": "https://github.com/Spomky-Labs/base64url/tree/v2.0.4" |
| 765 | }, |
| 766 | "funding": [ |
| 767 | { |
| 768 | "url": "https://github.com/Spomky", |
| 769 | "type": "github" |
| 770 | }, |
| 771 | { |
| 772 | "url": "https://www.patreon.com/FlorentMorselli", |
| 773 | "type": "patreon" |
| 774 | } |
| 775 | ], |
| 776 | "time": "2020-11-03T09:10:25+00:00" |
| 777 | }, |
| 778 | { |
| 779 | "name": "spomky-labs/pki-framework", |
| 780 | "version": "1.4.1", |
| 781 | "source": { |
| 782 | "type": "git", |
| 783 | "url": "https://github.com/Spomky-Labs/pki-framework.git", |
| 784 | "reference": "f0e9a548df4e3942886adc9b7830581a46334631" |
| 785 | }, |
| 786 | "dist": { |
| 787 | "type": "zip", |
| 788 | "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/f0e9a548df4e3942886adc9b7830581a46334631", |
| 789 | "reference": "f0e9a548df4e3942886adc9b7830581a46334631", |
| 790 | "shasum": "" |
| 791 | }, |
| 792 | "require": { |
| 793 | "brick/math": "^0.10|^0.11|^0.12|^0.13|^0.14", |
| 794 | "ext-mbstring": "*", |
| 795 | "php": ">=8.1" |
| 796 | }, |
| 797 | "require-dev": { |
| 798 | "ekino/phpstan-banned-code": "^1.0|^2.0|^3.0", |
| 799 | "ext-gmp": "*", |
| 800 | "ext-openssl": "*", |
| 801 | "infection/infection": "^0.28|^0.29|^0.31", |
| 802 | "php-parallel-lint/php-parallel-lint": "^1.3", |
| 803 | "phpstan/extension-installer": "^1.3|^2.0", |
| 804 | "phpstan/phpstan": "^1.8|^2.0", |
| 805 | "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", |
| 806 | "phpstan/phpstan-phpunit": "^1.1|^2.0", |
| 807 | "phpstan/phpstan-strict-rules": "^1.3|^2.0", |
| 808 | "phpunit/phpunit": "^10.1|^11.0|^12.0", |
| 809 | "rector/rector": "^1.0|^2.0", |
| 810 | "roave/security-advisories": "dev-latest", |
| 811 | "symfony/string": "^6.4|^7.0|^8.0", |
| 812 | "symfony/var-dumper": "^6.4|^7.0|^8.0", |
| 813 | "symplify/easy-coding-standard": "^12.0" |
| 814 | }, |
| 815 | "suggest": { |
| 816 | "ext-bcmath": "For better performance (or GMP)", |
| 817 | "ext-gmp": "For better performance (or BCMath)", |
| 818 | "ext-openssl": "For OpenSSL based cyphering" |
| 819 | }, |
| 820 | "type": "library", |
| 821 | "autoload": { |
| 822 | "psr-4": { |
| 823 | "SpomkyLabs\\Pki\\": "src/" |
| 824 | } |
| 825 | }, |
| 826 | "notification-url": "https://packagist.org/downloads/", |
| 827 | "license": [ |
| 828 | "MIT" |
| 829 | ], |
| 830 | "authors": [ |
| 831 | { |
| 832 | "name": "Joni Eskelinen", |
| 833 | "email": "jonieske@gmail.com", |
| 834 | "role": "Original developer" |
| 835 | }, |
| 836 | { |
| 837 | "name": "Florent Morselli", |
| 838 | "email": "florent.morselli@spomky-labs.com", |
| 839 | "role": "Spomky-Labs PKI Framework developer" |
| 840 | } |
| 841 | ], |
| 842 | "description": "A PHP framework for managing Public Key Infrastructures. It comprises X.509 public key certificates, attribute certificates, certification requests and certification path validation.", |
| 843 | "homepage": "https://github.com/spomky-labs/pki-framework", |
| 844 | "keywords": [ |
| 845 | "DER", |
| 846 | "Private Key", |
| 847 | "ac", |
| 848 | "algorithm identifier", |
| 849 | "asn.1", |
| 850 | "asn1", |
| 851 | "attribute certificate", |
| 852 | "certificate", |
| 853 | "certification request", |
| 854 | "cryptography", |
| 855 | "csr", |
| 856 | "decrypt", |
| 857 | "ec", |
| 858 | "encrypt", |
| 859 | "pem", |
| 860 | "pkcs", |
| 861 | "public key", |
| 862 | "rsa", |
| 863 | "sign", |
| 864 | "signature", |
| 865 | "verify", |
| 866 | "x.509", |
| 867 | "x.690", |
| 868 | "x509", |
| 869 | "x690" |
| 870 | ], |
| 871 | "support": { |
| 872 | "issues": "https://github.com/Spomky-Labs/pki-framework/issues", |
| 873 | "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.4.1" |
| 874 | }, |
| 875 | "funding": [ |
| 876 | { |
| 877 | "url": "https://github.com/Spomky", |
| 878 | "type": "github" |
| 879 | }, |
| 880 | { |
| 881 | "url": "https://www.patreon.com/FlorentMorselli", |
| 882 | "type": "patreon" |
| 883 | } |
| 884 | ], |
| 885 | "time": "2025-12-20T12:57:40+00:00" |
| 886 | }, |
| 887 | { |
| 888 | "name": "symfony/deprecation-contracts", |
| 889 | "version": "v3.6.0", |
| 890 | "source": { |
| 891 | "type": "git", |
| 892 | "url": "https://github.com/symfony/deprecation-contracts.git", |
| 893 | "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" |
| 894 | }, |
| 895 | "dist": { |
| 896 | "type": "zip", |
| 897 | "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", |
| 898 | "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", |
| 899 | "shasum": "" |
| 900 | }, |
| 901 | "require": { |
| 902 | "php": ">=8.1" |
| 903 | }, |
| 904 | "type": "library", |
| 905 | "extra": { |
| 906 | "thanks": { |
| 907 | "url": "https://github.com/symfony/contracts", |
| 908 | "name": "symfony/contracts" |
| 909 | }, |
| 910 | "branch-alias": { |
| 911 | "dev-main": "3.6-dev" |
| 912 | } |
| 913 | }, |
| 914 | "autoload": { |
| 915 | "files": [ |
| 916 | "function.php" |
| 917 | ] |
| 918 | }, |
| 919 | "notification-url": "https://packagist.org/downloads/", |
| 920 | "license": [ |
| 921 | "MIT" |
| 922 | ], |
| 923 | "authors": [ |
| 924 | { |
| 925 | "name": "Nicolas Grekas", |
| 926 | "email": "p@tchwork.com" |
| 927 | }, |
| 928 | { |
| 929 | "name": "Symfony Community", |
| 930 | "homepage": "https://symfony.com/contributors" |
| 931 | } |
| 932 | ], |
| 933 | "description": "A generic function and convention to trigger deprecation notices", |
| 934 | "homepage": "https://symfony.com", |
| 935 | "support": { |
| 936 | "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" |
| 937 | }, |
| 938 | "funding": [ |
| 939 | { |
| 940 | "url": "https://symfony.com/sponsor", |
| 941 | "type": "custom" |
| 942 | }, |
| 943 | { |
| 944 | "url": "https://github.com/fabpot", |
| 945 | "type": "github" |
| 946 | }, |
| 947 | { |
| 948 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", |
| 949 | "type": "tidelift" |
| 950 | } |
| 951 | ], |
| 952 | "time": "2024-09-25T14:21:43+00:00" |
| 953 | }, |
| 954 | { |
| 955 | "name": "web-token/jwt-library", |
| 956 | "version": "4.1.3", |
| 957 | "source": { |
| 958 | "type": "git", |
| 959 | "url": "https://github.com/web-token/jwt-library.git", |
| 960 | "reference": "690d4dd47b78f423cb90457f858e4106e1deb728" |
| 961 | }, |
| 962 | "dist": { |
| 963 | "type": "zip", |
| 964 | "url": "https://api.github.com/repos/web-token/jwt-library/zipball/690d4dd47b78f423cb90457f858e4106e1deb728", |
| 965 | "reference": "690d4dd47b78f423cb90457f858e4106e1deb728", |
| 966 | "shasum": "" |
| 967 | }, |
| 968 | "require": { |
| 969 | "brick/math": "^0.12|^0.13|^0.14", |
| 970 | "php": ">=8.2", |
| 971 | "psr/clock": "^1.0", |
| 972 | "spomky-labs/pki-framework": "^1.2.1" |
| 973 | }, |
| 974 | "conflict": { |
| 975 | "spomky-labs/jose": "*" |
| 976 | }, |
| 977 | "suggest": { |
| 978 | "ext-bcmath": "GMP or BCMath is highly recommended to improve the library performance", |
| 979 | "ext-gmp": "GMP or BCMath is highly recommended to improve the library performance", |
| 980 | "ext-openssl": "For key management (creation, optimization, etc.) and some algorithms (AES, RSA, ECDSA, etc.)", |
| 981 | "ext-sodium": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys", |
| 982 | "paragonie/sodium_compat": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys", |
| 983 | "spomky-labs/aes-key-wrap": "For all Key Wrapping algorithms (AxxxKW, AxxxGCMKW, PBES2-HSxxx+AyyyKW...)", |
| 984 | "symfony/console": "Needed to use console commands", |
| 985 | "symfony/http-client": "To enable JKU/X5U support." |
| 986 | }, |
| 987 | "type": "library", |
| 988 | "autoload": { |
| 989 | "psr-4": { |
| 990 | "Jose\\Component\\": "" |
| 991 | } |
| 992 | }, |
| 993 | "notification-url": "https://packagist.org/downloads/", |
| 994 | "license": [ |
| 995 | "MIT" |
| 996 | ], |
| 997 | "authors": [ |
| 998 | { |
| 999 | "name": "Florent Morselli", |
| 1000 | "homepage": "https://github.com/Spomky" |
| 1001 | }, |
| 1002 | { |
| 1003 | "name": "All contributors", |
| 1004 | "homepage": "https://github.com/web-token/jwt-framework/contributors" |
| 1005 | } |
| 1006 | ], |
| 1007 | "description": "JWT library", |
| 1008 | "homepage": "https://github.com/web-token", |
| 1009 | "keywords": [ |
| 1010 | "JOSE", |
| 1011 | "JWE", |
| 1012 | "JWK", |
| 1013 | "JWKSet", |
| 1014 | "JWS", |
| 1015 | "Jot", |
| 1016 | "RFC7515", |
| 1017 | "RFC7516", |
| 1018 | "RFC7517", |
| 1019 | "RFC7518", |
| 1020 | "RFC7519", |
| 1021 | "RFC7520", |
| 1022 | "bundle", |
| 1023 | "jwa", |
| 1024 | "jwt", |
| 1025 | "symfony" |
| 1026 | ], |
| 1027 | "support": { |
| 1028 | "issues": "https://github.com/web-token/jwt-library/issues", |
| 1029 | "source": "https://github.com/web-token/jwt-library/tree/4.1.3" |
| 1030 | }, |
| 1031 | "funding": [ |
| 1032 | { |
| 1033 | "url": "https://github.com/Spomky", |
| 1034 | "type": "github" |
| 1035 | }, |
| 1036 | { |
| 1037 | "url": "https://www.patreon.com/FlorentMorselli", |
| 1038 | "type": "patreon" |
| 1039 | } |
| 1040 | ], |
| 1041 | "time": "2025-12-18T14:27:35+00:00" |
| 1042 | } |
| 1043 | ], |
| 1044 | "packages-dev": [], |
| 1045 | "aliases": [], |
| 1046 | "minimum-stability": "stable", |
| 1047 | "stability-flags": [], |
| 1048 | "prefer-stable": false, |
| 1049 | "prefer-lowest": false, |
| 1050 | "platform": [], |
| 1051 | "platform-dev": [], |
| 1052 | "plugin-api-version": "2.6.0" |
| 1053 | } |
| 1054 |
| 1 |
Este archivo ayuda a detectar errores en los archivos del proyecto.
Lo utiliza principalmente Visual Studio Code.
No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.
| 1 |
{
|
| 2 |
"compilerOptions": {
|
| 3 |
"checkJs": true, |
| 4 |
"strictNullChecks": true, |
| 5 |
"target": "ES6", |
| 6 |
"module": "Node16", |
| 7 |
"moduleResolution": "Node16", |
| 8 |
"lib": [
|
| 9 |
"ES2017",
|
| 10 |
"WebWorker",
|
| 11 |
"DOM"
|
| 12 |
] |
| 18 |
} |
En esta lección se muestró un ejemplo de notificaciones push.