[PR][PHP/Perl RegExp] wybrane znaczniki HTML

0
  1. Mój problem: chcę uzyskać coś, co byłoby w pewnym sensie połączeniem wyposażonej w rozumienie składni funkcji strip_tags() i funkcji htmlspecialchars(). Mianowicie przepuszczającą konkretne tagi, a resztę zamieniającą na encje. Dodatkowym wymogiem jest to, żeby uniknąć niezamkniętych tagów lub tagów z krzyżowym zamknięciem (coś w stylu przykładowo <b><i>tekst</b></i>) - innymi słowy generowany dokument ma być prawidłowo parsowany - dodajmy - w XHTML 1.1.

  2. Moje dotychczasowe próby: W sumie cały dzisiejszy dzień (odkąd wstałem, tj. ok 14 ;) ) poświęciłem tej funkcji, z przerwami na jedzenie i inne takie, oraz zajrzenie parę razy na 4p i forum.jabberpl.org, ale mniejsza o to. Dotychczas udało mi się sklecić coś takiego:

function htmlallowtags($string = '')
  {
  $string = htmlspecialchars($string, ENT_NOQUOTES);
  $string = str_replace('&gt;', '>', $string);
  $string = preg_replace('/&lt;br\s*\/>/', "\n", $string);
  $string = nl2br($string);
  $string = str_replace("\n", ' ', $string);

  $pattern = '/&lt;((a)|(q|blockquote|strong|ins|em|b|u|i))(?(2)( href="[^<>"]*")?|())?>((([^<])|(<br\s*\/>)|(<[^<>]*?>[^<]*?<[^<>]*?>))*?)&lt;\/\1>/';
  $replacement = '<$1$4>$6</$1>';
  $oldstring = '';
  while($oldstring!=$string) { $oldstring=$string; $string = preg_replace($pattern, $replacement, $string); }

  $string = str_replace(Array('<blockquote>', '</blockquote>'), Array('<blockquote><div>', '</div></blockquote>'), $string);
  
  $string = preg_replace(Array('/<(\/?)b>/', '/<(\/?)u>/', '/<(\/?)i>/'), Array('<$1strong>', '<$1ins>', '<$1em>'), $string);

  return $string;
  }
?>

Już wyjaśniam po kolei co funkcja ma za zadanie robić po kolei: Najpierw wszystko zamieniane jest przez funkcję htmlspecialchars. Efekt chyba wiadomy. Następnie zamieniam > na >, coby mieć uproszczone zadanie potem. Następnym krokiem jest zamiana wszystkich
na znak końca linii, zastosowanie nl2br i usunięcie znaków końca linii - wszystko w celu unormalizowania przykładowo
,
i
do jednego
, zamianę znaków końca linii na
i pozbycie się znaków końca linii, żeby regexp działał jak trzeba (na dmkhost "\n" nie jest znakiem podlegającym pod . w regexp Perla).

Następnym krokiem jest moje wyrażenie regularne.

Na koniec zamieniane są <u> na <ins></code>, <code><b> na <strong></code>, <code><i></code> na <code><em></code> i <code><blockquote> na <blockquote><div></code> - wszystko w celu uniknięcia problemów z faktem, że blockquote nie może zawierać czystego tekstu, tylko elementy blokowe, a nie istnieje w xhtml, zaś <code><b></code> i <code><i> zamieniam jakoby "przy okazji".

Co nie działa: Przy wystąpieniu przykładowo</li> </ol>
<blockquote><blockquote>cytat w cytacie</blockquote>cytat</blockquote>

wszystko się rozjeżdża, w związku z zabezpieczeniem przed krzyżowaniem tagów. Niestety nie mogę zamienić zachłanności przeszukiwania, bo wtedy przykładowo <b>tekst</b> tekst <b>tekst</b> będzie zamieniane na <b>tekst&lt;/b> tekst &lt;b>tekst</b>, gdyż najpierw zamienione zostaną pierwszy tag otwierający z ostatnim zamykającym, a reszta nie będzie się zgadzać.

Ma ktoś jakiś pomysł, jak ten problem możnaby rozwiązać?

P.S. Właściwie zaczynam dopiero z RegExp, pewnie to jest nieoptymalne i dałoby się zrobić prościej. Jak ktoś podpowie jak, będę wielce wdzięczny.

