#!/usr/bin/perl

# ---------------------------------------------------------------
# Prontus CMS
# http://www.prontus.cl/
# 2002 - 2012 Altavoz.net
#
# licensed under LGPL license.
# http://www.prontus.cl/license.html
# ---------------------------------------------------------------

package Artic;
use strict;
use utf8;
use Digest::MD5 qw(md5_hex);
use DBI;
use JSON;
use warnings;
no warnings 'uninitialized';

require lib_thumb;
require lib_tax;
require lib_tags;
require lib_urlf5;
require lib_friendly;

use glib_fildir_03;
use glib_dbi_03;
use glib_str_03;
use glib_hrfec_03;
use lib_helper;
use File::Copy qw(move copy);
use URI::Escape;
use HTML::Entities;

# ---------------------------------------------------------------
# errores orientados a que los pueda leer el usuario, sin info del ambiente.
# el texto completo del error se tira al STDERR
our $ERR = '';

# elementos privados de un articulo Prontus
our @PRIVATE_ELEMENTS = ('_txt_titular', '_url', '_slug', '_custom_slug', '_art_autoinc', '_users_id', '_fid', '_plt', '_fechap', '_horap', '_fechae', '_horae', '_fecha_ini', '_fecha_fin', '_soloportadas', '_publicarvistas', '_seccion1', '_tema1', '_subtema1', '_seccion2', '_tema2', '_subtema2', '_seccion3', '_tema3', '_subtema3', '_nom_seccion1', '_nom_tema1', '_nom_subtema1', '_nom_seccion2', '_nom_tema2', '_nom_subtema2', '_nom_seccion3', '_nom_tema3', '_nom_subtema3', '_autor', '_txt_bajada', '_alta', '_tax', '_tags', '_tagnames', '_multitag_seccion', '_multitag_tema', '_multitag_subtema', '_galeria_prontus_conf', '_galeria_flag_imagen_temporal', '_galeria_prontus_str', '_txt_galeria_description_total', '_txt_galeria_credito_total', '_s3', '_mam', '_s3_size', '_s3_media_size', '_s3_asset_size', '_total_size', '_disk_size', '_media_size', '_asset_size', '_imag_size', '_modtime', '_indexar', '_priorizar');

# ---------------------------------------------------------------
# Crea el objeto y retorna una referencia a el.
# Este metodo es el primer paso obligado tanto para crear o leer un articulo.
sub new {
    my $clase = shift;
    my $artic = {@_};
    bless $artic, $clase;

    # Atributos recibidos.
    $artic->{document_root}      ||= $ENV{'DOCUMENT_ROOT'};    # valor por omision
    $artic->{prontus_id}         ||= '';
    $artic->{public_server_name} ||= '';
    $artic->{cpan_server_name}   ||= '';
    $artic->{no_check_dirs}      ||= '';
    if (   (!&lib_prontus::valida_prontus($artic->{prontus_id}))
        || ($artic->{public_server_name} eq '')
        || ($artic->{cpan_server_name}   eq '')) {
        $Artic::ERR = "Artic::new con params no validos:\n" . "prontus_id[$artic->{prontus_id}] \n" . "public_server_name[$artic->{public_server_name}] \n" . "cpan_server_name[$artic->{cpan_server_name}] \n";
        return 0;
    }

    # Si no se indica el TS, asigna uno nuevo
    $artic->{ts} ||= '';
    if ($artic->{ts} ne '') {
        if ($artic->{ts} !~ /^\d{14}$/) {
            $Artic::ERR = "TS indicado a Artic::new no es valido. TS[$artic->{ts}]";
            return 0;
        }
    } else {
        $artic->{ts} = $artic->_get_new_ts();
    }
    $artic->{fechac} = &lib_prontus::get_dirfecha_by_ts($artic->{ts});

    # Establece y chequea dirs
    $artic->_set_dirs() || return 0;
    $artic->{fullpath_xml} = "$artic->{dst_xml}/$artic->{ts}" . '.xml';
    $artic->{fullpath_json} = "$artic->{dst_json}/$artic->{ts}" . '.json';

    # Si el hash no viene, se inicializa como vacio.
    # Los campos de este hash se accesan asi: my $val_campo = $artic->{campos}->{$nom_campo};
    if (!ref($artic->{campos})) {
        $artic->{campos} = {};    # es {} porque es una referencia, sino seria () # asume todos los nombres de campo en minusc.
    }

    # Inicializa hash de datos que cargaran desde el xml
    $artic->{xml_content} = {};
    # contendra el xml leido desde el disco
    $artic->{xml_data} = '';

    # Inicializa hash de datos de videos multimedia
    $artic->{media_metadata} = {};

    # Se llena en el metodo que genera la vista generar_vista_art()
    $artic->{post_proceso_lista} = ();

    # indica si el artículo esta publicado (tiene alta y cumple la fecha de publicacion)
    $artic->{published} = -1;

    # Indica si el articulo se está previsualizando o no.
    $artic->{is_preview} = 0;

    # Indica si el articulo se guardó, aunque no haya completado los demas procesos
    $artic->{saved} = 0;
    return $artic;
}

# ---------------------------------------------------------------
# Establece que el articulo es un preview.
sub set_preview_artic {
    my ($this, $ts) = @_;

    $this->{is_preview} = 1;
    # Fuerza el alta en el xml del preview.
    $this->{'campos'}->{'_alta'} = 1;

    # Setea los directorios para realizar el preview.
    $this->_set_dirs_preview();

    # Re-asigna el path xml
    $this->{fullpath_xml} = "$this->{dst_preview}/$this->{fechac}/xml/$this->{ts}" . '.xml';

    print STDERR "set_preview_artic: fullpath_xml[$this->{fullpath_xml}]\n";
}

# ---------------------------------------------------------------
# Retorna la ruta absoluta del artic si es que se cuenta con ella en el objeto.
# plt: puede venir de los campos recibidos del form, o bien, desde el xml
sub get_fullpath_artic {
    my ($this, $mv, $plt) = @_;
    my $fullpath_artic = '';

    if (($plt ne '') && ($this->{ts} ne '')) {
        my $ext           = &lib_prontus::get_file_extension($plt);
        my $fulldir_vista = $this->_get_fulldir_vista($mv);
        $fullpath_artic = "$fulldir_vista/$this->{ts}" . '.' . $ext;
    }
    return $fullpath_artic;
}

# ---------------------------------------------------------------
# Retorna el nombre del archivo del articulo, por ejemplo 20090522104356.html
# plt: puede venir de los campos recibidos del form, o bien, desde el xml
sub get_nom_artic {
    my ($this, $plt)  = @_;
    my $nom_artic;

    if (($plt ne '') && ($this->{ts} ne '')) {
        my $ext = &lib_prontus::get_file_extension($plt);
        $nom_artic = $this->{ts} . '.' . $ext;
    }
    return $nom_artic;

}

# ---------------------------------------------------------------
# No implementada. Se llama automaticamente al destruirse el objeto
sub DESTROY {
}

# --------------------------------------------------------------------
# Una vez escrito el articulo, ubica el nombre del script de post-proceso (con extension y
# con path absoluto completo) y lo ejecuta.
# Script de postproceso es optativo y puede ir en cualquier parte del articulo.
# Pueden ser varios.
sub exec_post_proceso_art {
    my ($this, $pathArticulo, $isNew) = @_;

    return if (!defined($this->{post_proceso_lista}));

    # Ejecuta en bground el proceso pasandole x param. el path completo al articulo.
    my $rutaScript = "$prontus_varglb::DIR_SERVER/$prontus_varglb::DIR_CGI_CPAN";

    my $newOrEdit = 'E';
    $newOrEdit = 'N' if ($isNew);

    my $pathnice = lib_prontus::get_path_nice();
    $pathnice = "$pathnice -n19 " if($pathnice);

    print STDERR "[PPROC]\n";
    # lista de postprocesos
    foreach my $pp (@{$this->{post_proceso_lista}}) {
        # Para que sea un script valido debe ubicarse en el mismo dir. de cgi del prontus o a
        # lo mas un nivel hacia arriba.
        if (($pp =~ /^\w/) || ($pp =~ /^\.\.(\/|\\)\w/)) {
            $pp =~ s/[#;&<>|~]+//g;
            my $cmd = "$pathnice $rutaScript/$pp $pathArticulo $this->{public_server_name} $newOrEdit >/dev/null 2>&1 &";
            print STDERR "[" . &glib_hrfec_03::get_dtime_pack4() . "]$cmd\n";
            system $cmd;
        }
    }
    print STDERR "[/PPROC]\n";
}

# ---------------------------------------------------------------
# Asigna timestamp unico para el articulo.
sub _get_new_ts {
    my $this = shift;
    my $ts     = &glib_hrfec_03::get_dtime_pack4();
    my $fechac = &lib_prontus::get_dirfecha_by_ts($ts);
    while (-f "$this->{document_root}/$this->{prontus_id}/site/artic/$fechac/xml/$ts" . '.xml') {
        $ts = &glib_hrfec_03::suma_segs($ts, 1);
    }
    return $ts;
}

# ---------------------------------------------------------------
# Genera la estructura de datos esencial para guardar en XML, y escribe a disco.
# En este XML queda solo lo esencial. Los datos derivados que se calculan u obtienen en base a los esenciales no se almacenan aqui.
sub generar_xml_artic {
    my ($this, $keep_time) =  @_;

    # Construccion xml articulo
    $this->{xml_data} = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<artic_data>\n<_private>\n";

    # Adecuar algunos datos que lo requieran para luego reemplazar todo de una en el XML
    if ($this->{campos}->{'_fechap'} eq '') {
        $this->{campos}->{'_fechap'} = &lib_prontus::asigna_fecha_default('_fechap');
    }

    # Cada vez que se guarda el articulo se actualiza la fecha de modificacion en formato epoch.
    $this->{campos}->{'_modtime'} = time if (!$keep_time);
    # si campo indexar no viene se asigna valor 1
    $this->{campos}->{'_indexar'} = 0 if (!defined($this->{campos}->{'_indexar'}));

    $this->{campos}->{'_s3'} = 0 if (!defined($this->{campos}->{'_s3'}));
    $this->{campos}->{'_s3_size'} = 0 if (!defined($this->{campos}->{'_s3_size'}));

    if ($this->{campos}->{'_fechae'} eq '') {
        $this->{campos}->{'_fechae'} = &lib_prontus::asigna_fecha_default('_fechae');
    }

    # titular
    if ($this->{campos}->{'_txt_titular'} eq '') {
        $this->{campos}->{'_txt_titular'} = 'SIN TITULO ' . (time - $prontus_varglb::URL_NUMBER);
    } else {
        $this->{campos}->{'_txt_titular'} =~ s/^\s+//;
        $this->{campos}->{'_txt_titular'} =~ s/\s+$//;
    }

    $this->{campos}->{'_tags'} =~ s/[^\w\,\-áéíóúüñÁÉÍÓÚÜÑ \.]//g;
    $this->{campos}->{'_tags'} =~ tr/ÁÉÍÓÚÜÑ/áéíóúüñ/;
    $this->{campos}->{'_tags'} = lc($this->{campos}->{'_tags'});

    $this->_artic_ajusta_horas();

    # fechas de calendario
    if (!defined($this->{campos}->{'_fecha_ini'}) or $this->{campos}->{'_fecha_ini'} eq '') {
        $this->{campos}->{'_fecha_ini'} = $this->{campos}->{'_fechap'};
    }

    if ($this->{campos}->{'_fecha_ini'} eq '99999999' or $this->{campos}->{'_fecha_ini'} eq '') {
        $this->{campos}->{'_fecha_ini'} = substr($this->{ts}, 0, 8);
    }

    if (!defined($this->{campos}->{'_fecha_fin'}) or $this->{campos}->{'_fecha_fin'} eq '') {
        $this->{campos}->{'_fecha_fin'} = $this->{campos}->{'_fechap'};
    }

    if ($this->{campos}->{'_fecha_fin'} eq '99999999' or $this->{campos}->{'_fecha_fin'} eq '') {
        $this->{campos}->{'_fecha_fin'} = substr($this->{ts}, 0, 8);
    }

    # Genera y arma campos privados de xml
    foreach my $nom_campo (@PRIVATE_ELEMENTS) {
        my $val_campo = $this->{campos}->{$nom_campo};
        $val_campo = '' if (!defined($val_campo));
        $val_campo = '' if ($nom_campo =~ /^(_seccion|_tema|_subtema)/ && $val_campo eq '0');

        # Codigo de reemplazo por ALD 08/02/2011
        # quita caracteres de control
        $val_campo =~ s/[\x01-\x09\x0B\x0C\x0E-\x1F]//g;
        if ($val_campo ne '' && substr($nom_campo, 0, 5) eq  '_txt_' || $val_campo =~ /[<>&]/) {
            $val_campo = "<![CDATA[$val_campo]]>";
        }
        $this->{xml_data} .= "<$nom_campo>$val_campo</$nom_campo>\n";
    }

    $this->{xml_data} .= "</_private>\n<_public>\n</_public>\n</artic_data>\n";

    # Guarda recursos del articulo
    $this->_guarda_recursos('asocfile');
    $this->_guarda_recursos('multimedia');
    $this->_guarda_recursos('foto');

    # Fotos fijas, a continuacion de las fotos
    $this->_guarda_fotosfijas();

    foreach my $nom_campo (keys %{$this->{campos}}) {
        # Solo permite campos con _ para los reservados que van en el XML
        next if (substr($nom_campo, 0, 1)  eq '_');

        # Salta los recursos, ya procesados previamente
        next if ($nom_campo =~ /^(asocfile_|multimedia_|foto_|fotofija_|_wfoto_|_hfoto_)/);
        my $val_campo = $this->{campos}->{$nom_campo};
        next if (!defined($val_campo));
        # Codigo de reemplazo por ALD 08/02/2011
        $val_campo =~ s/[\x01-\x09\x0B\x0C\x0E-\x1F]//g;
        next if ($val_campo eq '');

        if (substr($nom_campo, 0, 4)  eq 'vtxt') {
            # extrae fotos en base 64 publicadas en el cuerpo
            $val_campo = $this->_guarda_fotos_cuerpo($val_campo);
        }

        if ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'VTXT_RELPATH_LINK'} eq 'SI' && $nom_campo =~ /^vtxt_/i) {
            $val_campo =~ s/https?:\/\/$this->{cpan_server_name}\//\//isg;
            $val_campo =~ s/https?:\/\/$this->{public_server_name}\//\//isg;
        }
        $this->{xml_data} = &lib_prontus::replace_in_xml($this->{xml_data}, $nom_campo, $val_campo, 1);
    }

    $this->{campos}->{'_s3_size'} = 0 if (!defined($this->{campos}->{'_s3_size'}) or $this->{campos}->{'_s3_size'} eq '');
    my ($disk_size, $imag_size, $asset_size, $media_size) = $this->get_disk_size();

    my $total_size = $this->{campos}->{'_s3_size'} + $disk_size;
    $this->update_art_size($total_size, $disk_size, $imag_size, $asset_size, $media_size);

    # borra el contenido anterior del hash de campos, porque si se escribe uno nuevo, pues ya no corre.
    # esta var lo usa el obj. para no leer mas de 1 vez el xml
    %{$this->{xml_content}} = ();

    # Se escribe el XML a disco
    return $this->_flush_xml();
}

# ---------------------------------------------------------------
# Retorna hash de datos leidos del xml con los nombres de campo en minusculas.
# Si los datos ya estan cargados en el objeto, no lo lee de nuevo.
sub get_xml_content {
    my $this = shift;
    my $num_elem = scalar keys(%{$this->{xml_content}});
    # print STDERR "ts [$this->{ts}] get content[$this->{fullpath_xml}] num_elem [$num_elem]\n";
    if ($num_elem < 1) {
        $this->{xml_data} = &glib_fildir_03::read_file($this->{fullpath_xml});
        my $xml_data = $this->{xml_data};
        $xml_data =~ s/.+?<_private>(.*?)<\/_private>/$1/is;
        $xml_data =~ s/<_public>(.*?)<\/_public>.+/$1/is;
        %{$this->{xml_content}} = &lib_prontus::getCamposXml($xml_data);

        # Cargar datos de los archivos multimedia_video
        my @multimedia = grep { substr($_, 0, 11) eq 'multimedia_' && index($_, '.') < 0} keys %{$this->{xml_content}};
        foreach my $media (@multimedia) {
            $this->{xml_content}->{$media} =~ s/\s//g;
            my $fullpath_json = "$this->{dst_multimedia}/$media$this->{ts}.json";
            my $type = substr($media, 0, 16);
            # print STDERR "path json [$fullpath_json]\n";
            if (-f $fullpath_json) {
                my $buffer_json = &glib_fildir_03::read_file_bin($fullpath_json);
                # print STDERR "[$this->{ts}] buffer_json [$buffer_json]\n";

                # decodifica datos de json
                eval { $this->{media_metadata}->{$media} = decode_json($buffer_json); };
                if ($@) {
                    print STDERR "Error al decodificar json: $@, file[$fullpath_json] json:[$buffer_json]\n";
                    $this->{media_metadata}->{$media} = undef;
                    next if ($type eq 'multimedia_video');
                }

                $this->{xml_content}->{"$media.json"} = &lib_prontus::remove_front_string($fullpath_json, $this->{document_root});
            }

            if ($type eq 'multimedia_video') {
                # pasamos los datos de un hash a marcas separadas por ".", hasta 3 niveles de profundidad
                foreach my $key1 (keys %{$this->{media_metadata}->{$media}}) {
                    if ( ref $this->{media_metadata}->{$media}->{$key1} eq 'HASH') {
                        foreach my $key2 (keys %{$this->{media_metadata}->{$media}->{$key1}}) {
                            if ( ref $this->{media_metadata}->{$media}->{$key1}->{$key2} eq 'HASH') {
                                foreach my $key3 (keys %{$this->{media_metadata}->{$media}->{$key1}->{$key2}}) {
                                    $this->{xml_content}->{"$media.$key1.$key2.$key3"} = $this->{media_metadata}->{$media}->{$key1}->{$key2}->{$key3};
                                }
                            } else {
                                $this->{xml_content}->{"$media.$key1.$key2"} = $this->{media_metadata}->{$media}->{$key1}->{$key2};
                            }
                        }
                    } else {
                        $this->{xml_content}->{"$media.$key1"} = $this->{media_metadata}->{$media}->{$key1};
                    }
                }

                # sino existe 'size' en la metadata se debe determinar
                if (!defined($this->{media_metadata}->{$media}->{'size'})) {
                    # Obtenemos tamaño del archivo original
                    $this->{xml_content}->{"$media.size"} = &lib_prontus::bytes2kb(-s "$this->{dst_multimedia}/$this->{xml_content}->{$media}");
                }

            } elsif ($type eq 'multimedia_audio') {
                # Si no existen los datos intentamos generarlos
                if (!defined($this->{media_metadata}->{$media}) or !defined($this->{media_metadata}->{$media}->{duration})) {
                    my $fullpath      = $this->{dst_multimedia} .'/'. $this->{xml_content}->{$media};
                    my $resp;
                    if (-s $fullpath) {
                        $resp = `$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_FFMPEG'}/ffmpeg -i $fullpath 2>&1`;
                    }

                    my %data = ();
                    if ($resp =~ /Duration:\s*((\d+):(\d+):([\d\.]+))/is) {
                        $data{duration_hms} = $1;
                        $data{duration} = $2*3600 + $3*60 + $4;
                        if ($resp =~ /Audio: ([^,]+),(.+?)([0-9\.]+) kb\/s/is) {
                            $data{acodec} = $1;
                            my $other = $2;
                            $data{abitrate} = $3;

                            if ($other =~ /(\d+)\s*Hz/) {
                                $data{rate} = $1;
                            }
                            $data{size} = &lib_prontus::bytes2kb(-s "$this->{dst_multimedia}/$this->{xml_content}->{$media}");

                            my $buffer_json = encode_json(\%data);
                            &glib_fildir_03::write_file_bin($fullpath_json, $buffer_json);
                        } else {
                            print STDERR "[" . $this->{ts} . "][$fullpath] No se pudieron determinar las características [$media]\n";
                        }
                    } else {
                        print STDERR "[" . $this->{ts} . "][$fullpath] No se pudo determinar la duracion [$media]\n";
                    }

                    $this->{media_metadata}->{$media} = \%data;
                }

                # pasamos los datos de un hash a marcas separadas por "."
                foreach my $key1 (keys %{$this->{media_metadata}->{$media}}) {
                    $this->{xml_content}->{"$media.$key1"} = $this->{media_metadata}->{$media}->{$key1};
                }
            }

            # extension de la media
            $this->{xml_content}->{"$media.ext"} = &lib_prontus::get_file_extension($this->{xml_content}->{$media});

            # campos legacy
            $this->{xml_content}->{"_e$media"} = $this->{xml_content}->{"$media.ext"};
            $this->{xml_content}->{"_s$media"} = $this->{xml_content}->{"$media.size"};
            $this->{xml_content}->{"_d$media"} = $this->{xml_content}->{"$media.duration_hms"};
        }

        # indexar solo puede valer 1 o 0, si no existía en xml, entonces debe ser 1
        if (!defined($this->{xml_content}->{'_indexar'})) {
            $this->{xml_content}->{'_indexar'} = 1;
        }

        # priorizar solo puede valer 1 o 0, si no existía en xml, entonces debe ser 0
        if (!defined($this->{xml_content}->{'_priorizar'})) {
            $this->{xml_content}->{'_priorizar'} = 0;
        }

        # publicar vistas no esta definido en artículos antiguos
        if (!defined($this->{xml_content}->{'_publicarvistas'})) {
            if (defined($this->{xml_content}->{'_soloportadas'}) && $this->{xml_content}->{'_soloportadas'}) {
                $this->{xml_content}->{'_publicarvistas'} = 1;
            }
        }

        # inicializa con valor vacío campos reservados del artículo que no se encontraron
        foreach my $nom_campo (@PRIVATE_ELEMENTS) {
            if (!defined($this->{xml_content}->{$nom_campo})) {
                $this->{xml_content}->{$nom_campo} = '';
            }
        }

        # fechas de calendario
        if (!defined($this->{xml_content}->{'_fecha_ini'}) or $this->{xml_content}->{'_fecha_ini'} eq '') {
            $this->{xml_content}->{'_fecha_ini'} = $this->{xml_content}->{'_fechap'};
        }

        if ($this->{xml_content}->{'_fecha_ini'} eq '99999999' or $this->{xml_content}->{'_fecha_ini'} eq '') {
            $this->{xml_content}->{'_fecha_ini'} = substr($this->{ts}, 0, 8);
        }

        if (!defined($this->{xml_content}->{'_fecha_fin'}) or $this->{xml_content}->{'_fecha_fin'} eq '') {
            $this->{xml_content}->{'_fecha_fin'} = $this->{xml_content}->{'_fechap'};
        }

        if ($this->{xml_content}->{'_fecha_fin'} eq '99999999' or $this->{xml_content}->{'_fecha_fin'} eq '') {
            $this->{xml_content}->{'_fecha_fin'} = substr($this->{ts}, 0, 8);
        }
    }
    return %{$this->{xml_content}};
}

# ---------------------------------------------------------------
# Guarda tags en bd
# Se invoca al momento de guardar un articulo.
sub tags2bd {
    my ($this, $base) = @_;

    $this->get_xml_content();
    my @tags = split(/,/, $this->{xml_content}->{'_tags'});
    # para evitar posibles tags repetidos
    my %temp;
    foreach my $tag (@tags) {
        $temp{$tag} = 1;
    }
    @tags = keys %temp;

    # Primero elimina los tags del artic actual:
    my $sql = "delete from TAGSART where TAGSART_IDART = ?";
    my @params = ($this->{ts});
    my $salida = &glib_dbi_03::ejecutar_sql_param($base, $sql, \@params);
    if ($base->errstr) {
        $Artic::ERR = "Error actualizando tabla de Tags, ts[$this->{ts}]\n";
        print STDERR "$Artic::ERR sql[$sql][$!]";
        return 0;
    }

    # Asocia los TAGS al artic
    foreach my $idtag (@tags) {
        $sql = "insert into TAGSART set TAGSART_IDTAGS= ?, TAGSART_IDART = ?";
        @params = ($idtag, $this->{ts});
        $salida = &glib_dbi_03::ejecutar_sql_param($base, $sql, \@params);
        if ($base->errstr) {
            $Artic::ERR = "Error actualizando tabla de Tags, ts[$this->{ts}]\n";
            print STDERR "$Artic::ERR sql[$sql][$!]";
            return 0;
        }
    }

    return 1;
}

# ---------------------------------------------------------------
# puebla tabla de multitag
# Se invoca al momento de guardar un articulo.
sub multitags2bd {
    my ($this, $base) = @_;
    my %tipos = ('S' => '_multitag_seccion', 'T' => '_multitag_tema', 'ST' => '_multitag_subtema');

    foreach my $type (keys %tipos) {
        # Primero elimina los tags del artic actual:
        my $sql = "delete from MULTITAG_ART_$type where MULTITAG_ART_$type\_ART_ID = ?";
        my @params = ($this->{ts});
        my $salida = &glib_dbi_03::ejecutar_sql_param($base, $sql, \@params);
        if ($base->errstr) {
            $Artic::ERR = "Error actualizando tabla de Multitag, ts[$this->{ts}]\n";
            print STDERR "$Artic::ERR sql[$sql][$!]";
            return 0;
        }

        $this->get_xml_content();

        # Asocia los TAGS al artic
        next if (!defined($this->{'xml_content'}->{$tipos{$type}}));
        my @multitags = split(/,/, $this->{'xml_content'}->{$tipos{$type}});
        foreach my $id (@multitags) {
            $sql = "insert into MULTITAG_ART_$type set MULTITAG_ART_$type\_ART_ID = ?, MULTITAG_ART_$type\_ID = ?";
            @params = ($this->{ts}, $id);
            my $salida = &glib_dbi_03::ejecutar_sql_param($base, $sql, \@params);
            if ($base->errstr) {
                $Artic::ERR = "Error actualizando tabla de Multitag, ts[$this->{ts}]\n";
                print STDERR "$Artic::ERR sql[$sql][$!]";
                return 0;
            }
        }
    }
    return 1;
}

# ---------------------------------------------------------------
# puebla tabla de urls friendly v5
# Se invoca al regenerar la DB de prontus
sub friendly_v5_2bd {
    my ($this, $base) = @_;

    $this->get_xml_content();

    if (!exists $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'FRIENDLY_EXCLUDE_FID'}{$this->{'xml_content'}->{'_fid'}}) {
        my %options = %{$_[1]};
        my $sql = "INSERT INTO URLF5 SET URLF5_TYPE = ?, URLF5_SUBTYPE = ?,";
        $sql .= " URLF5_SUBTYPEID = ?, URLF5_FILE = ?, URLF5_URL = ?";

        $this->{'xml_content'}{'_plt'} =~ /\.(\w+)$/;
        my $ext = $1;
        my $artic_path = "$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_SITE'}$prontus_varglb::DIR_ARTIC/";
        $artic_path .= $this->{fechac} . $prontus_varglb::DIR_PAG . "/$this->{ts}.$ext";
        if ($this->{'xml_content'}{'_url'} eq '') {
            $Artic::ERR = "Error actualizando tabla de URLF5 [0], ts[$this->{ts}] no tiene url asignada en xml\n";
            print STDERR $Artic::ERR;
            return 0;
        }

        my @params = ('artic', 'artic', $this->{ts}, $artic_path, $this->{'xml_content'}{'_url'});
        my $salida = &glib_dbi_03::ejecutar_sql_param($base, $sql, \@params);

        if ($base->errstr) {
            $Artic::ERR = "Error actualizando tabla de URLF5 [0], ts[$this->{ts}]\n";
            print STDERR "$Artic::ERR [$sql][$!][".$base->errstr."]\n";
            return 0;
        }
    }
    return 1;
};

# ---------------------------------------------------------------
# puebla tabla de urls friendly v4
# Se invoca al regenerar la DB de prontus
sub friendly_v4_2bd {
    my ($this, $base) = @_;

    $this->get_xml_content();

    if (!exists $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'FRIENDLY_EXCLUDE_FID'}{$this->{'xml_content'}->{'_fid'}}) {
        # Primero elimina la url del artic actual:
        my $sql = "delete from URL where URL_ART_ID = ?";
        my @params = ($this->{ts});
        my $salida = &glib_dbi_03::ejecutar_sql_param($base, $sql, \@params);
        if ($base->errstr) {
            $Artic::ERR = "Error actualizando tabla de urls [0], ts[$this->{ts}]\n";
            print STDERR "$Artic::ERR sql[$sql][$!][".$base->errstr."]";
            return 0;
        }

        my $titularV4;
        if (defined($this->{'xml_content'}{'_custom_slug'}) && $this->{'xml_content'}{'_custom_slug'} eq 'SI' && $this->{'xml_content'}{'_slug'} ne '') {
            $titularV4 = &lib_prontus::ajusta_titular_friendly($this->{'xml_content'}{'_slug'});
        } else {
            $titularV4 = &lib_prontus::ajusta_titular_friendly($this->{'xml_content'}{'_txt_titular'});
        }

        my $ts = $this->verifica_colision_url_titular($base, $titularV4);
        if ($ts) {
            $Artic::ERR = "Error actualizando tabla de URLs [1], Conflicto de titular entre [$this->{ts}] y [$ts]\n";
            print STDERR "$Artic::ERR [$sql][$!][".$base->errstr."]\n";
            return 0;
        }

        $sql = "insert into URL set URL_ART_ID = ?, URL_ART_URI = ?";
        @params = ($this->{ts}, $titularV4);
        $salida = &glib_dbi_03::ejecutar_sql_param($base, $sql, \@params);
        if ($base->errstr) {
            $Artic::ERR = "Error actualizando tabla de urls [2], ts[$this->{ts}]\n";
            print STDERR "$Artic::ERR sql[$sql][$!][".$base->errstr."]";
            return 0;
        }
    }
    return 1;
}

