[Pregunta]: ¿Como puedo hacer esto equivalente con javascript?

Iniciado por Leguim, 30 Junio 2020, 02:27 AM

0 Miembros y 2 Visitantes están viendo este tema.

Leguim

Tengo una función php que recibe un parámetro que es una cadena cualquiera, lo que va hacer es que toda esa cadena va a detectar urls planas y las va a poner en html... Hasta acá todo va perfecto, el problema está al tratar de pasarla a js... No me llevo muy bien con las expresiones regulares y ese tipo de cosas... estuve viendo otras funciones pero son muy diferentes y terminan teniendo algún error...

Código (php) [Seleccionar]

function Convert_Content($string)
{
    $result = preg_replace('/((http|https|www)[^\s]+)/', '<a target="_blank" href="$1">$0</a>', $string);
    $result = preg_replace('/href=\"www/', 'href="http://www', $result);

    return $result;
}

[u]nsigned

Podes usar la libreria Linkifyque procesa tanto texto plano como html.

No hay atajo ante la duda, el misterio se hace aquí...
Se hace carne en cada uno, el misterio es existir!

Leguim

Muchas gracias! ya había escuchado de linkify pero por lo general no me gusta usar librerías de terceros, no digo que no lo haga pero si puedo evitarlo mucho mejor.

#!drvy

#3
Generalmente no tiene nada de malo utilizar librerías de terceros. Te ahorras tiempo, dolores de cabeza, y casos en los que no has pensado. Por ejemplo, enla funcion PHP que muestras, que pasa si le paso un string como este:

<p>http://google.com</p>

Te va a devolver esto:

Código (html5) [Seleccionar]
<p><a target="_blank" href="http://google.com</p>">http://google.com</p></a>

Lo cual obviamente esta rotisimo... E incluso no tenemos que ir tan lejos, basta con que el texto incluya comillas:

Puedes leer más sobre tortugas en "https://es.wikipedia.org/wiki/Testudines"

Código (html5) [Seleccionar]
Puedes leer más sobre tortugas en "<a target="_blank" href="https://es.wikipedia.org/wiki/Testudines"">https://es.wikipedia.org/wiki/Testudines"</a>

Ahí rompes todo el HTML con una simple camilla doble. Ya que usas ReGeX, te puedes asegurar de capturar solo lo que consideraríamos como una URL valida.

https://regexr.com/3e6m0

Eso sacado de StackOverflow con más de 600 votos.. Y aun así, no es válido porque no aceptaría dominios perfectamente válidos como:

https://hello.amsterdam/
https://barça.cat
http://ñoño.com


Porque limita el TLD a un máximo de 6 caracteres y solo espera a-z sin caracteres UTF-8. Tampoco acepta todo mayúsculas...

