Publi

Ejemplo para analizar y procesar expresiones matemáticas (y más) en PHP (Parsear en PHP)

Parsear expresiones en PHP

Una de las mayores armas de doble filo en cuanto a los lenguajes de programación interpretados es la función o expresión eval. Esta orden nos suele permitir escribir una cadena de caracteres con un texto en el mismo lenguaje que estamos escribiendo y ejecutarla. Es decir, si nos encontramos en Python y escribimos:

1
eval("100+23")

Devolverá 123. O si estamos en PHP y hacemos:

1
2
<?php
eval("echo 100+23;");

En la cadena de caracteres que introducimos podemos utilizar variables, bucles, condiciones… vamos, todo lo que nos permite el lenguaje de programación. Lo que ponemos en la cadena se ejecutará de forma nativa en el lenguaje. Esto puede ser muy útil a veces. Pero tendremos que pensar un poco antes de utilizarlo.

Peligros de utilizar eval

Quiero hacer una introducción más o menos rápida y sin centrarme en un lenguaje de programación. En principio, hemos visto que eval() admite una cadena de caracteres como entrada. Si pensamos un poco, que esa cadena de caracteres sea fija, es decir, que en nuestro programa tuviera algo como los ejemplos de arriba no tiene mucho sentido, porque podemos prescindir de eval y todo se ejecutaría igual. Bueno, es cierto que en algunos casos concretos nos puede interesar la pequeña pérdida de tiempo que introduce eval(), aunque hay formas más elegantes de hacerlo. O incluso podemos hacer que el lenguaje que estemos utilizando no utilice el caché de la expresión que estamos escribiendo. Pero son, de hecho casos muy raros y excepcionales.

Otro de los usos es construir una cadena de caracteres nosotros mismos, en tiempo de ejecución y pasárselo a eval(). En este caso, podemos sacar de una base de datos un fragmento de código para ejecutar o incluso el usuario puede introducir una expresión en un formulario y nosotros ejecutarlo. Imaginad que le pedimos al usuario una fórmula para calcular el precio de venta de un producto y claro, al usuario no vamos a pedirle programación. Eso sí, podemos tener un grave agujero de seguridad en nuestra aplicación. Sencillamente, porque eval() va a ejecutar todo lo que nosotros le pasemos. Lo mismo hace operaciones matemáticas, que llamada a cualquier función del lenguaje y ahí está el problema, al usuario no le podemos dar tanto control. Un gran poder conlleva una gran responsabilidad y, otra cosa no, pero el usuario, de responsable tiene poco y, sobre todo si estamos hablando de una plataforma web en la que cualquiera podría explotar una vulnerabilidad, mucho más.

Incluso si se nos ocurre filtrar lo que le pasamos a eval(), acotando el número de expresiones que le pasamos y cómo se lo pasamos, no me fiaría yo mucho de que algún usuario malintencionado fuera capaz de, incluso pasando los filtros, ejecutar código malicioso. Así que, mi consejo, es que si alguna vez ejecutamos eval() sea solo para hacer pruebas, a la hora de hacer tests de nuestro programa y algún caso contado más, pero no hacerlo con código en producción.

Expresiones del usuario

Así que, ¿qué hacemos si aún así queremos que el usuario ejecute sus propias expresiones? No nos queda otra que analizar nosotros la expresión, y evaluarla. Como si estuviéramos programando un intérprete de un lenguaje de programación nosotros mismos. Así que, manos a la obra, vamos a construir un parser que analice una expresión y luego la procesaremos. Este programa tendrá algunas expresiones internas que evaluará directamente, aunque luego tendremos la opción de añadir funciones personalizadas.

Atención: El código que voy a mostrar tiene ya un tiempo, aunque para escribir el post me he asegurado de que funciona con PHP7. Está basado en un código de hack.code.it de hace mucho tiempo, retocado y con algunas mejoras por mi parte.

No pretendo crear un lenguaje de programación, solo un sistema en el que los usuarios puedan pasar de forma segura expresiones matemáticas, pueda analizarlas, evaluarlas y dar un resultado. Aunque todo se puede complicar, podemos utilizar funciones como senos, cosenos, raíces, etc, incluso funciones creadas por nosotros. Y debemos tener una manera de decirle las funciones que admitimos.

Programando…