# ---------------------------------------------------------------
# genera todo lo que necesita la friendly v4 para funcionar
# guarda el titular formateado en la BD
# genera el symlink en el filesystem
# si no esta activada friendly 4 no se hace nada
sub genera_friendly_v4 {
    if ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'FRIENDLY_URLS_VERSION'} ne '4') {
        return 1;
    }

    my ($this, $base) = @_;

    if ($this->{is_preview} == 1) {
        return 1;
    }

    $this->get_xml_content();

    if (exists $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'FRIENDLY_EXCLUDE_FID'}{$this->{'xml_content'}->{'_fid'}}) {
        return 1;
    }

    my ($buffer);
    my ($salida, $artTs, $friendlyAntigua, $sql);

    my $titularV4;
    if ($this->{'xml_content'}{'_custom_slug'} eq 'SI' && $this->{'xml_content'}{'_slug'} ne '') {
        $titularV4 = &lib_prontus::ajusta_titular_friendly($this->{'xml_content'}{'_slug'});
    } else {
        $titularV4 = &lib_prontus::ajusta_titular_friendly($this->{'xml_content'}{'_txt_titular'});
    }

    if (length($titularV4) < 5) {
        $Artic::ERR = "Error: La url generada es muy corta, debe tener al menos 5 caracteres [$this->{ts}] [$titularV4]\n";
        return 0;
    }

    $this->{'xml_content'}{'_plt'} =~ /\.(\w+)$/;
    my $ext = $1;
    my $filepath;

    if ($base) {
        my $ts = $this->verifica_colision_url_titular($base, $titularV4);
        if ($ts) {
            $Artic::ERR = "Error actualizando tabla de URLs [3], Conflicto de titular entre [$this->{ts}] y [$ts]\n";
            return 0;
        }

        # se busca el titular friendly de este articulo, si existe, para borrar el archivo actual
        # antes de generar uno nuevo
        # nunca es vacio si esta asociado un ts a una url, ya que no se permiten titulares vacios
        $sql = "select URL_ART_URI, URL_ART_ID from URL where URL_ART_ID = ?";
        my @params = ($this->{ts});
        $salida = &glib_dbi_03::ejecutar_sql_bind_param($base, $sql, \@params, \($friendlyAntigua, $artTs));
        $salida->fetch;
        $salida->finish;

        if ($base->errstr) {
            $Artic::ERR = "Error al obtener datos del articulo de la tabla de URLs [1][$this->{ts}]]\n";
            print STDERR "$Artic::ERR [$sql][$!][".$base->errstr."]\n";
            return 0;
        }

        # si hay friendly asociada al ts, se debe eliminar el archivo
        # conservamos los directorios ya que pueden haber mas archivos
        # si no hay en algun momento se reutilizara la ruta
                # si la friendly antigua no es vacia se debe borrar
        if ($friendlyAntigua ne '') {
            $filepath = '/' . substr($friendlyAntigua, 0, 2) . '/' . substr($friendlyAntigua, 2, 2) . "/$friendlyAntigua.$ext";
            # si el archivo existe intentamos borrarlo
            if (-e $this->{dst_links_url} . $filepath) {
                if (!unlink($this->{dst_links_url} . $filepath)) {
                    print STDERR "Error borrando archivo [$this->{dst_links_url}$filepath/]\n";
                }
            }

            # se eliminan los archivos de las multivistas
            foreach my $mv (keys %{$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'MULTIVISTAS'}}) {
                # si el archivo existe intentamos borrarlo
                if (-e $this->{dst_links_url} . "-$mv" . $filepath) {
                    if (!unlink($this->{dst_links_url} . "-$mv" . $filepath)) {
                        print STDERR "Error borrando archivo [$this->{dst_links_url}-$mv$filepath/]\n";
                    }
                }
            }
        }
    }

    my $published = $this->check_publish_art();
    # se genera el path de destino del archivo para crear los directorios
    $filepath = '/' . substr($titularV4, 0, 2) . '/' . substr($titularV4, 2, 2);
    if (&glib_fildir_03::check_dir($this->{dst_links_url} . $filepath)) {
        # escribimos el nuevo symlink relativo, ej:
        # ../../../../../artic/20161011/pags/20161011142332.html
        my $realPath      = '../../../..' . $prontus_varglb::DIR_ARTIC . '/' . $this->{fechac} . $prontus_varglb::DIR_PAG . '/' . $this->{ts} . ".$ext";
        my $friendly_path = "$this->{dst_links_url}$filepath/$titularV4.$ext";
        my $cmd           = "ln -s $realPath $friendly_path";
        if ($published) {
            system($cmd) if (!-l $friendly_path);
        } else {
            unlink $friendly_path;
        }
    } else {
        $Artic::ERR = "Error creando path [$this->{dst_links_url}$filepath/]\n";
        return 0;
    }

    if ($base) {
        # actualizamos la DB con este articulo
        # si el art id es vacio, es articulo nuevo
        if ($artTs eq '') {
            $sql = "INSERT INTO URL SET URL_ART_URI = ?, URL_ART_ID = ?";
        } else {
            # si hay TS se debe actualizar el registro
            $sql = "UPDATE URL SET URL_ART_URI = ? WHERE URL_ART_ID = ?";
        }
        my @params = ($titularV4, $this->{ts});
        $salida = &glib_dbi_03::ejecutar_sql_param($base, $sql, \@params);
        if ($base->errstr) {
            $Artic::ERR = "Error actualizando tabla de URLs [4], ts[$this->{ts}]\n";
            print STDERR "$Artic::ERR [$sql][$!][".$base->errstr."]\n";
            return 0;
        }
    }

    # Se generan las multivistas
    foreach my $mv (keys %{$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'MULTIVISTAS'}}) {
        # se genera el path de destino del archivo para crear los directorios
        $filepath = '/' . substr($titularV4, 0, 2) . '/' . substr($titularV4, 2, 2);
        if (&glib_fildir_03::check_dir($this->{dst_links_url} . "-$mv" . $filepath)) {
            # escribimos el nuevo symlink relativo, ej:
            # ../../../../../artic/20161011/pags/20161011142332.html
            my $realPath      = '../../../..' . $prontus_varglb::DIR_ARTIC . '/' . $this->{fechac} . $prontus_varglb::DIR_PAG . "-$mv/" . $this->{ts} . ".$ext";
            my $friendly_path = "$this->{dst_links_url}-$mv$filepath/$titularV4.$ext";
            my $cmd           = "ln -s $realPath $friendly_path";
            if ($published) {
                system($cmd) if (!-l $friendly_path);
            } else {
                unlink $friendly_path;
            }
        } else {
            print STDERR "Error creando path [$this->{dst_links_url}-$mv$filepath/]\n";
        }
    }
    return 1;
}

sub genera_friendly_v5 {
    if ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'FRIENDLY_URLS_VERSION'} ne '5') {
        return 1;
    }

    my ($this, $base) = @_;

    if ($this->{is_preview} == 1) {
        return 1;
    }

    $this->get_xml_content();

    if (exists $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'FRIENDLY_EXCLUDE_FID'}{$this->{'xml_content'}->{'_fid'}}) {
        return 1;
    }

    $this->{'xml_content'}{'_plt'} =~ /\.(\w+)$/;
    my $ext = $1;
    my $file = $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_SITE'} . $prontus_varglb::DIR_ARTIC . '/' . $this->{fechac} . $prontus_varglb::DIR_PAG . '/' . $this->{ts} . ".$ext";

    my @vistas = (keys %{$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'MULTIVISTAS'}});
    &lib_friendly::init($prontus_varglb::DIR_SERVER, $prontus_varglb::DIR_FRIENDLY5, \@vistas);

    my ($friendlyAntigua, $artTs) = ('', '');
    if ($base) {
        # se busca la url antigua de este articulo, si existe, para borrarla antes de generar una nuevo
        # nunca es vacio si esta asociado un ts a una url, ya que no se permiten titulares vacios
        my $sql = "select URLF5_URL, URLF5_SUBTYPEID from URLF5 where URLF5_SUBTYPEID = ?";
        my @params = ($this->{ts});
        my $salida = &glib_dbi_03::ejecutar_sql_bind_param($base, $sql, \@params, \($friendlyAntigua, $artTs));
        $salida->fetch;
        $salida->finish;

        # La url fue borrada, se borra si existe
        if ($this->{'xml_content'}->{'_url'} eq '') {
            if ($friendlyAntigua ne '') {
                my ($res, $msg) = &lib_friendly::deleteFriendly($friendlyAntigua);
                my %options = (
                    'type'          => 'artic',
                    'subtype_id'    => $this->{ts},
                    'file'          => $file,
                    'url'           => $friendlyAntigua
                );

                lib_urlf5::borrar_url($base, \%options);
            }
            return 1;
        }

        my $ts = $this->verifica_colision_url_v5($base);
        if ($ts) {
            $Artic::ERR = "Error actualizando tabla de URLF5s [0], Conflicto de url entre [$this->{ts}] y [$ts]\n";
            return 0;
        }

        if ($base->errstr) {
            $Artic::ERR = "Error al obtener datos del articulo de la tabla de URLF5 [0][$this->{ts}]]\n";
            print STDERR "$Artic::ERR [$sql][$!][".$base->errstr."]\n";
            return 0;
        }

        # si hay friendly asociada al ts, se debe eliminar
        if ($friendlyAntigua ne '') {
            my ($res, $msg) = &lib_friendly::deleteFriendly($friendlyAntigua);
        }

        # actualizamos la DB con este articulo
        # si el art id es vacio, es articulo nuevo
        my %options = (
            'type'          => 'artic',
            'subtype'       => 'artic',
            'subtype_id'    => $this->{ts},
            'file'          => $file,
            'url'           => $this->{'xml_content'}->{'_url'}
        );
        if ($artTs eq '') {
            lib_urlf5::insert_url($base, \%options);
        } else {
            # si hay TS se debe actualizar el registro
            lib_urlf5::actualizar_url($base, \%options);
        }
    }

    my $published = $this->check_publish_art();
    if ($published && $this->{'xml_content'}->{'_url'} ne '') {
        my ($res, $data, $msg) = &lib_friendly::insertFriendly($this->{'xml_content'}->{'_url'}, $file, 200);
        if ($res == 0) {
            $Artic::ERR = "Error al crear friendly de articulo [$res][$msg]ts[$this->{ts}]\n";
            print STDERR $Artic::ERR;
            return 0;
        }
    }
    return 1;
}

# ---------------------------------------------------------------
# Revisa el titular a usar para url friendly v4 para evitar colisiones con otros existentes.
sub verifica_colision_url_v5 {
    my ($this, $base) = @_;
    my $artTs;
    # revisamos si existe otro articulo con el mismo titular (las urls son unicas)
    my @params = ($this->{'xml_content'}->{'_url'});
    my $sql = "select URLF5_SUBTYPEID from URLF5 where URLF5_URL = ?";
    my $salida = &glib_dbi_03::ejecutar_sql_bind_param($base, $sql, \@params, \($artTs));
    $salida->fetch;
    $salida->finish;

    if ($base->errstr) {
        $Artic::ERR = "Error al verificar colision, ts[$this->{ts}]\n";
        print STDERR "$Artic::ERR [$sql][$!][".$base->errstr."]";
        return 0;
    }

    # si existe un articulo con la misma url que no es este
    # no se continua el proceso y se devuelve el ts encontrado
    if (defined($artTs) && $artTs ne '' && $artTs ne $this->{ts}) {
        return $artTs;
    }
    return 0;
}

# ---------------------------------------------------------------
# Revisa el titular a usar para url friendly v4 para evitar colisiones con otros existentes.
sub verifica_colision_url_titular {
    my ($this, $base, $titularV4) = @_;
    my $artTs;
    # revisamos si existe otro articulo con el mismo titular (las urls son unicas)
    my $sql = "select URL_ART_ID from URL where URL_ART_URI = ?";
    my @params = ($titularV4);
    my $salida = &glib_dbi_03::ejecutar_sql_bind_param($base, $sql, \@params, \($artTs));
    $salida->fetch;
    $salida->finish;

    if ($base->errstr) {
        $Artic::ERR = "Error al verificar colision, ts[$this->{ts}]\n";
        print STDERR "$Artic::ERR [$sql][$!][".$base->errstr."]\n";
        return 0;
    }

    # si existe un articulo con la misma url que no es este
    # no se continua el proceso y se devuelve el ts encontrado
    if (defined($artTs) && $artTs ne '' && $artTs ne $this->{ts}) {
        return $artTs;
    }
    return 0;
}

# ---------------------------------------------------------------
# verifica si el articulo es visible:
# si es una preview, es visible. De otro modo,
# alta = 1
# fecha de publicacion es anterior a la fecha actual
# fecha de expiracion es posterior a la fecha actual
# si esta marcado "solo despublicar de portadas" el articulo es visible
sub check_publish_art {
    my $this = shift;

    if ($this->{is_preview} == 1)  {
        return 1;
    }

    $this->get_xml_content();
    if ($this->{'xml_content'}{'_alta'} ne '1') {
        return 0;
    }

    if ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'CONTROL_FECHA'} eq 'SI') {
        my $ts_now = &glib_hrfec_03::get_dtime_pack4();
        my $fechahorap = $this->{'xml_content'}{'_fechap'} . $this->{'xml_content'}{'_horap'} . '00';
        $fechahorap =~ s/://g;
        my $fechahorae = $this->{'xml_content'}{'_fechae'} . $this->{'xml_content'}{'_horae'} . '00';
        $fechahorae =~ s/://g;
        # fecha actual es mayor o = que fecha de publicación
        if ($ts_now >= $fechahorap) {
            if ($ts_now <= $fechahorae || $this->{'xml_content'}{'_soloportadas'}) {
                return 1;
            } else {
                # articulo expirado, no hay vistas
                return 0;
            }
        } elsif ($this->{'xml_content'}{'_publicarvistas'}) {
            # vistas se crean aunque no se cumpla la fecha de publicación
            return 1;
        } else {
            # articulo aun no se debe publicar
            return 0;
        }
    }
    return 1;
}

# ---------------------------------------------------------------
# Invocada al eliminar vistas de artículos para el cron fechas.
# Elimina todos los archivos de vista.
# Elimina archivos de friendly URL.
# No elimina XML ni archivos asociados.
sub borra_pags_artic {
    my ($this, $base, $no_run_regen, $list_procs) = @_;

    # si el articulo esta publicado, no se eliminan las paginas
    if ($this->check_publish_art()) {
        return '';
    }

    my $ts        = $this->{ts};
    my $dir_fecha = $this->{fechac};

    my $dirpag      = $this->{dst_pags};
    my $dirpagpar   = $this->{dst_pagspar};

    my $pathnice = &lib_prontus::get_path_nice();
    $pathnice = "$pathnice -n19 " if ($pathnice);

    # Cargar datos del xml. Necesarios para poder armar friendly url.
    my %campos_xml = $this->get_xml_content();

    # Borra paginas generadas
    my @files2delete = glob("$dirpag/$ts" . '.*');
    foreach my $file2delete (@files2delete) {
        unlink $file2delete;

        if (!exists $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'CACHE_PURGE_EXCLUDE_FID'}{$campos_xml{'_fid'}}) {
            &lib_prontus::purge_cache($file2delete);
        }
    }

    if (!exists $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'CACHE_PURGE_EXCLUDE_FID'}{$campos_xml{'_fid'}}) {
        # Arma friendly url para hacer el purge
        my $marca_file = $this->get_fullpath_artic('', $campos_xml{'_plt'});
        $marca_file = &lib_prontus::remove_front_string($marca_file, $this->{document_root});

        my %artic_data = ();
        $artic_data{'fid'} = $campos_xml{'_fid'};
        $artic_data{'custom_slug'} = $campos_xml{'_custom_slug'};
        $artic_data{'slug'} = $campos_xml{'_slug'};
        $artic_data{'_url'} = $campos_xml{'_url'};
        $artic_data{'nom_seccion'} = $campos_xml{'_nom_seccion1'};
        $artic_data{'nom_tema'} = $campos_xml{'_nom_tema1'};
        $artic_data{'nom_subtema'} = $campos_xml{'_nom_subtema1'};

        my $fileurl = &lib_prontus::parse_filef('%%_FILEURL%%', $campos_xml{'_txt_titular'}, $this->{ts}, $this->{prontus_id}, $marca_file, \%artic_data);
        &lib_prontus::purge_cache($fileurl);
    }

    # Borrar paginas paralelas
    @files2delete = glob("$dirpagpar/$ts*" . '.*');
    foreach my $file2delete (@files2delete) {
        unlink $file2delete;
        if (!exists $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'CACHE_PURGE_EXCLUDE_FID'}{$campos_xml{'_fid'}}) {
            &lib_prontus::purge_cache($file2delete);
        }
    }

    # Borra paginas de multivistas
    foreach my $mv (keys %{$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'MULTIVISTAS'}}) {
        my $dir_art_mv = $dirpag;
        $dir_art_mv =~ s/(\d{8})\/pags/$1\/pags-$mv/;
        my @files2delete_mv = glob("$dir_art_mv/$ts" . '.*');
        foreach my $file2delete (@files2delete_mv) {
            unlink $file2delete;
            if (!exists $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'CACHE_PURGE_EXCLUDE_FID'}{$campos_xml{'_fid'}}) {
                &lib_prontus::purge_cache($file2delete);
            }
        }

        # Paginas paralelas
        $dir_art_mv = $dirpagpar;
        $dir_art_mv =~ s/(\d{8})\/pagspar/$1\/pagspar-$mv/;
        @files2delete_mv = glob("$dir_art_mv/$ts*" . '.*');
        foreach my $file2delete (@files2delete_mv) {
            unlink $file2delete;
            if (!exists $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'CACHE_PURGE_EXCLUDE_FID'}{$campos_xml{'_fid'}}) {
                &lib_prontus::purge_cache($file2delete);
            }
        }
    }

    # borramos archivos de friendly v4
    if ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'FRIENDLY_URLS_VERSION'} eq '4') {
        my $titularV4 = &lib_prontus::ajusta_titular_friendly($this->{'xml_content'}{'_txt_titular'});
        my $filepath = '/' . substr($titularV4, 0, 2) . '/' . substr($titularV4, 2, 2) . "/$titularV4.html";
        unlink $this->{dst_links_url} . $filepath;

        # se eliminan los archivos de las multivistas
        foreach my $mv (keys %{$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'MULTIVISTAS'}}) {
            unlink $this->{dst_links_url} . "-$mv" . $filepath;
        }
        # se borra el articulo de la tabla
        my $sql = "delete from URL where URL_ART_ID= ?";
        my @params = ($ts);
        my $salida = &glib_dbi_03::ejecutar_sql_param($base, $sql, \@params);
        if ($base->errstr) {
            $Artic::ERR = "Error al borrar de la tabla URL, ts[$ts]\n";
            print STDERR "$Artic::ERR [$sql][$!][".$base->errstr."]\n";
            return 0;
        }
    }

    # Borra relacionados manual
    &lib_tax::borrar_relacionados_manualtax($dirpag, $ts);
    lib_tags::borrar_relacionados_tag($dirpag, $ts);

    # Obtiene taxonomia antes de eliminar
    my %hashtemp;
    my @taxtodelete;

    if ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'TAXONOMIA_NIVELES'} =~ /^[1-3]$/) {
        for (my $i = 1; $i <= $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'TAXONOMIA_NIVELES'}; $i++) {
            my ($secc, $tem, $stem) = &lib_tax::get_taxonomia($ts, $base, $i);
            my $if_last_in_tax = &lib_tax::get_tax_if_last_art($base, $secc, $tem, $stem);

            print STDERR "[$secc/$tem/$stem] if_last_in_tax[$if_last_in_tax]\n";

            if ($secc && $if_last_in_tax) {
                my $t = $tem;
                my $st = $stem;
                $t = "" if (!$tem);
                $st = "" if (!$stem);

                push @taxtodelete, "_$secc\_$t\_$st\_";
            }

            $hashtemp{$i} = "$secc/$tem/$stem" if ($secc > 0);    # se evita para generar relacionados sin taxonomia.
        }
    }

    foreach my $todelete (@taxtodelete) {
        my $path = "$prontus_varglb::DIR_SERVER$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_SITE'}$prontus_varglb::DIR_TAXPORT";
        my @files = `find $path -name "*$todelete*"`;
        print STDERR "files todelete path[$path -name \"*$todelete*\"]\n";

        foreach my $filetodelete (@files) {
            $filetodelete =~ s/\n//sg;
            print STDERR "filetodelete: $filetodelete\n";
            unlink $filetodelete;
        }
    }

    # Regenera relacionados
    if ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'TAXONOMIA_NIVELES'} =~ /^[1-3]$/) {
        lib_tax::set_vars($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_SITE'}, $prontus_varglb::DIR_ARTIC, $prontus_varglb::DIR_PAG, $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_TEMPLATES'}, $prontus_varglb::DIR_TAXONOMIA, $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'NUM_RELAC_DEFAULT'});
        for (my $i = 1 ; $i <= $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'TAXONOMIA_NIVELES'} ; $i++) {
            if (defined $hashtemp{$i}) {
                my ($secc, $tem, $stem) = split /\//, $hashtemp{$i};
                lib_tax::generar_relacionados($secc, $tem, $stem, $base);
                # Ahora parsea art relacionados para MVs
                foreach my $mv (keys %{$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'MULTIVISTAS'}}) {
                    print STDERR "generar_relacionados [$secc, $tem, $stem]\n";
                    lib_tax::generar_relacionados($secc, $tem, $stem, $base, $mv);
                }
            }
        }
    }

    # regenera tagports y taxports
    my $fid = $campos_xml{'_fid'};;

    # TODO: agrupar los datos de tags entre todos los artículos procesados para regenerar una sola vez.
    my $tags_data = $campos_xml{'_tags'};

    if ($tags_data) {
        # Regenerar portadas tagonomicas.
        my $param_especif_tagonomicas = $tags_data;
        $param_especif_tagonomicas =~ s/,/\//sg;
        my $cmd = "$pathnice $prontus_varglb::DIR_SERVER/$prontus_varglb::DIR_CGI_CPAN/prontus_tags_ports.cgi $prontus_varglb::PRONTUS_ID $param_especif_tagonomicas $fid >/dev/null 2>&1 &";
        print STDERR "[" . glib_hrfec_03::get_dtime_pack4() . "]$cmd\n";
        system $cmd;
    }

    # Borra cache listas de articulo
    glib_fildir_03::borra_dir("$prontus_varglb::DIR_SERVER$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_CPAN'}/data/cache");

    # regenera taxports
    my $cmd;
    my $notax = 1;
    for (my $i = 1; $i <= $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'TAXONOMIA_NIVELES'}; $i++) {
        last if (scalar @taxtodelete > 0);

        my $taxonomia = $hashtemp{$i};
        my ($secc, $tem, $stem) = split /\//, $taxonomia;

        # si el articulo no tiene taxonomia no hay que regenerar taxports
        next if ($secc < 1);

        $notax = 0;
        my $param_especif = $fid . '/' . $secc . '/' . $tem . '/' . $stem;

        if ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'TAXPORT_DINAMICAS'} eq 'NO') {
            $cmd = "$pathnice $prontus_varglb::DIR_SERVER/$prontus_varglb::DIR_CGI_CPAN/prontus_cron_taxport.cgi $prontus_varglb::PRONTUS_ID $param_especif >/dev/null 2>&1 &";

            if ($no_run_regen) {
                $$list_procs{"prontus_cron_taxport.cgi $prontus_varglb::PRONTUS_ID $param_especif"} = $cmd;
            } else {
                print STDERR "[" . &glib_hrfec_03::get_dtime_pack4() . "] $cmd\n";
                system $cmd;
            }
        } else {
            &lib_tax::borrar_taxport($param_especif);
        }

        # Regenera las salidas List
        $cmd = "$pathnice $prontus_varglb::DIR_SERVER/$prontus_varglb::DIR_CGI_CPAN/prontus_cron_list.cgi $prontus_varglb::PRONTUS_ID $param_especif >/dev/null 2>&1 &";

        if ($no_run_regen) {
            $$list_procs{"prontus_cron_list.cgi $prontus_varglb::PRONTUS_ID $param_especif"} = $cmd;
        } else {
            print STDERR "[" . &glib_hrfec_03::get_dtime_pack4() . "] $cmd\n";
            system $cmd;
        }
    }

    # si el articulo no tiene taxonomia debemos regenerar las listas
    if ($notax) {
        # Regenera las salidas List
        $cmd = "$pathnice $prontus_varglb::DIR_SERVER/$prontus_varglb::DIR_CGI_CPAN/prontus_cron_list.cgi $prontus_varglb::PRONTUS_ID $fid/// >/dev/null 2>&1 &";
        if ($no_run_regen) {
            $$list_procs{"prontus_cron_list.cgi $prontus_varglb::PRONTUS_ID $fid///"} = $cmd;
        } else {
            print STDERR "[" . &glib_hrfec_03::get_dtime_pack4() . "]$cmd\n";
            system $cmd;
        }
    }

    return '';
}

# ---------------------------------------------------------------
# Elimina un artículo con todos sus datos asociados.
sub borra_artic {
    my ($this, $base, $no_run_regen, $list_procs) = @_;
    my $ts        = $this->{ts};

    # borramos multitags asociados al articulo
    # se borra el articulo de las tablas multitag
    my @tipos = ('S', 'T', 'ST');
    my @params = ($this->{ts});
    foreach my $type (@tipos) {
        # Primero elimina los tags del artic actual:
        my $sql = "DELETE from MULTITAG_ART_$type where MULTITAG_ART_$type\_ART_ID = ?";
        my $salida = glib_dbi_03::ejecutar_sql_param($base, $sql, \@params);
        if ($base->errstr) {
            $Artic::ERR = "Error borrando de tabla de URL, ts[$this->{ts}]\n";
            print STDERR "$Artic::ERR [$sql][$!][".$base->errstr."]\n";
            return $Artic::ERR;
        }
    }

    # Desasocia tags al artic
    my $sql_deltag = "delete from TAGSART where TAGSART_IDART = ?";
    glib_dbi_03::ejecutar_sql_param($base, $sql_deltag, \@params);
    if ($base->errstr) {
        $Artic::ERR = "Error borrando de tabla de URL, ts[$this->{ts}]\n";
        print STDERR "$Artic::ERR [$sql_deltag][$!][".$base->errstr."]\n";
        return $Artic::ERR;
    }

    # borra articulo de tablas ART_PUBLICAR y ART_DESPUBLICAR
    my $sql_delpub = "delete from ART_PUBLICAR where ART_ID = ?";
    glib_dbi_03::ejecutar_sql_param($base, $sql_deltag, \@params);
    if ($base->errstr) {
        $Artic::ERR = "Error borrando de tabla de ART_PUBLICAR, ts[$this->{ts}]\n";
        print STDERR "$Artic::ERR [$sql_deltag][$!][".$base->errstr."]\n";
        return $Artic::ERR;
    }
    $sql_delpub = "delete from ART_DESPUBLICAR where ART_ID = ?";
    glib_dbi_03::ejecutar_sql_param($base, $sql_deltag, \@params);
    if ($base->errstr) {
        $Artic::ERR = "Error borrando de tabla de ART_DESPUBLICAR, ts[$this->{ts}]\n";
        print STDERR "$Artic::ERR [$sql_deltag][$!][".$base->errstr."]\n";
        return $Artic::ERR;
    }

    # Cargar datos del xml para poder modificarlos en memoria antes de realizar el borrado de paginas
    $this->get_xml_content();
    # marca articulo sin alta para poder borrar las paginas asociadas
    $this->{'xml_content'}{'_alta'} = '0';

    $this->borra_pags_artic($base, $no_run_regen, $list_procs);

    # Elimina el archivo de la Base de Datos
    my $resp = $this->art_delete_bd($base);
    if (!$resp) {
        return $Artic::ERR;
    }

    # borra url f5 del articulo
    if ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'FRIENDLY_URLS_VERSION'} eq '5') {
        my @vistas = (keys %{$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'MULTIVISTAS'}});
        lib_friendly::init($prontus_varglb::DIR_SERVER, $prontus_varglb::DIR_FRIENDLY5, \@vistas);

        my ($res, $msg) = lib_friendly::deleteFriendly($this->{'xml_content'}{'_url'});
        # print STDERR "$this->{'xml_content'}{'_url'} -> res[$res] msg[$msg]\n";
        my %options = (
            'type'          => 'artic',
            'subtype'       => 'artic',
            'subtype_id'    => $this->{ts}
        );

        lib_urlf5::borrar_url($base, \%options);
    }

    unlink ($this->{dst_xml} . "/$ts.xml");    # Borra xml
    unlink ($this->{dst_json} . "/$ts.json");    # Borra JSON

    # Borra imagenes
    $this->borrar_artic_files($this->{dst_foto});

    # Borra multimedia.
    $this->borrar_artic_files($this->{dst_multimedia});

    # borra thumbails de multimedia y asociados
    $this->borrar_artic_files($this->{dst_mm_thumbs});

    # Borra swf
    $this->borrar_artic_files($this->{dst_swf});

    # Borra archivos de MaM
    glib_fildir_03::borra_dir($this->{dst_mam});

    # Borra archivos asociados/documentos
    glib_fildir_03::borra_dir($this->{dst_asocfile});

    # Borra cache listas de articulo
    glib_fildir_03::borra_dir("$prontus_varglb::DIR_SERVER$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_CPAN'}/data/cache");

    # Regenera la tabla del DAM
    my $pathnice = lib_prontus::get_path_nice();
    $pathnice = "$pathnice -n19 " if ($pathnice);

    my $cmd = "$pathnice $prontus_varglb::DIR_SERVER/$prontus_varglb::DIR_CGI_CPAN/dam/prontus_dam_ppart_delete.cgi $ts $prontus_varglb::PRONTUS_ID $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'PUBLIC_SERVER_NAME'}  >/dev/null 2>&1 &";
    print STDERR "[" . &glib_hrfec_03::get_dtime_pack4() . "]$cmd\n";
    system $cmd;

    return '';
}