//Dodano:
Przepraszam za kiepskie formatowanie, ale użycie <u> i innych takich bez znacznika <code> powodowało, że były one interpretowane jako znaczniki formatowania.
P.S.2. Funkcja do zamykania tagów się tu rozjeżdża :P

</b></b></i></i></u></u></u>
0

Po mojemu zbyt wiele zbednych funkcji. Ja bym to zrobil na zasadzie jak dziala bbcode w phpbb. Czyli najpierw ponumerowal otwarte i zamkniete tagi wraz zmiejscami do jakiejs tablicy - wtedy mozna rozpoznac czy domniete sa dane tagi i czy sie nie krzyzuja. A co kropki w perlowkich wyrazeniach. Kropka moze uwzgledniac znak nowej linii jezeli dodasz opcje "s", przyklad:

/.{4,15}/s

p.s. zamykanie wyrazen w perl nie musisz robic tylko slashem, ale dowolnym znacznikiem, zeby nie bawic sie w /, czyli:

@</?b>@
0

Może ten wątek w czymś pomoże.
http://4programmers.net/Forum/viewtopic.php?id=75447
Z chęcią bym się wgłębił, ale nie mam kiedy ;)

0

Może to w czymś pomoże:
http://4programmers.net/Forum/278530#id278530

Z krzyżowym zamykaniem tagów jest ten problem:

..<div p=1><u><div p=2>BlaBlaBla</u></div>..

Tu występują jeden albo dwa błędy - brak zamknięcia jednego znacznika (przed < /u>), lub zamiana znaczników < /u> oraz < /div> oraz niezamknięty znacznik. Trudno teraz zdecydować, czy traktować to jako zmianę znaczników i zamknięcie < div p=2>, czy też zgubienie znacznika zamykającego dla < div p=2> i zamknięcie < u> oraz < div p=1>.

0

Hmm... Już się ucieszyłem, że ktoś odpowiedział, ale niestety nie całkiem o to mi chodziło :(. Problem polega na tym, że podane linki prowadzą do tematów o funkcjach zamykających tagi. Mi chodzi natomiast o pozostawienie tagów jak są (jeśli są otwarte, to niech będą), a tylko zamknięte prawidłowo tagi zamieniać z encji na zwykłe. Jak ktoś jeszcze ma jakiś pomysł (albo ja czegoś nie rozumiem w tych linkach), to chętnie przeczytam. Co do tego bbCode - jest to trochę skomplikowane, ale postaram się temu przyjrzeć i zobaczyć, czy mi coś pomoże tego typu rozwiązanie.

0

Popatrz na mój kod: zamyka tagi, ale tylko te zdefiniowane, pozostałe daje w postaci encji, zamknięcie tagów, które nie zostały otwarte też daje w postaci encji. - dodatkowo dba o prawidłowe zadnieżdżanie tagów.

0

Hmm... Ale nie o to chodzi:
Jak mam przykładowo (j.w.) dostępny tag <b> to chcę, by tekst <b></code> był zamieniany na <code>&lt;b> zaś <b>tekst</b> pozostawał tak jak był (bo znacznik jest).</b></b>

0

Już rozumiem o co Ci chodzi. Tylko pytanie - jaki wynik ma zwrócić to:

funkcja("<b><i>Tekst</b></i>");

?

0

hmm... W sumie niech zwraca co chce ;) Tzn. Albo <b>&lt;i>tekst</b>&lt;/i> albo &lt;b><i>tekst&lt;/b></i></code> albo <code>&lt;b>&lt;i>tekst&lt;/b>&lt;/i>
Głównym celem moim jest umożliwienie formatowania, a jednocześnie uniemożliwienie zrobienia czegoś, co jest niezgodne ze standardami i/lub niebezpieczne (w sensie ataków wstrzyknięcia kodu JS na stronę).

0

no to chyba umożliwi ale bardzo utrudni przeciętnemu userowi formatowanie, nie lepiej żeby zwracało

<b><i>tekst</i></b><i></i>

potem <i></i> można zamienić na nic i zostaje

<b><i>tekst</i></b>

