      1: <?php
      2: /*************************************************************************************/
      3: /*                                                                                   */
      4: /*      Thelia                                                                       */
      5: /*                                                                                   */
      6: /*      Copyright (c) OpenStudio                                                     */
      7: /*  email : info@thelia.net                                                      */
      8: /*      web : http://www.thelia.net                                                  */
      9: /*                                                                                   */
     10: /*      This program is free software; you can redistribute it and/or modify         */
     11: /*      it under the terms of the GNU General Public License as published by         */
     12: /*      the Free Software Foundation; either version 3 of the License                */
     13: /*                                                                                   */
     14: /*      This program is distributed in the hope that it will be useful,              */
     15: /*      but WITHOUT ANY WARRANTY; without even the implied warranty of               */
     16: /*      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                */
     17: /*      GNU General Public License for more details.                                 */
     18: /*                                                                                   */
     19: /*      You should have received a copy of the GNU General Public License            */
     20: /*      along with this program. If not, see <http://www.gnu.org/licenses/>.     */
     21: /*                                                                                   */
     22: /*************************************************************************************/
     23:
     24: namespace Thelia\Tpex;
     25:
     26: use Symfony\Component\HttpFoundation\Request;
     27: use Symfony\Component\EventDispatcher\EventDispatcherInterface;
     28:
     29: use Thelia\Tpex\Exception\FileNotFoundException;
     30: use Thelia\Tpex\Event\TpexEvent;
     31: use Thelia\Tpex\Tokenizer\Tokenize;
     32:
     33: /**
     34:  *
     35:  * Tpex is a parser used by Thelia CMS (http://thelia.net)
     36:  *
     37:  * Tpex use syntax like loop :
     38:  * <THELIA id="loopId" type="loopName" parameter1="value1" parameter2="value2">
     39:  *      #SUBTITUTION1 #SUBSTITUTION2 #SUBSTITUTION-n
     40:  * </THELIA>
     41:  *
     42:  * you have to implements your own loop, all the logical is in your loop not in Tpex
     43:  *
     44:  * filter syntax :
     45:  *
     46:  * [...(filter_name{param1, param2, ... }) ... ]
     47:  *
     48:  * base param syntax :
     49:  *
     50:  * PARAM_BASE_parameName=paramValue
     51:  *
     52:  *
     53:  * @author Manuel Raynaud <mraynaud@openstudio.fr>
     54:  */
     55:
     56: class Tpex
     57: {
     58:     /**
     59:      *
     60:      * $content contains string to parse
     61:      *
     62:      * @var string
     63:      */
     64:     protected $content;
     65:
     66:     /**
     67:      *
     68:      * the base directory for searching all files for inclusion
     69:      *
     70:      * @var string
     71:      */
     72:     protected $basedir;
     73:
     74:     /**
     75:      *
     76:      * @var \Symfony\Component\HttpFoundation\Request
     77:      */
     78:     protected $request;
     79:
     80:     /**
     81:      *
     82:      * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
     83:      */
     84:     protected $dispatcher;
     85:
     86:     /**
     87:      * associative array containing information for baseParam execution
     88:      *
     89:      * key is baseParam name
     90:      * value is the class implementing/extending base baseParam classes
     91:      *
     92:      * ex :
     93:      *
     94:      * $baseParam = array(
     95:      *  "secure" => "Thelia\BaseParam\Secure",
     96:      *  "reset" => "Thelia\BaseParam\reset",
     97:      *  "myBaseParam" => "My\Own\BaseParam"
     98:      * );
     99:      *
    100:      * @var Array
    101:      */
    102:     protected $baseParam = array();
    103:
    104:     protected $init = false;
    105:
    106:     /**
    107:      *
    108:      * config array
    109:      *
    110:      * cacheDir where cache is created
    111:      * cacheTimeFile File containing last cache pass
    112:      * fileLifeTime : file lifetime in seconds in cache system
    113:      * checkPeriod : Time in seconds between each cache examination
    114:      *
    115:      * @var array
    116:      */
    117:     protected $config = array(
    118:         "cacheDir" => false,
    119:         "cacheTimeFile" => ".time_cache",
    120:         "fileLifeTime" => 86400,
    121:         "checkPeriod" => 7200
    122:     );
    123:
    124:     protected $regex = array(
    125:         'include' => '/#INCLUDE[\s]*"([^"]*)"/',
    126:         'baseParam' => '/#PARAM_BASE_([a-zA-Z0-9_]+)[\s]*=[\s]*([0-9]*)[\s]*/'
    127:     );
    128:
    129:     /**
    130:      * @var \Thelia\Tpex\Tokenizer\Tokenize
    131:      */
    132:     protected $tokenizer;
    133:
    134:     /**
    135:      * Tpex constructor
    136:      *
    137:      * @param \Symfony\Component\HttpFoundation\Request $request
    138:      * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
    139:      */
    140:     public function __construct(Request $request, EventDispatcherInterface $dispatcher)
    141:     {
    142:         $this->request = $request;
    143:         $this->dispatcher = $dispatcher;
    144:
    145:         $this->tokenizer = new Tokenize($this->request, $this->dispatcher);
    146:     }
    147:
    148:     /**
    149:      * set the content to parse
    150:      *
    151:      * @param string $content
    152:      */
    153:     public function setContent($content)
    154:     {
    155:         $this->content = $content;
    156:         $this->init = true;
    157:     }
    158:
    159:     /**
    160:      * set the base directory. include are based from this base directory
    161:      *
    162:      * @param string $baseDir
    163:      */
    164:     public function setBaseDir($baseDir)
    165:     {
    166:         $this->basedir = rtrim($baseDir,"/")."/";
    167:     }
    168:
    169:     /**
    170:      * @param array $config
    171:      * @throws \RuntimeException
    172:      */
    173:     public function configure(array $config)
    174:     {
    175:         $this->config = array_merge($this->config, $config);
    176:
    177:         if ($this->config["cacheDir"] !== false) {
    178:
    179:             if (!is_dir($this->config["cacheDir"])) {
    180:                 if (mkdir($this->config["cacheDir"], 0777, true) === false) {
    181:                     throw new \RuntimeException(sprintf("%s directory does not exists and it is impossible to create it", $this->cacheDir));
    182:                 }
    183:
    184:                 if (file_exists($this->config["cacheDir"] . '/' . $this->config["cacheTimeFile"])) {
    185:                     file_put_contents($this->config["cacheDir"] . '/' . $this->config["cacheTimeFile"], time());
    186:                 }
    187:             }
    188:         }
    189:     }
    190:
    191:     /**
    192:      *
    193:      * verify if Tpex is initialized
    194:      *
    195:      * @return boolean
    196:      */
    197:     public function isInitialized()
    198:     {
    199:         return $this->init;
    200:     }
    201:
    202:     /**
    203:      *
    204:      * return the current at the current state during parsing
    205:      *
    206:      * @return string
    207:      */
    208:     public function getContent()
    209:     {
    210:         return $this->content;
    211:     }
    212:
    213:     public function setLoop(array $loop)
    214:     {
    215:         $this->tokenizer->setLoop($loop);
    216:     }
    217:
    218:     public function setFilter(array $filter)
    219:     {
    220:         $this->tokenizer->setFilter($filter);
    221:     }
    222:
    223:     public function setTestLoop(array $testLoop)
    224:     {
    225:         $this->tokenizer->setTestLoop($testLoop);
    226:     }
    227:
    228:     /**
    229:      *
    230:      * add multiple baseParam ine one time in Tpex Parser
    231:      *
    232:      * key is baseParam name
    233:      * value is the class implementing/extending base baseParam classes
    234:      *
    235:      * $baseParam = array(
    236:      *  "secure" => "Thelia\BaseParam\Secure",
    237:      *  "reset" => "Thelia\BaseParam\reset",
    238:      *  "myBaseParam" => "My\Own\BaseParam"
    239:      * );
    240:      *
    241:      * @param  array                     $baseParams
    242:      * @throws \InvalidArgumentException
    243:      */
    244:     public function setBaseParams(array $baseParams)
    245:     {
    246:         foreach ($baseParams as $name => $className) {
    247:             $this->addBaseParam($name, $className);
    248:         }
    249:     }
    250:
    251:     /**
    252:      *
    253:      * add a new baseParam to Tpex param
    254:      *
    255:      * @param  type                      $name      baseParam name. Must be unique
    256:      * @param  type                      $className class name implementing/extending filter classes
    257:      * @throws \InvalidArgumentException
    258:      */
    259:     public function addBaseParam($name, $className)
    260:     {
    261:         if (array_key_exists($name, $this->baseParam)) {
    262:             throw new \InvalidArgumentException(sprintf("%s baseParam name already exists for %s class name", $name, $className));
    263:         }
    264:
    265:         $this->baseParam[$name] = $className;
    266:     }
    267:
    268:     public function execute()
    269:     {
    270:         if ($this->isInitialized() === false) {
    271:             throw new \RuntimeException("Tpex must be initialize before executing. See Thelia\Tpex\Tpex::init() for more information");
    272:         }
    273:
    274:         $this->parseBaseParam();
    275:
    276:         $substitEvent = new TpexEvent($this->getContent(), $this->request);
    277:         $this->dispatcher->dispatch(TpexEvents::SUBSTITUTION, $substitEvent);
    278:
    279:         $this->content = $substitEvent->getContent();
    280:
    281:         $this->parseInclude();
    282:
    283:         //Now we can parse content
    284:         $this->config["cacheDir"] ? $this->parseStringWithCache() : $this->parseString();
    285:
    286:         return $this->content;
    287:     }
    288:
    289:     protected function parseBaseParam()
    290:     {
    291:         if (preg_match_all($this->regex['baseParam'], $this->content, $matches, PREG_SET_ORDER)) {
    292:             foreach ($matches as $match) {
    293:                 if (array_key_exists($match[1], $this->baseParam)) {
    294:                     $class = $this->baseParam[$match[1]];
    295:                     $obj = new $class($this->request, $match[2]);
    296:                     $obj->exec();
    297:                 } else {
    298:                     throw new \RuntimeException(sprintf("baseParam %s is not define", $match[1]));
    299:                 }
    300:             }
    301:
    302:            $this->content = str_replace($match[0], "", $this->content);
    303:         }
    304:
    305:     }
    306:
    307:     protected function parseInclude()
    308:     {
    309:         if (preg_match_all($this->regex["include"], $this->content, $matches, PREG_SET_ORDER)) {
    310:             foreach ($matches as $match) {
    311:                 $file = $this->basedir . $match[1];
    312:
    313:                 if (file_exists($file)) {
    314:                     $content = file_get_contents($file);
    315:
    316:                     if ($content !== false) {
    317:
    318:                        $this->content = str_replace($match[0], $content, $this->content);
    319:
    320:                        $this->parseInclude();
    321:                     }
    322:                 } else {
    323:                     throw new FileNotFoundException(sprintf("%s not found in %s directory", $match[1], $this->basedir));
    324:                 }
    325:             }
    326:         }
    327:     }
    328:
    329:     protected function parseString()
    330:     {
    331:         $this->tokenizer->setContent($this->content);
    332:
    333:        $compile = $this->tokenizer->tokenize();
    334:
    335:        $this->content =  $compile->exec();
    336:
    337:     }
    338:
    339:     protected function parseStringWithCache()
    340:     {
    341:         $this->cleanCache();
    342:         $fileCache = $this->config["cacheDir"] . '/' . md5($this->content) . ".cache";
    343:
    344:         if (file_exists($fileCache)) {
    345:
    346:             @touch($fileCache);
    347:             $this->content = unserialize(file_get_contents($fileCache));
    348:         } else {
    349:             $this->parseString();
    350:             file_put_contents($fileCache, $this->content);
    351:         }
    352:     }
    353:
    354:     protected function cleanCache()
    355:     {
    356:         $lastCheck = file_get_contents($this->config["cacheDir"] . "/" . $this->config["cacheTimeFile"]);
    357:
    358:         if(time() - $lastCheck < $this->config["checkPeriod"]) return;
    359:
    360:         file_put_contents($this->config["cacheDir"] . "/" . $this->config["cacheTimeFile"], time());
    361:
    362:         if ($dh = @opendir($this->config["cacheDir"])) {
    363:             while ($file = @readdir($dh)) {
    364:                 if (strstr($file, '.cache') !== false) {
    365:                     $filemtime = @filemtime($this->config["cacheDir"] . '/' . $file);
    366:
    367:                     if (! $filemtime || (time() - $filemtime) >= $this->config["fileLifetime"] ) {
    368:                         @unlink($this->config["cacheDir"] . '/' . $file);
    369:                     }
    370:                 }
    371:             }
    372:         }
    373:     }
    374: }
    375: