Beispiel #1
0
 /**
  * @link http://php-di.org/doc/container-configuration.html
  * @throws \Exception
  * @return Container
  */
 public function create()
 {
     $builder = new ContainerBuilder();
     $builder->useAnnotations(false);
     $builder->setDefinitionCache(new ArrayCache());
     // INI config
     $builder->addDefinitions(new IniConfigDefinitionSource(Config::getInstance()));
     // Global config
     $builder->addDefinitions(PIWIK_USER_PATH . '/config/global.php');
     // Plugin configs
     $this->addPluginConfigs($builder);
     // Development config
     if (Development::isEnabled()) {
         $builder->addDefinitions(PIWIK_USER_PATH . '/config/environment/dev.php');
     }
     // User config
     if (file_exists(PIWIK_USER_PATH . '/config/config.php')) {
         $builder->addDefinitions(PIWIK_USER_PATH . '/config/config.php');
     }
     // Environment config
     $this->addEnvironmentConfig($builder);
     if (!empty($this->definitions)) {
         $builder->addDefinitions($this->definitions);
     }
     return $builder->build();
 }
Beispiel #2
0
 public function demo()
 {
     if (!Development::isEnabled() || !Piwik::isUserHasSomeAdminAccess()) {
         return;
     }
     return $this->renderTemplate('demo');
 }
Beispiel #3
0
 private function shouldSkipCategory($category)
 {
     $category = strtolower($category);
     if ($category === 'database') {
         return true;
     }
     $developmentOnlySections = array('database_tests', 'tests', 'debugtests');
     return !Development::isEnabled() && in_array($category, $developmentOnlySections);
 }
Beispiel #4
0
 public function configureUserMenu(MenuUser $menu)
 {
     $menu->registerMenuIcon('UsersManager_MenuPersonal', 'icon-user-personal');
     $menu->registerMenuIcon('CoreAdminHome_MenuManage', 'icon-user-manage');
     $menu->registerMenuIcon('CorePluginsAdmin_MenuPlatform', 'icon-user-platform');
     if (Development::isEnabled() && Piwik::isUserHasSomeAdminAccess()) {
         $menu->addPlatformItem('UI Demo', $this->urlForAction('demo'), $order = 15);
     }
 }
 /**
  * Here you can define any action that should be performed during the update. For instance executing SQL statements,
  * renaming config entries, updating files, etc.
  */
 public static function update()
 {
     if (!Development::isEnabled()) {
         return;
     }
     $config = Config::getInstance();
     $dbTests = $config->database_tests;
     if ($dbTests['username'] === '@USERNAME@') {
         $dbTests['username'] = '******';
     }
     $config->database_tests = $dbTests;
     $config->forceSave();
 }
 private static function populateCache()
 {
     if (Development::isEnabled()) {
         return;
     }
     if (SettingsServer::isTrackerApiRequest()) {
         $eventToPersist = 'Tracker.end';
         $mode = 'tracker';
     } else {
         $eventToPersist = 'Request.dispatch.end';
         $mode = 'ui';
     }
     $cache = self::getStorage()->get('StaticCache-' . $mode);
     if (is_array($cache)) {
         self::$content = $cache;
     }
     Piwik::addAction($eventToPersist, array(__CLASS__, 'persistCache'));
 }
Beispiel #7
0
 public function configureAdminMenu(MenuAdmin $menu)
 {
     $menu->registerMenuIcon('CoreAdminHome_MenuDevelopment', 'icon-admin-development');
     $menu->registerMenuIcon('CoreAdminHome_MenuDiagnostic', 'icon-admin-diagnostic');
     $menu->registerMenuIcon('CorePluginsAdmin_MenuPlatform', 'icon-admin-platform');
     $menu->registerMenuIcon('General_Settings', 'icon-admin-settings');
     $menu->registerMenuIcon('CoreAdminHome_Administration', 'icon-settings');
     $menu->registerMenuIcon('UsersManager_MenuPersonal', 'icon-user-personal');
     $menu->registerMenuIcon('CoreAdminHome_MenuSystem', 'icon-server');
     $menu->registerMenuIcon('CorePluginsAdmin_MenuPlatform', 'icon-user-platform');
     $manageMeasurablesIcon = 'icon-open-source';
     $menu->registerMenuIcon('CoreAdminHome_MenuMeasurables', $manageMeasurablesIcon);
     $menu->registerMenuIcon('SitesManager_Sites', $manageMeasurablesIcon);
     $menu->registerMenuIcon('MobileAppMeasurable_MobileApps', $manageMeasurablesIcon);
     if (Development::isEnabled() && Piwik::isUserHasSomeAdminAccess()) {
         $menu->addDevelopmentItem('UI Demo', $this->urlForAction('demo'));
     }
 }