Vale, voy a poner un montón de código por aquí, para tener funcionando esto. El código puede tener bugs, y, por supuesto podéis enviármelos para que poco a poco vayamos mejorando el programa. Yo lo he utilizado para unos pocos casos, pero realmente son muy pocos. El script soporta funciones, variables y operaciones como suma, resta, multiplicación, división y potencia.

Primero, aunque no utilizaremos ningún paquete, vamos a configurar composer para generar el autoload de los ficheros:
composer.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
    "name": "gasparfm/simplePHPexpr",
    "keywords": ["math", "expressions", "functions"],
    "homepage": "https://github.com/gasparfm/simplePHPexpr",
    "description": "An expression parser in PHP",
    "license": "MIT",
    "authors": [
        {
            "name": "smassey",
            "homepage": "http://codehackit.blogspot.fr/"
        },{
            "name": "Gaspar Fernández",
            "homepage": "https://gaspar.totaki.com/"
        }
    ],
    "require": {
        "php": ">=5.6"
    },
    "autoload":     {
        "psr-0": {
            "spex": "src/"
        }
    }
}

Crearemos un fichero principal (main.php) en el mismo directorio que composer.json. El esquema de archivos y directorios será el siguiente

|- composer.json
|- main.php
|- src/
| |-spex/
| | |-exceptions/
| | | |- DivisionByZeroException.php
| | | |- MaxDepthException.php
| | | |- OutOfScopeException.php
| | | |- ParseTreeNotFoundException.php
| | | |- ParsingException.php
| | | |- UnknownFunctionException.php
| | | |- UnknownTokenException.php
| | |-scopes/
| | | |- Scope.php
| | | |- FunScope.php
| | |- Parser.php
| | |- Util.php

spex/Parser.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<?php
namespace spex;

/**
 * this model handles the tokenizing, the context stack functions, and
 * the parsing (token list to tree trans).
 * as well as an evaluate method which delegates to the global scopes evaluate.
 */


class Parser {
    protected $_content = null;
    protected $_context_stack = array();
    protected $_tree = null;
    protected $_tokens = array();
    protected $_options;

    public function __construct($options = array(), $content = null) {
        $this->_options = $options;
        if ( $content ) {
            $this->set_content( $content );
        }
    }

    /**
     * this function does some simple syntax cleaning:
     * - removes all spaces
     * - replaces '**' by '^'
     * then it runs a regex to split the contents into tokens. the set
     * of possible tokens in this case is predefined to numbers (ints of floats)
     * math operators (*, -, +, /, **, ^) and parentheses.
     */

    public function tokenize() {
        $this->_content = str_replace(array("\n","\r","\t"), '', $this->_content);
        $this->_content = preg_replace('~"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"(*SKIP)(*F)|\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\'(*SKIP)(*F)|\s+~', '', $this->_content);
        $this->_content = str_replace('**', '^', $this->_content);
        $this->_content = str_replace('PI', (string)PI(), $this->_content);
        $this->_tokens = preg_split(
            '@([\d\.]+)|([a-zA-Z_]+\(|,|=|\+|\-|\*|/|\^|\(|\))@',
            $this->_content,
            null,
            PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
        );
        return $this;
    }

    /**
     * this is the the loop that transforms the tokens array into
     * a tree structure.
     */

    public function parse() {
        # this is the global scope which will contain the entire tree
       $this->pushContext( new \spex\scopes\Scope($this->_options) );
        foreach ( $this->_tokens as $token ) {
            # get the last context model from the context stack,
            # and have it handle the next token
            $this->getContext()->handleToken( $token );
        }
        $this->_tree = $this->popContext();

        return $this;
    }

    public function evaluate() {
        if ( ! $this->_tree ) {
            throw new \spex\exceptions\ParseTreeNotFoundException();
        }
        return $this->_tree->evaluate();
    }

    /*** accessors and mutators ***/

    public function getTree() {
        return $this->_tree;
    }

    public function setContent($content = null) {
        $this->_content = $content;
        return $this;
    }

    public function getTokens() {
        return $this->_tokens;
    }


    /*******************************************************
     * the context stack functions. for the stack im using
     * an array with the functions array_push, array_pop,
     * and end to push, pop, and get the current element
     * from the stack.
     *******************************************************/


    public function pushContext(  $context ) {
        array_push( $this->_context_stack, $context );
        $this->getContext()->setBuilder( $this );
    }

