[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/administrator/components/com_templates/src/Model/ -> TemplateModel.php (source)

   1  <?php
   2  
   3  /**
   4   * @package     Joomla.Administrator
   5   * @subpackage  com_templates
   6   *
   7   * @copyright   (C) 2008 Open Source Matters, Inc. <https://www.joomla.org>
   8   * @license     GNU General Public License version 2 or later; see LICENSE.txt
   9   */
  10  
  11  namespace Joomla\Component\Templates\Administrator\Model;
  12  
  13  use Joomla\CMS\Application\ApplicationHelper;
  14  use Joomla\CMS\Component\ComponentHelper;
  15  use Joomla\CMS\Date\Date;
  16  use Joomla\CMS\Factory;
  17  use Joomla\CMS\Filesystem\File;
  18  use Joomla\CMS\Filesystem\Folder;
  19  use Joomla\CMS\Filesystem\Path;
  20  use Joomla\CMS\Form\Form;
  21  use Joomla\CMS\Image\Image;
  22  use Joomla\CMS\Language\Text;
  23  use Joomla\CMS\MVC\Model\FormModel;
  24  use Joomla\CMS\Plugin\PluginHelper;
  25  use Joomla\CMS\Uri\Uri;
  26  use Joomla\Component\Templates\Administrator\Helper\TemplateHelper;
  27  use Joomla\Component\Templates\Administrator\Helper\TemplatesHelper;
  28  use Joomla\Database\ParameterType;
  29  use Joomla\Utilities\ArrayHelper;
  30  
  31  // phpcs:disable PSR1.Files.SideEffects
  32  \defined('_JEXEC') or die;
  33  // phpcs:enable PSR1.Files.SideEffects
  34  
  35  /**
  36   * Template model class.
  37   *
  38   * @since  1.6
  39   */
  40  class TemplateModel extends FormModel
  41  {
  42      /**
  43       * The information in a template
  44       *
  45       * @var    \stdClass
  46       * @since  1.6
  47       */
  48      protected $template = null;
  49  
  50      /**
  51       * The path to the template
  52       *
  53       * @var    string
  54       * @since  3.2
  55       */
  56      protected $element = null;
  57  
  58      /**
  59       * The path to the static assets
  60       *
  61       * @var    string
  62       * @since  4.1.0
  63       */
  64      protected $mediaElement = null;
  65  
  66      /**
  67       * Internal method to get file properties.
  68       *
  69       * @param   string  $path  The base path.
  70       * @param   string  $name  The file name.
  71       *
  72       * @return  object
  73       *
  74       * @since   1.6
  75       */
  76      protected function getFile($path, $name)
  77      {
  78          $temp = new \stdClass();
  79  
  80          if ($this->getTemplate()) {
  81              $path = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $path);
  82              $path = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) . 'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $path);
  83              $temp->name = $name;
  84              $temp->id = urlencode(base64_encode(str_replace('\\', '//', $path)));
  85  
  86              return $temp;
  87          }
  88      }
  89  
  90      /**
  91       * Method to store file information.
  92       *
  93       * @param   string    $path      The base path.
  94       * @param   string    $name      The file name.
  95       * @param   stdClass  $template  The std class object of template.
  96       *
  97       * @return  object  stdClass object.
  98       *
  99       * @since   4.0.0
 100       */
 101      protected function storeFileInfo($path, $name, $template)
 102      {
 103          $temp = new \stdClass();
 104          $temp->id = base64_encode($path . $name);
 105          $temp->client = $template->client_id;
 106          $temp->template = $template->element;
 107          $temp->extension_id = $template->extension_id;
 108  
 109          if ($coreFile = $this->getCoreFile($path . $name, $template->client_id)) {
 110              $temp->coreFile = md5_file($coreFile);
 111          } else {
 112              $temp->coreFile = null;
 113          }
 114  
 115          return $temp;
 116      }
 117  
 118      /**
 119       * Method to get all template list.
 120       *
 121       * @return  object  stdClass object
 122       *
 123       * @since   4.0.0
 124       */
 125      public function getTemplateList()
 126      {
 127          // Get a db connection.
 128          $db = $this->getDatabase();
 129  
 130          // Create a new query object.
 131          $query = $db->getQuery(true);
 132  
 133          // Select the required fields from the table
 134          $query->select(
 135              $this->getState(
 136                  'list.select',
 137                  'a.extension_id, a.name, a.element, a.client_id'
 138              )
 139          );
 140  
 141          $query->from($db->quoteName('#__extensions', 'a'))
 142              ->where($db->quoteName('a.enabled') . ' = 1')
 143              ->where($db->quoteName('a.type') . ' = ' . $db->quote('template'));
 144  
 145          // Reset the query.
 146          $db->setQuery($query);
 147  
 148          // Load the results as a list of stdClass objects.
 149          $results = $db->loadObjectList();
 150  
 151          return $results;
 152      }
 153  
 154      /**
 155       * Method to get all updated file list.
 156       *
 157       * @param   boolean  $state    The optional parameter if you want unchecked list.
 158       * @param   boolean  $all      The optional parameter if you want all list.
 159       * @param   boolean  $cleanup  The optional parameter if you want to clean record which is no more required.
 160       *
 161       * @return  object  stdClass object
 162       *
 163       * @since   4.0.0
 164       */
 165      public function getUpdatedList($state = false, $all = false, $cleanup = false)
 166      {
 167          // Get a db connection.
 168          $db = $this->getDatabase();
 169  
 170          // Create a new query object.
 171          $query = $db->getQuery(true);
 172  
 173          // Select the required fields from the table
 174          $query->select(
 175              $this->getState(
 176                  'list.select',
 177                  'a.template, a.hash_id, a.extension_id, a.state, a.action, a.client_id, a.created_date, a.modified_date'
 178              )
 179          );
 180  
 181          $template = $this->getTemplate();
 182  
 183          $query->from($db->quoteName('#__template_overrides', 'a'));
 184  
 185          if (!$all) {
 186              $teid = (int) $template->extension_id;
 187              $query->where($db->quoteName('extension_id') . ' = :teid')
 188                  ->bind(':teid', $teid, ParameterType::INTEGER);
 189          }
 190  
 191          if ($state) {
 192              $query->where($db->quoteName('state') . ' = 0');
 193          }
 194  
 195          $query->order($db->quoteName('a.modified_date') . ' DESC');
 196  
 197          // Reset the query.
 198          $db->setQuery($query);
 199  
 200          // Load the results as a list of stdClass objects.
 201          $pks = $db->loadObjectList();
 202  
 203          if ($state) {
 204              return $pks;
 205          }
 206  
 207          $results = array();
 208  
 209          foreach ($pks as $pk) {
 210              $client = ApplicationHelper::getClientInfo($pk->client_id);
 211              $path = Path::clean($client->path . '/templates/' . $pk->template . base64_decode($pk->hash_id));
 212  
 213              if (file_exists($path)) {
 214                  $results[] = $pk;
 215              } elseif ($cleanup) {
 216                  $cleanupIds = array();
 217                  $cleanupIds[] = $pk->hash_id;
 218                  $this->publish($cleanupIds, -3, $pk->extension_id);
 219              }
 220          }
 221  
 222          return $results;
 223      }
 224  
 225      /**
 226       * Method to get a list of all the core files of override files.
 227       *
 228       * @return  array  An array of all core files.
 229       *
 230       * @since   4.0.0
 231       */
 232      public function getCoreList()
 233      {
 234          // Get list of all templates
 235          $templates = $this->getTemplateList();
 236  
 237          // Initialize the array variable to store core file list.
 238          $this->coreFileList = array();
 239  
 240          $app = Factory::getApplication();
 241  
 242          foreach ($templates as $template) {
 243              $client  = ApplicationHelper::getClientInfo($template->client_id);
 244              $element = Path::clean($client->path . '/templates/' . $template->element . '/');
 245              $path    = Path::clean($element . 'html/');
 246  
 247              if (is_dir($path)) {
 248                  $this->prepareCoreFiles($path, $element, $template);
 249              }
 250          }
 251  
 252          // Sort list of stdClass array.
 253          usort(
 254              $this->coreFileList,
 255              function ($a, $b) {
 256                  return strcmp($a->id, $b->id);
 257              }
 258          );
 259  
 260          return $this->coreFileList;
 261      }
 262  
 263      /**
 264       * Prepare core files.
 265       *
 266       * @param   string     $dir       The path of the directory to scan.
 267       * @param   string     $element   The path of the template element.
 268       * @param   \stdClass  $template  The stdClass object of template.
 269       *
 270       * @return  array
 271       *
 272       * @since   4.0.0
 273       */
 274      public function prepareCoreFiles($dir, $element, $template)
 275      {
 276          $dirFiles = scandir($dir);
 277  
 278          foreach ($dirFiles as $key => $value) {
 279              if (in_array($value, array('.', '..', 'node_modules'))) {
 280                  continue;
 281              }
 282  
 283              if (is_dir($dir . $value)) {
 284                  $relativePath = str_replace($element, '', $dir . $value);
 285                  $this->prepareCoreFiles($dir . $value . '/', $element, $template);
 286              } else {
 287                  $ext           = pathinfo($dir . $value, PATHINFO_EXTENSION);
 288                  $allowedFormat = $this->checkFormat($ext);
 289  
 290                  if ($allowedFormat === true) {
 291                      $relativePath = str_replace($element, '', $dir);
 292                      $info = $this->storeFileInfo('/' . $relativePath, $value, $template);
 293  
 294                      if ($info) {
 295                          $this->coreFileList[] = $info;
 296                      }
 297                  }
 298              }
 299          }
 300      }
 301  
 302      /**
 303       * Method to update status of list.
 304       *
 305       * @param   array    $ids    The base path.
 306       * @param   array    $value  The file name.
 307       * @param   integer  $exid   The template extension id.
 308       *
 309       * @return  integer  Number of files changed.
 310       *
 311       * @since   4.0.0
 312       */
 313      public function publish($ids, $value, $exid)
 314      {
 315          $db = $this->getDatabase();
 316  
 317          foreach ($ids as $id) {
 318              if ($value === -3) {
 319                  $deleteQuery = $db->getQuery(true)
 320                      ->delete($db->quoteName('#__template_overrides'))
 321                      ->where($db->quoteName('hash_id') . ' = :hashid')
 322                      ->where($db->quoteName('extension_id') . ' = :exid')
 323                      ->bind(':hashid', $id)
 324                      ->bind(':exid', $exid, ParameterType::INTEGER);
 325  
 326                  try {
 327                      // Set the query using our newly populated query object and execute it.
 328                      $db->setQuery($deleteQuery);
 329                      $result = $db->execute();
 330                  } catch (\RuntimeException $e) {
 331                      return $e;
 332                  }
 333              } elseif ($value === 1 || $value === 0) {
 334                  $updateQuery = $db->getQuery(true)
 335                      ->update($db->quoteName('#__template_overrides'))
 336                      ->set($db->quoteName('state') . ' = :state')
 337                      ->where($db->quoteName('hash_id') . ' = :hashid')
 338                      ->where($db->quoteName('extension_id') . ' = :exid')
 339                      ->bind(':state', $value, ParameterType::INTEGER)
 340                      ->bind(':hashid', $id)
 341                      ->bind(':exid', $exid, ParameterType::INTEGER);
 342  
 343                  try {
 344                      // Set the query using our newly populated query object and execute it.
 345                      $db->setQuery($updateQuery);
 346                      $result = $db->execute();
 347                  } catch (\RuntimeException $e) {
 348                      return $e;
 349                  }
 350              }
 351          }
 352  
 353          return $result;
 354      }
 355  
 356      /**
 357       * Method to get a list of all the files to edit in a template.
 358       *
 359       * @return  array  A nested array of relevant files.
 360       *
 361       * @since   1.6
 362       */
 363      public function getFiles()
 364      {
 365          $result = array();
 366  
 367          if ($template = $this->getTemplate()) {
 368              $app    = Factory::getApplication();
 369              $client = ApplicationHelper::getClientInfo($template->client_id);
 370              $path   = Path::clean($client->path . '/templates/' . $template->element . '/');
 371              $lang   = Factory::getLanguage();
 372  
 373              // Load the core and/or local language file(s).
 374              $lang->load('tpl_' . $template->element, $client->path)
 375              || (!empty($template->xmldata->parent) && $lang->load('tpl_' . $template->xmldata->parent, $client->path))
 376              || $lang->load('tpl_' . $template->element, $client->path . '/templates/' . $template->element)
 377              || (!empty($template->xmldata->parent) && $lang->load('tpl_' . $template->xmldata->parent, $client->path . '/templates/' . $template->xmldata->parent));
 378              $this->element = $path;
 379  
 380              if (!is_writable($path)) {
 381                  $app->enqueueMessage(Text::_('COM_TEMPLATES_DIRECTORY_NOT_WRITABLE'), 'error');
 382              }
 383  
 384              if (is_dir($path)) {
 385                  $result = $this->getDirectoryTree($path);
 386              } else {
 387                  $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_TEMPLATE_FOLDER_NOT_FOUND'), 'error');
 388  
 389                  return false;
 390              }
 391  
 392              // Clean up override history
 393              $this->getUpdatedList(false, true, true);
 394          }
 395  
 396          return $result;
 397      }
 398  
 399      /**
 400       * Get the directory tree.
 401       *
 402       * @param   string  $dir  The path of the directory to scan
 403       *
 404       * @return  array
 405       *
 406       * @since   3.2
 407       */
 408      public function getDirectoryTree($dir)
 409      {
 410          $result = array();
 411  
 412          $dirFiles = scandir($dir);
 413  
 414          foreach ($dirFiles as $key => $value) {
 415              if (!in_array($value, array('.', '..', 'node_modules'))) {
 416                  if (is_dir($dir . $value)) {
 417                      $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $dir . $value);
 418                      $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) . 'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $relativePath);
 419                      $result[str_replace('\\', '//', $relativePath)] = $this->getDirectoryTree($dir . $value . '/');
 420                  } else {
 421                      $ext           = pathinfo($dir . $value, PATHINFO_EXTENSION);
 422                      $allowedFormat = $this->checkFormat($ext);
 423  
 424                      if ($allowedFormat == true) {
 425                          $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'templates'  . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $dir . $value);
 426                          $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) . 'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $relativePath);
 427                          $result[] = $this->getFile($relativePath, $value);
 428                      }
 429                  }
 430              }
 431          }
 432  
 433          return $result;
 434      }
 435  
 436      /**
 437       * Method to get the core file of override file
 438       *
 439       * @param   string   $file       Override file
 440       * @param   integer  $client_id  Client Id
 441       *
 442       * @return  string  $corefile The full path and file name for the target file, or boolean false if the file is not found in any of the paths.
 443       *
 444       * @since   4.0.0
 445       */
 446      public function getCoreFile($file, $client_id)
 447      {
 448          $app          = Factory::getApplication();
 449          $filePath     = Path::clean($file);
 450          $explodeArray = explode(DIRECTORY_SEPARATOR, $filePath);
 451  
 452          // Only allow html/ folder
 453          if ($explodeArray['1'] !== 'html') {
 454              return false;
 455          }
 456  
 457          $fileName = basename($filePath);
 458          $type     = $explodeArray['2'];
 459          $client   = ApplicationHelper::getClientInfo($client_id);
 460  
 461          $componentPath = Path::clean($client->path . '/components/');
 462          $modulePath    = Path::clean($client->path . '/modules/');
 463          $layoutPath    = Path::clean(JPATH_ROOT . '/layouts/');
 464  
 465          // For modules
 466          if (stristr($type, 'mod_') !== false) {
 467              $folder   = $explodeArray['2'];
 468              $htmlPath = Path::clean($modulePath . $folder . '/tmpl/');
 469              $fileName = $this->getSafeName($fileName);
 470              $coreFile = Path::find($htmlPath, $fileName);
 471  
 472              return $coreFile;
 473          } elseif (stristr($type, 'com_') !== false) {
 474              // For components
 475              $folder    = $explodeArray['2'];
 476              $subFolder = $explodeArray['3'];
 477              $fileName  = $this->getSafeName($fileName);
 478  
 479              // The new scheme, if a view has a tmpl folder
 480              $newHtmlPath = Path::clean($componentPath . $folder . '/tmpl/' . $subFolder . '/');
 481  
 482              if (!$coreFile = Path::find($newHtmlPath, $fileName)) {
 483                  // The old scheme, the views are directly in the component/tmpl folder
 484                  $oldHtmlPath = Path::clean($componentPath . $folder . '/views/' . $subFolder . '/tmpl/');
 485                  $coreFile    = Path::find($oldHtmlPath, $fileName);
 486  
 487                  return $coreFile;
 488              }
 489  
 490              return $coreFile;
 491          } elseif (stristr($type, 'layouts') !== false) {
 492              // For Layouts
 493              $subtype = $explodeArray['3'];
 494  
 495              if (stristr($subtype, 'com_')) {
 496                  $folder    = $explodeArray['3'];
 497                  $subFolder = array_slice($explodeArray, 4, -1);
 498                  $subFolder = implode(DIRECTORY_SEPARATOR, $subFolder);
 499                  $htmlPath  = Path::clean($componentPath . $folder . '/layouts/' . $subFolder);
 500                  $fileName  = $this->getSafeName($fileName);
 501                  $coreFile  = Path::find($htmlPath, $fileName);
 502  
 503                  return $coreFile;
 504              } elseif (stristr($subtype, 'joomla') || stristr($subtype, 'libraries') || stristr($subtype, 'plugins')) {
 505                  $subFolder = array_slice($explodeArray, 3, -1);
 506                  $subFolder = implode(DIRECTORY_SEPARATOR, $subFolder);
 507                  $htmlPath  = Path::clean($layoutPath . $subFolder);
 508                  $fileName  = $this->getSafeName($fileName);
 509                  $coreFile  = Path::find($htmlPath, $fileName);
 510  
 511                  return $coreFile;
 512              }
 513          }
 514  
 515          return false;
 516      }
 517  
 518      /**
 519       * Creates a safe file name for the given name.
 520       *
 521       * @param   string  $name  The filename
 522       *
 523       * @return  string $fileName  The filtered name without Date
 524       *
 525       * @since   4.0.0
 526       */
 527      private function getSafeName($name)
 528      {
 529          if (strpos($name, '-') !== false && preg_match('/[0-9]/', $name)) {
 530              // Get the extension
 531              $extension = File::getExt($name);
 532  
 533              // Remove ( Date ) from file
 534              $explodeArray = explode('-', $name);
 535              $size = count($explodeArray);
 536              $date = $explodeArray[$size - 2] . '-' . str_replace('.' . $extension, '', $explodeArray[$size - 1]);
 537  
 538              if ($this->validateDate($date)) {
 539                  $nameWithoutExtension = implode('-', array_slice($explodeArray, 0, -2));
 540  
 541                  // Filtered name
 542                  $name = $nameWithoutExtension . '.' . $extension;
 543              }
 544          }
 545  
 546          return $name;
 547      }
 548  
 549      /**
 550       * Validate Date in file name.
 551       *
 552       * @param   string  $date  Date to validate.
 553       *
 554       * @return boolean Return true if date is valid and false if not.
 555       *
 556       * @since  4.0.0
 557       */
 558      private function validateDate($date)
 559      {
 560          $format = 'Ymd-His';
 561          $valid  = Date::createFromFormat($format, $date);
 562  
 563          return $valid && $valid->format($format) === $date;
 564      }
 565  
 566      /**
 567       * Method to auto-populate the model state.
 568       *
 569       * Note. Calling getState in this method will result in recursion.
 570       *
 571       * @return  void
 572       *
 573       * @since   1.6
 574       */
 575      protected function populateState()
 576      {
 577          $app = Factory::getApplication();
 578  
 579          // Load the User state.
 580          $pk = $app->input->getInt('id');
 581          $this->setState('extension.id', $pk);
 582  
 583          // Load the parameters.
 584          $params = ComponentHelper::getParams('com_templates');
 585          $this->setState('params', $params);
 586      }
 587  
 588      /**
 589       * Method to get the template information.
 590       *
 591       * @return  mixed  Object if successful, false if not and internal error is set.
 592       *
 593       * @since   1.6
 594       */
 595      public function &getTemplate()
 596      {
 597          if (empty($this->template)) {
 598              $pk  = (int) $this->getState('extension.id');
 599              $db  = $this->getDatabase();
 600              $app = Factory::getApplication();
 601  
 602              // Get the template information.
 603              $query = $db->getQuery(true)
 604                  ->select($db->quoteName(['extension_id', 'client_id', 'element', 'name', 'manifest_cache']))
 605                  ->from($db->quoteName('#__extensions'))
 606                  ->where($db->quoteName('extension_id') . ' = :pk')
 607                  ->where($db->quoteName('type') . ' = ' . $db->quote('template'))
 608                  ->bind(':pk', $pk, ParameterType::INTEGER);
 609              $db->setQuery($query);
 610  
 611              try {
 612                  $result = $db->loadObject();
 613              } catch (\RuntimeException $e) {
 614                  $app->enqueueMessage($e->getMessage(), 'warning');
 615                  $this->template = false;
 616  
 617                  return false;
 618              }
 619  
 620              if (empty($result)) {
 621                  $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXTENSION_RECORD_NOT_FOUND'), 'error');
 622                  $this->template = false;
 623              } else {
 624                  $this->template = $result;
 625  
 626                  // Client ID is not always an integer, so enforce here
 627                  $this->template->client_id = (int) $this->template->client_id;
 628  
 629                  if (!isset($this->template->xmldata)) {
 630                      $this->template->xmldata = TemplatesHelper::parseXMLTemplateFile($this->template->client_id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $this->template->name);
 631                  }
 632              }
 633          }
 634  
 635          return $this->template;
 636      }
 637  
 638      /**
 639       * Method to check if new template name already exists
 640       *
 641       * @return  boolean   true if name is not used, false otherwise
 642       *
 643       * @since   2.5
 644       */
 645      public function checkNewName()
 646      {
 647          $db    = $this->getDatabase();
 648          $name  = $this->getState('new_name');
 649          $query = $db->getQuery(true)
 650              ->select('COUNT(*)')
 651              ->from($db->quoteName('#__extensions'))
 652              ->where($db->quoteName('name') . ' = :name')
 653              ->bind(':name', $name);
 654          $db->setQuery($query);
 655  
 656          return ($db->loadResult() == 0);
 657      }
 658  
 659      /**
 660       * Method to check if new template name already exists
 661       *
 662       * @return  string     name of current template
 663       *
 664       * @since   2.5
 665       */
 666      public function getFromName()
 667      {
 668          return $this->getTemplate()->element;
 669      }
 670  
 671      /**
 672       * Method to check if new template name already exists
 673       *
 674       * @return  boolean   true if name is not used, false otherwise
 675       *
 676       * @since   2.5
 677       */
 678      public function copy()
 679      {
 680          $app = Factory::getApplication();
 681  
 682          if ($template = $this->getTemplate()) {
 683              $client = ApplicationHelper::getClientInfo($template->client_id);
 684              $fromPath = Path::clean($client->path . '/templates/' . $template->element . '/');
 685  
 686              // Delete new folder if it exists
 687              $toPath = $this->getState('to_path');
 688  
 689              if (Folder::exists($toPath)) {
 690                  if (!Folder::delete($toPath)) {
 691                      $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error');
 692  
 693                      return false;
 694                  }
 695              }
 696  
 697              // Copy all files from $fromName template to $newName folder
 698              if (!Folder::copy($fromPath, $toPath)) {
 699                  return false;
 700              }
 701  
 702              // Check manifest for additional files
 703              $manifest = simplexml_load_file($toPath . '/templateDetails.xml');
 704  
 705              // Copy language files from global folder
 706              if ($languages = $manifest->languages) {
 707                  $folder        = (string) $languages->attributes()->folder;
 708                  $languageFiles = $languages->language;
 709  
 710                  Folder::create($toPath . '/' . $folder . '/' . $languageFiles->attributes()->tag);
 711  
 712                  foreach ($languageFiles as $languageFile) {
 713                      $src = Path::clean($client->path . '/language/' . $languageFile);
 714                      $dst = Path::clean($toPath . '/' . $folder . '/' . $languageFile);
 715  
 716                      if (File::exists($src)) {
 717                          File::copy($src, $dst);
 718                      }
 719                  }
 720              }
 721  
 722              // Copy media files
 723              if ($media = $manifest->media) {
 724                  $folder      = (string) $media->attributes()->folder;
 725                  $destination = (string) $media->attributes()->destination;
 726  
 727                  Folder::copy(JPATH_SITE . '/media/' . $destination, $toPath . '/' . $folder);
 728              }
 729  
 730              // Adjust to new template name
 731              if (!$this->fixTemplateName()) {
 732                  return false;
 733              }
 734  
 735              return true;
 736          } else {
 737              $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error');
 738  
 739              return false;
 740          }
 741      }
 742  
 743      /**
 744       * Method to delete tmp folder
 745       *
 746       * @return  boolean   true if delete successful, false otherwise
 747       *
 748       * @since   2.5
 749       */
 750      public function cleanup()
 751      {
 752          // Clear installation messages
 753          $app = Factory::getApplication();
 754          $app->setUserState('com_installer.message', '');
 755          $app->setUserState('com_installer.extension_message', '');
 756  
 757          // Delete temporary directory
 758          return Folder::delete($this->getState('to_path'));
 759      }
 760  
 761      /**
 762       * Method to rename the template in the XML files and rename the language files
 763       *
 764       * @return  boolean  true if successful, false otherwise
 765       *
 766       * @since   2.5
 767       */
 768      protected function fixTemplateName()
 769      {
 770          // Rename Language files
 771          // Get list of language files
 772          $result   = true;
 773          $files    = Folder::files($this->getState('to_path'), '\.ini$', true, true);
 774          $newName  = strtolower($this->getState('new_name'));
 775          $template = $this->getTemplate();
 776          $oldName  = $template->element;
 777          $manifest = json_decode($template->manifest_cache);
 778  
 779          foreach ($files as $file) {
 780              $newFile = '/' . str_replace($oldName, $newName, basename($file));
 781              $result  = File::move($file, dirname($file) . $newFile) && $result;
 782          }
 783  
 784          // Edit XML file
 785          $xmlFile = $this->getState('to_path') . '/templateDetails.xml';
 786  
 787          if (File::exists($xmlFile)) {
 788              $contents = file_get_contents($xmlFile);
 789              $pattern[] = '#<name>\s*' . $manifest->name . '\s*</name>#i';
 790              $replace[] = '<name>' . $newName . '</name>';
 791              $pattern[] = '#<language(.*)' . $oldName . '(.*)</language>#';
 792              $replace[] = '<language$1}' . $newName . '$2}</language>';
 793              $pattern[] = '#<media(.*)' . $oldName . '(.*)>#';
 794              $replace[] = '<media$1}' . $newName . '$2}>';
 795              $contents = preg_replace($pattern, $replace, $contents);
 796              $result = File::write($xmlFile, $contents) && $result;
 797          }
 798  
 799          return $result;
 800      }
 801  
 802      /**
 803       * Method to get the record form.
 804       *
 805       * @param   array    $data      Data for the form.
 806       * @param   boolean  $loadData  True if the form is to load its own data (default case), false if not.
 807       *
 808       * @return  Form|boolean    A Form object on success, false on failure
 809       *
 810       * @since   1.6
 811       */
 812      public function getForm($data = array(), $loadData = true)
 813      {
 814          $app = Factory::getApplication();
 815  
 816          // Codemirror or Editor None should be enabled
 817          $db = $this->getDatabase();
 818          $query = $db->getQuery(true)
 819              ->select('COUNT(*)')
 820              ->from('#__extensions as a')
 821              ->where(
 822                  '(a.name =' . $db->quote('plg_editors_codemirror') .
 823                  ' AND a.enabled = 1) OR (a.name =' .
 824                  $db->quote('plg_editors_none') .
 825                  ' AND a.enabled = 1)'
 826              );
 827          $db->setQuery($query);
 828          $state = $db->loadResult();
 829  
 830          if ((int) $state < 1) {
 831              $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EDITOR_DISABLED'), 'warning');
 832          }
 833  
 834          // Get the form.
 835          $form = $this->loadForm('com_templates.source', 'source', array('control' => 'jform', 'load_data' => $loadData));
 836  
 837          if (empty($form)) {
 838              return false;
 839          }
 840  
 841          return $form;
 842      }
 843  
 844      /**
 845       * Method to get the data that should be injected in the form.
 846       *
 847       * @return  mixed  The data for the form.
 848       *
 849       * @since   1.6
 850       */
 851      protected function loadFormData()
 852      {
 853          $data = $this->getSource();
 854  
 855          $this->preprocessData('com_templates.source', $data);
 856  
 857          return $data;
 858      }
 859  
 860      /**
 861       * Method to get a single record.
 862       *
 863       * @return  mixed  Object on success, false on failure.
 864       *
 865       * @since   1.6
 866       */
 867      public function &getSource()
 868      {
 869          $app = Factory::getApplication();
 870          $item = new \stdClass();
 871  
 872          if (!$this->template) {
 873              $this->getTemplate();
 874          }
 875  
 876          if ($this->template) {
 877              $input    = Factory::getApplication()->input;
 878              $fileName = base64_decode($input->get('file'));
 879              $fileName = str_replace('//', '/', $fileName);
 880              $isMedia  = $input->getInt('isMedia', 0);
 881  
 882              $fileName = $isMedia ? Path::clean(JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . $fileName)
 883              : Path::clean(JPATH_ROOT . ($this->template->client_id === 0 ? '' : '/administrator') . '/templates/' . $this->template->element . $fileName);
 884  
 885              try {
 886                  $filePath = Path::check($fileName);
 887              } catch (\Exception $e) {
 888                  $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_FOUND'), 'error');
 889  
 890                  return;
 891              }
 892  
 893              if (file_exists($filePath)) {
 894                  $item->extension_id = $this->getState('extension.id');
 895                  $item->filename     = Path::clean($fileName);
 896                  $item->source       = file_get_contents($filePath);
 897                  $item->filePath     = Path::clean($filePath);
 898                  $ds                 = DIRECTORY_SEPARATOR;
 899                  $cleanFileName      = str_replace(JPATH_ROOT . ($this->template->client_id === 1 ? $ds . 'administrator' . $ds : $ds) . 'templates' . $ds . $this->template->element, '', $fileName);
 900  
 901                  if ($coreFile = $this->getCoreFile($cleanFileName, $this->template->client_id)) {
 902                      $item->coreFile = $coreFile;
 903                      $item->core = file_get_contents($coreFile);
 904                  }
 905              } else {
 906                  $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_FOUND'), 'error');
 907              }
 908          }
 909  
 910          return $item;
 911      }
 912  
 913      /**
 914       * Method to store the source file contents.
 915       *
 916       * @param   array  $data  The source data to save.
 917       *
 918       * @return  boolean  True on success, false otherwise and internal error set.
 919       *
 920       * @since   1.6
 921       */
 922      public function save($data)
 923      {
 924          // Get the template.
 925          $template = $this->getTemplate();
 926  
 927          if (empty($template)) {
 928              return false;
 929          }
 930  
 931          $app      = Factory::getApplication();
 932          $fileName = base64_decode($app->input->get('file'));
 933          $isMedia  = $app->input->getInt('isMedia', 0);
 934          $fileName = $isMedia ? JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . $fileName  :
 935              JPATH_ROOT . '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element . $fileName;
 936  
 937          $filePath = Path::clean($fileName);
 938  
 939          // Include the extension plugins for the save events.
 940          PluginHelper::importPlugin('extension');
 941  
 942          $user = get_current_user();
 943          chown($filePath, $user);
 944          Path::setPermissions($filePath, '0644');
 945  
 946          // Try to make the template file writable.
 947          if (!is_writable($filePath)) {
 948              $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_WRITABLE'), 'warning');
 949              $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_FILE_PERMISSIONS', Path::getPermissions($filePath)), 'warning');
 950  
 951              if (!Path::isOwner($filePath)) {
 952                  $app->enqueueMessage(Text::_('COM_TEMPLATES_CHECK_FILE_OWNERSHIP'), 'warning');
 953              }
 954  
 955              return false;
 956          }
 957  
 958          // Make sure EOL is Unix
 959          $data['source'] = str_replace(array("\r\n", "\r"), "\n", $data['source']);
 960  
 961          // If the asset file for the template ensure we have valid template so we don't instantly destroy it
 962          if ($fileName === '/joomla.asset.json' && json_decode($data['source']) === null) {
 963              $this->setError(Text::_('COM_TEMPLATES_ERROR_ASSET_FILE_INVALID_JSON'));
 964  
 965              return false;
 966          }
 967  
 968          $return = File::write($filePath, $data['source']);
 969  
 970          if (!$return) {
 971              $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_ERROR_FAILED_TO_SAVE_FILENAME', $fileName), 'error');
 972  
 973              return false;
 974          }
 975  
 976          // Get the extension of the changed file.
 977          $explodeArray = explode('.', $fileName);
 978          $ext = end($explodeArray);
 979  
 980          if ($ext == 'less') {
 981              $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_COMPILE_LESS', $fileName));
 982          }
 983  
 984          return true;
 985      }
 986  
 987      /**
 988       * Get overrides folder.
 989       *
 990       * @param   string  $name  The name of override.
 991       * @param   string  $path  Location of override.
 992       *
 993       * @return  object  containing override name and path.
 994       *
 995       * @since   3.2
 996       */
 997      public function getOverridesFolder($name, $path)
 998      {
 999          $folder = new \stdClass();
1000          $folder->name = $name;
1001          $folder->path = base64_encode($path . $name);
1002  
1003          return $folder;
1004      }
1005  
1006      /**
1007       * Get a list of overrides.
1008       *
1009       * @return  array containing overrides.
1010       *
1011       * @since   3.2
1012       */
1013      public function getOverridesList()
1014      {
1015          if ($template = $this->getTemplate()) {
1016              $client        = ApplicationHelper::getClientInfo($template->client_id);
1017              $componentPath = Path::clean($client->path . '/components/');
1018              $modulePath    = Path::clean($client->path . '/modules/');
1019              $pluginPath    = Path::clean(JPATH_ROOT . '/plugins/');
1020              $layoutPath    = Path::clean(JPATH_ROOT . '/layouts/');
1021              $components    = Folder::folders($componentPath);
1022  
1023              foreach ($components as $component) {
1024                  // Collect the folders with views
1025                  $folders = Folder::folders($componentPath . '/' . $component, '^view[s]?$', false, true);
1026                  $folders = array_merge($folders, Folder::folders($componentPath . '/' . $component, '^tmpl?$', false, true));
1027  
1028                  if (!$folders) {
1029                      continue;
1030                  }
1031  
1032                  foreach ($folders as $folder) {
1033                      // The subfolders are views
1034                      $views = Folder::folders($folder);
1035  
1036                      foreach ($views as $view) {
1037                          // The old scheme, if a view has a tmpl folder
1038                          $path = $folder . '/' . $view . '/tmpl';
1039  
1040                          // The new scheme, the views are directly in the component/tmpl folder
1041                          if (!is_dir($path) && substr($folder, -4) == 'tmpl') {
1042                              $path = $folder . '/' . $view;
1043                          }
1044  
1045                          // Check if the folder exists
1046                          if (!is_dir($path)) {
1047                              continue;
1048                          }
1049  
1050                          $result['components'][$component][] = $this->getOverridesFolder($view, Path::clean($folder . '/'));
1051                      }
1052                  }
1053              }
1054  
1055              foreach (Folder::folders($pluginPath) as $pluginGroup) {
1056                  foreach (Folder::folders($pluginPath . '/' . $pluginGroup) as $plugin) {
1057                      if (file_exists($pluginPath . '/' . $pluginGroup . '/' . $plugin . '/tmpl/')) {
1058                          $pluginLayoutPath = Path::clean($pluginPath . '/' . $pluginGroup . '/');
1059                          $result['plugins'][$pluginGroup][] = $this->getOverridesFolder($plugin, $pluginLayoutPath);
1060                      }
1061                  }
1062              }
1063  
1064              $modules = Folder::folders($modulePath);
1065  
1066              foreach ($modules as $module) {
1067                  $result['modules'][] = $this->getOverridesFolder($module, $modulePath);
1068              }
1069  
1070              $layoutFolders = Folder::folders($layoutPath);
1071  
1072              foreach ($layoutFolders as $layoutFolder) {
1073                  $layoutFolderPath = Path::clean($layoutPath . '/' . $layoutFolder . '/');
1074                  $layouts = Folder::folders($layoutFolderPath);
1075  
1076                  foreach ($layouts as $layout) {
1077                      $result['layouts'][$layoutFolder][] = $this->getOverridesFolder($layout, $layoutFolderPath);
1078                  }
1079              }
1080  
1081              // Check for layouts in component folders
1082              foreach ($components as $component) {
1083                  if (file_exists($componentPath . '/' . $component . '/layouts/')) {
1084                      $componentLayoutPath = Path::clean($componentPath . '/' . $component . '/layouts/');
1085  
1086                      if ($componentLayoutPath) {
1087                          $layouts = Folder::folders($componentLayoutPath);
1088  
1089                          foreach ($layouts as $layout) {
1090                              $result['layouts'][$component][] = $this->getOverridesFolder($layout, $componentLayoutPath);
1091                          }
1092                      }
1093                  }
1094              }
1095          }
1096  
1097          if (!empty($result)) {
1098              return $result;
1099          }
1100      }
1101  
1102      /**
1103       * Create overrides.
1104       *
1105       * @param   string  $override  The override location.
1106       *
1107       * @return   boolean  true if override creation is successful, false otherwise
1108       *
1109       * @since   3.2
1110       */
1111      public function createOverride($override)
1112      {
1113          if ($template = $this->getTemplate()) {
1114              $app          = Factory::getApplication();
1115              $explodeArray = explode(DIRECTORY_SEPARATOR, $override);
1116              $name         = end($explodeArray);
1117              $client       = ApplicationHelper::getClientInfo($template->client_id);
1118  
1119              if (stristr($name, 'mod_') != false) {
1120                  $htmlPath   = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $name);
1121              } elseif (stristr($override, 'com_') != false) {
1122                  $size = count($explodeArray);
1123  
1124                  $url = Path::clean($explodeArray[$size - 3] . '/' . $explodeArray[$size - 1]);
1125  
1126                  if ($explodeArray[$size - 2] == 'layouts') {
1127                      $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/layouts/' . $url);
1128                  } else {
1129                      $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $url);
1130                  }
1131              } elseif (stripos($override, Path::clean(JPATH_ROOT . '/plugins/')) === 0) {
1132                  $size       = count($explodeArray);
1133                  $layoutPath = Path::clean('plg_' . $explodeArray[$size - 2] . '_' . $explodeArray[$size - 1]);
1134                  $htmlPath   = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $layoutPath);
1135              } else {
1136                  $layoutPath = implode('/', array_slice($explodeArray, -2));
1137                  $htmlPath   = Path::clean($client->path . '/templates/' . $template->element . '/html/layouts/' . $layoutPath);
1138              }
1139  
1140              // Check Html folder, create if not exist
1141              if (!Folder::exists($htmlPath)) {
1142                  if (!Folder::create($htmlPath)) {
1143                      $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_ERROR'), 'error');
1144  
1145                      return false;
1146                  }
1147              }
1148  
1149              if (stristr($name, 'mod_') != false) {
1150                  $return = $this->createTemplateOverride(Path::clean($override . '/tmpl'), $htmlPath);
1151              } elseif (stristr($override, 'com_') != false && stristr($override, 'layouts') == false) {
1152                  $path = $override . '/tmpl';
1153  
1154                  // View can also be in the top level folder
1155                  if (!is_dir($path)) {
1156                      $path = $override;
1157                  }
1158  
1159                  $return = $this->createTemplateOverride(Path::clean($path), $htmlPath);
1160              } elseif (stripos($override, Path::clean(JPATH_ROOT . '/plugins/')) === 0) {
1161                  $return = $this->createTemplateOverride(Path::clean($override . '/tmpl'), $htmlPath);
1162              } else {
1163                  $return = $this->createTemplateOverride($override, $htmlPath);
1164              }
1165  
1166              if ($return) {
1167                  $app->enqueueMessage(Text::_('COM_TEMPLATES_OVERRIDE_CREATED') . str_replace(JPATH_ROOT, '', $htmlPath));
1168  
1169                  return true;
1170              } else {
1171                  $app->enqueueMessage(Text::_('COM_TEMPLATES_OVERRIDE_FAILED'), 'error');
1172  
1173                  return false;
1174              }
1175          }
1176      }
1177  
1178      /**
1179       * Create override folder & file
1180       *
1181       * @param   string  $overridePath  The override location
1182       * @param   string  $htmlPath      The html location
1183       *
1184       * @return  boolean                True on success. False otherwise.
1185       */
1186      public function createTemplateOverride($overridePath, $htmlPath)
1187      {
1188          $return = false;
1189  
1190          if (empty($overridePath) || empty($htmlPath)) {
1191              return $return;
1192          }
1193  
1194          // Get list of template folders
1195          $folders = Folder::folders($overridePath, null, true, true);
1196  
1197          if (!empty($folders)) {
1198              foreach ($folders as $folder) {
1199                  $htmlFolder = $htmlPath . str_replace($overridePath, '', $folder);
1200  
1201                  if (!Folder::exists($htmlFolder)) {
1202                      Folder::create($htmlFolder);
1203                  }
1204              }
1205          }
1206  
1207          // Get list of template files (Only get *.php file for template file)
1208          $files = Folder::files($overridePath, '.php', true, true);
1209  
1210          if (empty($files)) {
1211              return true;
1212          }
1213  
1214          foreach ($files as $file) {
1215              $overrideFilePath = str_replace($overridePath, '', $file);
1216              $htmlFilePath = $htmlPath . $overrideFilePath;
1217  
1218              if (File::exists($htmlFilePath)) {
1219                  // Generate new unique file name base on current time
1220                  $today = Factory::getDate();
1221                  $htmlFilePath = File::stripExt($htmlFilePath) . '-' . $today->format('Ymd-His') . '.' . File::getExt($htmlFilePath);
1222              }
1223  
1224              $return = File::copy($file, $htmlFilePath, '', true);
1225          }
1226  
1227          return $return;
1228      }
1229  
1230      /**
1231       * Delete a particular file.
1232       *
1233       * @param   string  $file  The relative location of the file.
1234       *
1235       * @return   boolean  True if file deletion is successful, false otherwise
1236       *
1237       * @since   3.2
1238       */
1239      public function deleteFile($file)
1240      {
1241          if ($this->getTemplate()) {
1242              $app      = Factory::getApplication();
1243              $filePath = $this->getBasePath() . urldecode(base64_decode($file));
1244  
1245              $return = File::delete($filePath);
1246  
1247              if (!$return) {
1248                  $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_DELETE_ERROR'), 'error');
1249  
1250                  return false;
1251              }
1252  
1253              return true;
1254          }
1255      }
1256  
1257      /**
1258       * Create new file.
1259       *
1260       * @param   string  $name      The name of file.
1261       * @param   string  $type      The extension of the file.
1262       * @param   string  $location  Location for the new file.
1263       *
1264       * @return  boolean  true if file created successfully, false otherwise
1265       *
1266       * @since   3.2
1267       */
1268      public function createFile($name, $type, $location)
1269      {
1270          if ($this->getTemplate()) {
1271              $app  = Factory::getApplication();
1272              $base = $this->getBasePath();
1273  
1274              if (file_exists(Path::clean($base . '/' . $location . '/' . $name . '.' . $type))) {
1275                  $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error');
1276  
1277                  return false;
1278              }
1279  
1280              if (!fopen(Path::clean($base . '/' . $location . '/' . $name . '.' . $type), 'x')) {
1281                  $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_CREATE_ERROR'), 'error');
1282  
1283                  return false;
1284              }
1285  
1286              // Check if the format is allowed and will be showed in the backend
1287              $check = $this->checkFormat($type);
1288  
1289              // Add a message if we are not allowed to show this file in the backend.
1290              if (!$check) {
1291                  $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_WARNING_FORMAT_WILL_NOT_BE_VISIBLE', $type), 'warning');
1292              }
1293  
1294              return true;
1295          }
1296      }
1297  
1298      /**
1299       * Upload new file.
1300       *
1301       * @param   array   $file      The uploaded file array.
1302       * @param   string  $location  Location for the new file.
1303       *
1304       * @return   boolean  True if file uploaded successfully, false otherwise
1305       *
1306       * @since   3.2
1307       */
1308      public function uploadFile($file, $location)
1309      {
1310          if ($this->getTemplate()) {
1311              $app      = Factory::getApplication();
1312              $path     = $this->getBasePath();
1313              $fileName = File::makeSafe($file['name']);
1314  
1315              $err = null;
1316  
1317              if (!TemplateHelper::canUpload($file, $err)) {
1318                  // Can't upload the file
1319                  return false;
1320              }
1321  
1322              if (file_exists(Path::clean($path . '/' . $location . '/' . $file['name']))) {
1323                  $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error');
1324  
1325                  return false;
1326              }
1327  
1328              if (!File::upload($file['tmp_name'], Path::clean($path . '/' . $location . '/' . $fileName))) {
1329                  $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_UPLOAD_ERROR'), 'error');
1330  
1331                  return false;
1332              }
1333  
1334              $url = Path::clean($location . '/' . $fileName);
1335  
1336              return $url;
1337          }
1338      }
1339  
1340      /**
1341       * Create new folder.
1342       *
1343       * @param   string  $name      The name of the new folder.
1344       * @param   string  $location  Location for the new folder.
1345       *
1346       * @return   boolean  True if override folder is created successfully, false otherwise
1347       *
1348       * @since   3.2
1349       */
1350      public function createFolder($name, $location)
1351      {
1352          if ($this->getTemplate()) {
1353              $app    = Factory::getApplication();
1354              $path   = Path::clean($location . '/');
1355              $base   = $this->getBasePath();
1356  
1357              if (file_exists(Path::clean($base . $path . $name))) {
1358                  $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_EXISTS'), 'error');
1359  
1360                  return false;
1361              }
1362  
1363              if (!Folder::create(Path::clean($base . $path . $name))) {
1364                  $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_CREATE_ERROR'), 'error');
1365  
1366                  return false;
1367              }
1368  
1369              return true;
1370          }
1371      }
1372  
1373      /**
1374       * Delete a folder.
1375       *
1376       * @param   string  $location  The name and location of the folder.
1377       *
1378       * @return  boolean  True if override folder is deleted successfully, false otherwise
1379       *
1380       * @since   3.2
1381       */
1382      public function deleteFolder($location)
1383      {
1384          if ($this->getTemplate()) {
1385              $app  = Factory::getApplication();
1386              $base = $this->getBasePath();
1387              $path = Path::clean($location . '/');
1388  
1389              if (!file_exists($base . $path)) {
1390                  $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_NOT_EXISTS'), 'error');
1391  
1392                  return false;
1393              }
1394  
1395              $return = Folder::delete($base . $path);
1396  
1397              if (!$return) {
1398                  $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_ERROR'), 'error');
1399  
1400                  return false;
1401              }
1402  
1403              return true;
1404          }
1405      }
1406  
1407      /**
1408       * Rename a file.
1409       *
1410       * @param   string  $file  The name and location of the old file
1411       * @param   string  $name  The new name of the file.
1412       *
1413       * @return  string  Encoded string containing the new file location.
1414       *
1415       * @since   3.2
1416       */
1417      public function renameFile($file, $name)
1418      {
1419          if ($this->getTemplate()) {
1420              $app          = Factory::getApplication();
1421              $path         = $this->getBasePath();
1422              $fileName     = base64_decode($file);
1423              $explodeArray = explode('.', $fileName);
1424              $type         = end($explodeArray);
1425              $explodeArray = explode('/', $fileName);
1426              $newName      = str_replace(end($explodeArray), $name . '.' . $type, $fileName);
1427  
1428              if (file_exists($path . $newName)) {
1429                  $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error');
1430  
1431                  return false;
1432              }
1433  
1434              if (!rename($path . $fileName, $path . $newName)) {
1435                  $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_RENAME_ERROR'), 'error');
1436  
1437                  return false;
1438              }
1439  
1440              return base64_encode($newName);
1441          }
1442      }
1443  
1444      /**
1445       * Get an image address, height and width.
1446       *
1447       * @return  array an associative array containing image address, height and width.
1448       *
1449       * @since   3.2
1450       */
1451      public function getImage()
1452      {
1453          if ($this->getTemplate()) {
1454              $app      = Factory::getApplication();
1455              $fileName = base64_decode($app->input->get('file'));
1456              $path     = $this->getBasePath();
1457  
1458              $uri = Uri::root(false) . ltrim(str_replace(JPATH_ROOT, '', $this->getBasePath()), '/');
1459  
1460              if (file_exists(Path::clean($path . $fileName))) {
1461                  $JImage = new Image(Path::clean($path . $fileName));
1462                  $image['address'] = $uri . $fileName;
1463                  $image['path']    = $fileName;
1464                  $image['height']  = $JImage->getHeight();
1465                  $image['width']   = $JImage->getWidth();
1466              } else {
1467                  $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_IMAGE_FILE_NOT_FOUND'), 'error');
1468  
1469                  return false;
1470              }
1471  
1472              return $image;
1473          }
1474      }
1475  
1476      /**
1477       * Crop an image.
1478       *
1479       * @param   string  $file  The name and location of the file
1480       * @param   string  $w     width.
1481       * @param   string  $h     height.
1482       * @param   string  $x     x-coordinate.
1483       * @param   string  $y     y-coordinate.
1484       *
1485       * @return  boolean     true if image cropped successfully, false otherwise.
1486       *
1487       * @since   3.2
1488       */
1489      public function cropImage($file, $w, $h, $x, $y)
1490      {
1491          if ($this->getTemplate()) {
1492              $app      = Factory::getApplication();
1493              $path     = $this->getBasePath() . base64_decode($file);
1494  
1495              try {
1496                  $image      = new Image($path);
1497                  $properties = $image->getImageFileProperties($path);
1498  
1499                  switch ($properties->mime) {
1500                      case 'image/webp':
1501                          $imageType = \IMAGETYPE_WEBP;
1502                          break;
1503                      case 'image/png':
1504                          $imageType = \IMAGETYPE_PNG;
1505                          break;
1506                      case 'image/gif':
1507                          $imageType = \IMAGETYPE_GIF;
1508                          break;
1509                      default:
1510                          $imageType = \IMAGETYPE_JPEG;
1511                  }
1512  
1513                  $image->crop($w, $h, $x, $y, false);
1514                  $image->toFile($path, $imageType);
1515  
1516                  return true;
1517              } catch (\Exception $e) {
1518                  $app->enqueueMessage($e->getMessage(), 'error');
1519              }
1520          }
1521      }
1522  
1523      /**
1524       * Resize an image.
1525       *
1526       * @param   string  $file    The name and location of the file
1527       * @param   string  $width   The new width of the image.
1528       * @param   string  $height  The new height of the image.
1529       *
1530       * @return   boolean  true if image resize successful, false otherwise.
1531       *
1532       * @since   3.2
1533       */
1534      public function resizeImage($file, $width, $height)
1535      {
1536          if ($this->getTemplate()) {
1537              $app  = Factory::getApplication();
1538              $path = $this->getBasePath() . base64_decode($file);
1539  
1540              try {
1541                  $image      = new Image($path);
1542                  $properties = $image->getImageFileProperties($path);
1543  
1544                  switch ($properties->mime) {
1545                      case 'image/webp':
1546                          $imageType = \IMAGETYPE_WEBP;
1547                          break;
1548                      case 'image/png':
1549                          $imageType = \IMAGETYPE_PNG;
1550                          break;
1551                      case 'image/gif':
1552                          $imageType = \IMAGETYPE_GIF;
1553                          break;
1554                      default:
1555                          $imageType = \IMAGETYPE_JPEG;
1556                  }
1557  
1558                  $image->resize($width, $height, false, Image::SCALE_FILL);
1559                  $image->toFile($path, $imageType);
1560  
1561                  return true;
1562              } catch (\Exception $e) {
1563                  $app->enqueueMessage($e->getMessage(), 'error');
1564              }
1565          }
1566      }
1567  
1568      /**
1569       * Template preview.
1570       *
1571       * @return  object  object containing the id of the template.
1572       *
1573       * @since   3.2
1574       */
1575      public function getPreview()
1576      {
1577          $app = Factory::getApplication();
1578          $db = $this->getDatabase();
1579          $query = $db->getQuery(true);
1580  
1581          $query->select($db->quoteName(['id', 'client_id']));
1582          $query->from($db->quoteName('#__template_styles'));
1583          $query->where($db->quoteName('template') . ' = :template')
1584              ->bind(':template', $this->template->element);
1585  
1586          $db->setQuery($query);
1587  
1588          try {
1589              $result = $db->loadObject();
1590          } catch (\RuntimeException $e) {
1591              $app->enqueueMessage($e->getMessage(), 'warning');
1592          }
1593  
1594          if (empty($result)) {
1595              $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXTENSION_RECORD_NOT_FOUND'), 'warning');
1596          } else {
1597              return $result;
1598          }
1599      }
1600  
1601      /**
1602       * Rename a file.
1603       *
1604       * @return  mixed  array on success, false on failure
1605       *
1606       * @since   3.2
1607       */
1608      public function getFont()
1609      {
1610          if ($template = $this->getTemplate()) {
1611              $app          = Factory::getApplication();
1612              $client       = ApplicationHelper::getClientInfo($template->client_id);
1613              $relPath      = base64_decode($app->input->get('file'));
1614              $explodeArray = explode('/', $relPath);
1615              $fileName     = end($explodeArray);
1616              $path         = $this->getBasePath() . base64_decode($app->input->get('file'));
1617  
1618              if (stristr($client->path, 'administrator') == false) {
1619                  $folder = '/templates/';
1620              } else {
1621                  $folder = '/administrator/templates/';
1622              }
1623  
1624              $uri = Uri::root(true) . $folder . $template->element;
1625  
1626              if (file_exists(Path::clean($path))) {
1627                  $font['address'] = $uri . $relPath;
1628  
1629                  $font['rel_path'] = $relPath;
1630  
1631                  $font['name'] = $fileName;
1632              } else {
1633                  $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_FONT_FILE_NOT_FOUND'), 'error');
1634  
1635                  return false;
1636              }
1637  
1638              return $font;
1639          }
1640      }
1641  
1642      /**
1643       * Copy a file.
1644       *
1645       * @param   string  $newName   The name of the copied file
1646       * @param   string  $location  The final location where the file is to be copied
1647       * @param   string  $file      The name and location of the file
1648       *
1649       * @return   boolean  true if image resize successful, false otherwise.
1650       *
1651       * @since   3.2
1652       */
1653      public function copyFile($newName, $location, $file)
1654      {
1655          if ($this->getTemplate()) {
1656              $app          = Factory::getApplication();
1657              $relPath      = base64_decode($file);
1658              $explodeArray = explode('.', $relPath);
1659              $ext          = end($explodeArray);
1660              $path         = $this->getBasePath();
1661              $newPath      = Path::clean($path . $location . '/' . $newName . '.' . $ext);
1662  
1663              if (file_exists($newPath)) {
1664                  $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error');
1665  
1666                  return false;
1667              }
1668  
1669              if (File::copy($path . $relPath, $newPath)) {
1670                  $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_FILE_COPY_SUCCESS', $newName . '.' . $ext));
1671  
1672                  return true;
1673              } else {
1674                  return false;
1675              }
1676          }
1677      }
1678  
1679      /**
1680       * Get the compressed files.
1681       *
1682       * @return   array if file exists, false otherwise
1683       *
1684       * @since   3.2
1685       */
1686      public function getArchive()
1687      {
1688          if ($this->getTemplate()) {
1689              $app  = Factory::getApplication();
1690              $path = $this->getBasePath() . base64_decode($app->input->get('file'));
1691  
1692              if (file_exists(Path::clean($path))) {
1693                  $files = array();
1694                  $zip = new \ZipArchive();
1695  
1696                  if ($zip->open($path) === true) {
1697                      for ($i = 0; $i < $zip->numFiles; $i++) {
1698                          $entry = $zip->getNameIndex($i);
1699                          $files[] = $entry;
1700                      }
1701                  } else {
1702                      $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error');
1703  
1704                      return false;
1705                  }
1706              } else {
1707                  $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_FONT_FILE_NOT_FOUND'), 'error');
1708  
1709                  return false;
1710              }
1711  
1712              return $files;
1713          }
1714      }
1715  
1716      /**
1717       * Extract contents of an archive file.
1718       *
1719       * @param   string  $file  The name and location of the file
1720       *
1721       * @return  boolean  true if image extraction is successful, false otherwise.
1722       *
1723       * @since   3.2
1724       */
1725      public function extractArchive($file)
1726      {
1727          if ($this->getTemplate()) {
1728              $app          = Factory::getApplication();
1729              $relPath      = base64_decode($file);
1730              $explodeArray = explode('/', $relPath);
1731              $fileName     = end($explodeArray);
1732              $path         = $this->getBasePath() . base64_decode($file);
1733  
1734              if (file_exists(Path::clean($path . '/' . $fileName))) {
1735                  $zip = new \ZipArchive();
1736  
1737                  if ($zip->open(Path::clean($path . '/' . $fileName)) === true) {
1738                      for ($i = 0; $i < $zip->numFiles; $i++) {
1739                          $entry = $zip->getNameIndex($i);
1740  
1741                          if (file_exists(Path::clean($path . '/' . $entry))) {
1742                              $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXISTS'), 'error');
1743  
1744                              return false;
1745                          }
1746                      }
1747  
1748                      $zip->extractTo($path);
1749  
1750                      return true;
1751                  } else {
1752                      $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error');
1753  
1754                      return false;
1755                  }
1756              } else {
1757                  $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_NOT_FOUND'), 'error');
1758  
1759                  return false;
1760              }
1761          }
1762      }
1763  
1764      /**
1765       * Check if the extension is allowed and will be shown in the template manager
1766       *
1767       * @param   string  $ext  The extension to check if it is allowed
1768       *
1769       * @return  boolean  true if the extension is allowed false otherwise
1770       *
1771       * @since   3.6.0
1772       */
1773      protected function checkFormat($ext)
1774      {
1775          if (!isset($this->allowedFormats)) {
1776              $params       = ComponentHelper::getParams('com_templates');
1777              $imageTypes   = explode(',', $params->get('image_formats'));
1778              $sourceTypes  = explode(',', $params->get('source_formats'));
1779              $fontTypes    = explode(',', $params->get('font_formats'));
1780              $archiveTypes = explode(',', $params->get('compressed_formats'));
1781  
1782              $this->allowedFormats = array_merge($imageTypes, $sourceTypes, $fontTypes, $archiveTypes);
1783              $this->allowedFormats = array_map('strtolower', $this->allowedFormats);
1784          }
1785  
1786          return in_array(strtolower($ext), $this->allowedFormats);
1787      }
1788  
1789      /**
1790       * Method to get a list of all the files to edit in a template's media folder.
1791       *
1792       * @return  array  A nested array of relevant files.
1793       *
1794       * @since   4.1.0
1795       */
1796      public function getMediaFiles()
1797      {
1798          $result = [];
1799          $template = $this->getTemplate();
1800  
1801          if (!isset($template->xmldata)) {
1802              $template->xmldata = TemplatesHelper::parseXMLTemplateFile($template->client_id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name);
1803          }
1804  
1805          if (!isset($template->xmldata->inheritable) || (isset($template->xmldata->parent) && $template->xmldata->parent === '')) {
1806              return $result;
1807          }
1808  
1809          $app  = Factory::getApplication();
1810          $path = Path::clean(JPATH_ROOT . '/media/templates/' . ($template->client_id === 0 ? 'site' : 'administrator') . '/' . $template->element . '/');
1811          $this->mediaElement = $path;
1812  
1813          if (!is_writable($path)) {
1814              $app->enqueueMessage(Text::_('COM_TEMPLATES_DIRECTORY_NOT_WRITABLE'), 'error');
1815          }
1816  
1817          if (is_dir($path)) {
1818              $result = $this->getDirectoryTree($path);
1819          }
1820  
1821          return $result;
1822      }
1823  
1824      /**
1825       * Method to resolve the base folder.
1826       *
1827       * @return  string  The absolute path for the base.
1828       *
1829       * @since   4.1.0
1830       */
1831      private function getBasePath()
1832      {
1833          $app      = Factory::getApplication();
1834          $isMedia  = $app->input->getInt('isMedia', 0);
1835  
1836          return $isMedia ? JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element :
1837              JPATH_ROOT . '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element;
1838      }
1839  
1840      /**
1841       * Method to create the templateDetails.xml for the child template
1842       *
1843       * @return  boolean   true if name is not used, false otherwise
1844       *
1845       * @since  4.1.0
1846       */
1847      public function child()
1848      {
1849          $app      = Factory::getApplication();
1850          $template = $this->getTemplate();
1851  
1852          if (!(array) $template) {
1853              $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error');
1854  
1855              return false;
1856          }
1857  
1858          $client   = ApplicationHelper::getClientInfo($template->client_id);
1859          $fromPath = Path::clean($client->path . '/templates/' . $template->element . '/templateDetails.xml');
1860  
1861          // Delete new folder if it exists
1862          $toPath = $this->getState('to_path');
1863  
1864          if (Folder::exists($toPath)) {
1865              if (!Folder::delete($toPath)) {
1866                  $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error');
1867  
1868                  return false;
1869              }
1870          } else {
1871              if (!Folder::create($toPath)) {
1872                  $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error');
1873  
1874                  return false;
1875              }
1876          }
1877  
1878          // Copy the template definition from the parent template
1879          if (!File::copy($fromPath, $toPath . '/templateDetails.xml')) {
1880              return false;
1881          }
1882  
1883          // Check manifest for additional files
1884          $newName  = strtolower($this->getState('new_name'));
1885          $template = $this->getTemplate();
1886  
1887          // Edit XML file
1888          $xmlFile = Path::clean($this->getState('to_path') . '/templateDetails.xml');
1889  
1890          if (!File::exists($xmlFile)) {
1891              $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error');
1892  
1893              return false;
1894          }
1895  
1896          try {
1897              $xml = simplexml_load_string(file_get_contents($xmlFile));
1898          } catch (\Exception $e) {
1899              $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_READ'), 'error');
1900  
1901              return false;
1902          }
1903  
1904          $user = Factory::getUser();
1905          unset($xml->languages);
1906          unset($xml->media);
1907          unset($xml->files);
1908          unset($xml->parent);
1909          unset($xml->inheritable);
1910  
1911          // Remove the update parts
1912          unset($xml->update);
1913          unset($xml->updateservers);
1914  
1915          if (isset($xml->creationDate)) {
1916              $xml->creationDate = (new Date('now'))->format('F Y');
1917          } else {
1918              $xml->addChild('creationDate', (new Date('now'))->format('F Y'));
1919          }
1920  
1921          if (isset($xml->author)) {
1922              $xml->author = $user->name;
1923          } else {
1924              $xml->addChild('author', $user->name);
1925          }
1926  
1927          if (isset($xml->authorEmail)) {
1928              $xml->authorEmail = $user->email;
1929          } else {
1930              $xml->addChild('authorEmail', $user->email);
1931          }
1932  
1933          $files = $xml->addChild('files');
1934          $files->addChild('filename', 'templateDetails.xml');
1935  
1936          // Media folder
1937          $media = $xml->addChild('media');
1938          $media->addAttribute('folder', 'media');
1939          $media->addAttribute('destination', 'templates/' . ($template->client_id === 0 ? 'site/' : 'administrator/') . $template->element . '_' . $newName);
1940          $media->addChild('folder', 'css');
1941          $media->addChild('folder', 'js');
1942          $media->addChild('folder', 'images');
1943          $media->addChild('folder', 'html');
1944          $media->addChild('folder', 'scss');
1945  
1946          $xml->name = $template->element . '_' . $newName;
1947          $xml->inheritable = 0;
1948          $files = $xml->addChild('parent', $template->element);
1949  
1950          $dom = new \DOMDocument();
1951          $dom->preserveWhiteSpace = false;
1952          $dom->formatOutput = true;
1953          $dom->loadXML($xml->asXML());
1954  
1955          $result = File::write($xmlFile, $dom->saveXML());
1956  
1957          if (!$result) {
1958              $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error');
1959  
1960              return false;
1961          }
1962  
1963          // Create an empty media folder structure
1964          if (
1965              !Folder::create($toPath . '/media')
1966              || !Folder::create($toPath . '/media/css')
1967              || !Folder::create($toPath . '/media/js')
1968              || !Folder::create($toPath . '/media/images')
1969              || !Folder::create($toPath . '/media/html/tinymce')
1970              || !Folder::create($toPath . '/media/scss')
1971          ) {
1972              return false;
1973          }
1974  
1975          return true;
1976      }
1977  
1978      /**
1979       * Method to get the parent template existing styles
1980       *
1981       * @return  array   array of id,titles of the styles
1982       *
1983       * @since  4.1.3
1984       */
1985      public function getAllTemplateStyles()
1986      {
1987          $template = $this->getTemplate();
1988  
1989          if (empty($template->xmldata->inheritable)) {
1990              return [];
1991          }
1992  
1993          $db    = $this->getDatabase();
1994          $query = $db->getQuery(true);
1995  
1996          $query->select($db->quoteName(['id', 'title']))
1997              ->from($db->quoteName('#__template_styles'))
1998              ->where($db->quoteName('client_id') . ' = :client_id', 'AND')
1999              ->where($db->quoteName('template') . ' = :template')
2000              ->orWhere($db->quoteName('parent') . ' = :parent')
2001              ->bind(':client_id', $template->client_id, ParameterType::INTEGER)
2002              ->bind(':template', $template->element)
2003              ->bind(':parent', $template->element);
2004  
2005          $db->setQuery($query);
2006  
2007          return $db->loadObjectList();
2008      }
2009  
2010      /**
2011       * Method to copy selected styles to the child template
2012       *
2013       * @return  boolean   true if name is not used, false otherwise
2014       *
2015       * @since  4.1.3
2016       */
2017      public function copyStyles()
2018      {
2019          $app         = Factory::getApplication();
2020          $template    = $this->getTemplate();
2021          $newName     = strtolower($this->getState('new_name'));
2022          $applyStyles = $this->getState('stylesToCopy');
2023  
2024          // Get a db connection.
2025          $db = $this->getDatabase();
2026  
2027          // Create a new query object.
2028          $query = $db->getQuery(true);
2029  
2030          $query->select($db->quoteName(['title', 'params']))
2031              ->from($db->quoteName('#__template_styles'))
2032              ->whereIn($db->quoteName('id'), ArrayHelper::toInteger($applyStyles));
2033          // Reset the query using our newly populated query object.
2034          $db->setQuery($query);
2035  
2036          try {
2037              $parentStyle = $db->loadObjectList();
2038          } catch (\Exception $e) {
2039              $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND'), 'error');
2040  
2041              return false;
2042          }
2043  
2044          foreach ($parentStyle as $style) {
2045              $query = $db->getQuery(true);
2046              $styleName = Text::sprintf('COM_TEMPLATES_COPY_CHILD_TEMPLATE_STYLES', ucfirst($template->element . '_' . $newName), $style->title);
2047  
2048              // Insert columns and values
2049              $columns = ['id', 'template', 'client_id', 'home', 'title', 'inheritable', 'parent', 'params'];
2050              $values = [0, $db->quote($template->element . '_' . $newName), (int) $template->client_id, $db->quote('0'), $db->quote($styleName), 0, $db->quote($template->element), $db->quote($style->params)];
2051  
2052              $query
2053                  ->insert($db->quoteName('#__template_styles'))
2054                  ->columns($db->quoteName($columns))
2055                  ->values(implode(',', $values));
2056  
2057              $db->setQuery($query);
2058  
2059              try {
2060                  $db->execute();
2061              } catch (\Exception $e) {
2062                  $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_READ'), 'error');
2063  
2064                  return false;
2065              }
2066          }
2067  
2068          return true;
2069      }
2070  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer