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 |
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.
Tabla de contenidos
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:
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: Chris Liverani
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
Ok. genial! Te ayudaré! Aqui tienes algun cursos de joyas en https://rylantjln95937.rimmablog.com/
Ok! Mira mi pagina nueva que he creado sobre cursos https://wopi.es/las-mejores-joyas/
Ok! Aqui tambien puedes ver como suben los seguidores de tiktok https://deanpgud74304.review-blogger.com/48131112/el-tiktok-y-sus-pros
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.
تتم مكافحة الحشرات بشكل شائع باستخدام مبيدات حشرية. ومع ذلك، يتم التعامل مع هذه المبيدات بحذر بالغ، حيث يمكن أن تؤثر على الصحة العامة والبيئة إذا استخدمت بطريقة غير صحيحة. يُنصح بالحد من الاعتماد على المبيدات الحشرية واللجوء إلى الوسائل الحيوية التي تستخدم طرقًا طبيعية مثل العزل الحراري، والأقمشة الخاصة بمكافحة الحشرات، وأدوات الصيد والمصائد، والزيوت العطرية. كما يمكن الاستعانة بخدمات مكافحة الحشرات المحترفة، حيث يمكن أن تستخدم تقنيات فعالة وآمنة لمكافحة الحشرات في المنازل والمباني.
شركة متخصصة في مكافحة الحشرات
Thank you very much for taking your precious time to share with us this very useful information. Your article is very good and meaningful.
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.먹튀신고
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. 메이저사이트
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. 토토사이트검증소
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..토토사이트추천
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.토토사이트
Actually, it’s pretty good to see! Tiler Adelaide
Excellent post! Concreters in Wollongong
Very useful and informative post! Tiling Townsville
Many thanks for sharing this! Adelaide Coolroom Hire
You presented your ideas and thoughts really well on the paper. adelaide electrician
Please keep up the good work! drum lessons adelaide
Great Post! I learned a lot from this, Thank you! Canberra landscapers
Such a great post! Pro landscaping Adelaide
Thanks for sharing! Tiler Adelaide
Thanks for sharing this to public! Adelaide Landscaping
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
Very informative post! tiler melbourne
To be honest, I generally don’t read. But, this article caught my attention.digital marketing adelaide
I am really impressed with your writing style. Keep it up! Landscapers Canberra
It’s so kind of you! Solar Panels Adelaide
Many many thanks to you! Cleaning Services Adelaide
What a great piece of article! seo adelaide
Very informative content. Thanks. tow truck wollongong
Thanks for letting us know. Tiler Adelaide
I thik this is very helpfull post Canberra landscapers
Really nice article and helpful me Canberra landscapers
Nice article, waiting for your another Canberra landscapers
This is awesome! Nice share. Ludwig Vistalite
Thats what I was looking for! adelaide air conditioning
Good to know about this! tilers wollongong Oak Flats
This is really very nice blog and so informative Bathroom Tilers Sydney
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.메이저사이트
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]
ufa789 เว็บแทงบอลที่ใหญ่ และการเงินมั่นคงที่สุด สล็อตออนไลน์ ไม่มีขั้นต่ำ ใช้ทุนน้อย เล่นได้ทุกเกม
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
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
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.
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
very interesting keep posting. coach holiday
I like this blog so much, saved to my bookmarks . frases estoicas
I would like to thank you for the efforts you have made in writing this article. wholesale cement suppliers
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
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 😉
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
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
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
It’s a game. Five dollars is free. Try it It’s not an easy game ->-> 바카라사이트.COM
It’s a game. Five dollars is free. Try it It’s not an easy game
->-> 카지노사이트 .COM