znacznie prościej chyba wykonać - chodzi mi o to że jeśli jest polecenie zamknięcia tagu który nie był ostatnio otworzony to zamknąć wszystkie poprzednio utworzone wcześniej, zamknąć ten który ma być zamknięty a potem otworzyć na nowo te które user nie wprowadził żeby zamknąć a dla zgodności standardów zostały zamknięte

0

Dobra, dzięki pomocy Szczawika udało mi się to w końcu zrobić. Czy jest to optymalne? Zapewne nie, ale działa i to się liczy :). Jak ktoś zauważy jakiś błąd w optymalności, który da się prosto poprawić, to się nie obrażę za wskazówkę ;). Wygląda to tak:

function htmlallowtags($string = '')
  {
  $string = preg_replace('@<br\s*/>@s', "\n", $string);

  $stos1 = '';
  $stos2 = Array(0 => '');
  $stoslvl = 0;

  $pattern = '@^(.*?)<(((a)|(q|blockquote|strong|ins|em|b|u|i))(?(4)(\s*href="[^<>"]*")?|())())>(.*)$@s';
  while(preg_match($pattern, $string, $matches))
    {
    $stos2[$stoslvl] .= htmlspecialchars($matches[1], ENT_NOQUOTES);

//    print_r(Array('processinglevel' => 1, 'stos1' => $stos1, 'stos2' => $stos2, 'stoslvl' => $stoslvl, 'pattern' => $pattern, 'string' => $string, 'matches' => $matches));

    if(!empty($matches[3]))
      {
      $stoslvl++;
      if(empty($stos1)) $stos1 = $matches[3]; else $stos1 .= '|'.$matches[3];
      $stos2[$stoslvl] = str_replace('&amp;amp;', '&amp;', str_replace('&', '&amp;', $matches[2])).'>';
      }
    else
      {
      do
        {
//        print_r(Array('processinglevel' => 2, 'stos1' => $stos1, 'stos2' => $stos2, 'stoslvl' => $stoslvl, 'pattern' => $pattern, 'string' => $string, 'matches' => $matches));
        $stoslvl--;
        if(substr($stos1, -strlen($matches[8]))==$matches[8])
          {
          $stos1 = substr($stos1, 0, -strlen($matches[8])-1);
          $stos2[$stoslvl] .= '<'.$stos2[$stoslvl+1].'</'.$matches[8].'>';
          break;
          }
        else
          {
          $stos1 = substr($stos1, 0, -strrpos($stos1, '|')-1);
          $stos2[$stoslvl] .= '&lt;'.$stos2[$stoslvl+1];
          }
        } while(TRUE);
      }

    if(empty($stos1))
      $pattern = '@^(.*?)<(((a)|(q|blockquote|strong|ins|em|b|u|i))(?(4)(\s*href="[^<>"]*")?|())())>(.*)$@s';
    else
      $pattern = '@^(.*?)<(((a)|(q|blockquote|strong|ins|em|b|u|i))(?(4)(\s*href="[^<>"]*")?|())|/('.$stos1.'))>(.*)$@s';

    $string=$matches[9];
    }

  while($stoslvl > 0)
    {
//    print_r(Array('processinglevel' => 3, 'stos1' => $stos1, 'stos2' => $stos2, 'stoslvl' => $stoslvl, 'pattern' => $pattern, 'string' => $string, 'matches' => $matches));
    $stoslvl--;
    $stos2[$stoslvl] .= '&lt;'.$stos2[$stoslvl+1];
    }

  $string = $stos2[$stoslvl] . htmlspecialchars($string, ENT_NOQUOTES);

  $string = str_replace(Array('<blockquote>', '</blockquote>'), Array('<blockquote><div>', '</div></blockquote>'), $string);
  $string = preg_replace(Array('/<(\/?)b>/', '/<(\/?)u>/', '/<(\/?)i>/'), Array('<$1strong>', '<$1ins>', '<$1em>'), $string);
  $string = nl2br($string);

  return $string;
  }

Raz jeszcze wielkie dzięki dla Szczawika :)

P.S. Te zakomentowane linie służyły wyłącznie do debugowania, zostawiłem jakby ktoś chciał się pobawić, żeby wystarczyło odkomentować.
//Dopisane: Był błąd odnośnie
, trzeba było przenieść linijkę z nl2br na koniec, już poprawiłem.

1 użytkowników online, w tym zalogowanych: 0, gości: 1