# ----------------------------------------------------------------
# Elimina los archivos y directorios asociados a un artículo dentro del directorio especificado.
sub borrar_artic_files {
    my ($this, $dir_aux) = @_;

    my $ts        = $this->{ts};
    my $dir_fecha = $this->{fechac};

    my @files2delete = glob($dir_aux . '/*' . $ts . '*');
    foreach my $entry (@files2delete) {
        if (-d $entry) {
            &glib_fildir_03::borra_dir($entry);
        } else {
            unlink $entry;
        }
    }
}

# ---------------------------------------------------------------
# Borra la fila de un artículo en la tabla ART de la base de datos.
sub art_delete_bd {
    my ($this, $base) = @_;
    # Elimina
    my $sql = "delete from ART where ART_ID = ?";
    # print STDERR "$sql\n";
    my @params = ($this->{ts});
    my $salida = &glib_dbi_03::ejecutar_sql_param($base, $sql, \@params);
    if ($base->errstr) {
        $Artic::ERR = "Error al borrar artículo, ts[$this->{ts}] Cod[$DBI::err][".$base->errstr."]\n";
        print STDERR "$Artic::ERR sql[$sql][$!][".$DBI::errstr."]";
        return 0;
    }
    return 1;
}

# ---------------------------------------------------------------
# Actualiza art. en base de datos prontus a partir de info del xml
sub art_update_bd {
    my ($this, $base) = @_;

    my %campos = $this->_get_campos_art4bd();
    my $sql = "select ART_AUTOINC from ART where ART_ID = ?";
    my $autoinc = &lib_prontus::existe_registro($sql, $base, $this->{ts});
    if ($autoinc) {
        $sql = "update ART set ART_AUTOR = ?, "
            . " ART_IDSECC1 = ?, ART_IDTEMAS1 = ?, ART_IDSUBTEMAS1 = ?,"
            . " ART_IDSECC2 = ?, ART_IDTEMAS2 = ?, ART_IDSUBTEMAS2 = ?,"
            . " ART_IDSECC3 = ?, ART_IDTEMAS3 = ?, ART_IDSUBTEMAS3 = ?,"
            . " ART_TITU = ?, ART_BAJA = ?, ART_HORAP = ?, ART_TIPOFICHA = ?, ART_ALTA = ?, ART_FECHAPHORAP = ?,"
            . " ART_IDUSR = ?, ART_FECHAP = ?, ART_FECHAE = ?, ART_HORAE = ?, ART_SOLOPORTADAS = ?,ART_FECHAEHORAE = ?,"
            . " ART_FECHA_INICIO = ?, ART_FECHA_FIN = ?, ART_MODTIME = ?, ART_S3 = ?,"
            . " ART_S3_SIZE = ?, ART_S3_MEDIA_SIZE = ?, ART_S3_ASSET_SIZE = ?,"
            . " ART_DISK_SIZE = ?, ART_MEDIA_SIZE = ?, ART_ASSET_SIZE = ?, ART_IMAG_SIZE = ?, ART_TOTAL_SIZE = ?, "
            . " ART_INDEXAR = ?, ART_PRIORIZAR = ? "
            . " where ART_ID = ? ";

        my @params = ($campos{'_autor'},
            $campos{'_seccion1'}, $campos{'_tema1'}, $campos{'_subtema1'},
            $campos{'_seccion2'}, $campos{'_tema2'}, $campos{'_subtema2'},
            $campos{'_seccion3'}, $campos{'_tema3'}, $campos{'_subtema3'},
            $campos{'_txt_titular'}, $campos{'_txt_bajada'}, $campos{'_horap'}, $campos{'_fid'}, $campos{'_alta'}, $campos{'_fechap_horap'},
            $campos{'_users_id'}, $campos{'_fechap'}, $campos{'_fechae'}, $campos{'_horae'},
            $campos{'_soloportadas'}, $campos{'_fechae_horae'}, $campos{'_fecha_ini'},
            $campos{'_fecha_fin'}, $campos{'_modtime'}, $campos{'_s3'},
            $campos{'_s3_size'}, $campos{'_s3_media_size'}, $campos{'_s3_asset_size'},
            $campos{'_disk_size'}, $campos{'_media_size'}, $campos{'_asset_size'}, $campos{'_imag_size'}, $campos{'_total_size'},
            $campos{'_indexar'}, $campos{'_priorizar'}, $this->{ts});
        my $salida = &glib_dbi_03::ejecutar_sql_param($base, $sql, \@params);
        # print STDERR "$sql\n";
        if ($base->errstr) {
            $Artic::ERR = "Error actualizando tabla de artículos, ts[$this->{ts}] Cod[$DBI::err]\n";
            print STDERR "$Artic::ERR sql[$sql][$!]";
            return 0;
        }
    } else {
        $Artic::ERR = "Error actualizando tabla de artículos, no se pudo ubicar autoinc correspondiente al TS[$this->{ts}]\n";
        return 0;
    }

    return $autoinc;
}

# ---------------------------------------------------------------
# Crea art. en base de datos prontus a partir de info del xml
sub art_insert_bd {
    # $regenerar_registro: 1 => sacar autoinc del xml
    my ($this, $base, $regenerar_registro) = @_;
    # basicamente apara regeneracion de tabla de arts.

    my %campos4bd = $this->_get_campos_art4bd();
    my @params;

    # Setear autoinc a ser usado en el insert, si corresponde
    if ($regenerar_registro) {
        if ($campos4bd{'_art_autoinc'} !~ /^\d+$/) {
            $Artic::ERR = "No es posible regenerar registro, artículo TS[$this->{ts}] sin ID autoincremento en XML.\n";
            print STDERR "$Artic::ERR $this->{fullpath_xml} ";
            return 0;
        }
        push(@params, $campos4bd{'_art_autoinc'});
    } else {
        push(@params, undef);
    }

    push(@params, $this->{ts});
    push(@params, $campos4bd{'_seccion1'});
    push(@params, $campos4bd{'_tema1'});
    push(@params, $campos4bd{'_subtema1'});
    push(@params, $campos4bd{'_seccion2'});
    push(@params, $campos4bd{'_tema2'});
    push(@params, $campos4bd{'_subtema2'});
    push(@params, $campos4bd{'_seccion3'});
    push(@params, $campos4bd{'_tema3'});
    push(@params, $campos4bd{'_subtema3'});
    push(@params, $campos4bd{'_autor'});
    push(@params, $campos4bd{'_txt_titular'});
    push(@params, $campos4bd{'_txt_bajada'});
    push(@params, $campos4bd{'_fechap'});
    push(@params, $campos4bd{'_horap'});
    push(@params, $campos4bd{'_fechap_horap'});
    push(@params, $this->{fechac});
    push(@params, $campos4bd{'_extension'});
    push(@params, $campos4bd{'_fid'});
    push(@params, $campos4bd{'_alta'});
    push(@params, $campos4bd{'_users_id'});
    push(@params, $campos4bd{'_fechae'});
    push(@params, $campos4bd{'_horae'});
    push(@params, $campos4bd{'_soloportadas'});
    push(@params, $campos4bd{'_fechae_horae'});
    push(@params, $campos4bd{'_fecha_ini'});
    push(@params, $campos4bd{'_fecha_fin'});
    push(@params, $campos4bd{'_modtime'});
    push(@params, $campos4bd{'_s3'});
    push(@params, $campos4bd{'_s3_size'});
    push(@params, $campos4bd{'_s3_media_size'});
    push(@params, $campos4bd{'_s3_asset_size'});
    push(@params, $campos4bd{'_disk_size'});
    push(@params, $campos4bd{'_media_size'});
    push(@params, $campos4bd{'_asset_size'});
    push(@params, $campos4bd{'_imag_size'});
    push(@params, $campos4bd{'_total_size'});
    push(@params, $campos4bd{'_indexar'});
    push(@params, $campos4bd{'_priorizar'});

    my $sql = "insert into ART (ART_AUTOINC, ART_ID,"
        . " ART_IDSECC1, ART_IDTEMAS1, ART_IDSUBTEMAS1,"
        . " ART_IDSECC2, ART_IDTEMAS2, ART_IDSUBTEMAS2,"
        . " ART_IDSECC3, ART_IDTEMAS3, ART_IDSUBTEMAS3,"
        . " ART_AUTOR, ART_TITU, ART_BAJA, ART_FECHAP, ART_HORAP,"
        . " ART_FECHAPHORAP, ART_DIRFECHA, ART_EXTENSION, ART_TIPOFICHA,"
        . " ART_ALTA, ART_IDUSR, ART_FECHAE, ART_HORAE, ART_SOLOPORTADAS,"
        . " ART_FECHAEHORAE, ART_FECHA_INICIO, ART_FECHA_FIN, ART_MODTIME,"
        . " ART_S3, ART_S3_SIZE, ART_S3_MEDIA_SIZE, ART_S3_ASSET_SIZE,"
        . " ART_DISK_SIZE, ART_MEDIA_SIZE, ART_ASSET_SIZE, "
        . " ART_IMAG_SIZE, ART_TOTAL_SIZE, ART_INDEXAR, ART_PRIORIZAR)"
        . " values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";

    # Realizar el insert
    if ($regenerar_registro) {
        # inserta el registro y no espera retorno
        my $salida = &glib_dbi_03::ejecutar_sql_param($base, $sql, \@params);
        if ($base->errstr) {
            $Artic::ERR = "Error insertando en tabla de artículos, ts[$this->{ts}] Cod[$DBI::err]\n";
            print STDERR "$Artic::ERR [$sql][$!][".$base->errstr."]\n";
            return 0;
        }
        # se regenera registro asi que se devuelve el autoinc del xml
        return $campos4bd{'_art_autoinc'};
    } else {
        # es articulo nuevo, debe generar nuevo autoinc
        my $autoinc = &glib_dbi_03::insert_dev_id_param($base, $sql, \@params);
        if ($base->errstr) {
            $Artic::ERR = "Error insertando en tabla de artículos, ts[$this->{ts}] Cod[$DBI::err]\n";
            print STDERR "$Artic::ERR sql[$sql][$!]  $this->{fullpath_xml} [$DBI::errstr]";
            return 0;
        }
        if (!$autoinc) {
            $Artic::ERR = "Error al generar autoinc para artículo, ts[$this->{ts}] Cod[$DBI::err]\n";
            print STDERR "$Artic::ERR sql[$sql][$!]  $this->{fullpath_xml} [$DBI::errstr]";
            return 0;
        }
        return $autoinc;
    }
}

# ---------------------------------------------------------------
# Genera vista html (o lo que corresponda a la plantilla) del articulo y graba el archivo.
# Toma como entrada los datos del XML, no los que vengan en el hash $this->{campos}.
# @param self
# @param string $mv la multivista si corresponde
# @param string $plt la plantilla a usar
# @param bool $is_paralela si es vista paralela
# @param bool $only_pproc si solamente debe ejecutar postprocesos
#
sub generar_vista_art {
    my ($this, $mv, $plt, $is_paralela, $only_pproc) = @_;

    # Carga campos
    my %campos_xml  = $this->get_xml_content();

    if ($mv eq '' && !defined($plt) && !defined($is_paralela) && !defined($only_pproc)) {
        if ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'FRIENDLY_URLS_VERSION'} eq '4') {
            # generamos el link friendly v4
            $this->genera_friendly_v4() || return $Artic::ERR;
        } elsif ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'FRIENDLY_URLS_VERSION'} eq '5') {
            # generamos el link friendly v5
            $this->genera_friendly_v5() || return $Artic::ERR;
        }
    }

    # si el articulo no esta publicado, no se generan vistas, se borran si existen
    if (!defined($only_pproc) && !$this->check_publish_art()) {
        $plt = $campos_xml{'_plt'} if (!$plt);
        # Path completo al articulo a generar
        my $fullpath_vista = $this->get_fullpath_artic($mv, $plt);

        # Si es una plantilla paralela la ruta de destino es otra.
        # pagspar y pagspar-<mv>
        # El nombre del archivo es <ts>_<nombre_plantilla>.<extension>
        if ($is_paralela) {
            $plt =~ /(.*?)\.(\w+)$/;
            my $nom_plt     = $1;
            my $ext_plt     = $2;
            $fullpath_vista =~ s/\/pags(-.+)?\/($this->{ts})\.(\w+)$/\/pagspar$1\/$2\_$nom_plt.$ext_plt/;
        }
        unlink($fullpath_vista) if (-f $fullpath_vista);

        # Solo se entra aqui si el articulo que se esta parseando no es paralelo, ya que se genería un loop infinito.
        if (!$is_paralela) {
            # Plantillas paralelas del fid.
            foreach my $plt_paralela (@{$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'FORM_PLTS'}{$campos_xml{'_fid'}}{'plt_paralela'}}) {
                $this->generar_vista_art($mv, $plt_paralela, 1);
            }
        }

        # retorna 1 porque no es un error
        return 1;
    }

    # si el artículo no debe generar vista, terminamos acá.
    if (!&lib_helper::can_have_view($campos_xml{'_fid'})) {
        return 1;
    }

    # Fix por si es que las taxonomias vienen con cero
    for (my $i = 1 ; $i <= 3 ; $i++) {
        $campos_xml{'_seccion' . $i} = '' if ($campos_xml{'_seccion' . $i} eq '0');
        $campos_xml{'_tema' . $i}    = '' if ($campos_xml{'_tema' . $i}    eq '0');
        $campos_xml{'_subtema' . $i} = '' if ($campos_xml{'_subtema' . $i} eq '0');
    }

    $plt = $campos_xml{'_plt'} if (!$plt);
    # agrega vista a datos de artículo
    $campos_xml{'_mv'} = $mv;

    # Path completo al articulo a generar
    my $fullpath_vista = $this->get_fullpath_artic($mv, $plt);

    # Si es una plantilla paralela la ruta de destino es otra.
    # pagspar y pagspar-<mv>
    # El nombre del archivo es <ts>_<nombre_plantilla>.<extension>
    if ($is_paralela) {
        $plt =~ /(.*?)\.(\w+)$/;
        my $nom_plt     = $1;
        my $ext_plt     = $2;
        my $pagspar_dir = $this->{dst_pagspar};

        $fullpath_vista =~ s/\/pags(-.+)?\/($this->{ts})\.(\w+)$/\/pagspar$1\/$2\_$nom_plt.$ext_plt/;
        $pagspar_dir = "$pagspar_dir-$mv" if ($mv);
        &glib_fildir_03::check_dir($pagspar_dir);
    }

    # Carga plantilla
    my $fullpath_plt       = "$this->{pathdir_plt_pags}/$plt";
    my $pathdir_plt_macros = $this->{pathdir_plt_macros};
    my $buffer = &lib_prontus::carga_buffer_plt($fullpath_plt, $pathdir_plt_macros, $mv);

    if ($buffer) {
        # Se parsean los loops de Articulo
        $buffer = &lib_prontus::procesa_loop_artic($buffer,'##');

        # que no estan en el xml del artic
        my %claves_adicionales;

        # Recupera marcas externas (desde /xdata)
        %claves_adicionales = $this->get_xdata($buffer);
        # Buscamos las eventuales fixed_urls en db.
        $this->get_fixed_urls(\%claves_adicionales);

        # Agrega el TS para los condicionales
        $claves_adicionales{_ts} = $this->{ts};

        # procesar loops e if de multitag
        $buffer = $this->parsea_loop_multitag($buffer, \%campos_xml, \%claves_adicionales);

        # Procesar IFs y NIFs
        $buffer = &lib_prontus::procesa_condicional($buffer, \%campos_xml, \%claves_adicionales);

        # Parsea datos del artic
        $buffer = $this->parse_artic_data($fullpath_vista, $buffer, \%campos_xml, \%claves_adicionales);

        # prontus functions
        $buffer = &lib_prontus::parser_custom_function($buffer);

        # Borra marcas no sustituidas
        $buffer =~ s/%%.+?%%//g;
        # restituyo los %% escritos por el usuario al interior de los campos
        $buffer =~ s/&#37;&#37;/%%/sg;

        if (!$mv && !$is_paralela || defined($only_pproc)) {
            # Se listan los postprocesos
            $buffer = $this->_genera_lista_post_procesos($buffer);
        }

        if (defined($only_pproc)) {
            return 1;
        }

    } else {
        my $titular = &lib_prontus::saca_tags_rets($campos_xml{'_txt_titular'});

        my $vista;
        $vista = "<br/>Vista asociada[$mv]\n" if ($mv);
        $Artic::ERR = 'Plantilla de artículo vacia o no existe, el art. ' . $this->{ts} . ' (' . $titular . ") se creó en blanco.<br/>Archivo de plantilla con problemas [" . $campos_xml{'_plt'} . "]" . $vista;
        warn "$Artic::ERR fullpath_plt[$fullpath_plt]";
        return 0;
    }

    # Check dir
    my $fulldir_vista = $this->_get_fulldir_vista($mv);
    my $dir_ret       = &glib_fildir_03::check_dir($fulldir_vista);
    if (!$dir_ret) {
        $Artic::ERR = "Error creando directorio para el artículo. TS[$this->{ts}]\n";
        print STDERR "$Artic::ERR fulldir_vista[$fulldir_vista] $!";
        return 0;
    }

    # Escribe vista del articulo
    # warn "escribiendo[$fullpath_vista]";
    &glib_fildir_03::write_file($fullpath_vista, $buffer);

    # Chequea que se haya podido crear el archivo
    if (!-f $fullpath_vista) {
        $Artic::ERR = "No se pudo crear archivo vista para el artículo. TS[$this->{ts}]\n";
        print STDERR "$Artic::ERR fullpath_vista[$fullpath_vista] $!";
        return 0;
    }

    # regenera vistas de mam
    if ($campos_xml{'_mam'} && !$mv && !$is_paralela) {
        lib_mam::regeneraVistas($this);
    }

    # Para la vista ppal,
    if (!$mv) {
        # chequea que el archivo no haya quedado vacio.
        if (!-s $fullpath_vista) {
            $Artic::ERR = "Error escribiendo archivo de la vista principal para el artículo. Quedó de largo cero. Esto puede deberse a problemas de espacio disponible, o bien, a que no se dispone de plantilla de artículo o ésta está vacía. TS[$this->{ts}] PLT[$campos_xml{'_plt'}]\n";
            print STDERR "$Artic::ERR fullpath_vista[$fullpath_vista] $!";
            return 0;
        }

        # hace purge de la vista, ya sea del path normal o friendly url, dependiendo de como este configurado.
        if (!exists $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'CACHE_PURGE_EXCLUDE_FID'}{$campos_xml{'_fid'}} && &glib_hrfec_03::get_antiguedad_ts($this->{ts}) > 60) {
            my $marca_file = &lib_prontus::remove_front_string($fullpath_vista, $this->{document_root});

            my %artic_data = ();
            $artic_data{'fid'} = $campos_xml{'_fid'};
            $artic_data{'custom_slug'} = $campos_xml{'_custom_slug'};
            $artic_data{'slug'} = $campos_xml{'_slug'};
            $artic_data{'_url'} = $campos_xml{'_url'};
            $artic_data{'nom_seccion'} = $campos_xml{'_nom_seccion1'};
            $artic_data{'nom_tema'} = $campos_xml{'_nom_tema1'};
            $artic_data{'nom_subtema'} = $campos_xml{'_nom_subtema1'};

            my $fileurl = &lib_prontus::parse_filef('%%_FILEURL%%', $campos_xml{'_txt_titular'}, $this->{ts}, $this->{prontus_id}, $marca_file, \%artic_data);
            lib_prontus::purge_cache($marca_file);
            lib_prontus::purge_cache($fileurl);
        }
    }

    # Solo se entra aqui si el articulo que se esta parseando no es paralelo, ya que se genería
    # un loop infinito.
    if (!$is_paralela) {
        # Plantillas paralelas del fid.
        foreach my $plt_paralela (@{$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'FORM_PLTS'}{$campos_xml{'_fid'}}{'plt_paralela'}}) {
            # print STDERR "plt_paralela[$plt_paralela]\n";
            $this->generar_vista_art($mv, $plt_paralela, 1);
        }
    }

    return 1;
}

# ---------------------------------------------------------------
# parsea todas las marcas del xml mas algunas derivadas de estas en el buffer dado.
# Este metodo NO elimina las marcas que queden sin sustituir.
sub parse_artic_data {
    my ($this, $fullpath_vista, $buffer, $ref_campos_xml, $vars_adicionales, $use_xml_tax) = @_;

    my %campos_xml = %$ref_campos_xml;
    undef $ref_campos_xml;
    # Fix por si es que las taxonomias vienen con cero
    for (my $i = 1 ; $i <= 3 ; $i++) {
        $campos_xml{"_seccion$i"} = '' if ($campos_xml{"_seccion$i"} eq '0');
        $campos_xml{"_tema$i"}    = '' if ($campos_xml{"_tema$i"}    eq '0');
        $campos_xml{"_subtema$i"} = '' if ($campos_xml{"_subtema$i"} eq '0');
    }

    # parsea urlf5 de taxonomia
    my ($bd, $msg_err_bd) = lib_prontus::conectar_prontus_bd();
    if (!ref($bd)) {
        print STDERR "ERROR: $msg_err_bd\n";
    }

    my $reldir_port_dst = "$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_SITE'}$prontus_varglb::DIR_TAXPORT";
    my $mv = '';
    my $aux = $buffer;

    if ($fullpath_vista =~ /\/pags\-(\w+)\/[0-9]{14}\.\w+$/) {
        $mv = $1;
    }

    # No incluye el número de página en el link friendly de la taxport.
    $buffer = &lib_tax::parse_tax_url($buffer, \%campos_xml, $mv, 0);

    unless ($use_xml_tax) {
        # Obtiene nom de secc, tema y subtema en vista correspondiente
        if ($fullpath_vista =~ /\/pags\-(\w+)\/[0-9]{14}\.\w+$/) {
            my $mv = $1;
            ($campos_xml{'_nom_seccion1'}, $campos_xml{'_nom_tema1'}, $campos_xml{'_nom_subtema1'}) = &lib_prontus::get_nom4vistas($mv, $campos_xml{'_seccion1'}, $campos_xml{'_tema1'}, $campos_xml{'_subtema1'});
        } else {
            # Obtiene nom de secc, tema y subtema vista principal.
            ($campos_xml{'_nom_seccion1'}, $campos_xml{'_nom_tema1'}, $campos_xml{'_nom_subtema1'}) = &lib_prontus::get_nom4vistas('', $campos_xml{'_seccion1'}, $campos_xml{'_tema1'}, $campos_xml{'_subtema1'});
        }
    }

    # buscamos marcas de multimedia_video adicionales que haya que parsear
    while ($buffer =~ /%%(multimedia_video\w+)\.([^\.%]+)%%/isg) {
        my $marca_video = lc $1;
        my $property = lc $2;

        # no hay video asociado a esta marca
        next if (!defined($campos_xml{$marca_video}));

        my $marca = "$marca_video.$property";
        # si esta definido el campo continuamos, se cargan en get_xml_content o en un bucle anterior
        next if (defined($campos_xml{$marca}));

        # no hay extension, no hay video original
        next if (!defined($this->{xml_content}->{"$marca_video.ext"}));

        # suponemos que es una version del video original, verifica si existe
        my $file_version = "$marca_video$this->{ts}$property.".$this->{xml_content}->{"$marca_video.ext"};
        if (!$this->{xml_content}->{'_s3' } && -f "$this->{dst_multimedia}/$file_version") {
            $campos_xml{$marca} = "/$this->{prontus_id}/site$prontus_varglb::DIR_EXMEDIA/$this->{fechac}$prontus_varglb::DIR_MMEDIA/$file_version";
        } else {
            my $found = 0;
            if (defined($this->{media_metadata}->{$marca_video}->{'qualities'})) {
                foreach my $key ( keys %{$this->{media_metadata}->{$marca_video}->{'qualities'}}) {
                    if ($this->{media_metadata}->{$marca_video}->{'qualities'}->{$key}->{'file'} =~ /$file_version$/i) {
                        $campos_xml{$marca} = $this->{media_metadata}->{$marca_video}->{'qualities'}->{$key}->{'file'};
                        $found = 1;
                    }
                }
            }

            next if ($found);
            # si no existe, poner el video original.
            $campos_xml{$marca} = "/$this->{prontus_id}/site$prontus_varglb::DIR_EXMEDIA/$this->{fechac}$prontus_varglb::DIR_MMEDIA/$campos_xml{$marca_video}";
        }
        # agrega la clave al objeto artic para no volver a revisar el disco
        $this->{xml_content}->{$marca} = $campos_xml{$marca};
    }

    # Agrega algunas vars adicionales que no vienen en el XML.
    # Es posible que estas sobreescriban las del XML
    if (ref $vars_adicionales) {
        foreach my $k (keys %$vars_adicionales) {
            $campos_xml{lc $k} = $$vars_adicionales{$k};
        }
        undef $vars_adicionales;
    }

    # parsea tags de articulo
    my @id_tags = split("," , $campos_xml{'_tags'});
    my @name_tags = split("," , $campos_xml{'_tagnames'});
    my $tag_count = scalar @name_tags;
    my %tagsnomhash = lib_tags::get_tags_noms_hash($bd, $campos_xml{'_tags'});

    # Cargar json de tags.
    my $tags_path = "$prontus_varglb::DIR_SERVER$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_CPAN'}$prontus_varglb::DIR_DATA4FIDS/tags.json";
    my @tags_array;

    if ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'FRIENDLY_URLS_VERSION'} eq '5') {
        if (!scalar keys %lib_prontus::TAGS_HASH) {
            if (-f $tags_path) {
                my $tags_buffer = &glib_fildir_03::read_file_bin($tags_path);

                if ($tags_buffer =~ /^\[.*\]$/) {
                    my $arr_ref = decode_json($tags_buffer);
                    @tags_array = @{$arr_ref};

                    foreach my $hashref (@tags_array) {
                        if (exists $hashref->{'urls'}) {
                            $lib_prontus::TAGS_HASH{$hashref->{'id'}} = $hashref->{'urls'};
                        }
                    }
                }
            }
        }
    }

    while ($aux =~ /%%_loop_tags%%(.*?)%%\/_loop_tags%%/isg) {
        my $loop   = $1;
        my $totloop = '';

        for (my $i = 0; $i < $tag_count; $i++) {
            my $auxloop = $loop;
            my $custom_url = &lib_tags::get_tag_url($id_tags[$i]);
            my $tag_nom = $tagsnomhash{$id_tags[$i]}{''};
            my $tag_nom_vista = $tagsnomhash{$id_tags[$i]}{$mv};

            $tag_nom_vista = $tag_nom if ($tag_nom_vista eq '');

            $auxloop =~ s/%%_fixed_tag_url%%/$custom_url/ig;
            $auxloop =~ s/%%_tag_id%%/$id_tags[$i]/ig;
            $auxloop =~ s/%%_tag_nom%%/$tag_nom/ig;
            $auxloop =~ s/%%_tag_nom_vista%%/$tag_nom_vista/ig;

            if ($auxloop =~ /(%%_tag_url\(['"](.*?)['"],\s*['"](.*?)['"]\)%%)/) {
                my $tag_func = $1;
                my $tag_filtro = $2;
                my $tag_plt = $3;

                # print STDERR "tag_filtro[$tag_filtro] tag_plt[$tag_plt]\n";
                my $old_tag_url = "$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_SITE'}/tag/port/$tag_filtro/$tag_plt\_$id_tags[$i]_1.html";

                if (exists $lib_prontus::TAGS_HASH{$id_tags[$i]}) {
                    my %tags_urls = %{$lib_prontus::TAGS_HASH{$id_tags[$i]}};
                    my @hashkeys = keys %tags_urls;

                    if (scalar @hashkeys) {
                        my $url_found = 0;
                        foreach my $tag_url (keys %tags_urls) {
                            # print STDERR "tag_url[$tag_url] -> [$tags_urls{$tag_url}]\n";
                            if ($tag_url =~ /^\/site\/tag\/port\/$tag_filtro\/$tag_plt\_/) {
                                if ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'FRIENDLY_INCLUDE_VIEW_NAME'} eq 'SI' && $mv ne '') {
                                    $auxloop =~ s/\Q$tag_func\E/$mv$tags_urls{$tag_url}/sg;
                                } else {
                                    $auxloop =~ s/\Q$tag_func\E/$tags_urls{$tag_url}/sg;
                                }

                                $url_found = 1;

                                last;
                            }
                        }

                        if (!$url_found) {
                            $auxloop =~ s/\Q$tag_func\E/$old_tag_url/sg;
                        }

                    } else {
                        $auxloop =~ s/\Q$tag_func\E/$old_tag_url/sg;
                    }
                } else {
                    $auxloop =~ s/\Q$tag_func\E/$old_tag_url/sg;
                }
            }

            $totloop .=  $auxloop;
        }

        $buffer =~ s/%%_loop_tags%%\Q$loop\E%%\/_loop_tags%%/$totloop/is;
    }

    # Parsea campos
    foreach my $nom_campo (keys %campos_xml) {
        my $val_campo = $campos_xml{$nom_campo};
        next if (!defined($val_campo) or $val_campo eq '');
        # evitar que s/t/st se guarde con 0.
        next if ($nom_campo =~ /^(_seccion|_tema|_subtema)/ && $val_campo eq '0');
        next if ($nom_campo =~ /^_fecha(p|e)$/);
        next if ($nom_campo =~ /^chk_cuadrar_fotofija|^_nomfoto_|^_wfoto_|^_hfoto_|^foto_\d+/);

        if ($nom_campo =~ /^vtxt_/i) {
            # warn "[$nom_campo][$val_campo]";
            $buffer = $this->_parsing_vtxt($buffer, $nom_campo, $val_campo);

        } elsif ($nom_campo =~ /^(asocfile|multimedia)(_video|_audio)?/i) {
            # tipo basico (asocfile|multimedia)
            my $type = $1;
            my $use_s3 = 0;
            $use_s3 = 1 if ($campos_xml{'_s3'});

            # tipo compuesto (multimedia_video|multimedia_audio)
            $type = "$1$2" if ($type eq 'multimedia' && defined($2));

            $buffer = $this->_parsing_recursos($nom_campo, $val_campo, $buffer, $type, $use_s3);

        } elsif ($nom_campo =~ /^fotofija_/i) {
            $buffer = $this->_parsing_fotos($nom_campo, $val_campo, $buffer);

        } else {
            # Replace en artic, incluye minitext
            $buffer = &lib_prontus::replace_in_artic($val_campo, $nom_campo, $buffer);
        }
    }

    # Parseos especiales para fechas y horas
    $buffer = &lib_prontus::artic_parser_fechas($buffer, $campos_xml{'_fechap'}, $campos_xml{'_fechae'});

    # Replace en artic el UTC de publicacion
    my $fechap_horap_iso = $campos_xml{'_fechap'} . &lib_prontus::get_hora_iso($campos_xml{'_horap'});
    my $utc_pub          = &lib_prontus::get_dtime_rfc822($fechap_horap_iso);
    $buffer = &lib_prontus::replace_in_artic($utc_pub, '_utcp', $buffer);

    # Reemplaza TS, FECHAC, FECHACLONG, FECHACSHRT
    $buffer = &lib_prontus::replace_tsdata($buffer, $this->{ts});

    # reemplaza %%_fechamlong%% - %%_horam%%
    $buffer = lib_prontus::replace_mtime($buffer, $this->{fullpath_xml},$campos_xml{'_modtime'});

    # Parsea nombre y usuario.
    $buffer = &lib_prontus::parse_userinfo($buffer, $campos_xml{'_users_id'});

    # Marca _FILE y _FILEURL
    my $marca_file = $fullpath_vista;
    $marca_file = &lib_prontus::remove_front_string($marca_file, $this->{document_root});
    $buffer =~ s/%%_FILE%%/$marca_file/ig;

    my %artic_data = ();
    $artic_data{'fid'} = $campos_xml{'_fid'};
    $artic_data{'custom_slug'} = $campos_xml{'_custom_slug'};
    $artic_data{'slug'} = $campos_xml{'_slug'};
    $artic_data{'_url'} = $campos_xml{'_url'};
    $artic_data{'nom_seccion'} = $campos_xml{'_nom_seccion1'};
    $artic_data{'nom_tema'} = $campos_xml{'_nom_tema1'};
    $artic_data{'nom_subtema'} = $campos_xml{'_nom_subtema1'};

    $buffer = &lib_prontus::parse_filef($buffer, $campos_xml{'_txt_titular'}, $this->{ts}, $this->{prontus_id}, $marca_file, \%artic_data);

    # parsear globales de prontus
    $buffer = &lib_prontus::parse_globals($buffer, '');

    $buffer = &lib_prontus::parse_includes($this->{document_root}, $buffer);

    # Elimina marcas no parseadas del VTXT.
    $buffer =~ s/%%_SUBTIT_(.*?)%%(.*?)%%\/_SUBTIT_\1%%//isg;

    return $buffer;
}