Beispiel #8
0
 public function execute()
 {
     $label = $this->translator->translate('Installation_SystemCheckFileIntegrity');
     if (Development::isEnabled()) {
         return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_OK));
     }
     $messages = Filechecks::getFileIntegrityInformation();
     $ok = array_shift($messages);
     if (empty($messages)) {
         return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_OK));
     }
     if ($ok) {
         $status = DiagnosticResult::STATUS_WARNING;
         return array(DiagnosticResult::singleResult($label, $status, $messages[0]));
     }
     $comment = $this->translator->translate('General_FileIntegrityWarningExplanation');
     // Keep only the 20 first lines else it becomes unmanageable
     if (count($messages) > 20) {
         $messages = array_slice($messages, 0, 20);
         $messages[] = '...';
     }
     $comment .= '<br/><br/><pre style="overflow-x: scroll;max-width: 600px;">' . implode("\n", $messages) . '</pre>';
     return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $comment));
 }
Beispiel #9
0
 private static function notifyWhenDebugOnDemandIsEnabled($trackerSetting)
 {
     if (!Development::isEnabled() && Piwik::hasUserSuperUserAccess() && TrackerConfig::getConfigValue($trackerSetting)) {
         $message = Piwik::translate('General_WarningDebugOnDemandEnabled');
         $message = sprintf($message, '"' . $trackerSetting . '"', '"[Tracker] ' . $trackerSetting . '"', '"0"', '"config/config.ini.php"');
         $notification = new Notification($message);
         $notification->title = Piwik::translate('General_Warning');
         $notification->priority = Notification::PRIORITY_LOW;
         $notification->context = Notification::CONTEXT_WARNING;
         $notification->type = Notification::TYPE_TRANSIENT;
         $notification->flags = Notification::FLAG_NO_CLEAR;
         NotificationManager::notify('Tracker' . $trackerSetting, $notification);
     }
 }
Beispiel #10
0
 /**
  * Renders the current view. Also sends the stored 'Content-Type' HTML header.
  * See {@link setContentType()}.
  *
  * @return string Generated template.
  */
 public function render()
 {
     try {
         $this->currentModule = Piwik::getModule();
         $this->currentAction = Piwik::getAction();
         $this->url = Common::sanitizeInputValue(Url::getCurrentUrl());
         $this->token_auth = Piwik::getCurrentUserTokenAuth();
         $this->userHasSomeAdminAccess = Piwik::isUserHasSomeAdminAccess();
         $this->userIsAnonymous = Piwik::isUserIsAnonymous();
         $this->userIsSuperUser = Piwik::hasUserSuperUserAccess();
         $this->latest_version_available = UpdateCheck::isNewestVersionAvailable();
         $this->disableLink = Common::getRequestVar('disableLink', 0, 'int');
         $this->isWidget = Common::getRequestVar('widget', 0, 'int');
         $piwikAds = StaticContainer::get('Piwik\\ProfessionalServices\\Advertising');
         $this->areAdsForProfessionalServicesEnabled = $piwikAds->areAdsForProfessionalServicesEnabled();
         if (Development::isEnabled()) {
             $cacheBuster = rand(0, 10000);
         } else {
             $cacheBuster = UIAssetCacheBuster::getInstance()->piwikVersionBasedCacheBuster();
         }
         $this->cacheBuster = $cacheBuster;
         $this->loginModule = Piwik::getLoginPluginName();
         $user = APIUsersManager::getInstance()->getUser($this->userLogin);
         $this->userAlias = $user['alias'];
     } catch (Exception $e) {
         Log::debug($e);
         // can fail, for example at installation (no plugin loaded yet)
     }
     ProxyHttp::overrideCacheControlHeaders('no-store');
     Common::sendHeader('Content-Type: ' . $this->contentType);
     // always sending this header, sometimes empty, to ensure that Dashboard embed loads
     // - when calling sendHeader() multiple times, the last one prevails
     Common::sendHeader('X-Frame-Options: ' . (string) $this->xFrameOptions);
     return $this->renderTwigTemplate();
 }