    public function popContext() {
        return array_pop( $this->_context_stack );
    }

    public function getContext() {
        return end( $this->_context_stack );
    }
}

spex/Util.php
Este archivo proporciona compatibilidad con PHP5. Podemos adaptar el código para PHP7 y eliminar esta dependencia.

1
2
3
4
5
6
7
8
9
<?php
namespace spex;

class Util {
    public static function av($arr, $key, $default=null) {
        return (isset($arr[$key]))?$arr[$key]:$default;
    }

};

spex/scopes/Scope.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
<?php
namespace spex\scopes;

class Scope {
    protected $_builder = null;
    protected $_children_contexts = array();
    protected $_raw_content = array();
    protected $_operations = array();
    protected $options = array();
    protected $depth = 0;

    const T_NUMBER = 1;
    const T_OPERATOR = 2;
    const T_SCOPE_OPEN = 3;
    const T_SCOPE_CLOSE = 4;
    const T_FUNC_SCOPE_OPEN = 5;
    const T_SEPARATOR = 6;
    const T_VARIABLE = 7;
    const T_STR = 8;

    public function __construct(&$options, $depth=0) {
        $this->options = &$options;
        if (!isset($this->options['variables']))
            $this->options['variables'] = array();

        $this->depth = $depth;
        $maxdepth = \spex\Util::av($options, 'maxdepth',0);
        if ( ($maxdepth) && ($this->depth > $maxdepth) )
            throw new \spex\exceptions\MaxDepthException($maxdepth);
    }

    public function setBuilder( \spex\Parser $builder ) {
        $this->_builder = $builder;
    }

    public function __toString() {
        return implode('', $this->_raw_content);
    }

    protected function addOperation( $operation ) {
        $this->_operations[] = $operation;
    }

    protected function searchFunction ($functionName) {
        $functions = \spex\Util::av($this->options, 'functions', array());
        $func = \spex\Util::av($functions, $functionName);
        if (!$func)
            throw new \spex\exceptions\UnknownFunctionException($functionName);

        return $func;
    }

    /**
     * handle the next token from the tokenized list. example actions
     * on a token would be to add it to the current context expression list,
     * to push a new context on the the context stack, or pop a context off the
     * stack.
     */

    public function handleToken( $token ) {
        $type = null;
        $data = array();

        if ( in_array( $token, array('*','/','+','-','^','=') ) )
            $type = self::T_OPERATOR;
        if ( $token == ',' )
            $type = self::T_SEPARATOR;
        if ( $token === ')' )
            $type = self::T_SCOPE_CLOSE;
        if ( $token === '(' )
            $type = self::T_SCOPE_OPEN;
        if ( preg_match('/^([a-zA-Z_]+)\($/', $token, $matches) ) {
            $data['function'] = $matches[1];
            $type = self::T_FUNC_SCOPE_OPEN;
        }

        if ( is_null( $type ) ) {
            if ( is_numeric( $token ) ) {
                $type = self::T_NUMBER;
                $token = (float)$token;
            } elseif (preg_match('/^".*"$|^\'.*\'$/', $token)) {
                $type = self::T_STR;
            } elseif (preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $token)) {
                $type = self::T_VARIABLE;
            } else
                  echo "**".$token."**";
        }