((http|https)\:\/\/)?[\w0-9\-\.]{1,255}\.[\w]{2,63}(\/[\w_\.\/\#]*)?

Este más o menos cumple con su cometido... Aunque si no lleva http/https delate, habría que ponerlo y eso implica comprobar antes de hacer el replace. En PHP seria algo así:

Código (php) [Seleccionar]
function makeLinks(string $string): string
{
   $regex = '/((http|https)\:\/\/)?[\w0-9\-\.]{1,255}\.[\w]{2,63}(\/[\w_\.\/\#]*)?/iu';
   $links = preg_match_all($regex, $string, $output);

   foreach ($output[0] as $link) {
       $url = strtolower($link);

       // Comprobar si empieza por http/https
       if (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0) {
           $url = 'http://' . $url;
       }

       // Creamos un enlace valido manteniendo el formato original.
       $anchor = sprintf('<a href="%s" target="_blank">%s</a>', $url, $link);
       $string = str_replace($link, $anchor, $string);
   }

   return $string;
}



Donde un texto como este:

Puedes leer más sobre tortugas en "https://es.wikipedia.org/wiki/Testudines" y para todo lo demás google.com

Se convierte en esto:

Código (html5) [Seleccionar]
Puedes leer más sobre tortugas en "<a href="https://es.wikipedia.org/wiki/testudines" target="_blank">https://es.wikipedia.org/wiki/Testudines</a>" y para todo lo demás <a href="http://google.com" target="_blank">google.com</a>


Ahora, en javascript hay un problema enorme, hasta ES6 javascript no tenía soporte para unicode en regex. Ende, no podemos utilizar el importantísimo flag /u (unicode) y por lo tanto, no podemos targetear dominios como ñoño.com sin añadir parafernalias como \0-\t\x0B\f\x0E-\u2027\u202A-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF]) . Hay 3 opciones, o nos saltamos los dominios unicode... no muy recomendable, o solo soportamos navegadores con ES6 (tampoco muy recomendable) o toca transpilar ( https://github.com/mathiasbynens/regexpu )  y con suerte sacar algo funcional...

Yo este paso me lo salto y voy a quitarme unicode de por medio, más que nada para ahorrar tiempo:

Código (javascript) [Seleccionar]
function makeLinks(string) {
   var regex = /((http|https)\:\/\/)?[\w0-9\-\.]{1,255}\.[\w]{2,63}(\/[\w_\.\/\#]*)?/ig;
   var output = string.match(regex);

   if (!output || output.length < 1) {
       return string;
   }

   for (var i = 0, t = output.length; i < t; ++i) {
       var link = output[i];
       var url  = link.toLowerCase();

       if (!/^https?:\/\//.test(url)) {
           url = 'http://' + url;
       }

       var anchor = '<a href="' + url + '" target="_blank">' + link + '</a>';
       string = string.replace(link, anchor);
   }

   return string;
}


Con ES6 quedaría un poco más bonito:

Código (javascript) [Seleccionar]
function makeLinks(string) {
   const regex = /((http|https)\:\/\/)?[\w0-9\-\.]{1,255}\.[\w]{2,63}(\/[\w_\.\/\#]*)?/ig;
   const output = string.match(regex);

   if (!output || output.length < 1) {
       return string;
   }

   output.forEach((link) => {
       let url = link.toLowerCase();
           url = (!/^https?:\/\//.test(url) ? `http://${url}` : url);

       string = string.replace(link, `<a href="${url}" target="_blank">${link}</a>`);
   });

   return string;
}



Código (javascript) [Seleccionar]
console.log(makeLinks('Puedes leer más sobre tortugas en "https://es.wikipedia.org/wiki/Testudines" y para todo lo demás google.com'));


Y todo esto, te lo puedes ahorrar con:

Código (javascript) [Seleccionar]
<script src="linkify.min.js"></script>
<script src="linkify-html.min.js"></script>
<script>
   console.log(linkifyHtml('Puedes leer más sobre tortugas en "https://es.wikipedia.org/wiki/Testudines" y para todo lo demás google.com', {
       className: '',
       defaultProtocol: 'http'
   }));
</script>


... pero no quieres usar librerías de terceros xD


Saludos

Leguim

Cita de: #!drvy en  1 Julio 2020, 12:05 PM
Generalmente no tiene nada de malo utilizar librerías de terceros. Te ahorras tiempo, dolores de cabeza, y casos en los que no has pensado. Por ejemplo, enla funcion PHP que muestras, que pasa si le paso un string como este:

<p>http://google.com</p>

Te va a devolver esto:

Código (html5) [Seleccionar]
<p><a target="_blank" href="http://google.com</p>">http://google.com</p></a>

Lo cual obviamente esta rotisimo... E incluso no tenemos que ir tan lejos, basta con que el texto incluya comillas:

Puedes leer más sobre tortugas en "https://es.wikipedia.org/wiki/Testudines"

Código (html5) [Seleccionar]
Puedes leer más sobre tortugas en "<a target="_blank" href="https://es.wikipedia.org/wiki/Testudines"">https://es.wikipedia.org/wiki/Testudines"</a>

Ahí rompes todo el HTML con una simple camilla doble. Ya que usas ReGeX, te puedes asegurar de capturar solo lo que consideraríamos como una URL valida.

https://regexr.com/3e6m0

Eso sacado de StackOverflow con más de 600 votos.. Y aun así, no es válido porque no aceptaría dominios perfectamente válidos como:

https://hello.amsterdam/
https://barça.cat
http://ñoño.com


Porque limita el TLD a un máximo de 6 caracteres y solo espera a-z sin caracteres UTF-8. Tampoco acepta todo mayúsculas...

((http|https)\:\/\/)?[\w0-9\-\.]{1,255}\.[\w]{2,63}(\/[\w_\.\/\#]*)?

Este más o menos cumple con su cometido... Aunque si no lleva http/https delate, habría que ponerlo y eso implica comprobar antes de hacer el replace. En PHP seria algo así:

Código (php) [Seleccionar]
function makeLinks(string $string): string
{
   $regex = '/((http|https)\:\/\/)?[\w0-9\-\.]{1,255}\.[\w]{2,63}(\/[\w_\.\/\#]*)?/iu';
   $links = preg_match_all($regex, $string, $output);

   foreach ($output[0] as $link) {
       $url = strtolower($link);

       // Comprobar si empieza por http/https
       if (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0) {
           $url = 'http://' . $url;
       }

       // Creamos un enlace valido manteniendo el formato original.
       $anchor = sprintf('<a href="%s" target="_blank">%s</a>', $url, $link);
       $string = str_replace($link, $anchor, $string);
   }

   return $string;
}



Donde un texto como este:

Puedes leer más sobre tortugas en "https://es.wikipedia.org/wiki/Testudines" y para todo lo demás google.com

Se convierte en esto:

Código (html5) [Seleccionar]
Puedes leer más sobre tortugas en "<a href="https://es.wikipedia.org/wiki/testudines" target="_blank">https://es.wikipedia.org/wiki/Testudines</a>" y para todo lo demás <a href="http://google.com" target="_blank">google.com</a>


Ahora, en javascript hay un problema enorme, hasta ES6 javascript no tenía soporte para unicode en regex. Ende, no podemos utilizar el importantísimo flag /u (unicode) y por lo tanto, no podemos targetear dominios como ñoño.com sin añadir parafernalias como \0-\t\x0B\f\x0E-\u2027\u202A-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF]) . Hay 3 opciones, o nos saltamos los dominios unicode... no muy recomendable, o solo soportamos navegadores con ES6 (tampoco muy recomendable) o toca transpilar ( https://github.com/mathiasbynens/regexpu )  y con suerte sacar algo funcional...

Yo este paso me lo salto y voy a quitarme unicode de por medio, más que nada para ahorrar tiempo:

Código (javascript) [Seleccionar]
function makeLinks(string) {
   var regex = /((http|https)\:\/\/)?[\w0-9\-\.]{1,255}\.[\w]{2,63}(\/[\w_\.\/\#]*)?/ig;
   var output = string.match(regex);

   if (!output || output.length < 1) {
       return string;
   }

   for (var i = 0, t = output.length; i < t; ++i) {
       var link = output[i];
       var url  = link.toLowerCase();

       if (!/^https?:\/\//.test(url)) {
           url = 'http://' + url;
       }

       var anchor = '<a href="' + url + '" target="_blank">' + link + '</a>';
       string = string.replace(link, anchor);
   }

   return string;
}


Con ES6 quedaría un poco más bonito:

Código (javascript) [Seleccionar]
function makeLinks(string) {
   const regex = /((http|https)\:\/\/)?[\w0-9\-\.]{1,255}\.[\w]{2,63}(\/[\w_\.\/\#]*)?/ig;
   const output = string.match(regex);

   if (!output || output.length < 1) {
       return string;
   }

   output.forEach((link) => {
       let url = link.toLowerCase();
           url = (!/^https?:\/\//.test(url) ? `http://${url}` : url);

       string = string.replace(link, `<a href="${url}" target="_blank">${link}</a>`);
   });

   return string;
}



Código (javascript) [Seleccionar]
console.log(makeLinks('Puedes leer más sobre tortugas en "https://es.wikipedia.org/wiki/Testudines" y para todo lo demás google.com'));


Y todo esto, te lo puedes ahorrar con:

Código (javascript) [Seleccionar]
<script src="linkify.min.js"></script>
<script src="linkify-html.min.js"></script>
<script>
   console.log(linkifyHtml('Puedes leer más sobre tortugas en "https://es.wikipedia.org/wiki/Testudines" y para todo lo demás google.com', {
       className: '',
       defaultProtocol: 'http'
   }));
</script>


... pero no quieres usar librerías de terceros xD


Saludos

jaja me encanta como me decis al final "pero no quieres usar librerias de terceros" jaja... no, el tema es que las liberias de terceros (no se si todas) te ofrecen más cosas de las que quizás en mi proyecto necesito... yo se que jquery es una libreria de terceros que ofrece más cosas por ahí que yo vaya a necesitar pero es diferente... veo que es muy engorroso esto de detectar una url plana y pasarla a html y no me va a quedar de otra que usar linkify xD pero no porque no use... uso si no hay más remedio.. GRACIAS! xD

WHK

#!drvy , intenta convertir esto con tu función en php:

CitarTest. http://google.com/ y http://google.com/ test.

CitarTest. <a href="<a href="http://google.com/" target="_blank">http://google.com/</a>" target="_blank"><a href="http://google.com/" target="_blank">http://google.com/</a></a> y <a href="<a href="http://google.com/" target="_blank">http://google.com/</a>" target="_blank"><a href="http://google.com/" target="_blank">http://google.com/</a></a> test.

Tu función es recursiva y no respeta las posiciones de cada match, reemplaza los enlaces según el valor de la expresión encontrada independiente si hay más de una o no.

Hay maneras mas eficientes como por ejemplo la de https://stackoverflow.com/questions/1960461/convert-plain-text-urls-into-html-hyperlinks-in-php

Código (php) [Seleccionar]
$url = '@(http(s)?)?(://)?(([a-zA-Z])([-\w]+\.)+([^\s\.]+[^\s]*)+[^,.\s])@';
$string = preg_replace($url, '<a href="http$2://$4" target="_blank" title="$0">$0</a>', $string);
echo $string;


Pero en mi caso para estar totalmente seguro y evitar el uso excesivo de memoria al intentar parsear textos muy grandes sobre el motor de expresión regular y evitar vulnerabilidades (porque el ejemplo que puse de stackoverlow tiene xss) lo que haría sería ir en busca de todos los espacios en blanco, ponerle coordenadas en bytes y luego procesar cada texto completo y validar si es un enlace, y si lo es entonces reemplazar, para ello utilizaría un buffer principal donde se vaya acumulando el resultado y eliminando el original, como una pila pero de bytes. No usaría explode() porque eso podría generar arrays demasiado grandes.

De todas maneras el xss se puede evitar si escapas todo el string antes de crear los enlaces con htmlspecialchars y ENT_QUOTES.

#!drvy

#6
Mmm tienes razón xD Supongo se podria arreglar añadiendo una lista de dominios ya reemplazados para evitar que salten dos veces.

La funcion esa esta bastante guay pero habría que añadirle el u flag para soporte unicode y aparte del XSS que mencionas, convierte urls como (http://google.com) lo cual rompe el enlace entero (curiosamente el foro tiene el mismo problema jaja).

Citary luego procesar cada texto completo y validar si es un enlace, y si lo es entonces reemplazar, para ello utilizaría un buffer principal donde se vaya acumulando el resultado y eliminando el original, como una pila pero de bytes

Pero tendrías el mismo problema. ¿Y si el enlace va entre paréntesis sin espaciado? ¿Y si en una parte es google.com, en otra parte es http://google.com y en otra es https://google.com/holamundo?

Las librerías que hacen estas cosas ya han pasado por estos problemas xD

Saludos

WHK

Citar¿Y si el enlace va entre paréntesis sin espaciado?

De eso se encarga el RFC y la expresión regular, una url no puede comenzar con parentesis pero si terminar en el, por eso en github, wikipedia, wordpress y demás cuando pones una url en parentesis suelen incluir el cierre de parentesis unicamente y eso es normal.

CitarSupongo se podria arreglar añadiendo una lista de dominios

Da igual si el dominio está repetido, terminarás pisando uno sobre el otro porque el string que vas a procesar siempre será el mismo y este ya contendrá encierre de etiquetas <a>.

CitarPero tendrías el mismo problema

No porque haces una pila y procesas palabra por palabra, nunca vuelves a procesar la misma palabra dos veces.

WHK

Mira, hice una función, aver que opinas:

Código (php) [Seleccionar]
<?php

function parse($string){
    
    
// Listado de etiquetas y propiedades permitidas
    
$availables = array(
        
// [etiqueta], [propiedades,]
        
'p'     => array('style''class''id'),
        
'a'     => array('href',  'style''class''id''title'),
        
'span'  => array('style''class'),
        
'b'     => array('style''class'),
        
'i'     => array('style''class'),
        
's'     => array('style''class'),
        
'table' => array('style''class''id''rows''cols''width'),
        
'thead' => array(),
        
'tbody' => array(),
        
'tr'    => array(),
        
'td'    => array(),
        
'div'   => array('style''class''id'),
        
'ul'    => array('style''class''id'),
        
'li'    => array('style''class')
    );

    try
    {
        
$dom = new DOMDocument();
        @
$dom->loadHTML('
            <html>
                <body>
                    <div>'
.$string.'</div>
                </body>
            </html>
        '
);
        
$string null// Libera la memoria
    
}
    catch(
Exception $e)
    {
        
// Contenido corrupto, etiquetas sin cerrar,
        // intento de XSS, etc.
        
return '';
    }

    
$id 0;
    foreach(
$dom->getElementsByTagNameNS('''*') as $tag)
    {
        
$id++;
        if(
$id 4)
        {
            continue;
        }
        
        if(!
in_array($tag->nodeNamearray_keys($availables)))
        {
            
// Etiqueta no permitida
            
$escaped $dom->createTextNode(htmlspecialchars(
                
$dom->saveHTML($tag),
                
ENT_QUOTES
            
));
            
$tag->parentNode->replaceChild($escaped$tag);
        }
        else
        {
            
// Etiqueta permitida

            // Busca todos los atributos de la etiqueta
            
foreach($tag->attributes as $attribute)
            {
                
// Propiedad no permitida?
                
if(!in_array(
                    
strtolower($attribute->name),
                    
$availables[strtolower($tag->nodeName)]
                ))
                {
                    
// Elimina el atributo no permitido
                    
$tag->removeAttribute($attribute->name);
                }
            }
        }
    }

    return 
trim(substr($dom->saveHTML(
        
$dom->getElementsByTagName('body')[0]->childNodes[1]
    ), 
5, -6));
}

echo 
parse('
    <ul>
        <li><a onclick="alert(0)" style="color:red;"
               href="http://google.com/">http://google.com/</a></li>
        <li>http://google.com/</li>
        <li><script>alert(0)//http://google.com/</script></li>
        <li><b>http://google.com/</b></li>
    </ul>
'
);



   <ul>
       <li><a style="color:red;" href="http://google.com/">http://google.com/</a></li>
       <li>http://google.com/</li>
       <li>&amp;lt;script&amp;gt;alert(0)//http://google.com/&amp;lt;/script&amp;gt;</li>
       <li><b>http://google.com/</b></li>
   </ul>


Si el usuario ingresa caracteres no permitidos o etiquetas falsas o etiquetas mal formadas o sin cerrar, etc arrojará una excepción y retornará un contendo en blanco, asi que desde el código final dices que si el input está en blanco entonces el usuario debe volver a ingresar el texto.

Claramente esto funciona para habilitar etiquetas HTML y sólo sirve si el navegador ya envía el contenido en este formato, quiere decir que el usuario tendrá que ingresar su mensaje en un editor thml reducido así como wordpress, por ejemplo usando ckeditor. Así ya no necesitarás transformar las urls a enlaces, simplemente haces que el sitio web desde el lado del input ya integre formato html, o si no, también sirve para sanitizar tus conversiones de enlace con expresión regular y habilitar únicamente a los enlaces, pero sería un tanto redundante, en ves de eso es mejor que el input ya venga con los enlaces construidos por el mismo usuario o creados por javascript.

Saludos.

#!drvy

Cita de: WHK en  3 Julio 2020, 06:31 AM
De eso se encarga el RFC y la expresión regular, una url no puede comenzar con parentesis pero si terminar en el, por eso en github, wikipedia, wordpress y demás cuando pones una url en parentesis suelen incluir el cierre de parentesis unicamente y eso es normal.

No es tan sencillo. Un paréntesis en el dominio o en tld no está permitido. Y aunque técnicamente está permitido en la query, pocas veces se ve sin encode. El mismo PHP por ejemplo te hace encoding sobre los paréntesis tanto con urlencode como con rawurlencode.

De todos modos, puse paréntesis como cualquier otro carácter, incluido mayor o menor que (<>)  o un backslash (\) que por otro lado no son caracteres válidos.


Y bueno, la función esta bastante bien, pero creo que el provisto era precisamente convertir texto plano xD

Saludos