Beispiel #11
0
 public function configureUserMenu(MenuUser $menu)
 {
     if (Development::isEnabled() && Piwik::isUserHasSomeAdminAccess()) {
         $menu->addPlatformItem('UI Demo', $this->urlForAction('demo'), $order = 15);
     }
 }
 /**
  * Copies the given method and all needed use statements into an existing class. The target class name will be
  * built based on the given $replace argument.
  * @param string $sourceClassName
  * @param string $methodName
  * @param array $replace
  */
 protected function copyTemplateMethodToExisitingClass($sourceClassName, $methodName, $replace)
 {
     $targetClassName = $this->replaceContent($replace, $sourceClassName);
     if (Development::methodExists($targetClassName, $methodName)) {
         // we do not want to add the same method twice
         return;
     }
     Development::checkMethodExists($sourceClassName, $methodName, 'Cannot copy template method: ');
     $targetClass = new \ReflectionClass($targetClassName);
     $file = new \SplFileObject($targetClass->getFileName());
     $methodCode = Development::getMethodSourceCode($sourceClassName, $methodName);
     $methodCode = $this->replaceContent($replace, $methodCode);
     $methodLine = $targetClass->getEndLine() - 1;
     $sourceUses = Development::getUseStatements($sourceClassName);
     $targetUses = Development::getUseStatements($targetClassName);
     $usesToAdd = array_diff($sourceUses, $targetUses);
     if (empty($usesToAdd)) {
         $useCode = '';
     } else {
         $useCode = "\nuse " . implode("\nuse ", $usesToAdd) . "\n";
     }
     // search for namespace line before the class starts
     $useLine = 0;
     foreach (new \LimitIterator($file, 0, $targetClass->getStartLine()) as $index => $line) {
         if (0 === strpos(trim($line), 'namespace ')) {
             $useLine = $index + 1;
             break;
         }
     }
     $newClassCode = '';
     foreach (new \LimitIterator($file) as $index => $line) {
         if ($index == $methodLine) {
             $newClassCode .= $methodCode;
         }
         if (0 !== $useLine && $index == $useLine) {
             $newClassCode .= $useCode;
         }
         $newClassCode .= $line;
     }
     file_put_contents($targetClass->getFileName(), $newClassCode);
 }
Beispiel #13
0
 public function configureAdminMenu(MenuAdmin $menu)
 {
     if (Development::isEnabled() && Piwik::isUserHasSomeAdminAccess()) {
         $menu->addDevelopmentItem('LanguagesManager_TranslationSearch', array('module' => 'LanguagesManager', 'action' => 'searchTranslation'));
     }
 }
Beispiel #14
0
 private function enableDevelopmentLanguageInDevEnvironment(&$languages)
 {
     if (!Development::isEnabled()) {
         $key = array_search(DevelopmentLoader::LANGUAGE_ID, $languages);
         if ($key) {
             unset($languages[$key]);
         }
     }
 }
Beispiel #15
0
 /**
  * Initializes Profiling via XHProf.
  * See: https://github.com/piwik/piwik/blob/master/tests/README.xhprof.md
  */
 public static function setupProfilerXHProf($mainRun = false, $setupDuringTracking = false)
 {
     if (!$setupDuringTracking && SettingsServer::isTrackerApiRequest()) {
         // do not profile Tracker
         return;
     }
     if (self::$isXhprofSetup) {
         return;
     }
     $xhProfPath = PIWIK_INCLUDE_PATH . '/vendor/facebook/xhprof/extension/modules/xhprof.so';
     if (!file_exists($xhProfPath)) {
         throw new Exception("Cannot find xhprof, run 'composer install --dev' and build the extension.");
     }
     if (!function_exists('xhprof_enable')) {
         throw new Exception("Cannot find xhprof_enable, make sure to add 'extension={$xhProfPath}' to your php.ini.");
     }
     $outputDir = ini_get("xhprof.output_dir");
     if (empty($outputDir)) {
         throw new Exception("The profiler output dir is not set. Add 'xhprof.output_dir=...' to your php.ini.");
     }
     if (!is_writable($outputDir)) {
         throw new Exception("The profiler output dir '" . ini_get("xhprof.output_dir") . "' should exist and be writable.");
     }
     if (!function_exists('xhprof_error')) {
         function xhprof_error($out)
         {
             echo substr($out, 0, 300) . '...';
         }
     }
     $currentGitBranch = SettingsPiwik::getCurrentGitBranch();
     $profilerNamespace = "piwik";
     if ($currentGitBranch != 'master') {
         $profilerNamespace .= "-" . $currentGitBranch;
     }
     if ($mainRun) {
         self::setProfilingRunIds(array());
     }
     xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY);
     register_shutdown_function(function () use($profilerNamespace, $mainRun) {
         $xhprofData = xhprof_disable();
         $xhprofRuns = new XHProfRuns_Default();
         $runId = $xhprofRuns->save_run($xhprofData, $profilerNamespace);
         if (empty($runId)) {
             die('could not write profiler run');
         }
         $runs = Profiler::getProfilingRunIds();
         array_unshift($runs, $runId);
         if ($mainRun) {
             Profiler::aggregateXhprofRuns($runs, $profilerNamespace, $saveTo = $runId);
             $baseUrlStored = SettingsPiwik::getPiwikUrl();
             $out = "\n\n";
             $baseUrl = "http://" . @$_SERVER['HTTP_HOST'] . "/" . @$_SERVER['REQUEST_URI'];
             if (strlen($baseUrlStored) > strlen($baseUrl)) {
                 $baseUrl = $baseUrlStored;
             }
             $baseUrl = $baseUrlStored . "vendor/facebook/xhprof/xhprof_html/?source={$profilerNamespace}&run={$runId}";
             $out .= "Profiler report is available at:\n";
             $out .= "<a href='{$baseUrl}'>{$baseUrl}</a>";
             $out .= "\n\n";
             if (Development::isEnabled()) {
                 $out .= "WARNING: Development mode is enabled. Many runtime optimizations are not applied in development mode. ";
                 $out .= "Unless you intend to profile Piwik in development mode, your profile may not be accurate.";
                 $out .= "\n\n";
             }
             echo $out;
         } else {
             Profiler::setProfilingRunIds($runs);
         }
     });
     self::$isXhprofSetup = true;
 }
Beispiel #16
0
 private function checkIsValidWidget(WidgetConfig $widget)
 {
     if (!$widget->getModule()) {
         Development::error('No module is defined for added widget having name "' . $widget->getName());
     }
     if (!$widget->getAction()) {
         Development::error('No action is defined for added widget having name "' . $widget->getName());
     }
 }
Beispiel #17
0
 private function checkisValidCallable($module, $action)
 {
     if (!Development::isEnabled()) {
         return;
     }
     $prefix = 'Menu item added in ' . get_class($this) . ' will fail when being selected. ';
     if (!is_string($action)) {
         Development::error($prefix . 'No valid action is specified. Make sure the defined action that should be executed is a string.');
     }
     $reportAction = lcfirst(substr($action, 4));
     if (ReportsProvider::factory($module, $reportAction)) {
         return;
     }
     $controllerClass = '\\Piwik\\Plugins\\' . $module . '\\Controller';
     if (!Development::methodExists($controllerClass, $action)) {
         Development::error($prefix . 'The defined action "' . $action . '" does not exist in ' . $controllerClass . '". Make sure to define such a method.');
     }
     if (!Development::isCallableMethod($controllerClass, $action)) {
         Development::error($prefix . 'The defined action "' . $action . '" is not callable on "' . $controllerClass . '". Make sure the method is public.');
     }
 }
Beispiel #18
0
 public function configureAdminMenu(MenuAdmin $menu)
 {
     if (Development::isEnabled() && Piwik::isUserHasSomeAdminAccess()) {
         $menu->addDevelopmentItem('LanguagesManager_TranslationSearch', $this->urlForAction('searchTranslation'));
     }
 }
