/** * Uploads a new file for the given FileField instance. * * @param string $name EAV attribute name * @throws \Cake\Network\Exception\NotFoundException When invalid slug is given, * or when upload process could not be completed */ public function upload($name) { $instance = $this->_getInstance($name); require_once Plugin::classPath('Field') . 'Lib/class.upload.php'; $uploader = new \upload($this->request->data['Filedata']); if (!empty($instance->settings['extensions'])) { $exts = explode(',', $instance->settings['extensions']); $exts = array_map('trim', $exts); $exts = array_map('strtolower', $exts); if (!in_array(strtolower($uploader->file_src_name_ext), $exts)) { $this->_error(__d('field', 'Invalid file extension.'), 501); } } $response = ''; $uploader->file_overwrite = false; $folder = normalizePath(WWW_ROOT . "/files/{$instance->settings['upload_folder']}/"); $url = normalizePath("/files/{$instance->settings['upload_folder']}/", '/'); $uploader->process($folder); if ($uploader->processed) { $response = json_encode(['file_url' => Router::url($url . $uploader->file_dst_name, true), 'file_size' => FileToolbox::bytesToSize($uploader->file_src_size), 'file_name' => $uploader->file_dst_name, 'mime_icon' => FileToolbox::fileIcon($uploader->file_src_mime)]); } else { $this->_error(__d('field', 'File upload error, details: {0}', $uploader->error), 502); } $this->viewBuilder()->layout('ajax'); $this->title(__d('field', 'Upload File')); $this->set(compact('response')); }
/** * Validation rules when editing a comment in backend. * * @param \Cake\Validation\Validator $validator The validator object * @return \Cake\Validation\Validator */ public function validationAnonymous(Validator $validator) { $settings = Plugin::get('Comment')->settings; $validator = $this->validationDefault($validator); if ($settings['allow_anonymous']) { if ($settings['anonymous_name']) { $validator->requirePresence('author_name')->add('author_name', 'nameLength', ['rule' => ['minLength', 3], 'message' => __d('comment', 'Your name need to be at least 3 characters long.')]); if ($settings['anonymous_name_required']) { $validator->notEmpty('author_name', __d('comment', 'You must provide your name.')); } else { $validator->allowEmpty('author_name'); } } if ($settings['anonymous_email']) { $validator->requirePresence('author_email')->add('author_email', 'validEmail', ['rule' => 'email', 'message' => __d('comment', 'e-Mail must be valid.')]); if ($settings['anonymous_email_required']) { $validator->notEmpty('author_email', __d('comment', 'You must provide an email.')); } else { $validator->allowEmpty('anonymous_email'); } } if ($settings['anonymous_web']) { $validator->requirePresence('author_web')->add('author_web', 'validURL', ['rule' => 'url', 'message' => __d('comment', 'Website must be a valid URL.')]); if ($settings['anonymous_web_required']) { $validator->notEmpty('author_web', __d('comment', 'You must provide a website URL.')); } else { $validator->allowEmpty('author_web'); } } } return $validator; }
/** * Gets a list of counties flags suitable for select boxes. * * @return array */ public static function flagsList() { $flags = []; $Folder = new Folder(Plugin::path('Locale') . 'webroot/img/flags/'); foreach ($Folder->read()[1] as $icon) { $value = $icon; $label = str_replace_last('.gif', '', $icon); $flags[$value] = $label; } asort($flags); return $flags; }
/** * Look for plugin/themes awaiting for installation and sets a flash message * with instructions about how to proceed. * * @param string $type Possible values `plugin` (default) or `theme`, defaults * to "plugin" * @return void */ protected function _awaitingPlugins($type = 'plugin') { $type = !in_array($type, ['plugin', 'theme']) ? 'plugin' : $type; $ignoreThemes = $type === 'plugin'; $plugins = Plugin::scan($ignoreThemes); foreach ($plugins as $name => $path) { if (Plugin::exists($name) || $type == 'theme' && !str_ends_with($name, 'Theme')) { unset($plugins[$name]); } } if (!empty($plugins)) { $this->Flash->set(__d('system', '{0} are awaiting for installation', $type == 'plugin' ? __d('system', 'Some plugins') : __d('system', 'Some themes')), ['element' => 'System.stashed_plugins', 'params' => compact('plugins')]); } }
/** * First step of the installation process. * * User must select the language they want to use for the installation process. * * @return void */ public function language() { $languages = ['en_US' => ['url' => '/installer/startup/requirements?locale=en_US', 'welcome' => 'Welcome to QuickAppsCMS', 'action' => 'Click here to install in English']]; $Folder = new Folder(Plugin::classPath('Installer') . 'Locale'); foreach ($Folder->read(false, true, true)[0] as $path) { $code = basename($path); $file = $path . '/installer.po'; if (is_readable($file)) { I18n::locale($code); // trick for __d() $languages[$code] = ['url' => "/installer/startup/requirements?locale={$code}", 'welcome' => __d('installer', 'Welcome to QuickAppsCMS'), 'action' => __d('installer', 'Click here to install in English')]; } } I18n::locale('en_US'); $this->title('Welcome to QuickAppsCMS'); $this->set('languages', $languages); $this->_step(); }
/** * Loads and registers plugin's namespace and loads its event listeners classes. * * This is used to allow plugins being installed to respond to events before * they are integrated to the system. Events such as `beforeInstall`, * `afterInstall`, etc. * * @param string $plugin Name of the plugin for which attach listeners * @param string $path Path to plugin's root directory (which contains "src") * @throws \Cake\Error\FatalErrorException On illegal usage of this method */ protected function _attachListeners($plugin, $path) { $path = normalizePath("{$path}/"); $eventsPath = normalizePath("{$path}/src/Event/"); if (is_readable($eventsPath) && is_dir($eventsPath)) { $EventManager = EventManager::instance(); $eventsFolder = new Folder($eventsPath); Plugin::load($plugin, ['autoload' => true, 'bootstrap' => false, 'routes' => false, 'path' => $path, 'classBase' => 'src', 'ignoreMissing' => true]); foreach ($eventsFolder->read(false, false, true)[1] as $classPath) { $className = preg_replace('/\\.php$/i', '', basename($classPath)); $fullClassName = implode('\\', [$plugin, 'Event', $className]); if (class_exists($fullClassName)) { $handler = new $fullClassName(); $this->_listeners[] = $handler; $EventManager->on($handler); } } } }
/** * Renders the help document of the given plugin. * * @param string $pluginName The plugin name * @return void * @throws \Cake\Network\Exception\NotFoundException When no help document was found */ public function about($pluginName) { $about = false; if (Plugin::loaded($pluginName)) { $locale = I18n::locale(); $templatePath = App::path('Template', $pluginName)[0] . 'Element/Help/'; $lookFor = ["help_{$locale}", 'help']; foreach ($lookFor as $ctp) { if (is_readable($templatePath . "{$ctp}.ctp")) { $about = "{$pluginName}.Help/{$ctp}"; break; } } } if ($about) { $this->set('about', $about); } else { throw new NotFoundException(__d('system', 'No help was found.')); } $this->title(__d('system', 'About "{0}"', $pluginName)); $this->Breadcrumb->push('/admin/system/help')->push(__d('system', 'About {0}', $pluginName), '#'); }
/** * Validates the content of working directory. * * @return bool True on success */ protected function _validateContent() { if (!$this->_workingDir) { return false; } $errors = []; if (!is_readable("{$this->_workingDir}src") || !is_dir("{$this->_workingDir}src")) { $errors[] = __d('installer', 'Invalid package, missing "src" directory.'); } if (!is_readable("{$this->_workingDir}composer.json")) { $errors[] = __d('installer', 'Invalid package, missing "composer.json" file.'); } else { $jsonErrors = Plugin::validateJson("{$this->_workingDir}composer.json", true); if (!empty($jsonErrors)) { $errors[] = __d('installer', 'Invalid "composer.json".'); $errors = array_merge($errors, (array) $jsonErrors); } else { $json = (new File("{$this->_workingDir}composer.json"))->read(); $json = json_decode($json, true); list(, $pluginName) = packageSplit($json['name'], true); if ($this->params['theme'] && !str_ends_with($pluginName, 'Theme')) { $this->err(__d('installer', 'The given package is not a valid theme.')); return false; } elseif (!$this->params['theme'] && str_ends_with($pluginName, 'Theme')) { $this->err(__d('installer', 'The given package is not a valid plugin.')); return false; } $this->_plugin = ['name' => $pluginName, 'packageName' => $json['name'], 'type' => str_ends_with($pluginName, 'Theme') ? 'theme' : 'plugin', 'composer' => $json]; if (Plugin::exists($this->_plugin['name'])) { $exists = plugin($this->_plugin['name']); if ($exists->status) { $errors[] = __d('installer', '{0} "{1}" is already installed.', [$this->_plugin['type'] == 'plugin' ? __d('installer', 'The plugin') : __d('installer', 'The theme'), $this->_plugin['name']]); } else { $errors[] = __d('installer', '{0} "{1}" is already installed but disabled, maybe you want try to enable it?.', [$this->_plugin['type'] == 'plugin' ? __d('installer', 'The plugin') : __d('installer', 'The theme'), $this->_plugin['name']]); } } if ($this->_plugin['type'] == 'theme' && !is_readable("{$this->_workingDir}webroot/screenshot.png")) { $errors[] = __d('installer', 'Missing "screenshot.png" file.'); } if (isset($json['require'])) { $checker = new RuleChecker($json['require']); if (!$checker->check()) { $errors[] = __d('installer', '{0} "{1}" depends on other packages, plugins or libraries that were not found: {2}', [$this->_plugin['type'] == 'plugin' ? __d('installer', 'The plugin') : __d('installer', 'The theme'), $this->_plugin['name'], $checker->fail(true)]); } } } } if (!file_exists(ROOT . '/plugins') || !is_dir(ROOT . '/plugins') || !is_writable(ROOT . '/plugins')) { $errors[] = __d('installer', 'Write permissions required for directory: {0}.', [ROOT . '/plugins/']); } foreach ($errors as $message) { $this->err($message); } return empty($errors); }
/** * Tries to get a QuickAppsCMS plugin. * * @param string $package Full package name * @return bool|\CMS\Core\Package\PluginPackage */ protected static function _getPlugin($package) { list(, $plugin) = packageSplit($package, true); if (Plugin::exists($plugin)) { return new PluginPackage(quickapps("plugins.{$plugin}.name"), quickapps("plugins.{$plugin}.path")); } return false; }
/** * Calculates comment's status using akismet. * * @param array $data Comment's data to be validated by Akismet * @return string Filtered comment's status */ protected function _akismetStatus($data) { require_once Plugin::classPath('Comment') . 'Lib/Akismet.php'; try { $akismet = new \Akismet(Router::url('/'), $this->config('settings.akismet_key')); if (!empty($data['author_name'])) { $akismet->setCommentAuthor($data['author_name']); } if (!empty($data['author_email'])) { $akismet->setCommentAuthorEmail($data['author_email']); } if (!empty($data['author_web'])) { $akismet->setCommentAuthorURL($data['author_web']); } if (!empty($data['body'])) { $akismet->setCommentContent($data['body']); } if ($akismet->isCommentSpam()) { return 'spam'; } } catch (\Exception $ex) { return 'pending'; } return $data['status']; }
} $filter = $plugin->status; if ($plugin->isTheme) { $filter = $filter && in_array($plugin->name, [option('front_theme'), option('back_theme')]); } if (!$filter) { return; } if (!in_array("{$plugin->name}\\", array_keys($classLoader->getPrefixesPsr4()))) { $classLoader->addPsr4("{$plugin->name}\\", normalizePath("{$plugin->path}/src/"), true); } if (!in_array("{$plugin->name}\\Test\\", array_keys($classLoader->getPrefixesPsr4()))) { $classLoader->addPsr4("{$plugin->name}\\Test\\", normalizePath("{$plugin->path}/tests/"), true); } $info = ['autoload' => false, 'bootstrap' => true, 'routes' => true, 'path' => normalizePath("{$plugin->path}/"), 'classBase' => 'src', 'ignoreMissing' => true]; Plugin::load($plugin->name, $info); foreach ($plugin->eventListeners as $fullClassName) { if (class_exists($fullClassName)) { if (str_ends_with($fullClassName, 'Shortcode')) { EventDispatcher::instance('Shortcode')->eventManager()->on(new $fullClassName()); } else { EventDispatcher::instance()->eventManager()->on(new $fullClassName()); } } } $pluginsPath[] = $info['path']; }); if (empty($pluginsPath)) { die("Ops, something went wrong. Try to clear your site's snapshot and verify write permissions on /tmp directory."); } /**
/** * Switch site's theme. * * @return void */ public function main() { if (empty($this->params['theme'])) { $this->err(__d('installer', 'You must provide a theme.')); return false; } if (!Plugin::exists($this->params['theme'])) { $this->err(__d('installer', 'Theme "{0}" was not found.', $this->params['theme'])); return false; } $plugin = plugin($this->params['theme']); if (!$plugin->isTheme) { $this->err(__d('installer', '"{0}" is not a theme.', $plugin->humanName)); return false; } if (in_array($this->params['theme'], [option('front_theme'), option('back_theme')])) { $this->err(__d('installer', 'Theme "{0}" is already active.', $plugin->humanName)); return false; } // MENTAL NOTE: As theme is "inactive" its listeners are not attached to the // system, so we need to manually attach them in order to trigger callbacks. if (!$this->params['no-callbacks']) { $this->_attachListeners($plugin->name, "{$plugin->path}/"); try { $event = $this->trigger("Plugin.{$plugin->name}.beforeActivate"); if ($event->isStopped() || $event->result === false) { $this->err(__d('installer', 'Task was explicitly rejected by the theme.')); $this->_detachListeners(); return false; } } catch (\Exception $ex) { $this->err(__d('installer', 'Internal error, theme did not respond to "beforeActivate" callback properly.')); $this->_detachListeners(); return false; } } if (isset($plugin->composer['extra']['admin']) && $plugin->composer['extra']['admin']) { $prefix = 'back_'; $previousTheme = option('back_theme'); } else { $prefix = 'front_'; $previousTheme = option('front_theme'); } $this->loadModel('System.Options'); $this->loadModel('System.Plugins'); if ($this->Plugins->updateAll(['status' => 0], ['name' => $previousTheme]) && $this->Plugins->updateAll(['status' => 1], ['name' => $this->params['theme']])) { if ($this->Options->update("{$prefix}theme", $this->params['theme'])) { $this->_copyBlockPositions($this->params['theme'], $previousTheme); } else { $this->err(__d('installer', 'Internal error, the option "{0}" could not be persisted on database.', "{$prefix}theme")); $this->_detachListeners(); return false; } } else { $this->err(__d('installer', 'Internal error, unable to turnoff current theme ({0}) and active new one ({1}).', $previousTheme, $this->params['theme'])); return false; } if (!$this->params['no-callbacks']) { try { $this->trigger("Plugin.{$plugin->name}.afterActivate"); } catch (\Exception $e) { $this->err(__d('installer', 'Theme did not respond to "afterActivate" callback.')); } } return true; }
/** * Renders a nested menu. * * This methods renders a HTML menu using a `threaded` result set: * * ```php * // In controller: * $this->set('links', $this->Links->find('threaded')); * * // In view: * echo $this->Menu->render('links'); * ``` * * ### Options: * * You can pass an associative array `key => value`. Any `key` not in * `$_defaultConfig` will be treated as an additional attribute for the top * level UL (root). If `key` is in `$_defaultConfig` it will temporally * overwrite default configuration parameters, it will be restored to its * default values after rendering completes: * * - `formatter`: Callable method used when formating each item. * - `activeClass`: CSS class to use when an item is active (its URL matches current URL). * - `firstItemClass`: CSS class for the first item. * - `lastItemClass`: CSS class for the last item. * - `hasChildrenClass`: CSS class to use when an item has children. * - `split`: Split menu into multiple root menus (multiple UL's) * - `templates`: The templates you want to use for this menu. Any templates * will be merged on top of the already loaded templates. This option can * either be a filename in App/config that contains the templates you want * to load, or an array of templates to use. * * You can also pass a callable function as second argument which will be * used as formatter: * * ```php * echo $this->Menu->render($links, function ($link, $info) { * // render $item here * }); * ``` * * Formatters receives two arguments, the item being rendered as first argument * and information abut the item (has children, depth, etc) as second. * * You can pass the ID or slug of a menu as fist argument to render that menu's * links: * * ```php * echo $this->Menu->render('management'); * * // OR * * echo $this->Menu->render(1); * ``` * * @param int|string|array|\Cake\Collection\Collection $items Nested items * to render, given as a query result set or as an array list. Or an integer as * menu ID in DB to render, or a string as menu Slug in DB to render. * @param callable|array $config An array of HTML attributes and options as * described above or a callable function to use as `formatter` * @return string HTML * @throws \Cake\Error\FatalErrorException When loop invocation is detected, * that is, when "render()" method is invoked within a callable method when * rendering menus. */ public function render($items, $config = []) { if ($this->_rendering) { throw new FatalErrorException(__d('menu', 'Loop detected, MenuHelper already rendering.')); } $items = $this->_prepareItems($items); if (empty($items)) { return ''; } list($config, $attrs) = $this->_prepareOptions($config); $this->_rendering = true; $this->countItems($items); $this->config($config); if ($this->config('breadcrumbGuessing')) { $this->Link->config(['breadcrumbGuessing' => $this->config('breadcrumbGuessing')]); } $out = ''; if (intval($this->config('split')) > 1) { $out .= $this->_renderPart($items, $config, $attrs); } else { $out .= $this->formatTemplate('root', ['attrs' => $this->templater()->formatAttributes($attrs), 'content' => $this->_render($items)]); } if ($this->config('beautify')) { include_once Plugin::classPath('Menu') . 'Lib/htmLawed.php'; $tidy = is_bool($this->config('beautify')) ? '1t0n' : $this->config('beautify'); $out = htmLawed($out, compact('tidy')); } $this->_clear(); return $out; }
/** * Disables a plugin. * * @return void */ protected function _disable() { $enabledPlugins = plugin()->filter(function ($plugin) { return $plugin->status && !$plugin->isTheme; })->toArray(); if (!count($enabledPlugins)) { $this->err(__d('installer', '<info>There are no active plugins!</info>')); $this->out(); return; } $index = 1; $this->out(); foreach ($enabledPlugins as $plugin) { $enabledPlugins[$index] = $plugin; $this->out(__d('installer', '[{0, number, integer}] {1}', [$index, $plugin->humanName])); $index++; } $this->out(); $message = __d('installer', "Which plugin would you like to disable?\n[Q]uit"); while (true) { $in = $this->in($message); if (strtoupper($in) === 'Q') { $this->err(__d('installer', 'Operation aborted')); break; } elseif (intval($in) < 1 || !isset($enabledPlugins[intval($in)])) { $this->err(__d('installer', 'Invalid option')); } else { $plugin = plugin($enabledPlugins[$in]->name()); $this->hr(); $this->out(__d('installer', '<info>The following plugin will be DISABLED</info>')); $this->hr(); $this->out(__d('installer', 'Name: {0}', $plugin->name)); $this->out(__d('installer', 'Description: {0}', $plugin->composer['description'])); $this->out(__d('installer', 'Status: {0}', $plugin->status ? __d('installer', 'Active') : __d('installer', 'Disabled'))); $this->out(__d('installer', 'Path: {0}', $plugin->path)); $this->hr(); $this->out(); $confirm = $this->in(__d('installer', 'Please type in "{0}" to disable this plugin', $enabledPlugins[$in]->name)); if ($confirm === $enabledPlugins[$in]->name) { $task = $this->dispatchShell("Installer.plugins toggle -p {$enabledPlugins[$in]->name} -s disable"); if ($task === 0) { $this->out(__d('installer', 'Plugin disabled!')); Plugin::dropCache(); } else { $this->err(__d('installer', 'Plugin could not be disabled.'), 2); $this->out(); } } break; } } $this->out(); }
/** * Gets the given (or in use) theme as a package object. * * ### Example: * * ```php * // current theme * $bgColor = theme()->settings['background_color']; * * // specific theme * $bgColor = theme('BlueTheme')->settings['background_color']; * ``` * * @param string|null $name Name of the theme to get, or null to get the theme * being used in current request * @return \CMS\Core\Package\PluginPackage * @throws \Cake\Error\FatalErrorException When theme could not be found */ function theme($name = null) { if ($name === null) { $option = Router::getRequest()->isAdmin() ? 'back_theme' : 'front_theme'; $name = option($option); } $theme = Plugin::get()->filter(function ($plugin) use($name) { return $plugin->isTheme && $plugin->name == $name; })->first(); if ($theme) { return $theme; } throw new FatalErrorException(__d('cms', 'Theme "{0}" was not found', $name)); }
/** * {@inheritDoc} * * Workaround patch that allows plugins and themes provide their own independent * "settings.ctp" files so themes won't "override" plugin element (as themes are * actually plugins and may have their own "settings.ctp"). * * The same goes for "help.ctp" template files. So themes and plugins can * provide help information. */ protected function _getElementFileName($name, $pluginCheck = true) { list($plugin, $element) = $this->pluginSplit($name, $pluginCheck); if ($plugin && ($element === 'settings' || strpos($element, 'Help/help') !== false)) { return Plugin::classPath($plugin) . "Template/Element/{$element}{$this->_ext}"; } return parent::_getElementFileName($name, $pluginCheck); }
/** * Switch site's theme. * * @return void */ protected function _change() { $disabledThemes = plugin()->filter(function ($theme) { return $theme->isTheme && !in_array($theme->name, [option('front_theme'), option('back_theme')]); })->toArray(); if (!count($disabledThemes)) { $this->err(__d('installer', '<info>There are no disabled themes!</info>')); $this->out(); return; } $index = 1; $this->out(); foreach ($disabledThemes as $theme) { $disabledThemes[$index] = $theme; $this->out(__d('installer', '[{0, number, integer}] {1} [{2}]', [$index, $theme->humanName, $theme->isAdmin ? __d('installer', 'backend') : __d('installer', 'frontend')])); $index++; } $this->out(); $message = __d('installer', "Which theme would you like to activate?\n[Q]uit"); while (true) { $in = $this->in($message); if (strtoupper($in) === 'Q') { $this->err(__d('installer', 'Operation aborted')); break; } elseif (intval($in) < 1 || !isset($disabledThemes[intval($in)])) { $this->err(__d('installer', 'Invalid option')); } else { $task = $this->dispatchShell("Installer.themes change -t {$disabledThemes[$in]->name}"); if ($task === 0) { $this->out(__d('installer', 'Theme changed!')); Plugin::dropCache(); } else { $this->err(__d('installer', 'Theme could not be changed.'), 2); $this->out(); } break; } } $this->out(); }
/** * {@inheritDoc} * * It will look for plugin's version in the following places: * * - Plugin's "composer.json" file. * - Plugin's "VERSION.txt" file (or any file matching "/version?(\.\w+)/i"). * - Composer's "installed.json" file. * * If not found `dev-master` is returned by default. If plugin is not registered * on QuickAppsCMS (not installed) an empty string will be returned instead. * * @return string Plugin's version, for instance `1.2.x-dev` */ public function version() { if (parent::version() !== null) { return parent::version(); } if (!Plugin::exists($this->name())) { $this->_version = ''; return $this->_version; } // from composer.json if (!empty($this->composer['version'])) { $this->_version = $this->composer['version']; return $this->_version; } // from version.txt $files = glob($this->path . '/*', GLOB_NOSORT); foreach ($files as $file) { $fileName = basename(strtolower($file)); if (preg_match('/version?(\\.\\w+)/i', $fileName)) { $versionFile = file($file); $version = trim(array_pop($versionFile)); $this->_version = $version; return $this->_version; } } // from installed.json $installedJson = normalizePath(VENDOR_INCLUDE_PATH . "composer/installed.json"); if (is_readable($installedJson)) { $json = (array) json_decode(file_get_contents($installedJson), true); foreach ($json as $pkg) { if (isset($pkg['version']) && strtolower($pkg['name']) === strtolower($this->_packageName)) { $this->_version = $pkg['version']; return $this->_version; } } } $this->_version = 'dev-master'; return $this->_version; }
/** * Disables the given plugin. * * @param \CMS\Core\Package\PluginPackage $plugin The plugin to disable * @return bool True on success */ protected function _disable(PluginPackage $plugin) { $requiredBy = Plugin::checkReverseDependency($plugin->name); if (!empty($requiredBy)) { $names = []; foreach ($requiredBy as $p) { $names[] = $p->name(); } $this->err(__d('installer', 'Plugin "{0}" cannot be disabled as it is required by: {1}', $plugin->humanName, implode(', ', $names))); return false; } if (!$this->params['no-callbacks']) { $trigger = $this->_triggerBeforeEvents($plugin); if (!$trigger) { return false; } } return $this->_finish($plugin); }
/** * Runs uninstallation logic inside a safe transactional thread. This prevent * DB inconsistencies on uninstall failure. * * @return bool True on success, false otherwise */ protected function _runTransactional() { // to avoid any possible issue snapshot(); if (!is_writable(TMP)) { $this->err(__d('installer', 'Enable write permissions in /tmp directory before uninstall any plugin or theme.')); return false; } if (!$this->params['plugin']) { $this->err(__d('installer', 'No plugin/theme was given to remove.')); return false; } $this->loadModel('System.Plugins'); try { $plugin = plugin($this->params['plugin']); $pluginEntity = $this->Plugins->find()->where(['name' => $this->params['plugin']])->limit(1)->first(); } catch (\Exception $ex) { $plugin = $pluginEntity = false; } if (!$plugin || !$pluginEntity) { $this->err(__d('installer', 'Plugin "{0}" was not found.', $this->params['plugin'])); return false; } $this->_plugin = $plugin; $type = $plugin->isTheme ? 'theme' : 'plugin'; if ($plugin->isTheme && in_array($plugin->name, [option('front_theme'), option('back_theme')])) { $this->err(__d('installer', '{0} "{1}" is currently being used and cannot be removed.', $type == 'plugin' ? __d('installer', 'The plugin') : __d('installer', 'The theme'), $plugin->humanName)); return false; } $requiredBy = Plugin::checkReverseDependency($this->params['plugin']); if (!empty($requiredBy)) { $names = []; foreach ($requiredBy as $p) { $names[] = $p->name(); } $this->err(__d('installer', '{0} "{1}" cannot be removed as it is required by: {2}', $type == 'plugin' ? __d('installer', 'The plugin') : __d('installer', 'The theme'), $plugin->humanName, implode(', ', $names))); return false; } if (!$this->_canBeDeleted($plugin->path)) { return false; } if (!$this->params['no-callbacks']) { try { $event = $this->trigger("Plugin.{$plugin->name}.beforeUninstall"); if ($event->isStopped() || $event->result === false) { $this->err(__d('installer', 'Task was explicitly rejected by {0}.', $type == 'plugin' ? __d('installer', 'the plugin') : __d('installer', 'the theme'))); return false; } } catch (\Exception $e) { $this->err(__d('installer', 'Internal error, {0} did not respond to "beforeUninstall" callback correctly.', $type == 'plugin' ? __d('installer', 'the plugin') : __d('installer', 'the theme'))); return false; } } if (!$this->Plugins->delete($pluginEntity)) { $this->err(__d('installer', '{0} "{1}" could not be unregistered from DB.', $type == 'plugin' ? __d('installer', 'The plugin') : __d('installer', 'The theme'), $plugin->humanName)); return false; } $this->_removeOptions(); $this->_clearAcoPaths(); $folder = new Folder($plugin->path); $folder->delete(); snapshot(); if (!$this->params['no-callbacks']) { try { $this->trigger("Plugin.{$plugin->name}.afterUninstall"); } catch (\Exception $e) { $this->err(__d('installer', '{0} did not respond to "afterUninstall" callback.', $type == 'plugin' ? __d('installer', 'The plugin') : __d('installer', 'The theme'))); } } Plugin::unload($plugin->name); Plugin::dropCache(); return true; }
/** * Fallback for template location when extending Comment UI API. * * If controller tries to render an unexisting template under its Template * directory, then we try to find that view under `Comment/Template/CommentUI` * directory. * * ### Example: * * Suppose you are using this trait to manage comments attached to `Persons` * entities. You would probably have a `Person` plugin and a `clean` controller * as follow: * * // http://example.com/admin/person/comments_manager * Person\Controller\CommentsManagerController::index() * * The above controller action will try to render * `/plugins/Person/Template/CommentsManager/index.ctp`. But if does not exists * then `<QuickAppsCorePath>/plugins/Comment/Template/CommentUI/index.ctp` will * be used instead. * * Of course you may create your own template and skip this fallback functionality. * * @param \Cake\Event\Event $event the event instance. * @return void */ public function beforeRender(Event $event) { $plugin = (string) Inflector::camelize($event->subject()->request->params['plugin']); $controller = Inflector::camelize($event->subject()->request->params['controller']); $action = Inflector::underscore($event->subject()->request->params['action']); $prefix = ''; if (!empty($event->subject()->request->params['prefix'])) { $prefix = Inflector::camelize($event->subject()->request->params['prefix']) . '/'; } $templatePath = Plugin::classPath($plugin) . "Template/{$prefix}{$controller}/{$action}.ctp"; if (!is_readable($templatePath)) { $alternativeTemplatePath = Plugin::classPath('Comment') . 'Template/CommentUI'; if (is_readable("{$alternativeTemplatePath}/{$action}.ctp")) { $this->plugin = 'Comment'; $this->viewBuilder()->templatePath('CommentUI'); } } parent::beforeRender($event); }
/** * This method should never be used unless you know what are you doing. * * Populates the "acos" DB with information of every installed plugin, or * for the given plugin. It will automatically extracts plugin's controllers * and actions for creating a tree structure as follow: * * - PluginName * - Admin * - PrivateController * - index * - some_action * - ControllerName * - index * - another_action * * After tree is created you should be able to change permissions using * User's permissions section in backend. * * @param string $for Optional, build ACOs for the given plugin, or all plugins * if not given * @param bool $sync Whether to sync the tree or not. When syncing all invalid * ACO entries will be removed from the tree, also new ones will be added. When * syn is set to false only new ACO entries will be added, any invalid entry * will remain in the tree. Defaults to false * @return bool True on success, false otherwise */ public static function buildAcos($for = null, $sync = false) { if (function_exists('ini_set')) { ini_set('max_execution_time', 300); } elseif (function_exists('set_time_limit')) { set_time_limit(300); } if ($for === null) { $plugins = plugin()->toArray(); } else { try { $plugins = [plugin($for)]; } catch (\Exception $e) { return false; } } $added = []; foreach ($plugins as $plugin) { if (!Plugin::exists($plugin->name)) { continue; } $aco = new AcoManager($plugin->name); $controllerDir = normalizePath("{$plugin->path}/src/Controller/"); $folder = new Folder($controllerDir); $controllers = $folder->findRecursive('.*Controller\\.php'); foreach ($controllers as $controller) { $controller = str_replace([$controllerDir, '.php'], '', $controller); $className = $plugin->name . '\\' . 'Controller\\' . str_replace(DS, '\\', $controller); $methods = static::_controllerMethods($className); if (!empty($methods)) { $path = explode('Controller\\', $className)[1]; $path = str_replace_last('Controller', '', $path); $path = str_replace('\\', '/', $path); foreach ($methods as $method) { if ($aco->add("{$path}/{$method}")) { $added[] = "{$plugin->name}/{$path}/{$method}"; } } } } } if ($sync && isset($aco)) { $aco->Acos->recover(); $existingPaths = static::paths($for); foreach ($existingPaths as $exists) { if (!in_array($exists, $added)) { $aco->remove($exists); } } $validLeafs = $aco->Acos->find()->select(['id'])->where(['id NOT IN' => $aco->Acos->find()->select(['parent_id'])->where(['parent_id IS NOT' => null])]); $aco->Acos->Permissions->deleteAll(['aco_id NOT IN' => $validLeafs]); } return true; }