        switch ( $type ) {
            case self::T_NUMBER:
            case self::T_OPERATOR:
                $this->_operations[] = $token;
                break;
            case self::T_STR:
                $delim = $token[0];
                $this->_operations[] = str_replace('\'.$delim, $delim, substr($token, 1, -1)) ;
                break;
            case self::T_VARIABLE:
                $this->_operations[] = array('
v', $token);
                break;
            case self::T_SEPARATOR:
                break;
            case self::T_SCOPE_OPEN:
                $this->_builder->pushContext( new namespace\Scope($this->options, $this->depth+1) );
            break;
            case self::T_FUNC_SCOPE_OPEN:
                $this->_builder->pushContext( new namespace\FunScope($this->options, $this->depth+1, $this->searchFunction($data['
function'])) );
            break;
            case self::T_SCOPE_CLOSE:
                $scope_operation = $this->_builder->popContext();
                $new_context = $this->_builder->getContext();
                if ( is_null( $scope_operation ) || ( ! $new_context ) ) {
                    # this means there are more closing parentheses than openning
                    throw new \spex\exceptions\OutOfScopeException();
                }
                $new_context->addOperation( $scope_operation );
            break;
            default:
                throw new \spex\exceptions\UnknownTokenException($token);
            break;
        }
    }

    private function isOperation($operation) {
        return ( in_array( $operation, array('
^','*','/','+','-','='), true ) );
    }

    protected function setVar($var, $value) {
        $this->options['
variables'][$var] = $value;
    }

    protected function getVar($var) {
        return \spex\Util::av($this->options['
variables'], $var, 0);
    }

    protected function getValue($val) {
        if (is_array($val)) {
            switch (\spex\Util::av($val, 0)) {
                case '
v': return $this->getVar(\spex\Util::av($val, 1));
                default:
                    throw new \spex\exceptions\UnknownValueException();
            }
        }
        return $val;
    }
    /**
     * order of operations:
     * - parentheses, these should all ready be executed before this method is called
     * - exponents, first order
     * - mult/divi, second order
     * - addi/subt, third order
     */
    protected function expressionLoop( & $operation_list ) {
        while ( list( $i, $operation ) = each ( $operation_list ) ) {
            if ( ! $this->isOperation($operation) )
                continue;
            $left =  isset( $operation_list[ $i - 1 ] ) ? $operation_list[ $i - 1 ] : null;
            $right = isset( $operation_list[ $i + 1 ] ) ? $operation_list[ $i + 1 ] : null;

            if ( (is_array($right)) && ($right[0]=='
v') )
                $right = $this->getVar($right[1]);
            if ( ($operation!='
=') && ( (is_array($left)) && ($left[0]=='v') ) )
                $left = $this->getVar($left[1]);

            if ( is_null( $right ) ) throw new \Exception('
syntax error');

            $first_order = ( in_array('
^', $operation_list, true) );
            $second_order = ( in_array('
*', $operation_list, true ) || in_array('/', $operation_list, true ) );
            $third_order = ( in_array('
-', $operation_list, true ) || in_array('+', $operation_list, true )|| in_array('=', $operation_list, true ) );
            $remove_sides = true;
            if ( $first_order ) {
                switch( $operation ) {
                    case '
^': $operation_list[ $i ] = pow( (float)$left, (float)$right ); break;
                    default: $remove_sides = false; break;
                }
            } elseif ( $second_order ) {
                switch ( $operation ) {
                    case '
*': $operation_list[ $i ] = (float)($left * $right); break;
                    case '
/':
                        if ($right==0)
                            throw new \spex\exceptions\DivisionByZeroException();
                        $operation_list[ $i ] = (float)($left / $right); break;
                    default: $remove_sides = false; break;
                }
            } elseif ( $third_order ) {
                switch ( $operation ) {
                    case '
+': $operation_list[ $i ] = (float)($left + $right);  break;
                    case '
-': $operation_list[ $i ] = (float)($left - $right);  break;
                    case '
=': $this->setVar($left[1], $right); $operation_list[$i]=$right; break;
                    default: $remove_sides = false; break;
                }
            }

            if ( $remove_sides ) {
                if (!$this->isOperation($operation_list[ $i + 1 ]))
                    unset($operation_list[ $i + 1 ]);
                unset ($operation_list[ $i - 1 ] );
                $operation_list = array_values( $operation_list );
                reset( $operation_list );
            }
        }
        if ( count( $operation_list ) === 1 ) {
            $val = end($operation_list );
            return $this->getValue($val);
        }
        return $operation_list;
    }

    # order of operations:
    # - sub scopes first
    # - multiplication, division
    # - addition, subtraction
    # evaluating all the sub scopes (recursivly):
    public function evaluate() {
        foreach ( $this->_operations as $i => $operation ) {
            if ( is_object( $operation ) ) {
                $this->_operations[ $i ] = $operation->evaluate();
            }
        }

        $operation_list = $this->_operations;

        while ( true ) {
            $operation_check = $operation_list;
            $result = $this->expressionLoop( $operation_list );

            if ( $result !== false ) return $result;
            if ( $operation_check === $operation_list ) {
                break;
            } else {
                $operation_list = array_values( $operation_list );
                reset( $operation_list );
            }
        }
        throw new \Exception('
failed... here');
    }
}

spex/scopes/FunScope.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
namespace spex\scopes;

class FunScope extends namespace\Scope {
    private $fun = null;

    public function __construct(&$options, $depth, $callable) {
        parent::__construct($options, $depth);
        $this->fun = $callable;
    }

    public function evaluate() {
        $arguments = parent::evaluate();
        return call_user_func_array($this->fun, (is_array($arguments))?$arguments:array( $arguments ) );
    }
}

spex/exceptions/UnknownFunctionException.php

1
2
3
4
5
6
7
8
9
10
<?php

namespace spex\exceptions;

class UnknownFunctionException extends \Exception
{
    function __construct($functionName) {
        parent::__construct('Unkown function '. $functionName);
    }
}

spex/exceptions/DivisionByZeroException.php
Todos los archivos de excecpción serán iguales, cambiando el nombre, el objetivo es diferenciar las excepciones para poder capturarlas.

1
2
3
4
5
6
7
<?php

namespace spex\exceptions;

class DivisionByZeroException extends \Exception
{
}

main.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
include('vendor/autoload.php');

$config = array(
    'maxdepth' => 2,
    'functions' => array(
        'sin' => function ($rads) { return sin(deg2rad($rads)); },
        'upper' => function ($str) { return strtoupper($str); },
        'fact' => function ($n) { $r=1; for ($i=2; $i<=$n; ++$i) $r*=$i; return $r; },
        'word' => function ($text, $nword) { $words=explode(' ', $text); return (isset($words[$nword]))?$words[$nword]:''; },
    )
);

$builder = new \spex\Parser($config);

while ( (fputs(STDOUT,'math > ')) && $e = fgets(STDIN) ) {
    if ( ! ($e = trim($e)) ) continue;
    if ( in_array( $e, array('quit','exit',':q') ) ) break;

    try {
        $result = $builder->setContent($e)->tokenize()->parse()->evaluate();
    } catch ( \spex\exceptions\UnknownTokenException $exception ) {
        echo 'unknown token exception thrown in expression: ', $e, PHP_EOL;
        echo 'token: "',$exception->getMessage(),'"',PHP_EOL;
        continue;
    } catch ( \spex\exceptions\ParseTreeNotFoundException $exception ) {
        echo 'parse tree not found (missing content): ', $e, PHP_EOL;
        continue;
    } catch ( \spex\exceptions\OutOfScopeException $exception ) {
        echo 'out of scope exception thrown in: ', $e, PHP_EOL;
        echo 'you should probably count your parentheses', PHP_EOL;
        continue;
    } catch ( \spex\exceptions\DivisionByZeroException $exception ) {
        echo 'division by zero exception thrown in: ', $e, PHP_EOL;
        continue;
    } catch ( \Exception $exception ) {
      echo 'exception thrown in ', $e, PHP_EOL;
      echo $exception->getMessage(), PHP_EOL;
      continue;
    }

    echo $result, PHP_EOL;
}

Probando el programa

Tras todo esto, podemos hacer una ejecución como esta:

php main.php
1+1
2
upper(«hola mundo»)
HOLA MUNDO
fact(10)
3628800
word(«Hola Mundo Mundial», 1)
Mundo
sin(fact(3))
0.10452846326765

Publicación del código

Quiero publicar en GitHub el código tal y como ha quedado con algunos ejemplos prácticos más en las próximas semanas.

Foto principal: unsplash-logoChris Liverani

También podría interesarte....

There are 53 comments left Ir a comentario

  1. OKBet /
    Usando Google Chrome Google Chrome 116.0.0.0 en Windows Windows NT

    There’s no doubt i would fully rate it after i read what is the idea about this article. You did a nice job.
    OKBet app

    1. Curso De Joyas /
      Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

      Ok. genial! Te ayudaré! Aqui tienes algun cursos de joyas en https://rylantjln95937.rimmablog.com/

      1. Wopi /
        Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

        Ok! Mira mi pagina nueva que he creado sobre cursos https://wopi.es/las-mejores-joyas/

    2. Seguidores De Tik Tok /
      Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

      Ok! Aqui tambien puedes ver como suben los seguidores de tiktok https://deanpgud74304.review-blogger.com/48131112/el-tiktok-y-sus-pros

  2. Jessicavi /
    Usando Google Chrome Google Chrome 116.0.0.0 en Windows Windows NT

    If you enjoy online games that can be accessed through a web browser, I concur with you that the Snake game is an excellent recommendation.

  3. alnogoom /
    Usando Google Chrome Google Chrome 117.0.0.0 en Windows Windows NT

    تتم مكافحة الحشرات بشكل شائع باستخدام مبيدات حشرية. ومع ذلك، يتم التعامل مع هذه المبيدات بحذر بالغ، حيث يمكن أن تؤثر على الصحة العامة والبيئة إذا استخدمت بطريقة غير صحيحة. يُنصح بالحد من الاعتماد على المبيدات الحشرية واللجوء إلى الوسائل الحيوية التي تستخدم طرقًا طبيعية مثل العزل الحراري، والأقمشة الخاصة بمكافحة الحشرات، وأدوات الصيد والمصائد، والزيوت العطرية. كما يمكن الاستعانة بخدمات مكافحة الحشرات المحترفة، حيث يمكن أن تستخدم تقنيات فعالة وآمنة لمكافحة الحشرات في المنازل والمباني.
    شركة متخصصة في مكافحة الحشرات

  4. Poppy playtime chapter 3 /
    Usando Google Chrome Google Chrome 117.0.0.0 en Windows Windows NT

    Thank you very much for taking your precious time to share with us this very useful information. Your article is very good and meaningful.

  5. 먹튀신고 /
    Usando Google Chrome Google Chrome 117.0.0.0 en Windows Windows NT

    After reading your article, I have benefited greatly. How to improve my social charm? I have posted the relevant links below for everyone to review.먹튀신고

  6. 메이저사이트 /
    Usando Google Chrome Google Chrome 117.0.0.0 en Windows Windows NT

    It is because I have read your article that I am able to improve my skills and ideas in this field. Thank you very much. 메이저사이트

  7. 토토사이트검증소 /
    Usando Google Chrome Google Chrome 117.0.0.0 en Windows Windows NT

    That’s a very different opinion from what I felt. It sounds amazing. I found out that there are various opinions. Once again, I felt that there was still a lot to learn. Thank you. 토토사이트검증소

  8. 토토사이트추천 /
    Usando Google Chrome Google Chrome 118.0.0.0 en Windows Windows NT

    I must say, I am deeply impressed. I rarely encounter a blog that is both educational and entertaining, no doubt you hit the nail on the head..토토사이트추천

  9. 토토사이트 /
    Usando Google Chrome Google Chrome 118.0.0.0 en Windows Windows NT

    You have written a very interesting article that inspires us in each subject and allows us to gain knowledge. Thank you for sharing this type of information with us to read this article.토토사이트

  10. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Actually, it’s pretty good to see! Tiler Adelaide

  11. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Excellent post! Concreters in Wollongong

  12. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Very useful and informative post! Tiling Townsville

  13. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Many thanks for sharing this! Adelaide Coolroom Hire

  14. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    You presented your ideas and thoughts really well on the paper. adelaide electrician

  15. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Please keep up the good work! drum lessons adelaide

  16. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Great Post! I learned a lot from this, Thank you! Canberra landscapers

  17. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Such a great post! Pro landscaping Adelaide

  18. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Thanks for sharing! Tiler Adelaide

  19. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Thanks for sharing this to public! Adelaide Landscaping

  20. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    I visited Your blog and got a massive number of informative articles. I read many articles carefully and got the information that I had been looking for for a long time. Hope you will write such a helpful article in future. Thanks for writing.Tilers in Hobart

  21. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Very informative post! tiler melbourne

  22. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    To be honest, I generally don’t read. But, this article caught my attention.digital marketing adelaide

  23. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    I am really impressed with your writing style. Keep it up! Landscapers Canberra

  24. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    It’s so kind of you! Solar Panels Adelaide

  25. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Many many thanks to you! Cleaning Services Adelaide

  26. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    What a great piece of article! seo adelaide

  27. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Very informative content. Thanks. tow truck wollongong

  28. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Thanks for letting us know. Tiler Adelaide

  29. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    I thik this is very helpfull post Canberra landscapers

  30. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Really nice article and helpful me Canberra landscapers

  31. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Nice article, waiting for your another Canberra landscapers

  32. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    This is awesome! Nice share. Ludwig Vistalite

  33. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Thats what I was looking for! adelaide air conditioning

  34. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Good to know about this! tilers wollongong Oak Flats

  35. James Song /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    This is really very nice blog and so informative Bathroom Tilers Sydney

  36. 메이저사이트 /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    This article gives a light with which we can observe reality. This is a very good one and provides in-depth information. Thanks for this great article.메이저사이트

  37. Kaabir Singh /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    The customers can avail the services of these agencies without even being present in India itself. Hence it is known as the ‘virtual’ Escort Service. This virtual Escort service is offered to the customers through the internet and through email. In this way, the clients will get the message to their inbox immediately after the date of the event.
    [url=https://www.saumyagiri.com/hari-nagar-ashram-call-girls.html] Escorts in Hari Nagar Ashram [/url]
    [url=https://www.saumyagiri.com/harsh-vihar-call-girls.html] Harsh Vihar Escorts Service [/url]
    [url=https://www.saumyagiri.com/hastsal-call-girls.html] Hastsal Call Girls[/url]
    [url=https://www.saumyagiri.com/hauz-khas-call-girls.html] Call Girls in Hauz khas [/url]
    [url=https://www.saumyagiri.com/himayunpur-call-girls.html] Himayunpur Escorts [/url]

  38. UFAAUTO789 /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    ufa789 เว็บแทงบอลที่ใหญ่ และการเงินมั่นคงที่สุด สล็อตออนไลน์ ไม่มีขั้นต่ำ ใช้ทุนน้อย เล่นได้ทุกเกม

  39. ยูฟ่า1688 /
    Usando Google Chrome Google Chrome 107.0.0.0 en Windows Windows NT

    I havent any word to appreciate this post.Really i am impressed from this post the person who create this post it was a great human..thanks for shared this with us. ยูฟ่า1688

  40. Uttarakhand Tour Packages /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    You need to take part in a contest for one of the greatest sites on the internet. I will recommend this web site! Uttarakhand Tour Packages

  41. Emma Harriet /
    Usando Google Chrome Google Chrome 121.0.0.0 en Windows Windows NT

    Gracias por tomarse su valioso tiempo para compartir esta información Jujuy escorts tan útil con nosotros. Tu artículo es muy bueno y significativo.

  42. SimonWhitehead /
    Usando Google Chrome Google Chrome 121.0.0.0 en Windows Windows NT

    Ihrem Alterungsprozess entgegenzuwirken verstehen wir als unsere Devise. Gemeinsam entscheiden wir nach Ihren Wünschen über die individuellen Möglichkeiten Ihrer Behandlung. Wir begleiten Sie mit fachkundiger Beratung und einem ganzheitlichen Ansatz auf dem Weg zu Ihrem Wohlbefinden. Botox

  43. SimonWhitehead /
    Usando Google Chrome Google Chrome 121.0.0.0 en Windows Windows NT

    very interesting keep posting. coach holiday

  44. SimonWhitehead /
    Usando Google Chrome Google Chrome 121.0.0.0 en Windows Windows NT

    I like this blog so much, saved to my bookmarks . frases estoicas

  45. SimonWhitehead /
    Usando Google Chrome Google Chrome 121.0.0.0 en Windows Windows NT

    I would like to thank you for the efforts you have made in writing this article. wholesale cement suppliers

  46. Seguidores TikTok /
    Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

    Me a venido muy bien yo he aplicado al seo todo esto que me cuentas y una parte me a servido.
    Os interesa leer mi nuevo post?
    https://wopi.es/2024/02/24/como-tomar-decisiones-informadas-para-el-crecimiento-de-tiktok

  47. Home Decor /
    Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

    I have to thank you for the efforts you have put in penning this site.
    I am hoping to check out the same high-grade blog posts from you in the future as well. In fact, your creative writing abilities has motivated me to get my own, personal blog now 😉

  48. BETFLIXSUPERVIP /
    Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

    I was recommended this blog by way of my cousin. I’m not sure whether or not this post is written through him as nobody else recognise such precise about my trouble. You’re incredible! Thanks! เบทฟิกvip

  49. Hafiz Kwabena /
    Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

    What’s up to all, the contents present at this web page are actually amazing for people knowledge, well, keep up the good work fellows.

    Emporiumperu.com

  50. Maha Kumbh Mela /
    Usando Google Chrome Google Chrome 123.0.0.0 en Windows Windows NT

    Your post is a gift, a glimpse into the depths of your soul. Thank you for sharing a piece of yourself with the world. Maha Kumbh Mela

Leave a Reply to Anónimo Cancle Reply