Beispiel #19
0
 /**
  * Load translations for loaded plugins
  *
  * @param bool|string $language Optional language code
  */
 public function loadPluginTranslations($language = false)
 {
     if (empty($language)) {
         $language = Translate::getLanguageToLoad();
     }
     $cache = new CacheFile('tracker', 43200);
     // ttl=12hours
     $cacheKey = 'PluginTranslations';
     if (!empty($language)) {
         $cacheKey .= '-' . trim($language);
     }
     if (!empty($this->loadedPlugins)) {
         // makes sure to create a translation in case loaded plugins change (ie Tests vs Tracker vs UI etc)
         $cacheKey .= '-' . md5(implode('', $this->getLoadedPluginsName()));
     }
     $translations = $cache->get($cacheKey);
     if (!empty($translations) && is_array($translations) && !Development::isEnabled()) {
         Translate::mergeTranslationArray($translations);
         return;
     }
     $translations = array();
     $pluginNames = self::getAllPluginsNames();
     foreach ($pluginNames as $pluginName) {
         if ($this->isPluginLoaded($pluginName) || $this->isPluginBundledWithCore($pluginName)) {
             $this->loadTranslation($pluginName, $language);
             if (isset($GLOBALS['Piwik_translations'][$pluginName])) {
                 $translations[$pluginName] = $GLOBALS['Piwik_translations'][$pluginName];
             }
         }
     }
     $cache->set($cacheKey, $translations);
 }
Beispiel #20
0
 public function sendReport($idReport, $period = false, $date = false, $force = false)
 {
     Piwik::checkUserIsNotAnonymous();
     $reports = $this->getReports($idSite = false, false, $idReport);
     $report = reset($reports);
     if ($report['period'] == 'never') {
         $report['period'] = 'day';
     }
     if (!empty($period)) {
         $report['period'] = $period;
     }
     if (empty($date)) {
         $date = Date::now()->subPeriod(1, $report['period'])->toString();
     }
     $language = \Piwik\Plugins\LanguagesManager\API::getInstance()->getLanguageForUser($report['login']);
     // generate report
     list($outputFilename, $prettyDate, $reportSubject, $reportTitle, $additionalFiles) = $this->generateReport($idReport, $date, $language, self::OUTPUT_SAVE_ON_DISK, $report['period']);
     if (!file_exists($outputFilename)) {
         throw new Exception("The report file wasn't found in {$outputFilename}");
     }
     $contents = file_get_contents($outputFilename);
     if (empty($contents)) {
         Log::warning("Scheduled report file '%s' exists but is empty!", $outputFilename);
     }
     /**
      * Triggered when sending scheduled reports.
      *
      * Plugins that provide new scheduled report transport mediums should use this event to
      * send the scheduled report.
      *
      * @param string $reportType A string ID describing how the report is sent, eg,
      *                           `'sms'` or `'email'`.
      * @param array $report An array describing the scheduled report that is being
      *                      generated.
      * @param string $contents The contents of the scheduled report that was generated
      *                         and now should be sent.
      * @param string $filename The path to the file where the scheduled report has
      *                         been saved.
      * @param string $prettyDate A prettified date string for the data within the
      *                           scheduled report.
      * @param string $reportSubject A string describing what's in the scheduled
      *                              report.
      * @param string $reportTitle The scheduled report's given title (given by a Piwik user).
      * @param array $additionalFiles The list of additional files that should be
      *                               sent with this report.
      * @param \Piwik\Period $period The period for which the report has been generated.
      * @param boolean $force A report can only be sent once per period. Setting this to true
      *                       will force to send the report even if it has already been sent.
      */
     Piwik::postEvent(self::SEND_REPORT_EVENT, array($report['type'], $report, $contents, $filename = basename($outputFilename), $prettyDate, $reportSubject, $reportTitle, $additionalFiles, \Piwik\Period\Factory::build($report['period'], $date), $force));
     // Update flag in DB
     $now = Date::now()->getDatetime();
     $this->getModel()->updateReport($report['idreport'], array('ts_last_sent' => $now));
     if (!Development::isEnabled()) {
         @chmod($outputFilename, 0600);
         Filesystem::deleteFileIfExists($outputFilename);
     }
 }
