diff --git a/pandora_console/extras/mr/62.sql b/pandora_console/extras/mr/62.sql index f92441a5f0..76f0a84c4e 100644 --- a/pandora_console/extras/mr/62.sql +++ b/pandora_console/extras/mr/62.sql @@ -87,4 +87,72 @@ CREATE INDEX idx_disabled ON talert_template_modules (disabled); INSERT INTO `treport_custom_sql` (`name`, `sql`) VALUES ('Agent safe mode not enable', 'select alias from tagente where safe_mode_module = 0'); +CREATE TABLE IF NOT EXISTS `twelcome_tip` ( + `id` INT NOT NULL AUTO_INCREMENT, + `id_lang` VARCHAR(20) NULL, + `id_profile` INT NOT NULL, + `title` VARCHAR(255) NOT NULL, + `text` TEXT NOT NULL, + `url` VARCHAR(255) NULL, + `enable` TINYINT NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4; + +CREATE TABLE IF NOT EXISTS `twelcome_tip_file` ( + `id` INT NOT NULL AUTO_INCREMENT, + `twelcome_tip_file` INT NOT NULL, + `filename` VARCHAR(255) NOT NULL, + `path` VARCHAR(255) NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `twelcome_tip_file` + FOREIGN KEY (`twelcome_tip_file`) + REFERENCES `twelcome_tip` (`id`) + ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4; + +INSERT INTO `twelcome_tip` VALUES +(1,'es',0,'¿Sabías que puedes monitorizar webs?','De manera sencilla a través de chequeos HTTP estándar o transaccional mediante transacciones centralizadas WUX, o descentralizadas con el plugin UX de agente.','https://pandorafms.com/manual/es/documentation/03_monitoring/06_web_monitoring','1'), +(2,'es',0,'Monitorización remota de dispositivos SNMP','Los dispositivos de red como switches, AP, routers y firewalls se pueden monitorizar remotamente usando el protocolo SNMP. Basta con saber su IP, la comunidad SNMP y lanzar un wizard SNMP desde la consola.','https://pandorafms.com/manual/es/documentation/03_monitoring/03_remote_monitoring#monitorizacion_snmp','1'), +(3,'es',0,'Monitorizar rutas desde una IP a otra','Existe un plugin especial que sirve para monitorizar visualmente las rutas desde una IP a otra de manera visual y dinámica, según va cambiando con el tiempo.','https://pandorafms.com/manual/es/documentation/03_monitoring/03_remote_monitoring#monitorizacion_de_rutas','1'), +(4,'es',0,'¿Tu red pierde paquetes?','Se puede medir la pérdida de paquetes en tu red usando un agente y un plugin libre llamado “Packet Loss”. Esto es especialmente útil en redes Wifi o redes compartidas con muchos usuarios. Escribimos un artículo en nuestro blog hablando de ello, echale un vistazo','https://pandorafms.com/blog/es/perdida-de-paquetes/','1'), +(5,'es',0,'Usar Telegram con Pandora FMS','Perfecto para recibir alertas con gráficas empotradas y personalizar así la recepción de avisos de manera individual o en un canal común con mas personas. ','https://pandorafms.com/library/telegram-bot-cli/','1'), +(6,'es',0,'Monitorizar JMX (Tomcat, Websphere, Weblogic, Jboss, Apache Kafka, Jetty, GlassFish…)','Existe un plugin Enterprise que sirve para monitorizar cualquier tecnología JMX. Se puede usar de manera local (como plugin local) o de manera remota con el plugin server.','https://pandorafms.com/library/jmx-monitoring/','1'), +(7,'es',0,'¿Sabes que cada usuario puede tener su propia Zona Horaria?','Se puede establecer zonas horarias diferentes para cada usuario, de manera que interprete los datos teniendo en cuenta la diferencia horaria. Pandora FMS también puede tener servidores y agentes en diferentes zonas horarias. ¡Por todo el mundo!','','1'), +(8,'es',0,'Paradas planificadas','Se puede definir, a nivel de agente y a nivel de módulo, períodos en los cuales se ignoren las alertas y/o los datos recogidos. Es perfecto para planificar paradas de servicio o desconexión de los sistemas monitorizados. También afecta a los informes SLA, evitando que se tengan en cuenta esos intervalos de tiempo. ','https://pandorafms.com/manual/es/documentation/04_using/11_managing_and_administration#paradas_de_servicio_planificadas','1'), +(9,'es',0,'Personalizar los emails de alerta ','¿Sabías que se pueden personalizar los mails de alertas de Pandora? Solo tienes que editar el código HTML por defecto de las acciones de alerta de tipo email. ','https://pandorafms.com/manual/en/documentation/04_using/01_alerts#editing_an_action','1'), +(10,'es',0,'Usando iconos personalizados en consolas visuales ','Gracias a los iconos personalizados se pueden crear vistas muy personalizadas, como la de la imagen, que representa racks con los tipos de servidores en el orden que están colocados dentro del rack. Perfecto para que un técnico sepa exactamente qué máquina esta fallando. Más visual no puede ser, de ahi el nombre. ','https://pandorafms.com/manual/start?id=es/documentation/04_using/05_data_presentation_visual_maps','1'), +(11,'es',0,'Consolas visuales: mapas de calor ','La consola permite integrar en un fondo personalizado una serie de datos, que en función de su valor se representen con unos colores u otros, en tiempo real. Las aplicaciones son infinitas, solo depende de tu imaginación. ','https://pandorafms.com/manual/es/documentation/04_using/05_data_presentation_visual_maps#mapa_de_calor_o_nube_de_color','1'), +(12,'es',0,'Auditoría interna de la consola ','La consola registra todas las actividades relevantes de cada usuario conectado a la consola. Esto incluye la aplicación de configuraciones, validaciones de eventos y alertas, conexión y desconexión y cientos de otras operaciones. La seguridad en Pandora FMS ha sido siempre una de las características del diseño de su arquitectura. ','https://pandorafms.com/manual/es/documentation/04_using/11_managing_and_administration#log_de_auditoria','1'), +(13,'es',0,'Sistema de provisión automática de agentes ','El sistema de autoprovisión de agentes, permite que un agente recién ingresado en el sistema aplique automáticamente cambios en su configuración (como moverlo de grupo, asignarle ciertos valores en campos personalizados) y por supuesto aplicarle determinadas politicas de monitorización. Es una de las funcionalidades más potentes, orientadas a gestionar parques de sistemas muy extensos. ','https://pandorafms.com/manual/start?id=es/documentation/02_installation/05_configuration_agents#configuracion_automatica_de_agentes','1'), +(14,'es',0,'Modo oscuro ','¿Sabes que existe un modo oscuro en Pandora FMS? Un administrador lo puede activar a nivel global desde las opciones de configuración visuales o cualquier usuario a nivel individual, en las opciones de usuario. ','','1'), +(15,'es',0,'Google Sheet ','¿Sabes que se puede coger el valor de una celda de una hoja de cálculo de Google Sheet?, utilizamos la API para pedir el dato a través de un plugin remoto. Es perfecto para construir cuadros de mando de negocio, obtener alertas en tiempo real y crear tus propios informes a medida. ','https://pandorafms.com/library/google-sheets-plugin/','1'), +(16,'es',0,'Tablas de ARP','¿Sabes que existe un módulo de inventario para sacar las tablas ARP de tus servidores windows? Es fácil de instalar y puede darte información muy detallada de tus equipos.','https://pandorafms.com/library/arp-table-windows-local/','1'), +(17,'es',0,'Enlaces de red en la consola visual ','Existe un elemento de consola visual llamado “Network link” que permite mostrar visualmente la unión de dos interfaces de red, su estado y el tráfico de subida/bajada, de una manera muy visual. ','https://pandorafms.com/manual/es/documentation/04_using/05_data_presentation_visual_maps#enlace_de_red','1'), +(18,'es',0,'¿Conoces los informes de disponibilidad? ','Son muy útiles ya que te dicen el tiempo (%) que un chequeo ha estado en diferentes estados a lo largo de un lapso de tiempo, por ejemplo, una semana. Ofrece datos crudos completos de lo que se ha hecho con el detalle suficiente para convencer a un proveedor o un cliente. ','','1'), +(19,'es',0,'Gráficas de disponibilidad ','Parecidos a los informes de disponibilidad, pero mucho mas visuales, ofrecen el detalle de estado de un monitor a lo largo del tiempo. Se pueden agrupar con otro módulo para ofrecer datos finales teniendo en cuenta la alta disponibilidad de un servicio. Son perfectos para su uso en informes a proveedores y/o clientes. ','https://pandorafms.com/manual/es/documentation/04_using/08_data_presentation_reports#grafico_de_disponibilidad','1'), +(20,'es',0,'Zoom en gráficas de datos ','¿Sabes que Pandora FMS permite hacer zoom en una parte de la gráfica. Con eso ampliarás la información de la gráfica. Si estás viendo una gráfica de un mes y amplías, podrás ver los datos de ese intervalo. Si utilizas una gráfica con datos de resolución completa (los llamamos gráficas TIP) podrás ver el detalle de cada dato, aunque tu gráfica tenga miles de muestras. ','','1'), +(21,'es',0,'Gráficas de resolución completa ','Pandora FMS y otras herramientas cuando tienen que mostrar una gráfica obtienen los datos de la fuente de datos y luego “simplifican” la gráfica, ya que si la serie de datos tiene 10,000 elementos y la gráfica solo tiene 300 pixeles de ancho no pueden caber todos, asi que se “simplifican” esos 10,000 puntos en solo 300. Sin embargo al simplificar se pierde “detalle” en la gráfica, y por supuesto no podemos “hacer zoom”. Las gráficas de Pandora FMS permiten mostrar y usar todos los datos en una gráfica, que llamamos “TIP” que muestra todos los puntos superpuestos y además permite que al hacer zoom no se pierda resolución. ','','1'), +(22,'es',0,'Política de contraseñas','La consola de Pandora FMS tiene un sistema de gestión de política de credenciales, para reforzar la seguridad local (además de permitir la autenticación externa contra un LDAP, Active Directory o SAML). A través de este sistema podemos forzar cambios de password cada X días, guardar un histórico de passwords usadas o evitar el uso de ciertas contraseñas entre otras acciones. ','https://pandorafms.com/manual/es/documentation/04_using/12_console_setup?s%5B%5D%3Dcontrase%25C3%25B1as#password_policy','1'), +(23,'es',0,'Autenticación de doble factor ','Es posible activar (y forzar su uso a todos los usuarios) un sistema de doble autenticación (usando Google Auth) para que cualquier usuario se autentique además de con una contraseña, con un sistema de token de un solo uso, dando al sistema mucha más seguridad. ','https://pandorafms.com/manual/en/documentation/04_using/12_console_setup?s%5B%5D%3Dgoogle%26s%5B%5D%3Dauth#authentication','1'); + +INSERT INTO `twelcome_tip_file` (`twelcome_tip_file`, `filename`, `path`) VALUES +(1, 'monitorizar_web.png', 'images/tips/'), +(2, 'monitorizar_snmp.png', 'images/tips/'), +(3, 'monitorizar_desde_ip.png', 'images/tips/'), +(4, 'tu_red_pierde_paquetes.png', 'images/tips/'), +(5, 'telegram_con_pandora.png', 'images/tips/'), +(6, 'monitorizar_con_jmx.png', 'images/tips/'), +(7, 'usuario_zona_horaria.png', 'images/tips/'), +(8, 'paradas_planificadas.png', 'images/tips/'), +(9, 'personalizar_los_emails.png', 'images/tips/'), +(10, 'iconos_personalizados.png', 'images/tips/'), +(11, 'mapa_de_calor.png', 'images/tips/'), +(12, 'auditoria.png', 'images/tips/'), +(15, 'google_sheets.png', 'images/tips/'), +(17, 'enlaces_consola_visual.png', 'images/tips/'), +(18, 'informe_disponibiliad.png', 'images/tips/'), +(19, 'graficas_disponibilidad.png', 'images/tips/'), +(20, 'zoom_en_graficas.png', 'images/tips/'), +(22, 'politica_de_pass.png', 'images/tips/'); + COMMIT; diff --git a/pandora_console/general/register.php b/pandora_console/general/register.php index 1c8249ddf5..8714d1ec12 100644 --- a/pandora_console/general/register.php +++ b/pandora_console/general/register.php @@ -31,6 +31,7 @@ global $config; require_once $config['homedir'].'/include/functions_register.php'; require_once $config['homedir'].'/include/class/WelcomeWindow.class.php'; +require_once $config['homedir'].'/include/class/TipsWindow.class.php'; if ((bool) is_ajax() === true) { @@ -109,6 +110,16 @@ try { $welcome = false; } +try { + if (isset($_SESSION['showed_tips_window']) === false) { + $tips_window = new TipsWindow(); + if ($tips_window !== null) { + $tips_window->run(); + } + } +} catch (Exception $e) { +} + $double_auth_enabled = (bool) db_get_value('id', 'tuser_double_auth', 'id_user', $config['id_user']); if (isset($config['2FA_all_users']) === false) { diff --git a/pandora_console/godmode/menu.php b/pandora_console/godmode/menu.php index af15b28f7a..2efafad20b 100644 --- a/pandora_console/godmode/menu.php +++ b/pandora_console/godmode/menu.php @@ -383,6 +383,9 @@ if ($access_console_node === true) { $sub2['godmode/setup/setup§ion=external_tools']['text'] = __('External Tools'); $sub2['godmode/setup/setup§ion=external_tools']['refr'] = 0; + $sub2['godmode/setup/setup§ion=welcome_tips']['text'] = __('Welcome Tips'); + $sub2['godmode/setup/setup§ion=welcome_tips']['refr'] = 0; + if ((bool) $config['activate_gis'] === true) { $sub2['godmode/setup/setup§ion=gis']['text'] = __('Map conections GIS'); } diff --git a/pandora_console/godmode/setup/setup.php b/pandora_console/godmode/setup/setup.php index 8c8587b1ef..e693d2f2e2 100644 --- a/pandora_console/godmode/setup/setup.php +++ b/pandora_console/godmode/setup/setup.php @@ -224,6 +224,11 @@ $buttons['external_tools'] = [ 'text' => ''.html_print_image('images/nettool.png', true, ['title' => __('External Tools'), 'class' => 'invert_filter']).'', ]; +$buttons['welcome_tips'] = [ + 'active' => false, + 'text' => ''.html_print_image('images/inventory.png', true, ['title' => __('Welcome tips'), 'class' => 'invert_filter']).'', +]; + if ($config['activate_gis']) { $buttons['gis'] = [ 'active' => false, @@ -312,6 +317,20 @@ switch ($section) { $help_header = ''; break; + case 'welcome_tips': + $view = get_parameter('view', ''); + $title = __('Welcome tips'); + if ($view === 'create') { + $title = __('Create tip'); + } else if ($view === 'edit') { + $title = __('Edit tip'); + } + + $buttons['welcome_tips']['active'] = true; + $subpage = ' » '.$title; + $help_header = ''; + break; + case 'enterprise': $buttons['enterprise']['active'] = true; $subpage = ' » '.__('Enterprise'); @@ -409,6 +428,10 @@ switch ($section) { include_once $config['homedir'].'/godmode/setup/setup_external_tools.php'; break; + case 'welcome_tips': + include_once $config['homedir'].'/godmode/setup/welcome_tips.php'; + break; + default: enterprise_hook('setup_enterprise_select_tab', [$section]); break; diff --git a/pandora_console/godmode/setup/welcome_tips.php b/pandora_console/godmode/setup/welcome_tips.php new file mode 100644 index 0000000000..1a9903a5ec --- /dev/null +++ b/pandora_console/godmode/setup/welcome_tips.php @@ -0,0 +1,161 @@ +getMessage(); + return; +} + +if ($view === 'create' || $view === 'edit') { + // IF exists actions + if ($action === 'create' || $action === 'edit') { + $files = $_FILES; + $id_lang = get_parameter('id_lang', ''); + $id_profile = get_parameter('id_profile', ''); + $title = get_parameter('title', ''); + $text = get_parameter('text', ''); + $url = get_parameter('url', ''); + $enable = get_parameter_switch('enable', ''); + $errors = []; + + if (count($files) > 0) { + $e = $tipsWindow->validateImages($files); + if ($e !== false) { + $errors = $e; + } + } + + if (empty($id_lang) === true) { + $errors[] = __('Language is empty'); + } + + if (empty($title) === true) { + $errors[] = __('Title is empty'); + } + + if (empty($text) === true) { + $errors[] = __('Text is empty'); + } + + switch ($action) { + case 'create': + if (count($errors) === 0) { + if (count($files) > 0) { + $uploadImages = $tipsWindow->uploadImages($files); + } + + $response = $tipsWindow->createTip($id_lang, $id_profile, $title, $text, $url, $enable, $uploadImages); + + if ($response === 0) { + $errors[] = __('Error in insert tip'); + } + } + + $tipsWindow->viewCreate($errors); + return; + + case 'edit': + $idTip = get_parameter('idTip', ''); + $imagesToDelete = get_parameter('images_to_delete', ''); + if (empty($idTip) === false) { + if (count($errors) === 0) { + if (empty($imagesToDelete) === false) { + $imagesToDelete = json_decode(io_safe_output($imagesToDelete), true); + $tipsWindow->deleteImagesFromTip($idTip, $imagesToDelete); + } + + if (count($files) > 0) { + $uploadImages = $tipsWindow->uploadImages($files); + } + + $response = $tipsWindow->updateTip($idTip, $id_profile, $id_lang, $title, $text, $url, $enable, $uploadImages); + + if ($response === 0) { + $errors[] = __('Error in update tip'); + } + } + + $tipsWindow->viewEdit($idTip, $errors); + } + return; + + default: + $tipsWindow->draw(); + return; + } + + + return; + } + + // If not exists actions + switch ($view) { + case 'create': + $tipsWindow->viewCreate(); + return; + + case 'edit': + $idTip = get_parameter('idTip', ''); + if (empty($idTip) === false) { + $tipsWindow->viewEdit($idTip); + } + return; + + default: + $tipsWindow->draw(); + return; + } +} + +if ($action === 'delete') { + $idTip = get_parameter('idTip', ''); + $errors = []; + if (empty($idTip) === true) { + $errors[] = __('Tip required'); + } + + if (count($errors) === 0) { + $response = $tipsWindow->deleteTip($idTip); + + if ($response === 0) { + $errors[] = __('Error in delete tip'); + } + } + + $tipsWindow->draw($errors); + return; +} + +$tipsWindow->draw(); diff --git a/pandora_console/godmode/users/configure_user.php b/pandora_console/godmode/users/configure_user.php index 468d789e0f..b396e41e4f 100644 --- a/pandora_console/godmode/users/configure_user.php +++ b/pandora_console/godmode/users/configure_user.php @@ -653,6 +653,7 @@ if ($update_user) { $values['timezone'] = (string) get_parameter('timezone'); $values['default_event_filter'] = (int) get_parameter('default_event_filter'); $values['default_custom_view'] = (int) get_parameter('default_custom_view'); + $values['show_tips_startup'] = (int) get_parameter_switch('show_tips_startup'); // API Token information. $apiTokenRenewed = (bool) get_parameter('renewAPIToken'); $values['api_token'] = ($apiTokenRenewed === true) ? api_token_generate() : users_get_API_token($values['id_user']); diff --git a/pandora_console/godmode/users/user_management.php b/pandora_console/godmode/users/user_management.php index 6969125a93..cff87aef6f 100644 --- a/pandora_console/godmode/users/user_management.php +++ b/pandora_console/godmode/users/user_management.php @@ -372,6 +372,8 @@ $userManagementTable->data['captions_loginErrorUser'][1] .= ui_print_input_place __('The user with local authentication enabled will always use local authentication.'), true ); +$userManagementTable->data['show_tips_startup'][0] = html_print_checkbox_switch('show_tips_startup', 1, ($user_info['show_tips_startup'] === null) ? true : $user_info['show_tips_startup'], true); +$userManagementTable->data['show_tips_startup'][1] = ''.__('Show usage tips at startup').''; // Session time input. $userManagementTable->rowclass['captions_userSessionTime'] = 'field_half_width'; diff --git a/pandora_console/images/arrow-left-grey.png b/pandora_console/images/arrow-left-grey.png new file mode 100644 index 0000000000..d88b661e27 Binary files /dev/null and b/pandora_console/images/arrow-left-grey.png differ diff --git a/pandora_console/images/arrow-right-grey.png b/pandora_console/images/arrow-right-grey.png new file mode 100644 index 0000000000..7b6290cda8 Binary files /dev/null and b/pandora_console/images/arrow-right-grey.png differ diff --git a/pandora_console/images/tips/auditoria.png b/pandora_console/images/tips/auditoria.png new file mode 100644 index 0000000000..5f13dc3a5f Binary files /dev/null and b/pandora_console/images/tips/auditoria.png differ diff --git a/pandora_console/images/tips/doble_autenticacion.png b/pandora_console/images/tips/doble_autenticacion.png new file mode 100644 index 0000000000..48a775ece4 Binary files /dev/null and b/pandora_console/images/tips/doble_autenticacion.png differ diff --git a/pandora_console/images/tips/enlaces_consola_visual.png b/pandora_console/images/tips/enlaces_consola_visual.png new file mode 100644 index 0000000000..760ba9a94e Binary files /dev/null and b/pandora_console/images/tips/enlaces_consola_visual.png differ diff --git a/pandora_console/images/tips/google_sheets.png b/pandora_console/images/tips/google_sheets.png new file mode 100644 index 0000000000..e8073e5f0e Binary files /dev/null and b/pandora_console/images/tips/google_sheets.png differ diff --git a/pandora_console/images/tips/graficas_disponibilidad.png b/pandora_console/images/tips/graficas_disponibilidad.png new file mode 100644 index 0000000000..ddb6303ee5 Binary files /dev/null and b/pandora_console/images/tips/graficas_disponibilidad.png differ diff --git a/pandora_console/images/tips/graficas_resolucion_completa.png b/pandora_console/images/tips/graficas_resolucion_completa.png new file mode 100644 index 0000000000..29109fcdb5 Binary files /dev/null and b/pandora_console/images/tips/graficas_resolucion_completa.png differ diff --git a/pandora_console/images/tips/iconos_personalizados.png b/pandora_console/images/tips/iconos_personalizados.png new file mode 100644 index 0000000000..39fc951f0a Binary files /dev/null and b/pandora_console/images/tips/iconos_personalizados.png differ diff --git a/pandora_console/images/tips/informe_disponibiliad.png b/pandora_console/images/tips/informe_disponibiliad.png new file mode 100644 index 0000000000..45fbce4a14 Binary files /dev/null and b/pandora_console/images/tips/informe_disponibiliad.png differ diff --git a/pandora_console/images/tips/mapa_de_calor.png b/pandora_console/images/tips/mapa_de_calor.png new file mode 100644 index 0000000000..05ec5a21e0 Binary files /dev/null and b/pandora_console/images/tips/mapa_de_calor.png differ diff --git a/pandora_console/images/tips/monitorizar_con_jmx.png b/pandora_console/images/tips/monitorizar_con_jmx.png new file mode 100644 index 0000000000..113d64117e Binary files /dev/null and b/pandora_console/images/tips/monitorizar_con_jmx.png differ diff --git a/pandora_console/images/tips/monitorizar_desde_ip.png b/pandora_console/images/tips/monitorizar_desde_ip.png new file mode 100644 index 0000000000..8eea933d56 Binary files /dev/null and b/pandora_console/images/tips/monitorizar_desde_ip.png differ diff --git a/pandora_console/images/tips/monitorizar_snmp.png b/pandora_console/images/tips/monitorizar_snmp.png new file mode 100644 index 0000000000..0a7c2f6e95 Binary files /dev/null and b/pandora_console/images/tips/monitorizar_snmp.png differ diff --git a/pandora_console/images/tips/monitorizar_web.png b/pandora_console/images/tips/monitorizar_web.png new file mode 100644 index 0000000000..594078436a Binary files /dev/null and b/pandora_console/images/tips/monitorizar_web.png differ diff --git a/pandora_console/images/tips/paradas_planificadas.png b/pandora_console/images/tips/paradas_planificadas.png new file mode 100644 index 0000000000..f42f1e5533 Binary files /dev/null and b/pandora_console/images/tips/paradas_planificadas.png differ diff --git a/pandora_console/images/tips/personalizar_los_emails.png b/pandora_console/images/tips/personalizar_los_emails.png new file mode 100644 index 0000000000..bfed72bdd5 Binary files /dev/null and b/pandora_console/images/tips/personalizar_los_emails.png differ diff --git a/pandora_console/images/tips/politica_de_pass.png b/pandora_console/images/tips/politica_de_pass.png new file mode 100644 index 0000000000..315388b3c2 Binary files /dev/null and b/pandora_console/images/tips/politica_de_pass.png differ diff --git a/pandora_console/images/tips/telegram_con_pandora.png b/pandora_console/images/tips/telegram_con_pandora.png new file mode 100644 index 0000000000..80702d7ec8 Binary files /dev/null and b/pandora_console/images/tips/telegram_con_pandora.png differ diff --git a/pandora_console/images/tips/tu_red_pierde_paquetes.png b/pandora_console/images/tips/tu_red_pierde_paquetes.png new file mode 100644 index 0000000000..d26c425dc9 Binary files /dev/null and b/pandora_console/images/tips/tu_red_pierde_paquetes.png differ diff --git a/pandora_console/images/tips/usuario_zona_horaria.png b/pandora_console/images/tips/usuario_zona_horaria.png new file mode 100644 index 0000000000..d53558dcb1 Binary files /dev/null and b/pandora_console/images/tips/usuario_zona_horaria.png differ diff --git a/pandora_console/images/tips/zoom_en_graficas.png b/pandora_console/images/tips/zoom_en_graficas.png new file mode 100644 index 0000000000..6f0b71f59a Binary files /dev/null and b/pandora_console/images/tips/zoom_en_graficas.png differ diff --git a/pandora_console/include/ajax/tips_window.ajax.php b/pandora_console/include/ajax/tips_window.ajax.php new file mode 100644 index 0000000000..ce8de4c078 --- /dev/null +++ b/pandora_console/include/ajax/tips_window.ajax.php @@ -0,0 +1,60 @@ +ajaxMethod($method) === true) { + $actions->{$method}(); + } else { + $actions->error('Unavailable method.'); + } +} else { + $actions->error('Method not found. ['.$method.']'); +} + + +// Stop any execution. +exit; diff --git a/pandora_console/include/class/TipsWindow.class.php b/pandora_console/include/class/TipsWindow.class.php new file mode 100644 index 0000000000..6f159c8ec7 --- /dev/null +++ b/pandora_console/include/class/TipsWindow.class.php @@ -0,0 +1,1057 @@ + $msg] + ); + } + + + /** + * Checks if target method is available to be called using AJAX. + * + * @param string $method Target method. + * + * @return boolean True allowed, false not. + */ + public function ajaxMethod($method) + { + // Check access. + check_login(); + + return in_array($method, $this->AJAXMethods); + } + + + /** + * Constructor. + * + * @param string $ajax_controller Controller. + * + * @return object + * @throws Exception On error. + */ + public function __construct( + $ajax_controller='include/ajax/tips_window.ajax' + ) { + $this->ajaxController = $ajax_controller; + + return $this; + } + + + /** + * Main method. + * + * @return void + */ + public function run() + { + global $config; + $_SESSION['showed_tips_window'] = true; + $userInfo = users_get_user_by_id($config['id_user']); + + if ((bool) $userInfo['show_tips_startup'] === false) { + return; + } + + ui_require_css_file('tips_window'); + ui_require_css_file('jquery.bxslider'); + ui_require_javascript_file('tipsWindow'); + ui_require_javascript_file('jquery.bxslider.min'); + echo '
'; + $totalTips = $this->getTotalTipsShowUser(); + if ($totalTips > 0) { + echo ''; + } + } + + + /** + * Render view modal with random tip + * + * @return void + */ + public function renderView() + { + $initialTip = $this->getRandomTip(true); + View::render( + 'dashboard/tipsWindow', + [ + 'title' => $initialTip['title'], + 'text' => $initialTip['text'], + 'url' => $initialTip['url'], + 'files' => $initialTip['files'], + 'id' => $initialTip['id'], + ] + ); + } + + + /** + * Render preview view modal with parameter + * + * @return void + */ + public function renderPreview() + { + $title = get_parameter('title', ''); + $text = get_parameter('text', ''); + $url = get_parameter('url', ''); + $files = get_parameter('files', ''); + $totalFiles64 = get_parameter('totalFiles64', ''); + $files64 = false; + + if ($totalFiles64 > 0) { + $files64 = []; + for ($i = 0; $i < $totalFiles64; $i++) { + $files64[] = get_parameter('file64_'.$i, ''); + } + } + + if (empty($files) === false) { + $files = explode(',', $files); + foreach ($files as $key => $value) { + $files[$key] = str_replace(ui_get_full_url('/'), '', $value); + } + } + + View::render( + 'dashboard/tipsWindow', + [ + 'title' => $title, + 'text' => $text, + 'url' => $url, + 'preview' => true, + 'files' => (empty($files) === false) ? $files : false, + 'files64' => (empty($files64) === false) ? $files64 : false, + ] + ); + } + + + /** + * Search a tip by id + * + * @param integer $idTip Id from tip. + * + * @return array $tip + */ + public function getTipById($idTip) + { + $tip = db_get_row( + 'twelcome_tip', + 'id', + $idTip, + ); + if ($tip !== false) { + $tip['title'] = io_safe_output($tip['title']); + $tip['text'] = io_safe_output($tip['text']); + $tip['url'] = io_safe_output($tip['url']); + } + + return $tip; + } + + + /** + * Return a tip or print it in json + * + * @param boolean $return Param for return or print json. + * + * @return array $tip + */ + public function getRandomTip($return=false) + { + global $config; + $exclude = get_parameter('exclude', ''); + $userInfo = users_get_user_by_id($config['id_user']); + $profilesUser = users_get_user_profile($config['id_user']); + $language = ($userInfo['language'] !== 'default') ? $userInfo['language'] : $config['language']; + + $idProfilesFilter = '0'; + foreach ($profilesUser as $key => $profile) { + $idProfilesFilter .= ','.$profile['id_perfil']; + } + + $sql = 'SELECT id, title, text, url + FROM twelcome_tip + WHERE enable = "1" '; + + if (empty($exclude) === false && $exclude !== null) { + $exclude = implode(',', json_decode($exclude, true)); + if ($exclude !== '') { + $sql .= sprintf(' AND id NOT IN (%s)', $exclude); + } + } + + $sql .= sprintf(' AND id_profile IN (%s)', $idProfilesFilter); + + $sql .= ' ORDER BY CASE WHEN id_lang = "'.$language.'" THEN id_lang END DESC, RAND()'; + + $tip = db_get_row_sql($sql); + + if (empty($tip) === false) { + $tip['files'] = $this->getFilesFromTip($tip['id']); + + $tip['title'] = io_safe_output($tip['title']); + $tip['text'] = io_safe_output($tip['text']); + $tip['url'] = io_safe_output($tip['url']); + } + + if ($return) { + if (empty($tip) === false) { + return $tip; + } else { + return false; + } + } else { + if (empty($tip) === false) { + echo json_encode(['success' => true, 'data' => $tip]); + return; + } else { + echo json_encode(['success' => false]); + return; + } + } + } + + + /** + * Get number of tips in database + * + * @return integer + */ + public function getTotalTips() + { + return db_get_sql('SELECT count(*) FROM twelcome_tip'); + } + + + /** + * Get totals tips that user can show + * + * @return array + */ + public function getTotalTipsShowUser() + { + global $config; + $profilesUser = users_get_user_profile($config['id_user']); + $idProfilesFilter = '0'; + foreach ($profilesUser as $key => $profile) { + $idProfilesFilter .= ','.$profile['id_perfil']; + } + + $sql = 'SELECT count(*) + FROM twelcome_tip + WHERE enable = "1" '; + + $sql .= sprintf(' AND id_profile IN (%s)', $idProfilesFilter); + + $sql .= ' ORDER BY CASE WHEN id_lang = "'.$config['language'].'" THEN id_lang END DESC, RAND()'; + + return db_get_sql($sql); + } + + + /** + * Return files from tip + * + * @param integer $idTip Id from tip. + * + * @return array + */ + public function getFilesFromTip($idTip) + { + if (empty($idTip) === true) { + return false; + } + + $sql = sprintf('SELECT id, filename, path FROM twelcome_tip_file WHERE twelcome_tip_file = %s', $idTip); + + return db_get_all_rows_sql($sql); + + } + + + /** + * Delete all images from tip in db and files + * + * @param integer $idTip Id from tip. + * @param array $imagesToRemove Array with id and images path. + * + * @return void + */ + public function deleteImagesFromTip($idTip, $imagesToRemove) + { + foreach ($imagesToRemove as $id => $image) { + unlink($image); + db_process_sql_delete( + 'twelcome_tip_file', + [ + 'id' => $id, + 'twelcome_tip_file' => $idTip, + ] + ); + } + } + + + /** + * Update token user for show tips at startup + * + * @return void + */ + public function setShowTipsAtStartup() + { + global $config; + $showTipsStartup = get_parameter('show_tips_startup', ''); + if ($showTipsStartup !== '' && $showTipsStartup !== null) { + $result = db_process_sql_update( + 'tusuario', + ['show_tips_startup' => $showTipsStartup], + ['id_user' => $config['id_user']] + ); + + if ($result !== false) { + echo json_encode(['success' => true]); + return; + } else { + echo json_encode(['success' => false]); + return; + } + } else { + echo json_encode(['success' => false]); + return; + } + + } + + + /** + * Draw table in list tips + * + * @param array $errors Array of errors if exists. + * + * @return void + */ + public function draw($errors=null) + { + ui_require_css_file('tips_window'); + + if ($errors !== null) { + if (count($errors) > 0) { + foreach ($errors as $key => $value) { + ui_print_error_message($value); + } + } else { + ui_print_success_message(__('Tip deleted')); + } + } + + try { + $columns = [ + 'language', + 'title', + 'text', + 'enable', + 'actions', + ]; + + $columnNames = [ + __('Language'), + __('Title'), + __('Text'), + __('Enable'), + __('Actions'), + ]; + + // Load datatables user interface. + ui_print_datatable( + [ + 'id' => 'list_tips_windows', + 'class' => 'info_table', + 'style' => 'width: 100%', + 'columns' => $columns, + 'column_names' => $columnNames, + 'ajax_url' => $this->ajaxController, + 'ajax_data' => ['method' => 'getTips'], + 'no_sortable_columns' => [-1], + 'order' => [ + 'field' => 'title', + 'direction' => 'asc', + ], + 'search_button_class' => 'sub filter float-right', + 'form' => [ + 'inputs' => [ + [ + 'label' => __('Search by title'), + 'type' => 'text', + 'name' => 'filter_title', + 'size' => 12, + ], + ], + ], + ] + ); + echo ' '; + } catch (Exception $e) { + echo $e->getMessage(); + } + } + + + /** + * Delete tip and his files. + * + * @param integer $idTip Id from tip. + * + * @return integer Status from sql query. + */ + public function deleteTip($idTip) + { + $files = $this->getFilesFromTip($idTip); + if ($files !== false) { + if (count($files) > 0) { + foreach ($files as $key => $file) { + unlink($file['path'].'/'.$file['filename']); + } + } + } + + return db_process_sql_delete( + 'twelcome_tip', + ['id' => $idTip] + ); + } + + + /** + * Return tips for datatable + * + * @return void + */ + public function getTips() + { + global $config; + + $data = []; + $start = get_parameter('start', 0); + $length = get_parameter('length', $config['block_size']); + $orderDatatable = get_datatable_order(true); + $filters = get_parameter('filter', []); + $pagination = ''; + $filter = ''; + $order = ''; + + try { + ob_start(); + + if (key_exists('filter_title', $filters) === true) { + if (empty($filters['filter_title']) === false) { + $filter = ' WHERE title like "%'.$filters['filter_title'].'%"'; + } + } + + if (isset($orderDatatable)) { + $order = sprintf( + ' ORDER BY %s %s', + $orderDatatable['field'], + $orderDatatable['direction'] + ); + } + + if (isset($length) && $length > 0 + && isset($start) && $start >= 0 + ) { + $pagination = sprintf( + ' LIMIT %d OFFSET %d ', + $length, + $start + ); + } + + $sql = sprintf( + 'SELECT id, name AS language, title, text, url, enable + FROM twelcome_tip t + LEFT JOIN tlanguage l ON t.id_lang = l.id_language + %s %s %s', + $filter, + $order, + $pagination + ); + + $data = db_get_all_rows_sql($sql); + + foreach ($data as $key => $row) { + if ($row['enable'] === '1') { + $data[$key]['enable'] = ''; + } else { + $data[$key]['enable'] = ''; + } + + $data[$key]['title'] = io_safe_output($row['title']); + $data[$key]['text'] = io_safe_output($row['text']); + $data[$key]['url'] = io_safe_output($row['url']); + $data[$key]['actions'] = ' '; + } + + if (empty($data) === true) { + $total = 0; + $data = []; + } else { + $total = $this->getTotalTips(); + } + + echo json_encode( + [ + 'data' => $data, + 'recordsTotal' => $total, + 'recordsFiltered' => $total, + ] + ); + // Capture output. + $response = ob_get_clean(); + } catch (Exception $e) { + echo json_encode(['error' => $e->getMessage()]); + exit; + } + + json_decode($response); + if (json_last_error() === JSON_ERROR_NONE) { + echo $response; + } else { + echo json_encode( + [ + 'success' => false, + 'error' => $response, + ] + ); + } + + exit; + } + + + /** + * Render view create tips + * + * @param array $errors Array of errors if exists. + * + * @return void + */ + public function viewCreate($errors=null) + { + ui_require_css_file('tips_window'); + ui_require_css_file('jquery.bxslider'); + ui_require_javascript_file('tipsWindow'); + ui_require_javascript_file('jquery.bxslider.min'); + + if ($errors !== null) { + if (count($errors) > 0) { + foreach ($errors as $key => $value) { + ui_print_error_message($value); + } + } else { + ui_print_success_message(__('Tip created')); + } + } + + $profiles = profile_get_profiles(); + + echo ''; + $table = new stdClass(); + $table->width = '100%'; + $table->class = 'databox filters'; + + $table->style[0] = 'font-weight: bold'; + + $table->data = []; + $table->data[0][0] = __('Images'); + $table->data[0][1] .= html_print_div(['id' => 'inputs_images'], true); + $table->data[0][1] .= html_print_div( + [ + 'id' => 'notices_images', + 'class' => 'invisible', + 'content' => ''.__('Wrong size, we recommend images of 464x260 px').'
', + ], + true + ); + $table->data[0][1] .= html_print_button(__('Add image'), 'button_add_image', false, '', '', true); + $table->data[1][0] = __('Language'); + $table->data[1][1] = html_print_select_from_sql( + 'SELECT id_language, name FROM tlanguage', + 'id_lang', + '', + '', + '', + '0', + true + ); + $table->data[2][0] = __('Profile'); + $table->data[2][1] = html_print_select($profiles, 'id_profile', '0', '', __('All'), 0, true); + $table->data[3][0] = __('Title'); + $table->data[3][1] = html_print_input_text('title', '', '', 35, 100, true); + $table->data[4][0] = __('Text'); + $table->data[4][1] = html_print_textarea('text', 5, 50, '', '', true); + $table->data[5][0] = __('Url'); + $table->data[5][1] = html_print_input_text('url', '', '', 35, 100, true); + $table->data[6][0] = __('Enable'); + $table->data[6][1] = html_print_checkbox_switch('enable', true, true, true); + + echo ''; + html_print_div(['id' => 'tips_window_modal_preview']); + } + + + /** + * Render view edit tips + * + * @param integer $idTip Id from tips. + * @param array $errors Array of errors if exists. + * + * @return void + */ + public function viewEdit($idTip, $errors=null) + { + $tip = $this->getTipById($idTip); + if ($tip === false) { + return; + } + + $files = $this->getFilesFromTip($idTip); + + ui_require_css_file('tips_window'); + ui_require_css_file('jquery.bxslider'); + ui_require_javascript_file('tipsWindow'); + ui_require_javascript_file('jquery.bxslider.min'); + + if ($errors !== null) { + if (count($errors) > 0) { + foreach ($errors as $key => $value) { + ui_print_error_message($value); + } + } else { + ui_print_success_message(__('Tip edited')); + } + } + + $outputImagesTip = ''; + if (empty($files) === false) { + foreach ($files as $key => $value) { + $namePath = $value['path'].$value['filename']; + $imageTip = html_print_image($namePath, true); + $imageTip .= html_print_input_image( + 'delete_image_tip', + 'images/delete.svg', + '', + '', + true, + [ + 'onclick' => 'deleteImage(this, \''.$value['id'].'\', \''.$namePath.'\')', + 'class' => 'remove-image', + ] + ); + $outputImagesTip .= html_print_div( + [ + 'class' => 'image_tip', + 'content' => $imageTip, + ], + true + ); + } + } + + $profiles = profile_get_profiles(); + + echo ''; + $table = new stdClass(); + $table->width = '100%'; + $table->class = 'databox filters'; + + $table->style[0] = 'font-weight: bold'; + + $table->data = []; + $table->data[0][0] = __('Images'); + $table->data[0][1] .= $outputImagesTip; + $table->data[0][1] .= html_print_div(['id' => 'inputs_images'], true); + $table->data[0][1] .= html_print_input_hidden('images_to_delete', '{}', true); + $table->data[0][1] .= html_print_div( + [ + 'id' => 'notices_images', + 'class' => 'invisible', + 'content' => ''.__('Wrong size, we recommend images of 464x260 px').'
', + ], + true + ); + $table->data[0][1] .= html_print_button(__('Add image'), 'button_add_image', false, '', '', true); + $table->data[1][0] = __('Language'); + $table->data[1][1] = html_print_select_from_sql( + 'SELECT id_language, name FROM tlanguage', + 'id_lang', + $tip['id_lang'], + '', + '', + '0', + true + ); + $table->data[2][0] = __('Profile'); + $table->data[2][1] = html_print_select($profiles, 'id_profile', $tip['id_profile'], '', __('All'), 0, true); + $table->data[3][0] = __('Title'); + $table->data[3][1] = html_print_input_text('title', $tip['title'], '', 35, 100, true); + $table->data[4][0] = __('Text'); + $table->data[4][1] = html_print_textarea('text', 5, 50, $tip['text'], '', true); + $table->data[5][0] = __('Url'); + $table->data[5][1] = html_print_input_text('url', $tip['url'], '', 35, 100, true); + $table->data[6][0] = __('Enable'); + $table->data[6][1] = html_print_checkbox_switch('enable', 1, ($tip['enable'] === '1') ? true : false, true); + + echo ''; + html_print_div(['id' => 'tips_window_modal_preview']); + } + + + /** + * Udpdate tip + * + * @param integer $id Id from tip. + * @param integer $id_profile Id profile. + * @param string $id_lang Id langugage. + * @param string $title Title from tip. + * @param string $text Text from tip. + * @param string $url Url from tip. + * @param boolean $enable Indicates if the tip is activated. + * @param array $images Images from tip. + * + * @return boolean + */ + public function updateTip($id, $id_profile, $id_lang, $title, $text, $url, $enable, $images=null) + { + db_process_sql_begin(); + + $idTip = db_process_sql_update( + 'twelcome_tip', + [ + 'id_lang' => $id_lang, + 'id_profile' => (empty($id_profile) === false) ? $id_profile : 0, + 'title' => $title, + 'text' => $text, + 'url' => $url, + 'enable' => $enable, + ], + ['id' => $id] + ); + if ($idTip === false) { + db_process_sql_rollback(); + return false; + } + + if ($images !== null) { + foreach ($images as $key => $image) { + $res = db_process_sql_insert( + 'twelcome_tip_file', + [ + 'twelcome_tip_file' => $id, + 'filename' => $image, + 'path' => 'images/tips/', + ] + ); + if ($res === false) { + db_process_sql_rollback(); + return false; + } + } + } + + db_process_sql_commit(); + + return true; + } + + + /** + * Create tip + * + * @param string $id_lang Id langugage. + * @param integer $id_profile Id profile. + * @param string $title Title from tip. + * @param string $text Text from tip. + * @param string $url Url from tip. + * @param boolean $enable Indicates if the tip is activated. + * @param array $images Images from tip. + * + * @return boolean + */ + public function createTip($id_lang, $id_profile, $title, $text, $url, $enable, $images=null) + { + db_process_sql_begin(); + $idTip = db_process_sql_insert( + 'twelcome_tip', + [ + 'id_lang' => $id_lang, + 'id_profile' => (empty($id_profile) === false) ? $id_profile : 0, + 'title' => $title, + 'text' => $text, + 'url' => $url, + 'enable' => $enable, + ] + ); + if ($idTip === false) { + db_process_sql_rollback(); + return false; + } + + if ($images !== null) { + foreach ($images as $key => $image) { + $res = db_process_sql_insert( + 'twelcome_tip_file', + [ + 'twelcome_tip_file' => $idTip, + 'filename' => $image, + 'path' => 'images/tips/', + ] + ); + if ($res === false) { + db_process_sql_rollback(); + return false; + } + } + } + + db_process_sql_commit(); + + return true; + } + + + /** + * Validate images uploads for the user + * + * @param array $files List images for validate. + * + * @return boolean Return boolean or array errors. + */ + public function validateImages($files) + { + $formats = [ + 'image/png', + 'image/jpg', + 'image/jpeg', + 'image/gif', + ]; + $errors = []; + $maxsize = 6097152; + + foreach ($files as $key => $file) { + if ($file['error'] !== 0) { + $errors[] = __('Incorrect file'); + } + + if (in_array($file['type'], $formats) === false) { + $errors[] = __('Format image invalid'); + } + + if ($file['size'] > $maxsize) { + $errors[] = __('Image size too large'); + } + } + + if (count($errors) > 0) { + return $errors; + } else { + return false; + } + } + + + /** + * Upload images passed by user + * + * @param array $files List of files for upload. + * + * @return array List of names files. + */ + public function uploadImages($files) + { + $dir = 'images/tips/'; + $imagesOk = []; + foreach ($files as $key => $file) { + $name = str_replace(' ', '_', $file['name']); + $name = str_replace('.', uniqid().'.', $name); + move_uploaded_file($file['tmp_name'], $dir.'/'.$name); + $imagesOk[] = $name; + } + + return $imagesOk; + } + + +} diff --git a/pandora_console/include/javascript/jquery.bxslider.min.js b/pandora_console/include/javascript/jquery.bxslider.min.js new file mode 100644 index 0000000000..16e64c0dd5 --- /dev/null +++ b/pandora_console/include/javascript/jquery.bxslider.min.js @@ -0,0 +1,7 @@ +/** + * bxSlider v4.2.12 + * Copyright 2013-2015 Steven Wanderski + * Written while drinking Belgian ales and listening to jazz + * Licensed under MIT (http://opensource.org/licenses/MIT) + */ +!function(t){var e={mode:"horizontal",slideSelector:"",infiniteLoop:!0,hideControlOnEnd:!1,speed:500,easing:null,slideMargin:0,startSlide:0,randomStart:!1,captions:!1,ticker:!1,tickerHover:!1,adaptiveHeight:!1,adaptiveHeightSpeed:500,video:!1,useCSS:!0,preloadImages:"visible",responsive:!0,slideZIndex:50,wrapperClass:"bx-wrapper",touchEnabled:!0,swipeThreshold:50,oneToOneTouch:!0,preventDefaultSwipeX:!0,preventDefaultSwipeY:!1,ariaLive:!0,ariaHidden:!0,keyboardEnabled:!1,pager:!0,pagerType:"full",pagerShortSeparator:" / ",pagerSelector:null,buildPager:null,pagerCustom:null,controls:!0,nextText:"Next",prevText:"Prev",nextSelector:null,prevSelector:null,autoControls:!1,startText:"Start",stopText:"Stop",autoControlsCombine:!1,autoControlsSelector:null,auto:!1,pause:4e3,autoStart:!0,autoDirection:"next",stopAutoOnClick:!1,autoHover:!1,autoDelay:0,autoSlideForOnePage:!1,minSlides:1,maxSlides:1,moveSlides:0,slideWidth:0,shrinkItems:!1,onSliderLoad:function(){return!0},onSlideBefore:function(){return!0},onSlideAfter:function(){return!0},onSlideNext:function(){return!0},onSlidePrev:function(){return!0},onSliderResize:function(){return!0}};t.fn.bxSlider=function(n){if(0===this.length)return this;if(this.length>1)return this.each(function(){t(this).bxSlider(n)}),this;var s={},o=this,r=t(window).width(),a=t(window).height();if(!t(o).data("bxSlider")){var l=function(){t(o).data("bxSlider")||(s.settings=t.extend({},e,n),s.settings.slideWidth=parseInt(s.settings.slideWidth),s.children=o.children(s.settings.slideSelector),s.children.length'.__('Hello! These are the tips of the day.').'
'; +$output .= ''.html_print_checkbox( + 'show_tips_startup', + true, + true, + true, + false, + 'show_tips_startup(this)', + false, + '', + ($preview === true) ? '' : 'checkbox_tips_startup' +).__('Show usage tips at startup').'
'; +$output .= ''; +$output .= $text; +$output .= '
'; + +if (empty($url) === false && $url !== '') { + $output .= ''.__('See more info').'→'; +} + +$output .= '