# ---------------------------------------------------------------
sub parsea_loop_multitag {
    my $this       = $_[0];
    my $buffer     = $_[1];
    my %campos_xml = %{$_[2]};
    my %vars_adicionales = %{$_[3]};

    &lib_prontus::carga_multitag_json();
    my $aux = $buffer;
    my @multitag_subtema = split("," , $campos_xml{'_multitag_subtema'});
    # busca todos los loops, se usa aux para no interferir con el contador interno de la regexp
    while ($aux =~ /%%_loop_multitag_subtema%%(.*?)%%\/_loop_multitag_subtema%%/isg) {
        my $loop   = $1;
        my $totloop = '';

        foreach my $id (@multitag_subtema) {
            my $auxloop = $loop;

            $auxloop =~ s/%%_multitag_subtema_id%%/$id/ig;
            $auxloop =~ s/%%_multitag_subtema_nom%%/$lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'subtema'}{$id}{'name'}/ig;
            $auxloop =~ s/%%_multitag_subtema_friendly%%/$lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'subtema'}{$id}{'friendly'}/ig;

            # hay un if dentro del loop
            if (index($auxloop, 'if') > -1) {
                my %data_loop = %vars_adicionales;
                $data_loop{'_multitag_subtema_id'} = $id;
                $data_loop{'_multitag_subtema_nom'} = $lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'subtema'}{$id}{'name'};
                $data_loop{'_multitag_subtema_friendly'} = $lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'subtema'}{$id}{'friendly'};
                $auxloop = &lib_prontus::procesa_condicional($auxloop, \%campos_xml, \%data_loop);
            }

            $totloop .=  $auxloop;
        }
        # reemplaza la primera ocurrencia, se asume que los loops no se repiten exactamente iguales
        $buffer =~ s/%%_loop_multitag_subtema%%\Q$loop\E%%\/_loop_multitag_subtema%%/$totloop/is;
    }

    $aux = $buffer;
    my @multitag_tema = split("," , $campos_xml{'_multitag_tema'});
    while ($aux =~ /%%_loop_multitag_tema%%(.*?)%%\/_loop_multitag_tema%%/isg) {
        my $loop   = $1;
        my $totloop = '';

        foreach my $id (@multitag_tema) {
            my $auxloop = $loop;

            $auxloop =~ s/%%_multitag_tema_id%%/$id/ig;
            $auxloop =~ s/%%_multitag_tema_nom%%/$lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'tema'}{$id}{'name'}/ig;
            $auxloop =~ s/%%_multitag_tema_friendly%%/$lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'tema'}{$id}{'friendly'}/ig;

            # hay un if dentro del loop
            if (index($auxloop, 'if') > -1) {
                my %data_loop = %vars_adicionales;
                $data_loop{'_multitag_tema_id'} = $id;
                $data_loop{'_multitag_tema_nom'} = $lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'tema'}{$id}{'name'};
                $data_loop{'_multitag_tema_friendly'} = $lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'tema'}{$id}{'friendly'};
                $auxloop = &lib_prontus::procesa_condicional($auxloop, \%campos_xml, \%data_loop);
            }

            $totloop .=  $auxloop;
        }
        $buffer =~ s/%%_loop_multitag_tema%%\Q$loop\E%%\/_loop_multitag_tema%%/$totloop/is;
    }

    $aux = $buffer;
    my @multitag_seccion_ids = split("," , $campos_xml{'_multitag_seccion'});
    # Se parsean los loops de multitag
    while ($aux =~ /%%_loop_multitag_seccion%%(.*?)%%\/_loop_multitag_seccion%%/isg) {
        my $loop   = $1;
        my $totloop = '';

        foreach my $id (@multitag_seccion_ids) {
            my $auxloop = $loop;

            $auxloop =~ s/%%_multitag_seccion_id%%/$id/ig;
            $auxloop =~ s/%%_multitag_seccion_nom%%/$lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'seccion'}{$id}{'name'}/ig;
            $auxloop =~ s/%%_multitag_seccion_friendly%%/$lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'seccion'}{$id}{'friendly'}/ig;

            # hay un if dentro del loop
            if (index($auxloop, 'if') > -1) {
                my %data_loop = %vars_adicionales;
                $data_loop{'_multitag_seccion_id'} = $id;
                $data_loop{'_multitag_seccion_nom'} = $lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'seccion'}{$id}{'name'};
                $data_loop{'_multitag_seccion_friendly'} = $lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'seccion'}{$id}{'friendly'};

                $auxloop = &lib_prontus::procesa_condicional($auxloop, \%campos_xml, \%data_loop);
            }

            $totloop .=  $auxloop;
        }
        $buffer =~ s/%%_loop_multitag_seccion%%\Q$loop\E%%\/_loop_multitag_seccion%%/$totloop/is;
    }

    $aux = $buffer;
    # parsea todas las multitag
    @multitag_subtema = keys  %{$lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'subtema'}};
    while ($aux =~ /%%_loop_all_multitag_subtema%%(.*?)%%\/_loop_all_multitag_subtema%%/isg) {
        my $loop   = $1;
        my $totloop = '';

        foreach my $id (@multitag_subtema) {
            my $auxloop = $loop;

            $auxloop =~ s/%%_multitag_subtema_id%%/$id/ig;
            $auxloop =~ s/%%_multitag_subtema_nom%%/$lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'subtema'}{$id}{'name'}/ig;
            $auxloop =~ s/%%_multitag_subtema_friendly%%/$lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'subtema'}{$id}{'friendly'}/ig;

            # hay un if dentro del loop
            if (index($auxloop, 'if') > -1) {
                my %data_loop = %vars_adicionales;
                $data_loop{'_multitag_subtema_id'} = $id;
                $data_loop{'_multitag_subtema_nom'} = $lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'subtema'}{$id}{'name'};
                $data_loop{'_multitag_subtema_friendly'} = $lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'subtema'}{$id}{'friendly'};
                $auxloop = &lib_prontus::procesa_condicional($auxloop, \%campos_xml, \%data_loop);
            }

            $totloop .=  $auxloop;
        }
        $buffer =~ s/%%_loop_all_multitag_subtema%%\Q$loop\E%%\/_loop_all_multitag_subtema%%/$totloop/is;
    }

    $aux = $buffer;
    @multitag_tema = keys %{$lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'tema'}};
    while ($aux =~ /%%_loop_all_multitag_tema%%(.*?)%%\/_loop_all_multitag_tema%%/isg) {
        my $loop   = $1;
        my $totloop = '';

        foreach my $id (@multitag_tema) {
            my $auxloop = $loop;

            $auxloop =~ s/%%_multitag_tema_id%%/$id/ig;
            $auxloop =~ s/%%_multitag_tema_nom%%/$lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'tema'}{$id}{'name'}/ig;
            $auxloop =~ s/%%_multitag_tema_friendly%%/$lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'tema'}{$id}{'friendly'}/ig;

            # hay un if dentro del loop
            if (index($auxloop, 'if') > -1) {
                my %data_loop = %vars_adicionales;
                $data_loop{'_multitag_tema_id'} = $id;
                $data_loop{'_multitag_tema_nom'} = $lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'tema'}{$id}{'name'};
                $data_loop{'_multitag_tema_friendly'} = $lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'tema'}{$id}{'friendly'};
                $auxloop = &lib_prontus::procesa_condicional($auxloop, \%campos_xml, \%data_loop);
            }

            $totloop .=  $auxloop;
        }
        $buffer =~ s/%%_loop_all_multitag_tema%%\Q$loop\E%%\/_loop_all_multitag_tema%%/$totloop/is;
    }

    $aux = $buffer;
    @multitag_seccion_ids = keys %{$lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'seccion'}};
    while ($aux =~ /%%_loop_all_multitag_seccion%%(.*?)%%\/_loop_all_multitag_seccion%%/isg) {
        my $loop   = $1;
        my $totloop = '';

        foreach my $id (@multitag_seccion_ids) {
            my $auxloop = $loop;

            $auxloop =~ s/%%_multitag_seccion_id%%/$id/ig;
            $auxloop =~ s/%%_multitag_seccion_nom%%/$lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'seccion'}{$id}{'name'}/ig;
            $auxloop =~ s/%%_multitag_seccion_friendly%%/$lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'seccion'}{$id}{'friendly'}/ig;

            # hay un if dentro del loop
            if (index($auxloop, 'if') > -1) {
                my %data_loop = %vars_adicionales;
                $data_loop{'_multitag_seccion_id'} = $id;
                $data_loop{'_multitag_seccion_nom'} = $lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'seccion'}{$id}{'name'};
                $data_loop{'_multitag_seccion_friendly'} = $lib_prontus::MULTITAG{$prontus_varglb::PRONTUS_ID}{'seccion'}{$id}{'friendly'};

                $auxloop = &lib_prontus::procesa_condicional($auxloop, \%campos_xml, \%data_loop);
            }

            $totloop .=  $auxloop;
        }
        $buffer =~ s/%%_loop_all_multitag_seccion%%\Q$loop\E%%\/_loop_all_multitag_seccion%%/$totloop/is;
    }

    return $buffer;
}

# ---------------------------------------------------------------
# Obtiene hash con las marcas externas (y su contenido) desde el directorio /<prontus_id>/xdata
# No permite marcas q comiencen con _ para evitar conflictos con las reservadas.
# Las marcas que se carguen por esta via sobreescribiran las marcas del articulo.
# Estructura del dir:
# <prontus_id>/
#     xdata/
#        <sistema_externo>/
#           <nom_marca_grl1>.txt
#           ...
#           <nom_marca_grlN>.txt
#           <ts>/
#               <nom_marca1>.txt
#               ...
#               <nom_marcaN>.txt
sub get_xdata {
    my ($this) = shift;

    my (%xdata, $nom_marca, $valor_marca);

    my ($pathdir_xdata) = "$this->{document_root}/$this->{prontus_id}/xdata";
    return %xdata if (!-d $pathdir_xdata);

    my @lisdir = &glib_fildir_03::lee_dir($pathdir_xdata);
    @lisdir = grep !/^\./, @lisdir;    # Elimina directorios . y ..

    if (!exists $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'globalxdata'}) {
        $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'globalxdata'} = ();
    }

    foreach my $sist_externo (@lisdir) {
        next if (!-d "$pathdir_xdata/$sist_externo");    # solo lee directorios

        # Detecta marcas comunes a todos los articulos
        if (!keys %{$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'globalxdata'}}) {
            # Si no está cacheado, lee el directorio y busca las marcas
            my @lisdirext = &glib_fildir_03::lee_dir("$pathdir_xdata/$sist_externo");
            @lisdirext = grep !/^\./, @lisdirext;        # Elimina directorios . y ..
            foreach my $k (@lisdirext) {
                if ((-f "$pathdir_xdata/$sist_externo/$k") && ($k =~ /^(\w+)\.txt$/i)) {
                    $nom_marca = $1;
                    next if ($nom_marca =~ /^_/);        # descarta las q comienzan con _
                    $valor_marca = &glib_fildir_03::read_file("$pathdir_xdata/$sist_externo/$k");

                    if (defined $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'globalxdata'}{$nom_marca}) {
                        print STDERR "Conflicto con marca externa comun!! [$nom_marca]\n. Se sobreescribe.\n";
                    }

                    $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'globalxdata'}{$nom_marca} = $valor_marca;
                }
            }
        }

        # Detecta marcas especificas para este articulo
        my $fechac = $this->{fechac};
        my $path   = '';
        if (-d "$pathdir_xdata/$sist_externo/$this->{ts}") {
            $path = "$pathdir_xdata/$sist_externo/$this->{ts}";

        } elsif (-d "$pathdir_xdata/$sist_externo/$fechac/$this->{ts}") {
            $path = "$pathdir_xdata/$sist_externo/$fechac/$this->{ts}";
        }
        if ($path) {
            my @lisdir_ts = &glib_fildir_03::lee_dir($path);
            @lisdir_ts = grep !/^\./, @lisdir_ts;    # Elimina directorios . y ..
            foreach my $marca (@lisdir_ts) {
                next if ($marca =~ /^_/);            # descarta las q comienzan con _
                if ((-f "$path/$marca") && ($marca =~ /^(\w+)\.txt$/i)) {
                    $nom_marca         = $1;
                    $valor_marca       = &glib_fildir_03::read_file("$path/$marca");
                    $xdata{$nom_marca} = $valor_marca;
                }
            }
        }
    }

    # Incluir marcas comunes.
    foreach my $marca_comun (keys %{$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'globalxdata'}}) {
        if (!defined $xdata{$marca_comun}) {
            $xdata{$marca_comun} = $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'globalxdata'}{$marca_comun};
        } else {
            print STDERR "Nombre de marca comun [$marca_comun] existe como especifica, no se sobreescribe!\n";
        }
    }

    return %xdata;
};

# ---------------------------------------------------------------
# Ingresa o actualiza en el xml el id autoincremental de base de datos a partir del valor pasado.
sub setear_autoinc {
    my ($this, $autoinc) = @_;

    my $buffer = &glib_fildir_03::read_file($this->{fullpath_xml});
    $buffer =~ s/<_art_autoinc>.*?<\/_art_autoinc>/<_art_autoinc>$autoinc<\/_art_autoinc>/is;
    $this->{xml_data} =~ s/<_art_autoinc>.*?<\/_art_autoinc>/<_art_autoinc>$autoinc<\/_art_autoinc>/is;

    &glib_fildir_03::write_file($this->{fullpath_xml}, $buffer);

    $this->{xml_content}->{'_art_autoinc'} = $autoinc;
}

# ---------------------------------------------------------------
# Recibe una ref a la base de datos, y un comando (pub|exp).
# Hace un insert o update de los datos en la tabla ART_PUBLICAR o ART_DESPUBLICAR, según el comando pasado.
# Si la fecha pasada es anterior a la fecha actual, intenta hacer un delete de la misma tabla.
sub guardar_pub_exp_artic {
    my ($this, $base, $comando) = @_;
    my %campos = $this->get_xml_content();
    my ($tabla, $fechahora, $colfecha, $colhora, $colfechahora, $sql, $res);
    my $ts_now = &glib_hrfec_03::get_dtime_pack4();

    if ($comando eq 'pub') {
        $tabla = 'ART_PUBLICAR';
        $colfechahora = 'ART_FECHAPHORAP';
        if ($campos{'_fechap'} eq '99999999') {
            return 1;
        }
        $fechahora = $campos{'_fechap'} . $campos{'_horap'} . '00';
    } elsif ($comando eq 'exp') {
        $tabla = 'ART_DESPUBLICAR';
        $colfechahora = 'ART_FECHAEHORAE';
        if ($campos{'_fechae'} eq '99999999') {
            return 1;
        }
        $fechahora = $campos{'_fechae'} . $campos{'_horae'} . '00';
    }

    $fechahora =~ s/://g;

    my @params;
    if ($fechahora > $ts_now) {
        # Intenta insertar. Si los datos ya existían en la db, se actualizan.
        $sql = "INSERT INTO $tabla (ART_ID, $colfechahora) VALUES (?, ?) ON DUPLICATE KEY UPDATE $colfechahora = ?";
        @params = ($this->{ts}, $fechahora, $fechahora);
    } else {
        # si la fecha pasada es igual o anterior a la fecha actual, borro los eventuales registros que hayan en db.
        $sql = "DELETE FROM $tabla WHERE ART_ID = ?";
        @params = ($this->{ts});
    }

    my $salida = &glib_dbi_03::ejecutar_sql_param($base, $sql, \@params);
    if ($base->errstr) {
        $Artic::ERR = "Error actualizando tabla $tabla, ts[$this->{ts}]\n";
        print STDERR "$Artic::ERR sql[$sql][$!]";
        return 0;
    }

    return 1;
}

# ---------------------------------------------------------------
# Recibe una ref a la base de datos y un comando (pub|exp).
# Borra todos los registros en la tabla ART_PUBLICAR o ART_DESPUBLICAR con fecha anterior a la actual, según el comando pasado.
sub borrar_pub_exp_artic {
    my ($this, $base, $comando) = @_;
    my ($tabla, $colfechahora);
    my $ts_now = &glib_hrfec_03::get_dtime_pack4();

    if ($comando eq 'pub') {
        $tabla = 'ART_PUBLICAR';
        $colfechahora = 'ART_FECHAPHORAP';
    } elsif ($comando eq 'exp') {
        $tabla = 'ART_DESPUBLICAR';
        $colfechahora = 'ART_FECHAEHORAE';
    }

    my $sql = "DELETE FROM $tabla WHERE $colfechahora < ?";
    my @params =($ts_now);

    my $salida = &glib_dbi_03::ejecutar_sql_param($base, $sql, \@params);
    if ($base->errstr) {
        $Artic::ERR = "Error borrando de tabla $tabla, ts[$this->{ts}]\n";
        print STDERR "$Artic::ERR sql[$sql][$!]";
        return 0;
    }

    return 1;
}

# ---------------------------------------------------------------
# Recibe referencias a los arrays claves_adicionales y campos_xml.
# Modifica el array claves_adicionales agregando las claves url_fixed_* para la tripleta 1.
sub get_fixed_urls {
    my ($this, $ref_claves_adicionales) = @_;

    my $nivel = 1;
    $this->get_xml_content();

    while ($nivel <= $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'TAXONOMIA_NIVELES'}) {
        my ($url_seccion, $url_tema, $url_subtema)= &lib_tax::get_fixed_urls($this->{'xml_content'}->{"_seccion$nivel"},
                                $this->{'xml_content'}->{"_tema$nivel"}, $this->{'xml_content'}->{"_subtema$nivel"});
        $ref_claves_adicionales->{"_url_fixed_seccion$nivel"} = "$url_seccion";
        $ref_claves_adicionales->{"_url_fixed_tema$nivel"} = "$url_tema";
        $ref_claves_adicionales->{"_url_fixed_subtema$nivel"} = "$url_subtema";
        $nivel++;
    }
}

# ---------------------------------------------------------------
# METODOS PRIVADOS
# ---------------------------------------------------------------
# Lee el path en dst_pags y retorna el mismo path pero adaptado a la vista pasada.
sub _get_fulldir_vista {
    my ($this, $mv) = @_;

    my $fulldir_vista = $this->{dst_pags};
    # warn "fulldir_vista1[$fulldir_vista]";
    $fulldir_vista =~ s/(\/site\/artic\/\d{8}\/pags)/$1-$mv/ if ($mv);
    # warn "fulldir_vista2[$fulldir_vista] mv[$mv]";

    return $fulldir_vista;
}
# ---------------------------------------------------------------
# Configura directorios de trabajo del articulo.
sub _set_dirs {
    my $this = shift;

    $this->{dst_base}       = $this->{document_root} . $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_SITE'} . $prontus_varglb::DIR_ARTIC . "/$this->{fechac}";
    $this->{dst_base_mm}    = $this->{document_root} . $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_SITE'} . $prontus_varglb::DIR_EXMEDIA . "/$this->{fechac}";
    $this->{dst_base_docs}  = $this->{document_root} . $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_SITE'} . $prontus_varglb::DIR_ASOCFILE ."/$this->{fechac}";
    $this->{dst_bkp}        = $this->{document_root} . $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_SITE'} . $prontus_varglb::DIR_BKP ."/$this->{fechac}";

    $this->{dst_preview}    = $this->{document_root} . $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'DIR_SITE'}. "/preview";
    $this->{dst_xml}        = "$this->{dst_base}/xml";
    $this->{dst_json}       = "$this->{dst_base}/json";
    $this->{dst_pags}       = "$this->{dst_base}/pags";
    $this->{dst_pagspar}    = "$this->{dst_base}/pagspar";
    $this->{dst_asocfile}   = "$this->{dst_base_docs}/$this->{ts}";
    $this->{dst_foto}       = "$this->{dst_base}/imag";
    $this->{dst_swf}        = "$this->{dst_base}/swf";
    $this->{dst_multimedia} = "$this->{dst_base_mm}$prontus_varglb::DIR_MMEDIA";
    $this->{dst_mm_thumbs}  = "$this->{dst_base_mm}$prontus_varglb::DIR_THUMBS";
    $this->{dst_mam}        = "$this->{dst_base}$prontus_varglb::DIR_MAM/$this->{ts}";

    if ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'FRIENDLY_URLS_VERSION'} eq '4') {
        $this->{dst_links_url} = $this->{document_root} . "/$this->{prontus_id}" . "/site/friendly/links";
    }

    $this->{pathdir_plt}        = $this->{document_root} . "/$this->{prontus_id}" . "/plantillas/artic/fecha";
    $this->{pathdir_plt_macros} = "$this->{pathdir_plt}/macros";
    $this->{pathdir_plt_pags}   = "$this->{pathdir_plt}/pags";

    if (!$this->{no_check_dirs}) {
        if (!$this->_check_artic_dirs()) {
            $Artic::ERR = "No se pueden crear directorios del artículo: [$Artic::ERR]";
            print STDERR "$Artic::ERR [$!]\n";
            return 0;
        }
    }

    return 1;
}

# ---------------------------------------------------------------
# Configura los directorios de trabajo del articulo preview.
sub _set_dirs_preview {
    my $this = shift;

    # Directorio principal del preview.
    &glib_fildir_03::check_dir($this->{dst_preview});

    # Misma estructura que el artículo normal.
    $this->{dst_xml}        = "$this->{dst_preview}/$this->{fechac}/xml";
    $this->{dst_pags}       = "$this->{dst_preview}/$this->{fechac}/pags";
    $this->{dst_pagspar}    = "$this->{dst_preview}/$this->{fechac}/pagspar";
    $this->{dst_asocfile}   = "$this->{dst_preview}/$this->{fechac}/docs/$this->{ts}";
    $this->{dst_foto}       = "$this->{dst_preview}/$this->{fechac}/imag";
    # La multimedia externa y thumbs se dejan en el mismo directorio del preview.
    $this->{dst_multimedia} = "$this->{dst_preview}$prontus_varglb::DIR_MMEDIA";
    $this->{dst_mm_thumbs}  = "$this->{dst_preview}$prontus_varglb::DIR_THUMBS";

    if (!$this->_check_artic_dirs()) {
        $Artic::ERR = "No se pueden crear directorios del artículo: [$Artic::ERR]";
        print STDERR "$Artic::ERR [$!]\n";
        return 0;
    }

    return 1;
}

# ---------------------------------------------------------------
# Revisa la existencia de todos los directorios necesarios para la creación del artículo.
sub _check_artic_dirs {
    my $this = shift;
    unless(&glib_fildir_03::check_dir($this->{dst_xml})) {
        $ERR = $this->{dst_xml};
        return 0;
    }
    unless(&glib_fildir_03::check_dir($this->{dst_json})) {
        $ERR = $this->{dst_json};
        return 0;
    }
    unless(&glib_fildir_03::check_dir($this->{dst_pags})) {
        $ERR = $this->{dst_pags};
        return 0;
    }
    unless(&glib_fildir_03::check_dir($this->{dst_pagspar})) {
        $ERR = $this->{dst_pagspar};
        return 0;
    }
    unless(&glib_fildir_03::check_dir($this->{dst_asocfile})) {
        $ERR = $this->{dst_asocfile};
        return 0;
    }
    unless(&glib_fildir_03::check_dir($this->{dst_foto})) {
        $ERR = $this->{dst_foto};
        return 0;
    }
    unless(&glib_fildir_03::check_dir($this->{dst_multimedia})) {
        $ERR = $this->{dst_multimedia};
        return 0;
    }
    unless(&glib_fildir_03::check_dir($this->{dst_mm_thumbs})) {
        $ERR = $this->{dst_mm_thumbs};
        return 0;
    }
    unless(&glib_fildir_03::check_dir($this->{dst_bkp})) {
        $ERR = $this->{dst_bkp};
        return 0;
    }

    if ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'FRIENDLY_URLS_VERSION'} eq '4') {
        unless (&glib_fildir_03::check_dir($this->{dst_links_url})) {
            $ERR = $this->{dst_links_url};
            return 0;
        }
    }
    return 1;
}

sub write_json {
    my $this = $_[0];

    %{$this->{xml_content}} = ();
    my %campos_xml = $this->get_xml_content();

    # Agregar marca _ts
    $campos_xml{"_ts"} = $this->{ts};

    # Agregar marca _fileurl
    $campos_xml{"_fileurl"} = $this->get_friendly_url();

    foreach my $key (keys %campos_xml) {
        if ($key =~ /^vtxt_/i) {
            my $vtxtBuffer = $campos_xml{$key};
            # Quitar de VTXT prontus_card.
            $vtxtBuffer =~ s/<prontus_card(.*?)>.*?<\/prontus_card>//isg;
            # Parsear vtxt.
            $campos_xml{$key} = $this->_parsing_vtxt("%%$key%%", $key, $vtxtBuffer);
        }

        if ($key =~ /^fotofija_/i) {
            $campos_xml{$key} =~ /(foto_\d+)$this->{ts}/;
            my $fotoNom = $1;

            $campos_xml{"_w$key"} = int($campos_xml{"_w$fotoNom"});
            $campos_xml{"_h$key"} = int($campos_xml{"_h$fotoNom"});
        }
    }

    foreach my $key (keys %campos_xml) {
        delete $campos_xml{$key} if ($key =~ /^foto_/);
        delete $campos_xml{$key} if ($key =~ /^_ofoto_/);
        delete $campos_xml{$key} if ($key =~ /^_nomfoto/);
        delete $campos_xml{$key} if ($key =~ /^_bfoto/);
        delete $campos_xml{$key} if ($key =~ /^_wfoto_/);
        delete $campos_xml{$key} if ($key =~ /^_hfoto_/);
        delete $campos_xml{$key} if ($key =~ /^_mfoto/);
        delete $campos_xml{$key} if ($key =~ /^_galeria/);
        delete $campos_xml{$key} if ($key =~ /^chk_cuadrar_fotofija/);
    }

    delete $campos_xml{"_disk_size"};
    delete $campos_xml{"_priorizar"};
    delete $campos_xml{"_plt"};
    # delete $campos_xml{"_fid"};
    delete $campos_xml{"_txt_galeria_credito_total"};
    delete $campos_xml{"_txt_galeria_description_total"};
    delete $campos_xml{"_autor"};
    delete $campos_xml{"_alta"};
    delete $campos_xml{"_soloportadas"};
    delete $campos_xml{"_users_id"};
    delete $campos_xml{"_indexar"};
    delete $campos_xml{"_s3_size"};
    delete $campos_xml{"_asset_size"};
    delete $campos_xml{"_s3_media_size"};
    delete $campos_xml{"_s3_asset_size"};
    delete $campos_xml{"_s3"};
    delete $campos_xml{"_media_size"};

    glib_fildir_03::check_dir($this->{dst_json});

    &glib_fildir_03::write_file_bin($this->{fullpath_json}, JSON::encode_json(\%campos_xml));
}

# ---------------------------------------------------------------
# Escribe a disco el XML del artículo
sub _flush_xml {
    my $this = $_[0];

    # comparamos el md5 del xml existente, vs lo que se guardara
    # para determinar si hay que hacer un respaldo
    my $old_xml = &glib_fildir_03::read_file($this->{fullpath_xml});
    # xml existe y no es vacio
    if ($old_xml ne ''){
        # modtime se quita ya que cambia al guardar
        $old_xml =~ s/<_modtime>\d+<\/_modtime>//;
        utf8::encode($old_xml);
        my $old_md5 = Digest::MD5::md5_hex($old_xml);

        my $aux = $this->{xml_data};
        $aux =~ s/<_modtime>\d+<\/_modtime>//;
        utf8::encode($aux);
        my $new_md5 = Digest::MD5::md5_hex($aux);
        # si son diferentes se crea respaldo
        if ($new_md5 ne $old_md5) {
            #respaldamos el xml anterior antes de sobreescribirlo
            # se copia  y verifica que no ocurrio un error al copiar
            if (copy($this->{fullpath_xml}, "$this->{dst_bkp}/$this->{ts}.xml." . time) == 0) {
                $Artic::ERR = 'Error al crear respaldo del XML  del artículo [' . $this->{fullpath_xml} . "]->[$this->{dst_bkp}/$this->{ts}.xml." . time .']';
                print STDERR "$Artic::ERR [$!]";
                return 0;
            }

            # No hubo error al crear el respaldo, guarda XML nuevo
            &glib_fildir_03::write_file($this->{fullpath_xml}, $this->{xml_data});
        }

        # Escribe JSON con la información del artículo.
        $this->write_json();
        # si son iguales no se vuelve a guardar, ya que son los mismos datos
        # proceso terminado ok
        return 1;
    }
    # xml antiguo es vacio o no existe, guarda xml nuevo
    &glib_fildir_03::write_file($this->{fullpath_xml}, $this->{xml_data});
    # guardado ok

    # Escribe JSON con la información del artículo.
    $this->write_json();

    return 1;
}

# ---------------------------------------------------------------
# Ajusta horas desde los valores de entrada antes de guardarlos en el xml
sub _artic_ajusta_horas {
    my ($this) = @_;
    my %horas;

    $horas{'_horap'} = $this->{campos}->{'_horap'};
    if (!$horas{'_horap'}) {
        $horas{'_horap'} = &glib_hrfec_03::get_dtime_pack4();
        if ($horas{'_horap'} =~ /(\d{2})(\d{2})\d{2}$/) {
            $horas{'_horap'} = $1 . ':' . $2;
        }
    }
    $horas{'_horae'} = $this->{campos}->{'_horae'};

    my ($iso_horap, $iso_horae);

    foreach my $nom_campo (keys %horas) {
        my $val_campo = $horas{$nom_campo};
        $val_campo = '00:00' if ($val_campo !~ /^\d\d:\d\d$/);
        my ($hh, $mm);
        if ($val_campo =~ /^(\d\d):(\d\d)$/) {
            $hh        = $1;
            $mm        = $2;
            $hh        = '00' if ($hh eq '24');
            $hh        = '23' if ($hh > 23);
            $hh        = '59' if ($mm > 59);
            $val_campo = $hh . ':' . $mm;
        }
        $this->{campos}->{$nom_campo} = $val_campo;
    }    # foreach horas
}

# ---------------------------------------------------------------
# Guarda en xml y disco los recursos de un cierto tipo (FOTO, ASOCFILE, MULTIMEDIA) asociados al articulo
# type: FOTO, ASOCFILE, MULTIMEDIA
sub _guarda_recursos {
    my ($this, $type) = @_;
    my ($dst_dir);
    return if ($type ne 'foto' && $type ne 'asocfile' && $type ne 'multimedia');
    $dst_dir = $this->{'dst_' . $type};
    # print STDERR "type[$type]\n";

    foreach my $nom_campo (keys %{$this->{campos}}) {
        my $val_campo = $this->{campos}->{$nom_campo};
        next if ($nom_campo !~ /^$type\_\w+/);

        # Ver si hay archivo existente para el recurso
        # ej: foto_1120090528115643.jpg
        my $arch_existente = $this->{campos}->{'_hidd_' . $nom_campo};
        $arch_existente = '' if (!defined($arch_existente));
        my $nom_arch = '';

        # Variables especiales solo para FOTOs
        my ($msg_size, $wfoto, $hfoto, $ofoto, $orientation, $size, $mtime);

        # Se subio un nuevo archivo
        if ($val_campo ne '') {
            $nom_arch = $this->_get_newnom_arch($type, $nom_campo, $val_campo, $arch_existente);
            next if ($nom_arch eq '');
            &glib_fildir_03::check_dir($dst_dir);

            # Antes de mover lo nuevo, se elimina lo antiguo, tipo multimedia
            if ($arch_existente ne '' && $type eq 'multimedia') {
                # Obtener nombre de arch. sin extension
                my $nom_sin_ext = $nom_arch;
                $nom_sin_ext =~ s/\.\w*$//;

                my @archivos_rel = glob("$dst_dir/$nom_sin_ext*");
                foreach my $archivo_rel (@archivos_rel) {
                    if (-d $archivo_rel) {
                        &glib_fildir_03::borra_dir($archivo_rel);
                    } else {
                        unlink $archivo_rel;
                    }
                }

                if ($nom_sin_ext =~ /multimedia_video/i) {
                    @archivos_rel = glob("$this->{dst_mm_thumbs}/$nom_sin_ext*");
                    foreach my $archivo_rel (@archivos_rel) {
                        if (-d $archivo_rel) {
                            &glib_fildir_03::borra_dir($archivo_rel);
                        } else {
                            unlink $archivo_rel;
                        }
                    }
                }
            }
            # print STDERR "val_campo[$val_campo] [$dst_dir/$nom_arch]\n";

            # Se mueve el nuevo archivo a la ruta final
            &move($val_campo, "$dst_dir/$nom_arch");

            # tamaño del archivo
            $size = -s "$dst_dir/$nom_arch";

            # Si es foto, medirla
            if ($type eq 'foto') {
                my $file = "$dst_dir/$nom_arch";
                ($msg_size, $wfoto, $hfoto, $orientation) = &lib_prontus::dev_tam_img($file);
                if ($msg_size) {
                    print STDERR "Error: $msg_size foto[$dst_dir/$nom_arch] - dim[$wfoto, $hfoto]\n";
                    next;
                }

                my $img_type = &lib_thumb::get_imag_extension($file);
                if (&lib_prontus::can_edit_img($img_type)) {
                    # corrige la rotacion de la imagen de acuerdo al exif
                    if ($img_type eq 'jpg' && $orientation > 1) {
                        my $binfoto = &lib_thumb::apply_orientation_jpg($file, $orientation);
                        &lib_thumb::write_image($file, $binfoto);
                    }
                    # Redimensiona la foto al limite establecido si es que lo supera.
                    if ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'FOTO_MAX_PIXEL'} ne '') { # si no está vacio se reduce calidad de jpg, si corresponde.
                        my ($wmax, $hmax) = split("x", $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'FOTO_MAX_PIXEL'});

                        if ($wfoto > $wmax || $hfoto > $hmax) {
                            my ($wnew, $hnew) = &lib_thumb::calcular_proporcion_img($wfoto, $hfoto, $wmax, $hmax);
                            my ($binfoto, $wfoto, $hfoto) = &lib_thumb::make_resize($wnew, $hnew, $file);
                            # si es gif, se genera png
                            if ($file =~ /\.gif$/i) {
                                unlink $file; # se borra la foto original.
                                $file =~ s/\.gif$/\.png/i;
                                $nom_arch =~ s/\.gif$/\.png/i;
                            }
                            &lib_thumb::write_image($file, $binfoto) if ($binfoto);
                        }
                    } elsif ($img_type eq 'jpg' && $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'REDUCIR_CALIDAD_JPEGS'} eq 'SI') {
                        # Si es un JPEG, y está habilitada la reducción de calidad de las imágenes, lo guardamos con la calidad especificada en prontus_varglb::NIVEL_OPTIMIZACION_JPG.
                        my ($ancho, $alto, $ratio) = &lib_thumb::get_propiedades($file);
                        my ($binfoto, $wfoto, $hfoto) = &lib_thumb::make_resize($ancho, $alto, $file);
                        # Se reemplaza el path por el de la foto redimensionada. Si el redimensionado fue exitoso
                        if ($binfoto) {
                            # se borra la foto original.
                            unlink $file;
                            # se reemplaza con la foto "optimizada"
                            &lib_thumb::write_image($file, $binfoto);
                        }
                    }
                }
                # tamaño del archivo
                $size = -s $file;
            }

            # Si no se esta subiendo nada nuevo, ver si habia uno existente
        } else {
            if ($arch_existente ne '') {
                $nom_arch = $arch_existente;

                if ($type eq 'foto') {
                    # ancho
                    $wfoto = $this->{campos}->{'_w' . $nom_campo};
                    # alto
                    $hfoto = $this->{campos}->{'_h' . $nom_campo};
                    # si es original o no
                    $ofoto = 0;
                    if ($this->{campos}->{'_o' . $nom_campo}  =~ /^\d+$/) {
                        $ofoto = $this->{campos}->{'_o' . $nom_campo};
                    }

                    # tamaño del archivo
                    if (defined($this->{campos}->{'_b' . $nom_campo}) && $this->{campos}->{'_b' . $nom_campo} ne '') {
                        $size = $this->{campos}->{'_b' . $nom_campo};
                    } else {
                        $size = -s "$dst_dir/$nom_arch";
                    }
                    if (defined($this->{campos}->{'_m' . $nom_campo}) && $this->{campos}->{'_m' . $nom_campo} ne '') {
                        $mtime = $this->{campos}->{'_m' . $nom_campo};
                    } else {
                        $mtime = (stat("$dst_dir/$nom_arch"))[9];
                    }
                } else {
                    # tamaño del archivo
                    if (defined($this->{campos}->{"$nom_campo.bytes"}) && $this->{campos}->{"$nom_campo.bytes"} ne '') {
                        $size = $this->{campos}->{"$nom_campo.bytes"};
                    } else {
                        $size = -s "$dst_dir/$nom_arch";
                    }
                }

                if ($type eq 'multimedia' && substr($nom_campo, 0, 16) eq 'multimedia_video') {
                    if (!defined($this->{campos}->{"$nom_campo.total"}) || $this->{campos}->{"$nom_campo.total"} eq '') {
                        $this->{campos}->{"$nom_campo.total"} = $size;
                    }
                }

                # se debe borrar un archivo cargado anteriormente
                if (defined($this->{campos}->{'_borr_' . $nom_campo}) && $this->{campos}->{'_borr_' . $nom_campo} eq 'S') {
                    # Obtener nombre de arch. sin extension
                    my $nom_sin_ext = $nom_arch;
                    $nom_sin_ext =~ s/\.\w*$//;

                    # Borra todos los relacionados
                    my @archivos_rel = glob("$dst_dir/$nom_sin_ext*");
                    foreach my $archivo_rel (@archivos_rel) {
                        if (-d $archivo_rel) {
                            &glib_fildir_03::borra_dir($archivo_rel);
                        } else {
                            unlink $archivo_rel;
                        }
                    }

                    if ($type eq 'multimedia' &&  $nom_sin_ext =~ /multimedia_video/i) {
                        @archivos_rel = glob("$this->{dst_mm_thumbs}/$nom_sin_ext*");
                        foreach my $archivo_rel (@archivos_rel) {
                            if (-d $archivo_rel) {
                                &glib_fildir_03::borra_dir($archivo_rel);
                            } else {
                                unlink $archivo_rel;
                            }
                        }
                    }
                    # para no grabar la entrada en el xml
                    $nom_arch = '';
                }
            }
        }

        # Guardar nombre del archivo en el xml
        if ($nom_arch ne '') {
            if ($type eq 'foto') {
                $this->_add_foto_prontus_xml($wfoto, $hfoto, $nom_arch, $size, $ofoto, $mtime);
                if ($nom_campo =~ /^foto_posting_.*?$/) {
                    $this->_guarda_fotos_posting($nom_campo, $nom_arch);
                }
            } else {
                $this->{xml_data} = &lib_prontus::replace_in_xml($this->{xml_data}, $nom_campo, $nom_arch);
                $this->{xml_data} = &lib_prontus::replace_in_xml($this->{xml_data}, "$nom_campo.bytes", $size) if ($size);
                $this->{xml_data} = &lib_prontus::replace_in_xml($this->{xml_data}, "$nom_campo.total", $this->{campos}->{"$nom_campo.total"}) if (defined($this->{campos}->{"$nom_campo.total"}));
                $this->{xml_data} = &lib_prontus::replace_in_xml($this->{xml_data}, "$nom_campo.s3", $this->{campos}->{"$nom_campo.s3"}) if (defined($this->{campos}->{"$nom_campo.s3"}));
            }
        }

        # Purge de archivos en cache
        # retorna si no corresponde el fid
        next if (exists $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'CACHE_PURGE_EXCLUDE_FID'}{$this->{campos}->{'_fid'}});

        # Para el caso de Video, revisamos el screenshot
        if ($nom_campo =~ /^multimedia_video\w+$/) {
            my @videos;
            push @videos, "$nom_campo$this->{ts}.mp4";

            # Se obtienen los formatos de video
            my %formatos = &lib_prontus::get_formatos_multimedia($nom_campo);
            foreach my $formato (keys %formatos) {
                if ($formato =~ /\.(\w+)$/) {
                    my $newfile = "$nom_campo$this->{ts}" . lc($1).".mp4";
                    push @videos, $newfile;
                }
            }

            # Recorremos todas las versiones
            foreach my $file (@videos) {
                if (-f "$dst_dir/$file") {
                    # print STDERR "Agregando a purge [$dst_dir/$file]\n";
                    if (&glib_hrfec_03::get_antiguedad_ts($this->{ts}) > 60 && &glib_fildir_03::get_antiguedad_archivo("$dst_dir/$file") <= 60) {
                        &lib_prontus::purge_cache("$dst_dir/$file");
                    }
                }
                my $img = $file;
                $img =~ s/\.mp4/.jpg/;
                if (-f "$dst_dir/$img") {
                    # print STDERR "Agregando a purge [$dst_dir/$img]\n";
                    if (&glib_hrfec_03::get_antiguedad_ts($this->{ts}) > 60 && &glib_fildir_03::get_antiguedad_archivo("$dst_dir/$img") <= 60) {
                        &lib_prontus::purge_cache("$dst_dir/$img");
                    }
                }
            }
        } else {
            if (-f "$dst_dir/$nom_arch") {
                # print STDERR "Agregando a purge [$dst_dir/$nom_arch]\n";
                if ( &glib_hrfec_03::get_antiguedad_ts($this->{ts}) > 60 && &glib_fildir_03::get_antiguedad_archivo("$dst_dir/$nom_arch") <= 60) {
                    &lib_prontus::purge_cache("$dst_dir/$nom_arch");
                }
            }
        }
    }
}

# ---------------------------------------------------------------
# Obtiene un nuevo nombre para el recurso
sub _get_newnom_arch {
    my ($this, $type, $nom_campo, $val_campo, $arch_existente, $nom_foto_orig) = @_;

    # Valida extensiones permitidas por prontus (seguridad)
    return '' if (!&lib_prontus::check_extension($val_campo, $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'UPLOADS_PERMITIDOS'}));

    my $nom_arch;

    if ($type eq 'multimedia') {
        my $extension_arch = &lib_prontus::get_file_extension($val_campo);
        $extension_arch = 'xxx' if ($extension_arch eq '');
        $extension_arch = lc($extension_arch);
        $nom_arch = "$nom_campo$this->{ts}.$extension_arch";
    }

    if ($type eq 'foto') {
        $this->_check_ext_foto($val_campo) || return '';
        $nom_arch = $this->_get_nom_foto($val_campo, $nom_foto_orig);
    }

    if ($type eq 'asocfile') {
        my $real_path = $this->{campos}->{'_real_path'}{$nom_campo};
        $nom_arch = $this->_get_nom_asocfile($real_path, $arch_existente);
    }

    return $nom_arch;
}

# ---------------------------------------------------------------
# Valida la extensión de un archivo de foto.
sub _check_ext_foto {
    my ($this, $path_foto) = @_;

    if ($path_foto =~ /\.(jpe?g|jpeg?|gif|png|svg|webp)$/i) {
        return 1;
    } else {
        return 0;
    }
}

# ---------------------------------------------------------------
# retorna nombre de archivo para asocfile_i, con extension y sin path
sub _get_nom_asocfile {
    my ($this, $texto, $af_existente) = @_;

    my $nomfile = '';
    my $ext     = '';
    # print STDERR "texto[$texto] - af_existente[$af_existente]\n";
    if ($texto =~ /([^\/\\]+?)(\.\w+|)$/) {
        $nomfile = lc $1;
        $ext     = lc $2;    # ext con punto
    }
    $ext = '.xxx' if ($ext eq '');

    # print STDERR "nomfile[$nomfile]\n";
    $nomfile =~ tr/\xe1\xe9\xed\xf3\xfa\xc1\xc9\xcd\xd3\xda\xd1\xf1\x20\xfc\xdc/aeiouaeiounn_uu/;
    $nomfile =~ s/\W/_/sg;
    # quita simbolos utf8 superiores a ascii para urls
    $nomfile =~ s/[\x{7b}-\x{ffffff}]//g;

    return '' if ($nomfile eq '');

    # elimina el existente porque ahora viene uno nuevo
    if (($af_existente ne '') && (-f "$this->{dst_asocfile}/$af_existente")) {
        unlink "$this->{dst_asocfile}/$af_existente";
    }

    if (-f "$this->{dst_asocfile}/$nomfile$ext") {
        # Si el arch. existe debo ponerle un nombre distinto,
        # salvo q ya haya un archivo para este control,
        # en cuyo caso lo reemplazo, usease, uso el mismo nombre de archivo.
        if ($af_existente eq "$nomfile$ext") {
            return "$nomfile$ext";
        }

        my $sufijo       = '1';
        my $nomfile_orig = $nomfile;
        while (-f "$this->{dst_asocfile}/$nomfile$ext") {
            $nomfile = $nomfile_orig . '_' . $sufijo;
            $sufijo++;
        }
        return "$nomfile$ext";
    }

    return "$nomfile$ext";
}

# ---------------------------------------------------------------
# Obtiene nombre de la siguiente foto que toca para este articulo.
# Dicho nombre comenzara con el correlativo sgte al ultimo ingresado
# 10/03/2011 - CVI - Ahora se hará un random para minimizar que el nombre no se repita
# 20/02/2012 - CVI - Se corrige random, y se cambia a un esquema mejor
# 21/04/2016 - SCT - Se procesa el nombre original de la foto para agregarlo al path final
sub _get_nom_foto {
    my $this = $_[0];
    my $path_foto = $_[1];
    # para mantener compatibilidad si vienen 4 parametros, en vez de 3
    my $nom_foto_orig = $_[(scalar @_) - 1];

    if ($nom_foto_orig) {
        my $ext = '';
        if ($nom_foto_orig =~ /(.*)(\.\w+)$/) {
            $nom_foto_orig = $1;
            $ext = lc $2; # ext con punto
        }
        # la extensión sigue vacía, se intenta obtener del path
        if ($ext eq '' && $path_foto =~ /(\.\w+)$/) {
            # ext con punto
            $ext = lc $1;
        }
        $nom_foto_orig = &lib_prontus::notildes($nom_foto_orig);
        #convierte todos los caracteres que no son de palabras en espacios
        $nom_foto_orig =~ s/[^a-zA-Z0-9_\-\.]/ /g;
        $nom_foto_orig =~ s/\_/ /g; # convierte _ a espacio
        $nom_foto_orig =~ s/ {2,}/ /g; # elimina espacios repetidos
        $nom_foto_orig =~ s/^\s+//g; # elimina espacios iniciales
        $nom_foto_orig =~ s/\s+$//g; # elimina espacios finales
        $nom_foto_orig =~ s/^-+//g;   # elimina todos los guiones al principio
        $nom_foto_orig =~ s/-+$//g;   # elimina todos los guiones al final

        $nom_foto_orig = &lib_prontus::ajusta_nchars($nom_foto_orig, 50);
        $nom_foto_orig =~ s/ /\_/g; # convierte espacios a _
        $nom_foto_orig =~ s/\.\.\.//;
        if ($nom_foto_orig ne '') {
            $nom_foto_orig = "_$nom_foto_orig$ext";
        } else {
            $nom_foto_orig = $ext;
        }
    } else {
        # no viene nombre de archivo de la foto original
        # asi que hay que obtener la extensión desde el path
        # que debe venir completo incluyendo el nombre de archivo de la foto
        # con extensión
        $nom_foto_orig = '';
        if ($path_foto =~ /(\.\w+)$/) {
            # ext con punto
            $nom_foto_orig = lc $1;
        }
    }

    my @files = &glib_fildir_03::lee_dir($this->{dst_foto});

    my $max = 0;
    foreach my $imag (@files) {
        if ($imag =~ /foto_(\d+)$this->{ts}/i) {
            my $thisnum = $1;
            if ($thisnum > $max) {
                $max = $thisnum;
            }
        }
    }
    my $num = $max + 1;
    $num = &glib_str_03::format_n($num, 8) if (length($num) < 8);

    my $nomfile  = 'foto_' . $num . $this->{ts} . $nom_foto_orig;
    return $nomfile;
}

# ---------------------------------------------------------------
# Guarda imágenes externas o locales al prontus. Delega el trabajo a subs como _add_foto_sitioexterno().
sub _guarda_fotosfijas {
    my $this             = shift;
    my $cpan_server_name = $this->{cpan_server_name};
    my $document_root    = $this->{document_root};

    foreach my $nom_campo (keys %{$this->{campos}}) {
        my $val_campo = $this->{campos}->{$nom_campo};
        next if ($nom_campo !~ /^fotofija_\w+/);
        next if (!$val_campo);

        my $cuadrar = $this->{campos}->{'chk_cuadrar_' . $nom_campo};
        #warn("cuadrar[$cuadrar], nom_campo[$nom_campo]");

        # foto externa al sitio
        if (($val_campo =~ /^https?:/i) && ($val_campo !~ /^https?\:\/\/$cpan_server_name/i)) {
            $this->_add_foto_sitioexterno($nom_campo, $val_campo);
            next;
        }

        # foto local al sitio, asi q saca el server name.
        $val_campo =~ s/^https?\:\/\/$cpan_server_name//i;

        # no se permiten rutas relativas
        # warn("1-nom_campo[$nom_campo] val_campo[$val_campo]");
        if ($val_campo =~ /^\.\.\//) {
            $val_campo =~ s/^(\.\.\/)+(\w)/\/$2/;
        }
        # warn("2-nom_campo[$nom_campo] val_campo[$val_campo]");

        # En adelante, puede tratarse de una foto local al articulo, o bien,
        # local al sitio (pero no al artic). -
        if (!-f "$document_root$val_campo") {
            my $parse_as_cdata = 1;
            $this->{xml_data} = &lib_prontus::replace_in_xml($this->{xml_data}, $nom_campo, '', $parse_as_cdata);
            next;
        }

        # Obtiene dimensiones actuales de la foto
        my ($msg, $foto_dimx, $foto_dimy) = &lib_prontus::dev_tam_img("$document_root$val_campo");
        my $img_type = lib_thumb::get_imag_extension($val_campo);

        # Obtiene dimensiones maximas permitidas para la foto
        my ($maxw) = $this->{campos}->{'_maxw' . $nom_campo};
        my ($maxh) = $this->{campos}->{'_maxh' . $nom_campo};

        # Al hacer esto la imagen no se agrega como nueva al xml. Se reutiliza la misma.
        # Como algunas imágenes no se pueden manipular en prontus, se mantiene el archivo original,
        # solo se asigna al controlador de fotofija.
        if (!&lib_prontus::can_edit_img($img_type)) {
            $foto_dimx = $maxw;
            $foto_dimy = $maxh;
        }

        # Solo procede si viene ancho o alto
        if (($maxw =~ /^\d+$/) || ($maxh =~ /^\d+$/)) {
            # si la FOTO es externa al articulo pero local al server:
            # 1.1 Se hace match del nombre de la foto
            # 1.2 Se agrega if para fotos que mantengan el nombre con el que se sube - SCT
            my $nom_foto_orig = "";
            if ($val_campo =~ /\/(foto_\w+)$this->{ts}(.*?)\.\w+/) {
                # foto local al articulo
                my $nom_campo_orig = $1;
                my $nom_foto_orig  = $2;

                $this->_add_foto_sitiolocal_articulolocal($nom_campo, $val_campo, $maxw, $maxh, $foto_dimx, $foto_dimy, $cuadrar, $nom_campo_orig, $nom_foto_orig);
            } elsif ($val_campo =~ /\/(foto_\w+)$this->{ts}\.\w+/) {
                # foto local al articulo
                my $nom_campo_orig = $1;
                $this->_add_foto_sitiolocal_articulolocal($nom_campo, $val_campo, $maxw, $maxh, $foto_dimx, $foto_dimy, $cuadrar, $nom_campo_orig, $nom_foto_orig);
            } else {
                # print STDERR "add_foto_sitiolocal_articuloexterno\n";
                $this->_add_foto_sitiolocal_articuloexterno($nom_campo, $val_campo, $maxw, $maxh, $foto_dimx, $foto_dimy, $cuadrar);
            }
        }
    }
}