Beispiel #21
0
 public function isAvailable()
 {
     return PiwikDevelopment::isEnabled();
 }
 /**
  * See {@link AddSegmentByLabel}.
  *
  * @param DataTable $table
  */
 public function filter($table)
 {
     if (empty($this->segments)) {
         $msg = 'AddSegmentByLabel is called without having any segments defined';
         Development::error($msg);
         return;
     }
     if (count($this->segments) === 1) {
         $segment = reset($this->segments);
         foreach ($table->getRowsWithoutSummaryRow() as $key => $row) {
             $label = $row->getColumn('label');
             if (!empty($label)) {
                 $row->setMetadata('segment', $segment . '==' . urlencode($label));
             }
         }
     } elseif (!empty($this->delimiter)) {
         $numSegments = count($this->segments);
         $conditionAnd = ';';
         foreach ($table->getRowsWithoutSummaryRow() as $key => $row) {
             $label = $row->getColumn('label');
             if (!empty($label)) {
                 $parts = explode($this->delimiter, $label);
                 if (count($parts) === $numSegments) {
                     $filter = array();
                     foreach ($this->segments as $index => $segment) {
                         if (!empty($segment)) {
                             $filter[] = $segment . '==' . urlencode($parts[$index]);
                         }
                     }
                     $row->setMetadata('segment', implode($conditionAnd, $filter));
                 }
             }
         }
     } else {
         $names = implode(', ', $this->segments);
         $msg = 'Multiple segments are given but no delimiter defined. Segments: ' . $names;
         Development::error($msg);
     }
 }
Beispiel #23
0
 private function checkIsValidWidget($name, $method)
 {
     if (!Development::isEnabled()) {
         return;
     }
     if (empty($name)) {
         Development::error('No name is defined for added widget having method "' . $method . '" in ' . get_class($this));
     }
     if (Development::isCallableMethod($this, $method)) {
         return;
     }
     $controllerClass = 'Piwik\\Plugins\\' . $this->getModule() . '\\Controller';
     if (!Development::methodExists($this, $method) && !Development::methodExists($controllerClass, $method)) {
         Development::error('The added method "' . $method . '" neither exists in "' . get_class($this) . '" nor "' . $controllerClass . '". Make sure to define such a method.');
     }
     $definedInClass = get_class($this);
     if (Development::methodExists($controllerClass, $method)) {
         if (Development::isCallableMethod($controllerClass, $method)) {
             return;
         }
         $definedInClass = $controllerClass;
     }
     Development::error('The method "' . $method . '" is not callable on "' . $definedInClass . '". Make sure the method is public.');
 }
Beispiel #24
0
 private function checkIsValidTask($objectOrClassName, $methodName)
 {
     Development::checkMethodIsCallable($objectOrClassName, $methodName, 'The registered task is not valid as the method');
 }
Beispiel #25
0
 public function configureAdminMenu(MenuAdmin $menu)
 {
     if (Development::isEnabled() && Piwik::isUserHasSomeAdminAccess()) {
         $menu->addDevelopmentItem('UI demo', $this->urlForAction('demo'));
     }
 }
 /**
  * This method ensures that Piwik Platform cannot be running when using a NEWER database.
  */
 private function throwIfPiwikVersionIsOlderThanDBSchema()
 {
     // When developing this situation happens often when switching branches
     if (Development::isEnabled()) {
         return;
     }
     $updater = new Updater();
     $dbSchemaVersion = $updater->getCurrentComponentVersion('core');
     $current = Version::VERSION;
     if (-1 === version_compare($current, $dbSchemaVersion)) {
         $messages = array(Piwik::translate('General_ExceptionDatabaseVersionNewerThanCodebase', array($current, $dbSchemaVersion)), Piwik::translate('General_ExceptionDatabaseVersionNewerThanCodebaseWait'), Piwik::translate('General_ExceptionContactSupportGeneric', array('', '')));
         throw new DatabaseSchemaIsNewerThanCodebaseException(implode(" ", $messages));
     }
 }
Beispiel #27
0
 public function isEnabled()
 {
     return Development::isEnabled() && SettingsPiwik::isGitDeployment();
 }
Beispiel #28
0
 public function isEnabled()
 {
     return Development::isEnabled();
 }
Beispiel #29
0
 /**
  * See {@link add()}. Adds a new menu item to the development section of the admin menu.
  * @param string $menuName
  * @param array $url
  * @param int $order
  * @param bool|string $tooltip
  * @api
  * @since 2.5.0
  */
 public function addDevelopmentItem($menuName, $url, $order = 50, $tooltip = false)
 {
     if (Development::isEnabled()) {
         $this->addItem('CoreAdminHome_MenuDevelopment', $menuName, $url, $order, $tooltip);
     }
 }