. * */ namespace Vvveb\Controller\Editor; use \Vvveb\Sql\menuSQL; use function Vvveb\__; use Vvveb\Controller\Base; use function Vvveb\getUrl; use function Vvveb\model; use function Vvveb\sanitizeFileName; use function Vvveb\slugify; use Vvveb\Sql\PostSQL; use Vvveb\Sql\ProductSQL; use Vvveb\System\Cache; use Vvveb\System\CacheManager; use Vvveb\System\Core\View; use Vvveb\System\Event; use Vvveb\System\PageCache; use Vvveb\System\Sites; use function Vvveb\url; class Editor extends Base { use GlobalTrait; protected $revisionDateFormat = 'Y-m-d_H:i:s'; private $themeConfig = []; private $skipFolders = ['src', 'source', 'backup', 'import']; private $skipFiles = []; function init() { $return = parent::init(); $this->loadThemeConfig(); return $return; } function oEmbedProxy() { $url = $this->request->get['url']; if (! $url) { return; } $result = getUrl($url, false); $this->response->setType('json'); $this->response->output($result); } private function getTheme() { return $theme = sanitizeFileName($this->request->get['theme'] ?? Sites::getTheme($this->global['site_id']) ?? 'default'); } private function getThemeFolder() { return DIR_THEMES . $this->getTheme(); } private function loadThemeConfig() { $config = $this->getThemeFolder() . DS . 'theme.php'; if (file_exists($config)) { $this->themeConfig = include $config; } else { //default config $this->themeConfig = [ ]; } } private function loadTemplateList($theme = null) { $list = $this->themeConfig['pages'] ?? []; $pages = $list + Cache::getInstance()->cache(APP, 'template-list-' . $theme, function () use ($theme) { return \Vvveb\getTemplateList($theme); }, 604800); list($pages) = Event::trigger(__CLASS__, __FUNCTION__, $pages); return $pages; } private function clearTemplateListCache($theme = null) { return Cache::getInstance()->delete(APP, 'template-list-' . $theme); } private function loadEditorData() { $data = []; //menu list $menuSql = new \Vvveb\Sql\menuSQL(); $results = $menuSql->getAll($this->global); $data += $results; list($data) = Event::trigger(__CLASS__, __FUNCTION__, $data); return $data; } /* Load theme sections, components and inputs */ private function loadEditorAssets() { } /* Load theme sections, components and inputs */ private function loadThemeAssets() { $themeFolder = $this->getThemeFolder(); $view = &$this->view; $themeJs = []; foreach (['inputs', 'components', 'blocks', 'sections'] as $type) { $$type = []; $glob = glob("$themeFolder/$type/*.js"); foreach ($glob as &$file) { $base = str_replace('.js', '', basename($file)); $$type[$base] = str_replace($themeFolder, $view->themeBaseUrl, $file); } } list($inputs, $components, $blocks, $sections) = Event::trigger(__CLASS__, __FUNCTION__, $inputs, $components, $blocks, $sections); $vvvebJs = '/js/vvvebjs.js'; if (file_exists($themeFolder . $vvvebJs)) { $themeJs['vvvebjs'] = $view->themeBaseUrl . $vvvebJs; } $view->themeInputs = $inputs; $view->themeComponents = $components; $view->themeSections = $sections; $view->themeBlocks = $blocks; $view->themeJs = $themeJs; } function proxy() { } function index() { $theme = $this->getTheme(); $themeParam = ($theme ? '&theme=' . $theme : ''); $view = View::getInstance(); $view->themeBaseUrl = PUBLIC_PATH . 'themes/' . $theme . '/'; $view->themeName = $theme; $view->pages = $this->loadTemplateList($theme); $view->themeFonts = Cache::getInstance()->cache(APP,'fonts-list-' . $theme, function () use ($theme) { $fonts = \Vvveb\System\Media\Font::themeFonts($theme); $names = []; if ($fonts) { foreach ($fonts as $font) { if (isset($font['font-family'])) { $names[$font['font-family']] = null; } } } return $names; }, 604800); $this->loadThemeAssets(); $urlPrefix = ''; $urlParams = ''; //if admin host is different than site host (for multi site install) use full url including host if ($_SERVER['HTTP_HOST'] !== $this->global['site_url']) { //$urlPrefix = '//' . $this->global['site_url']; $urlParams = '&site_id=' . $this->global['site_id']; } $this->posts = new PostSQL(); $options = [ 'type' => 'page', 'limit' => 100, ] + $this->global; $results = $this->posts->getAll($options); $posts = []; if ($results && isset($results['post'])) { foreach ($results['post'] as $post) { //skip posts without translations if (! isset($post['slug']) || ! isset($post['name'])) { continue; } $slug = htmlspecialchars($post['slug']); $url = url('content/page/index',['slug' => $slug, 'post_id' => $post['post_id']]); $posts["$slug-page"] = [ 'name' => "$slug-page", 'file' => $post['template'] ? $post['template'] : 'content/page.html', 'url' => $urlPrefix . $url . ($theme ? '?theme=' . $theme : '') . $urlParams, 'title' => htmlspecialchars($post['name']), 'post_id' => $post['post_id'], 'folder' => '', 'className' => 'page', ]; } } if ($posts) { $view->pages = $posts + $view->pages; } if (isset($this->request->get['url'])) { $url = sanitizeFileName($this->request->get['url']); $name = $this->request->get['name'] ?? ''; $template = $this->request->get['template'] ?? ''; //\Vvveb\getUrlTemplate($url) ?? 'index.html'; $folder = $this->request->get['folder'] ?? false; $route = \Vvveb\getUrlRoute($url); $file = $template; $className = 'url'; $current_page = []; //check if url and template is relative if (strpos($url, '//') !== false && strpos($template, '..') !== false) { $this->notFound(); exit(); } //check if the url has extension and is a html file and exists in the theme folder if (strpos($url, '.') !== false) { if (substr_compare($url, '.html', -5 ,5) === 0) { //check if plugin template exists if (substr_compare($url, '/plugins/', 0 ,9) === 0) { $pluginFile = substr($url, 9); $pluginFile = preg_replace('@^(.+/)@', '$1/public/',$pluginFile); if (! file_exists(DIR_PLUGINS . $pluginFile)) { $this->notFound(sprintf(__('%s does not exist!'), $pluginFile)); exit(); } } else { //check if theme template exists if (! file_exists(DIR_THEMES . $theme . DS . $url)) { $this->notFound(sprintf(__('%s does not exist!'), $url)); exit(); } } } else { $this->notFound(sprintf(__('%s does not exist!'), $url)); exit(); } } //check if template belongs to theme and is a html file if (substr_compare($template, '.html', -5 ,5) === 0) { //check if plugin template exists if (substr_compare($template, '/plugins/', 0 ,9) === 0) { $pluginFile = substr($template, 9); $pluginFile = preg_replace('@^(.+/)@', '$1/public/', $pluginFile); if (! file_exists(DIR_PLUGINS . $pluginFile)) { $this->notFound(sprintf(__('%s does not exist!'), $pluginFile)); exit(); } } else { //check if theme template exists if (! file_exists(DIR_THEMES . $theme . DS . $template)) { $this->notFound(sprintf(__('%s does not exist!'), $template)); exit(); } } } else { $this->notFound(); exit(); } if ($route && isset($route['module'])) { switch ($route['module']) { case 'product/product/index': $className = 'product'; if (isset($route['product_id'])) { $current_page['product_id'] = $route['product_id']; } else { if (isset($this->request->get['product_id'])) { $current_page['product_id'] = $this->request->get['product_id']; } else { if (isset($route['slug'])) { $current_page['slug'] = htmlspecialchars($route['slug']); } } } break; case 'content/post/index': case 'content/page/index': $className = 'page'; if (isset($route['post_id'])) { $current_page['post_id'] = $route['post_id']; } else { if (isset($this->request->get['post_id'])) { $current_page['post_id'] = $this->request->get['post_id']; } else { if (isset($route['slug'])) { $current_page['slug'] = htmlspecialchars($route['slug']); } } } break; } } $key = slugify($url); $slug = slugify(str_replace('.html', '', $template)); if ($url == '/') { $key = $slug = 'index'; } if (! $name) { //if in page list get pretty name if (isset($view->pages[$slug])) { $name = $view->pages[$slug]['title']; } else { $name = \Vvveb\humanReadable($url); } } $global = false; if ($file == 'index.html') { $global = true; } $current_page += [ 'name' => $key, 'file' => $file, 'url' => $urlPrefix . $url . ($theme ? '?theme=' . $theme : '') . $urlParams, 'title' => $name, 'folder' => '', 'className' => $className, 'global' => $global, ]; $view->pages = [$key => $current_page] + $view->pages; } $admin_path = \Vvveb\adminPath(); $mediaControllerPath = $admin_path . 'index.php?module=media/media'; $controllerPath = $admin_path . 'index.php?module=editor/editor' . $themeParam; $revisionsPath = $admin_path . 'index.php?module=editor/revisions' . $themeParam; $reusablePath = $admin_path . 'index.php?module=editor/reusable' . $themeParam; //media endpoints $this->view->scanUrl = "$mediaControllerPath&action=scan"; $this->view->uploadUrl = "$mediaControllerPath&action=upload"; $this->view->deleteUrl = "$mediaControllerPath&action=delete"; $this->view->renameUrl = "$mediaControllerPath&action=rename"; //editor endpoints $this->view->namespaceUrl = $admin_path . 'index.php?module=editor'; $this->view->editorUrl = $controllerPath; $this->view->saveUrl = "$controllerPath&action=save"; $this->view->deleteFileUrl = "$controllerPath&action=delete"; $this->view->renameFileUrl = "$controllerPath&action=rename"; $this->view->saveReusableUrl = "$reusablePath&action=save"; $this->view->oEmbedProxyUrl = "$controllerPath&action=oEmbedProxy"; $this->view->revisionsUrl = "$revisionsPath&action=revisions"; $this->view->revisionLoadUrl = "$revisionsPath&action=load"; $this->view->revisionDeleteUrl = "$revisionsPath&action=delete"; $this->view->linkUrl = $admin_path . 'index.php?module=content/post&action=urlAutocomplete&type=key-value'; $view->templates = \Vvveb\getTemplateList($theme); $view->folders = \Vvveb\getThemeFolderList($theme); $view->data = $this->loadEditorData(); } private function backup($page) { $themeFolder = $this->getThemeFolder() . DS; $backupFolder = $themeFolder . 'backup' . DS; $page = str_replace('.html', '', sanitizeFileName($page)); $backupName = str_replace(DS, '-', $page) . '@' . str_replace(':',';', date($this->revisionDateFormat)) . '.html'; $file = $themeFolder . $page . '.html'; if (is_dir($backupFolder)) { if (file_exists($file)) { $content = file_get_contents($themeFolder . $page . '.html'); if (strpos($content, '/', '', $content); } else { $content = str_replace('', "\n\n", $content); } return @file_put_contents($backupFolder . $backupName, $content); } } return false; } private function saveElements($elements) { $products = new ProductSQL(); $posts = new PostSQL(); $components = []; foreach ($elements as $element) { $component = $element['component']; $type = $element['type']; $id = $element['id']; $fields = $element['fields']; //todo: check and load components from plugins $componentName = "\Vvveb\Component\\$component"; if (! class_exists($componentName)) { include_once DIR_ROOT . "app/component/$component.php"; } if (! isset($components[$component])) { $components[$component] = new $componentName(); } $components[$component]->editorSave($id, $fields, $type); } /* switch ($type) { case 'product': $product_content = []; foreach ($fields as $field) { $name = $field['name']; $value = $field['value']; if ($name == 'name' || $name == 'content') { $product_content[$name] = $value; } else { $product[$name] = $value; } } //$product_content['product_id'] = $id; $product_content['language_id'] = 1; $product['product_content'][] = $product_content; $result = $products->edit(['product' => $product, 'product_id' => $id]); break; case 'post': $post_content = []; foreach ($fields as $field) { $name = $field['name']; $value = $field['value']; if ($name == 'name' || $name == 'content') { $post_content[$name] = $value; } else { $post[$name] = $value; } } //$post['post_content']['post_id'] = $id; $post_content['language_id'] = 1; $post['post_content'][] = $post_content; $result = $posts->edit(['post' => $post, 'post_id' => $id]); break; } */ return true; } function delete() { $post_id = $this->request->post['post_id'] ?? false; $file = sanitizeFileName($this->request->post['file'] ?? ''); $themeFolder = $this->getThemeFolder(); if ($post_id) { $type = 'page'; if ($post_id) { if (is_numeric($post_id)) { $post_id = [$post_id]; } $this->posts = new PostSQL(); $options = [ 'post_id' => $post_id, 'type' => $type, ] + $this->global; $result = $this->posts->delete($options); if ($result && isset($result['post'])) { $message = ['success' => true, 'message' => ucfirst($type) . __(' deleted!')]; } else { $message = ['success' => false, 'message' => sprintf(__('Error deleting %s!'), $type)]; } } } else { if ($file && file_exists($themeFolder . DS . $file)) { if (unlink($themeFolder . DS . $file)) { $message = ['success' => true, 'message' => __('File deleted!')]; } else { $message = ['success' => false, 'message' => __('Error deleting file!')]; } } else { $message = ['success' => false, 'message' => __('File does not exist!')]; } } $this->response->setType('json'); $this->response->output($message); } function rename() { $post_id = $this->request->post['post_id'] ?? false; $product_id = $this->request->post['product_id'] ?? false; $file = sanitizeFileName($this->request->post['file']); $newfile = sanitizeFileName($this->request->post['newfile']); $duplicate = $this->request->post['duplicate'] ?? 'false'; $themeFolder = $this->getThemeFolder(); $name = $this->request->post['name'] ?? $newfile; $theme = $this->getTheme(); $dir = dirname($file); if ($newfile) { $slug = sanitizeFileName(str_ireplace('.html', '', (basename($newfile)))); $newfile = $slug . '.html'; } $currentFile = $themeFolder . DS . $file; $targetFile = dirname($currentFile) . DS . $newfile; //save in same folder $message = ['success' => false, 'message' => __('Error!')]; if ($post_id || $product_id) { $model = 'post'; $type = 'page'; $namespace = 'content'; $model_id = $post_id; if ($product_id) { $model_id = $product_id; $namespace = 'product'; $model = 'product'; $type = 'product'; } if ($newfile) { $name = sanitizeFileName($this->request->post['name']); $slug = slugify($name); $this->posts = model($model); if ($duplicate === 'true') { $data = $this->posts->get([$model . '_id' => $model_id]); if ($data) { unset($data[$model . '_id']); $id = rand(1, 1000); foreach ($data[$model . '_content'] as &$content) { unset($content[$model . '_id']); if ($content['language_id'] == $this->global['language_id']) { $content['name'] = $name; $content['slug'] = $slug; } else { $content['name'] .= ' [' . __('duplicate') . ']'; $content['slug'] .= '-' . __('duplicate') . "-$id"; } } if (isset($data[$model . '_to_taxonomy_item'])) { foreach ($data[$model . '_to_taxonomy_item'] as &$item) { $taxonomy_item[] = $item['taxonomy_item_id']; } } if (isset($data[$model . '_to_site'])) { foreach ($data[$model . '_to_site'] as &$item) { $site_id[] = $item['site_id']; } } $startTemplateUrl = $data['template'] ?? "$namespace/$type.html"; $template = "$namespace/$slug.html"; if (! @copy($themeFolder . DS . $startTemplateUrl, $themeFolder . DS . $template)) { $template = $data['template'] ?? ''; } $new = [ $model => [ 'taxonomy_item' => $taxonomy_item ?? [], 'template' => $template, ] + $data, $model . '_content' => $data[$model . '_content'], 'site_id' => $site_id, ] + $data + $this->global; $result = $this->posts->add($new); $newfile = PUBLIC_PATH . "themes/$theme/" . ($dir ? $dir . '/' : '') . $template; if ($result && isset($result[$model])) { $model_id = $result[$model]; $message = ['success' => true, 'name' => $name, 'slug' => $slug, $model . '_id' => $model_id, 'newfile' => $newfile, 'url' => url("$namespace/$type/index", ['slug' => $slug, $model . '_id' => $model_id]), 'message' => ucfirst($type) . ' ' . __('duplicated') . '!']; } else { $message = ['success' => false, 'message' => sprintf(__('Error duplicating %s!'), $type)]; } } } else { $data = [ $model . '_content' => ['name' => $name, 'slug' => $slug], $model . '_id' => $model_id, 'language_id' => $this->global['language_id'], ]; $result = $this->posts->editContent($data); $newfile = PUBLIC_PATH . "themes/$theme/" . ($dir ? $dir . '/' : '') . $newfile; if ($result && isset($result[$model . '_content'])) { $message = ['success' => true, 'name' => $name, 'slug' => $slug, $model . '_id' => $model_id, 'newfile' => $newfile, 'url' => url("$namespace/$type/index", ['slug' => $slug, $model . '_id' => $model_id]), 'message' => ucfirst($type) . ' ' . __('renamed') . '!']; } else { $message = ['success' => false, 'message' => sprintf(__('Error renaming %s!'), $type)]; } } } } else { $newfile = PUBLIC_PATH . "themes/$theme/" . ($dir ? $dir . '/' : '') . $newfile; if ($duplicate === 'true') { if (@copy($currentFile, $targetFile)) { $message = ['success' => true, 'newfile' => $newfile, 'name' => $name, 'message' => __('File copied!'), 'url' => $newfile]; } else { $message = ['success' => false, 'message' => __('Error copying file!')]; } } else { if (rename($currentFile, $targetFile)) { $message = ['success' => true, 'newfile' => $newfile, 'name' => $name, 'message' => __('File renamed!')]; } else { $message = ['success' => false, 'message' => __('Error renaming file!')]; } } } $this->clearTemplateListCache(sanitizeFileName($this->request->get['theme'] ?? false)); $this->response->setType('json'); $this->response->output($message); } function save() { $file = $this->request->post['file'] ?? ''; $folder = $this->request->post['folder'] ?? ''; $startTemplateUrl = $this->request->post['startTemplateUrl'] ?? ''; $name = $this->request->post['name'] ?? ''; $content = $this->request->post['content'] ?? 'Lorem ipsum'; $image = $this->request->post['image'] ?? ''; $type = $this->request->post['type'] ?? false; $templateType = $this->request->post['template-type'] ?? false; $addMenu = $this->request->post['add-menu'] ?? false; $menu_id = $this->request->post['menu_id'] ?? false; $uri = $this->request->post['url'] ?? '/'; $theme = $this->getTheme(); $url = ''; $file = sanitizeFileName(str_replace('.html', '', $file)) . '.html'; $folder = trim(sanitizeFileName($folder), '/'); $startTemplateUrl = sanitizeFileName($startTemplateUrl); if ($type && $name) { $slug = slugify($name); $success = false; switch ($type) { case 'page': case 'post': $file = $templateType == 'global' ? "content/$type.html" : "content/$slug.html"; $file = sanitizeFileName($file); $template = $templateType == 'global' && $startTemplateUrl ? $startTemplateUrl : $file; $post = new PostSQL(); $result = $post->add([ 'post' => [ 'template' => $template, 'type' => $type, 'image' => $image, ] + $this->global, 'post_content' => [[ 'slug' => $slug, 'name' => $name, 'content' => $content, 'language_id' => $this->global['language_id'], ]], 'site_id' => [$this->global['site_id']], ] + $this->global); if ($result['post']) { $post_id = $result['post']; $success = true; $route = "content/{$type}/index"; $url = \Vvveb\url($route, ['slug'=> $slug, 'post_id'=> $post_id]); } break; case 'product': $file = $templateType == 'global' ? "product/$type.html" : "product/$slug.html"; $file = sanitizeFileName($file); $template = $templateType == 'global' && $startTemplateUrl ? $startTemplateUrl : $file; $price = $this->request->post['price'] ?? 0; $product = new ProductSQL(); $result = $product->add([ 'product' => [ 'model' => '', 'image' => $image, 'status' => 1, //active 'template' => $file, 'price' => $price, ] + $this->global, 'product_content' => [[ 'slug' => $slug, 'name' => $name, 'name' => $name, 'content' => $content, 'language_id' => $this->global['language_id'], ]], 'site_id' => [$this->global['site_id']], ] + $this->global); if ($result['product']) { $product_id = $result['product']; $success = true; $route = "product/{$type}/index"; $url = \Vvveb\url($route, ['slug' => $slug, 'product_id'=> $product_id]); } break; } } $html = $this->request->post['html'] ?? false; $elements = $this->request->post['elements'] ?? false; $setTemplate = $this->request->post['setTemplate'] ?? false; $view = View::getInstance(); $view->noJson = true; $success = false; $text = ''; $baseUrl = PUBLIC_PATH . 'themes/' . $theme . '/' . ($folder ? $folder . '/' : ''); $themeFolder = $this->getThemeFolder(); $relativeBase = ''; $pos = 0; //if more than one level deep add one level up for each level $fileBase = ($folder ? $folder . '/' : '') . $file; while (($pos = strpos($fileBase, '/', $pos + 1)) !== false) { $relativeBase .= '../'; } if ($startTemplateUrl) { $startTemplate = $themeFolder . DS . $startTemplateUrl; if (file_exists($startTemplate)) { if (! ($html = @file_get_contents($startTemplate))) { $text .= sprintf(__('%s is not readable!'), $startTemplate); } } else { $text .= sprintf(__('%s does not exist!'), $startTemplate); } $html = preg_replace('@]+>@', '', $html); } if (! $url) { $url = "$baseUrl$file"; } $data = compact('file', 'name', 'url', 'startTemplateUrl'); if ($elements) { if ($this->saveElements($elements)) { $success = true; $text = __('Elements saved!') . "\n"; } else { $success = false; $text = __('Error saving elements!') . "\n"; } } //if plugins template use public path $isPlugin = false; if (strncmp($file, '/plugins/', 9) === 0) { $pluginFile = substr($file, 9); $pluginFile = preg_replace('@^(.+/)@', '$1/public/', $pluginFile); $fileName = DIR_PLUGINS . /*($folder ? $folder . DS : '') .*/ $pluginFile; $isPlugin = true; } else { $fileName = $themeFolder . DS . ($folder ? $folder . DS : '') . $file; } if (! $startTemplateUrl && ! $isPlugin) { $backupFolder = $themeFolder . DS . 'backup' . DS; if (is_writable($backupFolder)) { if ($this->backup($file)) { } else { $success = false; $text .= __('Error saving revision!') . "\n"; } } else { $success = false; $text .= sprintf(__('%s folder not writable!'), $theme . DS . 'backup') . "\n"; } } if ($addMenu && $menu_id) { $menuData = ['menu_item' => [ 'menu_id' => $menu_id, 'url' => $url, 'menu_item_content' => [[ 'name' => $name, 'slug' => $slug, 'language_id' => $this->global['language_id'], 'content' => '', ]], ]] + $this->global; $menus = new menuSQL(); $results = $menus->addMenuItem($menuData); if ($results) { $text .= "\n" . __('Menu item added!'); } } if ($html) { //reset base href so that html file can be loaded properly in browser directly from folder //$html = preg_replace('@]+>@', '', $html); $html = preg_replace('@]+>@', '', $html); //remove empty value for data attributes $html = preg_replace('/(data-[\w\-]+)="\s*"/','$1', $html); if (@file_put_contents($fileName, $html)) { $globalOptions = []; //keep css inline for email templates if (strpos($fileName, '/email/') !== false) { $globalOptions['inline-css'] = true; } $html = $this->saveGlobalElements($html, $globalOptions); $success = true; $text .= __('File saved!'); } else { if (! is_writable($fileName)) { $text .= sprintf(__('%s is not writable!'), $file); } else { $text .= sprintf(__('Error saving %s!'), $file); } } } else { $text .= __('Page html empty!'); } $cssFile = DS . 'css' . DS . 'custom.css'; //try to create custom.css if it doesn't exist if (! file_exists($themeFolder . $cssFile) && ! touch($themeFolder . $cssFile)) { $text .= '
' . sprintf(__('Can not create file %s!'), $theme . $cssFile); } else { if (! is_writable($themeFolder . $cssFile)) { $text .= '
' . sprintf(__('%s is not writable!'), $theme . $cssFile); } } if (CacheManager::clearCompiledFiles('app', $this->global['site_id'], $theme)) { $pageCache = new PageCache(); $uri = preg_replace('/\?.+/', '', $uri); $path = $pageCache->uri($uri); $pageCache->purge($path); } $data += ['success' => $success, 'message' => $text]; $this->response->setType('json'); $this->response->output($data); } }