# ---------------------------------------------------------------
# Guarda imágenes enviadas via form posting.
sub _guarda_fotos_posting {
    my ($this, $nom_campo_foto, $nom_foto) = @_;
    my $ts               = $this->{ts};
    return unless ($ts =~ /(\d{8})\d{6}/);
    my $fechac = $1;

    my $full_path_foto = $this->{dst_foto} . "/$nom_foto";
    my $path_foto = &lib_prontus::remove_front_string($full_path_foto, $this->{document_root});

    # Se comprueba que la foto realmente exista
    if (!-f $full_path_foto) {
        print STDERR "\t$full_path_foto no existe\n";
        return;
    }

    # Obtiene dimensiones actuales de la foto
    my ($msg, $foto_dimx, $foto_dimy) = &lib_prontus::dev_tam_img($full_path_foto);
    unless ($foto_dimx && $foto_dimy) {
        print STDERR "\tError dev_tam_img [0] $full_path_foto: $msg\n";
        return;
    }

    foreach my $nom_campo (keys %{$this->{campos}}) {
        my $val_campo = $this->{campos}->{$nom_campo};
        next if ($nom_campo !~ /^fotofija_\w+/i);
        next if (!$val_campo);
        next if ($val_campo !~ /$nom_campo_foto *\((\d+) *\, *(\d+)\)/i);

        # Obtiene dimensiones maximas permitidas para la foto
        my ($maxw) = $1;
        my ($maxh) = $2;

        # Solo procede si viene ancho o alto
        if (($maxw =~ /^\d+$/) || ($maxh =~ /^\d+$/)) {
            # print STDERR "\tval_campo[$val_campo], maxw[$maxw], maxh[$maxh]\n";

            my $cuadrar = $this->{campos}->{'chk_cuadrar_' . $nom_campo};
            #print STDERR "\tcuadrar[$cuadrar], nom_campo[$nom_campo]";

            if ($path_foto =~ /\/(foto_\w+)$this->{ts}\.\w+/) {
                my $nom_campo_orig = $1;
                $this->{campos}->{'_hidd_' . $nom_campo_orig} = $nom_foto;
                $this->{campos}->{'_path_' . $nom_campo_orig} = $path_foto;
                $this->{campos}->{$nom_campo}                 = '';
                $this->_add_foto_sitiolocal_articulolocal($nom_campo, $path_foto, $maxw, $maxh, $foto_dimx, $foto_dimy, $cuadrar, $nom_campo_orig);
                print STDERR "\tdatos: $nom_campo, $path_foto, $maxw, $maxh, $foto_dimx, $foto_dimy, $cuadrar, $nom_campo_orig\n";

            }
        }
    }
}

# ---------------------------------------------------------------
# Carga en el objeto xml_data los datos de una foto externa a Prontus.
sub _add_foto_sitioexterno {
    my ($this, $nom_campo, $val_campo) = @_;

    my $wfoto_externa = $this->{campos}->{'_w' . $nom_campo};
    my $hfoto_externa = $this->{campos}->{'_h' . $nom_campo};

    # warn "add foto sitio externo val_campo[$val_campo] nom_campo[$nom_campo] wfoto_externa[$wfoto_externa]\n";

    my $foto_externa_xml = "<$nom_campo>" . "\n<_w$nom_campo>$wfoto_externa</_w$nom_campo>" . "\n<_h$nom_campo>$hfoto_externa</_h$nom_campo>" . "\n<![CDATA[$val_campo]]>" . "\n</$nom_campo>\n";

    if (!($this->{xml_data} =~ s/<$nom_campo>.*?<\/$nom_campo>/$foto_externa_xml/is)) {
        $this->{xml_data} =~ s/<_public>/<_public>\n$foto_externa_xml/s;
    }
}

# ---------------------------------------------------------------
# Guarda una foto del artículo basada en otra foto del mismo artículo
sub _add_foto_sitiolocal_articulolocal {
    my ($this, $nom_campo, $val_campo, $maxw, $maxh, $dx, $dy, $cuadrar, $nom_campo_orig, $nom_foto_orig) = @_;
    my $document_root  = $this->{document_root};
    my $actions        = $this->{campos}->{'_actions' . $nom_campo};
    my ($binfoto, $final_dimx, $final_dimy, $nom_arch);

    # Si hay acciones las aplica sobre la misma imagen, luego continua.
    if ($actions) {
        ($nom_arch, $final_dimx, $final_dimy) = $this->_aplica_fotofija_actions($val_campo, $actions, $nom_foto_orig);
        return if ($nom_arch eq '');

        # Reasigna val_campo para parsear en el artic.
        $val_campo = "$this->{dst_foto}/$nom_arch";
        # tamaño del archivo
        my $size = -s $val_campo;
        my $mtime = (stat($val_campo))[9];
        $this->_add_foto_prontus_xml($final_dimx, $final_dimy, $nom_arch, $size, 0, $mtime);
        # quita doc root del path completo
        $val_campo =~ s/^$document_root//;

    } else {    # Continua normal...
        # si la foto es mas grande lo que debe ser, se redimensiona
        # y se crea una nueva fotoprontus y esa es asignada a fotofija
        ($final_dimx, $final_dimy) = ($dx, $dy);

        if (($dx > $maxw) || ($dy > $maxh)) {
            ($binfoto, $final_dimx, $final_dimy) = &lib_thumb::make_thumbnail($maxw, $maxh, "$document_root$val_campo", $cuadrar);
            $nom_arch = $this->_guarda_binfoto_prontus($val_campo, $binfoto, $nom_foto_orig);
            return if ($nom_arch eq '');

            # Reasigna val_campo para parsear en el artic.
            $val_campo = "$this->{dst_foto}/$nom_arch";
            # tamaño del archivo
            my $size = -s $val_campo;
            my $mtime = (stat($val_campo))[9];
            $this->_add_foto_prontus_xml($final_dimx, $final_dimy, $nom_arch, $size, 0, $mtime);
            # quita doc root del path completo
            $val_campo =~ s/^$document_root//;
        }
    }

    # foto local, asi q saca el server name.
    $val_campo =~ s/^https?:\/\/$this->{cpan_server_name}//i;
    $val_campo =~ s/^https?:\/\/$this->{public_server_name}//i;

    # Reemplaza fotofija en xml
    my $parse_as_cdata = 1;
    $this->{xml_data} = &lib_prontus::replace_in_xml($this->{xml_data}, $nom_campo, $val_campo, $parse_as_cdata);

    return ($final_dimx, $final_dimy, $nom_arch, $val_campo);
}

# ---------------------------------------------------------------
# Aplica a una foto acciones del editor (resize, crop, etc.)
sub _aplica_fotofija_actions {
    my ($this, $lafoto, $actions, $nom_foto_orig) = @_;
    my ($maxw, $maxh, $left, $top, $neww, $newh);
    my $document_root = $this->{document_root};
    my $nom_arch      = $this->_get_nom_foto($lafoto, $nom_foto_orig);
    my $dst_binfoto   = "$this->{dst_foto}/$nom_arch";
    my ($binfoto, $finalw, $finalh);
    my $resize = 0;

    if ($actions =~ /resize\[([^\]]+)/i) {    # resize[100x100]
        my $args = $1;
        if ($args =~ /(\d+)x(\d+)/) {
            ($neww, $newh) = ($1, $2);
            #print STDERR "neww[$neww] newh[$newh]\n";
            ($binfoto, $finalw, $finalh) = &lib_thumb::make_resize($neww, $newh, "$document_root$lafoto");
            &lib_thumb::write_image($dst_binfoto, $binfoto);    # se escribe la foto.
            $resize = 1;
        }
    }

    if ($actions =~ /crop\[([^\]]+)/i) {    # crop[80x80+388+250!875x656]
        my $args = $1;
        if ($args =~ /(\d+)x(\d+)\+(-?\d+)\+(-?\d+)!(\d+)x(\d+)/) {
            ($maxw, $maxh, $left, $top, $neww, $newh) = ($1, $2, $3, $4, $5, $6);
            #print STDERR "maxw[$maxw] maxh[$maxh] left[$left] top[$top] neww[$neww] newh[$newh]\n";

            if ($resize) {
                ($binfoto, $finalw, $finalh) = &lib_thumb::make_crop($left, $top, $maxw, $maxh, $dst_binfoto);
                &lib_thumb::write_image($dst_binfoto, $binfoto);    # se escribe la foto.
            } else {
                ($binfoto, $finalw, $finalh) = &lib_thumb::make_crop($left, $top, $maxw, $maxh, "$document_root$lafoto");
                &lib_thumb::write_image($dst_binfoto, $binfoto);    # se escribe la foto.
            }

            if ($neww && $newh) {
                ($binfoto, $finalw, $finalh) = &lib_thumb::make_resize($neww, $newh, $dst_binfoto);
                &lib_thumb::write_image($dst_binfoto, $binfoto);    # se escribe la foto.
            }

            return ($nom_arch, $finalw, $finalh);
        }
    }
}

# ---------------------------------------------------------------
# Parsea los datos de una foto y los guarda al xml de artículo.
# nom_arch: FOTO_23<TS>.jpg
sub _add_foto_prontus_xml {
    my ($this, $wfoto, $hfoto, $nom_arch, $size, $original, $modified_time) = @_;
    $original = $original || 0;
    $modified_time = $modified_time || 0;
    my $num = '';
    my $ts = $this->{ts};
    if ($nom_arch =~ /^foto_(\w{5,8})$ts/i) {
        $num = $1;

        # print STDERR "_add_foto_prontus_xml: [$ts] $num\n";
        # Guardar foto en el xml
        my $fotoxml = "<foto_$num>\n<_nomfoto_$num>$nom_arch</_nomfoto_$num>\n<_wfoto_$num>$wfoto</_wfoto_$num>";
        $fotoxml .= "\n<_hfoto_$num>$hfoto</_hfoto_$num>";
        $fotoxml .= "\n<_bfoto_$num>$size</_bfoto_$num>" if (defined($size));
        $fotoxml .= "\n<_mfoto_$num>$modified_time</_mfoto_$num>";
        $fotoxml .= "\n<_ofoto_$num>$original</_ofoto_$num>\n</foto_$num>";
        $this->{xml_data} =~ s/<_public>/<_public>\n$fotoxml/is;
    } else {
        print STDERR "Error: art [$this->{ts}] no se pudo determinar identificador de foto [$nom_arch]\n";
    }
}

# ---------------------------------------------------------------
# Guarda una foto externa al articulo pero local al Prontus
# Si la foto externa es mas grande de lo que debe ser, se redimensiona.
# Requiera o no ser redimensionada, siempre se genera una nueva foto
# prontus para el articulo y luego esta nueva foto, ya internalizada, se asigna a la fotofija.
sub _add_foto_sitiolocal_articuloexterno {
    my ($this, $nom_campo, $val_campo, $maxw, $maxh, $dx, $dy, $cuadrar) = @_;
    my $document_root = $this->{document_root};

    my ($binfoto, $final_dimx, $final_dimy);
    if (($dx > $maxw) || ($dy > $maxh)) {
        ($binfoto, $final_dimx, $final_dimy) = &lib_thumb::make_thumbnail($maxw, $maxh, "$document_root$val_campo", $cuadrar);
        # si no se puede redim, tonces la agarra asi no mas.
        if (!$binfoto || !$final_dimx || !$final_dimy) {
            ($final_dimx, $final_dimy) = ($dx, $dy);
            $binfoto = &glib_fildir_03::read_file_bin("$document_root$val_campo");
        }
    } else {
        ($final_dimx, $final_dimy) = ($dx, $dy);
        $binfoto = &glib_fildir_03::read_file_bin("$document_root$val_campo");
    }

    my $nom_arch = $this->_guarda_binfoto_prontus($val_campo, $binfoto);
    return if ($nom_arch eq '');

    # Reasigna val_campo al nuevo valor de la nueva foto generada para el articulo.
    $val_campo = "$this->{dst_foto}/$nom_arch";
    # tamaño del archivo
    my $size = -s $val_campo;
    my $mtime = (stat($val_campo))[9];
    $this->_add_foto_prontus_xml($final_dimx, $final_dimy, $nom_arch, $size, 0, $mtime);
    # quita doc root del path completo
    $val_campo =~ s/^$document_root//;

    # Guardar en xml y parsear en html la nueva fotofija
    my $parse_as_cdata = 1;
    $this->{xml_data} = &lib_prontus::replace_in_xml($this->{xml_data}, $nom_campo, $val_campo, $parse_as_cdata);
}

# ---------------------------------------------------------------
# Copia un archivo de foto, y agrega los datos de la foto (nombre, dimensiones) al xml del artículo.
# a pesar de llamarse con _, es usada solamente en cgis.
sub _add_foto_filesystem {
    my ($this, $path_foto, $nom_foto_orig, $foto_original) = @_;
    my $document_root    = $this->{document_root};
    my $ts               = $this->{ts};

    return unless ($ts =~ /(\d{8})\d{6}/);
    if ($path_foto eq '' || $nom_foto_orig eq '') {
        print STDERR "Error: path_foto[$path_foto] nom_foto_orig[$nom_foto_orig]\n";
        return;
    }

    my $fechac             = $1;
    my $new_full_path_foto = $this->{dst_foto};
    my $new_path_foto      = &lib_prontus::remove_front_string($new_full_path_foto, $this->{document_root});
    my $nom_foto           = $this->_get_nom_foto($new_path_foto, $nom_foto_orig);

    # print STDERR "copiando... $document_root$path_foto  -->  $document_root$new_path_foto$nom_foto\n";
    &copy("$document_root$path_foto", "$document_root$new_path_foto/$nom_foto");

    # Obtiene dimensiones actuales de la foto
    my ($msg, $foto_dimx, $foto_dimy) = &lib_prontus::dev_tam_img("$document_root$new_path_foto/$nom_foto");
    unless ($foto_dimx && $foto_dimy) {
        print STDERR "\tError dev_tam_img [1] $document_root$new_path_foto/$nom_foto: $msg\n";
        return;
    }

    # tamaño del archivo
    my $size = -s "$document_root$new_path_foto/$nom_foto";
    my $mtime = (stat("$document_root$new_path_foto/$nom_foto"))[9];
    # Se agrega la imagen al xml
    $this->_add_foto_prontus_xml($foto_dimx, $foto_dimy, $nom_foto, $size, $foto_original, $mtime);
    return $nom_foto;
}
# ---------------------------------------------------------------
# extrae fotos en base 64 publicadas en un cuerpo del articulo
# las guarda como archivo binario y las asocia al articulo
sub _guarda_fotos_cuerpo {
    my ($this, $cuerpo) = @_;
    # data:[<mediatype>][;base64],<data>
    my $aux = $cuerpo;
    while($aux =~ /src="(data:(image\/\w+);base64,([a-zA-Z0-9+\/=]+))"/g){
        my $match = $1;
        my $type = $2;
        my $binfoto = MIME::Base64::decode_base64($3);
        my $ext = '.'. mime_types::type_2_ext($type);
        # para convertir las extensiones a jpeg a jpg
        $ext = '.jpg' if ($ext eq '.jpeg');
        my $nom_arch = $this->_get_nom_foto($this->{dst_foto}, $ext);
        # print STDERR "dst_img[$this->{dst_foto}] - nom_arch[$nom_arch]\n";
        my $path_foto = "$this->{dst_foto}/$nom_arch";
        # guarda la foto en disco
        lib_thumb::write_image($path_foto, $binfoto);
        # tamaño del archivo
        my $size = -s $path_foto;
        my $mtime = (stat($path_foto))[9];
        my ($msg_size, $wfoto, $hfoto, $orientation) = &lib_prontus::dev_tam_img($path_foto);
        # foto no es original ya que se genero con el editor del cuerpo
        $this->_add_foto_prontus_xml($wfoto, $hfoto, $nom_arch, $size, 0, $mtime);
        # quita doc root del path completo
        $path_foto =~ s/^$this->{document_root}//;
        $cuerpo =~ s/\Q$match\E/$path_foto/;
    }
    return $cuerpo;
}

# ---------------------------------------------------------------
# Genera un archivo binario a partir de una foto. En caso de que el original sea un GIF, genera un archivo PNG.
sub _guarda_binfoto_prontus {
    my ($this, $val_campo, $binfoto, $nom_foto_orig) = @_;
    return '' if (!$binfoto);
    $this->_check_ext_foto($val_campo) || return '';

    my $nom_arch = $this->_get_nom_foto($val_campo, $nom_foto_orig);

    # si es gif , se genera png
    if ($val_campo =~ /\.gif$/i) {
        $nom_arch =~ s/\.gif$/\.png/i;
    }

    # warn "dst_img[$this->{dst_foto}] - nom_arch[$nom_arch]";
    my $dst_binfoto = "$this->{dst_foto}/$nom_arch";
    &lib_thumb::write_image($dst_binfoto, $binfoto);

    if (!exists $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'CACHE_PURGE_EXCLUDE_FID'}{$this->{campos}->{'_fid'}}) {
        &lib_prontus::purge_cache($dst_binfoto)
          if (   &glib_hrfec_03::get_antiguedad_ts($this->{ts}) > 60
              && &glib_fildir_03::get_antiguedad_archivo($dst_binfoto) <= 60);
    }

    return $nom_arch;
}

# ---------------------------------------------------------------
# Genera una fotofija con nuevas dimensiones a partir de los datos pasados.
# Valida datos y llama a $this->_add_foto_sitiolocal_articulo_local()
# Recibe como parametro:
# - El nombre de la foto_n. Ej: foto_134520110619122632.jpg
# - El nombre la foto fija (nombre marca marca)
# - El ancho maximo
# - El alto maximo
# - Si se debe cuadrar la imagen o no: si|<anything>
sub _genera_fotofija {
    my ($this, $nom_foto, $fotofija, $maxw, $maxh, $cuadrar) = @_;
    my $img_type = &lib_thumb::get_imag_extension($nom_foto);

    $cuadrar = '' unless ($cuadrar eq 'si' || &lib_prontus::can_edit_img($img_type));

    my $full_path_foto = $this->{dst_foto} . "/$nom_foto";
    my $path_foto = &lib_prontus::remove_front_string($full_path_foto, $this->{document_root});

    # Se revisa que la foto exista
    return ("La foto no existe $nom_foto: $full_path_foto", '', '', '', '') unless (-f "$full_path_foto");

    my ($msg, $foto_dimx, $foto_dimy);

    # Si el tipo de imagen se puede editar, continua normal.
    if (&lib_prontus::can_edit_img($img_type)) {
        # Obtiene dimensiones actuales de la foto
        ($msg, $foto_dimx, $foto_dimy) = &lib_prontus::dev_tam_img("$full_path_foto");
        return ($msg, '', '', '', '') if ($msg);
    } else {
        ($msg, $foto_dimx, $foto_dimy) = ('', $maxw, $maxh);
    }

    # Solo procede si viene ancho o alto
    if (($maxw =~ /^\d+$/) || ($maxh =~ /^\d+$/) || ($foto_dimx =~ /^\d+$/) || ($foto_dimy =~ /^\d+$/)) {
        if ($nom_foto =~ /(foto_\w+)$this->{ts}(_[\w\-\.]+)?\.\w+/) {
            # Foto local al articulo
            my $nom_campo_orig = $1;
            #print STDERR "_genera_fotofija: $fotofija, $path_foto, $maxw, $maxh, $foto_dimx, $foto_dimy, $cuadrar, $nom_campo_orig\n";
            my ($final_dimx, $final_dimy, $final_nomfoto, $final_relfoto) = $this->_add_foto_sitiolocal_articulolocal($fotofija, $path_foto, $maxw, $maxh, $foto_dimx, $foto_dimy, $cuadrar, $nom_campo_orig);

            return ('', $final_dimx, $final_dimy, $final_nomfoto, $final_relfoto);
        } else {
            return ("Error en regexp: nom_foto[$nom_foto]", '', '', '', '');
        }
    } else {
        return ("Error en las dimensiones: maxw[$maxw], maxh[$maxh], foto_dimx[$foto_dimx], foto_dimy[$foto_dimy]", '', '', '', '');
    }
};


# ---------------------------------------------------------------
# limpia y formatea los campos a ingresar en base de datos.
sub _get_campos_art4bd {
    my $this           = shift;
    my %campos    = $this->get_xml_content();

    if (!defined($campos{'_autor'})) {
        $campos{'_autor'} = '';
    } else {
        $campos{'_autor'} = substr($campos{'_autor'}, 0, 60);
    }
    $campos{'_fid'}   = '' if (!defined($campos{'_fid'}));
    $campos{'_alta'}  = '' if (!defined($campos{'_alta'}));
    $campos{'_indexar'}  = 0 if (!defined($campos{'_indexar'}) || $campos{'_indexar'} eq '');
    $campos{'_priorizar'}  = 0 if (!defined($campos{'_priorizar'}) || $campos{'_priorizar'} eq '');

    $campos{'_txt_bajada'}   = '' if (!defined($campos{'_txt_bajada'}));
    $campos{'_users_id'}     = '' if (!defined($campos{'_users_id'}));
    $campos{'_soloportadas'} = '' if (!defined($campos{'_soloportadas'}));

    $campos{'_seccion1'} = '0' if ($campos{'_seccion1'} eq '');
    $campos{'_tema1'}    = '0' if ($campos{'_tema1'}    eq '');
    $campos{'_subtema1'} = '0' if ($campos{'_subtema1'} eq '');
    $campos{'_seccion2'} = '0' if ($campos{'_seccion2'} eq '');
    $campos{'_tema2'}    = '0' if ($campos{'_tema2'}    eq '');
    $campos{'_subtema2'} = '0' if ($campos{'_subtema2'} eq '');
    $campos{'_seccion3'} = '0' if ($campos{'_seccion3'} eq '');
    $campos{'_tema3'}    = '0' if ($campos{'_tema3'}    eq '');
    $campos{'_subtema3'} = '0' if ($campos{'_subtema3'} eq '');

    $campos{'_s3'} = 0 if ($campos{'_s3'} eq '');
    $campos{'_s3_size'} = 0 if ($campos{'_s3_size'} eq '');
    $campos{'_s3_media_size'}  = 0 if ($campos{'_s3_media_size'} eq '');
    $campos{'_s3_asset_size'}  = 0 if ($campos{'_s3_asset_size'} eq '');

    $campos{'_disk_size'}  = 0 if ($campos{'_disk_size'} eq '');
    $campos{'_media_size'} = 0 if ($campos{'_media_size'} eq '');
    $campos{'_asset_size'} = 0 if ($campos{'_asset_size'} eq '');
    $campos{'_imag_size'}  = 0 if ($campos{'_imag_size'} eq '');
    $campos{'_total_size'} = 0 if ($campos{'_total_size'} eq '');

    $campos{'_modtime'} = 0 if ($campos{'_modtime'} eq '');

    $campos{'_indexar'} = '0' if ($campos{'_indexar'} eq '');

    $campos{'_horap'} =~ s/://g;
    $campos{'_horap'} = '0000' if ($campos{'_horap'} eq '');

    $campos{'_horae'} =~ s/://g;
    $campos{'_horae'}  = '0000'     if ($campos{'_horae'}  eq '');
    $campos{'_fechae'} = '99999999' if ($campos{'_fechae'} eq '');

    $campos{'_extension'} = &lib_prontus::get_file_extension($campos{'_plt'});
    $campos{'vtxt_cuerpo'} = '' if (!defined($campos{'vtxt_cuerpo'}));

    # Ajusta bajada
    my $bajada = $campos{'_txt_bajada'};
    if ($bajada eq '' && $campos{'vtxt_cuerpo'} ne '') {
        $bajada = $campos{'vtxt_cuerpo'};
        $bajada = &lib_prontus::get_minitext_value($bajada);
    }
    if ($bajada ne '') {
        $bajada =~ s/\s+$//s;
        $bajada =~ s/^\s+//s;
        $bajada =~ s/ +/ /sg;
        $bajada =~ s/ +$//sg;

        # Ajusta en caso de bajada muy larga, de manera de evitar truncado brusco en la BD
        if ((length $bajada) < 200) {
            if (($bajada !~ /\.$/) && ($bajada ne '')) {
                $bajada .= '.';    # queda de 200
            }
        } else {
            $bajada = &lib_prontus::ajusta_nchars($bajada, 200, 1);    # ajusta por palabras en modo conteo de bytes
        }
        $campos{'_txt_bajada'} = $bajada;
    }

    # Ajusta titular
    my $titu = $campos{'_txt_titular'};
    # Ajusta en caso de titulares muy largos, de manera de evitar truncado brusco en la BD
    if ((length $titu) > 255) {
        $titu = substr($titu, 0, 252) . '...';
    }
    $campos{'_txt_titular'} = $titu;

    $campos{'_fechap_horap'} = "$campos{'_fechap'}$campos{'_horap'}";
    $campos{'_fechae_horae'} = "$campos{'_fechae'}$campos{'_horae'}";

    return %campos;
}

# ---------------------------------------------------------------
# Parsea en vista del artic los recursos de un cierto tipo asociados al mismo
# nom_campo: asocfile_xx, multimedia_xx
sub _parsing_recursos {
    my ($this, $nom_campo, $val_campo, $buffer, $type, $use_s3) = @_;

    # es una marca multimedia con . o sea una propiedad del elemento
    # se reemplaza directamente y retorna
    if (($type eq 'multimedia_audio' or $type eq 'multimedia_video') && index($nom_campo, '.') > -1) {
        $buffer =~ s/%%$nom_campo%%/$val_campo/ig;
        return $buffer;
    }

    my $path;
    # si el valor contiene el tipo de la marca es posible que sea archivo
    # comprobamos si existe en las rutas
    if ($type eq 'multimedia_video' or $type eq 'multimedia_audio' ) {
        # valor del campo no lleva ningun tipo de directorio, por ej multimedia_video120190926165638.mp4
        $path = "$this->{'dst_multimedia'}/$val_campo";

        if ($use_s3 || -f $path) {
            my $parse_val = "/$this->{prontus_id}/site$prontus_varglb::DIR_EXMEDIA/$this->{fechac}$prontus_varglb::DIR_MMEDIA/$val_campo";
            # Parsea el recurso como tal
            $buffer =~ s/%%$nom_campo%%/$parse_val/ig;
        } else {
            print STDERR "[$this->{ts}] Archivo no existe [$this->{'dst_multimedia'}/$val_campo]\n";
            $buffer =~ s/%%_S$nom_campo%%/0/ig;
            $buffer =~ s/%%$nom_campo.size%%/0/ig;
            $buffer =~ s/%%$nom_campo%%/$nom_campo\[$val_campo\]FILE_NOT_FOUND/ig;
            $buffer =~ s/%%_E$nom_campo%%//ig;
            $buffer =~ s/%%$nom_campo.ext%%//ig;
            return $buffer;
        }
    } elsif ($type eq 'asocfile') {
        $path = "$this->{'dst_' . $type}/$val_campo";

        # Parsea el size.
        my $size;
        if (defined($this->{xml_content}->{"$nom_campo.size"})) {
            $size = $this->{xml_content}->{"$nom_campo.size"};
        } else {
            $size = &lib_prontus::bytes2kb(-s $path);

            # para no repetir la busqueda
            $this->{xml_content}->{"_s$nom_campo"} = $size;
            $this->{xml_content}->{"$nom_campo.size"} = $size;
        }
        $buffer =~ s/%%_S$nom_campo%%/$size/ig;
        $buffer =~ s/%%$nom_campo.size%%/$size/ig;

        # Parsea el recurso como tal
        $path = &lib_prontus::remove_front_string($path, $this->{document_root});
        $buffer =~ s/%%$nom_campo%%/$path/ig;

        # Parsear la extension del archivo
        my $ext;
        if (defined($this->{xml_content}->{"$nom_campo.ext"})) {
            $ext = $this->{xml_content}->{"$nom_campo.ext"};
        } else {
            $ext = &lib_prontus::get_file_extension($val_campo);

            # para no repetir la busqueda
            $this->{xml_content}->{"_e$nom_campo"} = $ext;
            $this->{xml_content}->{"$nom_campo.ext"} = $ext;
        }
        $buffer =~ s/%%_E$nom_campo%%/$ext/ig;
        $buffer =~ s/%%$nom_campo.ext%%/$ext/ig;

    } else {
        # en caso de que no cumpla nada de lo anterior
        $buffer =~ s/%%$nom_campo%%/$val_campo/ig;
    }

    return $buffer;
}

# ---------------------------------------------------------------
# Parseo de fotos fijas
sub _parsing_fotos {
    my ($this, $nom_campo, $val_campo, $buffer) = @_;
    my ($msg, $foto_dimx, $foto_dimy);
    my %campos = $this->get_xml_content();

    $val_campo =~ s/(foto_\d+)\_(.*)$/$1\/$2/si;
    $buffer =~ s/%%$nom_campo%%/$val_campo/ig;

    my $este_prontus = $this->{prontus_id};

    if ($val_campo =~ /$este_prontus/i) {    # val_campo es del tipo: /prontus_dev/site/artic/20060410/imag/FOTO_0120060410165548.jpg
        my $ts = $this->{ts};
        # parseo ademas las dimensiones de la foto en el articulo
        if ($val_campo =~ /(foto_\d+)$ts(.+?)?\.(\w+)$/i) {
            my $nom_foto_original = lc $1;
            my $ext = lc $3;

            $foto_dimx = $campos{"_w$nom_foto_original"};
            $foto_dimy = $campos{"_h$nom_foto_original"};
            $buffer =~ s/%%_W$nom_campo%%/$foto_dimx/ig;
            $buffer =~ s/%%_H$nom_campo%%/$foto_dimy/ig;

            $buffer =~ s/%%$nom_campo\.width%%/$foto_dimx/ig;
            $buffer =~ s/%%$nom_campo\.height%%/$foto_dimy/ig;
            $buffer =~ s/%%$nom_campo\.ext%%/$ext/ig;
            $buffer =~ s/%%$nom_campo\.size%%/$campos{"_b$nom_foto_original"}/ig;
        }

    } elsif ($val_campo =~ /https?:\/\//i) {    # url externa
        # parseo dimensiones
        $foto_dimx = $campos{"_w$nom_campo"};    # _wfotofija_art200
        $foto_dimy = $campos{"_h$nom_campo"};

        # warn "foto externa nom_campo[$nom_campo] nom_campo_w[_w$nom_campo] foto_dimx[$foto_dimx]";
        $buffer =~ s/%%_w$nom_campo%%/$foto_dimx/ig;
        $buffer =~ s/%%_h$nom_campo%%/$foto_dimy/ig;

        $buffer =~ s/%%$nom_campo\.width%%/$foto_dimx/ig;
        $buffer =~ s/%%$nom_campo\.height%%/$foto_dimy/ig;
    }
    return $buffer;
}

# ---------------------------------------------------------------
# Parsea en vista del artic los campos vtxt asociadas al mismo.
sub _parsing_vtxt {
    my ($this, $buffer, $nom_campo, $val_campo) = @_;

    $val_campo =  '' if (!defined($val_campo));

    my ($looptit, $tithtml) = $this->_get_data4subtit($buffer, $nom_campo);
    my $curr_nrotit = '0';
    my %hash_subtits;

    if ($prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'VTXT_RELPATH_LINK'} eq 'SI') {
        # Saca el server name.
        $val_campo =~ s/https?:\/\/$this->{cpan_server_name}\//\//isg;
        $val_campo =~ s/https?:\/\/$this->{public_server_name}\//\//isg;
    }

    $val_campo =~ s/^\s+//g;
    $val_campo =~ s/^(&nbsp;)+//ig;

    $val_campo =~ s/\s+$//g;
    $val_campo =~ s/(&nbsp;)+$//ig;

    # Corrige issue ffox 3.6.11, elimina las rutas relativas, ppalmente de las imagenes.
    # Da por sentado que si encuentra alguna ruta relativa, al sacarle los puntos quedara absoluta
    # Esto siempre sera true para imagenes prontus
    if ($val_campo =~ /src *= *("|')\.\.\//i) {
        $val_campo =~ s/src *= *("|')(\.\.\/)+(\w)/src=$1\/$3/ig;
    } elsif ($val_campo =~ /src *= *("|')\w+?\//i) {
        $val_campo =~ s/(src) *= *("|')(\w+?)\//$1=$2\/$3\//ig;
    }

    my $vtxt_aux = $val_campo;

    # Elimina funciones para double click del html del vtxt.
    $vtxt_aux =~ s/ondblclick *= *".+?"//isg;

    # Parsea los subtits en el vtxt
    my $vtxt_aux_consubtit = $vtxt_aux;
    while ($vtxt_aux =~ /(<(\w+)\s+[^<>]*class=["']subtit([a-zA-Z0-9\-]*)["'][^>]*>[^<]*?<\/\2>)/isg) {
        my $tag          = $1;
        my $level_subtit = $3;
        my $tit          = $tag;
        $tit =~ s/<.*?>//sg;
        next if ($tit !~ /\w/);
        if ($tithtml) {
            # Usa mini tpl si es q esta en la plantilla del art.
            my $tithtml_aux = $tithtml;
            $tithtml_aux        =~ s/%%_SUBTIT%%/$tit/i;
            $tithtml_aux        =~ s/%%_SUBTIT_ANAME%%/$nom_campo\_T$curr_nrotit/i;
            $tithtml_aux        =~ s/%%_level_subtit%%/$level_subtit/ig;
            $vtxt_aux_consubtit =~ s/\Q$tag\E/$tithtml_aux/is;
        } else {
            $vtxt_aux_consubtit =~ s/\Q$tag\E/<a name="$nom_campo\_T$curr_nrotit">$tag<\/a>/is;
        }

        # Guarda item a incluir en el menu
        my $looptit_aux = $looptit;
        $looptit_aux =~ s/%%_SUBTIT_KEY%%/#$nom_campo\_T$curr_nrotit/ig;
        $looptit_aux =~ s/%%_SUBTIT%%/$tit/ig;
        $looptit_aux =~ s/%%_level_subtit%%/$level_subtit/ig;
        # acumula item para ponerlo despues en el menu
        $hash_subtits{$curr_nrotit} = '<!--STIT_' . $nom_campo . '-->' . $looptit_aux . '<!--/STIT_' . $nom_campo . '-->';
        $curr_nrotit++;
    }

    foreach my $st (sort { $a <=> $b } (keys %hash_subtits)) {
        # print STDERR "st[$st]\n";
        my $item_menu = $hash_subtits{$st};
        # Reemplazar en la pagina misma.
        $buffer =~ s/%%_SUBTIT_LOOP_$nom_campo%%(.*?)%%\/_SUBTIT_LOOP_$nom_campo%%/$item_menu%%_SUBTIT_LOOP_$nom_campo%%$1%%\/_SUBTIT_LOOP_$nom_campo%%/isg;
    }
    # Eliminar TITLOOP sobrante.
    $buffer =~ s/%%_SUBTIT_LOOP_$nom_campo%%(.*?)%%\/_SUBTIT_LOOP_$nom_campo%%//isg;
    # Eliminar BLOQUE %%TITHTML%% sobrante
    $buffer =~ s/%%_SUBTIT_HTML_$nom_campo%%(.*?)%%\/_SUBTIT_HTML_$nom_campo%%//isg;

    # Detectar si el VTXT esta insertando invocaciones a funciones js derivadas del uso del boton media.
    # Si las encuentra, incluye el JS necesario.

    # Para que no escapear código PHP
    # elimina secuencias <?
    $vtxt_aux_consubtit =~ s/< *\?/< &#63;/g;

    # Para el plugin prontus_insert
    my $safe_counter = 0;
    while ($vtxt_aux_consubtit =~ /(<prontus_insert(.*?)>.*?<\/prontus_insert>)/is) {
        my $code  = $1;
        my $attrs = $2;

        $safe_counter++;
        if ($safe_counter > 100) {
            print STDERR "[vtxt] Salida de seguridad <prontus_insert> $this->{ts}\n";
            $vtxt_aux_consubtit =~ s/<prontus_insert(.*?)>.*?<\/prontus_insert>//isg;
            last;
        }
        # <prontus_insert data-type="js" data-code="var%20myvar%20%3D%20'Hello'%3B%0D%0Aalert(myvar)%3B">Código Javascript</prontus_insert>

        my $newnode = '';
        if ($attrs =~ / data-type="(php|ssi|ssi2|ssi_virtual)"/) {
            my $tipo = $1;
            if ($attrs =~ / data-src="(.*?)"/) {
                my $file = $1;

                if ($file =~ /^\// && $file !~ /\/..\//) {
                    if (!-f "$this->{document_root}$file") {
                        $newnode = "Archivo No Existe [$this->{document_root}$file]";
                    } else {
                        if ($tipo eq 'ssi') {
                            $newnode = '<!--#include file="' . $file . '" -->';
                        } elsif ($tipo eq 'ssi_virtual' || $tipo eq 'ssi2') {
                            $newnode = '<!--#include virtual="' . $file . '" -->';
                        } else {
                            $newnode = '<?php include($_SERVER["DOCUMENT_ROOT"] . "' . $file . '"); ?>';
                        }
                    }
                } else {
                    $newnode = "Archivo no tiene ruta absoluta [$file]";
                }
            }
        } elsif ($attrs =~ / data-type="js"/) {
            if ($attrs =~ / data-src="(.*?)"/) {
                my $file = $1;
                if ($file =~ /^\// && $file !~ /\/..\//) {
                    if (-f "$this->{document_root}$file") {
                        $newnode = '<script src="' . $file . '"></script>';
                    } else {
                        $newnode = "Archivo No Existe [$this->{document_root}$file]";
                    }
                } elsif ($file =~ /^https?:\/\//) {
                    $newnode = '<script src="' . $file . '"></script>';
                }
            } elsif ($attrs =~ / data-code="(.*?)"/s) {
                my $jscode = $1;
                $jscode = uri_unescape($jscode);
                $jscode = decode_entities(decode_entities($jscode));
                $newnode = '<script>'."\n".$jscode."\n".'</script>';
            }
        }
        $code = quotemeta $code;
        $vtxt_aux_consubtit =~ s/$code/$newnode/is;
    }

    # para el plugin html_insert
    $safe_counter = 0;
    while ($vtxt_aux_consubtit =~ /(<html_insert(.*?)>.*?<\/html_insert>)/is) {
        my $code  = $1;
        my $attrs = $2;

        $safe_counter++;
        if ($safe_counter > 100) {
            print STDERR "[vtxt] Salida <html_insert> $this->{ts}\n";
            $vtxt_aux_consubtit =~ s/<html_insert(.*?)>.*?<\/html_insert>//isg;
            last;
        }
        # <html_insert data-code="&amp;lt;iframe width=&amp;quot;560&amp;quot; height=&amp;quot;315&amp;quot; src=&amp;quot;https://www.youtube.com/embed/FLcyRrbh1RI?rel=0&amp;quot; frameborder=&amp;quot;0&amp;quot; allow=&amp;quot;encrypted-media&amp;quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;">Código HTML</html_insert>

        my $newnode = '';
        if ($attrs =~ / data-code="(.*?)"/s) {
            my $htmlcode = $1;
            $htmlcode = uri_unescape($htmlcode);
            $htmlcode = decode_entities(decode_entities($htmlcode));
            $newnode = "\n<div>".$htmlcode."</div>\n";
        }
        $code = quotemeta $code;
        $vtxt_aux_consubtit =~ s/$code/$newnode/is;
    }

    # Prontus Card - <prontus_card />
    $safe_counter = 0;
    while ($vtxt_aux_consubtit =~ /(<prontus_card(.*?)>.*?<\/prontus_card>)/is) {
        my $code  = $1;
        my $attrs = $2;

        $safe_counter++;
        if ($safe_counter > 100) {
            print STDERR "[vtxt] Salida <prontus_card> $this->{ts}\n";
            $vtxt_aux_consubtit =~ s/<prontus_card(.*?)>.*?<\/prontus_card>//isg;
            last;
        }

        # <prontus_card data-ts="20210912213325" data-plt="card_large">Prontus Card 'card_large' - Artículo 20210912213325</prontus_card>

        my $newnode = '';
        my ($dataTs, $dataPlt, $dataUrl, $dataExtJson);

        if ($attrs =~ / data-ts="(.*?)"/s) {
            $dataTs = $1;
        }

        if ($attrs =~ / data-plt="(.*?)"/s) {
            $dataPlt = $1;
            $dataPlt =~ s/[^a-z0-9_]+//sig;
        }

        if ($attrs =~ / data-url="(.*?)"/s) {
            $dataUrl = $1;
        }

        if ($attrs =~ / data-data="(.*?)"/s) {
            $dataExtJson = $1;
        }

        if ($dataTs && $dataPlt) {
            # Encontrar html de plantilla
            my $dirPlt = "$prontus_varglb::DIR_SERVER/$prontus_varglb::PRONTUS_ID/plantillas/vtxt/cards";
            my $pathPlt = "$dirPlt/$dataPlt.html";

            print STDERR "pathPlt[$pathPlt]\n";

            if (-f $pathPlt) {
                my $bufferPlt = &glib_fildir_03::read_file($pathPlt);
                # Convertir <script *> en type text/template.
                $bufferPlt =~ s/<script(.*?)>/<script type=\"text\/template\">/sg;

                $newnode = "<div class=\"prontus-card-container\"><div class=\"prontus-card-content\" data-ts=\"$dataTs\" data-prontus-id=\"$prontus_varglb::PRONTUS_ID\" style=\"display:none;\">\n$bufferPlt\n</div></div>";
            }
        }

        if ($dataUrl && $dataPlt && $dataExtJson) {
            # Encontrar html de plantilla
            my $dirPlt = "$prontus_varglb::DIR_SERVER/$prontus_varglb::PRONTUS_ID/plantillas/vtxt/cards";
            my $pathPlt = "$dirPlt/$dataPlt.html";

            if (-f $pathPlt) {
                my $bufferPlt = &glib_fildir_03::read_file($pathPlt);

                # Convertir <script *> en type text/template.
                $bufferPlt =~ s/<script(.*?)>/<script type=\"text\/template\">/sg;

                $dataExtJson =~ s/&quot;/"/sg;

                $newnode = "<div class=\"prontus-card-container\"><div class=\"prontus-card-content\" data-url=\"$dataUrl\" data-data='$dataExtJson' style=\"display:none;\">\n$bufferPlt\n</div></div>";
            }
        }

        $code = quotemeta $code;
        $vtxt_aux_consubtit =~ s/$code/$newnode/is;
    }

    # Se le indica que no se deben escapear los PHP
    $buffer = &lib_prontus::replace_in_artic($vtxt_aux_consubtit, $nom_campo, $buffer, 1);

    return $buffer;
}

# ---------------------------------------------------------------
# Parsea el buffer de artículo buscando campos de subtítulo.
# Obtiene un template de indice de titulos, y busca las marcas _SUBTIT_HTML_xx, devuelve el loop repetido para cada título y el valor de cada título.
sub _get_data4subtit {
    my ($this, $buffer, $nom_campo) = @_;

    # Obtencion de template de indice de titulos
    my ($looptit, $tithtml);

    # La cosa es sin framesets, o sea el indice va en la pagina misma.
    $buffer =~ /%%_SUBTIT_LOOP_$nom_campo%%(.*?)%%\/_SUBTIT_LOOP_$nom_campo%%/is;
    $looptit = $1;    # segmento html que se repite para cada titulo.
    $looptit =~ s/%%_prontus_id%%/$prontus_varglb::PRONTUS_ID/g;

    # Plantilla para los subtitulos
    if ($buffer =~ /%%_SUBTIT_HTML_$nom_campo%%(.+?)%%\/_SUBTIT_HTML_$nom_campo%%/is) {
        $tithtml = $1;
        $tithtml =~ s/%%_prontus_id%%/$prontus_varglb::PRONTUS_ID/g;
    }

    return ($looptit, $tithtml);
}
# ---------------------------------------------------------------
# Parsea el buffer de artículo buscando tags de postproceso.
# Agrega cada postproceso encontrado a la variable Artic->post_proceso_lista.
# Retorna el buffer limpio.
sub _genera_lista_post_procesos {
    my ($this, $buffer)= @_;

    while ($buffer =~ /<!--POST_PROCESO=(.+?)-->/isg) {
        # $this->{post_proceso_lista} .= "$1\t";
        push(@{$this->{post_proceso_lista}}, $1);
    }
    $buffer =~ s/<!--POST_PROCESO=.+?-->//isg;
    return $buffer;
}

# ---------------------------------------------------------------
# Devuelve la friendly URL correcta para el artículo en cuestión según la configuración de Prontus.
sub get_friendly_url {
    my ($this) = @_;
    # carga datos del xml si no estan
    $this->get_xml_content();

    my %artic_data = ();
    $artic_data{'fid'} = $this->{xml_content}{'_fid'};
    $artic_data{'custom_slug'} = $this->{xml_content}{'_custom_slug'};
    $artic_data{'slug'} = $this->{xml_content}{'_slug'};
    $artic_data{'_url'} = $this->{xml_content}{'_url'};
    $artic_data{'nom_seccion'} = $this->{xml_content}{'_nom_seccion1'};
    $artic_data{'nom_tema'} = $this->{xml_content}{'_nom_tema1'};
    $artic_data{'nom_subtema'} = $this->{xml_content}{'_nom_subtema1'};

    my $friendly_url = &lib_prontus::parse_filef("%%_FILEURL%%", $this->{xml_content}{'_txt_titular'}, $this->{ts}, $this->{prontus_id}, $this->{xml_content}{'_plt'}, \%artic_data);

    return $friendly_url;
}

# Calcula y retorna el tamaño que utiliza el artículo en el disco físico
# Indexar imagenes, multimedia y adjuntos por separado, html se obtiene del total - el contenido
sub get_disk_size {
    my ($this) = @_;
    my $ts = $this->{ts};

    my $disk_size = -s ($this->{dst_xml}."/$ts.xml");

    # paginas principales
    my @file_list = glob ($this->{dst_pags}."/$ts*");
    foreach my $file (@file_list) {
        $disk_size += -s $file;
    }

    # vistas alternativas
    foreach my $mv (keys %{$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'MULTIVISTAS'}}) {
        @file_list = glob ($this->{dst_pags}."-$mv/$ts*");
        foreach my $file (@file_list) {
            $disk_size += -s $file;
        }
    }

    # paginas paralelas
    @file_list = glob ($this->{dst_pagspar}."/$ts*");
    foreach my $file (@file_list) {
        $disk_size += -s $file;
    }

    my $imag_size = 0;
    # imagenes del articulo
    @file_list = glob ($this->{dst_foto}."/foto*$ts*");
    foreach my $file (@file_list) {
        $imag_size += -s $file;
    }

    my $asset_size = 0;
    # documentos/adjuntos del articulo
    @file_list = glob ($this->{dst_asocfile}. '/*');
    foreach my $file (@file_list) {
        $asset_size += -s $file;
    }

    my $media_size = 0;
    # multimedia: datos del video y thumbs
    @file_list = glob ($this->{dst_mm_thumbs}. "/*$ts*");
    foreach my $file (@file_list) {
        $media_size += -s $file;
    }

    # multimedia: videos y calidades
    @file_list = glob ($this->{dst_multimedia}. "/*$ts*");
    foreach my $file (@file_list) {
        if (-d $file) {
            my @subdir_file_list = glob ($file. '/*');
            foreach my $sub_file (@subdir_file_list) {
                $media_size += -s $sub_file;
            }
        } else {
            $media_size += -s $file;
        }
    }

    $disk_size = 0 if (!defined($disk_size));
    $imag_size = 0 if (!defined($imag_size));
    $asset_size = 0 if (!defined($asset_size));
    $media_size = 0 if (!defined($media_size));

    $disk_size = $disk_size + $imag_size + $asset_size + $media_size;

    return ($disk_size, $imag_size, $asset_size, $media_size);
}

sub update_art_size {
    my ($this, $total_size, $disk_size, $imag_size, $asset_size, $media_size) = @_;

    $this->{xml_data} = lib_prontus::replace_in_xml($this->{xml_data}, "_media_size", $media_size, 0);
    $this->{xml_data} = lib_prontus::replace_in_xml($this->{xml_data}, "_asset_size", $asset_size, 0);
    $this->{xml_data} = lib_prontus::replace_in_xml($this->{xml_data}, "_imag_size", $imag_size, 0);
    $this->{xml_data} = lib_prontus::replace_in_xml($this->{xml_data}, "_disk_size", $disk_size, 0);
    $this->{xml_data} = lib_prontus::replace_in_xml($this->{xml_data}, "_total_size", $total_size, 0);
}

sub update_s3_size {
    my ($this, $s3_size, $s3_media_size, $s3_asset_size) = @_;

    $this->{xml_data} = lib_prontus::replace_in_xml($this->{xml_data}, "_s3_size", $s3_size, 0);
    $this->{xml_data} = lib_prontus::replace_in_xml($this->{xml_data}, "_s3_media_size", $s3_media_size, 0);
    $this->{xml_data} = lib_prontus::replace_in_xml($this->{xml_data}, "_s3_asset_size", $s3_asset_size, 0);
}

1;

# Para desplegar la doc. desde linea de comandos:
# perldoc Artic.pm
# Para generar la doc en html:
# perldoc -T -o html Artic.pm > Artic.pm.html

__END__

=encoding utf8

=head1 NAME

B<Artic.pm> - Clase para manipulacion de articulos Prontus, por medio de la creacion de un objeto tipo "Artic"

=for comment

=head1 DESCRIPTION

Esta clase contiene operaciones tipicas a realizar con los artículos, como por ejemplo leer su XML,
obtener el path de este, generar el html, parsear sus datos, etc., sin necesidad de conocer la logica
interna, solo basta instanciar correctamente el objeto de tipo 'Artic'. Esta clase se utiliza en todas
las partes de Prontus en donde se manipulan los articulos.
Ademas de ser util internamente para las CGIs de prontus, tambien puede servir para simplificar
la programacion de post-procesos, scripts de migracion de contenidos, etc., utilizando los metodos
publicos y atributos de la clase.

=for comment

=head1 SYNOPSIS

Instanciar y acceder a un metodo publico:

    use Artic;

    # Creacion del objeto Artic.

OPCION 1: utilizando variables globales prontus ($prontus_varglb::NOM_VARIABLE).
Esta es la opción más segura y recomendada, pero requiere cargar la conf de prontus primero,
de la sgte. forma:

    # Path conf y load config de prontus
    use prontus_varglb; &prontus_varglb::init();
    use lib_prontus;
    my $prontus_id = 'prontus_noticias';
    &lib_prontus::load_config_by_id($prontus_id);

    # Creacion de obj Artic usando variables globales Prontus
    my $artic_obj = Artic->new(
                    'prontus_id'         => $prontus_varglb::PRONTUS_ID,
                    'public_server_name' => $prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'PUBLIC_SERVER_NAME'},
                    'cpan_server_name'   => $prontus_varglb::CPAN_SERVER_NAME,
                    'document_root'      => $prontus_varglb::DIR_SERVER,
                    'ts'                 => $ts,
                    'no_check_dirs'      => 1
                    # ********** Ver Nota (1) ************
                    'campos' => {})
                    || die("Error inicializando objeto articulo: $Artic::ERR TS[$ts]\n");

Nota (1): la opción "no_check_dirs" se utiliza para no verificar ni crear los directorios asociados al artículo, se puede usar
cuando se necesita acceder a los datos de un artículo existente sin modificarlo


OPCION 2: Creacion de obj Artic sin usar var. globales Prontus

    # Creacion del obj Artic, sin var Prontus
    my $artic_obj = Artic->new(
                    'prontus_id'        => 'prontus_noticias',
                    'public_server_name'=> 'www.misitio.cl',
                    'cpan_server_name'  => 'prontus.misitio.cl',
                    'document_root'     => '/sites/misitio/web',
                    'ts'                => '20081023172849',
                    # ********** Ver Nota (1) ************
                    'campos' => {})
                    # ********** Ver Nota (2) ************
                    || die("Error inicializando objeto articulo: $Artic::ERR TS[$ts]\n");

Ejemplo de utilizacion de metodos del obj Artic

    # Generar vista o html del articulo.
    $artic_obj->generar_vista_art($mv)
                || die("Error: $Artic::ERR");

    # carga hash con campos del XML del articulo, con nombre_campo => valor_campo
    my %campos_xml = $artic_obj->get_xml_content();


Nota (1):
El atributo 'campos' en el constructor es para poder pasarle un hash con todos los datos
necesarios para poder crear un articulo. Para el caso de que solo se necesite LEER info del articulo,
basta con dejarlo de la forma indicada en el ejemplo.

Nota (2):
Si se trata del procesamiento de un solo articulo, generalmente el die() sera suficiente.
pero en caso de que estemos procesando varios articulos dentro de un bucle, en donde se desea que
aunque uno falle, se continue con el resto, en vez de die() se puede realizar algo como lo siguiente:

    foreach my $ts (@arr_artic) {
        # Creacion del objeto Artic.
        my $artic_obj = Artic->new(...)
                        || print("Error inicializando objeto articulo: $Artic::ERR TS[$ts]\n");

        # En la instruccion siguiente al constructor, revisamos si se creo el objeto y en caso negativo
        # hacemos un next al siguiente articulo
        next if (!ref($artic_obj));

        # Si el artic se instancio OK, realizo el resto de las operaciones, por ejemplo,
        # reparsearlo en todas las vistas a partir de los datos del XML:
        # Para la vista ppal:
        $artic_obj->generar_vista_art('') || print $Artic::ERR;

        # Para vistas secundarias, agregar:
        foreach my $mv (keys %{$prontus_varglb::CONFIG{$prontus_varglb::PRONTUS_ID}{'MULTIVISTAS'}}) {
            $artic_obj->generar_vista_art($mv) || print $Artic::ERR;
        };

    };

=for comment
# ---------------------------------------------------------------

=head2 CONSTRUCTOR

=over 8

=item B<new()>

Devuelve un nuevo objeto Artic. Los parametros son los indicados en la sinopsis.
Es importante considerar que los campos pasados en el hash 'campos' al constructor deben tener
los nombres correspondientes a los campos del XML del artic.
para un ejemplo sobre como pasar pasarle este hash al objeto a fin de ingresar un nuevo articulo,
revisar la lib_artic.pm

B<Parametros>

$clase

  la referencia a usar para $this.

$artic

  referencia a hash con los campos del artículo.

B<Retorna>

  Referencia al objeto artic.

=for comment
# ---------------------------------------------------------------

=head2 VARIABLES DE CLASE ESTATICAS

=over 8

=item C<$Artic::XML_BASE>

Contiene el XML matriz de los articulos prontus:

    <?xml version="1.0" encoding="UTF-8"?>
    <artic_data>
    <_private>
    <_txt_titular></_txt_titular>
    <_slug></_slug>
    <_custom_slug></_custom_slug>
    <_art_autoinc></_art_autoinc>
    <_users_id></_users_id>
    <_fid></_fid>
    <_plt></_plt>
    <_fechap></_fechap>
    <_horap></_horap>
    <_fechae></_fechae>
    <_horae></_horae>
    <_soloportadas></_soloportadas>
    <publicarvistas></publicarvistas>
    <_seccion1></_seccion1>
    <_tema1></_tema1>
    <_subtema1></_subtema1>
    <_seccion2></_seccion2>
    <_tema2></_tema2>
    <_subtema2></_subtema2>
    <_seccion3></_seccion3>
    <_tema3></_tema3>
    <_subtema3></_subtema3>
    <_nom_seccion1></_nom_seccion1>
    <_nom_tema1></_nom_tema1>
    <_nom_subtema1></_nom_subtema1>
    <_nom_seccion2></_nom_seccion2>
    <_nom_tema2></_nom_tema2>
    <_nom_subtema2></_nom_subtema2>
    <_nom_seccion3></_nom_seccion3>
    <_nom_tema3></_nom_tema3>
    <_nom_subtema3></_nom_subtema3>
    <_autor></_autor>
    <_txt_bajada></_txt_bajada>
    <_alta></_alta>
    <_indexar></_indexar>
    <_tax></_tax>
    <_tags></_tags>
    <_tagnames></_tagnames>
    <_multitag_seccion></_multitag_seccion>
    <_multitag_tema></_multitag_tema>
    <_multitag_subtema></_multitag_subtema>
    <_galeria_prontus_conf></_galeria_prontus_conf>
    <_galeria_flag_imagen_temporal></_galeria_flag_imagen_temporal>
    <_galeria_prontus_str></_galeria_prontus_str>
    <_txt_galeria_description_total></_txt_galeria_description_total>
    <_txt_galeria_credito_total></_txt_galeria_credito_total>
    <_modtime></_modtime>
    </_private>
    <_public>
    </_public>
    </artic_data>

=item C<$Artic::ERR>

Variable que se carga con el texto descriptivo del error en caso de que un metodo retorne 0.
Este texto esta adecuado para ser presentado directamente al usuario, ya que el texto completo
del error, incluyendo paths y backtrace es enviado al STDERR.

=back

=for comment
# ---------------------------------------------------------------

=head2 ATRIBUTOS

Luego de creado el objeto, se puede acceder a una serie de atributos con informacion del articulo.
Estos atributos se acceden de la forma:

    my $attr = $artic_obj->{nombre_atributo};

Los atributos son:

B<ts>

    timestamp del articulo, por ejemplo '20081023172849'

B<fechac>

    string correspondiente a la fecha de creacion del artic,
    por ejemplo '20081023'

B<fullpath_xml>

    path completo al XML del articulo.
    por ejemplo: '/sites/misitio.cl/web/prontus_noticias/site/artic/20081023/xml/20081023172849.xml'

B<prontus_id>

    nombre del publicador al que pertenece el articulo,
    por ejemplo 'prontus_noticias'

B<public_server_name>

    nombre del servidor donde estara publicado el articulo,
    por ejemplo 'www.misitio.cl'

B<cpan_server_name>

    nombre del servidor desde donde se publicara el articulo,
    usualmente es el mismo que public_server_name

B<document_root>

    document root asociado al server del articulo,
    por ejemplo '/sites/misitio/web'

B<campos>

    Hash con todos los campos pasados al constructor.
    Para acceder a alguno en particular:
    my $plt = $artic_obj->{campos}->{'_plt'};

B<dst_base>

    path completo del directorio donde se ubican los archivos del articulo,
    por ejemplo: '/sites/misitio.cl/web/prontus_noticias/site/artic/20081023'

B<dst_pags>

    path completo del directorio donde se ubica el html del articulo,
    por ejemplo: '/sites/misitio.cl/web/prontus_noticias/site/artic/20081023/pags'

B<dst_xml>

    path completo del directorio donde se ubica el xml del articulo,
    por ejemplo: '/sites/misitio.cl/web/prontus_noticias/site/artic/20081023/xml'

B<dst_asocfile>

    path completo del directorio donde se ubican los asocfile del articulo,
    por ejemplo: '/sites/misitio.cl/web/prontus_noticias/site/artic/20081023/asocfile/20081023172849'

B<dst_foto>

    path completo del directorio donde se ubican las imag del articulo,
    por ejemplo: '/sites/misitio.cl/web/prontus_noticias/site/artic/20081023/imag'

B<dst_multimedia>

    path completo del directorio donde se ubican los mmedia del articulo,
    por ejemplo: '/sites/misitio.cl/web/prontus_noticias/site/artic/20081023/mmedia'

B<lnk_multimedia>

    path relativo al doc. root del directorio donde se ubican los mmedia del articulo,
    por ejemplo: '/prontus_noticias/site/artic/20081023/mmedia'

B<pathdir_plt>

    path completo del directorio donde se ubican las plantillas de articulo,
    por ejemplo: '/sites/misitio.cl/web/prontus_noticias/plantillas/artic/fecha'

B<pathdir_plt_macros>

    path completo del directorio donde se ubican las macros de articulo,
    por ejemplo: '/sites/misitio.cl/web/prontus_noticias/plantillas/artic/fecha/macros'

B<pathdir_plt_pags>

    path completo del directorio donde se ubican las plantillas de la vista default de articulo,
    por ejemplo: '/sites/misitio.cl/web/prontus_noticias/plantillas/artic/fecha/pags'



=for comment
# ---------------------------------------------------------------

=head2 METODOS PUBLICOS

=for comment
# ---------------------------------------------------------------

=over 8

=item B<generar_vista_art($mv, $plt, $is_paralela)>

Genera HTML (o lo que corresponda a la plantilla) del articulo y graba el archivo.

Toma como entrada los datos del XML, no los que vengan en el hash $this->{campos}.

B<Parametros>

$mv

  nombre de la vista en la que se desea generar el html articulo.
  para la vista default, poner ''. Para otras vistas poner por ejemplo: 'pda'

$plt

  nombre de la plantilla de la vista a generar

$is_paralela

  Flag para indicar si es una plantilla paralela

$only_pproc

  Flag para indicar si está corriendo por postprocesos.

B<Retorna>

1 | 0 (exito o error).
En caso de error, ademas setea la variable de clase $Artic::ERR con el texto correspondiente, y hace print STDERR de ese error.

=for comment
# ---------------------------------------------------------------

=item B<generar_xml_artic()>

Genera la estructura de datos esencial para guardar en XML, y escribe a disco. Los datos derivados que se calculan u obtienen en base a los esenciales no se almacenan aqui.

B<Parametros>

void

B<Retorna>

1 | 0 (exito o error) directo de _flush_xml()

=for comment
# ---------------------------------------------------------------

=item B<get_fullpath_artic($mv, $plt)>

Obtiene el path completo al html del artic., por ejemplo:
'/sites/misitio.cl/web/prontus_noticias/site/artic/20081023/pags/20081023172849.html'

B<Parametros>

$mv

  nombre de la vista en la que se desea generar el html articulo.
  para la vista default, poner ''. Para otras vistas poner
  por ejemplo: 'pda'

$plt

  nombre del archivo de plantilla de artic.
  Puede obtenerse de los campos recibidos del form, o bien,
  desde el xml, invocando antes a get_xml_content()

B<Retorna>

El path completo al html del artic., por ejemplo:
'/sites/misitio.cl/web/prontus_noticias/site/artic/20081023/pags/20081023172849.html'

=for comment
# ---------------------------------------------------------------

=item B<get_xml_content()>

Obtiene un hash con los campos del XML del artic.
Cada entrada es del tipo nom_campo => valor_campo
Ya vienen eliminados los bloques CDATA, en caso de haberlos.

Uso tipico:

  my %campos_xml = $artic_obj->get_xml_content();

B<Retorna>

Hash con los campos del XML del artic.

=for comment
# ---------------------------------------------------------------

=item B<parse_artic_data($fullpath_vista, $buffer, \%ref_campos_xml, \%vars_adicionales, $use_xml_tax)>

Parsea todas las marcas del XML mas algunas derivadas de estas en el buffer dado.
Este metodo NO elimina las marcas que queden sin sustituir.

Uso tipico:

  my $fullpath_vista = $artic_obj->get_fullpath_artic($mv, $campos_xml{'_plt'});
  $buffer = $artic_obj->parse_artic_data($fullpath_vista, $buffer, \%ref_campos_xml, \%vars_adicionales);

B<Parametros>

$fullpath_vista

  path completo del html del articulo. Se requiere minimo el relativo, si es el abs, se despulga. Se puede obtener facilmente asi:

  my $fullpath_vista = $artic_obj->get_fullpath_artic($mv, $campos_xml{'_plt'});

$buffer

  string, normalmente html, en donde se deben parsear los datos del articulo

\%ref_campos_xml

  Hash con los campos del XML.
  Si el articulo ya existe, este dato se obtiene facilmente asi:
  my %campos_xml = $artic_obj->get_xml_content();

\%vars_adicionales

  Hash con campos custom que se deseen parsear en el buffer.

$use_xml_tax

  especifica usar las taxonomías desde el xml de artículo.


B<Retorna>

String con buffer parseado


=for comment
# ---------------------------------------------------------------

=item B<get_nom_artic($plt)>

Obtiene el nombre del archivo del articulo, por ejemplo:
'20081023172849.html'

B<Parametros>

$plt

  nombre del archivo de plantilla de artic.
  Puede obtenerse de los campos recibidos del form, o bien,
  desde el xml, invocando antes a get_xml_content():

  my %campos_xml = $artic_obj->get_xml_content();
  my $plt = $campos_xml{'_plt'};

  o bien,desde el mismo hash de campos pasado al constructor:

  my $plt = $artic_obj->{campos}->{'_plt'};

B<Retorna>:

El path completo al html del artic., por ejemplo:
'/sites/misitio.cl/web/prontus_noticias/site/artic/20081023/pags/20081023172849.html'

=for comment
# ---------------------------------------------------------------

=item B<art_insert_bd($base, $regenerar_registro)>

Crea art. en base de datos prontus a partir de info del xml.

Uso tipico:

  # Conectar a BD Prontus
  my ($base, $msg_err_bd) = &lib_prontus::conectar_prontus_bd();
  $regenerar_registro = 1;
  $autoinc = $ARTIC_OBJ->art_insert_bd($base, $regenerar_registro);

  my ($this, $base, $regenerar_registro) = @_; # $regenerar_registro: 1 => sacar autoinc del xml
                                                 # basicamente apara regeneracion de tabla de arts.

B<Parametros>

$base

  handler de la BD Prontus

$regenerar_registro

  booleano 1 o 0. 1 implica sacar el autoinc del xml y usar ese. Si es 0, implica asignar uno nuevo.
  Este parametro es util setearlo en 1 para regeneracion de tabla de articulos.

B<Retorna>

Id autoincremento del articulo.

=for comment
# ---------------------------------------------------------------

=item B<exec_post_proceso_art($pathArticulo, $isNew)>

Una vez escrito el articulo, ubica el nombre del script de post-proceso (con extensión y con path absoluto completo) y lo ejecuta.
El script de postproceso es optativo y puede ir en cualquier parte del articulo. Pueden ser varios.
Usa $this->{post_proceso_lista}, una lista de postprocesos separada por \t

B<Parametros>

$pathArticulo:

  el path completo al artículo.

$isNew N o E (nuevo o edición)

B<Retorna>

  void

=for comment
# ---------------------------------------------------------------

=item B<art_update_bd($base)>

Actualiza el ID autoincremental en base de datos prontus a partir de info del xml

B<Parametros>

$base

  handler de la BD Prontus

B<Retorna>

ID autoincremental del artículo en base de datos, o 0.

=for comment
# ---------------------------------------------------------------

=item B<setear_autoinc($autoinc)>

Ingresa o actualiza en el xml el id autoincremental de base de datos a partir del valor pasado.

B<Parametros>

$autoinc

  el ID autoincremental a escribir en el xml del artículo.

B<Retorna>

  void

=for comment
# ---------------------------------------------------------------

=item B<guardar_pub_exp_artic($base, $comando)>

Recibe una ref a la base de datos, y un comando (pub|exp).
Hace un insert o update de los datos en la tabla ART_PUBLICAR o ART_DESPUBLICAR, según el comando pasado.
Si la fecha pasada es anterior a la fecha actual, intenta hacer un delete de la misma tabla.

B<Parametros>

$base

  handler de la BD Prontus

$comando

  'pub' o 'exp' para especificar publicar o despublicar.

B<Retorna>

  1 en caso de éxito, 0 de otro modo. Setea $Artic::Err y hace print STDERR del eventual error.

=for comment
# ---------------------------------------------------------------

=item B<borrar_pub_exp_artic($base, $comando)>

Recibe una ref a la base de datos, y un comando (pub|exp).
Borra todos los registros en la tabla ART_PUBLICAR o ART_DESPUBLICAR con fecha anterior a la actual, según el comando pasado.

B<Parametros>

$base

  handler de la BD Prontus

$comando

  'pub' o 'exp' para especificar publicar o despublicar.

B<Retorna>

  1 en caso de éxito, 0 de otro modo. Setea $Artic::Err y hace print STDERR del eventual error.

=for comment
# ---------------------------------------------------------------

=item B<tags2bd($base)>

Guarda tags en base de datos. Se invoca al guardar un artículo.

B<Parametros>

$base

  handler de la BD Prontus

B<Retorna>

1 si todo anduvo bien, 0 de otro modo. Hace print STDERR a STDERR del eventual error.

=for comment
# ---------------------------------------------------------------

=item B<multitags2bd($base)>

Puebla tabla de multitags en base de datos. Se invoca al guardar un artículo.

B<Parametros>

$base

  handler de la BD Prontus

B<Retorna>

1 si todo anduvo bien, 0 de otro modo. Hace print STDERR a STDERR del eventual error.

=for comment
# ---------------------------------------------------------------

=item B<friendly_v4_2bd($base)>

Puebla la tabla de urls friendly v4 en base de datos. Se invoca al regenerar la db de prontus.

B<Parametros>

$base

  handler de la BD Prontus

Usa $this->{xml_content}

B<Retorna>

1 si todo anduvo bien, 0 de otro modo. Hace print STDERR a STDERR del eventual error.

=for comment
# ---------------------------------------------------------------

=item B<genera_friendly_v4($base)>

Genera todo lo que necesita la friendly v4 para funcionar
Guarda el titular formateado en la BD
Genera el symlink en el filesystem
Si no esta activada friendly 4 no hace nada
TODO: revisar si no hace demasiado.

B<Parametros>

$base

  handler de la BD Prontus

B<Retorna>

1 si todo anduvo bien, 0 de otro modo. Hace print a STDERR del eventual error.

=for comment
# ---------------------------------------------------------------

=item B<verifica_colision_url_titular($base, $titularV4)>

Revisa el titular a usar para url friendly v4 para evitar colisiones con otros existentes.

B<Parametros>

$base

  handler de la BD Prontus

$titularV4:

  el titular modificado para uso en url v4.

B<Retorna>

0 si todo anduvo bien, el TS del otro artículo encontrado de otro modo. TODO: Debe emitir el eventual error.

=for comment
# ---------------------------------------------------------------

=item B<check_publish_art()>

Verifica si el articulo es visible. El criterio de publicación es
- si es una preview, es visible. De otro modo, deben cumplirse:
- alta = 1
- fecha de publicacion es anterior a la fecha actual
- fecha de expiracion es posterior a la fecha actual
- si esta marcado "solo despublicar de portadas" el articulo es visible

B<Parametros>

  void

B<Retorna>

1 si se cumplen los criterios detallados, 0 de otro modo.

=for comment
# ---------------------------------------------------------------

=item B<borra_pags_artic($base)>

Invocada al eliminar vistas de artículos para el cron fechas.
Elimina todos los archivos de vista.
Elimina archivos de friendly URL.
No elimina XML ni archivos asociados.

B<Parametros>

$base

  handler de la BD Prontus

B<Retorna>

''

=for comment
# ---------------------------------------------------------------

=item B<borra_artic($base, $no_run_regen, $list_procs)>

Elimina un artículo con todos sus datos asociados.

B<Parametros>

$base

  handler de la BD Prontus

$no_run_regen

  flag para evitar regenerar tagonómicas.

$list_procs

  referencia a hash de lista de procesos a ejecutar como parte de la eliminación.

B<Retorna>

''

=for comment
# ---------------------------------------------------------------

=item B<borrar_artic_files($dir_aux)>

Elimina los archivos y directorios asociados a un artículo dentro del directorio especificado.

B<Parametros>

$dir_aux

  el directorio que contiene subdirectorios y/o archivos a borrar. ej: dir de imágenes, de multimedia, de thumbnails

B<Retorna>

  void

=for comment
# ---------------------------------------------------------------

=item B<art_delete_bd($base)>

Borra la fila de un artículo en la tabla ART de la base de datos.

B<Parametros>

$base

  handler de la BD Prontus

B<Retorna>

1 si logró borrar, 0 de otro modo. Hace print STDERR del eventual error.

=for comment
# ---------------------------------------------------------------

=item B<art_update_bd($base)>

Actualiza el registro de un artículo en la tabla ART de la base de datos.

B<Parametros>

$base

  handler de la BD Prontus

B<Retorna>

Id autoincremental del artículo en db, 0 de otro modo. setea $Artic::ERR con el eventual error.

=for comment
# ---------------------------------------------------------------

=item B<art_insert_bd($base, $regenerar_registro)>

Agrega el registro de un artículo en la tabla ART de la base de datos.

B<Parametros>

$base

  handler de la BD Prontus

$regenerar_registro

  1 => sacar autoinc del xml para regeneracion de tabla de arts.

B<Retorna>

Id autoincremental del artículo en db, 0 de otro modo. setea $Artic::ERR con el eventual error y hace print STDERR del error.

=for comment
# ---------------------------------------------------------------

=item B<ajusta_campos_art4bd($base, %refhash_campos)>

Limpia y formatea los campos a ingresar en base de datos.

TODO: hacer que reciba y retorne una referencia, o que reciba y retorne un hash.

B<Parametros>

$refhash_campos

  referencia a hash con los campos del artículo.

B<Retorna>

Hash con los campos formateados correctamente.

=for comment
# ---------------------------------------------------------------

=item B<get_xdata()>

Obtiene hash con las marcas externas (y su contenido) desde el directorio /<prontus_id>/xdata

No permite marcas que comiencen con _, para evitar conflictos con las reservadas.

Las marcas que se carguen por esta via sobreescribirán las marcas del articulo.

Estructura del dir:

<prontus_id>/
    xdata/
       <sistema_externo>/
          <nom_marca_grl1>.txt
          ...
          <nom_marca_grlN>.txt
          <ts>/
              <nom_marca1>.txt
              ...
              <nom_marcaN>.txt

B<Parametros>

  void

B<Retorna>

  Hash con los datos

=for comment
# ---------------------------------------------------------------

=item B<set_preview_artic()>

Establece que el ts para todos los efectos es 'preview'. Usar al generar preview de artículos.

B<Parametros>

  void

B<Retorna>

  void

=for comment
# ---------------------------------------------------------------

=for comment
# ---------------------------------------------------------------

=back

=for comment
# ---------------------------------------------------------------

=head2 METODOS PRIVADOS

=for comment
# ---------------------------------------------------------------

=over 8

=item B<_flush_xml()>

Escribe a disco el XML del artículo.

Usada en generar_xml_artic()

B<Parametros>

void

B<Retorna>

1 (éxito), 0 (error). En caso de error setea $Artic::ERR y hace print STDERR.

=for comment
# ---------------------------------------------------------------

=item B<_guarda_recursos($type)>

Guarda en xml y disco los recursos de un cierto tipo (FOTO, ASOCFILE, MULTIMEDIA) asociados al articulo

Usada en generar_xml_artic()

B<Parametros>

$type

  /^(foto|asocfile|multimedia)$/

B<Retorna>

void

=for comment
# ---------------------------------------------------------------

=item B<_get_newnom_arch($type, $nom_campo, $val_campo, $arch_existente, $nom_foto_orig)>

Obtiene un nuevo nombre para el recurso del tipo dado.

Usada en _guarda_recursos()

B<Parametros>

$type

  /^(foto|asocfile|multimedia)$/

$nom_campo

  el nombre del campo a procesar

$val_campo

  el valor contenido en $nom_campo

$arch_existente

  el archivo existente

$nom_foto_orig

  el archivo de foto original

B<Retorna>

El nuevo nombre.

=for comment
# ---------------------------------------------------------------

=item B<_get_nom_asocfile($texto, $af_existente)>

Retorna nombre de archivo para asocfile_i, con extension y sin path

Usada en _get_newnom_arch()

B<Parametros>

$texto

  el texto original del nombre de archivo

$af_existente

  el archivo original

B<Retorna>

El nuevo nombre.

=for comment
# ---------------------------------------------------------------

=item B<_artic_ajusta_horas()>

Ajusta horas desde los valores de entrada antes de guardarlos en el XML

Usada en generar_xml_artic()

B<Parametros>

void

B<Retorna>

void

=for comment
# ---------------------------------------------------------------

=item B<_get_fulldir_vista($mv)>

Lee el path en this->dst_pags, retorna el mismo path pero adaptado a la vista pasada.

Ejemplo: my $fulldir_vista = $this->_get_fulldir_vista('movil');

Usada en generar_vista_art() y get_fullpath_artic()

B<Parametros>

$mv

  nombre de la vista: '', 'movil', 'en', ...

B<Retorna>

El path contenido en this->dst_pags adaptado según la vista.

=for comment
# ---------------------------------------------------------------

=item B<_set_dirs()>

Configura directorios de trabajo del articulo.

Usada en new()

B<Parametros>

void

B<Retorna>

1

=for comment
# ---------------------------------------------------------------

=item B<_parsing_recursos($nom_campo, $val_campo, $buffer)>

Parsea en vista del artic los recursos de un cierto tipo (ASOCFILE, MULTIMEDIA) asociados al mismo

Usada en parse_artic_data()

B<Parametros>

$nom_campo

  ASOCFILE_xx, MULTIMEDIA_xx

$val_campo

  el valor del campo $nom_campo

$buffer

  el buffer del artículo

B<Retorna>

El buffer procesado.

=for comment
# ---------------------------------------------------------------

=item B<_parsing_fotos($nom_campo, $val_campo, $buffer)>

Parsea en vista del artic las fotos (FOTOFIJA) asociadas al mismo

Usada en parse_artic_data()

B<Parametros>

$nom_campo

  FOTOFIJA_xx

$val_campo

  el valor del campo $nom_campo

$buffer

  el buffer del artículo

B<Retorna>

El buffer procesado.

=for comment
# ---------------------------------------------------------------

=item B<_parsing_vtxt($buffer, $nom_campo, $val_campo)>

Parsea en vista del artic los campos vtxt asociadas al mismo.

Usada en parse_artic_data()

B<Parametros>

$buffer

  el buffer del artículo

$nom_campo

  el nombre del campo a procesar, debe ser un vtxt.

$val_campo

  el valor del campo $nom_campo

B<Retorna>

El buffer procesado.

=for comment
# ---------------------------------------------------------------

=item B<_get_data4subtit($buffer, $nom_campo)>

Parsea el buffer de artículo buscando campos de subtítulo.

Obtiene un template de indice de titulos, y busca las marcas _SUBTIT_HTML_xx, devuelve el loop repetido para cada título y el valor de cada título.

Usada en _parsing_vtxt().

B<Parametros>

$buffer

  el buffer del artículo

$nom_campo

  el nombre del campo a buscar

B<Retorna>

Array con el segmento de código y el título correspondientes.

=for comment
# ---------------------------------------------------------------

=item B<_aplica_fotofija_actions($lafoto, $actions, $foto_existente, $nom_foto_orig)>

Aplica a una foto acciones del editor (resize, crop, etc.)

Usada en _add_foto_sitiolocal_articulolocal().

B<Parametros>

$lafoto

  nombre de la imagen

$actions

  las acciones a efectuar sobre la foto (resize, crop...)

$foto_existente

  el archivo original

$nom_foto_orig

  el nombre original de la foto

B<Retorna>

Array ($nom_arch, $finalw, $finalh)

=for comment
# ---------------------------------------------------------------

=item B<_add_foto_prontus_xml($wfoto, $hfoto, $nom_arch, $size, $original)>

Parsea los datos de una foto y los guarda al xml de artículo.

Usada en _guarda_recursos(), _add_foto_sitiolocal_articulolocal(), _add_foto_sitiolocal_articuloexterno(), _add_foto_filesystem()

B<Parametros>

$wfoto

  el ancho de la foto

$hfoto

  El alto de la foto

$nom_arch

  el nombre del archivo
$size

  tamaño de archivo en bytes

$original

  indica si la foto es original, fue subida por el usuario o no (fue recortada o generada por fotofija)

B<Retorna>

void

=for comment
# ---------------------------------------------------------------

=item B<_genera_fotofija($nom_foto, $fotofija, $maxw, $maxh, $cuadrar)>

Genera una fotofija con nuevas dimensiones a partir de los datos pasados.
Valida datos y llama a $this->_add_foto_sitiolocal_articulo_local()

Usada en CGIs pese a llamarse con _. cgi-cpn/prontus_art_sbmit.cgi, cgi-cpn/prontus_art_sbmit_ajax.cgi

B<Parametros>

$nom_foto

  El nombre de la foto_n. Ej: foto_134520110619122632.jpg

$fotofija
  El nombre de la foto fija (nombre marca marca)

$maxw

  El ancho maximo

$maxh

  El alto maximo

$cuadrar

  Si se debe cuadrar la imagen o no: si|<anything>

B<Retorna>

Exito:

  Array ( '', #mensaje de error
    $final_dimx, # dimensión X
    $final_dimy, # dimensión Y
    $final_nomfoto, # nombre final
    $final_relfoto # path relativo
    )

Error:
  Array( Mensaje de error, '', '', '','' )

=for comment
# ---------------------------------------------------------------

=item B<_get_nom_foto($path_foto, $nom_foto_orig)>
Obtiene nombre de la siguiente foto que toca para este articulo. Dicho nombre comenzara con el correlativo sgte al ultimo ingresado

Aplica un valor random para minimizar que el nombre no se repita

Procesa el nombre original de la foto para agregarlo al path final

Usada en _aplica_fotofija_actions(), _add_foto_filesystem(), _guarda_binfoto_prontus(), _get_newnom_arch()

B<Parametros>

$path_foto

  el path a la imagen

$nom_foto_orig

  Nombre original de la foto. si se pasa este parametro, se usa para extraer la extensión, se limpia el nombre y se agrega al final del nuevo nombre.

B<Retorna>

El nuevo nombre a usar.

=for comment
# ---------------------------------------------------------------

=item B<_guarda_fotosfijas()>

Guarda imágenes externas o locales al prontus. Delega el trabajo a subs como _add_foto_sitioexterno().

Usada en generar_xml_artic(),

B<Parametros>

void

B<Retorna>

void

=for comment
# ---------------------------------------------------------------

=item B<_guarda_fotos_posting($nom_campo_foto, nom_foto)>

Guarda imágenes enviadas via form posting.

Usada en _guarda_recursos()

B<Parametros>

$nom_campo_foto

  nombre del campo xml para la imagen. Usa el match /^foto_posting_.*?$/

$nom_foto

  nombre del archivo

B<Retorna>

void

=for comment
# ---------------------------------------------------------------

=item B<_guarda_binfoto_prontus($val_campo, $final_dimx, $final_dimy, $binfoto, $arch_existente, $nom_foto_orig)>

Genera un archivo binario a partir de una foto. En caso de que el original sea un GIF, genera un archivo PNG.
Usada en _add_foto_sitiolocal_articuloexterno() y en add_foto_sitiolocal_articulolocal()

B<Parametros>

$val_campo

  path al archivo

$final_dimx

  dimension horizontal final

$final_dimy

  dimensión vertical final

$binfoto

  archivo de foto

$arch_existente

  archivo original con la foto.

$nom_foto_orig

  nombre inicial del archivo de la foto.

B<Retorna>

El nombre del archivo guardado, o '' en caso de error en parámetros.

=for comment
# ---------------------------------------------------------------

=item B<_add_foto_filesystem($path_foto, $nom_foto_orig)>

Copia un archivo de foto, y agrega los datos de la foto (nombre, dimensiones) al xml del artículo.

A pesar de llamarse con _, es usada solamente en cgis. (prontus_art_sbmit*.cgi, prontus_art_upload.cgi, prontus_editor_imag_guardar.cgi)

B<Parametros>

$path_foto

  el path al archivo sin document_root

$nom_foto_orig

  El path a la foto original

B<Retorna>

El nombre nuevo de la foto.

=for comment
# ---------------------------------------------------------------

=item B<_add_foto_sitioexterno($nom_campo, $val_campo)>

Carga en el objeto xml_data los datos de una foto externa a Prontus.

Usada en _guarda_fotosfijas()

B<Parametros>

$nom_campo

  El nombre del campo a procesar

$val_campo

  El valor de $nom_campo

B<Retorna>

void

=for comment
# ---------------------------------------------------------------

=item B<_add_foto_sitiolocal_articulolocal($nom_campo, $val_campo, $maxw, $maxh, $dx, $dy, $cuadrar, $nom_campo_orig, $nom_foto_orig)>

Guarda una foto del artículo basada en otra foto del mismo artículo

Usada en _guarda_fotosfijas(), _guarda_fotos_posting(), _genera_fotofija()

B<Parametros>

$nom_campo

  nombre del campo a procesar.

$val_campo

  valor del campo $nom_campo

$maxw

  ancho maximo

$maxh

  alto maximo

$dx

  dimension x

$dy

  dimensión y

$cuadrar

  flag para indicar si debe cuadrarse la imagen

$nom_campo_orig

  nombre del campo del que proviene la foto

$nom_foto_orig

  nombre del archivo de foto original

B<Retorna>

Array (
    $final_dimx,
    $final_dimy,
    $nom_arch,
    $val_campo,
)

=for comment
# ---------------------------------------------------------------

=item B<_add_foto_sitiolocal_articuloexterno($nom_campo, $val_campo, $maxw, $maxh, $dx, $dy, $cuadrar)>

Guarda una foto externa al articulo pero local al prontus

Si la foto externa es mas grande de lo que debe ser, se redimensiona. Requiera o no ser redimensionada, siempre se genera una nueva foto prontus para el articulo y luego esta nueva foto, ya internalizada, se asigna a la fotofija.

Usada en _guarda_fotosfijas()

B<Parametros>

$nom_campo

  nombre del campo a procesar.

$val_campo

  valor del campo $nom_campo

$maxw

  ancho maximo

$maxh

  alto maximo

$dx

  dimension x

$dy

  dimensión y

$cuadrar

  flag para indicar si debe cuadrarse la imagen

B<Retorna>

void

=for comment
# ---------------------------------------------------------------

=item B<_check_ext_foto($path_foto)>

Valida que el nombre de archivo pasado termine en una extensión de imagen válida ( jpe?g | gif | png )

Usada en _guarda_binfoto_prontus(), _get_newnom_arch()

B<Parametros>

$path_foto

  el path al archivo

B<Retorna>

1 en caso de ser una extensión válida, 0 de otro modo

=for comment
# ---------------------------------------------------------------

=item B<_get_new_ts()>

Asigna un nuevo timestamp único para el artículo

Usada en new()

B<Parametros>

void

B<Retorna>

$ts, el nuevo timestamp del artículo (yyyymmddhhmmss)

=for comment
# ---------------------------------------------------------------

=item B<_check_artic_dirs()>

Revisa la existencia de todos los directorios necesarios para la creación del artículo.

Setea la variable global $ERR, TODO: probablemente hay que unificar esto con $Artic::ERR para hacer el código más legible.

Usada en _set_dirs()

B<Parametros>

void

B<Retorna>

1 (éxito), 0 (error)

=for comment
# ---------------------------------------------------------------
=item B<get_friendly_url()>

Obtiene la URL friendly del artículo según la configuración de Prontus.

Usada en prontus_art_sbmit.cgi para pasar la URL friendly a Twitter.

Usa la sub &lib_prontus::parse_filef().

B<Parametros>

void

B<Retorna>

Una string con la URL friendly (o la URL común).

=for comment
# ---------------------------------------------------------------


=item B<_genera_lista_post_procesos()>

Parsea el buffer de artículo buscando tags de postproceso.
Agrega cada postproceso encontrado a la variable Artic->post_proceso_lista.

B<Parametros>

$buffer, string, el buffer de plantilla.

B<Retorna>

el buffer de plantilla, limpio.

=for comment
# ---------------------------------------------------------------


=back

=head1 AUTHORS

Por favor reportar bugs usando el formulario de contacto en https://www.prontus.cl/

Alfredo Luco G., E<lt>aluco@altavoz.netE<gt>

Pierina Riffo B., E<lt>priffo@altavoz.netE<gt>

Yerko Chapanoff,

César Vasquez I.,

Eduardo Alarcón G., E<lt>ealarcon@altavoz.net<gt>

Jorge Olivares R., E<lt>jolivares@altavoz.net<gt>

Sebastián Castillo T., E<lt>scastillo@altavoz.net<gt>

Adriano Varoli Piazza G., E<lt>avaroli@altavoz.net<gt>

Manuel Tabilo C.,

Ricardo Vivanco, E<lt>rvivanco@altavoz.net<gt>

Daniel García O.,

Anderson Bracho D.,

Ignacio Nicovani C.,

Waldo Morales A.,

Hector Cortés,

Nicolás Albornoz R.,

Manuel Pedraza G.,

=cut
